Skip to content

Commit 3096e64

Browse files
authored
feat: date type (#9)
* feat: add date type * feat: add arrays of dates * feat: dates are deserialised as Date objects
1 parent 6142fcc commit 3096e64

14 files changed

+185
-6
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class SimpleModel extends Persist.Type.Model {
1717
static boolean = Persist.Type.Boolean;
1818
static string = Persist.Type.String;
1919
static number = Persist.Type.Number;
20+
static date = Persist.Type.Date;
2021
}
2122
```
2223

@@ -29,6 +30,7 @@ export class SimpleModel extends Persist.Type.Model {
2930
static requiredBoolean = Persist.Type.Boolean.required;
3031
static requiredString = Persist.Type.String.required;
3132
static requiredNumber = Persist.Type.Number.required;
33+
static requiredDate = Persist.Type.Date.required;
3234
}
3335
```
3436

@@ -41,6 +43,11 @@ export class SimpleModel extends Persist.Type.Model {
4143
static arrayOfBooleans = Persist.Type.Array.of(Type.Boolean);
4244
static arrayOfStrings = Persist.Type.Array.of(Type.String);
4345
static arrayOfNumbers = Persist.Type.Array.of(Type.Number);
46+
static arrayOfDates = Persist.Type.Array.of(Type.Date);
47+
static requiredArrayOfBooleans = Persist.Type.Array.of(Type.Boolean).required;
48+
static requiredArrayOfStrings = Persist.Type.Array.of(Type.String).required;
49+
static requiredArrayOfNumbers = Persist.Type.Array.of(Type.Number).required;
50+
static requiredArrayOfDates = Persist.Type.Array.of(Type.Date).required;
4451
}
4552
```
4653

package-lock.json

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"dependencies": {
2626
"ajv": "^8.16.0",
2727
"ajv-errors": "^3.0.0",
28+
"ajv-formats": "^3.0.1",
2829
"lunr": "^2.3.9",
2930
"slugify": "^1.6.6",
3031
"ulid": "^2.3.0"

src/SchemaCompiler.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Type from './type/index.js';
22
import ajv from 'ajv';
33
import ajvErrors from 'ajv-errors';
4+
import ajvFormats from 'ajv-formats';
45

56
/**
67
* @class SchemaCompiler
@@ -15,6 +16,7 @@ export default class SchemaCompiler {
1516
const validation = new ajv({allErrors: true});
1617

1718
ajvErrors(validation);
19+
ajvFormats(validation);
1820

1921
const schema = {
2022
type: 'object',
@@ -58,9 +60,17 @@ export default class SchemaCompiler {
5860

5961
schema.properties[name] = {type: property?._type};
6062

63+
if (property?._format) {
64+
schema.properties[name].format = property._format;
65+
}
66+
6167
if (property?._type === 'array') {
6268
schema.properties[name].items = {type: property?._items._type};
6369

70+
if (property?._items?._format) {
71+
schema.properties[name].items.format = property?._items._format;
72+
}
73+
6474
if (Type.Model.isModel(property?._items)) {
6575
schema.properties[name].items = {
6676
type: 'object',
@@ -102,11 +112,14 @@ export class CompiledSchema {
102112
* @throws {ValidationError}
103113
*/
104114
static validate(data) {
105-
let inputData = data;
115+
let inputData = Object.assign({}, data);
116+
106117
if (Type.Model.isModel(data)) {
107118
inputData = data.toData();
108119
}
120+
109121
const valid = this._validator?.(inputData);
122+
110123
if (valid) return valid;
111124

112125
throw new ValidationError(inputData, this._validator.errors);

src/SchemaCompiler.test.js

+41-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ const schema = {
1818
requiredNumber: Type.Number.required,
1919
boolean: Type.Boolean,
2020
requiredBoolean: Type.Boolean.required,
21+
date: Type.Date,
22+
requiredDate: Type.Date.required,
2123
arrayOfString: Type.Array.of(Type.String),
2224
arrayOfNumber: Type.Array.of(Type.Number),
2325
arrayOfBoolean: Type.Array.of(Type.Boolean),
26+
arrayOfDate: Type.Array.of(Type.Date),
2427
requiredArrayOfString: Type.Array.of(Type.String).required,
2528
requiredArrayOfNumber: Type.Array.of(Type.Number).required,
2629
requiredArrayOfBoolean: Type.Array.of(Type.Boolean).required,
30+
requiredArrayOfDate: Type.Array.of(Type.Date).required,
2731
};
2832

2933
const invalidDataErrors = [{
@@ -44,6 +48,12 @@ const invalidDataErrors = [{
4448
message: 'must have required property \'requiredBoolean\'',
4549
params: {missingProperty: 'requiredBoolean'},
4650
schemaPath: '#/required',
51+
}, {
52+
instancePath: '',
53+
keyword: 'required',
54+
message: 'must have required property \'requiredDate\'',
55+
params: {missingProperty: 'requiredDate'},
56+
schemaPath: '#/required',
4757
}, {
4858
instancePath: '/string',
4959
keyword: 'type',
@@ -62,6 +72,12 @@ const invalidDataErrors = [{
6272
message: 'must be boolean',
6373
params: {type: 'boolean'},
6474
schemaPath: '#/properties/boolean/type',
75+
}, {
76+
instancePath: '/date',
77+
keyword: 'format',
78+
message: 'must match format "iso-date-time"',
79+
params: {format: 'iso-date-time'},
80+
schemaPath: '#/properties/date/format',
6581
}, {
6682
instancePath: '/arrayOfString/0',
6783
keyword: 'type',
@@ -80,6 +96,12 @@ const invalidDataErrors = [{
8096
message: 'must be boolean',
8197
params: {type: 'boolean'},
8298
schemaPath: '#/properties/arrayOfBoolean/items/type',
99+
}, {
100+
instancePath: '/arrayOfDate/0',
101+
keyword: 'format',
102+
message: 'must match format "iso-date-time"',
103+
params: {format: 'iso-date-time'},
104+
schemaPath: '#/properties/arrayOfDate/items/format',
83105
}, {
84106
instancePath: '/requiredArrayOfString/0',
85107
keyword: 'type',
@@ -98,6 +120,12 @@ const invalidDataErrors = [{
98120
message: 'must be boolean',
99121
params: {type: 'boolean'},
100122
schemaPath: '#/properties/requiredArrayOfBoolean/items/type',
123+
}, {
124+
instancePath: '/requiredArrayOfDate/0',
125+
keyword: 'format',
126+
message: 'must match format "iso-date-time"',
127+
params: {format: 'iso-date-time'},
128+
schemaPath: '#/properties/requiredArrayOfDate/items/format',
101129
}];
102130

103131
test('.compile(schema) is an instance of CompiledSchema', t => {
@@ -112,9 +140,11 @@ test('.compile(schema) has the given schema associated with it', t => {
112140
'requiredString',
113141
'requiredNumber',
114142
'requiredBoolean',
143+
'requiredDate',
115144
'requiredArrayOfString',
116145
'requiredArrayOfNumber',
117146
'requiredArrayOfBoolean',
147+
'requiredArrayOfDate',
118148
],
119149
properties: {
120150
custom: {
@@ -131,12 +161,16 @@ test('.compile(schema) has the given schema associated with it', t => {
131161
requiredNumber: {type: 'number'},
132162
boolean: {type: 'boolean'},
133163
requiredBoolean: {type: 'boolean'},
164+
date: {type: 'string', format: 'iso-date-time'},
165+
requiredDate: {type: 'string', format: 'iso-date-time'},
134166
arrayOfString: {type: 'array', items: {type: 'string'}},
135167
arrayOfNumber: {type: 'array', items: {type: 'number'}},
136168
arrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
169+
arrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
137170
requiredArrayOfString: {type: 'array', items: {type: 'string'}},
138171
requiredArrayOfNumber: {type: 'array', items: {type: 'number'}},
139172
requiredArrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
173+
requiredArrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
140174
},
141175
});
142176
});
@@ -156,7 +190,7 @@ test('.compile(schema).validate(invalid) throws a ValidationError', t => {
156190
);
157191

158192
t.is(error.message, 'Validation failed');
159-
t.is(error.data, invalid);
193+
t.deepEqual(error.data, invalid);
160194
t.deepEqual(error.errors, invalidDataErrors);
161195
});
162196

@@ -169,9 +203,11 @@ test('.compile(MainModel) has the given schema associated with it', t => {
169203
'requiredString',
170204
'requiredNumber',
171205
'requiredBoolean',
206+
'requiredDate',
172207
'requiredArrayOfString',
173208
'requiredArrayOfNumber',
174209
'requiredArrayOfBoolean',
210+
'requiredArrayOfDate',
175211
'requiredLinked',
176212
],
177213
properties: {
@@ -190,12 +226,16 @@ test('.compile(MainModel) has the given schema associated with it', t => {
190226
requiredNumber: {type: 'number'},
191227
boolean: {type: 'boolean'},
192228
requiredBoolean: {type: 'boolean'},
229+
date: {type: 'string', format: 'iso-date-time'},
230+
requiredDate: {type: 'string', format: 'iso-date-time'},
193231
arrayOfString: {type: 'array', items: {type: 'string'}},
194232
arrayOfNumber: {type: 'array', items: {type: 'number'}},
195233
arrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
234+
arrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
196235
requiredArrayOfString: {type: 'array', items: {type: 'string'}},
197236
requiredArrayOfNumber: {type: 'array', items: {type: 'number'}},
198237
requiredArrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
238+
requiredArrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
199239
requiredLinked: {
200240
type: 'object',
201241
additionalProperties: false,

src/engine/Engine.api.test.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,13 @@ for (const {engine, configuration, configurationIgnores} of engines) {
7575

7676
const got = await store.get(MainModel, 'MainModel/000000000000');
7777

78-
79-
t.like(got, getTestModelInstance(valid).toData());
78+
t.like(got, {
79+
...getTestModelInstance(valid).toData(),
80+
date: new Date(valid.date),
81+
requiredDate: new Date(valid.requiredDate),
82+
arrayOfDate: [new Date(valid.arrayOfDate[0])],
83+
requiredArrayOfDate: [new Date(valid.requiredArrayOfDate[0])],
84+
});
8085
});
8186

8287
test(`${engine.toString()}.get(MainModel, id) throws NotFoundEngineError when no model exists`, async t => {

src/engine/HTTPEngine.test.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,13 @@ test('HTTPEngine.search(MainModel, "Str") when a matching model exists', async t
830830
t.like(models, [{
831831
ref: 'MainModel/000000000000',
832832
score: 0.211,
833-
model: model0.toData(),
833+
model: {
834+
...model0.toData(),
835+
date: new Date(model0.date),
836+
requiredDate: new Date(model0.requiredDate),
837+
arrayOfDate: model0.arrayOfDate[0] ? [new Date(model0.arrayOfDate[0])] : [],
838+
requiredArrayOfDate: [new Date(model0.requiredArrayOfDate[0])],
839+
},
834840
}, {
835841
ref: 'MainModel/111111111111',
836842
score: 0.16,

src/type/Model.js

+11
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ export default class Model {
9393

9494
for (const [name, value] of Object.entries(data)) {
9595
if (this[name]?._resolved) continue;
96+
97+
if (this[name].name.endsWith('DateType')) {
98+
model[name] = new Date(value);
99+
continue;
100+
}
101+
102+
if (this[name].name.endsWith('ArrayOf(Date)Type')) {
103+
model[name] = data[name].map(d => new Date(d));
104+
continue;
105+
}
106+
96107
model[name] = value;
97108
}
98109

src/type/complex/ArrayType.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default class ArrayType {
77
static _items = type;
88

99
static toString() {
10-
return `ArrayOf(${type})`;
10+
return `ArrayOf(${type.toString()})`;
1111
}
1212

1313
static get required() {
@@ -25,6 +25,8 @@ export default class ArrayType {
2525
}
2626
}
2727

28+
Object.defineProperty(ArrayOf, 'name', {value: `${ArrayOf.toString()}Type`});
29+
2830
return ArrayOf;
2931
}
3032
}

src/type/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ArrayType from './complex/ArrayType.js';
22
import BooleanType from './simple/BooleanType.js';
33
import CustomType from './complex/CustomType.js';
4+
import DateType from './simple/DateType.js';
45
import Model from './Model.js';
56
import NumberType from './simple/NumberType.js';
67
import SlugType from './resolved/SlugType.js';
@@ -11,6 +12,7 @@ const Type = {};
1112
Type.String = StringType;
1213
Type.Number = NumberType;
1314
Type.Boolean = BooleanType;
15+
Type.Date = DateType;
1416
Type.Array = ArrayType;
1517
Type.Custom = CustomType;
1618
Type.Resolved = {Slug: SlugType};

src/type/simple/DateType.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import SimpleType from './SimpleType.js';
2+
3+
export default class DateType extends SimpleType {
4+
static _type = 'string';
5+
static _format = 'iso-date-time';
6+
7+
static isDate(possibleDate) {
8+
return possibleDate instanceof Date || !isNaN(new Date(possibleDate));
9+
}
10+
}

src/type/simple/DateType.test.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import DateType from './DateType.js';
2+
import test from 'ava';
3+
4+
test('DateType is Date', t => {
5+
t.is(DateType.toString(), 'Date');
6+
});
7+
8+
test('DateType is not required', t => {
9+
t.is(DateType._required, false);
10+
});
11+
12+
test('DateType does not have properties', t => {
13+
t.is(DateType._properties, undefined);
14+
});
15+
16+
test('DateType does not have items', t => {
17+
t.is(DateType._items, undefined);
18+
});
19+
20+
test('DateType is not a resolved type', t => {
21+
t.is(DateType._resolved, false);
22+
});
23+
24+
test('RequiredDateType is RequiredDate', t => {
25+
t.is(DateType.required.toString(), 'RequiredDate');
26+
});
27+
28+
test('RequiredDateType is required', t => {
29+
t.is(DateType.required._required, true);
30+
});
31+
32+
test('RequiredDateType does not have properties', t => {
33+
t.is(DateType.required._properties, undefined);
34+
});
35+
36+
test('RequiredDateType does not have items', t => {
37+
t.is(DateType.required._items, undefined);
38+
});
39+
40+
test('RequiredDateType is not a resolved type', t => {
41+
t.is(DateType.required._resolved, false);
42+
});

0 commit comments

Comments
 (0)