@balena/pinejs 16.0.0-build--batch-f2ffc3d6bcb9f3294fd4fc9de3c21bfe167e100d-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.
- package/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +2168 -11
- package/CHANGELOG.md +815 -2
- package/Gruntfile.ts +9 -6
- package/README.md +10 -0
- package/build/browser.ts +2 -2
- package/build/config.ts +1 -1
- package/build/module.ts +2 -2
- package/build/server.ts +2 -2
- package/docker-compose.npm-test.yml +21 -3
- package/out/bin/abstract-sql-compiler.js +5 -5
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +10 -10
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +34 -11
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/bin/utils.js +25 -2
- package/out/bin/utils.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +4 -2
- package/out/config-loader/config-loader.js +54 -13
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +2 -1
- package/out/config-loader/env.js +5 -2
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +1 -1
- package/out/data-server/sbvr-server.js +3 -1
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.js +40 -14
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +5 -3
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +1 -1
- package/out/http-transactions/transactions.js +10 -5
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.js +32 -5
- package/out/migrator/async.js.map +1 -1
- package/out/migrator/sync.d.ts +2 -1
- package/out/migrator/sync.js +29 -3
- package/out/migrator/sync.js.map +1 -1
- package/out/migrator/utils.d.ts +6 -3
- package/out/migrator/utils.js +30 -4
- package/out/migrator/utils.js.map +1 -1
- package/out/odata-metadata/odata-metadata-generator.js +4 -1
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/passport-pinejs/mount-login-router.d.ts +3 -0
- package/out/passport-pinejs/mount-login-router.js +65 -0
- package/out/passport-pinejs/mount-login-router.js.map +1 -0
- package/out/passport-pinejs/passport-pinejs.d.ts +2 -1
- package/out/passport-pinejs/passport-pinejs.js +28 -2
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +30 -7
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +2 -2
- package/out/sbvr-api/abstract-sql.js +35 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +9 -6
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/common-types.d.ts +1 -1
- package/out/sbvr-api/control-flow.js +5 -2
- package/out/sbvr-api/control-flow.js.map +1 -1
- package/out/sbvr-api/express-extension.d.ts +10 -7
- package/out/sbvr-api/express-extension.js +1 -0
- package/out/sbvr-api/hooks.d.ts +5 -1
- package/out/sbvr-api/hooks.js +12 -10
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.d.ts +5 -2
- package/out/sbvr-api/odata-response.js +36 -6
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.d.ts +6 -7
- package/out/sbvr-api/permissions.js +69 -38
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +20 -9
- package/out/sbvr-api/sbvr-utils.js +134 -136
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +2 -2
- package/out/sbvr-api/translations.js +17 -10
- package/out/sbvr-api/translations.js.map +1 -1
- package/out/sbvr-api/uri-parser.d.ts +7 -10
- package/out/sbvr-api/uri-parser.js +46 -19
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/server-glue/global-ext.d.ts +2 -1
- package/out/server-glue/module.d.ts +3 -1
- package/out/server-glue/module.js +40 -13
- package/out/server-glue/module.js.map +1 -1
- package/out/server-glue/sbvr-loader.js.map +1 -1
- package/out/server-glue/server.js +31 -39
- package/out/server-glue/server.js.map +1 -1
- package/out/webresource-handler/handlers/NoopHandler.d.ts +7 -0
- package/out/webresource-handler/handlers/NoopHandler.js +20 -0
- package/out/webresource-handler/handlers/NoopHandler.js.map +1 -0
- package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
- package/out/webresource-handler/handlers/S3Handler.js +97 -0
- package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
- package/out/webresource-handler/handlers/index.d.ts +2 -0
- package/out/webresource-handler/handlers/index.js +19 -0
- package/out/webresource-handler/handlers/index.js.map +1 -0
- package/out/webresource-handler/index.d.ts +34 -0
- package/out/webresource-handler/index.js +307 -0
- package/out/webresource-handler/index.js.map +1 -0
- package/package.json +68 -62
- package/src/bin/abstract-sql-compiler.ts +7 -9
- package/src/bin/odata-compiler.ts +12 -15
- package/src/bin/sbvr-compiler.ts +14 -18
- package/src/bin/utils.ts +1 -1
- package/src/config-loader/config-loader.ts +44 -10
- package/src/config-loader/env.ts +1 -1
- package/src/data-server/sbvr-server.js +3 -1
- package/src/database-layer/db.ts +23 -19
- package/src/express-emulator/express.js +5 -3
- package/src/extended-sbvr-parser/extended-sbvr-parser.ts +1 -1
- package/src/http-transactions/transactions.js +10 -5
- package/src/migrator/async.ts +7 -6
- package/src/migrator/sync.ts +10 -7
- package/src/migrator/utils.ts +11 -5
- package/src/odata-metadata/odata-metadata-generator.ts +2 -2
- package/src/passport-pinejs/mount-login-router.ts +46 -0
- package/src/passport-pinejs/passport-pinejs.ts +7 -3
- package/src/pinejs-session-store/pinejs-session-store.ts +6 -6
- package/src/sbvr-api/abstract-sql.ts +5 -5
- package/src/sbvr-api/cached-compile.ts +1 -2
- package/src/sbvr-api/common-types.ts +1 -1
- package/src/sbvr-api/control-flow.ts +1 -1
- package/src/sbvr-api/express-extension.ts +12 -8
- package/src/sbvr-api/hooks.ts +11 -11
- package/src/sbvr-api/odata-response.ts +56 -9
- package/src/sbvr-api/permissions.ts +44 -35
- package/src/sbvr-api/sbvr-utils.ts +118 -172
- package/src/sbvr-api/translations.ts +9 -6
- package/src/sbvr-api/uri-parser.ts +22 -28
- package/src/server-glue/global-ext.d.ts +2 -1
- package/src/server-glue/module.ts +8 -2
- package/src/server-glue/sbvr-loader.ts +1 -1
- package/src/server-glue/server.ts +11 -49
- package/src/webresource-handler/handlers/NoopHandler.ts +21 -0
- package/src/webresource-handler/handlers/S3Handler.ts +143 -0
- package/src/webresource-handler/handlers/index.ts +2 -0
- package/src/webresource-handler/index.ts +450 -0
- package/tsconfig.dev.json +2 -1
- package/tsconfig.json +1 -1
- package/typings/lf-to-abstract-sql.d.ts +1 -1
- 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
|
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
|
-
//
|
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
|
-
|
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
|
-
|
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,
|
1060
|
+
const { body: responseBody, statusCode, headers } = response as Response;
|
1029
1061
|
|
1030
|
-
if (
|
1062
|
+
if (statusCode != null && statusCode >= 400) {
|
1031
1063
|
const ErrorClass =
|
1032
|
-
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(
|
1068
|
+
throw new HttpError(statusCode, undefined, responseBody, headers);
|
1037
1069
|
}
|
1038
1070
|
|
1039
1071
|
return responseBody as AnyObject | undefined;
|
@@ -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
|
-
|
1114
|
-
id: request.batchRequestId,
|
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
|
1161
|
-
|
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.
|
1303
|
-
|
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
|
-
|
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 =
|
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)) {
|
@@ -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.
|
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
|
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).
|
1473
|
-
responses
|
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
|
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,
|
1455
|
+
const { body, headers, statusCode } = response as Response;
|
1515
1456
|
res.set(headers);
|
1516
|
-
res.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
|
-
|
1527
|
-
): RequiredField<Response, 'status'> => {
|
1528
|
-
const message = err.getResponseBody();
|
1467
|
+
): RequiredField<Response, 'statusCode'> => {
|
1529
1468
|
return {
|
1530
|
-
|
1531
|
-
body:
|
1469
|
+
statusCode: err.status,
|
1470
|
+
body: err.getResponseBody(),
|
1532
1471
|
headers: err.headers,
|
1533
1472
|
};
|
1534
1473
|
};
|
@@ -1642,8 +1581,7 @@ const runChangeSet =
|
|
1642
1581
|
throw new Error('No request id');
|
1643
1582
|
}
|
1644
1583
|
result.headers ??= {};
|
1645
|
-
result.headers['content-id'] = request.
|
1646
|
-
result.id = request.batchRequestId;
|
1584
|
+
result.headers['content-id'] = request.id;
|
1647
1585
|
changeSetResults.set(request.id, result);
|
1648
1586
|
};
|
1649
1587
|
|
@@ -1682,29 +1620,22 @@ const prepareResponse = async (
|
|
1682
1620
|
result: any,
|
1683
1621
|
tx: Db.Tx,
|
1684
1622
|
): Promise<Response> => {
|
1685
|
-
let response: Response;
|
1686
1623
|
switch (request.method) {
|
1687
1624
|
case 'GET':
|
1688
|
-
|
1689
|
-
break;
|
1625
|
+
return await respondGet(req, request, result, tx);
|
1690
1626
|
case 'POST':
|
1691
|
-
|
1692
|
-
break;
|
1627
|
+
return await respondPost(req, request, result, tx);
|
1693
1628
|
case 'PUT':
|
1694
1629
|
case 'PATCH':
|
1695
1630
|
case 'MERGE':
|
1696
|
-
|
1697
|
-
break;
|
1631
|
+
return await respondPut(req, request, result, tx);
|
1698
1632
|
case 'DELETE':
|
1699
|
-
|
1700
|
-
break;
|
1633
|
+
return await respondDelete(req, request, result, tx);
|
1701
1634
|
case 'OPTIONS':
|
1702
|
-
|
1703
|
-
break;
|
1635
|
+
return await respondOptions(req, request, result, tx);
|
1704
1636
|
default:
|
1705
1637
|
throw new MethodNotAllowedError();
|
1706
1638
|
}
|
1707
|
-
return { ...response, id: request.batchRequestId };
|
1708
1639
|
};
|
1709
1640
|
|
1710
1641
|
const checkReadOnlyRequests = (request: uriParser.ODataRequest) => {
|
@@ -1827,7 +1758,7 @@ const respondGet = async (
|
|
1827
1758
|
);
|
1828
1759
|
|
1829
1760
|
const response = {
|
1830
|
-
|
1761
|
+
statusCode: 200,
|
1831
1762
|
body: { d },
|
1832
1763
|
headers: { 'content-type': 'application/json' },
|
1833
1764
|
};
|
@@ -1842,14 +1773,14 @@ const respondGet = async (
|
|
1842
1773
|
} else {
|
1843
1774
|
if (request.resourceName === '$metadata') {
|
1844
1775
|
return {
|
1845
|
-
|
1776
|
+
statusCode: 200,
|
1846
1777
|
body: models[vocab].odataMetadata,
|
1847
1778
|
headers: { 'content-type': 'xml' },
|
1848
1779
|
};
|
1849
1780
|
} else {
|
1850
1781
|
// TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that
|
1851
1782
|
return {
|
1852
|
-
|
1783
|
+
statusCode: 404,
|
1853
1784
|
};
|
1854
1785
|
}
|
1855
1786
|
}
|
@@ -1905,7 +1836,7 @@ const respondPost = async (
|
|
1905
1836
|
}
|
1906
1837
|
|
1907
1838
|
const response = {
|
1908
|
-
|
1839
|
+
statusCode: 201,
|
1909
1840
|
body: result.d[0],
|
1910
1841
|
headers: {
|
1911
1842
|
'content-type': 'application/json',
|
@@ -1953,7 +1884,7 @@ const respondPut = async (
|
|
1953
1884
|
tx: Db.Tx,
|
1954
1885
|
): Promise<Response> => {
|
1955
1886
|
const response = {
|
1956
|
-
|
1887
|
+
statusCode: 200,
|
1957
1888
|
};
|
1958
1889
|
await runHooks('PRERESPOND', request.hooks, {
|
1959
1890
|
req,
|
@@ -2051,3 +1982,18 @@ export const setup = async (
|
|
2051
1982
|
// we can't use IF NOT EXISTS on all dbs, so we have to ignore the error raised if this index already exists
|
2052
1983
|
}
|
2053
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
|
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
|
-
`${
|
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) {
|