@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.
@@ -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: AbstractSQLCompiler.SqlModel;
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 = getAbstractSqlModel(request);
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 = getAbstractSqlModel(request);
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 = getAbstractSqlModel(request);
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
- models[modelName].sql.rules.map(async (rule) => {
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 const generateModels = (
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: ReturnType<AbstractSQLCompiler.EngineInstance['compileSchema']>;
462
- try {
463
- sql = generateSqlModel(abstractSql, targetDatabaseEngine);
464
- } catch (e) {
465
- console.error(`Error compiling model '${vocab}':`, e);
466
- throw new Error(`Error compiling model '${vocab}': ` + e);
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 { vocab, se, lf, abstractSql, sql, odataMetadata };
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
- // Create tables related to terms and fact types
490
- // Run statements sequentially, as the order of the CREATE TABLE statements matters (eg. for foreign keys).
491
- for (const createStatement of compiledModel.sql.createSchema) {
492
- const promise = tx.executeSql(createStatement);
493
- if (db.engine === 'websql') {
494
- promise.catch((err) => {
495
- console.warn(
496
- "Ignoring errors in the create table statements for websql as it doesn't support CREATE IF NOT EXISTS",
497
- err,
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
- models[apiRoot] = compiledModel;
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
- await validateModel(tx, apiRoot);
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
- await db.transaction(async (tx) => {
615
- const dropStatements: Array<Promise<any>> = models[
616
- vocabulary
617
- ].sql.dropSchema.map((dropStatement) => tx.executeSql(dropStatement));
618
- await Promise.all(
619
- dropStatements.concat([
620
- api.dev.delete({
621
- resource: 'model',
622
- passthrough: {
623
- tx,
624
- req: permissions.root,
625
- },
626
- options: {
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
- 'vocabulary' | 'abstractSqlModel' | 'resourceName'
998
+ | 'translateVersions'
999
+ | 'finalAbstractSqlModel'
1000
+ | 'abstractSqlModel'
1001
+ | 'resourceName'
1002
+ | 'vocabulary'
931
1003
  >,
932
- ) => getAbstractSqlModel(request).tables[resolveSynonym(request)].idField;
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 reqHooks = getHooks({
1029
- method: req.method as SupportedMethod,
1030
- vocabulary,
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
- // Mark that the engine is required now that we've set it
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
- await runHooks('POSTPARSE', $request.hooks, {
1087
- req,
1088
- request: $request,
1089
- tx: req.tx,
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 Object.values(hooks).every(
1426
- (hookTypeHooks) =>
1427
- hookTypeHooks == null || hookTypeHooks.every((hook) => hook.readOnlyTx),
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.resourceName,
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.vocabulary, 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(vocab, request.resourceName, id);
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.vocabulary, 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.vocabulary, 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
+ };