@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.
@@ -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
- if (permissionIds.length > 0) {
142
- await authApiTx.delete({
143
- resource: 'user__has__permission',
144
- options: {
145
- $filter: {
146
- user: userID,
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
- switch (path.extname(filename)) {
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
- migrations[migrationKey] = nodeRequire(filePath);
319
+ assignMigrationWithCategory(
320
+ migrationKey,
321
+ nodeRequire(filePath),
322
+ );
277
323
  break;
278
324
  case '.sql':
279
- migrations[migrationKey] = await fs.promises.readFile(
280
- filePath,
281
- 'utf8',
325
+ assignMigrationWithCategory(
326
+ migrationKey,
327
+ await fs.promises.readFile(filePath, 'utf8'),
282
328
  );
283
329
  break;
284
330
  default:
@@ -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
- opts?: CacheFnOpts<T>,
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
- userId,
1219
- })) as Array<{ name: string }>;
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 (userId: number): Promise<string[]> => {
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
- apiKey,
1342
- })) as Array<{ name: string }>;
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
- apiKey,
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 migrator from '../migrator/migrator';
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 migrator.run(tx, model);
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 migrator.postRun(tx, model);
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/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 { migrator as Migrator };
21
+ export type { migratorUtils as Migrator };
21
22
 
22
23
  let envDatabaseOptions: dbModule.DatabaseOptions<string>;
23
24
  if (dbModule.engines.websql != null) {