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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) {