@balena/pinejs 16.0.0-build--batch-09b8c466600d7df13e6df3eacabaf463d96f652f-1 → 16.0.0-build-fisehara-update-sbvr-types-b58e72aca3193964afac96c955fde178fe39d077-1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +2164 -15
  3. package/CHANGELOG.md +815 -3
  4. package/Gruntfile.ts +9 -6
  5. package/README.md +10 -0
  6. package/build/browser.ts +2 -2
  7. package/build/config.ts +1 -1
  8. package/build/module.ts +2 -2
  9. package/build/server.ts +2 -2
  10. package/docker-compose.npm-test.yml +21 -3
  11. package/out/bin/abstract-sql-compiler.js +5 -5
  12. package/out/bin/abstract-sql-compiler.js.map +1 -1
  13. package/out/bin/odata-compiler.js +10 -10
  14. package/out/bin/odata-compiler.js.map +1 -1
  15. package/out/bin/sbvr-compiler.js +34 -11
  16. package/out/bin/sbvr-compiler.js.map +1 -1
  17. package/out/bin/utils.js +25 -2
  18. package/out/bin/utils.js.map +1 -1
  19. package/out/config-loader/config-loader.d.ts +4 -2
  20. package/out/config-loader/config-loader.js +54 -13
  21. package/out/config-loader/config-loader.js.map +1 -1
  22. package/out/config-loader/env.d.ts +2 -1
  23. package/out/config-loader/env.js +5 -2
  24. package/out/config-loader/env.js.map +1 -1
  25. package/out/data-server/sbvr-server.d.ts +1 -1
  26. package/out/data-server/sbvr-server.js +3 -1
  27. package/out/data-server/sbvr-server.js.map +1 -1
  28. package/out/database-layer/db.js +40 -14
  29. package/out/database-layer/db.js.map +1 -1
  30. package/out/express-emulator/express.js +5 -3
  31. package/out/express-emulator/express.js.map +1 -1
  32. package/out/http-transactions/transactions.d.ts +1 -1
  33. package/out/http-transactions/transactions.js +10 -5
  34. package/out/http-transactions/transactions.js.map +1 -1
  35. package/out/migrator/async.js +32 -5
  36. package/out/migrator/async.js.map +1 -1
  37. package/out/migrator/sync.d.ts +2 -1
  38. package/out/migrator/sync.js +29 -3
  39. package/out/migrator/sync.js.map +1 -1
  40. package/out/migrator/utils.d.ts +6 -3
  41. package/out/migrator/utils.js +30 -4
  42. package/out/migrator/utils.js.map +1 -1
  43. package/out/odata-metadata/odata-metadata-generator.js +4 -1
  44. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  45. package/out/passport-pinejs/mount-login-router.d.ts +3 -0
  46. package/out/passport-pinejs/mount-login-router.js +65 -0
  47. package/out/passport-pinejs/mount-login-router.js.map +1 -0
  48. package/out/passport-pinejs/passport-pinejs.d.ts +2 -1
  49. package/out/passport-pinejs/passport-pinejs.js +28 -2
  50. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  51. package/out/pinejs-session-store/pinejs-session-store.js +30 -7
  52. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  53. package/out/sbvr-api/abstract-sql.d.ts +2 -2
  54. package/out/sbvr-api/abstract-sql.js +35 -9
  55. package/out/sbvr-api/abstract-sql.js.map +1 -1
  56. package/out/sbvr-api/cached-compile.js +9 -6
  57. package/out/sbvr-api/cached-compile.js.map +1 -1
  58. package/out/sbvr-api/common-types.d.ts +1 -1
  59. package/out/sbvr-api/control-flow.js +5 -2
  60. package/out/sbvr-api/control-flow.js.map +1 -1
  61. package/out/sbvr-api/express-extension.d.ts +10 -7
  62. package/out/sbvr-api/express-extension.js +1 -0
  63. package/out/sbvr-api/hooks.d.ts +5 -1
  64. package/out/sbvr-api/hooks.js +12 -10
  65. package/out/sbvr-api/hooks.js.map +1 -1
  66. package/out/sbvr-api/odata-response.d.ts +5 -2
  67. package/out/sbvr-api/odata-response.js +36 -6
  68. package/out/sbvr-api/odata-response.js.map +1 -1
  69. package/out/sbvr-api/permissions.d.ts +6 -7
  70. package/out/sbvr-api/permissions.js +69 -38
  71. package/out/sbvr-api/permissions.js.map +1 -1
  72. package/out/sbvr-api/sbvr-utils.d.ts +21 -10
  73. package/out/sbvr-api/sbvr-utils.js +128 -124
  74. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  75. package/out/sbvr-api/translations.d.ts +2 -2
  76. package/out/sbvr-api/translations.js +17 -10
  77. package/out/sbvr-api/translations.js.map +1 -1
  78. package/out/sbvr-api/uri-parser.d.ts +10 -12
  79. package/out/sbvr-api/uri-parser.js +46 -19
  80. package/out/sbvr-api/uri-parser.js.map +1 -1
  81. package/out/server-glue/global-ext.d.ts +2 -1
  82. package/out/server-glue/module.d.ts +3 -1
  83. package/out/server-glue/module.js +40 -13
  84. package/out/server-glue/module.js.map +1 -1
  85. package/out/server-glue/sbvr-loader.js.map +1 -1
  86. package/out/server-glue/server.js +31 -39
  87. package/out/server-glue/server.js.map +1 -1
  88. package/out/webresource-handler/handlers/NoopHandler.d.ts +7 -0
  89. package/out/webresource-handler/handlers/NoopHandler.js +20 -0
  90. package/out/webresource-handler/handlers/NoopHandler.js.map +1 -0
  91. package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
  92. package/out/webresource-handler/handlers/S3Handler.js +97 -0
  93. package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
  94. package/out/webresource-handler/handlers/index.d.ts +2 -0
  95. package/out/webresource-handler/handlers/index.js +19 -0
  96. package/out/webresource-handler/handlers/index.js.map +1 -0
  97. package/out/webresource-handler/index.d.ts +34 -0
  98. package/out/webresource-handler/index.js +307 -0
  99. package/out/webresource-handler/index.js.map +1 -0
  100. package/package.json +68 -62
  101. package/src/bin/abstract-sql-compiler.ts +7 -9
  102. package/src/bin/odata-compiler.ts +12 -15
  103. package/src/bin/sbvr-compiler.ts +14 -18
  104. package/src/bin/utils.ts +1 -1
  105. package/src/config-loader/config-loader.ts +44 -10
  106. package/src/config-loader/env.ts +1 -1
  107. package/src/data-server/sbvr-server.js +3 -1
  108. package/src/database-layer/db.ts +23 -19
  109. package/src/express-emulator/express.js +5 -3
  110. package/src/extended-sbvr-parser/extended-sbvr-parser.ts +1 -1
  111. package/src/http-transactions/transactions.js +10 -5
  112. package/src/migrator/async.ts +7 -6
  113. package/src/migrator/sync.ts +10 -7
  114. package/src/migrator/utils.ts +11 -5
  115. package/src/odata-metadata/odata-metadata-generator.ts +2 -2
  116. package/src/passport-pinejs/mount-login-router.ts +46 -0
  117. package/src/passport-pinejs/passport-pinejs.ts +7 -3
  118. package/src/pinejs-session-store/pinejs-session-store.ts +6 -6
  119. package/src/sbvr-api/abstract-sql.ts +5 -5
  120. package/src/sbvr-api/cached-compile.ts +1 -2
  121. package/src/sbvr-api/common-types.ts +1 -1
  122. package/src/sbvr-api/control-flow.ts +1 -1
  123. package/src/sbvr-api/express-extension.ts +12 -8
  124. package/src/sbvr-api/hooks.ts +11 -11
  125. package/src/sbvr-api/odata-response.ts +56 -9
  126. package/src/sbvr-api/permissions.ts +44 -35
  127. package/src/sbvr-api/sbvr-utils.ts +117 -165
  128. package/src/sbvr-api/translations.ts +9 -6
  129. package/src/sbvr-api/uri-parser.ts +25 -30
  130. package/src/server-glue/global-ext.d.ts +2 -1
  131. package/src/server-glue/module.ts +8 -2
  132. package/src/server-glue/sbvr-loader.ts +1 -1
  133. package/src/server-glue/server.ts +11 -49
  134. package/src/webresource-handler/handlers/NoopHandler.ts +21 -0
  135. package/src/webresource-handler/handlers/S3Handler.ts +143 -0
  136. package/src/webresource-handler/handlers/index.ts +2 -0
  137. package/src/webresource-handler/index.ts +450 -0
  138. package/tsconfig.dev.json +2 -1
  139. package/tsconfig.json +1 -1
  140. package/typings/lf-to-abstract-sql.d.ts +1 -1
  141. package/typings/memoizee.d.ts +3 -4
@@ -3,7 +3,9 @@ import type * as Db from '../database-layer/db';
3
3
  import type { Model } from '../config-loader/config-loader';
4
4
  import type { AnyObject, RequiredField } from './common-types';
5
5
 
6
+ // Augment the Express typings
6
7
  declare global {
8
+ // eslint-disable-next-line @typescript-eslint/no-namespace
7
9
  namespace Express {
8
10
  export interface Request {
9
11
  tx?: Db.Tx;
@@ -12,9 +14,9 @@ declare global {
12
14
  }
13
15
  }
14
16
 
15
- import * as _ from 'lodash';
17
+ import _ from 'lodash';
16
18
 
17
- import { TypedError } from 'typed-error';
19
+ import type { TypedError } from 'typed-error';
18
20
  import { cachedCompile } from './cached-compile';
19
21
 
20
22
  type LFModel = any[];
@@ -23,14 +25,14 @@ import { version as AbstractSQLCompilerVersion } from '@balena/abstract-sql-comp
23
25
  import * as LF2AbstractSQL from '@balena/lf-to-abstract-sql';
24
26
 
25
27
  import {
26
- ODataBinds,
28
+ type ODataBinds,
27
29
  odataNameToSqlName,
28
30
  sqlNameToODataName,
29
- SupportedMethod,
31
+ type SupportedMethod,
30
32
  } from '@balena/odata-to-abstract-sql';
31
33
  import sbvrTypes from '@balena/sbvr-types';
32
34
  import deepFreeze = require('deep-freeze');
33
- import { PinejsClientCore, PromiseResultTypes } from 'pinejs-client-core';
35
+ import { PinejsClientCore, type PromiseResultTypes } from 'pinejs-client-core';
34
36
 
35
37
  import { ExtendedSBVRParser } from '../extended-sbvr-parser/extended-sbvr-parser';
36
38
 
@@ -38,7 +40,7 @@ import * as asyncMigrator from '../migrator/async';
38
40
  import * as syncMigrator from '../migrator/sync';
39
41
  import { generateODataMetadata } from '../odata-metadata/odata-metadata-generator';
40
42
 
41
- // tslint:disable-next-line:no-var-requires
43
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
42
44
  const devModel = require('./dev.sbvr');
43
45
  import * as permissions from './permissions';
44
46
  import {
@@ -58,20 +60,20 @@ import {
58
60
  UnauthorizedError,
59
61
  } from './errors';
60
62
  import * as uriParser from './uri-parser';
61
- export { ODataRequest } from './uri-parser';
63
+ export type { ODataRequest } from './uri-parser';
62
64
  import {
63
- HookReq,
64
- HookArgs,
65
+ type HookReq,
66
+ type HookArgs,
65
67
  rollbackRequestHooks,
66
68
  getHooks,
67
69
  runHooks,
68
- InstantiatedHooks,
70
+ type InstantiatedHooks,
69
71
  } from './hooks';
70
72
  export {
71
- HookReq,
72
- HookArgs,
73
- HookResponse,
74
- Hooks,
73
+ type HookReq,
74
+ type HookArgs,
75
+ type HookResponse,
76
+ type Hooks,
75
77
  addPureHook,
76
78
  addSideEffectHook,
77
79
  } from './hooks';
@@ -94,8 +96,10 @@ export { resolveOdataBind } from './abstract-sql';
94
96
  import * as odataResponse from './odata-response';
95
97
  import { env } from '../server-glue/module';
96
98
  import { translateAbstractSqlModel } from './translations';
97
-
98
- const validBatchMethods = new Set(['PUT', 'POST', 'PATCH', 'DELETE', 'GET']);
99
+ import {
100
+ type MigrationExecutionResult,
101
+ setExecutedMigrations,
102
+ } from '../migrator/utils';
99
103
 
100
104
  const LF2AbstractSQLTranslator = LF2AbstractSQL.createTranslator(sbvrTypes);
101
105
  const LF2AbstractSQLTranslatorVersion = `${LF2AbstractSQLVersion}+${sbvrTypesVersion}`;
@@ -117,6 +121,7 @@ interface CompiledModel {
117
121
  const models: {
118
122
  [vocabulary: string]: CompiledModel & {
119
123
  versions: string[];
124
+ modelExecutionResult?: ModelExecutionResult;
120
125
  };
121
126
  } = {};
122
127
 
@@ -135,8 +140,7 @@ export interface ApiKey extends Actor {
135
140
  }
136
141
 
137
142
  export interface Response {
138
- id?: string | undefined;
139
- status: number;
143
+ statusCode: number;
140
144
  headers?:
141
145
  | {
142
146
  [headerName: string]: any;
@@ -145,6 +149,12 @@ export interface Response {
145
149
  body?: AnyObject | string;
146
150
  }
147
151
 
152
+ export type ModelExecutionResult =
153
+ | undefined
154
+ | {
155
+ migrationExecutionResult?: MigrationExecutionResult;
156
+ };
157
+
148
158
  const memoizedResolvedSynonym = memoizeWeak(
149
159
  (
150
160
  abstractSqlModel: AbstractSQLCompiler.AbstractSqlModel,
@@ -241,13 +251,14 @@ const prettifyConstraintError = (
241
251
  err.message,
242
252
  );
243
253
  break;
244
- case 'postgres':
254
+ case 'postgres': {
245
255
  const resourceName = resolveSynonym(request);
246
256
  const abstractSqlModel = getFinalAbstractSqlModel(request);
247
257
  matches = new RegExp(
248
258
  '"' + abstractSqlModel.tables[resourceName].name + '_(.*?)_key"',
249
259
  ).exec(err.message);
250
260
  break;
261
+ }
251
262
  }
252
263
  // We know it's the right error type, so if matches exists just throw a generic error message, since we have failed to get the info for a more specific one.
253
264
  if (matches == null) {
@@ -274,7 +285,7 @@ const prettifyConstraintError = (
274
285
  err.message,
275
286
  );
276
287
  break;
277
- case 'postgres':
288
+ case 'postgres': {
278
289
  const resourceName = resolveSynonym(request);
279
290
  const abstractSqlModel = getFinalAbstractSqlModel(request);
280
291
  const tableName = abstractSqlModel.tables[resourceName].name;
@@ -293,6 +304,7 @@ const prettifyConstraintError = (
293
304
  ).exec(err.message);
294
305
  }
295
306
  break;
307
+ }
296
308
  }
297
309
  // We know it's the right error type, so if no matches exists just throw a generic error message,
298
310
  // since we have failed to get the info for a more specific one.
@@ -586,7 +598,7 @@ export const executeModels = async (
586
598
  execModels.map(async (model) => {
587
599
  const { apiRoot } = model;
588
600
 
589
- await syncMigrator.run(tx, model);
601
+ const migrationExecutionResult = await syncMigrator.run(tx, model);
590
602
  const compiledModel = generateModels(model, db.engine);
591
603
 
592
604
  if (compiledModel.sql) {
@@ -617,6 +629,9 @@ export const executeModels = async (
617
629
  models[apiRoot] = {
618
630
  ...compiledModel,
619
631
  versions,
632
+ modelExecutionResult: {
633
+ migrationExecutionResult,
634
+ },
620
635
  };
621
636
 
622
637
  // Validate the [empty] model according to the rules.
@@ -723,6 +738,23 @@ export const executeModels = async (
723
738
  }
724
739
  };
725
740
 
741
+ export const postExecuteModels = async (tx: Db.Tx): Promise<void> => {
742
+ // Executing the `migrations` model takes place after other models have been executed.
743
+ // Hence, skipped migrations from earlier models are not set as executed as the `migration` table is missing
744
+ // Here the skipped migrations that haven't been set properly are covered
745
+ // This is mostly an edge case when running on an empty database schema and migrations model hasn't been executed, yet.
746
+ // One specifc case are tests to run tests against migrated and unmigrated database states
747
+
748
+ for (const modelKey of Object.keys(models)) {
749
+ const pendingToSetExecutedMigrations =
750
+ models[modelKey]?.modelExecutionResult?.migrationExecutionResult
751
+ ?.pendingUnsetMigrations;
752
+ if (pendingToSetExecutedMigrations != null) {
753
+ await setExecutedMigrations(tx, modelKey, pendingToSetExecutedMigrations);
754
+ }
755
+ }
756
+ };
757
+
726
758
  const cleanupModel = (vocab: string) => {
727
759
  delete models[vocab];
728
760
  delete api[vocab];
@@ -955,7 +987,7 @@ export class PinejsClient extends PinejsClientCore<PinejsClient> {
955
987
  req?: permissions.PermissionReq;
956
988
  custom?: AnyObject;
957
989
  }) {
958
- return (await runURI(method, url, body, tx, req, custom)) as {};
990
+ return (await runURI(method, url, body, tx, req, custom)) as object;
959
991
  }
960
992
  }
961
993
 
@@ -1025,15 +1057,15 @@ export const runURI = async (
1025
1057
  throw response;
1026
1058
  }
1027
1059
 
1028
- const { body: responseBody, status, headers } = response as Response;
1060
+ const { body: responseBody, statusCode, headers } = response as Response;
1029
1061
 
1030
- if (status != null && status >= 400) {
1062
+ if (statusCode != null && statusCode >= 400) {
1031
1063
  const ErrorClass =
1032
- statusCodeToError[status as keyof typeof statusCodeToError];
1064
+ statusCodeToError[statusCode as keyof typeof statusCodeToError];
1033
1065
  if (ErrorClass != null) {
1034
1066
  throw new ErrorClass(undefined, responseBody, headers);
1035
1067
  }
1036
- throw new HttpError(status, undefined, responseBody, headers);
1068
+ throw new HttpError(statusCode, undefined, responseBody, headers);
1037
1069
  }
1038
1070
 
1039
1071
  return responseBody as AnyObject | undefined;
@@ -1072,7 +1104,7 @@ export const getAffectedIds = async (
1072
1104
  args: HookArgs & {
1073
1105
  tx: Db.Tx;
1074
1106
  },
1075
- ): Promise<string[]> => {
1107
+ ): Promise<number[]> => {
1076
1108
  const { request } = args;
1077
1109
  if (request.affectedIds) {
1078
1110
  return request.affectedIds;
@@ -1097,7 +1129,7 @@ const $getAffectedIds = async ({
1097
1129
  tx,
1098
1130
  }: HookArgs & {
1099
1131
  tx: Db.Tx;
1100
- }): Promise<string[]> => {
1132
+ }): Promise<number[]> => {
1101
1133
  if (!['PATCH', 'DELETE'].includes(request.method)) {
1102
1134
  // We can only find the affected ids in advance for requests that modify existing records, if they
1103
1135
  // can insert new records (POST/PUT) then we're unable to find the ids until the request has actually run
@@ -1110,8 +1142,7 @@ const $getAffectedIds = async ({
1110
1142
  // And we use the request's url rather than the req for things like batch where the req url is ../$batch
1111
1143
  const parsedRequest: uriParser.ParsedODataRequest &
1112
1144
  Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>> =
1113
- await uriParser.parseOData({
1114
- id: request.id,
1145
+ uriParser.parseOData({
1115
1146
  method: request.method,
1116
1147
  url: `/${request.vocabulary}${request.url}`,
1117
1148
  });
@@ -1157,90 +1188,8 @@ const $getAffectedIds = async ({
1157
1188
  return result.rows.map((row) => row[idField]);
1158
1189
  };
1159
1190
 
1160
- const validateBatch = (req: Express.Request) => {
1161
- const { requests } = req.body as { requests: uriParser.UnparsedRequest[] };
1162
- if (!Array.isArray(requests)) {
1163
- throw new BadRequestError(
1164
- 'Batch requests must include an array of requests in the body via the "requests" property',
1165
- );
1166
- }
1167
- if (req.headers != null && req.headers['content-type'] == null) {
1168
- throw new BadRequestError(
1169
- 'Headers in a batch request must include a "content-type" header if they are provided',
1170
- );
1171
- }
1172
- if (
1173
- requests.find(
1174
- (request) =>
1175
- request.headers?.authorization != null ||
1176
- request.url?.includes('apikey='),
1177
- ) != null
1178
- ) {
1179
- throw new BadRequestError(
1180
- 'Authorization may only be passed to the main batch request',
1181
- );
1182
- }
1183
- const ids = new Set<string>(
1184
- requests
1185
- .map((request) => request.id)
1186
- .filter((id) => typeof id === 'string') as string[],
1187
- );
1188
- if (ids.size !== requests.length) {
1189
- throw new BadRequestError(
1190
- 'All requests in a batch request must have unique string ids',
1191
- );
1192
- }
1193
-
1194
- for (const request of requests) {
1195
- if (
1196
- request.headers != null &&
1197
- request.headers['content-type'] == null &&
1198
- (req.headers == null || req.headers['content-type'] == null)
1199
- ) {
1200
- throw new BadRequestError(
1201
- 'Requests of a batch request that have headers must include a "content-type" header',
1202
- );
1203
- }
1204
- if (request.method == null) {
1205
- throw new BadRequestError(
1206
- 'Requests of a batch request must have a "method"',
1207
- );
1208
- }
1209
- const upperCaseMethod = request.method.toUpperCase();
1210
- if (!validBatchMethods.has(upperCaseMethod)) {
1211
- throw new BadRequestError(
1212
- `Requests of a batch request must have a method matching one of the following: ${Array.from(
1213
- validBatchMethods,
1214
- ).join(', ')}`,
1215
- );
1216
- }
1217
- if (
1218
- request.body !== undefined &&
1219
- (upperCaseMethod === 'GET' || upperCaseMethod === 'DELETE')
1220
- ) {
1221
- throw new BadRequestError(
1222
- 'GET and DELETE requests of a batch request must not have a body',
1223
- );
1224
- }
1225
- }
1226
-
1227
- const urls = new Set<string | undefined>(
1228
- requests.map((request) => request.url),
1229
- );
1230
- if (urls.has(undefined)) {
1231
- throw new BadRequestError('Requests of a batch request must have a "url"');
1232
- }
1233
- if (urls.has('/university/$batch')) {
1234
- throw new BadRequestError('Batch requests cannot contain batch requests');
1235
- }
1236
- const urlModels = new Set(
1237
- Array.from(urls.values()).map((url: string) => url.split('/')[1]),
1238
- );
1239
- if (urlModels.size > 1) {
1240
- throw new BadRequestError(
1241
- 'Batch requests must consist of requests for only one model',
1242
- );
1243
- }
1191
+ export const getModel = (vocabulary: string) => {
1192
+ return models[vocabulary];
1244
1193
  };
1245
1194
 
1246
1195
  const runODataRequest = (req: Express.Request, vocabulary: string) => {
@@ -1248,10 +1197,6 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1248
1197
  api[vocabulary].logger.log('Parsing', req.method, req.url);
1249
1198
  }
1250
1199
 
1251
- if (req.url.startsWith(`/${vocabulary}/$batch`)) {
1252
- validateBatch(req);
1253
- }
1254
-
1255
1200
  // Get the hooks for the current method/vocabulary as we know it,
1256
1201
  // in order to run PREPARSE hooks, before parsing gets us more info
1257
1202
  const { versions } = models[vocabulary];
@@ -1299,27 +1244,19 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1299
1244
  await runHooks('PREPARSE', reqHooks, { req, tx: req.tx });
1300
1245
  let requests: uriParser.UnparsedRequest[];
1301
1246
  // Check if it is a single request or a batch
1302
- if (req.url.startsWith(`/${vocabulary}/$batch`)) {
1303
- await Promise.all(
1304
- req.body.requests.map(
1305
- async (request: HookReq) =>
1306
- await runHooks('PREPARSE', reqHooks, {
1307
- req: request,
1308
- tx: req.tx,
1309
- }),
1310
- ),
1311
- );
1312
- requests = req.body.requests;
1247
+ if (req.batch != null && req.batch.length > 0) {
1248
+ requests = req.batch;
1313
1249
  } else {
1314
1250
  const { method, url, body } = req;
1315
- requests = [{ method, url, body }];
1251
+ requests = [{ method, url, data: body }];
1316
1252
  }
1317
1253
 
1318
1254
  const prepareRequest = async (
1319
1255
  parsedRequest: uriParser.ParsedODataRequest &
1320
1256
  Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>>,
1321
1257
  ): Promise<uriParser.ODataRequest> => {
1322
- if (models[parsedRequest.vocabulary] == null) {
1258
+ const abstractSqlModel = getAbstractSqlModel(parsedRequest);
1259
+ if (abstractSqlModel == null) {
1323
1260
  throw new BadRequestError(
1324
1261
  'Unknown vocabulary: ' + parsedRequest.vocabulary,
1325
1262
  );
@@ -1333,6 +1270,14 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1333
1270
  >;
1334
1271
  // Add/check the relevant permissions
1335
1272
  try {
1273
+ const resolvedResourceName = resolveSynonym($request);
1274
+ if (
1275
+ abstractSqlModel.tables[resolvedResourceName] == null &&
1276
+ !resolvedResourceName.endsWith('#canAccess')
1277
+ ) {
1278
+ throw new UnauthorizedError();
1279
+ }
1280
+
1336
1281
  $request.hooks = [];
1337
1282
  for (const version of versions) {
1338
1283
  // We get the hooks list between each `runHooks` so that any resource renames will be used
@@ -1376,13 +1321,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1376
1321
 
1377
1322
  // Parse the OData requests
1378
1323
  const results = await mappingFn(requests, async (requestPart) => {
1379
- const parsedRequest = await uriParser.parseOData(
1380
- requestPart,
1381
- req.url.startsWith(`/${vocabulary}/$batch`) &&
1382
- !requestPart.url.includes(`/${vocabulary}/$batch`)
1383
- ? req.headers
1384
- : undefined,
1385
- );
1324
+ const parsedRequest = uriParser.parseOData(requestPart);
1386
1325
 
1387
1326
  let request: uriParser.ODataRequest | uriParser.ODataRequest[];
1388
1327
  if (Array.isArray(parsedRequest)) {
@@ -1407,7 +1346,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1407
1346
  }
1408
1347
  });
1409
1348
  if (Array.isArray(request)) {
1410
- const changeSetResults = new Map<string, Response>();
1349
+ const changeSetResults = new Map<number, Response>();
1411
1350
  const changeSetRunner = runChangeSet(req, tx);
1412
1351
  for (const r of request) {
1413
1352
  await changeSetRunner(changeSetResults, r);
@@ -1428,7 +1367,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1428
1367
  if (
1429
1368
  !Array.isArray(result) &&
1430
1369
  result.body == null &&
1431
- result.status == null
1370
+ result.statusCode == null
1432
1371
  ) {
1433
1372
  console.error('No status or body set', req.url, responses);
1434
1373
  return new InternalRequestError();
@@ -1442,8 +1381,13 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1442
1381
  };
1443
1382
  };
1444
1383
 
1445
- export const handleODataRequest: Express.Handler = async (req, res, next) => {
1384
+ export const getApiRoot = (req: Express.Request): string | undefined => {
1446
1385
  const [, apiRoot] = req.url.split('/', 2);
1386
+ return apiRoot;
1387
+ };
1388
+
1389
+ export const handleODataRequest: Express.Handler = async (req, res, next) => {
1390
+ const apiRoot = getApiRoot(req);
1447
1391
  if (apiRoot == null || models[apiRoot] == null) {
1448
1392
  return next('route');
1449
1393
  }
@@ -1457,10 +1401,7 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {
1457
1401
 
1458
1402
  res.set('Cache-Control', 'no-cache');
1459
1403
  // If we are dealing with a single request unpack the response and respond normally
1460
- if (
1461
- !req.url.startsWith(`/${apiRoot}/$batch`) ||
1462
- req.body.requests?.length === 0
1463
- ) {
1404
+ if (req.batch == null || req.batch.length === 0) {
1464
1405
  let [response] = responses;
1465
1406
  if (response instanceof HttpError) {
1466
1407
  response = httpErrorToResponse(response);
@@ -1469,15 +1410,15 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {
1469
1410
 
1470
1411
  // Otherwise its a multipart request and we reply with the appropriate multipart response
1471
1412
  } else {
1472
- res.status(200).json({
1473
- responses: responses.map((response) => {
1413
+ (res.status(200) as any).sendMulti(
1414
+ responses.map((response) => {
1474
1415
  if (response instanceof HttpError) {
1475
1416
  return httpErrorToResponse(response);
1476
1417
  } else {
1477
1418
  return response;
1478
1419
  }
1479
1420
  }),
1480
- });
1421
+ );
1481
1422
  }
1482
1423
  } catch (e: any) {
1483
1424
  if (handleHttpErrors(req, res, e)) {
@@ -1504,16 +1445,16 @@ export const handleHttpErrors = (
1504
1445
  for (const handleErrorFn of handleErrorFns) {
1505
1446
  handleErrorFn(req, err);
1506
1447
  }
1507
- const response = httpErrorToResponse(err, req);
1448
+ const response = httpErrorToResponse(err);
1508
1449
  handleResponse(res, response);
1509
1450
  return true;
1510
1451
  }
1511
1452
  return false;
1512
1453
  };
1513
1454
  const handleResponse = (res: Express.Response, response: Response): void => {
1514
- const { body, headers, status } = response as Response;
1455
+ const { body, headers, statusCode } = response as Response;
1515
1456
  res.set(headers);
1516
- res.status(status);
1457
+ res.status(statusCode);
1517
1458
  if (!body) {
1518
1459
  res.end();
1519
1460
  } else {
@@ -1523,12 +1464,10 @@ const handleResponse = (res: Express.Response, response: Response): void => {
1523
1464
 
1524
1465
  const httpErrorToResponse = (
1525
1466
  err: HttpError,
1526
- req?: Express.Request,
1527
- ): RequiredField<Response, 'status'> => {
1528
- const message = err.getResponseBody();
1467
+ ): RequiredField<Response, 'statusCode'> => {
1529
1468
  return {
1530
- status: err.status,
1531
- body: req != null && 'batch' in req ? { responses: [], message } : message,
1469
+ statusCode: err.status,
1470
+ body: err.getResponseBody(),
1532
1471
  headers: err.headers,
1533
1472
  };
1534
1473
  };
@@ -1633,7 +1572,7 @@ const runRequest = async (
1633
1572
  const runChangeSet =
1634
1573
  (req: Express.Request, tx: Db.Tx) =>
1635
1574
  async (
1636
- changeSetResults: Map<string, Response>,
1575
+ changeSetResults: Map<number, Response>,
1637
1576
  request: uriParser.ODataRequest,
1638
1577
  ): Promise<void> => {
1639
1578
  request = updateBinds(changeSetResults, request);
@@ -1651,7 +1590,7 @@ const runChangeSet =
1651
1590
  // deferred untill the request they reference is run and returns an insert ID.
1652
1591
  // This function compiles the sql query of a request which has been deferred
1653
1592
  const updateBinds = (
1654
- changeSetResults: Map<string, Response>,
1593
+ changeSetResults: Map<number, Response>,
1655
1594
  request: uriParser.ODataRequest,
1656
1595
  ) => {
1657
1596
  if (request._defer) {
@@ -1819,8 +1758,7 @@ const respondGet = async (
1819
1758
  );
1820
1759
 
1821
1760
  const response = {
1822
- id: request.id,
1823
- status: 200,
1761
+ statusCode: 200,
1824
1762
  body: { d },
1825
1763
  headers: { 'content-type': 'application/json' },
1826
1764
  };
@@ -1835,15 +1773,14 @@ const respondGet = async (
1835
1773
  } else {
1836
1774
  if (request.resourceName === '$metadata') {
1837
1775
  return {
1838
- id: request.id,
1839
- status: 200,
1776
+ statusCode: 200,
1840
1777
  body: models[vocab].odataMetadata,
1841
1778
  headers: { 'content-type': 'xml' },
1842
1779
  };
1843
1780
  } else {
1844
1781
  // TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that
1845
1782
  return {
1846
- status: 404,
1783
+ statusCode: 404,
1847
1784
  };
1848
1785
  }
1849
1786
  }
@@ -1899,7 +1836,7 @@ const respondPost = async (
1899
1836
  }
1900
1837
 
1901
1838
  const response = {
1902
- status: 201,
1839
+ statusCode: 201,
1903
1840
  body: result.d[0],
1904
1841
  headers: {
1905
1842
  'content-type': 'application/json',
@@ -1947,7 +1884,7 @@ const respondPut = async (
1947
1884
  tx: Db.Tx,
1948
1885
  ): Promise<Response> => {
1949
1886
  const response = {
1950
- status: 200,
1887
+ statusCode: 200,
1951
1888
  };
1952
1889
  await runHooks('PRERESPOND', request.hooks, {
1953
1890
  req,
@@ -2045,3 +1982,18 @@ export const setup = async (
2045
1982
  // we can't use IF NOT EXISTS on all dbs, so we have to ignore the error raised if this index already exists
2046
1983
  }
2047
1984
  };
1985
+
1986
+ export const postSetup = async (
1987
+ _app: Express.Application,
1988
+ $db: Db.Database,
1989
+ ): Promise<void> => {
1990
+ exports.db = db = $db;
1991
+ try {
1992
+ await db.transaction(async (tx) => {
1993
+ await postExecuteModels(tx);
1994
+ });
1995
+ } catch (err: any) {
1996
+ console.error('Could not post execute models', err);
1997
+ process.exit(1);
1998
+ }
1999
+ };
@@ -1,5 +1,5 @@
1
- import * as _ from 'lodash';
2
- import {
1
+ import _ from 'lodash';
2
+ import type {
3
3
  AbstractSqlModel,
4
4
  Relationship,
5
5
  ReferencedFieldNode,
@@ -14,7 +14,7 @@ import {
14
14
  UnknownTypeNodes,
15
15
  NullNode,
16
16
  } from '@balena/abstract-sql-compiler';
17
- import { Dictionary } from './common-types';
17
+ import type { Dictionary } from './common-types';
18
18
 
19
19
  export type AliasValidNodeType =
20
20
  | ReferencedFieldNode
@@ -160,9 +160,8 @@ export const translateAbstractSqlModel = (
160
160
  if (synonym.includes('$')) {
161
161
  fromAbstractSqlModel.synonyms[synonym] = canonicalForm;
162
162
  } else {
163
- fromAbstractSqlModel.synonyms[
164
- `${synonym}$${toVersion}`
165
- ] = `${canonicalForm}$${toVersion}`;
163
+ fromAbstractSqlModel.synonyms[`${synonym}$${toVersion}`] =
164
+ `${canonicalForm}$${toVersion}`;
166
165
  }
167
166
  }
168
167
  const relationships = _.cloneDeep(toAbstractSqlModel.relationships);
@@ -199,6 +198,10 @@ export const translateAbstractSqlModel = (
199
198
  }
200
199
 
201
200
  for (const key of fromResourceKeys) {
201
+ if (key.includes('$')) {
202
+ // Skip translated resources, eg `resource$v2`
203
+ continue;
204
+ }
202
205
  const translationDefinition = translationDefinitions[key];
203
206
  const table = fromAbstractSqlModel.tables[key];
204
207
  if (translationDefinition) {