@balena/pinejs 14.60.1 → 14.61.0
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.
- package/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +13 -1
- package/CHANGELOG.md +5 -0
- package/VERSION +1 -1
- package/out/bin/odata-compiler.js +4 -1
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +5 -2
- package/out/config-loader/config-loader.js +32 -16
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/sbvr-api/abstract-sql.js +3 -1
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/hooks.d.ts +3 -3
- package/out/sbvr-api/hooks.js +44 -29
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/permissions.js +5 -3
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +8 -2
- package/out/sbvr-api/sbvr-utils.js +124 -56
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +6 -0
- package/out/sbvr-api/translations.js +136 -0
- package/out/sbvr-api/translations.js.map +1 -0
- package/out/sbvr-api/uri-parser.d.ts +4 -1
- package/out/sbvr-api/uri-parser.js +2 -0
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/package.json +2 -2
- package/src/bin/odata-compiler.ts +4 -2
- package/src/config-loader/config-loader.ts +62 -24
- package/src/sbvr-api/abstract-sql.ts +2 -1
- package/src/sbvr-api/hooks.ts +80 -33
- package/src/sbvr-api/permissions.ts +9 -3
- package/src/sbvr-api/sbvr-utils.ts +193 -77
- package/src/sbvr-api/translations.ts +219 -0
- package/src/sbvr-api/uri-parser.ts +6 -1
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
rollbackRequestHooks,
|
|
64
64
|
getHooks,
|
|
65
65
|
runHooks,
|
|
66
|
+
InstantiatedHooks,
|
|
66
67
|
} from './hooks';
|
|
67
68
|
export {
|
|
68
69
|
HookReq,
|
|
@@ -92,6 +93,7 @@ import {
|
|
|
92
93
|
export { resolveOdataBind } from './abstract-sql';
|
|
93
94
|
import * as odataResponse from './odata-response';
|
|
94
95
|
import { env } from '../server-glue/module';
|
|
96
|
+
import { translateAbstractSqlModel } from './translations';
|
|
95
97
|
|
|
96
98
|
const LF2AbstractSQLTranslator = LF2AbstractSQL.createTranslator(sbvrTypes);
|
|
97
99
|
const LF2AbstractSQLTranslatorVersion = `${LF2AbstractSQLVersion}+${sbvrTypesVersion}`;
|
|
@@ -102,14 +104,18 @@ export type ExecutableModel =
|
|
|
102
104
|
|
|
103
105
|
interface CompiledModel {
|
|
104
106
|
vocab: string;
|
|
107
|
+
translateTo?: string;
|
|
108
|
+
resourceRenames?: ReturnType<typeof translateAbstractSqlModel>;
|
|
105
109
|
se?: string | undefined;
|
|
106
110
|
lf?: LFModel | undefined;
|
|
107
111
|
abstractSql: AbstractSQLCompiler.AbstractSqlModel;
|
|
108
|
-
sql
|
|
112
|
+
sql?: AbstractSQLCompiler.SqlModel;
|
|
109
113
|
odataMetadata: ReturnType<typeof generateODataMetadata>;
|
|
110
114
|
}
|
|
111
115
|
const models: {
|
|
112
|
-
[vocabulary: string]: CompiledModel
|
|
116
|
+
[vocabulary: string]: CompiledModel & {
|
|
117
|
+
versions: string[];
|
|
118
|
+
};
|
|
113
119
|
} = {};
|
|
114
120
|
|
|
115
121
|
export interface Actor {
|
|
@@ -234,7 +240,7 @@ const prettifyConstraintError = (
|
|
|
234
240
|
break;
|
|
235
241
|
case 'postgres':
|
|
236
242
|
const resourceName = resolveSynonym(request);
|
|
237
|
-
const abstractSqlModel =
|
|
243
|
+
const abstractSqlModel = getFinalAbstractSqlModel(request);
|
|
238
244
|
matches = new RegExp(
|
|
239
245
|
'"' + abstractSqlModel.tables[resourceName].name + '_(.*?)_key"',
|
|
240
246
|
).exec(err.message);
|
|
@@ -267,7 +273,7 @@ const prettifyConstraintError = (
|
|
|
267
273
|
break;
|
|
268
274
|
case 'postgres':
|
|
269
275
|
const resourceName = resolveSynonym(request);
|
|
270
|
-
const abstractSqlModel =
|
|
276
|
+
const abstractSqlModel = getFinalAbstractSqlModel(request);
|
|
271
277
|
const tableName = abstractSqlModel.tables[resourceName].name;
|
|
272
278
|
matches = new RegExp(
|
|
273
279
|
'"' +
|
|
@@ -299,7 +305,7 @@ const prettifyConstraintError = (
|
|
|
299
305
|
|
|
300
306
|
if (err instanceof db.CheckConstraintError) {
|
|
301
307
|
const resourceName = resolveSynonym(request);
|
|
302
|
-
const abstractSqlModel =
|
|
308
|
+
const abstractSqlModel = getFinalAbstractSqlModel(request);
|
|
303
309
|
const table = abstractSqlModel.tables[resourceName];
|
|
304
310
|
if (table.checks) {
|
|
305
311
|
switch (db.engine) {
|
|
@@ -374,8 +380,12 @@ export const validateModel = async (
|
|
|
374
380
|
'abstractSqlQuery' | 'modifiedFields' | 'method' | 'vocabulary'
|
|
375
381
|
>,
|
|
376
382
|
): Promise<void> => {
|
|
383
|
+
const { sql } = models[modelName];
|
|
384
|
+
if (!sql) {
|
|
385
|
+
throw new Error(`Tried to validate a virtual model: '${modelName}'`);
|
|
386
|
+
}
|
|
377
387
|
await Promise.all(
|
|
378
|
-
|
|
388
|
+
sql.rules.map(async (rule) => {
|
|
379
389
|
if (!isRuleAffected(rule, request)) {
|
|
380
390
|
// If none of the fields intersect we don't need to run the rule! :D
|
|
381
391
|
return;
|
|
@@ -426,11 +436,19 @@ export const generateSqlModel = (
|
|
|
426
436
|
() => AbstractSQLCompiler[targetDatabaseEngine].compileSchema(abstractSql),
|
|
427
437
|
);
|
|
428
438
|
|
|
429
|
-
export
|
|
439
|
+
export function generateModels(
|
|
440
|
+
model: ExecutableModel & { translateTo?: undefined },
|
|
441
|
+
targetDatabaseEngine: AbstractSQLCompiler.Engines,
|
|
442
|
+
): RequiredField<CompiledModel, 'sql'>;
|
|
443
|
+
export function generateModels(
|
|
444
|
+
model: ExecutableModel,
|
|
445
|
+
targetDatabaseEngine: AbstractSQLCompiler.Engines,
|
|
446
|
+
): CompiledModel;
|
|
447
|
+
export function generateModels(
|
|
430
448
|
model: ExecutableModel,
|
|
431
449
|
targetDatabaseEngine: AbstractSQLCompiler.Engines,
|
|
432
|
-
): CompiledModel
|
|
433
|
-
const { apiRoot: vocab, modelText: se } = model;
|
|
450
|
+
): CompiledModel {
|
|
451
|
+
const { apiRoot: vocab, modelText: se, translateTo, translations } = model;
|
|
434
452
|
let { abstractSql: maybeAbstractSql } = model;
|
|
435
453
|
|
|
436
454
|
let lf: ReturnType<typeof generateLfModel> | undefined;
|
|
@@ -458,16 +476,41 @@ export const generateModels = (
|
|
|
458
476
|
() => generateODataMetadata(vocab, abstractSql),
|
|
459
477
|
);
|
|
460
478
|
|
|
461
|
-
let sql:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
479
|
+
let sql: AbstractSQLCompiler.SqlModel | undefined;
|
|
480
|
+
let resourceRenames: ReturnType<typeof translateAbstractSqlModel> | undefined;
|
|
481
|
+
|
|
482
|
+
if (translateTo != null) {
|
|
483
|
+
resourceRenames = translateAbstractSqlModel(
|
|
484
|
+
abstractSql,
|
|
485
|
+
models[translateTo].abstractSql,
|
|
486
|
+
model.apiRoot,
|
|
487
|
+
translateTo,
|
|
488
|
+
translations,
|
|
489
|
+
);
|
|
490
|
+
} else {
|
|
491
|
+
for (const [key, table] of Object.entries(abstractSql.tables)) {
|
|
492
|
+
// Alias the current version so it can be explicitly referenced
|
|
493
|
+
abstractSql.tables[`${key}$${model.apiRoot}`] = { ...table };
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
sql = generateSqlModel(abstractSql, targetDatabaseEngine);
|
|
497
|
+
} catch (e) {
|
|
498
|
+
console.error(`Error compiling model '${vocab}':`, e);
|
|
499
|
+
throw new Error(`Error compiling model '${vocab}': ` + e);
|
|
500
|
+
}
|
|
467
501
|
}
|
|
468
502
|
|
|
469
|
-
return {
|
|
470
|
-
|
|
503
|
+
return {
|
|
504
|
+
vocab,
|
|
505
|
+
translateTo,
|
|
506
|
+
resourceRenames,
|
|
507
|
+
se,
|
|
508
|
+
lf,
|
|
509
|
+
abstractSql,
|
|
510
|
+
sql,
|
|
511
|
+
odataMetadata,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
471
514
|
|
|
472
515
|
export const executeModel = (
|
|
473
516
|
tx: Db.Tx,
|
|
@@ -486,30 +529,42 @@ export const executeModels = async (
|
|
|
486
529
|
await syncMigrator.run(tx, model);
|
|
487
530
|
const compiledModel = generateModels(model, db.engine);
|
|
488
531
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
532
|
+
if (compiledModel.sql) {
|
|
533
|
+
// Create tables related to terms and fact types
|
|
534
|
+
// Run statements sequentially, as the order of the CREATE TABLE statements matters (eg. for foreign keys).
|
|
535
|
+
for (const createStatement of compiledModel.sql.createSchema) {
|
|
536
|
+
const promise = tx.executeSql(createStatement);
|
|
537
|
+
if (db.engine === 'websql') {
|
|
538
|
+
promise.catch((err) => {
|
|
539
|
+
console.warn(
|
|
540
|
+
"Ignoring errors in the create table statements for websql as it doesn't support CREATE IF NOT EXISTS",
|
|
541
|
+
err,
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
await promise;
|
|
500
546
|
}
|
|
501
|
-
await promise;
|
|
502
547
|
}
|
|
503
548
|
await syncMigrator.postRun(tx, model);
|
|
504
549
|
|
|
505
550
|
odataResponse.prepareModel(compiledModel.abstractSql);
|
|
506
551
|
deepFreeze(compiledModel.abstractSql);
|
|
507
|
-
|
|
552
|
+
|
|
553
|
+
const versions = [apiRoot];
|
|
554
|
+
if (compiledModel.translateTo != null) {
|
|
555
|
+
versions.push(...models[compiledModel.translateTo].versions);
|
|
556
|
+
}
|
|
557
|
+
models[apiRoot] = {
|
|
558
|
+
...compiledModel,
|
|
559
|
+
versions,
|
|
560
|
+
};
|
|
508
561
|
|
|
509
562
|
// Validate the [empty] model according to the rules.
|
|
510
563
|
// This may eventually lead to entering obligatory data.
|
|
511
564
|
// For the moment it blocks such models from execution.
|
|
512
|
-
|
|
565
|
+
if (compiledModel.sql) {
|
|
566
|
+
await validateModel(tx, apiRoot);
|
|
567
|
+
}
|
|
513
568
|
|
|
514
569
|
// TODO: Can we do this without the cast?
|
|
515
570
|
api[apiRoot] = new PinejsClient('/' + apiRoot + '/') as LoggingClient;
|
|
@@ -611,27 +666,30 @@ const cleanupModel = (vocab: string) => {
|
|
|
611
666
|
};
|
|
612
667
|
|
|
613
668
|
export const deleteModel = async (vocabulary: string) => {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
$filter: {
|
|
628
|
-
is_of__vocabulary: vocabulary,
|
|
669
|
+
const { sql } = models[vocabulary];
|
|
670
|
+
if (sql) {
|
|
671
|
+
await db.transaction(async (tx) => {
|
|
672
|
+
const dropStatements: Array<Promise<any>> = sql.dropSchema.map(
|
|
673
|
+
(dropStatement) => tx.executeSql(dropStatement),
|
|
674
|
+
);
|
|
675
|
+
await Promise.all(
|
|
676
|
+
dropStatements.concat([
|
|
677
|
+
api.dev.delete({
|
|
678
|
+
resource: 'model',
|
|
679
|
+
passthrough: {
|
|
680
|
+
tx,
|
|
681
|
+
req: permissions.root,
|
|
629
682
|
},
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
683
|
+
options: {
|
|
684
|
+
$filter: {
|
|
685
|
+
is_of__vocabulary: vocabulary,
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
}),
|
|
689
|
+
]),
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
635
693
|
await cleanupModel(vocabulary);
|
|
636
694
|
};
|
|
637
695
|
|
|
@@ -924,12 +982,28 @@ export const getAbstractSqlModel = (
|
|
|
924
982
|
return (request.abstractSqlModel ??= models[request.vocabulary].abstractSql);
|
|
925
983
|
};
|
|
926
984
|
|
|
985
|
+
const getFinalAbstractSqlModel = (
|
|
986
|
+
request: Pick<
|
|
987
|
+
uriParser.ODataRequest,
|
|
988
|
+
'translateVersions' | 'finalAbstractSqlModel'
|
|
989
|
+
>,
|
|
990
|
+
): AbstractSQLCompiler.AbstractSqlModel => {
|
|
991
|
+
const finalModel = _.last(request.translateVersions)!;
|
|
992
|
+
return (request.finalAbstractSqlModel ??= models[finalModel].abstractSql);
|
|
993
|
+
};
|
|
994
|
+
|
|
927
995
|
const getIdField = (
|
|
928
996
|
request: Pick<
|
|
929
997
|
uriParser.ODataRequest,
|
|
930
|
-
|
|
998
|
+
| 'translateVersions'
|
|
999
|
+
| 'finalAbstractSqlModel'
|
|
1000
|
+
| 'abstractSqlModel'
|
|
1001
|
+
| 'resourceName'
|
|
1002
|
+
| 'vocabulary'
|
|
931
1003
|
>,
|
|
932
|
-
) =>
|
|
1004
|
+
) =>
|
|
1005
|
+
// TODO: Should resolveSynonym also be using the finalAbstractSqlModel?
|
|
1006
|
+
getFinalAbstractSqlModel(request).tables[resolveSynonym(request)].idField;
|
|
933
1007
|
|
|
934
1008
|
export const getAffectedIds = async (
|
|
935
1009
|
args: HookArgs & {
|
|
@@ -972,17 +1046,18 @@ const $getAffectedIds = async ({
|
|
|
972
1046
|
// We reparse to make sure we get a clean odataQuery, without permissions already added
|
|
973
1047
|
// And we use the request's url rather than the req for things like batch where the req url is ../$batch
|
|
974
1048
|
const parsedRequest: uriParser.ParsedODataRequest &
|
|
975
|
-
Partial<Pick<uriParser.ODataRequest, 'engine'>> =
|
|
1049
|
+
Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>> =
|
|
976
1050
|
await uriParser.parseOData({
|
|
977
1051
|
method: request.method,
|
|
978
1052
|
url: `/${request.vocabulary}${request.url}`,
|
|
979
1053
|
});
|
|
980
1054
|
|
|
981
1055
|
parsedRequest.engine = request.engine;
|
|
1056
|
+
parsedRequest.translateVersions = request.translateVersions;
|
|
982
1057
|
// Mark that the engine is required now that we've set it
|
|
983
1058
|
let affectedRequest: uriParser.ODataRequest = parsedRequest as RequiredField<
|
|
984
1059
|
typeof parsedRequest,
|
|
985
|
-
'engine'
|
|
1060
|
+
'engine' | 'translateVersions'
|
|
986
1061
|
>;
|
|
987
1062
|
const abstractSqlModel = getAbstractSqlModel(affectedRequest);
|
|
988
1063
|
const resourceName = resolveSynonym(affectedRequest);
|
|
@@ -1025,10 +1100,18 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
|
|
|
1025
1100
|
|
|
1026
1101
|
// Get the hooks for the current method/vocabulary as we know it,
|
|
1027
1102
|
// in order to run PREPARSE hooks, before parsing gets us more info
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1103
|
+
const { versions } = models[vocabulary];
|
|
1104
|
+
const reqHooks = versions.map((version): [string, InstantiatedHooks] => [
|
|
1105
|
+
version,
|
|
1106
|
+
getHooks(
|
|
1107
|
+
{
|
|
1108
|
+
method: req.method as SupportedMethod,
|
|
1109
|
+
vocabulary: version,
|
|
1110
|
+
},
|
|
1111
|
+
// Only include the `all` vocab for the first model version
|
|
1112
|
+
version === versions[0],
|
|
1113
|
+
),
|
|
1114
|
+
]);
|
|
1032
1115
|
|
|
1033
1116
|
const transactions: Db.Tx[] = [];
|
|
1034
1117
|
const tryCancelRequest = () => {
|
|
@@ -1071,23 +1154,49 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
|
|
|
1071
1154
|
|
|
1072
1155
|
const prepareRequest = async (
|
|
1073
1156
|
parsedRequest: uriParser.ParsedODataRequest &
|
|
1074
|
-
Partial<Pick<uriParser.ODataRequest, 'engine'>>,
|
|
1157
|
+
Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>>,
|
|
1075
1158
|
): Promise<uriParser.ODataRequest> => {
|
|
1076
1159
|
parsedRequest.engine = db.engine;
|
|
1077
|
-
|
|
1160
|
+
parsedRequest.translateVersions = [...versions];
|
|
1161
|
+
// Mark that the engine/translateVersions is required now that we've set it
|
|
1078
1162
|
const $request: uriParser.ODataRequest = parsedRequest as RequiredField<
|
|
1079
1163
|
typeof parsedRequest,
|
|
1080
|
-
'engine'
|
|
1164
|
+
'engine' | 'translateVersions'
|
|
1081
1165
|
>;
|
|
1082
|
-
// Get the full hooks list now that we can.
|
|
1083
|
-
$request.hooks = getHooks($request);
|
|
1084
1166
|
// Add/check the relevant permissions
|
|
1085
1167
|
try {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1168
|
+
$request.hooks = [];
|
|
1169
|
+
for (const version of versions) {
|
|
1170
|
+
// We get the hooks list between each `runHooks` so that any resource renames will be used
|
|
1171
|
+
// when getting hooks for later versions
|
|
1172
|
+
const hooks: [string, InstantiatedHooks] = [
|
|
1173
|
+
version,
|
|
1174
|
+
getHooks(
|
|
1175
|
+
{
|
|
1176
|
+
resourceName: $request.resourceName,
|
|
1177
|
+
vocabulary: version,
|
|
1178
|
+
method: $request.method,
|
|
1179
|
+
},
|
|
1180
|
+
// Only include the `all` vocab for the first model version
|
|
1181
|
+
version === versions[0],
|
|
1182
|
+
),
|
|
1183
|
+
];
|
|
1184
|
+
$request.hooks.push(hooks);
|
|
1185
|
+
await runHooks('POSTPARSE', [hooks], {
|
|
1186
|
+
req,
|
|
1187
|
+
request: $request,
|
|
1188
|
+
tx: req.tx,
|
|
1189
|
+
});
|
|
1190
|
+
const { resourceRenames } = models[version];
|
|
1191
|
+
if (resourceRenames) {
|
|
1192
|
+
const resourceName = resolveSynonym($request);
|
|
1193
|
+
if (resourceRenames[resourceName]) {
|
|
1194
|
+
$request.resourceName = sqlNameToODataName(
|
|
1195
|
+
resourceRenames[resourceName],
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1091
1200
|
const translatedRequest = await uriParser.translateUri($request);
|
|
1092
1201
|
return await compileRequest(translatedRequest);
|
|
1093
1202
|
} catch (err: any) {
|
|
@@ -1422,9 +1531,12 @@ const checkReadOnlyRequests = (request: uriParser.ODataRequest) => {
|
|
|
1422
1531
|
return true;
|
|
1423
1532
|
}
|
|
1424
1533
|
// If there are hooks then check that they're all read-only
|
|
1425
|
-
return
|
|
1426
|
-
(hookTypeHooks) =>
|
|
1427
|
-
|
|
1534
|
+
return hooks.every(([, versionedHooks]) =>
|
|
1535
|
+
Object.values(versionedHooks).every((hookTypeHooks) => {
|
|
1536
|
+
return (
|
|
1537
|
+
hookTypeHooks == null || hookTypeHooks.every((hook) => hook.readOnlyTx)
|
|
1538
|
+
);
|
|
1539
|
+
}),
|
|
1428
1540
|
);
|
|
1429
1541
|
};
|
|
1430
1542
|
|
|
@@ -1519,7 +1631,7 @@ const respondGet = async (
|
|
|
1519
1631
|
const d = await odataResponse.process(
|
|
1520
1632
|
vocab,
|
|
1521
1633
|
getAbstractSqlModel(request),
|
|
1522
|
-
request.
|
|
1634
|
+
request.originalResourceName,
|
|
1523
1635
|
result.rows,
|
|
1524
1636
|
{ includeMetadata: metadata === 'full' },
|
|
1525
1637
|
);
|
|
@@ -1568,7 +1680,7 @@ const runPost = async (
|
|
|
1568
1680
|
if (rowsAffected === 0) {
|
|
1569
1681
|
throw new PermissionError();
|
|
1570
1682
|
}
|
|
1571
|
-
await validateModel(tx, request.
|
|
1683
|
+
await validateModel(tx, _.last(request.translateVersions)!, request);
|
|
1572
1684
|
|
|
1573
1685
|
return insertId;
|
|
1574
1686
|
};
|
|
@@ -1580,7 +1692,11 @@ const respondPost = async (
|
|
|
1580
1692
|
tx: Db.Tx,
|
|
1581
1693
|
): Promise<Response> => {
|
|
1582
1694
|
const vocab = request.vocabulary;
|
|
1583
|
-
const location = odataResponse.resourceURI(
|
|
1695
|
+
const location = odataResponse.resourceURI(
|
|
1696
|
+
vocab,
|
|
1697
|
+
request.originalResourceName,
|
|
1698
|
+
id,
|
|
1699
|
+
);
|
|
1584
1700
|
if (env.DEBUG) {
|
|
1585
1701
|
api[vocab].logger.log('Insert ID: ', request.resourceName, id);
|
|
1586
1702
|
}
|
|
@@ -1638,7 +1754,7 @@ const runPut = async (
|
|
|
1638
1754
|
({ rowsAffected } = await runQuery(tx, request, undefined, idField));
|
|
1639
1755
|
}
|
|
1640
1756
|
if (rowsAffected > 0) {
|
|
1641
|
-
await validateModel(tx, request.
|
|
1757
|
+
await validateModel(tx, _.last(request.translateVersions)!, request);
|
|
1642
1758
|
}
|
|
1643
1759
|
return undefined;
|
|
1644
1760
|
};
|
|
@@ -1676,7 +1792,7 @@ const runDelete = async (
|
|
|
1676
1792
|
getIdField(request),
|
|
1677
1793
|
);
|
|
1678
1794
|
if (rowsAffected > 0) {
|
|
1679
|
-
await validateModel(tx, request.
|
|
1795
|
+
await validateModel(tx, _.last(request.translateVersions)!, request);
|
|
1680
1796
|
}
|
|
1681
1797
|
|
|
1682
1798
|
return undefined;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as _ from 'lodash';
|
|
2
|
+
import {
|
|
3
|
+
AbstractSqlModel,
|
|
4
|
+
Relationship,
|
|
5
|
+
ReferencedFieldNode,
|
|
6
|
+
SelectNode,
|
|
7
|
+
AliasNode,
|
|
8
|
+
Definition,
|
|
9
|
+
RelationshipInternalNode,
|
|
10
|
+
RelationshipLeafNode,
|
|
11
|
+
SelectQueryNode,
|
|
12
|
+
NumberTypeNodes,
|
|
13
|
+
BooleanTypeNodes,
|
|
14
|
+
UnknownTypeNodes,
|
|
15
|
+
NullNode,
|
|
16
|
+
} from '@balena/abstract-sql-compiler';
|
|
17
|
+
import { Dictionary } from './common-types';
|
|
18
|
+
|
|
19
|
+
export type AliasValidNodeType =
|
|
20
|
+
| ReferencedFieldNode
|
|
21
|
+
| SelectQueryNode
|
|
22
|
+
| NumberTypeNodes
|
|
23
|
+
| BooleanTypeNodes
|
|
24
|
+
| UnknownTypeNodes
|
|
25
|
+
| NullNode;
|
|
26
|
+
const aliasFields = (
|
|
27
|
+
abstractSqlModel: AbstractSqlModel,
|
|
28
|
+
resourceName: string,
|
|
29
|
+
aliases: Dictionary<string | AliasValidNodeType>,
|
|
30
|
+
): SelectNode[1] => {
|
|
31
|
+
const fieldNames = abstractSqlModel.tables[resourceName].fields.map(
|
|
32
|
+
({ fieldName }) => fieldName,
|
|
33
|
+
);
|
|
34
|
+
const nonexistentFields = _.difference(Object.keys(aliases), fieldNames);
|
|
35
|
+
if (nonexistentFields.length > 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Tried to alias non-existent fields: '${nonexistentFields.join(', ')}'`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return fieldNames.map(
|
|
41
|
+
(fieldName): AliasNode<AliasValidNodeType> | ReferencedFieldNode => {
|
|
42
|
+
const alias = aliases[fieldName];
|
|
43
|
+
if (alias) {
|
|
44
|
+
if (typeof alias === 'string') {
|
|
45
|
+
return ['Alias', ['ReferencedField', resourceName, alias], fieldName];
|
|
46
|
+
}
|
|
47
|
+
return ['Alias', alias, fieldName];
|
|
48
|
+
}
|
|
49
|
+
return ['ReferencedField', resourceName, fieldName];
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const aliasResource = (
|
|
55
|
+
abstractSqlModel: AbstractSqlModel,
|
|
56
|
+
resourceName: string,
|
|
57
|
+
toResource: string,
|
|
58
|
+
aliases: Dictionary<string | AliasValidNodeType>,
|
|
59
|
+
): Definition => {
|
|
60
|
+
if (!abstractSqlModel.tables[toResource]) {
|
|
61
|
+
throw new Error(`Tried to alias to a non-existent resource: ${toResource}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
abstractSql: [
|
|
65
|
+
'SelectQuery',
|
|
66
|
+
['Select', aliasFields(abstractSqlModel, resourceName, aliases)],
|
|
67
|
+
['From', ['Alias', ['Resource', toResource], resourceName]],
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const namespaceRelationships = (
|
|
73
|
+
relationships: Relationship,
|
|
74
|
+
alias: string,
|
|
75
|
+
): void => {
|
|
76
|
+
for (const [key, relationship] of Object.entries(
|
|
77
|
+
relationships as RelationshipInternalNode,
|
|
78
|
+
)) {
|
|
79
|
+
if (key === '$') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let mapping = (relationship as RelationshipLeafNode).$;
|
|
84
|
+
if (mapping != null && mapping.length === 2) {
|
|
85
|
+
if (!key.includes('$')) {
|
|
86
|
+
mapping = _.cloneDeep(mapping);
|
|
87
|
+
mapping[1]![0] = `${mapping[1]![0]}$${alias}`;
|
|
88
|
+
(relationships as RelationshipInternalNode)[`${key}$${alias}`] = {
|
|
89
|
+
$: mapping,
|
|
90
|
+
};
|
|
91
|
+
delete (relationships as RelationshipInternalNode)[key];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
namespaceRelationships(relationship, alias);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const translateAbstractSqlModel = (
|
|
99
|
+
fromAbstractSqlModel: AbstractSqlModel,
|
|
100
|
+
toAbstractSqlModel: AbstractSqlModel,
|
|
101
|
+
fromVersion: string,
|
|
102
|
+
toVersion: string,
|
|
103
|
+
translationDefinitions: Dictionary<
|
|
104
|
+
| (Definition & { $toResource?: string })
|
|
105
|
+
| Dictionary<string | AliasValidNodeType>
|
|
106
|
+
> = {},
|
|
107
|
+
): Dictionary<string> => {
|
|
108
|
+
const isDefinition = (
|
|
109
|
+
d: (typeof translationDefinitions)[string],
|
|
110
|
+
): d is Definition => 'abstractSql' in d;
|
|
111
|
+
|
|
112
|
+
const resourceRenames: Dictionary<string> = {};
|
|
113
|
+
|
|
114
|
+
fromAbstractSqlModel.rules = toAbstractSqlModel.rules;
|
|
115
|
+
|
|
116
|
+
const fromResourceKeys = Object.keys(fromAbstractSqlModel.tables);
|
|
117
|
+
const nonexistentTables = _.difference(
|
|
118
|
+
Object.keys(translationDefinitions),
|
|
119
|
+
fromResourceKeys,
|
|
120
|
+
);
|
|
121
|
+
if (nonexistentTables.length > 0) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Tried to define non-existent resources: '${nonexistentTables.join(
|
|
124
|
+
', ',
|
|
125
|
+
)}'`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
for (const [synonym, canonicalForm] of Object.entries(
|
|
129
|
+
toAbstractSqlModel.synonyms,
|
|
130
|
+
)) {
|
|
131
|
+
// Don't double alias
|
|
132
|
+
if (synonym.includes('$')) {
|
|
133
|
+
fromAbstractSqlModel.synonyms[synonym] = canonicalForm;
|
|
134
|
+
} else {
|
|
135
|
+
fromAbstractSqlModel.synonyms[
|
|
136
|
+
`${synonym}$${toVersion}`
|
|
137
|
+
] = `${canonicalForm}$${toVersion}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const relationships = _.cloneDeep(toAbstractSqlModel.relationships);
|
|
141
|
+
namespaceRelationships(relationships, toVersion);
|
|
142
|
+
for (let [key, relationship] of Object.entries(relationships)) {
|
|
143
|
+
// Don't double alias
|
|
144
|
+
if (!key.includes('$')) {
|
|
145
|
+
key = `${key}$${toVersion}`;
|
|
146
|
+
}
|
|
147
|
+
fromAbstractSqlModel.relationships[key] = relationship;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// TODO: We also need to keep the original relationship refs to non $version resources
|
|
151
|
+
|
|
152
|
+
// Also alias for ourselves to allow explicit referencing
|
|
153
|
+
const aliasedFromRelationships = _.cloneDeep(
|
|
154
|
+
fromAbstractSqlModel.relationships,
|
|
155
|
+
);
|
|
156
|
+
namespaceRelationships(aliasedFromRelationships, fromVersion);
|
|
157
|
+
for (let [key, relationship] of Object.entries(aliasedFromRelationships)) {
|
|
158
|
+
// Don't double alias
|
|
159
|
+
if (!key.includes('$')) {
|
|
160
|
+
key = `${key}$${fromVersion}`;
|
|
161
|
+
fromAbstractSqlModel.relationships[key] = relationship;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (let [key, table] of Object.entries(toAbstractSqlModel.tables)) {
|
|
166
|
+
// Don't double alias
|
|
167
|
+
if (!key.includes('$')) {
|
|
168
|
+
key = `${key}$${toVersion}`;
|
|
169
|
+
}
|
|
170
|
+
fromAbstractSqlModel.tables[key] = _.cloneDeep(table);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const key of fromResourceKeys) {
|
|
174
|
+
const translationDefinition = translationDefinitions[key];
|
|
175
|
+
const table = fromAbstractSqlModel.tables[key];
|
|
176
|
+
if (translationDefinition) {
|
|
177
|
+
const { $toResource, ...definition } = translationDefinition;
|
|
178
|
+
const hasToResource = typeof $toResource === 'string';
|
|
179
|
+
if (hasToResource) {
|
|
180
|
+
resourceRenames[key] = `${$toResource}`;
|
|
181
|
+
}
|
|
182
|
+
const toResource = hasToResource ? $toResource : `${key}$${toVersion}`;
|
|
183
|
+
// TODO: Should this use the toAbstractSqlModel?
|
|
184
|
+
const toTable = fromAbstractSqlModel.tables[toResource];
|
|
185
|
+
if (!toTable) {
|
|
186
|
+
if (hasToResource) {
|
|
187
|
+
throw new Error(`Unknown $toResource: '${toResource}'`);
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error(`Missing $toResource: '${toResource}'`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
table.modifyFields = _.cloneDeep(toTable.modifyFields ?? toTable.fields);
|
|
193
|
+
table.modifyName = toTable.modifyName ?? toTable.name;
|
|
194
|
+
if (isDefinition(definition)) {
|
|
195
|
+
table.definition = definition;
|
|
196
|
+
} else {
|
|
197
|
+
table.definition = aliasResource(
|
|
198
|
+
fromAbstractSqlModel,
|
|
199
|
+
key,
|
|
200
|
+
toResource,
|
|
201
|
+
definition,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const toTable = fromAbstractSqlModel.tables[`${key}$${toVersion}`];
|
|
206
|
+
if (!toTable) {
|
|
207
|
+
throw new Error(`Missing translation for: '${key}'`);
|
|
208
|
+
}
|
|
209
|
+
table.modifyFields = _.cloneDeep(toTable.modifyFields ?? toTable.fields);
|
|
210
|
+
table.definition = {
|
|
211
|
+
abstractSql: ['Resource', `${key}$${toVersion}`],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Also alias the current version so it can be explicitly referenced
|
|
215
|
+
fromAbstractSqlModel.tables[`${key}$${fromVersion}`] = table;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return resourceRenames;
|
|
219
|
+
};
|