@balena/pinejs 14.44.0-linear-runtime-migrator-9d426d29f2b01ef0425c72333c2c691976fdc219 → 15.0.0-delete-state-default-user-permissions-57ce3dc6141cd1e8159f4c34da29d1fb113242c6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. package/.versionbot/CHANGELOG.yml +126 -31
  2. package/CHANGELOG.md +63 -5
  3. package/VERSION +1 -1
  4. package/docs/Migrations.md +1 -101
  5. package/out/bin/utils.d.ts +2 -2
  6. package/out/config-loader/config-loader.d.ts +2 -2
  7. package/out/config-loader/config-loader.js +20 -29
  8. package/out/config-loader/config-loader.js.map +1 -1
  9. package/out/config-loader/env.d.ts +0 -3
  10. package/out/config-loader/env.js +0 -3
  11. package/out/config-loader/env.js.map +1 -1
  12. package/out/data-server/sbvr-server.d.ts +1 -3
  13. package/out/data-server/sbvr-server.js +1 -4
  14. package/out/data-server/sbvr-server.js.map +1 -1
  15. package/out/migrator/migrations.sbvr +0 -66
  16. package/out/migrator/migrator.d.ts +17 -0
  17. package/out/migrator/migrator.js +185 -0
  18. package/out/migrator/migrator.js.map +1 -0
  19. package/out/pinejs-session-store/pinejs-session-store.js +1 -4
  20. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  21. package/out/sbvr-api/abstract-sql.d.ts +1 -1
  22. package/out/sbvr-api/permissions.d.ts +5 -5
  23. package/out/sbvr-api/permissions.js +3 -6
  24. package/out/sbvr-api/permissions.js.map +1 -1
  25. package/out/sbvr-api/sbvr-utils.d.ts +4 -6
  26. package/out/sbvr-api/sbvr-utils.js +5 -17
  27. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  28. package/out/server-glue/module.d.ts +3 -3
  29. package/out/server-glue/module.js +1 -2
  30. package/out/server-glue/module.js.map +1 -1
  31. package/package.json +7 -7
  32. package/src/config-loader/config-loader.ts +27 -62
  33. package/src/config-loader/env.ts +0 -3
  34. package/src/data-server/sbvr-server.js +1 -4
  35. package/src/migrator/migrations.sbvr +0 -66
  36. package/src/migrator/migrator.ts +278 -0
  37. package/src/pinejs-session-store/pinejs-session-store.ts +1 -4
  38. package/src/sbvr-api/permissions.ts +3 -6
  39. package/src/sbvr-api/sbvr-utils.ts +4 -22
  40. package/src/server-glue/module.ts +2 -3
  41. package/out/migrator/async.d.ts +0 -6
  42. package/out/migrator/async.js +0 -154
  43. package/out/migrator/async.js.map +0 -1
  44. package/out/migrator/sync.d.ts +0 -9
  45. package/out/migrator/sync.js +0 -117
  46. package/out/migrator/sync.js.map +0 -1
  47. package/out/migrator/utils.d.ts +0 -51
  48. package/out/migrator/utils.js +0 -187
  49. package/out/migrator/utils.js.map +0 -1
  50. package/src/migrator/async.ts +0 -273
  51. package/src/migrator/sync.ts +0 -167
  52. package/src/migrator/utils.ts +0 -293
@@ -1,167 +0,0 @@
1
- import {
2
- modelText,
3
- MigrationTuple,
4
- Migrations,
5
- MigrationError,
6
- defaultMigrationCategory,
7
- checkModelAlreadyExists,
8
- setExecutedMigrations,
9
- getExecutedMigrations,
10
- lockMigrations,
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
-
28
- const exists = await checkModelAlreadyExists(tx, modelName);
29
- if (!exists) {
30
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
31
- 'First time executing, running init script',
32
- );
33
-
34
- await lockMigrations(tx, modelName, async () => {
35
- try {
36
- await tx.executeSql(initSql);
37
- } catch (err) {
38
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
39
- `postRun locked sql execution error ${err} `,
40
- );
41
- }
42
- });
43
- }
44
- };
45
-
46
- export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
47
- const { migrations } = model;
48
- if (
49
- migrations == null ||
50
- _.isEmpty(migrations) ||
51
- migrations[defaultMigrationCategory] === undefined
52
- ) {
53
- return;
54
- }
55
- return $run(tx, model, migrations[defaultMigrationCategory]);
56
- };
57
-
58
- const $run = async (
59
- tx: Tx,
60
- model: ApiRootModel,
61
- migrations: Migrations,
62
- ): Promise<void> => {
63
- const modelName = model.apiRoot;
64
-
65
- // migrations only run if the model has been executed before,
66
- // to make changes that can't be automatically applied
67
- const exists = await checkModelAlreadyExists(tx, modelName);
68
- if (!exists) {
69
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
70
- 'First time model has executed, skipping migrations',
71
- );
72
-
73
- return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
74
- }
75
- await lockMigrations(tx, modelName, async () => {
76
- try {
77
- const executedMigrations = await getExecutedMigrations(tx, modelName);
78
- const pendingMigrations = filterAndSortPendingMigrations(
79
- migrations,
80
- executedMigrations,
81
- );
82
- if (pendingMigrations.length === 0) {
83
- return;
84
- }
85
-
86
- const newlyExecutedMigrations = await executeMigrations(
87
- tx,
88
- pendingMigrations,
89
- );
90
- await setExecutedMigrations(tx, modelName, [
91
- ...executedMigrations,
92
- ...newlyExecutedMigrations,
93
- ]);
94
- } catch (err) {
95
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
96
- `$run executedMigrations error ${err}`,
97
- );
98
- }
99
- });
100
- };
101
-
102
- // turns {"key1": migration, "key3": migration, "key2": migration}
103
- // into [["key1", migration], ["key2", migration], ["key3", migration]]
104
- const filterAndSortPendingMigrations = (
105
- migrations: NonNullable<Migrations>,
106
- executedMigrations: string[],
107
- ): MigrationTuple[] =>
108
- (_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
109
- .toPairs()
110
- .sortBy(([migrationKey]) => migrationKey)
111
- .value();
112
-
113
- const executeMigrations = async (
114
- tx: Tx,
115
- migrations: MigrationTuple[] = [],
116
- ): Promise<string[]> => {
117
- try {
118
- for (const migration of migrations) {
119
- await executeMigration(tx, migration);
120
- }
121
- } catch (err) {
122
- (sbvrUtils.api.migrations?.logger.error ?? console.error)(
123
- 'Error while executing migrations, rolled back',
124
- );
125
- throw new MigrationError(err);
126
- }
127
- return migrations.map(([migrationKey]) => migrationKey); // return migration keys
128
- };
129
-
130
- const executeMigration = async (
131
- tx: Tx,
132
- [key, migration]: MigrationTuple,
133
- ): Promise<void> => {
134
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
135
- `Running migration ${JSON.stringify(key)}`,
136
- );
137
-
138
- if (typeof migration === 'function') {
139
- await migration(tx, sbvrUtils);
140
- } else if (typeof migration === 'string') {
141
- await tx.executeSql(migration);
142
- } else {
143
- throw new MigrationError(`Invalid migration type: ${typeof migration}`);
144
- }
145
- };
146
-
147
- export const config: Config = {
148
- models: [
149
- {
150
- modelName: 'migrations',
151
- apiRoot: 'migrations',
152
- modelText,
153
- migrations: {
154
- [defaultMigrationCategory]: {
155
- '11.0.0-modified-at': `
156
- ALTER TABLE "migration"
157
- ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
158
- `,
159
- '11.0.1-modified-at': `
160
- ALTER TABLE "migration lock"
161
- ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
162
- `,
163
- },
164
- },
165
- },
166
- ],
167
- };
@@ -1,293 +0,0 @@
1
- import type { Result, Tx } from '../database-layer/db';
2
- import type { Resolvable } from '../sbvr-api/common-types';
3
-
4
- import { Engines } from '@balena/abstract-sql-compiler';
5
- import * as _ from 'lodash';
6
- import { TypedError } from 'typed-error';
7
- import { migrator as migratorEnv } from '../config-loader/env';
8
- export { migrator as migratorEnv } from '../config-loader/env';
9
- import { delay } from '../sbvr-api/control-flow';
10
-
11
- // tslint:disable-next-line:no-var-requires
12
- export const modelText = require('./migrations.sbvr');
13
-
14
- import * as sbvrUtils from '../sbvr-api/sbvr-utils';
15
-
16
- export const defaultMigrationCategory = 'sync';
17
- export enum migrationCategories {
18
- 'sync' = 'sync',
19
- 'async' = 'async',
20
- }
21
-
22
- type SbvrUtils = typeof sbvrUtils;
23
-
24
- export type MigrationTuple = [string, Migration];
25
-
26
- export type MigrationFn = (tx: Tx, sbvrUtils: SbvrUtils) => Resolvable<void>;
27
- export type AsyncMigrationFn = (
28
- tx: Tx,
29
- sbvrUtils: SbvrUtils,
30
- ) => Resolvable<Result>;
31
-
32
- export type AsyncMigration = {
33
- fn?: AsyncMigrationFn | undefined;
34
- sql?: string | undefined;
35
- delayMS?: number | undefined;
36
- backoffDelayMS?: number | undefined;
37
- errorThreshold?: number | undefined;
38
- };
39
-
40
- export type Migration = string | MigrationFn | AsyncMigration;
41
-
42
- export type Migrations = { [key: string]: Migration };
43
-
44
- export class MigrationError extends TypedError {}
45
-
46
- export type MigrationStatus = {
47
- migration_key: string;
48
- start_time: Date;
49
- last_run_time: Date;
50
- run_counter: number;
51
- migrated_rows: number;
52
- error_counter: number;
53
- error_threshold: number;
54
- delayMS: number;
55
- backoffDelayMS: number;
56
- converged_time: Date | undefined;
57
- last_error_message: string | undefined;
58
- is_backoff: boolean;
59
- should_stop: boolean;
60
- };
61
-
62
- // Tagged template to convert binds from `?` format to the necessary output format,
63
- // eg `$1`/`$2`/etc for postgres
64
- export const binds = (strings: TemplateStringsArray, ...bindNums: number[]) =>
65
- strings
66
- .map((str, i) => {
67
- if (i === bindNums.length) {
68
- return str;
69
- }
70
- if (i + 1 !== bindNums[i]) {
71
- throw new SyntaxError('Migration sql binds must be sequential');
72
- }
73
- if (sbvrUtils.db.engine === Engines.postgres) {
74
- return str + `$${bindNums[i]}`;
75
- }
76
- return str + `?`;
77
- })
78
- .join('');
79
-
80
- export const lockMigrations = async <T>(
81
- tx: Tx,
82
- modelName: string,
83
- fn: () => Promise<T>,
84
- ): Promise<T> => {
85
- try {
86
- await tx.executeSql(
87
- binds`
88
- DELETE FROM "migration lock"
89
- WHERE "model name" = ${1}
90
- AND "created at" < ${2}`,
91
- [modelName, new Date(Date.now() - migratorEnv.lockTimeout)],
92
- );
93
- await tx.executeSql(
94
- binds`
95
- INSERT INTO "migration lock" ("model name")
96
- VALUES (${1})`,
97
- [modelName],
98
- );
99
- } catch (err) {
100
- await delay(migratorEnv.lockFailDelay);
101
- throw err;
102
- }
103
- try {
104
- return await fn();
105
- } finally {
106
- try {
107
- await tx.executeSql(
108
- binds`
109
- DELETE FROM "migration lock"
110
- WHERE "model name" = ${1}`,
111
- [modelName],
112
- );
113
- } catch {
114
- // We ignore errors here as it's mostly likely caused by the migration failing and
115
- // rolling back the transaction, and if we rethrow here we'll overwrite the real error
116
- // making it much harder for users to see what went wrong and fix it
117
- }
118
- }
119
- };
120
-
121
- export const checkModelAlreadyExists = async (
122
- tx: Tx,
123
- modelName: string,
124
- ): Promise<boolean> => {
125
- const result = await tx.tableList("name = 'migration'");
126
- if (result.rows.length === 0) {
127
- return false;
128
- }
129
- const { rows } = await tx.executeSql(
130
- binds`
131
- SELECT 1
132
- FROM "model"
133
- WHERE "model"."is of-vocabulary" = ${1}
134
- LIMIT 1`,
135
- [modelName],
136
- );
137
-
138
- return rows.length > 0;
139
- };
140
-
141
- export const setExecutedMigrations = async (
142
- tx: Tx,
143
- modelName: string,
144
- executedMigrations: string[],
145
- ): Promise<void> => {
146
- const stringifiedMigrations = JSON.stringify(executedMigrations);
147
-
148
- const result = await tx.tableList("name = 'migration'");
149
- if (result.rows.length === 0) {
150
- return;
151
- }
152
-
153
- const { rowsAffected } = await tx.executeSql(
154
- binds`
155
- UPDATE "migration"
156
- SET "model name" = ${1},
157
- "executed migrations" = ${2}
158
- WHERE "migration"."model name" = ${3}`,
159
- [modelName, stringifiedMigrations, modelName],
160
- );
161
-
162
- if (rowsAffected === 0) {
163
- await tx.executeSql(
164
- binds`
165
- INSERT INTO "migration" ("model name", "executed migrations")
166
- VALUES (${1}, ${2})`,
167
- [modelName, stringifiedMigrations],
168
- );
169
- }
170
- };
171
-
172
- export const getExecutedMigrations = async (
173
- tx: Tx,
174
- modelName: string,
175
- ): Promise<string[]> => {
176
- const { rows } = await tx.executeSql(
177
- binds`
178
- SELECT "migration"."executed migrations" AS "executed_migrations"
179
- FROM "migration"
180
- WHERE "migration"."model name" = ${1}`,
181
- [modelName],
182
- );
183
-
184
- const data = rows[0];
185
- if (data == null) {
186
- return [];
187
- }
188
-
189
- return JSON.parse(data.executed_migrations) as string[];
190
- };
191
-
192
- // Just update the "modified at" field to show liveness
193
- export const initMigrationStatus = async (
194
- tx: Tx,
195
- migrationStatus: MigrationStatus,
196
- ): Promise<Result | undefined> => {
197
- try {
198
- const result = await tx.executeSql(
199
- binds`
200
- INSERT INTO "migration status" ("migration key", "start time", "delayMS", "backoffDelayMS", "is backoff", "error threshold", "should stop")
201
- VALUES (${1}, ${2}, ${3}, ${4}, ${5}, ${6}, ${7});
202
- `,
203
- [
204
- migrationStatus['migration_key'],
205
- migrationStatus['start_time'],
206
- migrationStatus['delayMS'],
207
- migrationStatus['backoffDelayMS'],
208
- migrationStatus['is_backoff'] === true ? 1 : 0,
209
- migrationStatus['error_threshold'],
210
- migrationStatus['should_stop'] === true ? 1 : 0,
211
- ],
212
- );
213
- return result;
214
- } catch (err) {
215
- // it already exists and we can ignore this error.
216
- }
217
- };
218
-
219
- // Just update the "modified at" field to show liveness
220
- export const updateMigrationStatus = async (
221
- tx: Tx,
222
- migrationStatus: MigrationStatus,
223
- ): Promise<Result | undefined> => {
224
- try {
225
- return await tx.executeSql(
226
- binds`
227
- UPDATE "migration status"
228
- SET
229
- "run counter" = ${1},
230
- "last run time" = ${2},
231
- "migrated rows" = ${3},
232
- "error counter" = ${4},
233
- "converged time" = ${5},
234
- "last error message" = ${6},
235
- "is backoff" = ${7},
236
- "should stop" = ${8}
237
- WHERE "migration status"."migration key" = ${9};`,
238
- [
239
- migrationStatus['run_counter'],
240
- migrationStatus['last_run_time'],
241
- migrationStatus['migrated_rows'],
242
- migrationStatus['error_counter'],
243
- migrationStatus['converged_time'],
244
- migrationStatus['last_error_message'],
245
- migrationStatus['is_backoff'] === true ? 1 : 0,
246
- migrationStatus['should_stop'] === true ? 1 : 0,
247
- migrationStatus['migration_key'],
248
- ],
249
- );
250
- } catch (err) {
251
- // we ignore all errors to not spam the api logs
252
- }
253
- };
254
-
255
- export const readMigrationStatus = async (
256
- tx: Tx,
257
- migrationKey: string,
258
- ): Promise<MigrationStatus | undefined> => {
259
- try {
260
- const { rows } = await tx.executeSql(
261
- binds`
262
- SELECT *
263
- FROM "migration status"
264
- WHERE "migration status"."migration key" = ${1}
265
- LIMIT 1;`,
266
- [migrationKey],
267
- );
268
-
269
- const data = rows[0];
270
- if (data == null) {
271
- return undefined;
272
- }
273
-
274
- return {
275
- migration_key: data['migration key'],
276
- start_time: data['start time'],
277
- last_run_time: data['last run time'],
278
- run_counter: data['run counter'],
279
- migrated_rows: data['migrated rows'],
280
- error_counter: data['error counter'],
281
- error_threshold: data['error threshold'],
282
- delayMS: data['delayMS'],
283
- backoffDelayMS: data['backoffDelayMS'],
284
- converged_time: data['converged_time'],
285
- last_error_message: data['last error message'],
286
- is_backoff: data['is backoff'] === 1 ? true : false,
287
- should_stop: data['should stop'] === 1 ? true : false,
288
- };
289
- } catch (err) {
290
- // we ignore all errors to not spam the api logs
291
- return undefined;
292
- }
293
- };