@balena/pinejs 15.0.0-build-15-x-4681209f5dd8d896491fb5ed64aea47df511c14d-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 (51) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +1479 -103
  3. package/CHANGELOG.md +523 -2
  4. package/Dockerfile +1 -1
  5. package/out/bin/odata-compiler.js +4 -1
  6. package/out/bin/odata-compiler.js.map +1 -1
  7. package/out/config-loader/config-loader.d.ts +5 -2
  8. package/out/config-loader/config-loader.js +46 -18
  9. package/out/config-loader/config-loader.js.map +1 -1
  10. package/out/database-layer/db.d.ts +1 -0
  11. package/out/database-layer/db.js +3 -0
  12. package/out/database-layer/db.js.map +1 -1
  13. package/out/migrator/async.js +14 -8
  14. package/out/migrator/async.js.map +1 -1
  15. package/out/migrator/utils.d.ts +3 -4
  16. package/out/migrator/utils.js +11 -1
  17. package/out/migrator/utils.js.map +1 -1
  18. package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
  19. package/out/passport-pinejs/passport-pinejs.js +3 -3
  20. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  21. package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
  22. package/out/sbvr-api/abstract-sql.js +2 -1
  23. package/out/sbvr-api/abstract-sql.js.map +1 -1
  24. package/out/sbvr-api/hooks.d.ts +3 -3
  25. package/out/sbvr-api/hooks.js +44 -29
  26. package/out/sbvr-api/hooks.js.map +1 -1
  27. package/out/sbvr-api/permissions.js +6 -4
  28. package/out/sbvr-api/permissions.js.map +1 -1
  29. package/out/sbvr-api/sbvr-utils.d.ts +11 -5
  30. package/out/sbvr-api/sbvr-utils.js +151 -61
  31. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  32. package/out/sbvr-api/translations.d.ts +6 -0
  33. package/out/sbvr-api/translations.js +150 -0
  34. package/out/sbvr-api/translations.js.map +1 -0
  35. package/out/sbvr-api/uri-parser.d.ts +5 -2
  36. package/out/sbvr-api/uri-parser.js +2 -0
  37. package/out/sbvr-api/uri-parser.js.map +1 -1
  38. package/package.json +19 -17
  39. package/src/bin/odata-compiler.ts +4 -2
  40. package/src/config-loader/config-loader.ts +84 -29
  41. package/src/database-layer/db.ts +3 -0
  42. package/src/migrator/async.ts +22 -12
  43. package/src/migrator/utils.ts +16 -5
  44. package/src/passport-pinejs/passport-pinejs.ts +4 -4
  45. package/src/sbvr-api/abstract-sql.ts +4 -2
  46. package/src/sbvr-api/hooks.ts +80 -33
  47. package/src/sbvr-api/permissions.ts +15 -7
  48. package/src/sbvr-api/sbvr-utils.ts +258 -84
  49. package/src/sbvr-api/translations.ts +248 -0
  50. package/src/sbvr-api/uri-parser.ts +9 -2
  51. package/tsconfig.dev.json +1 -0
@@ -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,
@@ -95,7 +96,8 @@ export const getAndCheckBindValues = async (
95
96
 
96
97
  const sqlTableName = odataNameToSqlName(tableName);
97
98
  const sqlFieldName = odataNameToSqlName(fieldName);
98
- const maybeField = sqlModelTables[sqlTableName].fields.find(
99
+ const table = sqlModelTables[sqlTableName];
100
+ const maybeField = (table.modifyFields ?? table.fields).find(
99
101
  (f) => f.fieldName === sqlFieldName,
100
102
  );
101
103
  if (maybeField == null) {
@@ -122,7 +124,7 @@ export const getAndCheckBindValues = async (
122
124
  throw new Error('Invalid binding');
123
125
  }
124
126
  let dataType;
125
- [dataType, value] = odataBinds[bindValue];
127
+ [dataType, value] = odataBinds[bindValue as BindKey];
126
128
  field = { dataType };
127
129
  } else {
128
130
  throw new Error(`Unknown binding: ${binding}`);
@@ -42,7 +42,9 @@ export interface Hooks {
42
42
  PREPARSE?: (options: Omit<HookArgs, 'request' | 'api'>) => HookResponse;
43
43
  POSTPARSE?: (options: HookArgs) => HookResponse;
44
44
  PRERUN?: (options: HookArgs & { tx: Tx }) => HookResponse;
45
+ /** These are run in reverse translation order from newest to oldest */
45
46
  POSTRUN?: (options: HookArgs & { tx: Tx; result: any }) => HookResponse;
47
+ /** These are run in reverse translation order from newest to oldest */
46
48
  PRERESPOND?: (
47
49
  options: HookArgs & {
48
50
  tx: Tx;
@@ -53,6 +55,7 @@ export interface Hooks {
53
55
  data?: any;
54
56
  },
55
57
  ) => HookResponse;
58
+ /** These are run in reverse translation order from newest to oldest */
56
59
  'POSTRUN-ERROR'?: (
57
60
  options: HookArgs & { error: TypedError | any },
58
61
  ) => HookResponse;
@@ -126,13 +129,13 @@ class SideEffectHook<T extends HookFn> extends Hook<T> {
126
129
 
127
130
  // The execution order of rollback actions is unspecified
128
131
  export const rollbackRequestHooks = <T extends InstantiatedHooks>(
129
- hooks: T | undefined,
132
+ hooksList: Array<[modelName: string, hooks: T]> | undefined,
130
133
  ): void => {
131
- if (hooks == null) {
134
+ if (hooksList == null) {
132
135
  return;
133
136
  }
134
- const sideEffectHooks = Object.values(hooks)
135
- .flatMap((v): Array<Hook<HookFn>> => v)
137
+ const sideEffectHooks = hooksList
138
+ .flatMap(([, v]): Array<Hook<HookFn>> => Object.values(v).flat())
136
139
  .filter(
137
140
  (hook): hook is SideEffectHook<HookFn> => hook instanceof SideEffectHook,
138
141
  );
@@ -177,21 +180,37 @@ const getResourceHooks = (vocabHooks: VocabHooks, resourceName?: string) => {
177
180
  const getVocabHooks = (
178
181
  methodHooks: MethodHooks,
179
182
  vocabulary: string,
180
- resourceName?: string,
183
+ resourceName: string | undefined,
184
+ includeAllVocab: boolean,
181
185
  ) => {
182
186
  if (methodHooks == null) {
183
187
  return {};
184
188
  }
189
+ const vocabHooks = getResourceHooks(methodHooks[vocabulary], resourceName);
190
+ if (!includeAllVocab) {
191
+ // Do not include `vocabulary='all'` hooks, useful for translated vocabularies
192
+ return vocabHooks;
193
+ }
185
194
  return mergeHooks(
186
- getResourceHooks(methodHooks[vocabulary], resourceName),
195
+ vocabHooks,
187
196
  getResourceHooks(methodHooks['all'], resourceName),
188
197
  );
189
198
  };
190
199
  const getMethodHooks = memoize(
191
- (method: SupportedMethod, vocabulary: string, resourceName?: string) =>
200
+ (
201
+ method: SupportedMethod,
202
+ vocabulary: string,
203
+ resourceName: string | undefined,
204
+ includeAllVocab: boolean,
205
+ ) =>
192
206
  mergeHooks(
193
- getVocabHooks(apiHooks[method], vocabulary, resourceName),
194
- getVocabHooks(apiHooks['all'], vocabulary, resourceName),
207
+ getVocabHooks(
208
+ apiHooks[method],
209
+ vocabulary,
210
+ resourceName,
211
+ includeAllVocab,
212
+ ),
213
+ getVocabHooks(apiHooks['all'], vocabulary, resourceName, includeAllVocab),
195
214
  ),
196
215
  { primitive: true },
197
216
  );
@@ -200,6 +219,7 @@ export const getHooks = (
200
219
  OptionalField<ParsedODataRequest, 'resourceName'>,
201
220
  'resourceName' | 'method' | 'vocabulary'
202
221
  >,
222
+ includeAllVocab: boolean,
203
223
  ): InstantiatedHooks => {
204
224
  let { resourceName } = request;
205
225
  if (resourceName != null) {
@@ -208,10 +228,17 @@ export const getHooks = (
208
228
  ParsedODataRequest,
209
229
  'resourceName' | 'method' | 'vocabulary'
210
230
  >,
211
- );
231
+ )
232
+ // Remove version suffixes
233
+ .replace(/\$.*$/, '');
212
234
  }
213
235
  return instantiateHooks(
214
- getMethodHooks(request.method, request.vocabulary, resourceName),
236
+ getMethodHooks(
237
+ request.method,
238
+ request.vocabulary,
239
+ resourceName,
240
+ includeAllVocab,
241
+ ),
215
242
  );
216
243
  };
217
244
  getHooks.clear = () => getMethodHooks.clear();
@@ -343,12 +370,11 @@ export const addPureHook = (
343
370
  });
344
371
  };
345
372
 
346
- const defineApi = (args: HookArgs) => {
347
- const { request, req, tx } = args;
348
- const { vocabulary } = request;
373
+ const defineApi = (modelName: string, args: HookArgs) => {
374
+ const { req, tx } = args;
349
375
  Object.defineProperty(args, 'api', {
350
376
  get: _.once(() =>
351
- api[vocabulary].clone({
377
+ api[modelName].clone({
352
378
  passthrough: { req, tx },
353
379
  }),
354
380
  ),
@@ -360,6 +386,7 @@ type RunHookArgs<T extends keyof Hooks> = Omit<
360
386
  'api'
361
387
  >;
362
388
  const getReadOnlyArgs = <T extends keyof Hooks>(
389
+ modelName: string,
363
390
  args: RunHookArgs<T>,
364
391
  ): RunHookArgs<T> => {
365
392
  if (args.tx == null || args.tx.isReadOnly()) {
@@ -369,38 +396,58 @@ const getReadOnlyArgs = <T extends keyof Hooks>(
369
396
  let readOnlyArgs: typeof args;
370
397
  readOnlyArgs = { ...args, tx: args.tx.asReadOnly() };
371
398
  if ((args as HookArgs).request != null) {
372
- defineApi(readOnlyArgs as HookArgs);
399
+ defineApi(modelName, readOnlyArgs as HookArgs);
373
400
  }
374
401
  return readOnlyArgs;
375
402
  };
376
403
 
377
404
  export const runHooks = async <T extends keyof Hooks>(
378
405
  hookName: T,
379
- hooksList: InstantiatedHooks | undefined,
406
+ /**
407
+ * A list of modelName/hooks to run in order, which will be reversed for hooks after the "RUN" stage,
408
+ * ie POSTRUN/PRERESPOND/POSTRUN-ERROR
409
+ */
410
+ hooksList: Array<[modelName: string, hooks: InstantiatedHooks]> | undefined,
380
411
  args: RunHookArgs<T>,
381
412
  ) => {
382
413
  if (hooksList == null) {
383
414
  return;
384
415
  }
385
- const hooks = hooksList[hookName];
386
- if (hooks == null || hooks.length === 0) {
416
+ const hooks = hooksList
417
+ .map(([modelName, $hooks]): [string, InstantiatedHooks[T] | undefined] => [
418
+ modelName,
419
+ $hooks[hookName],
420
+ ])
421
+ .filter(
422
+ (v): v is [string, InstantiatedHooks[T]] =>
423
+ v[1] != null && v[1].length > 0,
424
+ );
425
+ if (hooks.length === 0) {
387
426
  return;
388
427
  }
389
-
390
- if ((args as HookArgs).request != null) {
391
- defineApi(args as HookArgs);
428
+ if (['POSTRUN', 'PRERESPOND', 'POSTRUN-ERROR'].includes(hookName)) {
429
+ // Any hooks after we "run" the query are executed in reverse order from newest to oldest
430
+ // as they'll be translating the query results from "latest" backwards to the model that
431
+ // was actually requested
432
+ hooks.reverse();
392
433
  }
393
434
 
394
- let readOnlyArgs: RunHookArgs<T>;
435
+ for (const [modelName, modelHooks] of hooks) {
436
+ const modelArgs = { ...args };
437
+ let modelReadOnlyArgs: typeof modelArgs;
438
+ if ((args as HookArgs).request != null) {
439
+ defineApi(modelName, modelArgs as HookArgs);
440
+ }
395
441
 
396
- await Promise.all(
397
- (hooks as Array<Hook<HookFn>>).map(async (hook) => {
398
- if (hook.readOnlyTx) {
399
- readOnlyArgs ??= getReadOnlyArgs(args);
400
- await hook.run(readOnlyArgs);
401
- } else {
402
- await hook.run(args);
403
- }
404
- }),
405
- );
442
+ await Promise.all(
443
+ (modelHooks as Array<Hook<HookFn>>).map(async (hook) => {
444
+ if (hook.readOnlyTx) {
445
+ modelReadOnlyArgs ??= getReadOnlyArgs(modelName, modelArgs);
446
+ await hook.run(modelReadOnlyArgs);
447
+ } else {
448
+ await hook.run(modelArgs);
449
+ }
450
+ }),
451
+ );
452
+ }
406
453
  };
@@ -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,
@@ -140,7 +140,7 @@ const $parsePermissions = env.createCache(
140
140
  },
141
141
  );
142
142
 
143
- const rewriteBinds = (
143
+ const rewriteODataBinds = (
144
144
  { tree, extraBinds }: { tree: ODataQuery; extraBinds: ODataBinds },
145
145
  odataBinds: ODataBinds,
146
146
  ): ODataQuery => {
@@ -163,7 +163,7 @@ const parsePermissions = (
163
163
  odataBinds: ODataBinds,
164
164
  ): ODataQuery => {
165
165
  const odata = $parsePermissions(filter);
166
- return rewriteBinds(odata, odataBinds);
166
+ return rewriteODataBinds(odata, odataBinds);
167
167
  };
168
168
 
169
169
  // Traverses all values in `check`, actions for the following data types:
@@ -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,9 +1045,12 @@ 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
  );
1053
+
1051
1054
  return permissionsTable;
1052
1055
  },
1053
1056
  },
@@ -1602,7 +1605,7 @@ const getGuestPermissions = memoize(
1602
1605
 
1603
1606
  const getReqPermissions = async (
1604
1607
  req: PermissionReq,
1605
- odataBinds: ODataBinds = [],
1608
+ odataBinds: ODataBinds = [] as any as ODataBinds,
1606
1609
  ) => {
1607
1610
  const guestPermissions = await (async () => {
1608
1611
  if (
@@ -1636,7 +1639,8 @@ export const addPermissions = async (
1636
1639
  req: PermissionReq,
1637
1640
  request: ODataRequest & { permissionType?: PermissionCheck },
1638
1641
  ): Promise<void> => {
1639
- const { vocabulary, resourceName, odataQuery, odataBinds } = request;
1642
+ const { resourceName, odataQuery, odataBinds } = request;
1643
+ const vocabulary = _.last(request.translateVersions)!;
1640
1644
  let abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
1641
1645
 
1642
1646
  let { permissionType } = request;
@@ -1759,6 +1763,10 @@ export const setup = () => {
1759
1763
  0,
1760
1764
  -'#canAccess'.length,
1761
1765
  );
1766
+ request.originalResourceName = request.originalResourceName.slice(
1767
+ 0,
1768
+ -'#canAccess'.length,
1769
+ );
1762
1770
  const resourceName = sbvrUtils.resolveSynonym(request);
1763
1771
  const resourceTable = abstractSqlModel.tables[resourceName];
1764
1772
  if (resourceTable == null) {