@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,16 +1,9 @@
1
1
  import type * as Express from 'express';
2
2
  import type { AbstractSqlModel } from '@balena/abstract-sql-compiler';
3
3
  import type { Database } from '../database-layer/db';
4
+ import type { Migration } from '../migrator/migrator';
4
5
  import type { AnyObject, Resolvable } from '../sbvr-api/common-types';
5
6
 
6
- import {
7
- Migration,
8
- Migrations,
9
- defaultMigrationCategory,
10
- MigrationCategories,
11
- MigrationCategory,
12
- } from '../migrator/utils';
13
-
14
7
  import * as fs from 'fs';
15
8
  import * as _ from 'lodash';
16
9
  import * as path from 'path';
@@ -32,7 +25,9 @@ export interface Model {
32
25
  modelText?: string;
33
26
  abstractSql?: AbstractSqlModel;
34
27
  migrationsPath?: string;
35
- migrations?: Migrations;
28
+ migrations?: {
29
+ [index: string]: Migration;
30
+ };
36
31
  initSqlPath?: string;
37
32
  initSql?: string;
38
33
  customServerCode?:
@@ -133,15 +128,31 @@ export const setup = (app: Express.Application) => {
133
128
  },
134
129
  );
135
130
  if (user.permissions != null) {
136
- await Promise.all(
131
+ const permissionIds = await Promise.all(
137
132
  user.permissions.map(async (permissionName) => {
138
133
  const permissionID = await permissionsCache[permissionName];
139
134
  await getOrCreate(authApiTx, 'user__has__permission', {
140
135
  user: userID,
141
136
  permission: permissionID,
142
137
  });
138
+ return permissionID;
143
139
  }),
144
140
  );
141
+ if (permissionIds.length > 0) {
142
+ await authApiTx.delete({
143
+ resource: 'user__has__permission',
144
+ options: {
145
+ $filter: {
146
+ user: userID,
147
+ $not: {
148
+ permission: {
149
+ $in: permissionIds,
150
+ },
151
+ },
152
+ },
153
+ },
154
+ });
155
+ }
145
156
  }
146
157
  } catch (e) {
147
158
  e.message = `Could not create or find user "${user.username}": ${e.message}`;
@@ -256,62 +267,18 @@ export const setup = (app: Express.Application) => {
256
267
  await Promise.all(
257
268
  fileNames.map(async (filename) => {
258
269
  const filePath = path.join(migrationsPath, filename);
259
- const fileNameParts = filename.split('.', 3);
260
- const fileExtension = path.extname(filename);
261
270
  const [migrationKey] = filename.split('-', 1);
262
- let migrationCategory: MigrationCategory =
263
- defaultMigrationCategory;
264
-
265
- if (fileNameParts.length === 3) {
266
- if (fileNameParts[1] in MigrationCategories) {
267
- migrationCategory = fileNameParts[1] as MigrationCategory;
268
- } else {
269
- console.error(
270
- `Unrecognised migration file category ${
271
- fileNameParts[1]
272
- }, skipping: ${path.extname(filename)}`,
273
- );
274
- return;
275
- }
276
- }
277
271
 
278
- /**
279
- * helper to assign migrations with category level to model
280
- * example migration file names:
281
- *
282
- * key0-name.ts ==> defaults startup migration
283
- * key1-name1.sql ==> defaults startup migration
284
- * key2-name2.sync.sql ==> explicit synchrony migration
285
- * key3-name3.async.ts ==> async migration (async datafiller)
286
- * key4-name4.async.sql ==> async migration (async datafiller)
287
- *
288
- */
289
- const assignMigrationWithCategory = (
290
- newMigrationKey: string,
291
- newMigration: Migration,
292
- ) => {
293
- const catMigrations = migrations[migrationCategory] || {};
294
- if (typeof catMigrations === 'object') {
295
- migrations[migrationCategory] = {
296
- [newMigrationKey]: newMigration,
297
- ...catMigrations,
298
- };
299
- }
300
- };
301
-
302
- switch (fileExtension) {
272
+ switch (path.extname(filename)) {
303
273
  case '.coffee':
304
274
  case '.ts':
305
275
  case '.js':
306
- assignMigrationWithCategory(
307
- migrationKey,
308
- nodeRequire(filePath),
309
- );
276
+ migrations[migrationKey] = nodeRequire(filePath);
310
277
  break;
311
278
  case '.sql':
312
- assignMigrationWithCategory(
313
- migrationKey,
314
- await fs.promises.readFile(filePath, 'utf8'),
279
+ migrations[migrationKey] = await fs.promises.readFile(
280
+ filePath,
281
+ 'utf8',
315
282
  );
316
283
  break;
317
284
  default:
@@ -331,26 +298,12 @@ export const setup = (app: Express.Application) => {
331
298
  }),
332
299
  );
333
300
  await loadConfig(configObj);
334
- runAsyncMigrations(configObj); // async migrations will run in background - nothing to wait for
335
301
  } catch (err) {
336
302
  console.error('Error loading application config', err, err.stack);
337
303
  process.exit(1);
338
304
  }
339
305
  };
340
306
 
341
- const runAsyncMigrations = (data: Config): void => {
342
- for (const model of data.models) {
343
- if (
344
- model.migrations != null &&
345
- Object.keys(model.migrations).includes(MigrationCategories.async)
346
- ) {
347
- sbvrUtils.executeAsyncModelMigration(
348
- model as sbvrUtils.ExecutableModel,
349
- );
350
- }
351
- }
352
- };
353
-
354
307
  return {
355
308
  loadConfig,
356
309
  loadApplicationConfig,
@@ -108,7 +108,4 @@ export const migrator = {
108
108
  lockTimeout: 5 * 60 * 1000,
109
109
  // Used to delay the failure on lock taking, to avoid spam taking
110
110
  lockFailDelay: 20 * 1000,
111
- asyncMigrationDefaultDelayMS: 1000,
112
- asyncMigrationDefaultBackoffDelayMS: 60000,
113
- asyncMigrationDefaultErrorThreshold: 10,
114
111
  };
@@ -7,7 +7,6 @@ Term: executed migrations
7
7
  Term: lock time
8
8
  Concept Type: Date Time (Type)
9
9
 
10
-
11
10
  Term: migration
12
11
  Reference Scheme: model name
13
12
  Database ID Field: model name
@@ -22,68 +21,3 @@ Term: migration lock
22
21
 
23
22
  Fact Type: migration lock has model name
24
23
  Necessity: each migration lock has exactly one model name
25
-
26
-
27
- Term: migration key
28
- Concept Type: Short Text (Type)
29
- Term: start time
30
- Concept Type: Date Time (Type)
31
- Term: last run time
32
- Concept Type: Date Time (Type)
33
- Term: run counter
34
- Concept Type: Integer (Type)
35
- Term: migrated rows
36
- Concept Type: Integer (Type)
37
- Term: error counter
38
- Concept Type: Integer (Type)
39
- Term: error threshold
40
- Concept Type: Integer (Type)
41
- Term: delayMS
42
- Concept Type: Integer (Type)
43
- Term: backoffDelayMS
44
- Concept Type: Integer (Type)
45
- Term: converged time
46
- Concept Type: Date Time (Type)
47
- Term: last error message
48
- Concept Type: Text (Type)
49
-
50
- Term: migration status
51
- Reference Scheme: migration key
52
- Database ID Field: migration key
53
-
54
- Fact Type: migration status has migration key
55
- Necessity: each migration status has exactly one migration key
56
-
57
- Fact Type: migration status has start time
58
- Necessity: each migration status has at most one start time
59
-
60
- Fact Type: migration status has last run time
61
- Necessity: each migration status has at most one last run time
62
-
63
- Fact Type: migration status has run counter
64
- Necessity: each migration status has at most one run counter
65
-
66
- Fact Type: migration status has migrated rows
67
- Necessity: each migration status has at most one migrated rows
68
-
69
- Fact Type: migration status has error counter
70
- Necessity: each migration status has at most one error counter
71
-
72
- Fact Type: migration status has error threshold
73
- Necessity: each migration status has at most one error threshold
74
-
75
- Fact Type: migration status has delayMS
76
- Necessity: each migration status has at most one delayMS
77
-
78
- Fact Type: migration status has backoffDelayMS
79
- Necessity: each migration status has at most one backoffDelayMS
80
-
81
- Fact Type: migration status is backoff
82
-
83
- Fact Type: migration status has converged time
84
- Necessity: each migration status has at most one converged time
85
-
86
- Fact Type: migration status has last error message
87
- Necessity: each migration status has at most one last error message
88
-
89
- Fact Type: migration status should stop
@@ -0,0 +1,278 @@
1
+ import type { Tx } from '../database-layer/db';
2
+ import type { Resolvable } from '../sbvr-api/common-types';
3
+ import type { Config, Model } from '../config-loader/config-loader';
4
+
5
+ import { Engines } from '@balena/abstract-sql-compiler';
6
+ import * as _ from 'lodash';
7
+ import { TypedError } from 'typed-error';
8
+ import { migrator as migratorEnv } from '../config-loader/env';
9
+ import * as sbvrUtils from '../sbvr-api/sbvr-utils';
10
+ import { delay } from '../sbvr-api/control-flow';
11
+
12
+ // tslint:disable-next-line:no-var-requires
13
+ const modelText: string = require('./migrations.sbvr');
14
+
15
+ type ApiRootModel = Model & { apiRoot: string };
16
+
17
+ type SbvrUtils = typeof sbvrUtils;
18
+
19
+ type MigrationTuple = [string, Migration];
20
+
21
+ export type MigrationFn = (tx: Tx, sbvrUtils: SbvrUtils) => Resolvable<void>;
22
+
23
+ export type Migration = string | MigrationFn;
24
+
25
+ export class MigrationError extends TypedError {}
26
+
27
+ // Tagged template to convert binds from `?` format to the necessary output format,
28
+ // eg `$1`/`$2`/etc for postgres
29
+ const binds = (strings: TemplateStringsArray, ...bindNums: number[]) =>
30
+ strings
31
+ .map((str, i) => {
32
+ if (i === bindNums.length) {
33
+ return str;
34
+ }
35
+ if (i + 1 !== bindNums[i]) {
36
+ throw new SyntaxError('Migration sql binds must be sequential');
37
+ }
38
+ if (sbvrUtils.db.engine === Engines.postgres) {
39
+ return str + `$${bindNums[i]}`;
40
+ }
41
+ return str + `?`;
42
+ })
43
+ .join('');
44
+
45
+ export const postRun = async (tx: Tx, model: ApiRootModel): Promise<void> => {
46
+ const { initSql } = model;
47
+ if (initSql == null) {
48
+ return;
49
+ }
50
+
51
+ const modelName = model.apiRoot;
52
+
53
+ const exists = await checkModelAlreadyExists(tx, modelName);
54
+ if (!exists) {
55
+ (sbvrUtils.api.migrations?.logger.info ?? console.info)(
56
+ 'First time executing, running init script',
57
+ );
58
+ await lockMigrations(tx, modelName, async () => {
59
+ await tx.executeSql(initSql);
60
+ });
61
+ }
62
+ };
63
+
64
+ export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
65
+ const { migrations } = model;
66
+ if (migrations == null || _.isEmpty(migrations)) {
67
+ return;
68
+ }
69
+
70
+ const modelName = model.apiRoot;
71
+
72
+ // migrations only run if the model has been executed before,
73
+ // to make changes that can't be automatically applied
74
+ const exists = await checkModelAlreadyExists(tx, modelName);
75
+ if (!exists) {
76
+ (sbvrUtils.api.migrations?.logger.info ?? console.info)(
77
+ 'First time model has executed, skipping migrations',
78
+ );
79
+
80
+ return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
81
+ }
82
+ await lockMigrations(tx, modelName, async () => {
83
+ const executedMigrations = await getExecutedMigrations(tx, modelName);
84
+ const pendingMigrations = filterAndSortPendingMigrations(
85
+ migrations,
86
+ executedMigrations,
87
+ );
88
+ if (pendingMigrations.length === 0) {
89
+ return;
90
+ }
91
+
92
+ const newlyExecutedMigrations = await executeMigrations(
93
+ tx,
94
+ pendingMigrations,
95
+ );
96
+ await setExecutedMigrations(tx, modelName, [
97
+ ...executedMigrations,
98
+ ...newlyExecutedMigrations,
99
+ ]);
100
+ });
101
+ };
102
+
103
+ const checkModelAlreadyExists = async (
104
+ tx: Tx,
105
+ modelName: string,
106
+ ): Promise<boolean> => {
107
+ const result = await tx.tableList("name = 'migration'");
108
+ if (result.rows.length === 0) {
109
+ return false;
110
+ }
111
+ const { rows } = await tx.executeSql(
112
+ binds`
113
+ SELECT 1
114
+ FROM "model"
115
+ WHERE "model"."is of-vocabulary" = ${1}
116
+ LIMIT 1`,
117
+ [modelName],
118
+ );
119
+
120
+ return rows.length > 0;
121
+ };
122
+
123
+ const getExecutedMigrations = async (
124
+ tx: Tx,
125
+ modelName: string,
126
+ ): Promise<string[]> => {
127
+ const { rows } = await tx.executeSql(
128
+ binds`
129
+ SELECT "migration"."executed migrations" AS "executed_migrations"
130
+ FROM "migration"
131
+ WHERE "migration"."model name" = ${1}`,
132
+ [modelName],
133
+ );
134
+
135
+ const data = rows[0];
136
+ if (data == null) {
137
+ return [];
138
+ }
139
+
140
+ return JSON.parse(data.executed_migrations) as string[];
141
+ };
142
+
143
+ const setExecutedMigrations = async (
144
+ tx: Tx,
145
+ modelName: string,
146
+ executedMigrations: string[],
147
+ ): Promise<void> => {
148
+ const stringifiedMigrations = JSON.stringify(executedMigrations);
149
+
150
+ const result = await tx.tableList("name = 'migration'");
151
+ if (result.rows.length === 0) {
152
+ return;
153
+ }
154
+
155
+ const { rowsAffected } = await tx.executeSql(
156
+ binds`
157
+ UPDATE "migration"
158
+ SET "model name" = ${1},
159
+ "executed migrations" = ${2}
160
+ WHERE "migration"."model name" = ${3}`,
161
+ [modelName, stringifiedMigrations, modelName],
162
+ );
163
+
164
+ if (rowsAffected === 0) {
165
+ await tx.executeSql(
166
+ binds`
167
+ INSERT INTO "migration" ("model name", "executed migrations")
168
+ VALUES (${1}, ${2})`,
169
+ [modelName, stringifiedMigrations],
170
+ );
171
+ }
172
+ };
173
+
174
+ // turns {"key1": migration, "key3": migration, "key2": migration}
175
+ // into [["key1", migration], ["key2", migration], ["key3", migration]]
176
+ const filterAndSortPendingMigrations = (
177
+ migrations: NonNullable<Model['migrations']>,
178
+ executedMigrations: string[],
179
+ ): MigrationTuple[] =>
180
+ (_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
181
+ .toPairs()
182
+ .sortBy(([migrationKey]) => migrationKey)
183
+ .value();
184
+
185
+ const lockMigrations = async <T>(
186
+ tx: Tx,
187
+ modelName: string,
188
+ fn: () => Promise<T>,
189
+ ): Promise<T> => {
190
+ try {
191
+ await tx.executeSql(
192
+ binds`
193
+ DELETE FROM "migration lock"
194
+ WHERE "model name" = ${1}
195
+ AND "created at" < ${2}`,
196
+ [modelName, new Date(Date.now() - migratorEnv.lockTimeout)],
197
+ );
198
+ await tx.executeSql(
199
+ binds`
200
+ INSERT INTO "migration lock" ("model name")
201
+ VALUES (${1})`,
202
+ [modelName],
203
+ );
204
+ } catch (err) {
205
+ await delay(migratorEnv.lockFailDelay);
206
+ throw err;
207
+ }
208
+ try {
209
+ return await fn();
210
+ } finally {
211
+ try {
212
+ await tx.executeSql(
213
+ binds`
214
+ DELETE FROM "migration lock"
215
+ WHERE "model name" = ${1}`,
216
+ [modelName],
217
+ );
218
+ } catch {
219
+ // We ignore errors here as it's mostly likely caused by the migration failing and
220
+ // rolling back the transaction, and if we rethrow here we'll overwrite the real error
221
+ // making it much harder for users to see what went wrong and fix it
222
+ }
223
+ }
224
+ };
225
+
226
+ const executeMigrations = async (
227
+ tx: Tx,
228
+ migrations: MigrationTuple[] = [],
229
+ ): Promise<string[]> => {
230
+ try {
231
+ for (const migration of migrations) {
232
+ await executeMigration(tx, migration);
233
+ }
234
+ } catch (err) {
235
+ (sbvrUtils.api.migrations?.logger.error ?? console.error)(
236
+ 'Error while executing migrations, rolled back',
237
+ );
238
+ throw new MigrationError(err);
239
+ }
240
+ return migrations.map(([migrationKey]) => migrationKey); // return migration keys
241
+ };
242
+
243
+ const executeMigration = async (
244
+ tx: Tx,
245
+ [key, migration]: MigrationTuple,
246
+ ): Promise<void> => {
247
+ (sbvrUtils.api.migrations?.logger.info ?? console.info)(
248
+ `Running migration ${JSON.stringify(key)}`,
249
+ );
250
+
251
+ if (typeof migration === 'function') {
252
+ await migration(tx, sbvrUtils);
253
+ } else if (typeof migration === 'string') {
254
+ await tx.executeSql(migration);
255
+ } else {
256
+ throw new MigrationError(`Invalid migration type: ${typeof migration}`);
257
+ }
258
+ };
259
+
260
+ export const config: Config = {
261
+ models: [
262
+ {
263
+ modelName: 'migrations',
264
+ apiRoot: 'migrations',
265
+ modelText,
266
+ migrations: {
267
+ '11.0.0-modified-at': `
268
+ ALTER TABLE "migration"
269
+ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
270
+ `,
271
+ '11.0.1-modified-at': `
272
+ ALTER TABLE "migration lock"
273
+ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
274
+ `,
275
+ },
276
+ },
277
+ ],
278
+ };
@@ -33,8 +33,7 @@ import { PinejsClientCore, PromiseResultTypes } from 'pinejs-client-core';
33
33
 
34
34
  import { ExtendedSBVRParser } from '../extended-sbvr-parser/extended-sbvr-parser';
35
35
 
36
- import * as asyncMigrator from '../migrator/async';
37
- import * as syncMigrator from '../migrator/sync';
36
+ import * as migrator from '../migrator/migrator';
38
37
  import { generateODataMetadata } from '../odata-metadata/odata-metadata-generator';
39
38
 
40
39
  // tslint:disable-next-line:no-var-requires
@@ -431,20 +430,6 @@ export const generateModels = (
431
430
  return { vocab, se, lf, abstractSql, sql, odataMetadata };
432
431
  };
433
432
 
434
- export const executeAsyncModelMigration = (
435
- model: ExecutableModel,
436
- ): Promise<void> => executeAsyncModelMigrations([model]);
437
-
438
- export const executeAsyncModelMigrations = async (
439
- execModels: ExecutableModel[],
440
- ): Promise<void> => {
441
- await Promise.all(
442
- execModels.map(async (model) => {
443
- await asyncMigrator.run(model);
444
- }),
445
- );
446
- };
447
-
448
433
  export const executeModel = (
449
434
  tx: Db.Tx,
450
435
  model: ExecutableModel,
@@ -459,7 +444,7 @@ export const executeModels = async (
459
444
  execModels.map(async (model) => {
460
445
  const { apiRoot } = model;
461
446
 
462
- await syncMigrator.run(tx, model);
447
+ await migrator.run(tx, model);
463
448
  const compiledModel = generateModels(model, db.engine);
464
449
 
465
450
  // Create tables related to terms and fact types
@@ -476,7 +461,7 @@ export const executeModels = async (
476
461
  }
477
462
  await promise;
478
463
  }
479
- await syncMigrator.postRun(tx, model);
464
+ await migrator.postRun(tx, model);
480
465
 
481
466
  odataResponse.prepareModel(compiledModel.abstractSql);
482
467
  deepFreeze(compiledModel.abstractSql);
@@ -4,8 +4,7 @@ import './sbvr-loader';
4
4
 
5
5
  import * as dbModule from '../database-layer/db';
6
6
  import * as configLoader from '../config-loader/config-loader';
7
- import * as migrator from '../migrator/sync';
8
- import * as migratorUtils from '../migrator/utils';
7
+ import * as migrator from '../migrator/migrator';
9
8
 
10
9
  import * as sbvrUtils from '../sbvr-api/sbvr-utils';
11
10
 
@@ -18,7 +17,7 @@ export * as env from '../config-loader/env';
18
17
  export * as types from '../sbvr-api/common-types';
19
18
  export * as hooks from '../sbvr-api/hooks';
20
19
  export type { configLoader as ConfigLoader };
21
- export type { migratorUtils as Migrator };
20
+ export type { migrator as Migrator };
22
21
 
23
22
  let envDatabaseOptions: dbModule.DatabaseOptions<string>;
24
23
  if (dbModule.engines.websql != null) {
@@ -1,6 +0,0 @@
1
- import type { Model } from '../config-loader/config-loader';
2
- declare type ApiRootModel = Model & {
3
- apiRoot: string;
4
- };
5
- export declare const run: (model: ApiRootModel) => Promise<void>;
6
- export {};