Skip to content

Commit a22f397

Browse files
authored
fix: model hydration is broken when an array is empty (#10)
* docs: add jsdoc information for Persist.Type * fix: allow for empty arrays on models when hydrating
1 parent 3096e64 commit a22f397

8 files changed

+131
-11
lines changed

src/SchemaCompiler.test.js

+50
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const schema = {
2020
requiredBoolean: Type.Boolean.required,
2121
date: Type.Date,
2222
requiredDate: Type.Date.required,
23+
emptyArrayOfStrings: Type.Array.of(Type.String),
24+
emptyArrayOfNumbers: Type.Array.of(Type.Number),
25+
emptyArrayOfBooleans: Type.Array.of(Type.Boolean),
26+
emptyArrayOfDates: Type.Array.of(Type.Date),
2327
arrayOfString: Type.Array.of(Type.String),
2428
arrayOfNumber: Type.Array.of(Type.Number),
2529
arrayOfBoolean: Type.Array.of(Type.Boolean),
@@ -78,6 +82,30 @@ const invalidDataErrors = [{
7882
message: 'must match format "iso-date-time"',
7983
params: {format: 'iso-date-time'},
8084
schemaPath: '#/properties/date/format',
85+
}, {
86+
instancePath: '/emptyArrayOfStrings',
87+
keyword: 'type',
88+
message: 'must be array',
89+
params: {type: 'array'},
90+
schemaPath: '#/properties/emptyArrayOfStrings/type',
91+
}, {
92+
instancePath: '/emptyArrayOfNumbers',
93+
keyword: 'type',
94+
message: 'must be array',
95+
params: {type: 'array'},
96+
schemaPath: '#/properties/emptyArrayOfNumbers/type',
97+
}, {
98+
instancePath: '/emptyArrayOfBooleans',
99+
keyword: 'type',
100+
message: 'must be array',
101+
params: {type: 'array'},
102+
schemaPath: '#/properties/emptyArrayOfBooleans/type',
103+
}, {
104+
instancePath: '/emptyArrayOfDates',
105+
keyword: 'type',
106+
message: 'must be array',
107+
params: {type: 'array'},
108+
schemaPath: '#/properties/emptyArrayOfDates/type',
81109
}, {
82110
instancePath: '/arrayOfString/0',
83111
keyword: 'type',
@@ -163,6 +191,10 @@ test('.compile(schema) has the given schema associated with it', t => {
163191
requiredBoolean: {type: 'boolean'},
164192
date: {type: 'string', format: 'iso-date-time'},
165193
requiredDate: {type: 'string', format: 'iso-date-time'},
194+
emptyArrayOfStrings: {type: 'array', items: {type: 'string'}},
195+
emptyArrayOfNumbers: {type: 'array', items: {type: 'number'}},
196+
emptyArrayOfBooleans: {type: 'array', items: {type: 'boolean'}},
197+
emptyArrayOfDates: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
166198
arrayOfString: {type: 'array', items: {type: 'string'}},
167199
arrayOfNumber: {type: 'array', items: {type: 'number'}},
168200
arrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
@@ -236,6 +268,24 @@ test('.compile(MainModel) has the given schema associated with it', t => {
236268
requiredArrayOfNumber: {type: 'array', items: {type: 'number'}},
237269
requiredArrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
238270
requiredArrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
271+
emptyArrayOfStrings: {type: 'array', items: {type: 'string'}},
272+
emptyArrayOfNumbers: {type: 'array', items: {type: 'number'}},
273+
emptyArrayOfBooleans: {type: 'array', items: {type: 'boolean'}},
274+
emptyArrayOfDates: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
275+
emptyArrayOfModels: {
276+
type: 'array',
277+
items: {
278+
type: 'object',
279+
additionalProperties: false,
280+
required: ['id'],
281+
properties: {
282+
id: {
283+
type: 'string',
284+
pattern: '^LinkedManyModel/[A-Z0-9]+$',
285+
},
286+
},
287+
},
288+
},
239289
requiredLinked: {
240290
type: 'object',
241291
additionalProperties: false,

src/engine/FileEngine.test.js

+8
Original file line numberDiff line numberDiff line change
@@ -532,5 +532,13 @@ test('FileEngine.hydrate(model)', async t => {
532532
filesystem,
533533
}).hydrate(dryModel);
534534

535+
assertions.calledWith(t, filesystem.readFile, '/tmp/fileEngine/MainModel/000000000000.json');
536+
assertions.calledWith(t, filesystem.readFile, '/tmp/fileEngine/CircularModel/000000000000.json');
537+
assertions.calledWith(t, filesystem.readFile, '/tmp/fileEngine/LinkedModel/000000000000.json');
538+
assertions.calledWith(t, filesystem.readFile, '/tmp/fileEngine/LinkedModel/111111111111.json');
539+
assertions.calledWith(t, filesystem.readFile, '/tmp/fileEngine/LinkedManyModel/000000000000.json');
540+
assertions.calledWith(t, filesystem.readFile, '/tmp/fileEngine/CircularManyModel/000000000000.json');
541+
542+
t.is(filesystem.readFile.getCalls().length, 6);
535543
t.deepEqual(hydratedModel, model);
536544
});

src/engine/HTTPEngine.test.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -885,13 +885,7 @@ test('HTTPEngine.hydrate(model)', async t => {
885885
const dryModel = new MainModel();
886886
dryModel.id = 'MainModel/000000000000';
887887

888-
const fetch = stubFetch({}, [
889-
getTestModelInstance(valid),
890-
getTestModelInstance({
891-
id: 'MainModel/111111111111',
892-
string: 'another string',
893-
}),
894-
]);
888+
const fetch = stubFetch({}, [getTestModelInstance(valid)]);
895889

896890
const hydratedModel = await HTTPEngine.configure({
897891
host: 'https://example.com',
@@ -906,5 +900,7 @@ test('HTTPEngine.hydrate(model)', async t => {
906900
assertions.calledWith(t, fetch, new URL('https://example.com/test/LinkedManyModel/000000000000.json'), {headers: {Accept: 'application/json'}});
907901
assertions.calledWith(t, fetch, new URL('https://example.com/test/CircularManyModel/000000000000.json'), {headers: {Accept: 'application/json'}});
908902

903+
t.is(fetch.getCalls().length, 6);
904+
909905
t.deepEqual(hydratedModel, model);
910906
});

src/engine/S3Engine.test.js

+26
Original file line numberDiff line numberDiff line change
@@ -936,5 +936,31 @@ test('S3Engine.hydrate(model)', async t => {
936936
client,
937937
}).hydrate(dryModel);
938938

939+
assertions.calledWith(t, client.send, new GetObjectCommand({
940+
Bucket: 'test-bucket',
941+
Key: 'test/MainModel/000000000000.json',
942+
}));
943+
assertions.calledWith(t, client.send, new GetObjectCommand({
944+
Bucket: 'test-bucket',
945+
Key: 'test/CircularModel/000000000000.json',
946+
}));
947+
assertions.calledWith(t, client.send, new GetObjectCommand({
948+
Bucket: 'test-bucket',
949+
Key: 'test/LinkedModel/000000000000.json',
950+
}));
951+
assertions.calledWith(t, client.send, new GetObjectCommand({
952+
Bucket: 'test-bucket',
953+
Key: 'test/LinkedModel/111111111111.json',
954+
}));
955+
assertions.calledWith(t, client.send, new GetObjectCommand({
956+
Bucket: 'test-bucket',
957+
Key: 'test/LinkedManyModel/000000000000.json',
958+
}));
959+
assertions.calledWith(t, client.send, new GetObjectCommand({
960+
Bucket: 'test-bucket',
961+
Key: 'test/CircularManyModel/000000000000.json',
962+
}));
963+
964+
t.is(client.send.getCalls().length, 6);
939965
t.deepEqual(hydratedModel, model);
940966
});

src/type/Model.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,13 @@ export default class Model {
118118
}
119119

120120
static isDryModel(possibleDryModel) {
121-
return (
122-
Object.keys(possibleDryModel).includes('id') &&
123-
!!possibleDryModel.id.match(/[A-Za-z]+\/[A-Z0-9]+/)
124-
);
121+
try {
122+
return (
123+
Object.keys(possibleDryModel).includes('id') &&
124+
!!possibleDryModel.id.match(/[A-Za-z]+\/[A-Z0-9]+/)
125+
);
126+
} catch (_) {
127+
return false;
128+
}
125129
}
126130
}

src/type/Model.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ test('model.toData() returns an object representation of the model', t => {
3333
requiredLinked: {id: 'LinkedModel/111111111111'},
3434
circular: {id: 'CircularModel/000000000000'},
3535
linkedMany: [{id: 'LinkedManyModel/000000000000'}],
36+
emptyArrayOfModels: [],
3637
circularMany: [{id: 'CircularManyModel/000000000000'}],
3738
});
3839
});

src/type/index.js

+16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ import NumberType from './simple/NumberType.js';
77
import SlugType from './resolved/SlugType.js';
88
import StringType from './simple/StringType.js';
99

10+
/**
11+
* @class Type
12+
* @property {StringType} String
13+
* @property {NumberType} Number
14+
* @property {BooleanType} Boolean
15+
* @property {DateType} Date
16+
* @property {ArrayType} Array
17+
* @property {CustomType} Custom
18+
* @property {ResolvedType} Resolved
19+
* @property {Model} Model
20+
*/
1021
const Type = {};
1122

1223
Type.String = StringType;
@@ -15,6 +26,11 @@ Type.Boolean = BooleanType;
1526
Type.Date = DateType;
1627
Type.Array = ArrayType;
1728
Type.Custom = CustomType;
29+
30+
/**
31+
* @class ResolvedType
32+
* @property {SlugType} Slug
33+
*/
1834
Type.Resolved = {Slug: SlugType};
1935
Type.Model = Model;
2036

test/fixtures/TestModel.js

+19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const valid = {
1010
requiredBoolean: true,
1111
date: new Date().toISOString(),
1212
requiredDate: new Date().toISOString(),
13+
emptyArrayOfStrings: [],
14+
emptyArrayOfNumbers: [],
15+
emptyArrayOfBooleans: [],
16+
emptyArrayOfDates: [],
1317
arrayOfString: ['String'],
1418
arrayOfNumber: [24.5],
1519
arrayOfBoolean: [false],
@@ -29,6 +33,10 @@ export const invalid = {
2933
requiredBoolean: undefined,
3034
date: 'not-a-date',
3135
requiredDate: undefined,
36+
emptyArrayOfStrings: 'not-a-list',
37+
emptyArrayOfNumbers: 'not-a-list',
38+
emptyArrayOfBooleans: 'not-a-list',
39+
emptyArrayOfDates: 'not-a-list',
3240
arrayOfString: [true],
3341
arrayOfNumber: ['string'],
3442
arrayOfBoolean: [15.8],
@@ -97,6 +105,11 @@ export class MainModel extends Type.Model {
97105
static requiredBoolean = Type.Boolean.required;
98106
static date = Type.Date;
99107
static requiredDate = Type.Date.required;
108+
static emptyArrayOfStrings = Type.Array.of(Type.String);
109+
static emptyArrayOfNumbers = Type.Array.of(Type.Number);
110+
static emptyArrayOfBooleans = Type.Array.of(Type.Boolean);
111+
static emptyArrayOfDates = Type.Array.of(Type.Date);
112+
static emptyArrayOfModels = () => Type.Array.of(LinkedManyModel);
100113
static arrayOfString = Type.Array.of(Type.String);
101114
static arrayOfNumber = Type.Array.of(Type.Number);
102115
static arrayOfBoolean = Type.Array.of(Type.Boolean);
@@ -145,6 +158,12 @@ export function getTestModelInstance(data = {}) {
145158
if (data.arrayOfDate) model.arrayOfDate = data.arrayOfDate.map(d => DateType.isDate(d) ? new Date(d) : d);
146159
if (data.requiredArrayOfDate) model.requiredArrayOfDate = data.requiredArrayOfDate.map(d => DateType.isDate(d) ? new Date(d) : d);
147160

161+
if (!model.emptyArrayOfStrings) model.emptyArrayOfStrings = [];
162+
if (!model.emptyArrayOfNumbers) model.emptyArrayOfNumbers = [];
163+
if (!model.emptyArrayOfBooleans) model.emptyArrayOfBooleans = [];
164+
if (!model.emptyArrayOfDates) model.emptyArrayOfDates = [];
165+
if (!model.emptyArrayOfModels) model.emptyArrayOfModels = [];
166+
148167
const circular = new CircularModel({linked: model});
149168
circular.id = circular.id.replace(/[a-zA-Z0-9]+$/, '000000000000');
150169
model.circular = circular;

0 commit comments

Comments
 (0)