@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.
- package/.versionbot/CHANGELOG.yml +126 -31
- package/CHANGELOG.md +63 -5
- package/VERSION +1 -1
- package/docs/Migrations.md +1 -101
- package/out/bin/utils.d.ts +2 -2
- package/out/config-loader/config-loader.d.ts +2 -2
- package/out/config-loader/config-loader.js +20 -29
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +0 -3
- package/out/config-loader/env.js +0 -3
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +1 -3
- package/out/data-server/sbvr-server.js +1 -4
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/migrator/migrations.sbvr +0 -66
- package/out/migrator/migrator.d.ts +17 -0
- package/out/migrator/migrator.js +185 -0
- package/out/migrator/migrator.js.map +1 -0
- package/out/pinejs-session-store/pinejs-session-store.js +1 -4
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +1 -1
- package/out/sbvr-api/permissions.d.ts +5 -5
- package/out/sbvr-api/permissions.js +3 -6
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +4 -6
- package/out/sbvr-api/sbvr-utils.js +5 -17
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/server-glue/module.d.ts +3 -3
- package/out/server-glue/module.js +1 -2
- package/out/server-glue/module.js.map +1 -1
- package/package.json +7 -7
- package/src/config-loader/config-loader.ts +27 -62
- package/src/config-loader/env.ts +0 -3
- package/src/data-server/sbvr-server.js +1 -4
- package/src/migrator/migrations.sbvr +0 -66
- package/src/migrator/migrator.ts +278 -0
- package/src/pinejs-session-store/pinejs-session-store.ts +1 -4
- package/src/sbvr-api/permissions.ts +3 -6
- package/src/sbvr-api/sbvr-utils.ts +4 -22
- package/src/server-glue/module.ts +2 -3
- package/out/migrator/async.d.ts +0 -6
- package/out/migrator/async.js +0 -154
- package/out/migrator/async.js.map +0 -1
- package/out/migrator/sync.d.ts +0 -9
- package/out/migrator/sync.js +0 -117
- package/out/migrator/sync.js.map +0 -1
- package/out/migrator/utils.d.ts +0 -51
- package/out/migrator/utils.js +0 -187
- package/out/migrator/utils.js.map +0 -1
- package/src/migrator/async.ts +0 -273
- package/src/migrator/sync.ts +0 -167
- package/src/migrator/utils.ts +0 -293
package/src/migrator/sync.ts
DELETED
@@ -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
|
-
};
|
package/src/migrator/utils.ts
DELETED
@@ -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
|
-
};
|