@balena/pinejs 15.0.0-build-15-x-9f7b9bfe63741d7ae5f446e013251758d9d6ca26-1 → 15.0.0-build-15-x-4acccbc677d2f8b6c23ace10945cd167339fc8df-1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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;
@@ -614,27 +669,30 @@ const cleanupModel = (vocab: string) => {
614
669
  };
615
670
 
616
671
  export const deleteModel = async (vocabulary: string) => {
617
- await db.transaction(async (tx) => {
618
- const dropStatements: Array<Promise<any>> = models[
619
- vocabulary
620
- ].sql.dropSchema.map((dropStatement) => tx.executeSql(dropStatement));
621
- await Promise.all(
622
- dropStatements.concat([
623
- api.dev.delete({
624
- resource: 'model',
625
- passthrough: {
626
- tx,
627
- req: permissions.root,
628
- },
629
- options: {
630
- $filter: {
631
- is_of__vocabulary: vocabulary,
672
+ const { sql } = models[vocabulary];
673
+ if (sql) {
674
+ await db.transaction(async (tx) => {
675
+ const dropStatements: Array<Promise<any>> = sql.dropSchema.map(
676
+ (dropStatement) => tx.executeSql(dropStatement),
677
+ );
678
+ await Promise.all(
679
+ dropStatements.concat([
680
+ api.dev.delete({
681
+ resource: 'model',
682
+ passthrough: {
683
+ tx,
684
+ req: permissions.root,
632
685
  },
633
- },
634
- }),
635
- ]),
636
- );
637
- });
686
+ options: {
687
+ $filter: {
688
+ is_of__vocabulary: vocabulary,
689
+ },
690
+ },
691
+ }),
692
+ ]),
693
+ );
694
+ });
695
+ }
638
696
  await cleanupModel(vocabulary);
639
697
  };
640
698
 
@@ -927,12 +985,28 @@ export const getAbstractSqlModel = (
927
985
  return (request.abstractSqlModel ??= models[request.vocabulary].abstractSql);
928
986
  };
929
987
 
988
+ const getFinalAbstractSqlModel = (
989
+ request: Pick<
990
+ uriParser.ODataRequest,
991
+ 'translateVersions' | 'finalAbstractSqlModel'
992
+ >,
993
+ ): AbstractSQLCompiler.AbstractSqlModel => {
994
+ const finalModel = _.last(request.translateVersions)!;
995
+ return (request.finalAbstractSqlModel ??= models[finalModel].abstractSql);
996
+ };
997
+
930
998
  const getIdField = (
931
999
  request: Pick<
932
1000
  uriParser.ODataRequest,
933
- 'vocabulary' | 'abstractSqlModel' | 'resourceName'
1001
+ | 'translateVersions'
1002
+ | 'finalAbstractSqlModel'
1003
+ | 'abstractSqlModel'
1004
+ | 'resourceName'
1005
+ | 'vocabulary'
934
1006
  >,
935
- ) => getAbstractSqlModel(request).tables[resolveSynonym(request)].idField;
1007
+ ) =>
1008
+ // TODO: Should resolveSynonym also be using the finalAbstractSqlModel?
1009
+ getFinalAbstractSqlModel(request).tables[resolveSynonym(request)].idField;
936
1010
 
937
1011
  export const getAffectedIds = async (
938
1012
  args: HookArgs & {
@@ -975,17 +1049,18 @@ const $getAffectedIds = async ({
975
1049
  // We reparse to make sure we get a clean odataQuery, without permissions already added
976
1050
  // And we use the request's url rather than the req for things like batch where the req url is ../$batch
977
1051
  const parsedRequest: uriParser.ParsedODataRequest &
978
- Partial<Pick<uriParser.ODataRequest, 'engine'>> =
1052
+ Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>> =
979
1053
  await uriParser.parseOData({
980
1054
  method: request.method,
981
1055
  url: `/${request.vocabulary}${request.url}`,
982
1056
  });
983
1057
 
984
1058
  parsedRequest.engine = request.engine;
1059
+ parsedRequest.translateVersions = request.translateVersions;
985
1060
  // Mark that the engine is required now that we've set it
986
1061
  let affectedRequest: uriParser.ODataRequest = parsedRequest as RequiredField<
987
1062
  typeof parsedRequest,
988
- 'engine'
1063
+ 'engine' | 'translateVersions'
989
1064
  >;
990
1065
  const abstractSqlModel = getAbstractSqlModel(affectedRequest);
991
1066
  const resourceName = resolveSynonym(affectedRequest);
@@ -1028,10 +1103,18 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1028
1103
 
1029
1104
  // Get the hooks for the current method/vocabulary as we know it,
1030
1105
  // in order to run PREPARSE hooks, before parsing gets us more info
1031
- const reqHooks = getHooks({
1032
- method: req.method as SupportedMethod,
1033
- vocabulary,
1034
- });
1106
+ const { versions } = models[vocabulary];
1107
+ const reqHooks = versions.map((version): [string, InstantiatedHooks] => [
1108
+ version,
1109
+ getHooks(
1110
+ {
1111
+ method: req.method as SupportedMethod,
1112
+ vocabulary: version,
1113
+ },
1114
+ // Only include the `all` vocab for the first model version
1115
+ version === versions[0],
1116
+ ),
1117
+ ]);
1035
1118
 
1036
1119
  const transactions: Db.Tx[] = [];
1037
1120
  const tryCancelRequest = () => {
@@ -1074,23 +1157,49 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
1074
1157
 
1075
1158
  const prepareRequest = async (
1076
1159
  parsedRequest: uriParser.ParsedODataRequest &
1077
- Partial<Pick<uriParser.ODataRequest, 'engine'>>,
1160
+ Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>>,
1078
1161
  ): Promise<uriParser.ODataRequest> => {
1079
1162
  parsedRequest.engine = db.engine;
1080
- // Mark that the engine is required now that we've set it
1163
+ parsedRequest.translateVersions = [...versions];
1164
+ // Mark that the engine/translateVersions is required now that we've set it
1081
1165
  const $request: uriParser.ODataRequest = parsedRequest as RequiredField<
1082
1166
  typeof parsedRequest,
1083
- 'engine'
1167
+ 'engine' | 'translateVersions'
1084
1168
  >;
1085
- // Get the full hooks list now that we can.
1086
- $request.hooks = getHooks($request);
1087
1169
  // Add/check the relevant permissions
1088
1170
  try {
1089
- await runHooks('POSTPARSE', $request.hooks, {
1090
- req,
1091
- request: $request,
1092
- tx: req.tx,
1093
- });
1171
+ $request.hooks = [];
1172
+ for (const version of versions) {
1173
+ // We get the hooks list between each `runHooks` so that any resource renames will be used
1174
+ // when getting hooks for later versions
1175
+ const hooks: [string, InstantiatedHooks] = [
1176
+ version,
1177
+ getHooks(
1178
+ {
1179
+ resourceName: $request.resourceName,
1180
+ vocabulary: version,
1181
+ method: $request.method,
1182
+ },
1183
+ // Only include the `all` vocab for the first model version
1184
+ version === versions[0],
1185
+ ),
1186
+ ];
1187
+ $request.hooks.push(hooks);
1188
+ await runHooks('POSTPARSE', [hooks], {
1189
+ req,
1190
+ request: $request,
1191
+ tx: req.tx,
1192
+ });
1193
+ const { resourceRenames } = models[version];
1194
+ if (resourceRenames) {
1195
+ const resourceName = resolveSynonym($request);
1196
+ if (resourceRenames[resourceName]) {
1197
+ $request.resourceName = sqlNameToODataName(
1198
+ resourceRenames[resourceName],
1199
+ );
1200
+ }
1201
+ }
1202
+ }
1094
1203
  const translatedRequest = await uriParser.translateUri($request);
1095
1204
  return await compileRequest(translatedRequest);
1096
1205
  } catch (err: any) {
@@ -1425,9 +1534,12 @@ const checkReadOnlyRequests = (request: uriParser.ODataRequest) => {
1425
1534
  return true;
1426
1535
  }
1427
1536
  // If there are hooks then check that they're all read-only
1428
- return Object.values(hooks).every(
1429
- (hookTypeHooks) =>
1430
- hookTypeHooks == null || hookTypeHooks.every((hook) => hook.readOnlyTx),
1537
+ return hooks.every(([, versionedHooks]) =>
1538
+ Object.values(versionedHooks).every((hookTypeHooks) => {
1539
+ return (
1540
+ hookTypeHooks == null || hookTypeHooks.every((hook) => hook.readOnlyTx)
1541
+ );
1542
+ }),
1431
1543
  );
1432
1544
  };
1433
1545
 
@@ -1522,7 +1634,7 @@ const respondGet = async (
1522
1634
  const d = await odataResponse.process(
1523
1635
  vocab,
1524
1636
  getAbstractSqlModel(request),
1525
- request.resourceName,
1637
+ request.originalResourceName,
1526
1638
  result.rows,
1527
1639
  { includeMetadata: metadata === 'full' },
1528
1640
  );
@@ -1571,7 +1683,7 @@ const runPost = async (
1571
1683
  if (rowsAffected === 0) {
1572
1684
  throw new PermissionError();
1573
1685
  }
1574
- await validateModel(tx, request.vocabulary, request);
1686
+ await validateModel(tx, _.last(request.translateVersions)!, request);
1575
1687
 
1576
1688
  return insertId;
1577
1689
  };
@@ -1583,7 +1695,11 @@ const respondPost = async (
1583
1695
  tx: Db.Tx,
1584
1696
  ): Promise<Response> => {
1585
1697
  const vocab = request.vocabulary;
1586
- const location = odataResponse.resourceURI(vocab, request.resourceName, id);
1698
+ const location = odataResponse.resourceURI(
1699
+ vocab,
1700
+ request.originalResourceName,
1701
+ id,
1702
+ );
1587
1703
  if (env.DEBUG) {
1588
1704
  api[vocab].logger.log('Insert ID: ', request.resourceName, id);
1589
1705
  }
@@ -1641,7 +1757,7 @@ const runPut = async (
1641
1757
  ({ rowsAffected } = await runQuery(tx, request, undefined, idField));
1642
1758
  }
1643
1759
  if (rowsAffected > 0) {
1644
- await validateModel(tx, request.vocabulary, request);
1760
+ await validateModel(tx, _.last(request.translateVersions)!, request);
1645
1761
  }
1646
1762
  return undefined;
1647
1763
  };
@@ -1679,7 +1795,7 @@ const runDelete = async (
1679
1795
  getIdField(request),
1680
1796
  );
1681
1797
  if (rowsAffected > 0) {
1682
- await validateModel(tx, request.vocabulary, request);
1798
+ await validateModel(tx, _.last(request.translateVersions)!, request);
1683
1799
  }
1684
1800
 
1685
1801
  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
+ };