@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.
Files changed (145) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/workflows/flowzone.yml +21 -0
  3. package/.husky/pre-commit +4 -0
  4. package/.pinejs-cache.json +1 -0
  5. package/.resinci.yml +1 -0
  6. package/.versionbot/CHANGELOG.yml +9678 -2001
  7. package/CHANGELOG.md +2975 -2
  8. package/Dockerfile +14 -0
  9. package/Gruntfile.ts +3 -6
  10. package/README.md +10 -1
  11. package/VERSION +1 -0
  12. package/build/browser.ts +1 -1
  13. package/build/config.ts +0 -1
  14. package/docker-compose.npm-test.yml +11 -0
  15. package/docs/AdvancedUsage.md +77 -63
  16. package/docs/GettingStarted.md +90 -41
  17. package/docs/Migrations.md +102 -1
  18. package/docs/ProjectConfig.md +12 -21
  19. package/docs/Testing.md +7 -0
  20. package/out/bin/abstract-sql-compiler.js +17 -17
  21. package/out/bin/abstract-sql-compiler.js.map +1 -1
  22. package/out/bin/odata-compiler.js +23 -20
  23. package/out/bin/odata-compiler.js.map +1 -1
  24. package/out/bin/sbvr-compiler.js +22 -22
  25. package/out/bin/sbvr-compiler.js.map +1 -1
  26. package/out/bin/utils.d.ts +2 -2
  27. package/out/bin/utils.js +3 -3
  28. package/out/bin/utils.js.map +1 -1
  29. package/out/config-loader/config-loader.d.ts +9 -8
  30. package/out/config-loader/config-loader.js +135 -78
  31. package/out/config-loader/config-loader.js.map +1 -1
  32. package/out/config-loader/env.d.ts +41 -16
  33. package/out/config-loader/env.js +46 -2
  34. package/out/config-loader/env.js.map +1 -1
  35. package/out/data-server/sbvr-server.d.ts +2 -19
  36. package/out/data-server/sbvr-server.js +44 -38
  37. package/out/data-server/sbvr-server.js.map +1 -1
  38. package/out/database-layer/db.d.ts +32 -14
  39. package/out/database-layer/db.js +120 -41
  40. package/out/database-layer/db.js.map +1 -1
  41. package/out/express-emulator/express.js +10 -11
  42. package/out/express-emulator/express.js.map +1 -1
  43. package/out/http-transactions/transactions.d.ts +2 -18
  44. package/out/http-transactions/transactions.js +29 -21
  45. package/out/http-transactions/transactions.js.map +1 -1
  46. package/out/migrator/async.d.ts +7 -0
  47. package/out/migrator/async.js +168 -0
  48. package/out/migrator/async.js.map +1 -0
  49. package/out/migrator/migrations.sbvr +43 -0
  50. package/out/migrator/sync.d.ts +9 -0
  51. package/out/migrator/sync.js +106 -0
  52. package/out/migrator/sync.js.map +1 -0
  53. package/out/migrator/utils.d.ts +78 -0
  54. package/out/migrator/utils.js +283 -0
  55. package/out/migrator/utils.js.map +1 -0
  56. package/out/odata-metadata/odata-metadata-generator.js +10 -13
  57. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  58. package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
  59. package/out/passport-pinejs/passport-pinejs.js +8 -7
  60. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  61. package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
  62. package/out/pinejs-session-store/pinejs-session-store.js +20 -6
  63. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  64. package/out/sbvr-api/abstract-sql.d.ts +3 -2
  65. package/out/sbvr-api/abstract-sql.js +9 -9
  66. package/out/sbvr-api/abstract-sql.js.map +1 -1
  67. package/out/sbvr-api/cached-compile.js +1 -1
  68. package/out/sbvr-api/cached-compile.js.map +1 -1
  69. package/out/sbvr-api/common-types.d.ts +6 -5
  70. package/out/sbvr-api/control-flow.d.ts +8 -1
  71. package/out/sbvr-api/control-flow.js +36 -9
  72. package/out/sbvr-api/control-flow.js.map +1 -1
  73. package/out/sbvr-api/errors.d.ts +47 -40
  74. package/out/sbvr-api/errors.js +78 -77
  75. package/out/sbvr-api/errors.js.map +1 -1
  76. package/out/sbvr-api/express-extension.d.ts +4 -0
  77. package/out/sbvr-api/hooks.d.ts +16 -15
  78. package/out/sbvr-api/hooks.js +74 -48
  79. package/out/sbvr-api/hooks.js.map +1 -1
  80. package/out/sbvr-api/odata-response.d.ts +2 -2
  81. package/out/sbvr-api/odata-response.js +28 -30
  82. package/out/sbvr-api/odata-response.js.map +1 -1
  83. package/out/sbvr-api/permissions.d.ts +17 -16
  84. package/out/sbvr-api/permissions.js +369 -304
  85. package/out/sbvr-api/permissions.js.map +1 -1
  86. package/out/sbvr-api/sbvr-utils.d.ts +33 -15
  87. package/out/sbvr-api/sbvr-utils.js +397 -235
  88. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  89. package/out/sbvr-api/translations.d.ts +6 -0
  90. package/out/sbvr-api/translations.js +150 -0
  91. package/out/sbvr-api/translations.js.map +1 -0
  92. package/out/sbvr-api/uri-parser.d.ts +23 -17
  93. package/out/sbvr-api/uri-parser.js +33 -27
  94. package/out/sbvr-api/uri-parser.js.map +1 -1
  95. package/out/sbvr-api/user.sbvr +2 -0
  96. package/out/server-glue/module.d.ts +6 -6
  97. package/out/server-glue/module.js +4 -2
  98. package/out/server-glue/module.js.map +1 -1
  99. package/out/server-glue/server.js +5 -5
  100. package/out/server-glue/server.js.map +1 -1
  101. package/package.json +89 -73
  102. package/pinejs.png +0 -0
  103. package/repo.yml +9 -9
  104. package/src/bin/abstract-sql-compiler.ts +5 -7
  105. package/src/bin/odata-compiler.ts +11 -13
  106. package/src/bin/sbvr-compiler.ts +11 -17
  107. package/src/bin/utils.ts +3 -5
  108. package/src/config-loader/config-loader.ts +167 -53
  109. package/src/config-loader/env.ts +106 -6
  110. package/src/data-server/sbvr-server.js +44 -38
  111. package/src/database-layer/db.ts +205 -64
  112. package/src/express-emulator/express.js +10 -11
  113. package/src/http-transactions/transactions.js +29 -21
  114. package/src/migrator/async.ts +323 -0
  115. package/src/migrator/migrations.sbvr +43 -0
  116. package/src/migrator/sync.ts +152 -0
  117. package/src/migrator/utils.ts +458 -0
  118. package/src/odata-metadata/odata-metadata-generator.ts +12 -15
  119. package/src/passport-pinejs/passport-pinejs.ts +9 -7
  120. package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
  121. package/src/sbvr-api/abstract-sql.ts +17 -14
  122. package/src/sbvr-api/common-types.ts +2 -1
  123. package/src/sbvr-api/control-flow.ts +45 -11
  124. package/src/sbvr-api/errors.ts +82 -77
  125. package/src/sbvr-api/express-extension.ts +6 -1
  126. package/src/sbvr-api/hooks.ts +123 -50
  127. package/src/sbvr-api/odata-response.ts +23 -28
  128. package/src/sbvr-api/permissions.ts +548 -415
  129. package/src/sbvr-api/sbvr-utils.ts +581 -259
  130. package/src/sbvr-api/translations.ts +248 -0
  131. package/src/sbvr-api/uri-parser.ts +63 -49
  132. package/src/sbvr-api/user.sbvr +2 -0
  133. package/src/server-glue/module.ts +16 -10
  134. package/src/server-glue/server.ts +5 -5
  135. package/tsconfig.dev.json +1 -0
  136. package/tsconfig.json +1 -2
  137. package/typings/lf-to-abstract-sql.d.ts +6 -9
  138. package/typings/memoizee.d.ts +1 -1
  139. package/.github/CODEOWNERS +0 -1
  140. package/circle.yml +0 -37
  141. package/docs/todo.txt +0 -22
  142. package/out/migrator/migrator.d.ts +0 -20
  143. package/out/migrator/migrator.js +0 -188
  144. package/out/migrator/migrator.js.map +0 -1
  145. 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
+ };