@balena/pinejs 15.0.0-build-15-x-4acccbc677d2f8b6c23ace10945cd167339fc8df-1 → 15.0.0-build-15-x-bf3a44dfe299de8966cf9bc201fa13fdf9d4a747-1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +1383 -66
  3. package/CHANGELOG.md +507 -2
  4. package/out/config-loader/config-loader.js +16 -4
  5. package/out/config-loader/config-loader.js.map +1 -1
  6. package/out/database-layer/db.d.ts +1 -0
  7. package/out/database-layer/db.js +3 -0
  8. package/out/database-layer/db.js.map +1 -1
  9. package/out/migrator/async.js +14 -8
  10. package/out/migrator/async.js.map +1 -1
  11. package/out/migrator/utils.d.ts +3 -4
  12. package/out/migrator/utils.js +11 -1
  13. package/out/migrator/utils.js.map +1 -1
  14. package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
  15. package/out/passport-pinejs/passport-pinejs.js +3 -3
  16. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  17. package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
  18. package/out/sbvr-api/abstract-sql.js.map +1 -1
  19. package/out/sbvr-api/hooks.js +1 -1
  20. package/out/sbvr-api/hooks.js.map +1 -1
  21. package/out/sbvr-api/permissions.js +1 -1
  22. package/out/sbvr-api/permissions.js.map +1 -1
  23. package/out/sbvr-api/sbvr-utils.d.ts +3 -3
  24. package/out/sbvr-api/sbvr-utils.js +28 -5
  25. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  26. package/out/sbvr-api/translations.js +26 -11
  27. package/out/sbvr-api/translations.js.map +1 -1
  28. package/out/sbvr-api/uri-parser.d.ts +1 -1
  29. package/out/sbvr-api/uri-parser.js.map +1 -1
  30. package/package.json +18 -16
  31. package/src/config-loader/config-loader.ts +24 -7
  32. package/src/database-layer/db.ts +3 -0
  33. package/src/migrator/async.ts +22 -12
  34. package/src/migrator/utils.ts +16 -5
  35. package/src/passport-pinejs/passport-pinejs.ts +4 -4
  36. package/src/sbvr-api/abstract-sql.ts +2 -1
  37. package/src/sbvr-api/hooks.ts +1 -1
  38. package/src/sbvr-api/permissions.ts +6 -4
  39. package/src/sbvr-api/sbvr-utils.ts +65 -7
  40. package/src/sbvr-api/translations.ts +43 -14
  41. package/src/sbvr-api/uri-parser.ts +3 -1
  42. package/tsconfig.dev.json +1 -0
@@ -14,7 +14,6 @@ type InitialMigrationStatus = MigrationStatus &
14
14
  import {
15
15
  MigrationTuple,
16
16
  AsyncMigrationFn,
17
- setExecutedMigrations,
18
17
  getExecutedMigrations,
19
18
  migratorEnv,
20
19
  lockMigrations,
@@ -64,19 +63,10 @@ const $run = async (
64
63
  }> = [];
65
64
 
66
65
  // get a transaction for setting up the async migrator
67
- const modelIsNew = await sbvrUtils.isModelNew(setupTx, modelName);
68
- const executedMigrations = await getExecutedMigrations(setupTx, modelName);
69
66
 
70
- if (modelIsNew) {
71
- (sbvrUtils.api.migrations?.logger.info ?? console.info)(
72
- 'First time model has executed, skipping async migrations',
73
- );
67
+ const executedMigrations = await getExecutedMigrations(setupTx, modelName);
74
68
 
75
- return await setExecutedMigrations(setupTx, modelName, [
76
- ...executedMigrations,
77
- ...Object.keys(migrations),
78
- ]);
79
- }
69
+ // if the model is new, the sync migration parts (marked by finalize=true) are already marked in the sync migration runner.
80
70
 
81
71
  /**
82
72
  * preflight check if there are already migrations executed before starting the async scheduler
@@ -246,6 +236,26 @@ const $run = async (
246
236
  // when it fails it would break the transaction for managing the migration status
247
237
  const migratedRows = await sbvrUtils.db.transaction(
248
238
  async (migrationTx) => {
239
+ // disable automatic close on the management transaction as the migration transaction consumes up to max autoClose time
240
+ // disable first here, to let if fail when it takes to long before coming here to actually migrate.
241
+ tx.disableAutomaticClose();
242
+ const rollbackMigrationTx = async () => {
243
+ // if the parent transaction fails for any reason, the actual running migration transaction has to be rolled back to stop parallel unsafe async migrations.
244
+ try {
245
+ if (!migrationTx.isClosed()) {
246
+ await migrationTx.rollback();
247
+ }
248
+ } catch (err) {
249
+ (
250
+ sbvrUtils.api.migrations?.logger.error ??
251
+ console.error
252
+ )(
253
+ `error rolling back pending async migration tx on mgmt tx end/rollback: ${key}: ${err}`,
254
+ );
255
+ }
256
+ };
257
+ tx.on('rollback', rollbackMigrationTx);
258
+ tx.on('end', rollbackMigrationTx);
249
259
  return (
250
260
  (await asyncRunnerMigratorFn?.(migrationTx)) ?? 0
251
261
  );
@@ -19,11 +19,17 @@ export const migrations: Migrations = {
19
19
  await tx.executeSql(`\
20
20
  ALTER TABLE "migration"
21
21
  MODIFY "executed migrations" JSON NOT NULL;`);
22
+ await tx.executeSql(`\
23
+ ALTER TABLE "migration status"
24
+ MODIFY "is backing off" JSON NOT NULL;`);
22
25
  break;
23
26
  case 'postgres':
24
27
  await tx.executeSql(`\
25
28
  ALTER TABLE "migration"
26
29
  ALTER COLUMN "executed migrations" SET DATA TYPE JSONB USING b::JSONB;`);
30
+ await tx.executeSql(`\
31
+ ALTER TABLE "migration status"
32
+ ALTER COLUMN "is backing off" SET DATA TYPE JSONB USING b::JSONB;`);
27
33
  break;
28
34
  // No need to migrate for websql
29
35
  }
@@ -53,8 +59,7 @@ export type AsyncMigrationFn = (
53
59
  ) => Resolvable<Result>;
54
60
 
55
61
  type AddFn<T extends {}, x extends 'sync' | 'async'> = T & {
56
- syncFn: MigrationFn;
57
- asyncFn: AsyncMigrationFn;
62
+ [key in `${x}Fn`]: key extends 'syncFn' ? MigrationFn : AsyncMigrationFn;
58
63
  } & {
59
64
  [key in `${x}Sql`]?: undefined;
60
65
  };
@@ -79,13 +84,19 @@ export type AsyncMigration =
79
84
  | AddFn<AddSql<BaseAsyncMigration, 'sync'>, 'async'>;
80
85
 
81
86
  export function isAsyncMigration(
82
- migration: AsyncMigration | RunnableMigrations,
87
+ migration: string | MigrationFn | AsyncMigration | RunnableMigrations,
83
88
  ): migration is AsyncMigration {
84
- return (migration as AsyncMigration).type === MigrationCategories.async;
89
+ return (
90
+ ((typeof (migration as AsyncMigration).asyncFn === 'function' ||
91
+ typeof (migration as AsyncMigration).asyncSql === 'string') &&
92
+ (typeof (migration as AsyncMigration).syncFn === 'function' ||
93
+ typeof (migration as AsyncMigration).syncSql === 'string')) ||
94
+ (migration as AsyncMigration).type === MigrationCategories.async
95
+ );
85
96
  }
86
97
 
87
98
  export function isSyncMigration(
88
- migration: string | MigrationFn | RunnableMigrations,
99
+ migration: string | MigrationFn | RunnableMigrations | AsyncMigration,
89
100
  ): migration is MigrationFn {
90
101
  return typeof migration === 'function' || typeof migration === 'string';
91
102
  }
@@ -10,7 +10,7 @@ import * as permissions from '../sbvr-api/permissions';
10
10
  export let login: (
11
11
  fn: (
12
12
  err: any,
13
- user: {} | undefined,
13
+ user: {} | null | false | undefined,
14
14
  req: Express.Request,
15
15
  res: Express.Response,
16
16
  next: Express.NextFunction,
@@ -54,15 +54,15 @@ const setup: ConfigLoader.SetupFunction = async (app: Express.Application) => {
54
54
  passport.use(new LocalStrategy(checkPassword));
55
55
 
56
56
  login = (fn) => (req, res, next) =>
57
- passport.authenticate('local', (err, user?) => {
58
- if (err || user == null) {
57
+ passport.authenticate('local', ((err, user) => {
58
+ if (err || user == null || user === false) {
59
59
  fn(err, user, req, res, next);
60
60
  return;
61
61
  }
62
62
  req.login(user, (error) => {
63
63
  fn(error, user, req, res, next);
64
64
  });
65
- })(req, res, next);
65
+ }) as Passport.AuthenticateCallback)(req, res, next);
66
66
 
67
67
  logout = (req, _res, next) => {
68
68
  req.logout((error) => {
@@ -1,6 +1,7 @@
1
1
  import * as _ from 'lodash';
2
2
 
3
3
  import * as AbstractSQLCompiler from '@balena/abstract-sql-compiler';
4
+ import type { BindKey } from '@balena/odata-parser';
4
5
  import {
5
6
  ODataBinds,
6
7
  odataNameToSqlName,
@@ -123,7 +124,7 @@ export const getAndCheckBindValues = async (
123
124
  throw new Error('Invalid binding');
124
125
  }
125
126
  let dataType;
126
- [dataType, value] = odataBinds[bindValue];
127
+ [dataType, value] = odataBinds[bindValue as BindKey];
127
128
  field = { dataType };
128
129
  } else {
129
130
  throw new Error(`Unknown binding: ${binding}`);
@@ -442,7 +442,7 @@ export const runHooks = async <T extends keyof Hooks>(
442
442
  await Promise.all(
443
443
  (modelHooks as Array<Hook<HookFn>>).map(async (hook) => {
444
444
  if (hook.readOnlyTx) {
445
- modelReadOnlyArgs ??= getReadOnlyArgs(modelName, args);
445
+ modelReadOnlyArgs ??= getReadOnlyArgs(modelName, modelArgs);
446
446
  await hook.run(modelReadOnlyArgs);
447
447
  } else {
448
448
  await hook.run(modelArgs);
@@ -1,8 +1,8 @@
1
1
  import type {
2
2
  AbstractSqlModel,
3
3
  AbstractSqlQuery,
4
- AbstractSqlType,
5
4
  AliasNode,
5
+ AnyTypeNodes,
6
6
  Definition,
7
7
  FieldNode,
8
8
  ReferencedFieldNode,
@@ -657,7 +657,7 @@ const generateConstrainedAbstractSql = (
657
657
  const select = abstractSqlQuery.find(
658
658
  (v): v is SelectNode => v[0] === 'Select',
659
659
  )!;
660
- select[1] = select[1].map((selectField): AbstractSqlType => {
660
+ select[1] = select[1].map((selectField): AnyTypeNodes => {
661
661
  if (selectField[0] === 'Alias') {
662
662
  const sqlName = odataNameToSqlName((selectField as AliasNode<any>)[2]);
663
663
  const maybeField = (
@@ -1045,7 +1045,9 @@ const getBoundConstrainedMemoizer = memoizeWeak(
1045
1045
  permissionsLookup,
1046
1046
  permissions,
1047
1047
  vocabulary,
1048
- sqlNameToODataName(permissionsTable.name),
1048
+ sqlNameToODataName(
1049
+ permissionsTable.modifyName ?? permissionsTable.name,
1050
+ ),
1049
1051
  ),
1050
1052
  );
1051
1053
 
@@ -1603,7 +1605,7 @@ const getGuestPermissions = memoize(
1603
1605
 
1604
1606
  const getReqPermissions = async (
1605
1607
  req: PermissionReq,
1606
- odataBinds: ODataBinds = [],
1608
+ odataBinds: ODataBinds = [] as any as ODataBinds,
1607
1609
  ) => {
1608
1610
  const guestPermissions = await (async () => {
1609
1611
  if (
@@ -23,6 +23,7 @@ import { version as AbstractSQLCompilerVersion } from '@balena/abstract-sql-comp
23
23
  import * as LF2AbstractSQL from '@balena/lf-to-abstract-sql';
24
24
 
25
25
  import {
26
+ ODataBinds,
26
27
  odataNameToSqlName,
27
28
  sqlNameToODataName,
28
29
  SupportedMethod,
@@ -372,12 +373,60 @@ export const isModelNew = async (
372
373
  return !cachedIsModelNew.has(modelName);
373
374
  };
374
375
 
376
+ const bindsForAffectedIds = (
377
+ bindings: AbstractSQLCompiler.Binding[],
378
+ request?: Pick<
379
+ uriParser.ODataRequest,
380
+ | 'vocabulary'
381
+ | 'abstractSqlModel'
382
+ | 'method'
383
+ | 'resourceName'
384
+ | 'affectedIds'
385
+ >,
386
+ ) => {
387
+ if (request?.affectedIds == null) {
388
+ return {};
389
+ }
390
+
391
+ const tableName =
392
+ getAbstractSqlModel(request).tables[resolveSynonym(request)].name;
393
+
394
+ // If we're deleting the affected IDs then we can't narrow our rule to
395
+ // those IDs that are now missing
396
+ const isDelete = request.method === 'DELETE';
397
+
398
+ const odataBinds: { [key: string]: any } = {};
399
+ for (const bind of bindings) {
400
+ if (
401
+ bind.length !== 2 ||
402
+ bind[0] !== 'Bind' ||
403
+ typeof bind[1] !== 'string'
404
+ ) {
405
+ continue;
406
+ }
407
+
408
+ const bindName = bind[1];
409
+ if (!isDelete && bindName === tableName) {
410
+ odataBinds[bindName] = ['Text', `{${request.affectedIds}}`];
411
+ } else {
412
+ odataBinds[bindName] = ['Text', '{}'];
413
+ }
414
+ }
415
+
416
+ return odataBinds;
417
+ };
418
+
375
419
  export const validateModel = async (
376
420
  tx: Db.Tx,
377
421
  modelName: string,
378
422
  request?: Pick<
379
423
  uriParser.ODataRequest,
380
- 'abstractSqlQuery' | 'modifiedFields' | 'method' | 'vocabulary'
424
+ | 'abstractSqlQuery'
425
+ | 'modifiedFields'
426
+ | 'method'
427
+ | 'vocabulary'
428
+ | 'resourceName'
429
+ | 'affectedIds'
381
430
  >,
382
431
  ): Promise<void> => {
383
432
  const { sql } = models[modelName];
@@ -394,7 +443,16 @@ export const validateModel = async (
394
443
  const values = await getAndCheckBindValues(
395
444
  {
396
445
  vocabulary: modelName,
397
- odataBinds: [],
446
+ // TODO: `odataBinds` is of type `ODataBinds`, which is an
447
+ // array with extra arbitrary fields. `getAndCheckBindValues`
448
+ // accepts that and also a pure object form for this
449
+ // argument. Given how arrays have predefined symbols,
450
+ // `bindsForAffectedIds` cannot return an `ODataBinds`.
451
+ // Both `ODataBinds` and `getAndCheckBindValues` should be
452
+ // fixed to accept a pure object with both string and
453
+ // numerical keys, for named and positional binds
454
+ // respectively
455
+ odataBinds: bindsForAffectedIds(rule.bindings, request) as any,
398
456
  values: {},
399
457
  engine: db.engine,
400
458
  },
@@ -832,7 +890,7 @@ export const runRule = (() => {
832
890
  const values = await getAndCheckBindValues(
833
891
  {
834
892
  vocabulary: vocab,
835
- odataBinds: [],
893
+ odataBinds: [] as any as ODataBinds,
836
894
  values: {},
837
895
  engine: db.engine,
838
896
  },
@@ -1479,7 +1537,8 @@ const updateBinds = (
1479
1537
  request: uriParser.ODataRequest,
1480
1538
  ) => {
1481
1539
  if (request._defer) {
1482
- request.odataBinds = request.odataBinds.map(([tag, id]) => {
1540
+ for (let i = 0; i < request.odataBinds.length; i++) {
1541
+ const [tag, id] = request.odataBinds[i];
1483
1542
  if (tag === 'ContentReference') {
1484
1543
  const ref = changeSetResults.get(id);
1485
1544
  if (
@@ -1491,10 +1550,9 @@ const updateBinds = (
1491
1550
  'Reference to a non existing resource in Changeset',
1492
1551
  );
1493
1552
  }
1494
- return uriParser.parseId(ref.body.id);
1553
+ request.odataBinds[i] = uriParser.parseId(ref.body.id);
1495
1554
  }
1496
- return [tag, id];
1497
- });
1555
+ }
1498
1556
  }
1499
1557
  return request;
1500
1558
  };
@@ -24,47 +24,75 @@ export type AliasValidNodeType =
24
24
  | UnknownTypeNodes
25
25
  | NullNode;
26
26
  const aliasFields = (
27
- abstractSqlModel: AbstractSqlModel,
28
- resourceName: string,
27
+ fromAbstractSqlModel: AbstractSqlModel,
28
+ toAbstractSqlModel: AbstractSqlModel,
29
+ fromResourceName: string,
30
+ toResource: string,
29
31
  aliases: Dictionary<string | AliasValidNodeType>,
30
32
  ): SelectNode[1] => {
31
- const fieldNames = abstractSqlModel.tables[resourceName].fields.map(
32
- ({ fieldName }) => fieldName,
33
- );
34
- const nonexistentFields = _.difference(Object.keys(aliases), fieldNames);
33
+ const fromFieldNames = fromAbstractSqlModel.tables[
34
+ fromResourceName
35
+ ].fields.map(({ fieldName }) => fieldName);
36
+ const nonexistentFields = _.difference(Object.keys(aliases), fromFieldNames);
35
37
  if (nonexistentFields.length > 0) {
36
38
  throw new Error(
37
39
  `Tried to alias non-existent fields: '${nonexistentFields.join(', ')}'`,
38
40
  );
39
41
  }
40
- return fieldNames.map(
42
+ const toFieldNames = toAbstractSqlModel.tables[toResource].fields.map(
43
+ ({ fieldName }) => fieldName,
44
+ );
45
+ const checkToFieldExists = (fromFieldName: string, toFieldName: string) => {
46
+ if (!toFieldNames.includes(toFieldName)) {
47
+ throw new Error(
48
+ `Tried to alias '${fromFieldName}' to the non-existent target field: '${toFieldName}'`,
49
+ );
50
+ }
51
+ };
52
+ return fromFieldNames.map(
41
53
  (fieldName): AliasNode<AliasValidNodeType> | ReferencedFieldNode => {
42
54
  const alias = aliases[fieldName];
43
55
  if (alias) {
44
56
  if (typeof alias === 'string') {
45
- return ['Alias', ['ReferencedField', resourceName, alias], fieldName];
57
+ checkToFieldExists(fieldName, alias);
58
+ return [
59
+ 'Alias',
60
+ ['ReferencedField', fromResourceName, alias],
61
+ fieldName,
62
+ ];
46
63
  }
47
64
  return ['Alias', alias, fieldName];
48
65
  }
49
- return ['ReferencedField', resourceName, fieldName];
66
+ checkToFieldExists(fieldName, fieldName);
67
+ return ['ReferencedField', fromResourceName, fieldName];
50
68
  },
51
69
  );
52
70
  };
53
71
 
54
72
  const aliasResource = (
55
- abstractSqlModel: AbstractSqlModel,
56
- resourceName: string,
73
+ fromAbstractSqlModel: AbstractSqlModel,
74
+ toAbstractSqlModel: AbstractSqlModel,
75
+ fromResourceName: string,
57
76
  toResource: string,
58
77
  aliases: Dictionary<string | AliasValidNodeType>,
59
78
  ): Definition => {
60
- if (!abstractSqlModel.tables[toResource]) {
79
+ if (!toAbstractSqlModel.tables[toResource]) {
61
80
  throw new Error(`Tried to alias to a non-existent resource: ${toResource}`);
62
81
  }
63
82
  return {
64
83
  abstractSql: [
65
84
  'SelectQuery',
66
- ['Select', aliasFields(abstractSqlModel, resourceName, aliases)],
67
- ['From', ['Alias', ['Resource', toResource], resourceName]],
85
+ [
86
+ 'Select',
87
+ aliasFields(
88
+ fromAbstractSqlModel,
89
+ toAbstractSqlModel,
90
+ fromResourceName,
91
+ toResource,
92
+ aliases,
93
+ ),
94
+ ],
95
+ ['From', ['Alias', ['Resource', toResource], fromResourceName]],
68
96
  ],
69
97
  };
70
98
  };
@@ -196,6 +224,7 @@ export const translateAbstractSqlModel = (
196
224
  } else {
197
225
  table.definition = aliasResource(
198
226
  fromAbstractSqlModel,
227
+ toAbstractSqlModel,
199
228
  key,
200
229
  toResource,
201
230
  definition,
@@ -124,7 +124,9 @@ export const memoizedParseOdata = (() => {
124
124
  },
125
125
  );
126
126
  parsed.tree.options ??= {};
127
- for (const key of Object.keys(parsedParams.tree)) {
127
+ for (const key of Object.keys(
128
+ parsedParams.tree,
129
+ ) as ODataParser.BindKey[]) {
128
130
  parsed.tree.options[key] = parsedParams.tree[key];
129
131
  parsed.binds[key] = parsedParams.binds[key];
130
132
  }
package/tsconfig.dev.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "include": [
7
7
  "build/**/*",
8
8
  "src/**/*",
9
+ "test/**/*",
9
10
  "typings/**/*.d.ts"
10
11
  ]
11
12
  }