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

Commit 8914df9

Browse files
committed
WIP migration
1 parent 633b5de commit 8914df9

26 files changed

+373
-15
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"editor.defaultFormatter": "esbenp.prettier-vscode"
1919
},
2020

21+
"jest.autoRun": {},
22+
2123
"search.exclude": {
2224
".git": true,
2325
".eslintcache": true,

docs/Migrations.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
1. Добавить в `schemas` новую схему `{entity}/migrations/{Name}SchemaV{N}`
2+
2. Добавить в `schemas` в файл `index.ts` новую схему
3+
4+
```ts
5+
export const schemaMigrations: SchemaMigration[] = [
6+
...{ version: N, schema: NameSchemaVN },
7+
];
8+
```
9+
10+
3. В AbstractFileRepository.ts restore
11+
1. Парсит json
12+
2. Проверяет являются ли полученные данные объектом
13+
4. Миграция
14+
1. Смотрит есть ли `__version` в корне объекта
15+
2. Если нет, то валидирует с помощью схемы `{Name}SchemaV0`, иначе ищет схему с соответствующей версией
16+
3. Если версия не самая последняя из списка `schemaMigrations`, то применяет миграцию
17+
1. Миграция принимает версию V{N} и возвращает v{N+1}
18+
2. Применяем миграции с N до последней
19+
4. После того как все миграции прошли, передаем данные в конструктор модели

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@
231231
"@ant-design/colors": "6.0.0",
232232
"@ant-design/icons": "4.6.2",
233233
"@sentry/electron": "2.5.0",
234+
"ajv": "^8.8.2",
234235
"antd": "4.17.2",
235236
"caniuse-lite": "1.0.30001214",
236237
"clsx": "^1.1.1",

src/base/MigrationRunner.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import MigrationRunner from './MigrationRunner';
2+
import { SchemaMigration } from '../types/SchemaMigration';
3+
import { JSONSchemaType } from 'ajv';
4+
5+
describe('MigrationRunner tests', () => {
6+
describe('constructor tests', () => {
7+
test('throw error when schemaMigrations is empty', () => {
8+
expect(() => new MigrationRunner([])).toThrow(
9+
'schemaMigrations can`t be empty'
10+
);
11+
});
12+
13+
test('throw error when there is no migration `version=0`', () => {
14+
expect(() => new MigrationRunner([{ version: 1, schema: {} }])).toThrow(
15+
'schemaMigrations should have migration for `version=0`'
16+
);
17+
});
18+
19+
test('throw error when versions doesn`t go one after the other', () => {
20+
expect(
21+
() =>
22+
new MigrationRunner([
23+
{ version: 0, schema: {} },
24+
{ version: 2, schema: {} },
25+
])
26+
).toThrow('Each version should go one after the other');
27+
});
28+
});
29+
30+
describe('migration tests', () => {
31+
test('Test migration', () => {
32+
type TypeV0 = { data: number };
33+
type TypeV1 = { data: number[]; __version: number };
34+
type TypeV2 = {
35+
data: number[];
36+
additionalData: string;
37+
__version: number;
38+
};
39+
const schemaV0: JSONSchemaType<TypeV0> = {
40+
type: 'object',
41+
properties: {
42+
data: { type: 'number' },
43+
},
44+
required: ['data'],
45+
};
46+
const schemaV1: JSONSchemaType<TypeV1> = {
47+
type: 'object',
48+
properties: {
49+
__version: { type: 'number' },
50+
data: { type: 'array', items: { type: 'number' } },
51+
},
52+
required: ['data', '__version'],
53+
};
54+
const schemaV2: JSONSchemaType<TypeV2> = {
55+
type: 'object',
56+
properties: {
57+
__version: { type: 'number' },
58+
data: { type: 'array', items: { type: 'number' } },
59+
additionalData: { type: 'string' },
60+
},
61+
required: ['data', '__version', 'additionalData'],
62+
};
63+
const migrations: SchemaMigration[] = [
64+
{ version: 0, schema: schemaV0 },
65+
{
66+
version: 1,
67+
schema: schemaV1,
68+
migration(item: TypeV0): TypeV1 {
69+
return {
70+
data: [item.data],
71+
__version: 1,
72+
};
73+
},
74+
},
75+
{
76+
version: 2,
77+
schema: schemaV2,
78+
migration(item: TypeV1): TypeV2 {
79+
return {
80+
data: item.data,
81+
additionalData: 'test',
82+
__version: 2,
83+
};
84+
},
85+
},
86+
];
87+
88+
const dataV0: TypeV0 = { data: 77 };
89+
const expectedData: TypeV2 = {
90+
data: [77],
91+
additionalData: 'test',
92+
__version: 2,
93+
};
94+
const mr = new MigrationRunner(migrations);
95+
96+
const resultData = mr.runMigration(dataV0);
97+
expect(resultData).toStrictEqual(expectedData);
98+
});
99+
});
100+
});

src/base/MigrationRunner.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { SchemaMigration } from '../types/SchemaMigration';
2+
import { SchemaType } from '../types/SchemaType';
3+
import { last } from '../helpers/ArrayHelper';
4+
5+
export default class MigrationRunner {
6+
private schemaMigrations: SchemaMigration[] = [];
7+
private genErrorMsg = (message: string) => `[MigrationRunner] ${message}`;
8+
9+
constructor(schemaMigrations: SchemaMigration[]) {
10+
const schemaMigrationsSorted = schemaMigrations.slice();
11+
schemaMigrationsSorted.sort((a, b) => a.version - b.version);
12+
13+
if (!schemaMigrationsSorted.length) {
14+
throw new Error(this.genErrorMsg('schemaMigrations can`t be empty'));
15+
}
16+
17+
if (!schemaMigrationsSorted.find((i) => i.version === 0)) {
18+
throw new Error(
19+
this.genErrorMsg(
20+
'schemaMigrations should have migration for `version=0`'
21+
)
22+
);
23+
}
24+
25+
const hasAllVersions = schemaMigrations.every(
26+
(i, idx) => i.version === idx
27+
);
28+
if (!hasAllVersions) {
29+
throw new Error(
30+
this.genErrorMsg('Each version should go one after the other')
31+
);
32+
}
33+
34+
this.schemaMigrations = schemaMigrationsSorted;
35+
}
36+
37+
runMigration<T extends SchemaType>(data: T) {
38+
let newData: T = data;
39+
const lastVersion = last(this.schemaMigrations)?.version;
40+
41+
if (lastVersion === undefined) {
42+
throw new Error('There are no migrations');
43+
}
44+
45+
// if (!('__version' in data)) {
46+
// const migration = this.schemaMigrations.find((i) => i.version === 1);
47+
// if (!migration) {
48+
// throw new Error();
49+
// }
50+
// newData = migration.migration?.(data);
51+
// } else {
52+
// newData = data;
53+
// }
54+
55+
// if (newData === undefined || newData.__version === undefined) {
56+
// throw new Error();
57+
// }
58+
59+
let nextVersion =
60+
newData.__version !== undefined ? newData.__version + 1 : 1;
61+
62+
while (true) {
63+
if (nextVersion > lastVersion) {
64+
return newData;
65+
}
66+
67+
const migration = this.schemaMigrations.find(
68+
(m) => m.version === nextVersion
69+
);
70+
71+
if (!migration) {
72+
throw new Error(
73+
`Migration from ${newData.__version} to ${nextVersion} not found`
74+
);
75+
}
76+
77+
if (migration?.migration) {
78+
const nextData: T = migration.migration(newData);
79+
80+
if (nextData === undefined) {
81+
throw new Error();
82+
}
83+
if (nextData.__version === undefined) {
84+
throw new Error();
85+
}
86+
87+
newData = nextData;
88+
nextVersion = nextData.__version + 1;
89+
}
90+
}
91+
}
92+
}

src/base/repositories/AbstractFileRepository.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ const path = require('path');
55
import { ipcRenderer } from 'electron';
66

77
import FsHelper from '../../helpers/FsHelper';
8-
import PromiseQueue from '../../helpers/PromiseQueueHellper';
8+
import PromiseQueue from '../../helpers/PromiseQueueHelper';
9+
import { SchemaMigration } from '../../types/SchemaMigration';
910

1011
const APP_DIR =
1112
process.env.NODE_ENV === 'development'
@@ -18,8 +19,9 @@ export default abstract class AbstractFileRepository<T = any> {
1819
dirWithProfileData: string = 'profile1';
1920
fileName: string = 'defaultFileName.json';
2021
saveInRoot: boolean = false;
22+
schemaMigrations: SchemaMigration[] = [];
2123

22-
writeFileQueue = new PromiseQueue();
24+
private writeFileQueue = new PromiseQueue();
2325

2426
private get logPrefix() {
2527
const filePath = !this.saveInRoot ? this.dirWithProfileData : '';
@@ -55,7 +57,7 @@ export default abstract class AbstractFileRepository<T = any> {
5557
if (fs.existsSync(this.filePath)) {
5658
const data = fs.readFileSync(this.filePath, { encoding: 'utf-8' });
5759
// TODO handle parse error. Backup file with issues and return defaultValue
58-
return JSON.parse(data);
60+
const parsedData = JSON.parse(data);
5961
}
6062
return defaultValue;
6163
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Ajv from 'ajv';
2+
import ProjectSchemaV0 from './schemas/ProjectSchemaV0';
3+
4+
const ajv = new Ajv({ allErrors: true });
5+
export const validate = ajv.compile(ProjectSchemaV0);

src/modules/projects/migrations/MigrationV1.ts

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { JSONSchemaType } from 'ajv';
2+
import ProjectModel from '../models/ProjectModel';
3+
4+
const ProjectSchemaV0: JSONSchemaType<ProjectModel> = {
5+
type: 'object',
6+
properties: {
7+
key: { type: 'string' },
8+
title: { type: 'string' },
9+
color: { type: 'string' },
10+
expanded: { type: 'boolean', default: false, nullable: true },
11+
deletable: { type: 'boolean', default: true, nullable: true },
12+
children: {
13+
type: 'array',
14+
items: {
15+
type: 'object',
16+
$ref: '#',
17+
required: ['key', 'title', 'color'],
18+
},
19+
nullable: true,
20+
},
21+
parent: { type: 'object', $ref: '#', nullable: true },
22+
},
23+
required: [],
24+
};
25+
26+
export default ProjectSchemaV0;

0 commit comments

Comments
 (0)