diff --git a/src/base/MigrationRunner.test.ts b/src/base/MigrationRunner.test.ts index e4bf2a6..9172057 100644 --- a/src/base/MigrationRunner.test.ts +++ b/src/base/MigrationRunner.test.ts @@ -96,5 +96,35 @@ describe('MigrationRunner tests', () => { const resultData = mr.runMigration(dataV0); expect(resultData).toStrictEqual(expectedData); }); + + test('Test when there is schema validation error', () => { + type TypeV0 = { data: number; text: string; prop: { test: 1 } }; + const schemaV0: JSONSchemaType = { + type: 'object', + properties: { + data: { type: 'number' }, + text: { type: 'string' }, + prop: { + type: 'object', + properties: { test: { type: 'number' } }, + required: ['test'], + }, + }, + required: ['data', 'text'], + }; + const migrations: SchemaMigration[] = [{ version: 0, schema: schemaV0 }]; + + const dataV0 = { fakeData: 77, text: 123, prop: { testFail: 1 } }; + const mr = new MigrationRunner(migrations); + + expect(() => mr.runMigration(dataV0)).toThrow( + [ + '[MigrationRunner] Schema validation error "version=0". Found next errors:', + '"/": "must have required property \'data\'"', + '"/text": "must be string"', + '"/prop": "must have required property \'test\'"', + ].join('\n') + ); + }); }); }); diff --git a/src/base/MigrationRunner.ts b/src/base/MigrationRunner.ts index 994f728..3a2729b 100644 --- a/src/base/MigrationRunner.ts +++ b/src/base/MigrationRunner.ts @@ -1,17 +1,31 @@ +import Ajv from 'ajv'; import { SchemaMigration } from '../types/SchemaMigration'; import { SchemaType } from '../types/SchemaType'; import { last } from '../helpers/ArrayHelper'; +enum ErrorCodes { + NoMigrations, + NoZeroMigration, + IncorrectMigrationsOrder, +} + export default class MigrationRunner { private schemaMigrations: SchemaMigration[] = []; - private genErrorMsg = (message: string) => `[MigrationRunner] ${message}`; + private ajv: Ajv; + private genErrorMsg = (error: ErrorCodes | undefined, message: string) => + `[MigrationRunner] ${error ?? -1} ${message}`; constructor(schemaMigrations: SchemaMigration[]) { const schemaMigrationsSorted = schemaMigrations.slice(); schemaMigrationsSorted.sort((a, b) => a.version - b.version); if (!schemaMigrationsSorted.length) { - throw new Error(this.genErrorMsg('schemaMigrations can`t be empty')); + throw new Error( + this.genErrorMsg( + ErrorCodes.NoMigrations, + 'schemaMigrations can`t be empty' + ) + ); } if (!schemaMigrationsSorted.find((i) => i.version === 0)) { @@ -32,6 +46,7 @@ export default class MigrationRunner { } this.schemaMigrations = schemaMigrationsSorted; + this.ajv = new Ajv({ allErrors: true }); } runMigration(data: T) { @@ -42,19 +57,24 @@ export default class MigrationRunner { throw new Error('There are no migrations'); } - // if (!('__version' in data)) { - // const migration = this.schemaMigrations.find((i) => i.version === 1); - // if (!migration) { - // throw new Error(); - // } - // newData = migration.migration?.(data); - // } else { - // newData = data; - // } - - // if (newData === undefined || newData.__version === undefined) { - // throw new Error(); - // } + const firstMigration = this.schemaMigrations.find((i) => i.version === 0); + if (!firstMigration) { + throw new Error(); + } + const validate = this.ajv.compile(firstMigration.schema); + const validateResult = validate(newData); + if (!validateResult) { + throw new Error( + this.genErrorMsg( + `Schema validation error "version=0". Found next errors:\n${validate.errors + ?.map( + (e) => + `"${e.instancePath ? e.instancePath : '/'}": "${e.message}"` + ) + ?.join('\n')}` + ) + ); + } let nextVersion = newData.__version !== undefined ? newData.__version + 1 : 1; @@ -70,7 +90,9 @@ export default class MigrationRunner { if (!migration) { throw new Error( - `Migration from ${newData.__version} to ${nextVersion} not found` + this.genErrorMsg( + `Migration from ${newData.__version} to ${nextVersion} not found` + ) ); } @@ -84,6 +106,18 @@ export default class MigrationRunner { throw new Error(); } + const validate = this.ajv.compile(migration.schema); + const validateObj = validate(nextData); + if (!validateObj) { + throw new Error( + this.genErrorMsg( + `Schema validation error. version=${nextVersion}: ${validate.errors?.join( + ', ' + )}` + ) + ); + } + newData = nextData; nextVersion = nextData.__version + 1; }