@balena/pinejs 14.44.0-linear-runtime-migrator-e15bea04e85fb013aeed7d4770052b51747a619c → 15.0.0-delete-state-default-user-permissions-ba0732a0c5d0da9d1d5be818cb08cc898e86ebe3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/.versionbot/CHANGELOG.yml +24 -45
  2. package/CHANGELOG.md +8 -6
  3. package/VERSION +1 -1
  4. package/docs/Migrations.md +1 -101
  5. package/out/config-loader/config-loader.d.ts +4 -2
  6. package/out/config-loader/config-loader.js +20 -35
  7. package/out/config-loader/config-loader.js.map +1 -1
  8. package/out/config-loader/env.d.ts +0 -3
  9. package/out/config-loader/env.js +0 -3
  10. package/out/config-loader/env.js.map +1 -1
  11. package/out/migrator/migrations.sbvr +0 -66
  12. package/out/migrator/migrator.d.ts +17 -0
  13. package/out/migrator/migrator.js +185 -0
  14. package/out/migrator/migrator.js.map +1 -0
  15. package/out/sbvr-api/sbvr-utils.d.ts +1 -3
  16. package/out/sbvr-api/sbvr-utils.js +4 -13
  17. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  18. package/out/server-glue/module.d.ts +2 -2
  19. package/out/server-glue/module.js +1 -2
  20. package/out/server-glue/module.js.map +1 -1
  21. package/package.json +3 -3
  22. package/src/config-loader/config-loader.ts +26 -73
  23. package/src/config-loader/env.ts +0 -3
  24. package/src/migrator/migrations.sbvr +0 -66
  25. package/src/migrator/migrator.ts +278 -0
  26. package/src/sbvr-api/sbvr-utils.ts +3 -18
  27. package/src/server-glue/module.ts +2 -3
  28. package/out/migrator/async.d.ts +0 -6
  29. package/out/migrator/async.js +0 -160
  30. package/out/migrator/async.js.map +0 -1
  31. package/out/migrator/sync.d.ts +0 -9
  32. package/out/migrator/sync.js +0 -126
  33. package/out/migrator/sync.js.map +0 -1
  34. package/out/migrator/utils.d.ts +0 -56
  35. package/out/migrator/utils.js +0 -187
  36. package/out/migrator/utils.js.map +0 -1
  37. package/src/migrator/async.ts +0 -279
  38. package/src/migrator/sync.ts +0 -177
  39. package/src/migrator/utils.ts +0 -296
@@ -1,279 +0,0 @@
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
- import {
10
- MigrationTuple,
11
- AsyncMigrationFn,
12
- MigrationCategories,
13
- checkModelAlreadyExists,
14
- setExecutedMigrations,
15
- getExecutedMigrations,
16
- migratorEnv,
17
- lockMigrations,
18
- initMigrationStatus,
19
- readMigrationStatus,
20
- updateMigrationStatus,
21
- RunnableMigrations,
22
- } from './utils';
23
-
24
- export const run = async (model: ApiRootModel): Promise<void> => {
25
- const { migrations } = model;
26
- if (
27
- migrations == null ||
28
- _.isEmpty(migrations) ||
29
- migrations[MigrationCategories.async] === undefined
30
- ) {
31
- return;
32
- }
33
- let runMigrations: RunnableMigrations = {};
34
- for (const [key, value] of Object.entries(migrations)) {
35
- if (key === MigrationCategories.async && value instanceof Object) {
36
- runMigrations = { ...runMigrations, ...value };
37
- }
38
- }
39
- await $run(model, runMigrations);
40
- };
41
-
42
- const $run = async (
43
- model: ApiRootModel,
44
- migrations: RunnableMigrations,
45
- ): Promise<void> => {
46
- const modelName = model.apiRoot;
47
-
48
- // init migrations
49
- let exists;
50
- await sbvrUtils.db.transaction(async (tx) => {
51
- exists = await checkModelAlreadyExists(tx, modelName);
52
- if (!exists) {
53
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
54
- 'First time model has executed, skipping migrations',
55
- );
56
-
57
- return await setExecutedMigrations(
58
- tx,
59
- modelName,
60
- Object.keys(migrations),
61
- );
62
- }
63
- });
64
-
65
- if (!exists) {
66
- return;
67
- }
68
-
69
- /**
70
- * preflight check if there are already migrations executed before starting the async scheduler
71
- * this will impplicitly skip async migrations that have been superceeded by synchron migrations.
72
- * e.g.:
73
- *
74
- * sync migrations in repo: [001,002,004,005]
75
- * async migrations in repo: [003,006]
76
- *
77
- * executed migrations at this point should always contain all sync migrations:
78
- * executed migrations: [001,002,004,005]
79
- *
80
- * This will result in only async migration 006 being executed.
81
- *
82
- * The async migrations are ment to be used in seperate deployments to make expensive data migrations
83
- * of multiple million row update queries cheaper and with no downtime / long lasting table lock.
84
- * In the end, after each async migration, the next deploymemnt should follow up the data migration
85
- * with a final sync data migrations.
86
- * A async migration will be executed in iterations until no rows are updated anymore (keep row locks short)
87
- * then it switches into a backoff mode to check with longer delay if data needs to be migrated in the future.
88
- * Example query:
89
- * UPDATE tableA
90
- * SET columnB = columnA
91
- * WHERE id IN (SELECT id
92
- * FROM tableA
93
- * WHERE (columnA <> columnB) OR (columnA IS NOT NULL AND columnB IS NULL)
94
- * LIMIT 1000);
95
- *
96
- * The final sync data migration would look like:
97
- * UPDATE tableA
98
- * SET columnB = columnA
99
- * WHERE (columnA <> columnB) OR (columnA IS NOT NULL AND columnB IS NULL);
100
- *
101
- * And will update remaining rows, which ideally are 0 and therefore now rows are locked for the update
102
- *
103
- * In the case of a column rename the columnA could be savely dropped:
104
- * ALTER TABLE tableA
105
- * DROP COLUMN IF EXISTS columnA;
106
- */
107
-
108
- let pendingMigrations: MigrationTuple[] = [];
109
- await sbvrUtils.db.transaction(async (tx) => {
110
- const executedMigrations = await getExecutedMigrations(tx, modelName);
111
- pendingMigrations = filterAndSortPendingAsyncMigrations(
112
- migrations,
113
- executedMigrations,
114
- );
115
- });
116
-
117
- // Just schedule the migration workers and don't wait for any return of them
118
- // the migration workers run until the next deployment and may synchronise with other
119
- // instances via database tables: migration lock and migration status
120
- for (const [key, migration] of pendingMigrations) {
121
- const initMigrationState = {
122
- migration_key: key,
123
- start_time: new Date(Date.now()),
124
- last_run_time: new Date(Date.now()),
125
- run_counter: 0,
126
- migrated_rows: 0,
127
- error_counter: 0,
128
- error_threshold: migratorEnv.asyncMigrationDefaultErrorThreshold,
129
- delayMS: migratorEnv.asyncMigrationDefaultDelayMS,
130
- backoffDelayMS: migratorEnv.asyncMigrationDefaultBackoffDelayMS,
131
- converged_time: undefined,
132
- last_error_message: undefined,
133
- is_backoff: false,
134
- should_stop: false,
135
- };
136
-
137
- let asyncRunnerMigratorFn: ((tx: Tx) => Promise<number>) | undefined;
138
-
139
- if (typeof migration === 'object') {
140
- if (migration.fn && typeof migration.fn === 'function') {
141
- asyncRunnerMigratorFn = async (tx: Tx) =>
142
- (await (migration.fn as AsyncMigrationFn)(tx, sbvrUtils))
143
- .rowsAffected;
144
- } else if (migration.sql && typeof migration.sql === 'string') {
145
- asyncRunnerMigratorFn = async (tx: Tx) =>
146
- (await tx.executeSql(migration.sql as string)).rowsAffected;
147
- } else {
148
- // don't break the async migration b/c of one migration fails
149
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
150
- `Invalid migration object: ${JSON.stringify(migration, null, 2)}`,
151
- );
152
- continue;
153
- }
154
-
155
- initMigrationState.backoffDelayMS =
156
- migration.backoffDelayMS ||
157
- migratorEnv.asyncMigrationDefaultBackoffDelayMS;
158
- initMigrationState.delayMS =
159
- migration.delayMS || migratorEnv.asyncMigrationDefaultDelayMS;
160
- initMigrationState.error_threshold =
161
- migration.errorThreshold ||
162
- migratorEnv.asyncMigrationDefaultErrorThreshold;
163
- } else if (typeof migration === 'string') {
164
- asyncRunnerMigratorFn = async (tx: Tx) =>
165
- (await tx.executeSql(migration)).rowsAffected;
166
- } else {
167
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
168
- `Invalid async migration object: ${JSON.stringify(migration, null, 2)}`,
169
- );
170
- continue;
171
- }
172
-
173
- await sbvrUtils.db.transaction(async (tx) =>
174
- initMigrationStatus(tx, initMigrationState),
175
- );
176
-
177
- const asyncRunner = async () => {
178
- await sbvrUtils.db.transaction(async (tx) => {
179
- await lockMigrations(tx, modelName, async () => {
180
- const migrationState = await readMigrationStatus(tx, key);
181
-
182
- if (!migrationState || migrationState.should_stop === true) {
183
- // migration status is unclear stop the migrator
184
- // or migration should stop
185
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
186
- `stopping async migration: ${key}`,
187
- );
188
- clearInterval(asyncScheduler);
189
- return;
190
- }
191
- try {
192
- // clear the interval to avoid retriggering before finisheing this migraiton query
193
- clearInterval(asyncScheduler);
194
-
195
- // sync on the last execution time between instances
196
- // precondition: All running instances are running on the same time/block
197
- // skip execution
198
- if (migrationState.last_run_time) {
199
- const durationSinceLastRun =
200
- Date.now().valueOf() - migrationState.last_run_time.valueOf();
201
- if (
202
- (migrationState.is_backoff &&
203
- durationSinceLastRun < migrationState.backoffDelayMS) ||
204
- (!migrationState.is_backoff &&
205
- durationSinceLastRun < migrationState.delayMS)
206
- ) {
207
- // will still execute finally block where the migration lock is released.
208
- return;
209
- }
210
- }
211
- // set last run time and run counter only when backoff time sync between
212
- // competing instances is in sync
213
- migrationState.last_run_time = new Date(Date.now());
214
- migrationState.run_counter += 1;
215
-
216
- let migratedRows = 0;
217
- await sbvrUtils.db.transaction(async (migrationTx) => {
218
- migratedRows = (await asyncRunnerMigratorFn?.(migrationTx)) || 0;
219
- });
220
-
221
- migrationState.migrated_rows += migratedRows;
222
- if (migratedRows === 0) {
223
- // when all rows have been catched up once we only catch up less frequently
224
- migrationState.is_backoff = true;
225
- if (!migrationState.converged_time) {
226
- // only store the first time when migrator converged to all data migrated
227
- migrationState.converged_time = new Date(Date.now());
228
- }
229
- } else {
230
- // Only here for the case that after backoff more rows need to be catched up faster
231
- // If rows have been updated recently we start the interval again with normal frequency
232
- migrationState.is_backoff = false;
233
- }
234
- } catch (err) {
235
- migrationState.error_counter++;
236
-
237
- if (
238
- migrationState.error_counter % migrationState.error_threshold ===
239
- 0
240
- ) {
241
- migrationState.last_error_message = `${err.name} ${err.message}`;
242
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
243
- `${key}: ${err.name} ${err.message}`,
244
- );
245
- migrationState.is_backoff = true;
246
- }
247
- } finally {
248
- if (migrationState.is_backoff) {
249
- asyncScheduler = setInterval(
250
- asyncRunner,
251
- migrationState.backoffDelayMS,
252
- );
253
- } else {
254
- asyncScheduler = setInterval(asyncRunner, migrationState.delayMS);
255
- }
256
- // using finally as it will also run when return statement is called inside the try block
257
- // either success or error release the lock
258
- await updateMigrationStatus(tx, migrationState);
259
- }
260
- });
261
- });
262
- };
263
- let asyncScheduler = setInterval(asyncRunner, initMigrationState.delayMS);
264
- }
265
- };
266
-
267
- const filterAndSortPendingAsyncMigrations = (
268
- migrations: NonNullable<RunnableMigrations>,
269
- executedMigrations: string[],
270
- ): MigrationTuple[] => {
271
- // using it for string comparison, any string is greater than ''
272
- const latestExecutedMigration = executedMigrations.sort().pop() ?? '';
273
-
274
- return (_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
275
- .toPairs()
276
- .filter(([migrationKey]) => migrationKey > latestExecutedMigration)
277
- .sortBy(([migrationKey]) => migrationKey)
278
- .value();
279
- };
@@ -1,177 +0,0 @@
1
- import {
2
- modelText,
3
- MigrationTuple,
4
- MigrationError,
5
- defaultMigrationCategory,
6
- checkModelAlreadyExists,
7
- setExecutedMigrations,
8
- getExecutedMigrations,
9
- lockMigrations,
10
- MigrationCategories,
11
- RunnableMigrations,
12
- } from './utils';
13
- import type { Tx } from '../database-layer/db';
14
- import type { Config, Model } from '../config-loader/config-loader';
15
-
16
- import * as _ from 'lodash';
17
- import * as sbvrUtils from '../sbvr-api/sbvr-utils';
18
-
19
- type ApiRootModel = Model & { apiRoot: string };
20
-
21
- export const postRun = async (tx: Tx, model: ApiRootModel): Promise<void> => {
22
- const { initSql } = model;
23
- if (initSql == null) {
24
- return;
25
- }
26
-
27
- const modelName = model.apiRoot;
28
-
29
- const exists = await checkModelAlreadyExists(tx, modelName);
30
- if (!exists) {
31
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
32
- 'First time executing, running init script',
33
- );
34
-
35
- await lockMigrations(tx, modelName, async () => {
36
- try {
37
- await tx.executeSql(initSql);
38
- } catch (err) {
39
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
40
- `postRun locked sql execution error ${err} `,
41
- );
42
- throw new MigrationError(err);
43
- }
44
- });
45
- }
46
- };
47
-
48
- export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
49
- const { migrations } = model;
50
- if (
51
- migrations == null ||
52
- _.isEmpty(migrations) ||
53
- migrations[defaultMigrationCategory] === undefined
54
- ) {
55
- return;
56
- }
57
-
58
- let runMigrations: RunnableMigrations = {};
59
- for (const [key, value] of Object.entries(migrations)) {
60
- if (key === defaultMigrationCategory && value instanceof Object) {
61
- runMigrations = { ...runMigrations, ...value };
62
- } else if (!(key in MigrationCategories)) {
63
- runMigrations = { ...runMigrations, ...{ [key]: value } };
64
- }
65
- }
66
- return $run(tx, model, runMigrations);
67
- };
68
-
69
- const $run = async (
70
- tx: Tx,
71
- model: ApiRootModel,
72
- migrations: RunnableMigrations,
73
- ): Promise<void> => {
74
- const modelName = model.apiRoot;
75
-
76
- // migrations only run if the model has been executed before,
77
- // to make changes that can't be automatically applied
78
- const exists = await checkModelAlreadyExists(tx, modelName);
79
- if (!exists) {
80
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
81
- 'First time model has executed, skipping migrations',
82
- );
83
-
84
- return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
85
- }
86
- await lockMigrations(tx, modelName, async () => {
87
- try {
88
- const executedMigrations = await getExecutedMigrations(tx, modelName);
89
- const pendingMigrations = filterAndSortPendingMigrations(
90
- migrations,
91
- executedMigrations,
92
- );
93
- if (pendingMigrations.length === 0) {
94
- return;
95
- }
96
-
97
- const newlyExecutedMigrations = await executeMigrations(
98
- tx,
99
- pendingMigrations,
100
- );
101
- await setExecutedMigrations(tx, modelName, [
102
- ...executedMigrations,
103
- ...newlyExecutedMigrations,
104
- ]);
105
- } catch (err) {
106
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
107
- `$run executedMigrations error ${err}`,
108
- );
109
- throw new MigrationError(err);
110
- }
111
- });
112
- };
113
-
114
- // turns {"key1": migration, "key3": migration, "key2": migration}
115
- // into [["key1", migration], ["key2", migration], ["key3", migration]]
116
- const filterAndSortPendingMigrations = (
117
- migrations: NonNullable<RunnableMigrations>,
118
- executedMigrations: string[],
119
- ): MigrationTuple[] =>
120
- (_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
121
- .toPairs()
122
- .sortBy(([migrationKey]) => migrationKey)
123
- .value();
124
-
125
- const executeMigrations = async (
126
- tx: Tx,
127
- migrations: MigrationTuple[] = [],
128
- ): Promise<string[]> => {
129
- try {
130
- for (const migration of migrations) {
131
- await executeMigration(tx, migration);
132
- }
133
- } catch (err) {
134
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
135
- 'Error while executing migrations, rolled back',
136
- );
137
- throw new MigrationError(err);
138
- }
139
- return migrations.map(([migrationKey]) => migrationKey); // return migration keys
140
- };
141
-
142
- const executeMigration = async (
143
- tx: Tx,
144
- [key, migration]: MigrationTuple,
145
- ): Promise<void> => {
146
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
147
- `Running migration ${JSON.stringify(key)}`,
148
- );
149
-
150
- if (typeof migration === 'function') {
151
- await migration(tx, sbvrUtils);
152
- } else if (typeof migration === 'string') {
153
- await tx.executeSql(migration);
154
- } else {
155
- throw new MigrationError(`Invalid migration type: ${typeof migration}`);
156
- }
157
- };
158
-
159
- export const config: Config = {
160
- models: [
161
- {
162
- modelName: 'migrations',
163
- apiRoot: 'migrations',
164
- modelText,
165
- migrations: {
166
- '11.0.0-modified-at': `
167
- ALTER TABLE "migration"
168
- ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
169
- `,
170
- '11.0.1-modified-at': `
171
- ALTER TABLE "migration lock"
172
- ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
173
- `,
174
- },
175
- },
176
- ],
177
- };