Skip to content
This repository has been archived by the owner on Dec 26, 2022. It is now read-only.

[1.0.9] JSON Migrations #17

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

"jest.autoRun": {},

"search.exclude": {
".git": true,
".eslintcache": true,
Expand All @@ -34,5 +36,5 @@
"*.{css,sass,scss}.d.ts": true
},

"cSpell.words": ["Popconfirm", "Sider"]
"cSpell.words": ["autorun", "Popconfirm", "Sider", "Yadro"]
}
19 changes: 19 additions & 0 deletions docs/Migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
1. Добавить в `schemas` новую схему `{entity}/migrations/{Name}SchemaV{N}`
2. Добавить в `schemas` в файл `index.ts` новую схему

```ts
export const schemaMigrations: SchemaMigration[] = [
...{ version: N, schema: NameSchemaVN },
];
```

3. В AbstractFileRepository.ts restore
1. Парсит json
2. Проверяет являются ли полученные данные объектом
4. Миграция
1. Смотрит есть ли `__version` в корне объекта
2. Если нет, то валидирует с помощью схемы `{Name}SchemaV0`, иначе ищет схему с соответствующей версией
3. Если версия не самая последняя из списка `schemaMigrations`, то применяет миграцию
1. Миграция принимает версию V{N} и возвращает v{N+1}
2. Применяем миграции с N до последней
4. После того как все миграции прошли, передаем данные в конструктор модели
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.6.2",
"@sentry/electron": "2.5.0",
"ajv": "^8.8.2",
"antd": "4.17.2",
"caniuse-lite": "1.0.30001214",
"clsx": "^1.1.1",
Expand Down
4 changes: 3 additions & 1 deletion src/base/AbstractFactory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// ConstructorParameters<typeof SomeClass>

export default abstract class AbstractFactory {
create<T>(Model: any, data: any): T {
return new Model(data);
}

createList<T>(Model: any, data: any): T[] {
createList<M, T>(Model: M, data: any): T[] {
let items: T[] = [];

data.forEach((json: any) => {
Expand Down
261 changes: 261 additions & 0 deletions src/base/MigrationRunner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import MigrationRunner from './MigrationRunner';
import { SchemaMigration } from '../types/SchemaMigration';
import { JSONSchemaType } from 'ajv';
import MigrationErrorCodes from '../types/MigrationErrorCodes';

describe('MigrationRunner tests', () => {
describe('Constructor tests', () => {
test(`Should be fine`, () => {
expect(
new MigrationRunner([
{ version: 0, schema: {} },
{ version: 1, schema: {}, migration: () => null },
])
).toBeDefined();
});

test(`Throw ErrorCode=${MigrationErrorCodes.NoMigrations} NoMigrations`, () => {
expect(() => new MigrationRunner([])).toThrow(
`schemaMigrations can't be empty`
);
});

test(`Throw ErrorCode=${MigrationErrorCodes.NoZeroMigration} NoZeroMigration`, () => {
expect(() => new MigrationRunner([{ version: 1, schema: {} }])).toThrow(
'schemaMigrations should have migration for `version=0`'
);
});

test(`Throw ErrorCode=${MigrationErrorCodes.IncorrectMigrationsOrder} IncorrectMigrationsOrder`, () => {
expect(
() =>
new MigrationRunner([
{ version: 0, schema: {} },
{ version: 2, schema: {} },
])
).toThrow('Each version should go one after the other');
});
});

describe('Migration tests', () => {
type TypeV0 = { data: number };
type TypeV1 = { data: number[]; __version: number };
type TypeV2 = {
data: number[];
additionalData: string;
__version: number;
};
const schemaV0: JSONSchemaType<TypeV0> = {
type: 'object',
properties: {
data: { type: 'number' },
},
required: ['data'],
};
const schemaV1: JSONSchemaType<TypeV1> = {
type: 'object',
properties: {
__version: { type: 'number' },
data: { type: 'array', items: { type: 'number' } },
},
required: ['data', '__version'],
};
const schemaV2: JSONSchemaType<TypeV2> = {
type: 'object',
properties: {
__version: { type: 'number' },
data: { type: 'array', items: { type: 'number' } },
additionalData: { type: 'string' },
},
required: ['data', '__version', 'additionalData'],
};
const migrations: SchemaMigration[] = [
{ version: 0, schema: schemaV0 },
{
version: 1,
schema: schemaV1,
migration(item: TypeV0): TypeV1 {
return {
data: [item.data],
__version: 1,
};
},
},
{
version: 2,
schema: schemaV2,
migration(item: TypeV1): TypeV2 {
return {
data: item.data,
additionalData: 'test',
__version: 2,
};
},
},
];

test('Test migration with 3 iterations', () => {
const dataV0: TypeV0 = { data: 777 };
const expectedData: TypeV2 = {
data: [777],
additionalData: 'test',
__version: 2,
};
const mr = new MigrationRunner(migrations);

const resultData = mr.runMigration(dataV0);
expect(resultData).toStrictEqual(expectedData);
});

test('Test continue migration', () => {
const dataV0: TypeV1 = { data: [777], __version: 1 };
const expectedData: TypeV2 = {
data: [777],
additionalData: 'test',
__version: 2,
};
const mr = new MigrationRunner(migrations);

const resultData = mr.runMigration(dataV0);
expect(resultData).toStrictEqual(expectedData);
});
});

describe('Migration errors', () => {
test(`Throw ErrorCode=${MigrationErrorCodes.ValidationFailed} ValidationFailed`, () => {
type TypeV0 = { data: number; text: string; prop: { test: 1 } };

const schemaV0: JSONSchemaType<TypeV0> = {
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(
[
'Migration to version=1. Schema validation error. Found next errors:',
'"/": "must have required property \'data\'"',
'"/text": "must be string"',
'"/prop": "must have required property \'test\'"',
].join('\n')
);
});

test(`Throw ErrorCode=${MigrationErrorCodes.MigrationNotFound} MigrationNotFound - no migration for version=1`, () => {
type TypeV0 = { data: number };

const schemaV0: JSONSchemaType<TypeV0> = {
type: 'object',
properties: {
data: { type: 'number' },
},
required: ['data'],
};

const migrations: SchemaMigration[] = [
{ version: 0, schema: schemaV0 },
{ version: 1, schema: schemaV0 },
];

const dataV0 = { data: 1 };

const mr = new MigrationRunner(migrations);

expect(() => mr.runMigration(dataV0)).toThrow(
/Migration {\d->\d} not found/
);
});

test(`Throw ErrorCode=${MigrationErrorCodes.MigrationFailed} MigrationFailed - migration returned undefined`, () => {
type TypeV0 = { data: number };
type TypeV1 = { data: number };

const schemaV0: JSONSchemaType<TypeV0> = {
type: 'object',
properties: {
data: { type: 'number' },
},
required: ['data'],
};
const schemaV1: JSONSchemaType<TypeV1> = {
type: 'object',
properties: {
data: { type: 'number' },
},
required: ['data'],
};

const migrations: SchemaMigration[] = [
{ version: 0, schema: schemaV0 },
{
version: 1,
schema: schemaV1,
migration() {
return undefined;
},
},
];

const dataV0 = { data: 1 };

const mr = new MigrationRunner(migrations);

expect(() => mr.runMigration(dataV0)).toThrow(
`migration returned 'undefined'`
);
});

test(`Throw ErrorCode=${MigrationErrorCodes.MigrationFailed} MigrationFailed migration returned 'data' without '__version'`, () => {
type TypeV0 = { data: number };
type TypeV1 = { data: number };

const schemaV0: JSONSchemaType<TypeV0> = {
type: 'object',
properties: {
data: { type: 'number' },
},
required: ['data'],
};
const schemaV1: JSONSchemaType<TypeV1> = {
type: 'object',
properties: {
data: { type: 'number' },
},
required: ['data'],
};

const migrations: SchemaMigration[] = [
{ version: 0, schema: schemaV0 },
{
version: 1,
schema: schemaV1,
migration(item) {
return item;
},
},
];

const dataV0 = { data: 1 };

const mr = new MigrationRunner(migrations);

expect(() => mr.runMigration(dataV0)).toThrow(
`migration returned 'data' without '__version'`
);
});
});
});
Loading