@getanthill/datastore
Version:
Event-Sourced Datastore
685 lines (641 loc) • 13.6 kB
text/typescript
import {
deepCoerce,
getDate,
getDateNow,
mapValuesDeep,
validateEntity,
} from '.';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { mapDateTimeFormatToEitherStringOrObject } from '../models/schema';
describe('utils', () => {
describe('#getDate', () => {
it('returns current Date', () => {
expect(getDate().getTime()).toBeLessThanOrEqual(Date.now());
});
it('returns exact Date', () => {
expect(getDate('2021-01-01T00:00:00.000Z').getTime()).toEqual(
new Date('2021-01-01T00:00:00.000Z').getTime(),
);
});
});
describe('#getDateNow', () => {
it('returns current time', () => {
expect(getDateNow()).toBeLessThanOrEqual(Date.now());
});
});
describe('#mapValuesDeep', () => {
it('returns the current object without handler', () => {
expect(
mapValuesDeep({
a: 1,
}),
).toEqual({
a: 1,
});
});
it('returns the mapped value', () => {
expect(mapValuesDeep({ a: 1 }, (v) => v + 1)).toEqual({
a: 2,
});
});
it('returns the nested mapped value', () => {
expect(
mapValuesDeep({ a: { b: 1 } }, (v) =>
typeof v === 'number' ? v + 1 : v,
),
).toEqual({
a: { b: 2 },
});
});
it('returns the nested mapped value in arrays', () => {
expect(
mapValuesDeep({ a: [1] }, (v) => (typeof v === 'number' ? v + 1 : v)),
).toEqual({
a: [2],
});
});
});
describe('#deepCoerce', () => {
it('does nothing on schemas not needing update', () => {
expect(
deepCoerce(
{
value: 1,
},
{
properties: {
value: {
type: 'number',
},
},
},
),
).toEqual({
value: 1,
});
});
it('does nothing on string values', () => {
expect(
deepCoerce(
{
value: '1',
},
{
properties: {
value: {
type: 'string',
},
},
},
),
).toEqual({
value: '1',
});
});
it('does nothing on array items', () => {
expect(
deepCoerce(
{
value: ['1', 1],
},
{
properties: {
value: {
type: 'array',
},
},
},
),
).toEqual({
value: ['1', 1],
});
});
it('coerces number values', () => {
expect(
deepCoerce(
{
value: '1',
},
{
properties: {
value: {
type: 'number',
},
},
},
),
).toEqual({
value: 1,
});
});
it('does noop on not valid number value', () => {
expect(
deepCoerce(
{
value: {
$exists: true,
},
},
{
properties: {
value: {
type: 'number',
},
},
},
),
).toEqual({
value: {
$exists: true,
},
});
});
it('coerces number values supporting arrays', () => {
expect(
deepCoerce(
{
value: ['1'],
},
{
properties: {
value: {
type: 'number',
},
},
},
),
).toEqual({
value: [1],
});
});
it('coerces integer values', () => {
expect(
deepCoerce(
{
value: '1',
},
{
properties: {
value: {
type: 'integer',
},
},
},
),
).toEqual({
value: 1,
});
});
it('does noop on not valid integer value', () => {
expect(
deepCoerce(
{
value: {
$exists: true,
},
},
{
properties: {
value: {
type: 'integer',
},
},
},
),
).toEqual({
value: {
$exists: true,
},
});
});
it('coerces integer values supporting arrays', () => {
expect(
deepCoerce(
{
value: ['1'],
},
{
properties: {
value: {
type: 'integer',
},
},
},
),
).toEqual({
value: [1],
});
});
it('coerces boolean values (1)', () => {
expect(
deepCoerce(
{
value: '1',
},
{
properties: {
value: {
type: 'boolean',
},
},
},
),
).toEqual({
value: true,
});
});
it('coerces boolean values (true)', () => {
expect(
deepCoerce(
{
value: 'true',
},
{
properties: {
value: {
type: 'boolean',
},
},
},
),
).toEqual({
value: true,
});
});
it('coerces boolean values supporting arrays', () => {
expect(
deepCoerce(
{
value: ['true', 'false'],
},
{
properties: {
value: {
type: 'boolean',
},
},
},
undefined,
[],
true,
),
).toEqual({
value: [true, false],
});
});
it('coerces array values', () => {
expect(
deepCoerce(
{
value: ['1', 1],
},
{
properties: {
value: {
type: 'array',
items: {
type: 'integer',
},
},
},
},
),
).toEqual({
value: [1, 1],
});
});
it('does nothing on nested schemas not needing update', () => {
expect(
deepCoerce(
{
nested: {
value: 1,
},
},
{
properties: {
nested: {
properties: {
value: {
type: 'number',
},
},
},
},
},
),
).toEqual({
nested: {
value: 1,
},
});
});
it('coerces format: date (string)', () => {
expect(
deepCoerce(
{
value: '2020-01-01T00:00:00.000Z',
},
{
properties: {
value: {
type: 'string',
format: 'date',
},
},
},
),
).toEqual({
value: new Date('2020-01-01T00:00:00.000Z'),
});
});
it('coerces format: date (number)', () => {
expect(
deepCoerce(
{
value: new Date('2020-01-01T00:00:00.000Z').getTime(),
},
{
properties: {
value: {
type: 'string',
format: 'date',
},
},
},
),
).toEqual({
value: new Date('2020-01-01T00:00:00.000Z'),
});
});
it('coerces format: date-time', () => {
expect(
deepCoerce(
{
value: '2020-01-01',
},
{
properties: {
value: {
type: 'string',
format: 'date-time',
},
},
},
),
).toEqual({
value: new Date('2020-01-01'),
});
});
it('coerces format: date on $exists property', () => {
expect(
deepCoerce(
{
value: {
$exists: true,
},
},
{
properties: {
value: {
type: 'string',
format: 'date',
},
},
},
),
).toEqual({
value: {
$exists: true,
},
});
});
it('coerces on sub keys not mapped on a schema (MongoDB like queries)', () => {
expect(
deepCoerce(
{
value: {
$gt: '2020-01-01',
},
value_2: {
sub: {
sub: {
sub: '2020-01-01',
},
},
},
},
{
properties: {
value: {
type: 'string',
format: 'date-time',
},
value_2: {
type: 'string',
format: 'date-time',
},
},
},
),
).toEqual({
value: {
$gt: new Date('2020-01-01'),
},
value_2: {
sub: {
sub: {
sub: new Date('2020-01-01'),
},
},
},
});
});
it('coerces on nested schemas', () => {
expect(
deepCoerce(
{
nested: {
value: '2020-01-01',
},
},
{
properties: {
nested: {
properties: {
value: {
type: 'string',
format: 'date',
},
},
},
},
},
),
).toEqual({
nested: {
value: new Date('2020-01-01'),
},
});
});
it('performs a noop on frozen object', () => {
expect(
deepCoerce(
Object.freeze({
nested: {
value: '2020-01-01',
},
}),
{
properties: {
nested: {
properties: {
value: {
type: 'string',
format: 'date',
},
},
},
},
},
),
).toEqual({
nested: {
value: '2020-01-01',
},
});
});
});
describe('#validateEntity', () => {
let validator;
beforeEach(() => {
validator = new Ajv({
strict: false,
schemas: [],
useDefaults: true,
});
// @ts-ignore
addFormats(validator);
});
it('returns true if the entity is validating the schema', () => {
expect(
validateEntity(
validator,
{
hello: 'john',
},
{
type: 'object',
},
),
).toEqual(true);
});
it('uses cache schema instead of second validation', () => {
const addSchemaSpy = jest.spyOn(validator, 'addSchema');
validateEntity(
validator,
{
hello: 'john',
},
{
type: 'object',
},
);
expect(
validateEntity(
validator,
{
hello: 'john',
},
{
type: 'object',
},
),
).toEqual(true);
expect(addSchemaSpy).toHaveBeenCalledTimes(1);
});
it('validates dates as strings', () => {
expect(
validateEntity(
validator,
{
created_at: new Date(),
},
mapDateTimeFormatToEitherStringOrObject({
type: 'object',
properties: {
created_at: {
type: 'string',
format: 'date-time',
},
},
}),
),
).toEqual(true);
});
it('validates dates in arrays', () => {
expect(
validateEntity(
validator,
{
valid_dates: [new Date()],
},
mapDateTimeFormatToEitherStringOrObject({
type: 'object',
properties: {
valid_dates: {
type: 'array',
items: {
type: 'string',
format: 'date-time',
},
},
},
}),
),
).toEqual(true);
});
it('throws an error on invalid entity', () => {
let error;
try {
validateEntity(
validator,
{
number: 'invalid',
},
{
type: 'object',
properties: {
number: {
type: 'number',
},
},
},
);
} catch (err) {
error = err;
}
expect(error.message).toEqual('State schema validation error');
expect(error).toHaveProperty('entity', {
number: 'invalid',
});
expect(error).toHaveProperty('details', [
{
instancePath: '/number',
keyword: 'type',
message: 'must be number',
params: {
type: 'number',
},
schemaPath: '#/properties/number/type',
},
{
entity: {
number: 'invalid',
},
},
]);
});
});
});