@balena/pinejs 15.0.0-delete-state-default-user-permissions-ba0732a0c5d0da9d1d5be818cb08cc898e86ebe3 → 15.0.0-delete-state-default-user-permissions-3639cb4f82f3d7c9636142a31f4df182080cef3f
Sign up to get free protection for your applications and to get access to all the features.
- package/.versionbot/CHANGELOG.yml +32 -2
- package/CHANGELOG.md +11 -1
- package/out/config-loader/config-loader.d.ts +2 -4
- package/out/config-loader/config-loader.js +34 -12
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +1 -1
- package/out/config-loader/env.js.map +1 -1
- package/out/migrator/sync.d.ts +9 -0
- package/out/migrator/sync.js +121 -0
- package/out/migrator/sync.js.map +1 -0
- package/out/migrator/utils.d.ts +28 -0
- package/out/migrator/utils.js +105 -0
- package/out/migrator/utils.js.map +1 -0
- package/out/sbvr-api/permissions.d.ts +5 -4
- package/out/sbvr-api/permissions.js +20 -18
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.js +3 -3
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/server-glue/module.d.ts +2 -2
- package/out/server-glue/module.js +2 -1
- package/out/server-glue/module.js.map +1 -1
- package/package.json +2 -2
- package/src/config-loader/config-loader.ts +64 -18
- package/src/config-loader/env.ts +3 -1
- package/src/migrator/sync.ts +171 -0
- package/src/migrator/utils.ts +160 -0
- package/src/sbvr-api/permissions.ts +44 -20
- package/src/sbvr-api/sbvr-utils.ts +3 -3
- package/src/server-glue/module.ts +3 -2
- package/out/migrator/migrator.d.ts +0 -17
- package/out/migrator/migrator.js +0 -185
- package/out/migrator/migrator.js.map +0 -1
- package/src/migrator/migrator.ts +0 -278
@@ -1,9 +1,15 @@
|
|
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';
|
5
4
|
import type { AnyObject, Resolvable } from '../sbvr-api/common-types';
|
6
5
|
|
6
|
+
import {
|
7
|
+
Migration,
|
8
|
+
Migrations,
|
9
|
+
defaultMigrationCategory,
|
10
|
+
MigrationCategories,
|
11
|
+
} from '../migrator/utils';
|
12
|
+
|
7
13
|
import * as fs from 'fs';
|
8
14
|
import * as _ from 'lodash';
|
9
15
|
import * as path from 'path';
|
@@ -25,9 +31,7 @@ export interface Model {
|
|
25
31
|
modelText?: string;
|
26
32
|
abstractSql?: AbstractSqlModel;
|
27
33
|
migrationsPath?: string;
|
28
|
-
migrations?:
|
29
|
-
[index: string]: Migration;
|
30
|
-
};
|
34
|
+
migrations?: Migrations;
|
31
35
|
initSqlPath?: string;
|
32
36
|
initSql?: string;
|
33
37
|
customServerCode?:
|
@@ -138,21 +142,22 @@ export const setup = (app: Express.Application) => {
|
|
138
142
|
return permissionID;
|
139
143
|
}),
|
140
144
|
);
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
145
|
+
|
146
|
+
await authApiTx.delete({
|
147
|
+
resource: 'user__has__permission',
|
148
|
+
options: {
|
149
|
+
$filter: {
|
150
|
+
user: userID,
|
151
|
+
...(permissionIds.length > 0 && {
|
147
152
|
$not: {
|
148
153
|
permission: {
|
149
154
|
$in: permissionIds,
|
150
155
|
},
|
151
156
|
},
|
152
|
-
},
|
157
|
+
}),
|
153
158
|
},
|
154
|
-
}
|
155
|
-
}
|
159
|
+
},
|
160
|
+
});
|
156
161
|
}
|
157
162
|
} catch (e) {
|
158
163
|
e.message = `Could not create or find user "${user.username}": ${e.message}`;
|
@@ -267,18 +272,59 @@ export const setup = (app: Express.Application) => {
|
|
267
272
|
await Promise.all(
|
268
273
|
fileNames.map(async (filename) => {
|
269
274
|
const filePath = path.join(migrationsPath, filename);
|
275
|
+
const fileNameParts = filename.split('.', 3);
|
276
|
+
const fileExtension = path.extname(filename);
|
270
277
|
const [migrationKey] = filename.split('-', 1);
|
278
|
+
let migrationCategory = defaultMigrationCategory;
|
271
279
|
|
272
|
-
|
280
|
+
if (fileNameParts.length === 3) {
|
281
|
+
if (fileNameParts[1] in MigrationCategories) {
|
282
|
+
migrationCategory = fileNameParts[1] as MigrationCategories;
|
283
|
+
} else {
|
284
|
+
console.error(
|
285
|
+
`Unrecognised migration file category ${
|
286
|
+
fileNameParts[1]
|
287
|
+
}, skipping: ${path.extname(filename)}`,
|
288
|
+
);
|
289
|
+
return;
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
/**
|
294
|
+
* helper to assign migrations with category level to model
|
295
|
+
* example migration file names:
|
296
|
+
*
|
297
|
+
* key0-name.ts ==> defaults startup migration
|
298
|
+
* key1-name1.sql ==> defaults startup migration
|
299
|
+
* key2-name2.sync.sql ==> explicit synchrony migration
|
300
|
+
*
|
301
|
+
*/
|
302
|
+
const assignMigrationWithCategory = (
|
303
|
+
newMigrationKey: string,
|
304
|
+
newMigration: Migration,
|
305
|
+
) => {
|
306
|
+
const catMigrations = migrations[migrationCategory] || {};
|
307
|
+
if (typeof catMigrations === 'object') {
|
308
|
+
migrations[migrationCategory] = {
|
309
|
+
[newMigrationKey]: newMigration,
|
310
|
+
...catMigrations,
|
311
|
+
};
|
312
|
+
}
|
313
|
+
};
|
314
|
+
|
315
|
+
switch (fileExtension) {
|
273
316
|
case '.coffee':
|
274
317
|
case '.ts':
|
275
318
|
case '.js':
|
276
|
-
|
319
|
+
assignMigrationWithCategory(
|
320
|
+
migrationKey,
|
321
|
+
nodeRequire(filePath),
|
322
|
+
);
|
277
323
|
break;
|
278
324
|
case '.sql':
|
279
|
-
|
280
|
-
|
281
|
-
'utf8',
|
325
|
+
assignMigrationWithCategory(
|
326
|
+
migrationKey,
|
327
|
+
await fs.promises.readFile(filePath, 'utf8'),
|
282
328
|
);
|
283
329
|
break;
|
284
330
|
default:
|
package/src/config-loader/env.ts
CHANGED
@@ -58,7 +58,9 @@ import memoizeWeak = require('memoizee/weak');
|
|
58
58
|
export const createCache = <T extends (...args: any[]) => any>(
|
59
59
|
cacheName: keyof typeof cache,
|
60
60
|
fn: T,
|
61
|
-
|
61
|
+
// TODO: Mark this as optional once TS is able to infer the `normalizer` types
|
62
|
+
// when the `weak` differentiating property is not provided.
|
63
|
+
opts: CacheFnOpts<T>,
|
62
64
|
) => {
|
63
65
|
const cacheOpts = cache[cacheName];
|
64
66
|
if (cacheOpts === false) {
|
@@ -0,0 +1,171 @@
|
|
1
|
+
import {
|
2
|
+
modelText,
|
3
|
+
MigrationTuple,
|
4
|
+
MigrationError,
|
5
|
+
defaultMigrationCategory,
|
6
|
+
checkModelAlreadyExists,
|
7
|
+
setExecutedMigrations,
|
8
|
+
getExecutedMigrations,
|
9
|
+
lockMigrations,
|
10
|
+
RunnableMigrations,
|
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
|
+
`initSql execution error ${err} `,
|
40
|
+
);
|
41
|
+
throw new MigrationError(err);
|
42
|
+
}
|
43
|
+
});
|
44
|
+
}
|
45
|
+
};
|
46
|
+
|
47
|
+
export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
|
48
|
+
const { migrations } = model;
|
49
|
+
if (migrations == null || _.isEmpty(migrations)) {
|
50
|
+
return;
|
51
|
+
}
|
52
|
+
const defaultMigrations = migrations[defaultMigrationCategory];
|
53
|
+
const runMigrations: RunnableMigrations =
|
54
|
+
defaultMigrationCategory in migrations
|
55
|
+
? typeof defaultMigrations === 'object'
|
56
|
+
? defaultMigrations
|
57
|
+
: {}
|
58
|
+
: migrations;
|
59
|
+
|
60
|
+
return $run(tx, model, runMigrations);
|
61
|
+
};
|
62
|
+
|
63
|
+
const $run = async (
|
64
|
+
tx: Tx,
|
65
|
+
model: ApiRootModel,
|
66
|
+
migrations: RunnableMigrations,
|
67
|
+
): Promise<void> => {
|
68
|
+
const modelName = model.apiRoot;
|
69
|
+
|
70
|
+
// migrations only run if the model has been executed before,
|
71
|
+
// to make changes that can't be automatically applied
|
72
|
+
const exists = await checkModelAlreadyExists(tx, modelName);
|
73
|
+
if (!exists) {
|
74
|
+
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
75
|
+
'First time model has executed, skipping migrations',
|
76
|
+
);
|
77
|
+
|
78
|
+
return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
|
79
|
+
}
|
80
|
+
await lockMigrations(tx, modelName, async () => {
|
81
|
+
try {
|
82
|
+
const executedMigrations = await getExecutedMigrations(tx, modelName);
|
83
|
+
const pendingMigrations = filterAndSortPendingMigrations(
|
84
|
+
migrations,
|
85
|
+
executedMigrations,
|
86
|
+
);
|
87
|
+
if (pendingMigrations.length === 0) {
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
|
91
|
+
const newlyExecutedMigrations = await executeMigrations(
|
92
|
+
tx,
|
93
|
+
pendingMigrations,
|
94
|
+
);
|
95
|
+
await setExecutedMigrations(tx, modelName, [
|
96
|
+
...executedMigrations,
|
97
|
+
...newlyExecutedMigrations,
|
98
|
+
]);
|
99
|
+
} catch (err) {
|
100
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
101
|
+
`Failed to executed synchronous migrations from api root model ${err}`,
|
102
|
+
);
|
103
|
+
throw new MigrationError(err);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
};
|
107
|
+
|
108
|
+
// turns {"key1": migration, "key3": migration, "key2": migration}
|
109
|
+
// into [["key1", migration], ["key2", migration], ["key3", migration]]
|
110
|
+
const filterAndSortPendingMigrations = (
|
111
|
+
migrations: NonNullable<RunnableMigrations>,
|
112
|
+
executedMigrations: string[],
|
113
|
+
): MigrationTuple[] =>
|
114
|
+
(_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
|
115
|
+
.toPairs()
|
116
|
+
.sortBy(([migrationKey]) => migrationKey)
|
117
|
+
.value();
|
118
|
+
|
119
|
+
const executeMigrations = async (
|
120
|
+
tx: Tx,
|
121
|
+
migrations: MigrationTuple[] = [],
|
122
|
+
): Promise<string[]> => {
|
123
|
+
try {
|
124
|
+
for (const migration of migrations) {
|
125
|
+
await executeMigration(tx, migration);
|
126
|
+
}
|
127
|
+
} catch (err) {
|
128
|
+
(sbvrUtils.api.migrations?.logger.error ?? console.error)(
|
129
|
+
'Error while executing migrations, rolled back',
|
130
|
+
);
|
131
|
+
throw new MigrationError(err);
|
132
|
+
}
|
133
|
+
return migrations.map(([migrationKey]) => migrationKey); // return migration keys
|
134
|
+
};
|
135
|
+
|
136
|
+
const executeMigration = async (
|
137
|
+
tx: Tx,
|
138
|
+
[key, migration]: MigrationTuple,
|
139
|
+
): Promise<void> => {
|
140
|
+
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
141
|
+
`Running migration ${JSON.stringify(key)}`,
|
142
|
+
);
|
143
|
+
|
144
|
+
if (typeof migration === 'function') {
|
145
|
+
await migration(tx, sbvrUtils);
|
146
|
+
} else if (typeof migration === 'string') {
|
147
|
+
await tx.executeSql(migration);
|
148
|
+
} else {
|
149
|
+
throw new MigrationError(`Invalid migration type: ${typeof migration}`);
|
150
|
+
}
|
151
|
+
};
|
152
|
+
|
153
|
+
export const config: Config = {
|
154
|
+
models: [
|
155
|
+
{
|
156
|
+
modelName: 'migrations',
|
157
|
+
apiRoot: 'migrations',
|
158
|
+
modelText,
|
159
|
+
migrations: {
|
160
|
+
'11.0.0-modified-at': `
|
161
|
+
ALTER TABLE "migration"
|
162
|
+
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
163
|
+
`,
|
164
|
+
'11.0.1-modified-at': `
|
165
|
+
ALTER TABLE "migration lock"
|
166
|
+
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
167
|
+
`,
|
168
|
+
},
|
169
|
+
},
|
170
|
+
],
|
171
|
+
};
|
@@ -0,0 +1,160 @@
|
|
1
|
+
import type { 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
|
+
export enum MigrationCategories {
|
16
|
+
'sync' = 'sync',
|
17
|
+
}
|
18
|
+
export const defaultMigrationCategory = MigrationCategories.sync;
|
19
|
+
export type CategorizedMigrations = {
|
20
|
+
[key in MigrationCategories]: RunnableMigrations;
|
21
|
+
};
|
22
|
+
|
23
|
+
type SbvrUtils = typeof sbvrUtils;
|
24
|
+
export type MigrationTuple = [string, Migration];
|
25
|
+
export type MigrationFn = (tx: Tx, sbvrUtils: SbvrUtils) => Resolvable<void>;
|
26
|
+
export type Migration = string | MigrationFn;
|
27
|
+
export type RunnableMigrations = { [key: string]: Migration };
|
28
|
+
export type Migrations = CategorizedMigrations | RunnableMigrations;
|
29
|
+
|
30
|
+
export class MigrationError extends TypedError {}
|
31
|
+
|
32
|
+
// Tagged template to convert binds from `?` format to the necessary output format,
|
33
|
+
// eg `$1`/`$2`/etc for postgres
|
34
|
+
export const binds = (strings: TemplateStringsArray, ...bindNums: number[]) =>
|
35
|
+
strings
|
36
|
+
.map((str, i) => {
|
37
|
+
if (i === bindNums.length) {
|
38
|
+
return str;
|
39
|
+
}
|
40
|
+
if (i + 1 !== bindNums[i]) {
|
41
|
+
throw new SyntaxError('Migration sql binds must be sequential');
|
42
|
+
}
|
43
|
+
if (sbvrUtils.db.engine === Engines.postgres) {
|
44
|
+
return str + `$${bindNums[i]}`;
|
45
|
+
}
|
46
|
+
return str + `?`;
|
47
|
+
})
|
48
|
+
.join('');
|
49
|
+
|
50
|
+
export const lockMigrations = async <T>(
|
51
|
+
tx: Tx,
|
52
|
+
modelName: string,
|
53
|
+
fn: () => Promise<T>,
|
54
|
+
): Promise<T> => {
|
55
|
+
try {
|
56
|
+
await tx.executeSql(
|
57
|
+
binds`
|
58
|
+
DELETE FROM "migration lock"
|
59
|
+
WHERE "model name" = ${1}
|
60
|
+
AND "created at" < ${2}`,
|
61
|
+
[modelName, new Date(Date.now() - migratorEnv.lockTimeout)],
|
62
|
+
);
|
63
|
+
await tx.executeSql(
|
64
|
+
binds`
|
65
|
+
INSERT INTO "migration lock" ("model name")
|
66
|
+
VALUES (${1})`,
|
67
|
+
[modelName],
|
68
|
+
);
|
69
|
+
} catch (err) {
|
70
|
+
await delay(migratorEnv.lockFailDelay);
|
71
|
+
throw err;
|
72
|
+
}
|
73
|
+
try {
|
74
|
+
return await fn();
|
75
|
+
} finally {
|
76
|
+
try {
|
77
|
+
await tx.executeSql(
|
78
|
+
binds`
|
79
|
+
DELETE FROM "migration lock"
|
80
|
+
WHERE "model name" = ${1}`,
|
81
|
+
[modelName],
|
82
|
+
);
|
83
|
+
} catch {
|
84
|
+
// We ignore errors here as it's mostly likely caused by the migration failing and
|
85
|
+
// rolling back the transaction, and if we rethrow here we'll overwrite the real error
|
86
|
+
// making it much harder for users to see what went wrong and fix it
|
87
|
+
}
|
88
|
+
}
|
89
|
+
};
|
90
|
+
|
91
|
+
export const checkModelAlreadyExists = async (
|
92
|
+
tx: Tx,
|
93
|
+
modelName: string,
|
94
|
+
): Promise<boolean> => {
|
95
|
+
const result = await tx.tableList("name = 'migration'");
|
96
|
+
if (result.rows.length === 0) {
|
97
|
+
return false;
|
98
|
+
}
|
99
|
+
const { rows } = await tx.executeSql(
|
100
|
+
binds`
|
101
|
+
SELECT 1
|
102
|
+
FROM "model"
|
103
|
+
WHERE "model"."is of-vocabulary" = ${1}
|
104
|
+
LIMIT 1`,
|
105
|
+
[modelName],
|
106
|
+
);
|
107
|
+
|
108
|
+
return rows.length > 0;
|
109
|
+
};
|
110
|
+
|
111
|
+
export const setExecutedMigrations = async (
|
112
|
+
tx: Tx,
|
113
|
+
modelName: string,
|
114
|
+
executedMigrations: string[],
|
115
|
+
): Promise<void> => {
|
116
|
+
const stringifiedMigrations = JSON.stringify(executedMigrations);
|
117
|
+
|
118
|
+
const result = await tx.tableList("name = 'migration'");
|
119
|
+
if (result.rows.length === 0) {
|
120
|
+
return;
|
121
|
+
}
|
122
|
+
|
123
|
+
const { rowsAffected } = await tx.executeSql(
|
124
|
+
binds`
|
125
|
+
UPDATE "migration"
|
126
|
+
SET "model name" = ${1},
|
127
|
+
"executed migrations" = ${2}
|
128
|
+
WHERE "migration"."model name" = ${3}`,
|
129
|
+
[modelName, stringifiedMigrations, modelName],
|
130
|
+
);
|
131
|
+
|
132
|
+
if (rowsAffected === 0) {
|
133
|
+
await tx.executeSql(
|
134
|
+
binds`
|
135
|
+
INSERT INTO "migration" ("model name", "executed migrations")
|
136
|
+
VALUES (${1}, ${2})`,
|
137
|
+
[modelName, stringifiedMigrations],
|
138
|
+
);
|
139
|
+
}
|
140
|
+
};
|
141
|
+
|
142
|
+
export const getExecutedMigrations = async (
|
143
|
+
tx: Tx,
|
144
|
+
modelName: string,
|
145
|
+
): Promise<string[]> => {
|
146
|
+
const { rows } = await tx.executeSql(
|
147
|
+
binds`
|
148
|
+
SELECT "migration"."executed migrations" AS "executed_migrations"
|
149
|
+
FROM "migration"
|
150
|
+
WHERE "migration"."model name" = ${1}`,
|
151
|
+
[modelName],
|
152
|
+
);
|
153
|
+
|
154
|
+
const data = rows[0];
|
155
|
+
if (data == null) {
|
156
|
+
return [];
|
157
|
+
}
|
158
|
+
|
159
|
+
return JSON.parse(data.executed_migrations) as string[];
|
160
|
+
};
|
@@ -20,6 +20,7 @@ import type {
|
|
20
20
|
ODataQuery,
|
21
21
|
SupportedMethod,
|
22
22
|
} from '@balena/odata-parser';
|
23
|
+
import type { Tx } from '../database-layer/db';
|
23
24
|
import type { ApiKey, User } from '../sbvr-api/sbvr-utils';
|
24
25
|
import type { AnyObject } from './common-types';
|
25
26
|
|
@@ -345,7 +346,6 @@ const getPermissionsLookup = env.createCache(
|
|
345
346
|
return permissionsLookup;
|
346
347
|
},
|
347
348
|
{
|
348
|
-
weak: undefined,
|
349
349
|
normalizer: ([permissions, guestPermissions]) =>
|
350
350
|
// When guestPermissions is present it should always be the same, so we can key by presence not content
|
351
351
|
`${permissions}${guestPermissions == null}`,
|
@@ -1213,19 +1213,27 @@ const $getUserPermissions = (() => {
|
|
1213
1213
|
);
|
1214
1214
|
return env.createCache(
|
1215
1215
|
'userPermissions',
|
1216
|
-
async (userId: number) => {
|
1217
|
-
const permissions = (await getUserPermissionsQuery()(
|
1218
|
-
|
1219
|
-
|
1216
|
+
async (userId: number, tx?: Tx) => {
|
1217
|
+
const permissions = (await getUserPermissionsQuery()(
|
1218
|
+
{
|
1219
|
+
userId,
|
1220
|
+
},
|
1221
|
+
undefined,
|
1222
|
+
{ tx },
|
1223
|
+
)) as Array<{ name: string }>;
|
1220
1224
|
return permissions.map((permission) => permission.name);
|
1221
1225
|
},
|
1222
1226
|
{
|
1223
1227
|
primitive: true,
|
1224
1228
|
promise: true,
|
1229
|
+
normalizer: ([userId]) => `${userId}`,
|
1225
1230
|
},
|
1226
1231
|
);
|
1227
1232
|
})();
|
1228
|
-
export const getUserPermissions = async (
|
1233
|
+
export const getUserPermissions = async (
|
1234
|
+
userId: number,
|
1235
|
+
tx?: Tx,
|
1236
|
+
): Promise<string[]> => {
|
1229
1237
|
if (typeof userId === 'string') {
|
1230
1238
|
userId = parseInt(userId, 10);
|
1231
1239
|
}
|
@@ -1233,7 +1241,7 @@ export const getUserPermissions = async (userId: number): Promise<string[]> => {
|
|
1233
1241
|
throw new Error(`User ID has to be numeric, got: ${typeof userId}`);
|
1234
1242
|
}
|
1235
1243
|
try {
|
1236
|
-
return await $getUserPermissions(userId);
|
1244
|
+
return await $getUserPermissions(userId, tx);
|
1237
1245
|
} catch (err) {
|
1238
1246
|
sbvrUtils.api.Auth.logger.error('Error loading user permissions', err);
|
1239
1247
|
throw err;
|
@@ -1336,26 +1344,32 @@ const $getApiKeyPermissions = (() => {
|
|
1336
1344
|
);
|
1337
1345
|
return env.createCache(
|
1338
1346
|
'apiKeyPermissions',
|
1339
|
-
async (apiKey: string) => {
|
1340
|
-
const permissions = (await getApiKeyPermissionsQuery()(
|
1341
|
-
|
1342
|
-
|
1347
|
+
async (apiKey: string, tx?: Tx) => {
|
1348
|
+
const permissions = (await getApiKeyPermissionsQuery()(
|
1349
|
+
{
|
1350
|
+
apiKey,
|
1351
|
+
},
|
1352
|
+
undefined,
|
1353
|
+
{ tx },
|
1354
|
+
)) as Array<{ name: string }>;
|
1343
1355
|
return permissions.map((permission) => permission.name);
|
1344
1356
|
},
|
1345
1357
|
{
|
1346
1358
|
primitive: true,
|
1347
1359
|
promise: true,
|
1360
|
+
normalizer: ([apiKey]) => apiKey,
|
1348
1361
|
},
|
1349
1362
|
);
|
1350
1363
|
})();
|
1351
1364
|
export const getApiKeyPermissions = async (
|
1352
1365
|
apiKey: string,
|
1366
|
+
tx?: Tx,
|
1353
1367
|
): Promise<string[]> => {
|
1354
1368
|
if (typeof apiKey !== 'string') {
|
1355
1369
|
throw new Error('API key has to be a string, got: ' + typeof apiKey);
|
1356
1370
|
}
|
1357
1371
|
try {
|
1358
|
-
return await $getApiKeyPermissions(apiKey);
|
1372
|
+
return await $getApiKeyPermissions(apiKey, tx);
|
1359
1373
|
} catch (err) {
|
1360
1374
|
sbvrUtils.api.Auth.logger.error('Error loading api key permissions', err);
|
1361
1375
|
throw err;
|
@@ -1386,10 +1400,14 @@ const getApiKeyActorId = (() => {
|
|
1386
1400
|
const apiActorPermissionError = new PermissionError();
|
1387
1401
|
return env.createCache(
|
1388
1402
|
'apiKeyActorId',
|
1389
|
-
async (apiKey: string) => {
|
1390
|
-
const apiKeyResult = await getApiKeyActorIdQuery()(
|
1391
|
-
|
1392
|
-
|
1403
|
+
async (apiKey: string, tx?: Tx) => {
|
1404
|
+
const apiKeyResult = await getApiKeyActorIdQuery()(
|
1405
|
+
{
|
1406
|
+
apiKey,
|
1407
|
+
},
|
1408
|
+
undefined,
|
1409
|
+
{ tx },
|
1410
|
+
);
|
1393
1411
|
if (apiKeyResult == null) {
|
1394
1412
|
// We reuse a constant permission error here as it will be cached, and
|
1395
1413
|
// using a single error instance can drastically reduce the memory used
|
@@ -1404,6 +1422,7 @@ const getApiKeyActorId = (() => {
|
|
1404
1422
|
{
|
1405
1423
|
promise: true,
|
1406
1424
|
primitive: true,
|
1425
|
+
normalizer: ([apiKey]) => apiKey,
|
1407
1426
|
},
|
1408
1427
|
);
|
1409
1428
|
})();
|
@@ -1411,13 +1430,14 @@ const getApiKeyActorId = (() => {
|
|
1411
1430
|
const checkApiKey = async (
|
1412
1431
|
req: PermissionReq,
|
1413
1432
|
apiKey: string,
|
1433
|
+
tx?: Tx,
|
1414
1434
|
): Promise<PermissionReq['apiKey']> => {
|
1415
1435
|
if (apiKey == null || req.apiKey != null) {
|
1416
1436
|
return;
|
1417
1437
|
}
|
1418
1438
|
let permissions: string[];
|
1419
1439
|
try {
|
1420
|
-
permissions = await getApiKeyPermissions(apiKey);
|
1440
|
+
permissions = await getApiKeyPermissions(apiKey, tx);
|
1421
1441
|
} catch (err) {
|
1422
1442
|
console.warn('Error with API key:', err);
|
1423
1443
|
// Ignore errors getting the api key and just use an empty permissions object.
|
@@ -1425,7 +1445,7 @@ const checkApiKey = async (
|
|
1425
1445
|
}
|
1426
1446
|
let actor;
|
1427
1447
|
if (permissions.length > 0) {
|
1428
|
-
actor = await getApiKeyActorId(apiKey);
|
1448
|
+
actor = await getApiKeyActorId(apiKey, tx);
|
1429
1449
|
}
|
1430
1450
|
const resolvedApiKey: PermissionReq['apiKey'] = {
|
1431
1451
|
key: apiKey,
|
@@ -1440,6 +1460,8 @@ const checkApiKey = async (
|
|
1440
1460
|
export const resolveAuthHeader = async (
|
1441
1461
|
req: Express.Request,
|
1442
1462
|
expectedScheme = 'Bearer',
|
1463
|
+
// TODO: Consider making tx the second argument in the next major
|
1464
|
+
tx?: Tx,
|
1443
1465
|
): Promise<PermissionReq['apiKey']> => {
|
1444
1466
|
const auth = req.header('Authorization');
|
1445
1467
|
if (!auth) {
|
@@ -1456,7 +1478,7 @@ export const resolveAuthHeader = async (
|
|
1456
1478
|
return;
|
1457
1479
|
}
|
1458
1480
|
|
1459
|
-
return await checkApiKey(req, apiKey);
|
1481
|
+
return await checkApiKey(req, apiKey, tx);
|
1460
1482
|
};
|
1461
1483
|
|
1462
1484
|
export const customAuthorizationMiddleware = (expectedScheme = 'Bearer') => {
|
@@ -1483,10 +1505,12 @@ export const authorizationMiddleware = customAuthorizationMiddleware();
|
|
1483
1505
|
export const resolveApiKey = async (
|
1484
1506
|
req: HookReq | Express.Request,
|
1485
1507
|
paramName = 'apikey',
|
1508
|
+
// TODO: Consider making tx the second argument in the next major
|
1509
|
+
tx?: Tx,
|
1486
1510
|
): Promise<PermissionReq['apiKey']> => {
|
1487
1511
|
const apiKey =
|
1488
1512
|
req.params[paramName] ?? req.body[paramName] ?? req.query[paramName];
|
1489
|
-
return await checkApiKey(req, apiKey);
|
1513
|
+
return await checkApiKey(req, apiKey, tx);
|
1490
1514
|
};
|
1491
1515
|
|
1492
1516
|
export const customApiKeyMiddleware = (paramName = 'apikey') => {
|
@@ -33,7 +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
|
36
|
+
import * as syncMigrator from '../migrator/sync';
|
37
37
|
import { generateODataMetadata } from '../odata-metadata/odata-metadata-generator';
|
38
38
|
|
39
39
|
// tslint:disable-next-line:no-var-requires
|
@@ -444,7 +444,7 @@ export const executeModels = async (
|
|
444
444
|
execModels.map(async (model) => {
|
445
445
|
const { apiRoot } = model;
|
446
446
|
|
447
|
-
await
|
447
|
+
await syncMigrator.run(tx, model);
|
448
448
|
const compiledModel = generateModels(model, db.engine);
|
449
449
|
|
450
450
|
// Create tables related to terms and fact types
|
@@ -461,7 +461,7 @@ export const executeModels = async (
|
|
461
461
|
}
|
462
462
|
await promise;
|
463
463
|
}
|
464
|
-
await
|
464
|
+
await syncMigrator.postRun(tx, model);
|
465
465
|
|
466
466
|
odataResponse.prepareModel(compiledModel.abstractSql);
|
467
467
|
deepFreeze(compiledModel.abstractSql);
|
@@ -4,7 +4,8 @@ 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/
|
7
|
+
import * as migrator from '../migrator/sync';
|
8
|
+
import * as migratorUtils from '../migrator/utils';
|
8
9
|
|
9
10
|
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
10
11
|
|
@@ -17,7 +18,7 @@ export * as env from '../config-loader/env';
|
|
17
18
|
export * as types from '../sbvr-api/common-types';
|
18
19
|
export * as hooks from '../sbvr-api/hooks';
|
19
20
|
export type { configLoader as ConfigLoader };
|
20
|
-
export type {
|
21
|
+
export type { migratorUtils as Migrator };
|
21
22
|
|
22
23
|
let envDatabaseOptions: dbModule.DatabaseOptions<string>;
|
23
24
|
if (dbModule.engines.websql != null) {
|