@balena/pinejs 15.0.0-delete-state-default-user-permissions-8b5de27c634f2e7581f0bcf3600066d2fe4bbf40 → 15.0.0-deprecate-node12-8a99d72ae66d7708293afc56c5d7eb19b39081cd

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. package/.resinci.yml +0 -1
  2. package/.versionbot/CHANGELOG.yml +158 -9
  3. package/CHANGELOG.md +57 -2
  4. package/out/bin/utils.js +1 -1
  5. package/out/bin/utils.js.map +1 -1
  6. package/out/config-loader/config-loader.d.ts +1 -1
  7. package/out/config-loader/config-loader.js +10 -28
  8. package/out/config-loader/config-loader.js.map +1 -1
  9. package/out/data-server/sbvr-server.js +8 -8
  10. package/out/data-server/sbvr-server.js.map +1 -1
  11. package/out/database-layer/db.d.ts +1 -1
  12. package/out/database-layer/db.js +3 -3
  13. package/out/database-layer/db.js.map +1 -1
  14. package/out/express-emulator/express.js +1 -1
  15. package/out/express-emulator/express.js.map +1 -1
  16. package/out/http-transactions/transactions.js +4 -4
  17. package/out/http-transactions/transactions.js.map +1 -1
  18. package/out/migrator/sync.js +6 -6
  19. package/out/migrator/sync.js.map +1 -1
  20. package/out/migrator/utils.d.ts +2 -2
  21. package/out/migrator/utils.js +16 -17
  22. package/out/migrator/utils.js.map +1 -1
  23. package/out/odata-metadata/odata-metadata-generator.js +6 -9
  24. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  25. package/out/passport-pinejs/passport-pinejs.js +1 -1
  26. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  27. package/out/pinejs-session-store/pinejs-session-store.js +1 -1
  28. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  29. package/out/sbvr-api/abstract-sql.d.ts +1 -1
  30. package/out/sbvr-api/abstract-sql.js.map +1 -1
  31. package/out/sbvr-api/control-flow.js.map +1 -1
  32. package/out/sbvr-api/hooks.d.ts +6 -3
  33. package/out/sbvr-api/hooks.js +3 -3
  34. package/out/sbvr-api/hooks.js.map +1 -1
  35. package/out/sbvr-api/odata-response.js +5 -5
  36. package/out/sbvr-api/odata-response.js.map +1 -1
  37. package/out/sbvr-api/permissions.js +25 -20
  38. package/out/sbvr-api/permissions.js.map +1 -1
  39. package/out/sbvr-api/sbvr-utils.d.ts +4 -3
  40. package/out/sbvr-api/sbvr-utils.js +68 -45
  41. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  42. package/out/sbvr-api/uri-parser.d.ts +13 -11
  43. package/out/sbvr-api/uri-parser.js +4 -4
  44. package/out/sbvr-api/uri-parser.js.map +1 -1
  45. package/out/server-glue/module.js.map +1 -1
  46. package/package.json +13 -13
  47. package/src/bin/utils.ts +1 -1
  48. package/src/config-loader/config-loader.ts +15 -35
  49. package/src/data-server/sbvr-server.js +8 -8
  50. package/src/database-layer/db.ts +11 -11
  51. package/src/express-emulator/express.js +1 -1
  52. package/src/http-transactions/transactions.js +4 -4
  53. package/src/migrator/sync.ts +9 -11
  54. package/src/migrator/utils.ts +18 -24
  55. package/src/odata-metadata/odata-metadata-generator.ts +8 -11
  56. package/src/sbvr-api/abstract-sql.ts +6 -3
  57. package/src/sbvr-api/control-flow.ts +2 -2
  58. package/src/sbvr-api/hooks.ts +18 -8
  59. package/src/sbvr-api/odata-response.ts +4 -4
  60. package/src/sbvr-api/permissions.ts +42 -36
  61. package/src/sbvr-api/sbvr-utils.ts +118 -55
  62. package/src/sbvr-api/uri-parser.ts +29 -21
  63. package/src/server-glue/module.ts +1 -1
  64. package/tsconfig.json +1 -3
  65. package/typings/lf-to-abstract-sql.d.ts +6 -9
@@ -1,6 +1,6 @@
1
1
  import type { OptionalField, Resolvable } from './common-types';
2
2
  import type { Tx } from '../database-layer/db';
3
- import type { ODataRequest } from './uri-parser';
3
+ import type { ODataRequest, ParsedODataRequest } from './uri-parser';
4
4
  import type { AnyObject } from 'pinejs-client-core';
5
5
  import type { TypedError } from 'typed-error';
6
6
  import type { SupportedMethod } from '@balena/odata-to-abstract-sql';
@@ -15,6 +15,7 @@ import {
15
15
  resolveSynonym,
16
16
  getAbstractSqlModel,
17
17
  api,
18
+ Response,
18
19
  } from './sbvr-utils';
19
20
 
20
21
  export interface HookReq {
@@ -46,6 +47,9 @@ export interface Hooks {
46
47
  options: HookArgs & {
47
48
  tx: Tx;
48
49
  result: any;
50
+ /** This can be mutated to modify the response sent to the client */
51
+ response: Response;
52
+ /** @deprecated Use the response object instead */
49
53
  data?: any;
50
54
  },
51
55
  ) => HookResponse;
@@ -127,11 +131,14 @@ export const rollbackRequestHooks = <T extends InstantiatedHooks>(
127
131
  if (hooks == null) {
128
132
  return;
129
133
  }
130
- settleMapSeries(_(hooks).flatMap().compact().value(), async (hook) => {
131
- if (hook instanceof SideEffectHook) {
132
- await hook.rollback();
133
- }
134
- });
134
+ settleMapSeries(
135
+ Object.values(hooks).flatMap((v): Array<Hook<HookFn>> => v),
136
+ async (hook) => {
137
+ if (hook instanceof SideEffectHook) {
138
+ await hook.rollback();
139
+ }
140
+ },
141
+ );
135
142
  };
136
143
 
137
144
  const instantiateHooks = (hooks: HookBlueprints): InstantiatedHooks =>
@@ -187,14 +194,17 @@ const getMethodHooks = memoize(
187
194
  );
188
195
  export const getHooks = (
189
196
  request: Pick<
190
- OptionalField<ODataRequest, 'resourceName'>,
197
+ OptionalField<ParsedODataRequest, 'resourceName'>,
191
198
  'resourceName' | 'method' | 'vocabulary'
192
199
  >,
193
200
  ): InstantiatedHooks => {
194
201
  let { resourceName } = request;
195
202
  if (resourceName != null) {
196
203
  resourceName = resolveSynonym(
197
- request as Pick<ODataRequest, 'resourceName' | 'method' | 'vocabulary'>,
204
+ request as Pick<
205
+ ParsedODataRequest,
206
+ 'resourceName' | 'method' | 'vocabulary'
207
+ >,
198
208
  );
199
209
  }
200
210
  return instantiateHooks(
@@ -148,16 +148,16 @@ export const process = async (
148
148
  );
149
149
 
150
150
  const odataIdField = sqlNameToODataName(table.idField);
151
- rows.forEach((row) => {
152
- processedFields.forEach((fieldName) => {
151
+ for (const row of rows) {
152
+ for (const fieldName of processedFields) {
153
153
  row[fieldName] = fetchProcessingFields[fieldName](row[fieldName]);
154
- });
154
+ }
155
155
  if (includeMetadata) {
156
156
  row.__metadata = {
157
157
  uri: resourceURI(vocab, resourceName, row[odataIdField]),
158
158
  };
159
159
  }
160
- });
160
+ }
161
161
 
162
162
  if (expandableFields.length > 0) {
163
163
  await Promise.all(
@@ -22,7 +22,7 @@ import type {
22
22
  } from '@balena/odata-parser';
23
23
  import type { Tx } from '../database-layer/db';
24
24
  import type { ApiKey, User } from '../sbvr-api/sbvr-utils';
25
- import type { AnyObject } from './common-types';
25
+ import type { AnyObject, Dictionary } from './common-types';
26
26
 
27
27
  import {
28
28
  isBindReference,
@@ -172,7 +172,7 @@ const parsePermissions = (
172
172
  // array: Treated as an AND of all elements
173
173
  // object: Must have only one key of either `AND` or `OR`, with an array value that will be treated according to the key.
174
174
  const isAnd = <T>(x: any): x is NestedCheckAnd<T> =>
175
- _.isObject(x) && 'and' in x;
175
+ typeof x === 'object' && 'and' in x;
176
176
  const isOr = <T>(x: any): x is NestedCheckOr<T> =>
177
177
  typeof x === 'object' && 'or' in x;
178
178
  export function nestedCheck<I, O>(
@@ -312,7 +312,7 @@ const namespaceRelationships = (
312
312
  });
313
313
  };
314
314
 
315
- type PermissionLookup = _.Dictionary<true | string[]>;
315
+ type PermissionLookup = Dictionary<true | string[]>;
316
316
 
317
317
  const getPermissionsLookup = env.createCache(
318
318
  'permissionsLookup',
@@ -420,9 +420,9 @@ const convertToLambda = (filter: AnyObject, identifier: string) => {
420
420
  return;
421
421
  }
422
422
  if (Array.isArray(object)) {
423
- object.forEach((element) => {
423
+ for (const element of object) {
424
424
  replaceObject(element);
425
- });
425
+ }
426
426
  }
427
427
 
428
428
  if (object.hasOwnProperty('name')) {
@@ -445,7 +445,7 @@ const rewriteSubPermissionBindings = (filter: AnyObject, counter: number) => {
445
445
  object.bind = counter + object.bind;
446
446
  }
447
447
 
448
- if (Array.isArray(object) || _.isObject(object)) {
448
+ if (Array.isArray(object) || typeof object === 'object') {
449
449
  _.forEach(object, (v) => {
450
450
  rewrite(v);
451
451
  });
@@ -487,7 +487,7 @@ const buildODataPermission = (
487
487
  return {
488
488
  filter: parsePermissions(permissionCheck, odata.binds),
489
489
  };
490
- } catch (e) {
490
+ } catch (e: any) {
491
491
  console.warn(
492
492
  'Failed to parse conditional permissions: ',
493
493
  permissionCheck,
@@ -704,7 +704,7 @@ const onceGetter = <T, U extends keyof T>(
704
704
  // and the delete removes that restriction
705
705
  delete this[propName];
706
706
  return (this[propName] = result);
707
- } catch (e) {
707
+ } catch (e: any) {
708
708
  thrownErr = e;
709
709
  throw thrownErr;
710
710
  } finally {
@@ -717,7 +717,7 @@ const onceGetter = <T, U extends keyof T>(
717
717
  const deepFreezeExceptDefinition = (obj: AnyObject) => {
718
718
  Object.freeze(obj);
719
719
 
720
- Object.getOwnPropertyNames(obj).forEach((prop) => {
720
+ for (const prop of Object.getOwnPropertyNames(obj)) {
721
721
  // We skip the definition because we know it's a property we've defined that will throw an error in some cases
722
722
  if (
723
723
  prop !== 'definition' &&
@@ -727,7 +727,7 @@ const deepFreezeExceptDefinition = (obj: AnyObject) => {
727
727
  ) {
728
728
  deepFreezeExceptDefinition(obj);
729
729
  }
730
- });
730
+ }
731
731
  };
732
732
 
733
733
  const createBypassDefinition = (definition: Definition) =>
@@ -880,7 +880,7 @@ const rewriteRelationship = memoizeWeak(
880
880
  canAccess: canAccessFunction,
881
881
  },
882
882
  );
883
- } catch (e) {
883
+ } catch (e: any) {
884
884
  throw new ODataParser.SyntaxError(e);
885
885
  }
886
886
  if (foundCanAccessLink) {
@@ -908,7 +908,7 @@ const rewriteRelationship = memoizeWeak(
908
908
  }
909
909
  }
910
910
 
911
- if (Array.isArray(object) || _.isObject(object)) {
911
+ if (Array.isArray(object) || typeof object === 'object') {
912
912
  _.forEach(object, (v) => {
913
913
  // we want to recurse into the relationship path, but
914
914
  // in case we hit a plain string, we don't need to bother
@@ -963,7 +963,9 @@ const getBoundConstrainedMemoizer = memoizeWeak(
963
963
  (permissionsLookup: PermissionLookup, vocabulary: string) => {
964
964
  const constrainedAbstractSqlModel = _.cloneDeep(abstractSqlModel);
965
965
 
966
- const origSynonyms = Object.keys(constrainedAbstractSqlModel.synonyms);
966
+ const origSynonyms = Object.entries(
967
+ constrainedAbstractSqlModel.synonyms,
968
+ );
967
969
  constrainedAbstractSqlModel.synonyms = new Proxy(
968
970
  constrainedAbstractSqlModel.synonyms,
969
971
  {
@@ -975,9 +977,9 @@ const getBoundConstrainedMemoizer = memoizeWeak(
975
977
  if (!alias) {
976
978
  return;
977
979
  }
978
- origSynonyms.forEach((canonicalForm, synonym) => {
980
+ for (const [synonym, canonicalForm] of origSynonyms) {
979
981
  synonyms[`${synonym}$${alias}`] = `${canonicalForm}$${alias}`;
980
- });
982
+ }
981
983
  return synonyms[permissionSynonym];
982
984
  },
983
985
  },
@@ -1242,7 +1244,7 @@ export const getUserPermissions = async (
1242
1244
  }
1243
1245
  try {
1244
1246
  return await $getUserPermissions(userId, tx);
1245
- } catch (err) {
1247
+ } catch (err: any) {
1246
1248
  sbvrUtils.api.Auth.logger.error('Error loading user permissions', err);
1247
1249
  throw err;
1248
1250
  }
@@ -1370,7 +1372,7 @@ export const getApiKeyPermissions = async (
1370
1372
  }
1371
1373
  try {
1372
1374
  return await $getApiKeyPermissions(apiKey, tx);
1373
- } catch (err) {
1375
+ } catch (err: any) {
1374
1376
  sbvrUtils.api.Auth.logger.error('Error loading api key permissions', err);
1375
1377
  throw err;
1376
1378
  }
@@ -1438,7 +1440,7 @@ const checkApiKey = async (
1438
1440
  let permissions: string[];
1439
1441
  try {
1440
1442
  permissions = await getApiKeyPermissions(apiKey, tx);
1441
- } catch (err) {
1443
+ } catch (err: any) {
1442
1444
  console.warn('Error with API key:', err);
1443
1445
  // Ignore errors getting the api key and just use an empty permissions object.
1444
1446
  permissions = [];
@@ -1568,7 +1570,7 @@ export const checkPermissionsMiddleware =
1568
1570
  'checkPermissionsMiddleware returned a conditional permission',
1569
1571
  );
1570
1572
  }
1571
- } catch (err) {
1573
+ } catch (err: any) {
1572
1574
  sbvrUtils.api.Auth.logger.error(
1573
1575
  'Error checking permissions',
1574
1576
  err,
@@ -1578,6 +1580,7 @@ export const checkPermissionsMiddleware =
1578
1580
  }
1579
1581
  };
1580
1582
 
1583
+ let guestPermissionsInitialized = false;
1581
1584
  const getGuestPermissions = memoize(
1582
1585
  async () => {
1583
1586
  // Get guest user
@@ -1601,6 +1604,7 @@ const getGuestPermissions = memoize(
1601
1604
  if (guestPermissions.some((p) => DEFAULT_ACTOR_BIND_REGEX.test(p))) {
1602
1605
  throw new Error('Guest permissions cannot reference actors');
1603
1606
  }
1607
+ guestPermissionsInitialized = true;
1604
1608
  return guestPermissions;
1605
1609
  },
1606
1610
  { promise: true },
@@ -1611,7 +1615,18 @@ const getReqPermissions = async (
1611
1615
  odataBinds: ODataBinds = [],
1612
1616
  ) => {
1613
1617
  const [guestPermissions] = await Promise.all([
1614
- getGuestPermissions(),
1618
+ (async () => {
1619
+ if (
1620
+ guestPermissionsInitialized === false &&
1621
+ (req.user === root.user || req.user === rootRead.user)
1622
+ ) {
1623
+ // In the case that guest permissions are not initialized yet and the query is being made with root permissions
1624
+ // then we need to bypass `getGuestPermissions` as it will cause an infinite loop back to here.
1625
+ // Therefore to break that loop we just ignore guest permissions.
1626
+ return [];
1627
+ }
1628
+ return await getGuestPermissions();
1629
+ })(),
1615
1630
  (async () => {
1616
1631
  // TODO: Remove this extra actor ID lookup making actor non-optional and updating open-balena-api.
1617
1632
  if (
@@ -1666,20 +1681,6 @@ export const addPermissions = async (
1666
1681
  }
1667
1682
  }
1668
1683
 
1669
- // This bypasses in the root cases, needed for fetching guest permissions to work, it can almost certainly be done better though
1670
- let permissions = req.user?.permissions ?? [];
1671
- permissions = permissions.concat(req.apiKey?.permissions ?? []);
1672
- if (
1673
- permissions.length > 0 &&
1674
- $checkPermissions(
1675
- getPermissionsLookup(permissions),
1676
- permissionType,
1677
- vocabulary,
1678
- ) === true
1679
- ) {
1680
- // We have unconditional permission to access the vocab so there's no need to intercept anything
1681
- return;
1682
- }
1683
1684
  const permissionsLookup = await getReqPermissions(req, odataBinds);
1684
1685
  // Update the request's abstract sql model to use the constrained version
1685
1686
  request.abstractSqlModel = abstractSqlModel = memoizedGetConstrainedModel(
@@ -1799,8 +1800,13 @@ export const setup = () => {
1799
1800
  }
1800
1801
  await addPermissions(req, request);
1801
1802
  },
1802
- PRERESPOND: ({ request, data }) => {
1803
- if (request.custom.isAction === 'canAccess' && _.isEmpty(data)) {
1803
+ PRERESPOND: ({ request, response }) => {
1804
+ if (
1805
+ request.custom.isAction === 'canAccess' &&
1806
+ (response.body == null ||
1807
+ typeof response.body === 'string' ||
1808
+ _.isEmpty(response.body?.d))
1809
+ ) {
1804
1810
  // If the caller does not have any permissions to access the
1805
1811
  // resource pine will throw a PermissionError. To have the
1806
1812
  // same behavior for the case that the user has permissions
@@ -125,8 +125,8 @@ export interface ApiKey extends Actor {
125
125
  actor?: number;
126
126
  }
127
127
 
128
- interface Response {
129
- status?: number;
128
+ export interface Response {
129
+ statusCode: number;
130
130
  headers?:
131
131
  | {
132
132
  [headerName: string]: any;
@@ -171,13 +171,12 @@ const memoizedResolveNavigationResource = memoizeWeak(
171
171
  resourceName: string,
172
172
  navigationName: string,
173
173
  ): string => {
174
- const navigation = _(odataNameToSqlName(navigationName))
174
+ const navigation = odataNameToSqlName(navigationName)
175
175
  .split('-')
176
176
  .flatMap((namePart) =>
177
177
  memoizedResolvedSynonym(abstractSqlModel, namePart).split('-'),
178
- )
179
- .concat('$')
180
- .value();
178
+ );
179
+ navigation.push('$');
181
180
  const resolvedResourceName = memoizedResolvedSynonym(
182
181
  abstractSqlModel,
183
182
  resourceName,
@@ -330,10 +329,49 @@ const prettifyConstraintError = (
330
329
  }
331
330
  };
332
331
 
332
+ let cachedIsModelNew: Set<string> | undefined;
333
+
334
+ /**
335
+ *
336
+ * @param tx database transaction - Needs to be executed in one contained config-loader transaction
337
+ * @param modelName name of the model to check the database for existence
338
+ * @returns true when the model was existing in database before pine config-loader is executed,
339
+ * false when the model was not existing before config-loader was executed.
340
+ *
341
+ * ATTENTION: This function needs to be executed in the same transaction as all model manipulating
342
+ * operations are executed in (as migration table operations). Otherwise it's possible that an INSERT INTO "model"
343
+ * statement executes and succeeds. Then afterwards execution fails and the isModelNew request would return `false`.
344
+ * In the case of migrations the table wouldn't have an entry and therefore it would try to run all the historical
345
+ * migrations on a new model.
346
+ */
347
+
348
+ export const isModelNew = async (
349
+ tx: Db.Tx,
350
+ modelName: string,
351
+ ): Promise<boolean> => {
352
+ const result = await tx.tableList("name = 'model'");
353
+ if (result.rows.length === 0) {
354
+ return true;
355
+ }
356
+ if (cachedIsModelNew == null) {
357
+ const { rows } = await tx.executeSql(
358
+ `SELECT "is of-vocabulary" FROM "model";`,
359
+ );
360
+ cachedIsModelNew = new Set<string>(
361
+ rows.map((row) => row['is of-vocabulary']),
362
+ );
363
+ }
364
+
365
+ return !cachedIsModelNew.has(modelName);
366
+ };
367
+
333
368
  export const validateModel = async (
334
369
  tx: Db.Tx,
335
370
  modelName: string,
336
- request?: uriParser.ODataRequest,
371
+ request?: Pick<
372
+ uriParser.ODataRequest,
373
+ 'abstractSqlQuery' | 'modifiedFields' | 'method' | 'vocabulary'
374
+ >,
337
375
  ): Promise<void> => {
338
376
  await Promise.all(
339
377
  models[modelName].sql.rules.map(async (rule) => {
@@ -550,7 +588,7 @@ export const executeModels = async (
550
588
  );
551
589
  }),
552
590
  );
553
- } catch (err) {
591
+ } catch (err: any) {
554
592
  await Promise.all(
555
593
  execModels.map(async ({ apiRoot }) => {
556
594
  await cleanupModel(apiRoot);
@@ -772,6 +810,7 @@ export type Passthrough = AnyObject & {
772
810
  };
773
811
 
774
812
  export class PinejsClient extends PinejsClientCore<PinejsClient> {
813
+ // @ts-expect-error This is actually assigned by `super` so it is always declared but that isn't detected here
775
814
  public passthrough: Passthrough;
776
815
  public async _request({
777
816
  method,
@@ -816,7 +855,7 @@ export const runURI = async (
816
855
  let user: User | undefined;
817
856
  let apiKey: ApiKey | undefined;
818
857
 
819
- if (req != null && _.isObject(req)) {
858
+ if (req != null && typeof req === 'object') {
820
859
  user = req.user;
821
860
  apiKey = req.apiKey;
822
861
  } else {
@@ -858,15 +897,15 @@ export const runURI = async (
858
897
  throw response;
859
898
  }
860
899
 
861
- const { body: responseBody, status, headers } = response as Response;
900
+ const { body: responseBody, statusCode, headers } = response as Response;
862
901
 
863
- if (status != null && status >= 400) {
902
+ if (statusCode != null && statusCode >= 400) {
864
903
  const ErrorClass =
865
- statusCodeToError[status as keyof typeof statusCodeToError];
904
+ statusCodeToError[statusCode as keyof typeof statusCodeToError];
866
905
  if (ErrorClass != null) {
867
906
  throw new ErrorClass(undefined, responseBody, headers);
868
907
  }
869
- throw new HttpError(status, undefined, responseBody, headers);
908
+ throw new HttpError(statusCode, undefined, responseBody, headers);
870
909
  }
871
910
 
872
911
  return responseBody as AnyObject | undefined;
@@ -925,12 +964,19 @@ const $getAffectedIds = async ({
925
964
  }
926
965
  // We reparse to make sure we get a clean odataQuery, without permissions already added
927
966
  // And we use the request's url rather than the req for things like batch where the req url is ../$batch
928
- let affectedRequest = await uriParser.parseOData({
929
- method: request.method,
930
- url: `/${request.vocabulary}${request.url}`,
931
- });
967
+ const parsedRequest: uriParser.ParsedODataRequest &
968
+ Partial<Pick<uriParser.ODataRequest, 'engine'>> =
969
+ await uriParser.parseOData({
970
+ method: request.method,
971
+ url: `/${request.vocabulary}${request.url}`,
972
+ });
932
973
 
933
- affectedRequest.engine = request.engine;
974
+ parsedRequest.engine = request.engine;
975
+ // Mark that the engine is required now that we've set it
976
+ let affectedRequest: uriParser.ODataRequest = parsedRequest as RequiredField<
977
+ typeof parsedRequest,
978
+ 'engine'
979
+ >;
934
980
  const abstractSqlModel = getAbstractSqlModel(affectedRequest);
935
981
  const resourceName = resolveSynonym(affectedRequest);
936
982
  const resourceTable = abstractSqlModel.tables[resourceName];
@@ -1016,8 +1062,16 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1016
1062
  requests = [{ method, url, data: body }];
1017
1063
  }
1018
1064
 
1019
- const prepareRequest = async ($request: uriParser.ODataRequest) => {
1020
- $request.engine = db.engine;
1065
+ const prepareRequest = async (
1066
+ parsedRequest: uriParser.ParsedODataRequest &
1067
+ Partial<Pick<uriParser.ODataRequest, 'engine'>>,
1068
+ ): Promise<uriParser.ODataRequest> => {
1069
+ parsedRequest.engine = db.engine;
1070
+ // Mark that the engine is required now that we've set it
1071
+ const $request: uriParser.ODataRequest = parsedRequest as RequiredField<
1072
+ typeof parsedRequest,
1073
+ 'engine'
1074
+ >;
1021
1075
  // Get the full hooks list now that we can.
1022
1076
  $request.hooks = getHooks($request);
1023
1077
  // Add/check the relevant permissions
@@ -1029,7 +1083,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1029
1083
  });
1030
1084
  const translatedRequest = await uriParser.translateUri($request);
1031
1085
  return await compileRequest(translatedRequest);
1032
- } catch (err) {
1086
+ } catch (err: any) {
1033
1087
  rollbackRequestHooks(reqHooks);
1034
1088
  rollbackRequestHooks($request.hooks);
1035
1089
  throw err;
@@ -1038,12 +1092,13 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1038
1092
 
1039
1093
  // Parse the OData requests
1040
1094
  const results = await mappingFn(requests, async (requestPart) => {
1041
- let request = await uriParser.parseOData(requestPart);
1095
+ const parsedRequest = await uriParser.parseOData(requestPart);
1042
1096
 
1043
- if (Array.isArray(request)) {
1044
- request = await controlFlow.mapSeries(request, prepareRequest);
1097
+ let request: uriParser.ODataRequest | uriParser.ODataRequest[];
1098
+ if (Array.isArray(parsedRequest)) {
1099
+ request = await controlFlow.mapSeries(parsedRequest, prepareRequest);
1045
1100
  } else {
1046
- request = await prepareRequest(request);
1101
+ request = await prepareRequest(parsedRequest);
1047
1102
  }
1048
1103
  // Run the request in its own transaction
1049
1104
  return await runTransaction<Response | Response[]>(
@@ -1054,9 +1109,9 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1054
1109
  tx.on('rollback', () => {
1055
1110
  rollbackRequestHooks(reqHooks);
1056
1111
  if (Array.isArray(request)) {
1057
- request.forEach(({ hooks }) => {
1112
+ for (const { hooks } of request) {
1058
1113
  rollbackRequestHooks(hooks);
1059
- });
1114
+ }
1060
1115
  } else {
1061
1116
  rollbackRequestHooks(request.hooks);
1062
1117
  }
@@ -1083,7 +1138,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1083
1138
  if (
1084
1139
  !Array.isArray(result) &&
1085
1140
  result.body == null &&
1086
- result.status == null
1141
+ result.statusCode == null
1087
1142
  ) {
1088
1143
  console.error('No status or body set', req.url, responses);
1089
1144
  return new InternalRequestError();
@@ -1131,7 +1186,7 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {
1131
1186
  }),
1132
1187
  );
1133
1188
  }
1134
- } catch (e) {
1189
+ } catch (e: any) {
1135
1190
  if (handleHttpErrors(req, res, e)) {
1136
1191
  return;
1137
1192
  }
@@ -1163,11 +1218,9 @@ export const handleHttpErrors = (
1163
1218
  return false;
1164
1219
  };
1165
1220
  const handleResponse = (res: Express.Response, response: Response): void => {
1166
- const { body, headers, status } = response as Response;
1221
+ const { body, headers, statusCode } = response as Response;
1167
1222
  res.set(headers);
1168
- if (status != null) {
1169
- res.status(status);
1170
- }
1223
+ res.status(statusCode);
1171
1224
  if (!body) {
1172
1225
  res.end();
1173
1226
  } else {
@@ -1177,9 +1230,9 @@ const handleResponse = (res: Express.Response, response: Response): void => {
1177
1230
 
1178
1231
  const httpErrorToResponse = (
1179
1232
  err: HttpError,
1180
- ): RequiredField<Response, 'status'> => {
1233
+ ): RequiredField<Response, 'statusCode'> => {
1181
1234
  return {
1182
- status: err.status,
1235
+ statusCode: err.status,
1183
1236
  body: err.getResponseBody(),
1184
1237
  headers: err.headers,
1185
1238
  };
@@ -1246,7 +1299,7 @@ const runRequest = async (
1246
1299
  result = await runDelete(req, request, tx);
1247
1300
  break;
1248
1301
  }
1249
- } catch (err) {
1302
+ } catch (err: any) {
1250
1303
  if (err instanceof db.DatabaseError) {
1251
1304
  prettifyConstraintError(err, request);
1252
1305
  logger.error(err);
@@ -1270,7 +1323,7 @@ const runRequest = async (
1270
1323
  }
1271
1324
 
1272
1325
  await runHooks('POSTRUN', request.hooks, { req, request, result, tx });
1273
- } catch (err) {
1326
+ } catch (err: any) {
1274
1327
  await runHooks('POSTRUN-ERROR', request.hooks, {
1275
1328
  req,
1276
1329
  request,
@@ -1294,7 +1347,7 @@ const runChangeSet =
1294
1347
  throw new Error('No request id');
1295
1348
  }
1296
1349
  result.headers ??= {};
1297
- result.headers['Content-Id'] = request.id;
1350
+ result.headers['content-id'] = request.id;
1298
1351
  changeSetResults.set(request.id, result);
1299
1352
  };
1300
1353
 
@@ -1464,24 +1517,31 @@ const respondGet = async (
1464
1517
  { includeMetadata: metadata === 'full' },
1465
1518
  );
1466
1519
 
1520
+ const response = {
1521
+ statusCode: 200,
1522
+ body: { d },
1523
+ headers: { 'content-type': 'application/json' },
1524
+ };
1467
1525
  await runHooks('PRERESPOND', request.hooks, {
1468
1526
  req,
1469
1527
  request,
1470
1528
  result,
1529
+ response,
1471
1530
  data: d,
1472
1531
  tx,
1473
1532
  });
1474
- return { body: { d }, headers: { contentType: 'application/json' } };
1533
+ return response;
1475
1534
  } else {
1476
1535
  if (request.resourceName === '$metadata') {
1477
1536
  return {
1537
+ statusCode: 200,
1478
1538
  body: models[vocab].odataMetadata,
1479
- headers: { contentType: 'xml' },
1539
+ headers: { 'content-type': 'xml' },
1480
1540
  };
1481
1541
  } else {
1482
1542
  // TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that
1483
1543
  return {
1484
- status: 404,
1544
+ statusCode: 404,
1485
1545
  };
1486
1546
  }
1487
1547
  }
@@ -1532,21 +1592,23 @@ const respondPost = async (
1532
1592
  }
1533
1593
  }
1534
1594
 
1595
+ const response = {
1596
+ statusCode: 201,
1597
+ body: result.d[0],
1598
+ headers: {
1599
+ 'content-type': 'application/json',
1600
+ location,
1601
+ },
1602
+ };
1535
1603
  await runHooks('PRERESPOND', request.hooks, {
1536
1604
  req,
1537
1605
  request,
1538
1606
  result,
1607
+ response,
1539
1608
  tx,
1540
1609
  });
1541
1610
 
1542
- return {
1543
- status: 201,
1544
- body: result.d[0],
1545
- headers: {
1546
- contentType: 'application/json',
1547
- Location: location,
1548
- },
1549
- };
1611
+ return response;
1550
1612
  };
1551
1613
 
1552
1614
  const runPut = async (
@@ -1580,16 +1642,17 @@ const respondPut = async (
1580
1642
  result: any,
1581
1643
  tx: Db.Tx,
1582
1644
  ): Promise<Response> => {
1645
+ const response = {
1646
+ statusCode: 200,
1647
+ };
1583
1648
  await runHooks('PRERESPOND', request.hooks, {
1584
1649
  req,
1585
1650
  request,
1586
1651
  result,
1652
+ response,
1587
1653
  tx,
1588
1654
  });
1589
- return {
1590
- status: 200,
1591
- headers: {},
1592
- };
1655
+ return response;
1593
1656
  };
1594
1657
  const respondDelete = respondPut;
1595
1658
  const respondOptions = respondPut;
@@ -1630,7 +1693,7 @@ export const executeStandardModels = async (tx: Db.Tx): Promise<void> => {
1630
1693
  });
1631
1694
  await executeModels(tx, permissions.config.models);
1632
1695
  console.info('Successfully executed standard models.');
1633
- } catch (err) {
1696
+ } catch (err: any) {
1634
1697
  console.error('Failed to execute standard models.', err);
1635
1698
  throw err;
1636
1699
  }
@@ -1646,7 +1709,7 @@ export const setup = async (
1646
1709
  await executeStandardModels(tx);
1647
1710
  await permissions.setup();
1648
1711
  });
1649
- } catch (err) {
1712
+ } catch (err: any) {
1650
1713
  console.error('Could not execute standard models', err);
1651
1714
  process.exit(1);
1652
1715
  }