@balena/pinejs 15.0.0-true-boolean-911aca4062d3132ad3c34712014739b6849fa13a → 15.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.dockerignore +4 -0
- package/.github/workflows/flowzone.yml +21 -0
- package/.husky/pre-commit +4 -0
- package/.pinejs-cache.json +1 -0
- package/.resinci.yml +1 -0
- package/.versionbot/CHANGELOG.yml +9678 -2001
- package/CHANGELOG.md +2975 -2
- package/Dockerfile +14 -0
- package/Gruntfile.ts +3 -6
- package/README.md +10 -1
- package/VERSION +1 -0
- package/build/browser.ts +1 -1
- package/build/config.ts +0 -1
- package/docker-compose.npm-test.yml +11 -0
- package/docs/AdvancedUsage.md +77 -63
- package/docs/GettingStarted.md +90 -41
- package/docs/Migrations.md +102 -1
- package/docs/ProjectConfig.md +12 -21
- package/docs/Testing.md +7 -0
- package/out/bin/abstract-sql-compiler.js +17 -17
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +23 -20
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +22 -22
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/bin/utils.d.ts +2 -2
- package/out/bin/utils.js +3 -3
- package/out/bin/utils.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +9 -8
- package/out/config-loader/config-loader.js +135 -78
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +41 -16
- package/out/config-loader/env.js +46 -2
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +2 -19
- package/out/data-server/sbvr-server.js +44 -38
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.d.ts +32 -14
- package/out/database-layer/db.js +120 -41
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +10 -11
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +2 -18
- package/out/http-transactions/transactions.js +29 -21
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.d.ts +7 -0
- package/out/migrator/async.js +168 -0
- package/out/migrator/async.js.map +1 -0
- package/out/migrator/migrations.sbvr +43 -0
- package/out/migrator/sync.d.ts +9 -0
- package/out/migrator/sync.js +106 -0
- package/out/migrator/sync.js.map +1 -0
- package/out/migrator/utils.d.ts +78 -0
- package/out/migrator/utils.js +283 -0
- package/out/migrator/utils.js.map +1 -0
- package/out/odata-metadata/odata-metadata-generator.js +10 -13
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
- package/out/passport-pinejs/passport-pinejs.js +8 -7
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +20 -6
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +3 -2
- package/out/sbvr-api/abstract-sql.js +9 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +1 -1
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/common-types.d.ts +6 -5
- package/out/sbvr-api/control-flow.d.ts +8 -1
- package/out/sbvr-api/control-flow.js +36 -9
- package/out/sbvr-api/control-flow.js.map +1 -1
- package/out/sbvr-api/errors.d.ts +47 -40
- package/out/sbvr-api/errors.js +78 -77
- package/out/sbvr-api/errors.js.map +1 -1
- package/out/sbvr-api/express-extension.d.ts +4 -0
- package/out/sbvr-api/hooks.d.ts +16 -15
- package/out/sbvr-api/hooks.js +74 -48
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.d.ts +2 -2
- package/out/sbvr-api/odata-response.js +28 -30
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.d.ts +17 -16
- package/out/sbvr-api/permissions.js +369 -304
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +33 -15
- package/out/sbvr-api/sbvr-utils.js +397 -235
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +6 -0
- package/out/sbvr-api/translations.js +150 -0
- package/out/sbvr-api/translations.js.map +1 -0
- package/out/sbvr-api/uri-parser.d.ts +23 -17
- package/out/sbvr-api/uri-parser.js +33 -27
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/sbvr-api/user.sbvr +2 -0
- package/out/server-glue/module.d.ts +6 -6
- package/out/server-glue/module.js +4 -2
- package/out/server-glue/module.js.map +1 -1
- package/out/server-glue/server.js +5 -5
- package/out/server-glue/server.js.map +1 -1
- package/package.json +89 -73
- package/pinejs.png +0 -0
- package/repo.yml +9 -9
- package/src/bin/abstract-sql-compiler.ts +5 -7
- package/src/bin/odata-compiler.ts +11 -13
- package/src/bin/sbvr-compiler.ts +11 -17
- package/src/bin/utils.ts +3 -5
- package/src/config-loader/config-loader.ts +167 -53
- package/src/config-loader/env.ts +106 -6
- package/src/data-server/sbvr-server.js +44 -38
- package/src/database-layer/db.ts +205 -64
- package/src/express-emulator/express.js +10 -11
- package/src/http-transactions/transactions.js +29 -21
- package/src/migrator/async.ts +323 -0
- package/src/migrator/migrations.sbvr +43 -0
- package/src/migrator/sync.ts +152 -0
- package/src/migrator/utils.ts +458 -0
- package/src/odata-metadata/odata-metadata-generator.ts +12 -15
- package/src/passport-pinejs/passport-pinejs.ts +9 -7
- package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
- package/src/sbvr-api/abstract-sql.ts +17 -14
- package/src/sbvr-api/common-types.ts +2 -1
- package/src/sbvr-api/control-flow.ts +45 -11
- package/src/sbvr-api/errors.ts +82 -77
- package/src/sbvr-api/express-extension.ts +6 -1
- package/src/sbvr-api/hooks.ts +123 -50
- package/src/sbvr-api/odata-response.ts +23 -28
- package/src/sbvr-api/permissions.ts +548 -415
- package/src/sbvr-api/sbvr-utils.ts +581 -259
- package/src/sbvr-api/translations.ts +248 -0
- package/src/sbvr-api/uri-parser.ts +63 -49
- package/src/sbvr-api/user.sbvr +2 -0
- package/src/server-glue/module.ts +16 -10
- package/src/server-glue/server.ts +5 -5
- package/tsconfig.dev.json +1 -0
- package/tsconfig.json +1 -2
- package/typings/lf-to-abstract-sql.d.ts +6 -9
- package/typings/memoizee.d.ts +1 -1
- package/.github/CODEOWNERS +0 -1
- package/circle.yml +0 -37
- package/docs/todo.txt +0 -22
- package/out/migrator/migrator.d.ts +0 -20
- package/out/migrator/migrator.js +0 -188
- package/out/migrator/migrator.js.map +0 -1
- package/src/migrator/migrator.ts +0 -286
@@ -0,0 +1,323 @@
|
|
1
|
+
import type { Tx } from '../database-layer/db';
|
2
|
+
import type { Model } from '../config-loader/config-loader';
|
3
|
+
|
4
|
+
import * as _ from 'lodash';
|
5
|
+
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
6
|
+
|
7
|
+
type ApiRootModel = Model & { apiRoot: string };
|
8
|
+
|
9
|
+
type InitialMigrationStatus = MigrationStatus &
|
10
|
+
Required<
|
11
|
+
Pick<BaseAsyncMigration, 'backoffDelayMS' | 'delayMS' | 'errorThreshold'>
|
12
|
+
>;
|
13
|
+
|
14
|
+
import {
|
15
|
+
MigrationTuple,
|
16
|
+
getExecutedMigrations,
|
17
|
+
migratorEnv,
|
18
|
+
lockMigrations,
|
19
|
+
initMigrationStatus,
|
20
|
+
readMigrationStatus,
|
21
|
+
updateMigrationStatus,
|
22
|
+
RunnableAsyncMigrations,
|
23
|
+
getRunnableAsyncMigrations,
|
24
|
+
filterAndSortPendingMigrations,
|
25
|
+
MigrationStatus,
|
26
|
+
BaseAsyncMigration,
|
27
|
+
} from './utils';
|
28
|
+
import { booleanToEnabledString } from '../config-loader/env';
|
29
|
+
|
30
|
+
// log the startup condition of the async migration
|
31
|
+
(sbvrUtils.api?.migrations?.logger.info ?? console.info)(
|
32
|
+
`Async migration execution is ${booleanToEnabledString(
|
33
|
+
migratorEnv.asyncMigrationIsEnabled,
|
34
|
+
)}`,
|
35
|
+
);
|
36
|
+
|
37
|
+
export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
|
38
|
+
const { migrations } = model;
|
39
|
+
if (migrations == null || _.isEmpty(migrations)) {
|
40
|
+
return;
|
41
|
+
}
|
42
|
+
const asyncMigrations: RunnableAsyncMigrations | undefined =
|
43
|
+
getRunnableAsyncMigrations(migrations);
|
44
|
+
if (asyncMigrations == null) {
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
|
48
|
+
await $run(tx, model, asyncMigrations);
|
49
|
+
};
|
50
|
+
|
51
|
+
const $run = async (
|
52
|
+
setupTx: Tx,
|
53
|
+
model: ApiRootModel,
|
54
|
+
migrations: RunnableAsyncMigrations,
|
55
|
+
): Promise<void> => {
|
56
|
+
const modelName = model.apiRoot;
|
57
|
+
|
58
|
+
const asyncMigrationSetup: Array<{
|
59
|
+
key: string;
|
60
|
+
initMigrationState: InitialMigrationStatus;
|
61
|
+
asyncRunnerMigratorFn: (tx: Tx) => Promise<number>;
|
62
|
+
}> = [];
|
63
|
+
|
64
|
+
// get a transaction for setting up the async migrator
|
65
|
+
|
66
|
+
const executedMigrations = await getExecutedMigrations(setupTx, modelName);
|
67
|
+
|
68
|
+
// if the model is new, the sync migration parts (marked by finalize=true) are already marked in the sync migration runner.
|
69
|
+
|
70
|
+
/**
|
71
|
+
* preflight check if there are already migrations executed before starting the async scheduler
|
72
|
+
* this will implicitly skip async migrations that have been superseded by synchro migrations.
|
73
|
+
* e.g.:
|
74
|
+
*
|
75
|
+
* sync migrations in repo: [001,002,004,005]
|
76
|
+
* async migrations in repo: [003,006]
|
77
|
+
*
|
78
|
+
* executed migrations at this point should always contain all sync migrations:
|
79
|
+
* executed migrations: [001,002,004,005]
|
80
|
+
*
|
81
|
+
* This will result in only async migration 006 being executed.
|
82
|
+
*
|
83
|
+
* The async migrations are meant to be used in separate deployments to make expensive data migrations
|
84
|
+
* of multiple million row update queries cheaper and with no downtime / long lasting table lock.
|
85
|
+
* In the end, after each async migration, the next deployment should follow up the data migration
|
86
|
+
* with a final sync data migrations.
|
87
|
+
* An async migration will be executed in iterations until no rows are updated anymore (keep row locks short)
|
88
|
+
* then it switches into a backoff mode to check with longer delay if data needs to be migrated in the future.
|
89
|
+
* Example query:
|
90
|
+
* UPDATE tableA
|
91
|
+
* SET columnB = columnA
|
92
|
+
* WHERE id IN (SELECT id
|
93
|
+
* FROM tableA
|
94
|
+
* WHERE (columnA <> columnB) OR (columnA IS NOT NULL AND columnB IS NULL)
|
95
|
+
* LIMIT 1000);
|
96
|
+
*
|
97
|
+
* The final sync data migration would look like:
|
98
|
+
* UPDATE tableA
|
99
|
+
* SET columnB = columnA
|
100
|
+
* WHERE (columnA <> columnB) OR (columnA IS NOT NULL AND columnB IS NULL);
|
101
|
+
*
|
102
|
+
* And will update remaining rows, which ideally are 0 and therefore no rows are locked for the update
|
103
|
+
*
|
104
|
+
* In the case of a column rename the columnA could be safely dropped:
|
105
|
+
* ALTER TABLE tableA
|
106
|
+
* DROP COLUMN IF EXISTS columnA;
|
107
|
+
*/
|
108
|
+
|
109
|
+
const pendingMigrations: MigrationTuple[] = filterAndSortPendingMigrations(
|
110
|
+
migrations,
|
111
|
+
executedMigrations,
|
112
|
+
);
|
113
|
+
|
114
|
+
// Just schedule the migration workers and don't wait for any return of them
|
115
|
+
// the migration workers run until the next deployment and may synchronise with other
|
116
|
+
// instances via database tables: migration lock and migration status
|
117
|
+
|
118
|
+
for (const [key, migration] of pendingMigrations) {
|
119
|
+
let asyncRunnerMigratorFn: (tx: Tx) => Promise<number>;
|
120
|
+
let initMigrationState: InitialMigrationStatus = {
|
121
|
+
migration_key: key,
|
122
|
+
start_time: new Date(),
|
123
|
+
last_run_time: new Date(),
|
124
|
+
run_count: 0,
|
125
|
+
migrated_row_count: 0,
|
126
|
+
error_count: 0,
|
127
|
+
errorThreshold: migratorEnv.asyncMigrationDefaultErrorThreshold,
|
128
|
+
delayMS: migratorEnv.asyncMigrationDefaultDelayMS,
|
129
|
+
backoffDelayMS: migratorEnv.asyncMigrationDefaultBackoffDelayMS,
|
130
|
+
converged_time: undefined,
|
131
|
+
is_backing_off: false,
|
132
|
+
};
|
133
|
+
|
134
|
+
if (typeof migration === 'object') {
|
135
|
+
const batchSize =
|
136
|
+
migration.asyncBatchSize || migratorEnv.asyncMigrationDefaultBatchSize;
|
137
|
+
if (migration.asyncFn && typeof migration.asyncFn === 'function') {
|
138
|
+
asyncRunnerMigratorFn = async (tx: Tx) => {
|
139
|
+
return await migration.asyncFn(
|
140
|
+
tx,
|
141
|
+
{
|
142
|
+
batchSize,
|
143
|
+
},
|
144
|
+
sbvrUtils,
|
145
|
+
);
|
146
|
+
};
|
147
|
+
} else if (migration.asyncSql && typeof migration.asyncSql === 'string') {
|
148
|
+
const asyncMigrationSqlStatement = migration.asyncSql?.replace(
|
149
|
+
'%%ASYNC_BATCH_SIZE%%',
|
150
|
+
`${batchSize}`,
|
151
|
+
);
|
152
|
+
asyncRunnerMigratorFn = async (tx: Tx) =>
|
153
|
+
(await tx.executeSql(asyncMigrationSqlStatement)).rowsAffected;
|
154
|
+
} else {
|
155
|
+
// don't break the async migration b/c of one migration fails
|
156
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
157
|
+
`Invalid migration object: ${JSON.stringify(migration, null, 2)}`,
|
158
|
+
);
|
159
|
+
continue;
|
160
|
+
}
|
161
|
+
|
162
|
+
initMigrationState = {
|
163
|
+
...initMigrationState,
|
164
|
+
..._.pickBy(
|
165
|
+
_.pick(migration, ['backoffDelayMS', 'delayMS', 'errorThreshold']),
|
166
|
+
(value) => value != null,
|
167
|
+
),
|
168
|
+
};
|
169
|
+
} else if (typeof migration === 'string') {
|
170
|
+
asyncRunnerMigratorFn = async (tx: Tx) =>
|
171
|
+
(await tx.executeSql(migration)).rowsAffected;
|
172
|
+
} else {
|
173
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
174
|
+
`Invalid async migration object: ${JSON.stringify(migration, null, 2)}`,
|
175
|
+
);
|
176
|
+
continue;
|
177
|
+
}
|
178
|
+
await initMigrationStatus(setupTx, initMigrationState);
|
179
|
+
|
180
|
+
asyncMigrationSetup.push({
|
181
|
+
key,
|
182
|
+
initMigrationState,
|
183
|
+
asyncRunnerMigratorFn,
|
184
|
+
});
|
185
|
+
}
|
186
|
+
|
187
|
+
// Only after the setupTransaction has successfully finalized the asyncMigration runners will be
|
188
|
+
// created. When the transaction fails, the setup async migration entries in the DB will be
|
189
|
+
// rolled back automatically.
|
190
|
+
setupTx.on('end', () => {
|
191
|
+
for (const {
|
192
|
+
key,
|
193
|
+
initMigrationState,
|
194
|
+
asyncRunnerMigratorFn,
|
195
|
+
} of asyncMigrationSetup) {
|
196
|
+
const asyncRunner = async () => {
|
197
|
+
// don't run the async migration but keep checking
|
198
|
+
if (!migratorEnv.asyncMigrationIsEnabled) {
|
199
|
+
setTimeout(asyncRunner, initMigrationState.backoffDelayMS);
|
200
|
+
return;
|
201
|
+
}
|
202
|
+
try {
|
203
|
+
const $migrationState = await sbvrUtils.db.transaction(
|
204
|
+
async (tx) =>
|
205
|
+
await lockMigrations(
|
206
|
+
{ tx, modelName, blocking: false },
|
207
|
+
async () => {
|
208
|
+
const migrationState = await readMigrationStatus(tx, key);
|
209
|
+
|
210
|
+
if (!migrationState) {
|
211
|
+
// migration status is unclear stop the migrator
|
212
|
+
// or migration should stop
|
213
|
+
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
214
|
+
`stopping async migration due to missing migration status: ${key}`,
|
215
|
+
);
|
216
|
+
return false;
|
217
|
+
}
|
218
|
+
// sync on the last execution time between instances
|
219
|
+
// precondition: All running instances are running on the same time/block
|
220
|
+
// skip execution
|
221
|
+
if (migrationState.last_run_time) {
|
222
|
+
const durationSinceLastRun =
|
223
|
+
Date.now() - migrationState.last_run_time.getTime();
|
224
|
+
const delayMs = migrationState.is_backing_off
|
225
|
+
? initMigrationState.backoffDelayMS
|
226
|
+
: initMigrationState.delayMS;
|
227
|
+
if (durationSinceLastRun < delayMs) {
|
228
|
+
// will still execute finally block where the migration lock is released.
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
}
|
232
|
+
try {
|
233
|
+
// here a separate transaction is needed as this migration may fail
|
234
|
+
// when it fails it would break the transaction for managing the migration status
|
235
|
+
const migratedRows = await sbvrUtils.db.transaction(
|
236
|
+
async (migrationTx) => {
|
237
|
+
// disable automatic close on the management transaction as the migration transaction consumes up to max autoClose time
|
238
|
+
// disable first here, to let if fail when it takes to long before coming here to actually migrate.
|
239
|
+
tx.disableAutomaticClose();
|
240
|
+
const rollbackMigrationTx = async () => {
|
241
|
+
// if the parent transaction fails for any reason, the actual running migration transaction has to be rolled back to stop parallel unsafe async migrations.
|
242
|
+
try {
|
243
|
+
if (!migrationTx.isClosed()) {
|
244
|
+
await migrationTx.rollback();
|
245
|
+
}
|
246
|
+
} catch (err) {
|
247
|
+
(
|
248
|
+
sbvrUtils.api.migrations?.logger.error ??
|
249
|
+
console.error
|
250
|
+
)(
|
251
|
+
`error rolling back pending async migration tx on mgmt tx end/rollback: ${key}: ${err}`,
|
252
|
+
);
|
253
|
+
}
|
254
|
+
};
|
255
|
+
tx.on('rollback', rollbackMigrationTx);
|
256
|
+
tx.on('end', rollbackMigrationTx);
|
257
|
+
return (
|
258
|
+
(await asyncRunnerMigratorFn?.(migrationTx)) ?? 0
|
259
|
+
);
|
260
|
+
},
|
261
|
+
);
|
262
|
+
migrationState.migrated_row_count += migratedRows;
|
263
|
+
if (migratedRows === 0) {
|
264
|
+
// when all rows have been catched up once we only catch up less frequently
|
265
|
+
migrationState.is_backing_off = true;
|
266
|
+
// only store the first time when migrator converged to all data migrated
|
267
|
+
migrationState.converged_time ??= new Date();
|
268
|
+
} else {
|
269
|
+
// Only here for the case that after backoff more rows need to be caught up faster
|
270
|
+
// If rows have been updated recently we start the interval again with normal frequency
|
271
|
+
migrationState.is_backing_off = false;
|
272
|
+
}
|
273
|
+
} catch (err: unknown) {
|
274
|
+
migrationState.error_count++;
|
275
|
+
if (err instanceof Error) {
|
276
|
+
if (
|
277
|
+
migrationState.error_count %
|
278
|
+
initMigrationState.errorThreshold ===
|
279
|
+
0
|
280
|
+
) {
|
281
|
+
(
|
282
|
+
sbvrUtils.api.migrations?.logger.error ??
|
283
|
+
console.error
|
284
|
+
)(`${key}: ${err.name} ${err.message}`);
|
285
|
+
migrationState.is_backing_off = true;
|
286
|
+
}
|
287
|
+
} else {
|
288
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
289
|
+
`async migration error unknown: ${key}: ${err}`,
|
290
|
+
);
|
291
|
+
}
|
292
|
+
} finally {
|
293
|
+
// using finally as it will also run when return statement is called inside the try block
|
294
|
+
// either success or error release the lock
|
295
|
+
migrationState.last_run_time = new Date();
|
296
|
+
migrationState.run_count += 1;
|
297
|
+
await updateMigrationStatus(tx, migrationState);
|
298
|
+
}
|
299
|
+
return migrationState;
|
300
|
+
},
|
301
|
+
),
|
302
|
+
);
|
303
|
+
if ($migrationState === false) {
|
304
|
+
// We've stopped the migration intentionally
|
305
|
+
return;
|
306
|
+
}
|
307
|
+
if ($migrationState == null || $migrationState.is_backing_off) {
|
308
|
+
setTimeout(asyncRunner, initMigrationState.backoffDelayMS);
|
309
|
+
} else {
|
310
|
+
setTimeout(asyncRunner, initMigrationState.delayMS);
|
311
|
+
}
|
312
|
+
} catch (err) {
|
313
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
314
|
+
`error running async migration: ${key}: ${err}`,
|
315
|
+
);
|
316
|
+
setTimeout(asyncRunner, initMigrationState.backoffDelayMS);
|
317
|
+
}
|
318
|
+
};
|
319
|
+
|
320
|
+
setTimeout(asyncRunner, initMigrationState.delayMS);
|
321
|
+
}
|
322
|
+
});
|
323
|
+
};
|
@@ -21,3 +21,46 @@ Term: migration lock
|
|
21
21
|
|
22
22
|
Fact Type: migration lock has model name
|
23
23
|
Necessity: each migration lock has exactly one model name
|
24
|
+
|
25
|
+
Term: migration key
|
26
|
+
Concept Type: Short Text (Type)
|
27
|
+
Term: start time
|
28
|
+
Concept Type: Date Time (Type)
|
29
|
+
Term: last run time
|
30
|
+
Concept Type: Date Time (Type)
|
31
|
+
Term: run count
|
32
|
+
Concept Type: Integer (Type)
|
33
|
+
Term: migrated row count
|
34
|
+
Concept Type: Integer (Type)
|
35
|
+
Term: error count
|
36
|
+
Concept Type: Integer (Type)
|
37
|
+
Term: converged time
|
38
|
+
Concept Type: Date Time (Type)
|
39
|
+
|
40
|
+
|
41
|
+
Term: migration status
|
42
|
+
Reference Scheme: migration key
|
43
|
+
Database ID Field: migration key
|
44
|
+
|
45
|
+
Fact Type: migration status has migration key
|
46
|
+
Necessity: each migration status has exactly one migration key
|
47
|
+
|
48
|
+
Fact Type: migration status has start time
|
49
|
+
Necessity: each migration status has at most one start time
|
50
|
+
|
51
|
+
Fact Type: migration status has last run time
|
52
|
+
Necessity: each migration status has at most one last run time
|
53
|
+
|
54
|
+
Fact Type: migration status has run count
|
55
|
+
Necessity: each migration status has exactly one run count
|
56
|
+
|
57
|
+
Fact Type: migration status has migrated row count
|
58
|
+
Necessity: each migration status has at most one migrated row count
|
59
|
+
|
60
|
+
Fact Type: migration status has error count
|
61
|
+
Necessity: each migration status has at most one error count
|
62
|
+
|
63
|
+
Fact Type: migration status is backing off
|
64
|
+
|
65
|
+
Fact Type: migration status has converged time
|
66
|
+
Necessity: each migration status has at most one converged time
|
@@ -0,0 +1,152 @@
|
|
1
|
+
import {
|
2
|
+
modelText,
|
3
|
+
MigrationTuple,
|
4
|
+
MigrationError,
|
5
|
+
setExecutedMigrations,
|
6
|
+
getExecutedMigrations,
|
7
|
+
lockMigrations,
|
8
|
+
RunnableMigrations,
|
9
|
+
filterAndSortPendingMigrations,
|
10
|
+
getRunnableSyncMigrations,
|
11
|
+
} from './utils';
|
12
|
+
import type { Tx } from '../database-layer/db';
|
13
|
+
import type { Config, Model } from '../config-loader/config-loader';
|
14
|
+
|
15
|
+
import * as _ from 'lodash';
|
16
|
+
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
17
|
+
|
18
|
+
type ApiRootModel = Model & { apiRoot: string };
|
19
|
+
|
20
|
+
export const postRun = async (tx: Tx, model: ApiRootModel): Promise<void> => {
|
21
|
+
const { initSql } = model;
|
22
|
+
if (initSql == null) {
|
23
|
+
return;
|
24
|
+
}
|
25
|
+
|
26
|
+
const modelName = model.apiRoot;
|
27
|
+
const modelIsNew = await sbvrUtils.isModelNew(tx, modelName);
|
28
|
+
if (modelIsNew) {
|
29
|
+
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
30
|
+
`First time executing '${modelName}', running init script`,
|
31
|
+
);
|
32
|
+
|
33
|
+
await lockMigrations({ tx, modelName, blocking: true }, async () => {
|
34
|
+
try {
|
35
|
+
await tx.executeSql(initSql);
|
36
|
+
} catch (err: any) {
|
37
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
38
|
+
`initSql execution error ${err} `,
|
39
|
+
);
|
40
|
+
throw new MigrationError(err);
|
41
|
+
}
|
42
|
+
});
|
43
|
+
}
|
44
|
+
};
|
45
|
+
|
46
|
+
export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
|
47
|
+
const { migrations } = model;
|
48
|
+
if (migrations == null || _.isEmpty(migrations)) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
const runnableMigrations = getRunnableSyncMigrations(migrations);
|
52
|
+
return $run(tx, model, runnableMigrations);
|
53
|
+
};
|
54
|
+
|
55
|
+
const $run = async (
|
56
|
+
tx: Tx,
|
57
|
+
model: ApiRootModel,
|
58
|
+
migrations: RunnableMigrations,
|
59
|
+
): Promise<void> => {
|
60
|
+
const modelName = model.apiRoot;
|
61
|
+
|
62
|
+
// migrations only run if the model has been executed before,
|
63
|
+
// to make changes that can't be automatically applied
|
64
|
+
const modelIsNew = await sbvrUtils.isModelNew(tx, modelName);
|
65
|
+
if (modelIsNew) {
|
66
|
+
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
67
|
+
`First time model '${modelName}' has executed, skipping migrations`,
|
68
|
+
);
|
69
|
+
|
70
|
+
return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
|
71
|
+
}
|
72
|
+
await lockMigrations({ tx, modelName, blocking: true }, async () => {
|
73
|
+
try {
|
74
|
+
const executedMigrations = await getExecutedMigrations(tx, modelName);
|
75
|
+
const pendingMigrations = filterAndSortPendingMigrations(
|
76
|
+
migrations,
|
77
|
+
executedMigrations,
|
78
|
+
);
|
79
|
+
if (pendingMigrations.length === 0) {
|
80
|
+
return;
|
81
|
+
}
|
82
|
+
|
83
|
+
const newlyExecutedMigrations = await executeMigrations(
|
84
|
+
tx,
|
85
|
+
pendingMigrations,
|
86
|
+
);
|
87
|
+
await setExecutedMigrations(tx, modelName, [
|
88
|
+
...executedMigrations,
|
89
|
+
...newlyExecutedMigrations,
|
90
|
+
]);
|
91
|
+
} catch (err: any) {
|
92
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
93
|
+
`Failed to executed synchronous migrations from api root model ${err}`,
|
94
|
+
);
|
95
|
+
throw new MigrationError(err);
|
96
|
+
}
|
97
|
+
});
|
98
|
+
};
|
99
|
+
|
100
|
+
const executeMigrations = async (
|
101
|
+
tx: Tx,
|
102
|
+
migrations: MigrationTuple[] = [],
|
103
|
+
): Promise<string[]> => {
|
104
|
+
try {
|
105
|
+
for (const migration of migrations) {
|
106
|
+
await executeMigration(tx, migration);
|
107
|
+
}
|
108
|
+
} catch (err: any) {
|
109
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
110
|
+
'Error while executing migrations, rolled back',
|
111
|
+
);
|
112
|
+
throw new MigrationError(err);
|
113
|
+
}
|
114
|
+
return migrations.map(([migrationKey]) => migrationKey); // return migration keys
|
115
|
+
};
|
116
|
+
|
117
|
+
const executeMigration = async (
|
118
|
+
tx: Tx,
|
119
|
+
[key, migration]: MigrationTuple,
|
120
|
+
): Promise<void> => {
|
121
|
+
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
122
|
+
`Running migration ${JSON.stringify(key)}`,
|
123
|
+
);
|
124
|
+
|
125
|
+
if (typeof migration === 'function') {
|
126
|
+
await migration(tx, sbvrUtils);
|
127
|
+
} else if (typeof migration === 'string') {
|
128
|
+
await tx.executeSql(migration);
|
129
|
+
} else {
|
130
|
+
throw new MigrationError(`Invalid migration type: ${typeof migration}`);
|
131
|
+
}
|
132
|
+
};
|
133
|
+
|
134
|
+
export const config: Config = {
|
135
|
+
models: [
|
136
|
+
{
|
137
|
+
modelName: 'migrations',
|
138
|
+
apiRoot: 'migrations',
|
139
|
+
modelText,
|
140
|
+
migrations: {
|
141
|
+
'11.0.0-modified-at': `
|
142
|
+
ALTER TABLE "migration"
|
143
|
+
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
144
|
+
`,
|
145
|
+
'11.0.1-modified-at': `
|
146
|
+
ALTER TABLE "migration lock"
|
147
|
+
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
148
|
+
`,
|
149
|
+
},
|
150
|
+
},
|
151
|
+
],
|
152
|
+
};
|