Skip to content

Commit

Permalink
feat: date type (#9)
Browse files Browse the repository at this point in the history
* feat: add date type

* feat: add arrays of dates

* feat: dates are deserialised as Date objects
  • Loading branch information
acodeninja authored Sep 10, 2024
1 parent 6142fcc commit 3096e64
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 6 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class SimpleModel extends Persist.Type.Model {
static boolean = Persist.Type.Boolean;
static string = Persist.Type.String;
static number = Persist.Type.Number;
static date = Persist.Type.Date;
}
```

Expand All @@ -29,6 +30,7 @@ export class SimpleModel extends Persist.Type.Model {
static requiredBoolean = Persist.Type.Boolean.required;
static requiredString = Persist.Type.String.required;
static requiredNumber = Persist.Type.Number.required;
static requiredDate = Persist.Type.Date.required;
}
```

Expand All @@ -41,6 +43,11 @@ export class SimpleModel extends Persist.Type.Model {
static arrayOfBooleans = Persist.Type.Array.of(Type.Boolean);
static arrayOfStrings = Persist.Type.Array.of(Type.String);
static arrayOfNumbers = Persist.Type.Array.of(Type.Number);
static arrayOfDates = Persist.Type.Array.of(Type.Date);
static requiredArrayOfBooleans = Persist.Type.Array.of(Type.Boolean).required;
static requiredArrayOfStrings = Persist.Type.Array.of(Type.String).required;
static requiredArrayOfNumbers = Persist.Type.Array.of(Type.Number).required;
static requiredArrayOfDates = Persist.Type.Array.of(Type.Date).required;
}
```

Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"ajv": "^8.16.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^3.0.1",
"lunr": "^2.3.9",
"slugify": "^1.6.6",
"ulid": "^2.3.0"
Expand Down
15 changes: 14 additions & 1 deletion src/SchemaCompiler.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Type from './type/index.js';
import ajv from 'ajv';
import ajvErrors from 'ajv-errors';
import ajvFormats from 'ajv-formats';

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

ajvErrors(validation);
ajvFormats(validation);

const schema = {
type: 'object',
Expand Down Expand Up @@ -58,9 +60,17 @@ export default class SchemaCompiler {

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

if (property?._format) {
schema.properties[name].format = property._format;
}

if (property?._type === 'array') {
schema.properties[name].items = {type: property?._items._type};

if (property?._items?._format) {
schema.properties[name].items.format = property?._items._format;
}

if (Type.Model.isModel(property?._items)) {
schema.properties[name].items = {
type: 'object',
Expand Down Expand Up @@ -102,11 +112,14 @@ export class CompiledSchema {
* @throws {ValidationError}
*/
static validate(data) {
let inputData = data;
let inputData = Object.assign({}, data);

if (Type.Model.isModel(data)) {
inputData = data.toData();
}

const valid = this._validator?.(inputData);

if (valid) return valid;

throw new ValidationError(inputData, this._validator.errors);
Expand Down
42 changes: 41 additions & 1 deletion src/SchemaCompiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ const schema = {
requiredNumber: Type.Number.required,
boolean: Type.Boolean,
requiredBoolean: Type.Boolean.required,
date: Type.Date,
requiredDate: Type.Date.required,
arrayOfString: Type.Array.of(Type.String),
arrayOfNumber: Type.Array.of(Type.Number),
arrayOfBoolean: Type.Array.of(Type.Boolean),
arrayOfDate: Type.Array.of(Type.Date),
requiredArrayOfString: Type.Array.of(Type.String).required,
requiredArrayOfNumber: Type.Array.of(Type.Number).required,
requiredArrayOfBoolean: Type.Array.of(Type.Boolean).required,
requiredArrayOfDate: Type.Array.of(Type.Date).required,
};

const invalidDataErrors = [{
Expand All @@ -44,6 +48,12 @@ const invalidDataErrors = [{
message: 'must have required property \'requiredBoolean\'',
params: {missingProperty: 'requiredBoolean'},
schemaPath: '#/required',
}, {
instancePath: '',
keyword: 'required',
message: 'must have required property \'requiredDate\'',
params: {missingProperty: 'requiredDate'},
schemaPath: '#/required',
}, {
instancePath: '/string',
keyword: 'type',
Expand All @@ -62,6 +72,12 @@ const invalidDataErrors = [{
message: 'must be boolean',
params: {type: 'boolean'},
schemaPath: '#/properties/boolean/type',
}, {
instancePath: '/date',
keyword: 'format',
message: 'must match format "iso-date-time"',
params: {format: 'iso-date-time'},
schemaPath: '#/properties/date/format',
}, {
instancePath: '/arrayOfString/0',
keyword: 'type',
Expand All @@ -80,6 +96,12 @@ const invalidDataErrors = [{
message: 'must be boolean',
params: {type: 'boolean'},
schemaPath: '#/properties/arrayOfBoolean/items/type',
}, {
instancePath: '/arrayOfDate/0',
keyword: 'format',
message: 'must match format "iso-date-time"',
params: {format: 'iso-date-time'},
schemaPath: '#/properties/arrayOfDate/items/format',
}, {
instancePath: '/requiredArrayOfString/0',
keyword: 'type',
Expand All @@ -98,6 +120,12 @@ const invalidDataErrors = [{
message: 'must be boolean',
params: {type: 'boolean'},
schemaPath: '#/properties/requiredArrayOfBoolean/items/type',
}, {
instancePath: '/requiredArrayOfDate/0',
keyword: 'format',
message: 'must match format "iso-date-time"',
params: {format: 'iso-date-time'},
schemaPath: '#/properties/requiredArrayOfDate/items/format',
}];

test('.compile(schema) is an instance of CompiledSchema', t => {
Expand All @@ -112,9 +140,11 @@ test('.compile(schema) has the given schema associated with it', t => {
'requiredString',
'requiredNumber',
'requiredBoolean',
'requiredDate',
'requiredArrayOfString',
'requiredArrayOfNumber',
'requiredArrayOfBoolean',
'requiredArrayOfDate',
],
properties: {
custom: {
Expand All @@ -131,12 +161,16 @@ test('.compile(schema) has the given schema associated with it', t => {
requiredNumber: {type: 'number'},
boolean: {type: 'boolean'},
requiredBoolean: {type: 'boolean'},
date: {type: 'string', format: 'iso-date-time'},
requiredDate: {type: 'string', format: 'iso-date-time'},
arrayOfString: {type: 'array', items: {type: 'string'}},
arrayOfNumber: {type: 'array', items: {type: 'number'}},
arrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
arrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
requiredArrayOfString: {type: 'array', items: {type: 'string'}},
requiredArrayOfNumber: {type: 'array', items: {type: 'number'}},
requiredArrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
requiredArrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
},
});
});
Expand All @@ -156,7 +190,7 @@ test('.compile(schema).validate(invalid) throws a ValidationError', t => {
);

t.is(error.message, 'Validation failed');
t.is(error.data, invalid);
t.deepEqual(error.data, invalid);
t.deepEqual(error.errors, invalidDataErrors);
});

Expand All @@ -169,9 +203,11 @@ test('.compile(MainModel) has the given schema associated with it', t => {
'requiredString',
'requiredNumber',
'requiredBoolean',
'requiredDate',
'requiredArrayOfString',
'requiredArrayOfNumber',
'requiredArrayOfBoolean',
'requiredArrayOfDate',
'requiredLinked',
],
properties: {
Expand All @@ -190,12 +226,16 @@ test('.compile(MainModel) has the given schema associated with it', t => {
requiredNumber: {type: 'number'},
boolean: {type: 'boolean'},
requiredBoolean: {type: 'boolean'},
date: {type: 'string', format: 'iso-date-time'},
requiredDate: {type: 'string', format: 'iso-date-time'},
arrayOfString: {type: 'array', items: {type: 'string'}},
arrayOfNumber: {type: 'array', items: {type: 'number'}},
arrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
arrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
requiredArrayOfString: {type: 'array', items: {type: 'string'}},
requiredArrayOfNumber: {type: 'array', items: {type: 'number'}},
requiredArrayOfBoolean: {type: 'array', items: {type: 'boolean'}},
requiredArrayOfDate: {type: 'array', items: {type: 'string', format: 'iso-date-time'}},
requiredLinked: {
type: 'object',
additionalProperties: false,
Expand Down
9 changes: 7 additions & 2 deletions src/engine/Engine.api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ for (const {engine, configuration, configurationIgnores} of engines) {

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


t.like(got, getTestModelInstance(valid).toData());
t.like(got, {
...getTestModelInstance(valid).toData(),
date: new Date(valid.date),
requiredDate: new Date(valid.requiredDate),
arrayOfDate: [new Date(valid.arrayOfDate[0])],
requiredArrayOfDate: [new Date(valid.requiredArrayOfDate[0])],
});
});

test(`${engine.toString()}.get(MainModel, id) throws NotFoundEngineError when no model exists`, async t => {
Expand Down
8 changes: 7 additions & 1 deletion src/engine/HTTPEngine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,13 @@ test('HTTPEngine.search(MainModel, "Str") when a matching model exists', async t
t.like(models, [{
ref: 'MainModel/000000000000',
score: 0.211,
model: model0.toData(),
model: {
...model0.toData(),
date: new Date(model0.date),
requiredDate: new Date(model0.requiredDate),
arrayOfDate: model0.arrayOfDate[0] ? [new Date(model0.arrayOfDate[0])] : [],
requiredArrayOfDate: [new Date(model0.requiredArrayOfDate[0])],
},
}, {
ref: 'MainModel/111111111111',
score: 0.16,
Expand Down
11 changes: 11 additions & 0 deletions src/type/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ export default class Model {

for (const [name, value] of Object.entries(data)) {
if (this[name]?._resolved) continue;

if (this[name].name.endsWith('DateType')) {
model[name] = new Date(value);
continue;
}

if (this[name].name.endsWith('ArrayOf(Date)Type')) {
model[name] = data[name].map(d => new Date(d));
continue;
}

model[name] = value;
}

Expand Down
4 changes: 3 additions & 1 deletion src/type/complex/ArrayType.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class ArrayType {
static _items = type;

static toString() {
return `ArrayOf(${type})`;
return `ArrayOf(${type.toString()})`;
}

static get required() {
Expand All @@ -25,6 +25,8 @@ export default class ArrayType {
}
}

Object.defineProperty(ArrayOf, 'name', {value: `${ArrayOf.toString()}Type`});

return ArrayOf;
}
}
2 changes: 2 additions & 0 deletions src/type/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ArrayType from './complex/ArrayType.js';
import BooleanType from './simple/BooleanType.js';
import CustomType from './complex/CustomType.js';
import DateType from './simple/DateType.js';
import Model from './Model.js';
import NumberType from './simple/NumberType.js';
import SlugType from './resolved/SlugType.js';
Expand All @@ -11,6 +12,7 @@ const Type = {};
Type.String = StringType;
Type.Number = NumberType;
Type.Boolean = BooleanType;
Type.Date = DateType;
Type.Array = ArrayType;
Type.Custom = CustomType;
Type.Resolved = {Slug: SlugType};
Expand Down
10 changes: 10 additions & 0 deletions src/type/simple/DateType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SimpleType from './SimpleType.js';

export default class DateType extends SimpleType {
static _type = 'string';
static _format = 'iso-date-time';

static isDate(possibleDate) {
return possibleDate instanceof Date || !isNaN(new Date(possibleDate));
}
}
42 changes: 42 additions & 0 deletions src/type/simple/DateType.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import DateType from './DateType.js';
import test from 'ava';

test('DateType is Date', t => {
t.is(DateType.toString(), 'Date');
});

test('DateType is not required', t => {
t.is(DateType._required, false);
});

test('DateType does not have properties', t => {
t.is(DateType._properties, undefined);
});

test('DateType does not have items', t => {
t.is(DateType._items, undefined);
});

test('DateType is not a resolved type', t => {
t.is(DateType._resolved, false);
});

test('RequiredDateType is RequiredDate', t => {
t.is(DateType.required.toString(), 'RequiredDate');
});

test('RequiredDateType is required', t => {
t.is(DateType.required._required, true);
});

test('RequiredDateType does not have properties', t => {
t.is(DateType.required._properties, undefined);
});

test('RequiredDateType does not have items', t => {
t.is(DateType.required._items, undefined);
});

test('RequiredDateType is not a resolved type', t => {
t.is(DateType.required._resolved, false);
});
Loading

0 comments on commit 3096e64

Please sign in to comment.