@sap/cds-compiler 4.4.4 → 4.6.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bin/cdsc.js +18 -11
  3. package/bin/cdsv2m.js +7 -5
  4. package/doc/CHANGELOG_BETA.md +22 -0
  5. package/lib/api/main.js +306 -144
  6. package/lib/api/options.js +18 -6
  7. package/lib/api/validate.js +1 -1
  8. package/lib/base/message-registry.js +45 -10
  9. package/lib/base/messages.js +33 -16
  10. package/lib/base/model.js +4 -0
  11. package/lib/base/optionProcessorHelper.js +45 -176
  12. package/lib/checks/annotationsOData.js +49 -0
  13. package/lib/checks/elements.js +32 -34
  14. package/lib/checks/enricher.js +39 -3
  15. package/lib/checks/validator.js +8 -7
  16. package/lib/compiler/assert-consistency.js +40 -17
  17. package/lib/compiler/builtins.js +30 -53
  18. package/lib/compiler/checks.js +46 -14
  19. package/lib/compiler/cycle-detector.js +1 -4
  20. package/lib/compiler/define.js +35 -10
  21. package/lib/compiler/extend.js +21 -7
  22. package/lib/compiler/generate.js +3 -0
  23. package/lib/compiler/populate.js +5 -1
  24. package/lib/compiler/propagator.js +46 -9
  25. package/lib/compiler/resolve.js +94 -35
  26. package/lib/compiler/shared.js +60 -33
  27. package/lib/compiler/tweak-assocs.js +188 -92
  28. package/lib/compiler/utils.js +11 -1
  29. package/lib/edm/annotations/edmJson.js +41 -66
  30. package/lib/edm/annotations/genericTranslation.js +27 -9
  31. package/lib/edm/annotations/preprocessAnnotations.js +2 -3
  32. package/lib/edm/csn2edm.js +28 -11
  33. package/lib/edm/edmInboundChecks.js +58 -15
  34. package/lib/edm/edmPreprocessor.js +12 -16
  35. package/lib/edm/edmUtils.js +5 -2
  36. package/lib/gen/Dictionary.json +10 -0
  37. package/lib/gen/language.checksum +1 -1
  38. package/lib/gen/language.interp +15 -2
  39. package/lib/gen/language.tokens +1 -0
  40. package/lib/gen/languageParser.js +6557 -5618
  41. package/lib/json/from-csn.js +4 -5
  42. package/lib/json/to-csn.js +29 -4
  43. package/lib/language/antlrParser.js +19 -1
  44. package/lib/language/errorStrategy.js +28 -7
  45. package/lib/language/genericAntlrParser.js +118 -24
  46. package/lib/language/textUtils.js +16 -0
  47. package/lib/main.d.ts +28 -3
  48. package/lib/main.js +3 -0
  49. package/lib/model/csnRefs.js +4 -1
  50. package/lib/model/csnUtils.js +20 -14
  51. package/lib/model/revealInternalProperties.js +5 -2
  52. package/lib/optionProcessor.js +23 -22
  53. package/lib/render/manageConstraints.js +13 -29
  54. package/lib/render/toCdl.js +47 -26
  55. package/lib/render/toHdbcds.js +63 -42
  56. package/lib/render/toRename.js +6 -10
  57. package/lib/render/toSql.js +71 -117
  58. package/lib/render/utils/common.js +41 -6
  59. package/lib/transform/.eslintrc.json +9 -1
  60. package/lib/transform/addTenantFields.js +228 -0
  61. package/lib/transform/db/applyTransformations.js +57 -4
  62. package/lib/transform/db/assertUnique.js +4 -4
  63. package/lib/transform/db/backlinks.js +13 -1
  64. package/lib/transform/db/cdsPersistence.js +1 -1
  65. package/lib/transform/db/expansion.js +24 -3
  66. package/lib/transform/db/flattening.js +70 -71
  67. package/lib/transform/db/killAnnotations.js +37 -0
  68. package/lib/transform/db/rewriteCalculatedElements.js +46 -6
  69. package/lib/transform/db/temporal.js +1 -1
  70. package/lib/transform/draft/db.js +2 -16
  71. package/lib/transform/draft/odata.js +3 -3
  72. package/lib/transform/effective/associations.js +3 -5
  73. package/lib/transform/effective/main.js +6 -9
  74. package/lib/transform/forOdata.js +26 -55
  75. package/lib/transform/forRelationalDB.js +38 -18
  76. package/lib/transform/odata/toFinalBaseType.js +3 -3
  77. package/lib/transform/odata/typesExposure.js +14 -5
  78. package/lib/transform/transformUtils.js +47 -34
  79. package/lib/transform/translateAssocsToJoins.js +45 -11
  80. package/lib/transform/universalCsn/coreComputed.js +1 -1
  81. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  82. package/package.json +7 -6
@@ -16,7 +16,8 @@
16
16
  * optionProcessor.command('toXyz')
17
17
  * .help(`Help text for command "toXyz")
18
18
  * .option('-y --y-in-long-form')
19
- * .option(' --bar-wiz <w>', ['bla', 'foo'])
19
+ * .option(' --bar-wiz <w>', { valid: ['bla', 'foo'] })
20
+ * .option('-z --name-in-cdsc', { optionName: 'nameInOptions' })
20
21
  * ```
21
22
  *
22
23
  * Options *must* have a long form, can have at most one <param>, and optionally
@@ -34,29 +35,27 @@ function createOptionProcessor() {
34
35
  optionClashes: [],
35
36
  option,
36
37
  command,
37
- positionalArgument: (argumentDefinition) => {
38
+ positionalArgument(argumentDefinition) {
38
39
  // Default positional arguments; may be overwritten by commands.
39
40
  _setPositionalArguments(argumentDefinition);
40
41
  return optionProcessor;
41
42
  },
42
43
  help,
43
44
  processCmdLine,
44
- verifyOptions,
45
- camelOptionsForCommand,
46
- // TODO: Why exported?
47
- _parseCommandString,
48
- _parseOptionString,
49
45
  };
50
46
  return optionProcessor;
51
47
 
52
48
  /**
53
49
  * API: Define a general option.
54
50
  * @param {string} optString Option string describing the command line option.
55
- * @param {string[]} [validValues] Array of valid values for the options.
56
- * @param {object} [options] Further options such as `ignoreCase: true`
51
+ * @param {object} [options] Further options such as `ignoreCase: true` and `valid: []`.
52
+ * @param {string[]} [options.valid] Valid values for the option.
53
+ * @param {string[]} [options.ignoreCase] Ignore the case for "options.valid".
54
+ * @param {string[]} [options.optionName] Name of the option after parsing CLI arguments.
55
+ * Defaults to the camelified name of the long name.
57
56
  */
58
- function option( optString, validValues, options ) {
59
- return _addOption(optionProcessor, optString, validValues, options);
57
+ function option( optString, options ) {
58
+ return _addOption(optionProcessor, optString, options);
60
59
  }
61
60
 
62
61
  /**
@@ -78,7 +77,7 @@ function createOptionProcessor() {
78
77
  options: {},
79
78
  positionalArguments: [],
80
79
  option: commandOption,
81
- positionalArgument: (argumentDefinition) => {
80
+ positionalArgument(argumentDefinition) {
82
81
  _setPositionalArguments(argumentDefinition, cmd.positionalArguments);
83
82
  return cmd;
84
83
  },
@@ -99,8 +98,8 @@ function createOptionProcessor() {
99
98
  return cmd;
100
99
 
101
100
  // Command API: Define a command option
102
- function commandOption( optString, validValues, options ) {
103
- return _addOption(cmd, optString, validValues, options);
101
+ function commandOption( optString, options ) {
102
+ return _addOption(cmd, optString, options);
104
103
  }
105
104
 
106
105
  // Command API: Define the command help text
@@ -124,13 +123,15 @@ function createOptionProcessor() {
124
123
  if (argList.find(arg => arg.isDynamic))
125
124
  throw new Error('Can\'t add positional arguments after a dynamic one');
126
125
 
127
-
128
126
  const registeredNames = argList.map(arg => arg.name);
129
127
  const args = argumentDefinition.split(' ');
130
128
 
131
129
  for (const arg of args) {
132
130
  // Remove braces, dots and camelify.
133
- const argName = arg.replace('<', '').replace('>', '').replace('...', '').replace(/[ -]./g, s => s.substring(1).toUpperCase());
131
+ const argName = arg.replace('<', '')
132
+ .replace('>', '')
133
+ .replace('...', '')
134
+ .replace(/[ -]./g, s => s.substring(1).toUpperCase());
134
135
 
135
136
  if (registeredNames.includes(argName))
136
137
  throw new Error(`Duplicate positional argument: ${ arg }`);
@@ -138,7 +139,6 @@ function createOptionProcessor() {
138
139
  if (!isParam(arg) && !isDynamicPositionalArgument(arg))
139
140
  throw new Error(`Unknown positional argument syntax: ${ arg }`);
140
141
 
141
-
142
142
  argList.push({
143
143
  name: argName,
144
144
  isDynamic: isDynamicPositionalArgument(arg),
@@ -157,65 +157,42 @@ function createOptionProcessor() {
157
157
  * @private
158
158
  * @see option()
159
159
  */
160
- function _addOption( cmd, optString, validValues, options ) {
161
- const cliOpt = _parseOptionString(optString, validValues);
160
+ function _addOption( cmd, optString, options ) {
161
+ const cliOpt = _parseOptionString(optString, options);
162
162
  Object.assign(cliOpt, options);
163
- _addLongOption(cmd, cliOpt.longName, cliOpt);
164
- _addShortOption(cmd, cliOpt.shortName, cliOpt);
163
+ _addOptionName(cmd, cliOpt.longName, cliOpt);
164
+ _addOptionName(cmd, cliOpt.shortName, cliOpt);
165
165
 
166
166
  for (const alias of cliOpt.aliases || []) {
167
167
  const aliasOpt = Object.assign({ }, cliOpt, { isAlias: true });
168
- _addLongOption(cmd, alias, aliasOpt); // use same camelName, etc. for alias
168
+ _addOptionName(cmd, alias, aliasOpt); // use same optionName, etc. for alias
169
169
  }
170
170
 
171
171
  return cmd;
172
172
  }
173
173
 
174
174
  /**
175
- * Internal: Add longName to the list of options.
176
- * Throws if the option is already registered in the given command context.
177
- * or in the given command.
178
- * `longName` may differ from `opt.longName`, e.g. for aliases.
179
- *
180
- * @private
181
- * @see _addOption()
182
- */
183
- function _addLongOption( cmd, longName, opt ) {
184
- if (cmd.options[longName]) {
185
- throw new Error(`Duplicate assignment for long option ${ longName }`);
186
- }
187
- else if (optionProcessor.options[longName]) {
188
- // This path is only taken if optString is for commands
189
- optionProcessor.optionClashes.push({
190
- option: longName,
191
- description: `Command '${ cmd.longName }' has option clash with general options for: ${ longName }`,
192
- });
193
- }
194
- cmd.options[longName] = opt;
195
- }
196
- /**
197
- * Internal: Add shortName to the list of options.
175
+ * Internal: Add a name to the list of options.
198
176
  * Throws if the option is already registered in the given command context.
199
177
  * or in the given command.
200
- * `longName` may differ from `opt.longName`, e.g. for aliases.
201
178
  *
202
179
  * @private
203
180
  * @see _addOption()
204
181
  */
205
- function _addShortOption( cmd, shortName, opt ) {
206
- if (!shortName)
182
+ function _addOptionName( cmd, name, opt ) {
183
+ if (!name)
207
184
  return;
208
- if (cmd.options[shortName]) {
209
- throw new Error(`Duplicate assignment for short option ${ shortName }`);
185
+ if (cmd.options[name]) {
186
+ throw new Error(`Duplicate assignment for option ${ name }`);
210
187
  }
211
- else if (optionProcessor.options[shortName]) {
188
+ else if (optionProcessor.options[name]) {
212
189
  // This path is only taken if optString is for commands
213
190
  optionProcessor.optionClashes.push({
214
- option: shortName,
215
- description: `Command '${ cmd.longName }' has option clash with general options for: ${ shortName }`,
191
+ option: name,
192
+ description: `Command '${ cmd.longName }' has option clash with general options for: ${ name }`,
216
193
  });
217
194
  }
218
- cmd.options[shortName] = opt;
195
+ cmd.options[name] = opt;
219
196
  }
220
197
 
221
198
  // Internal: Parse one command string like "F, toFoo". Return an object like this
@@ -251,11 +228,11 @@ function createOptionProcessor() {
251
228
  // {
252
229
  // longName: '--foo-bar',
253
230
  // shortName: '-f',
254
- // camelName: 'fooBar',
231
+ // optionName: 'fooBar', // or options.optionName if provided
255
232
  // param: '<p>'
256
- // validValues
233
+ // valid
257
234
  // }
258
- function _parseOptionString( optString, validValues ) {
235
+ function _parseOptionString( optString, options ) {
259
236
  let longName;
260
237
  let shortName;
261
238
  let param;
@@ -294,11 +271,11 @@ function createOptionProcessor() {
294
271
  if (!longName)
295
272
  throw new Error(`Invalid option description, missing long name: ${ optString }`);
296
273
 
297
- if (!param && validValues)
274
+ if (!param && options?.valid)
298
275
  throw new Error(`Option description has valid values but no param: ${ optString }`);
299
276
 
300
- if (validValues) {
301
- validValues.forEach((value) => {
277
+ if (options?.valid) {
278
+ options.valid.forEach((value) => {
302
279
  if (typeof value !== 'string')
303
280
  throw new Error(`Valid values must be of type string: ${ optString }`);
304
281
  });
@@ -307,9 +284,9 @@ function createOptionProcessor() {
307
284
  return {
308
285
  longName,
309
286
  shortName,
310
- camelName: camelifyLongOption(longName),
287
+ optionName: options?.option ?? camelifyLongOption(longName),
311
288
  param,
312
- validValues,
289
+ valid: options?.valid,
313
290
  isAlias: false, // default
314
291
  };
315
292
  }
@@ -520,10 +497,10 @@ function createOptionProcessor() {
520
497
  if (currentCommand) {
521
498
  if (!result.options[currentCommand])
522
499
  result.options[currentCommand] = {};
523
- result.options[currentCommand][currentOption.camelName] = val;
500
+ result.options[currentCommand][currentOption.optionName] = val;
524
501
  }
525
502
  else {
526
- result.options[currentOption.camelName] = val;
503
+ result.options[currentOption.optionName] = val;
527
504
  }
528
505
  }
529
506
 
@@ -542,7 +519,7 @@ function createOptionProcessor() {
542
519
  function reportInvalidValue( opt, value ) {
543
520
  const shortOption = opt.shortName ? `${ opt.shortName }, ` : '';
544
521
  const errors = currentCommand ? result.cmdErrors : result.errors;
545
- errors.push(`Invalid value "${ value }" for option "${ shortOption }${ opt.longName }" - use one of [${ opt.validValues }]`);
522
+ errors.push(`Invalid value "${ value }" for option "${ shortOption }${ opt.longName }" - use one of [${ opt.valid }]`);
546
523
  }
547
524
 
548
525
  /**
@@ -580,114 +557,14 @@ function createOptionProcessor() {
580
557
  }
581
558
  }
582
559
 
583
- // Assuming that 'options' came from this option processor, verify options therein.
584
- // If 'command' is supplied, check only 'options.command', otherwise check
585
- // only top-level options
586
- // Return an array of complaints (possibly empty)
587
- function verifyOptions( options, commandName = undefined, silent = false ) {
588
- const result = [];
589
- let opts;
590
-
591
- if ((options.betaMode || options.beta) && !options.testMode && !silent) {
592
- const mode = options.beta ? 'beta' : 'beta-mode';
593
- result.push(`Option --${ mode } was used. This option should not be used in productive scenarios!`);
594
- }
595
-
596
- if (options) {
597
- [
598
- 'defaultBinaryLength', 'defaultStringLength',
599
- /* 'length', 'precision', 'scale' */
600
- ].forEach((facet) => {
601
- if (options[facet] && isNaN(options[facet]))
602
- result.push(`Invalid value "${ options[facet] }" for option "--${ facet }" - not an Integer`);
603
- else
604
- options[facet] = parseInt(options[facet]);
605
- });
606
- }
607
-
608
- if (commandName) {
609
- const cmd = optionProcessor.commands[commandName];
610
- if (!cmd)
611
- throw new Error(`Expected existing command: "${ cmd }"`);
612
-
613
- opts = cmd.options;
614
- options = options[cmd] || {};
615
- if (typeof options === 'boolean') {
616
- // Special case: command without options
617
- options = {};
618
- }
619
- }
620
- else {
621
- opts = optionProcessor.options;
622
- }
623
- // Look at each supplied option
624
- for (const camelName in options) {
625
- const opt = opts[uncamelifyLongOption(camelName)];
626
- let error;
627
- if (!opt) {
628
- // Don't report commands in top-level options
629
- if ((commandName || !optionProcessor.commands[camelName]) && !silent) {
630
- const prefix = commandName ? `${commandName}.` : '';
631
- error = `Unknown option "${prefix}${camelName}"`;
632
- }
633
- }
634
- else {
635
- const param = options[camelName];
636
- error = verifyOptionParam(param, opt, commandName ? `${ commandName }.` : '');
637
- }
638
- if (error)
639
- result.push(error);
640
- }
641
- // hard-coded option dependencies (they disappear with command)
642
- return result;
643
-
644
- /**
645
- * Verify parameter value 'param' against option definition 'opt'. Return an error
646
- * string or false for an accepted param. Use 'prefix' when mentioning the option name.
647
- *
648
- * @param param
649
- * @param opt
650
- * @param prefix
651
- * @return {string|boolean}
652
- */
653
- function verifyOptionParam( param, opt, prefix ) {
654
- if (opt.param) {
655
- // Parameter is required for this option
656
- if (typeof param === 'boolean')
657
- return `Missing value for option "${ prefix }${ opt.camelName }"`;
658
- else if (!isValidOptionValue(opt, param))
659
- return `Invalid value "${ param }" for option "${ prefix }${ opt.camelName }" - use one of [${ opt.validValues }]`;
660
-
661
- return false;
662
- }
663
- // Option does not expect a parameter
664
- if (typeof param !== 'boolean') {
665
- // FIXME: Might be a bit too strict in case of internal sub-options like 'forHana' etc...
666
- return `Expecting boolean value for option "${ prefix }${ opt.camelName }"`;
667
- }
668
-
669
- return false;
670
- }
671
- }
672
-
673
560
  function isValidOptionValue( opt, value ) {
674
561
  // Explicitly convert to string, input 'value' may be boolean
675
562
  value = String(value);
676
- if (!opt.validValues || !opt.validValues.length)
563
+ if (!opt.valid?.length)
677
564
  return true;
678
565
  if (opt.ignoreCase)
679
- return opt.validValues.some( valid => valid.toLowerCase() === value.toLowerCase() );
680
- return opt.validValues.includes(value);
681
- }
682
-
683
- // Return an array of unique camelNames of the options for the specified command
684
- // If invalid command -> an empty array
685
- function camelOptionsForCommand( cmd ) {
686
- if (!cmd || !optionProcessor.commands[cmd])
687
- return [];
688
- cmd = optionProcessor.commands[cmd];
689
- const names = Object.keys(cmd.options).map(name => cmd.options[name].camelName);
690
- return [ ...new Set(names) ];
566
+ return opt.valid.some( valid => valid.toLowerCase() === value.toLowerCase() );
567
+ return opt.valid.includes(value);
691
568
  }
692
569
  }
693
570
 
@@ -713,14 +590,6 @@ function camelifyLongOption( opt ) {
713
590
  return opt.substring(2).replace(/-./g, s => s.substring(1).toUpperCase());
714
591
  }
715
592
 
716
- /**
717
- * Return a long option name like "--foo-bar" for a camel-case name "fooBar"
718
- */
719
- function uncamelifyLongOption( opt ) {
720
- const longForm = opt.replace(/[A-Z]/g, s => `-${ s.toLowerCase() }`);
721
- return `--${ longForm }`;
722
- }
723
-
724
593
  /**
725
594
  * Check if 'opt' looks like a "-f" short option
726
595
  */
@@ -66,9 +66,58 @@ function checkReadOnlyAndInsertOnly( artifact, artifactName ) {
66
66
  this.warning(null, artifact.$path, {}, 'Annotations “@readonly” and “@insertonly” can\'t be assigned in combination');
67
67
  }
68
68
 
69
+ /**
70
+ * Check temporal annotations @cds.valid.from, @cds.valid.to, @cds.valid.key
71
+ * assignment for the given artifact. This consists of the following:
72
+ * - @cds.valid.from/to/key annotation is assigned only once in the scope of the definition
73
+ * - annotation is assigned only to allowed element types. Not allowed on association/composition,
74
+ * structured elements, leaf element of a structure
75
+ * - when @cds.valid.key is used, it requires also @cds.valid.from and @cds.valid.to to be defined
76
+ * @param {CSN.Artifact} artifact
77
+ * @param {string} artifactName
78
+ */
79
+ function checkTemporalAnnotationsAssignment( artifact, artifactName ) {
80
+ const valid = { from: [], to: [], key: [] };
81
+
82
+ // collect annotation assignments throughout the elements of the definition
83
+ this.recurseElements( artifact, artifact.$path || [ 'definitions', artifactName ], (member, path) => {
84
+ checkForAnnoAssignmentAndApplicability.bind(this)('from', member, path);
85
+ checkForAnnoAssignmentAndApplicability.bind(this)('to', member, path);
86
+ checkForAnnoAssignmentAndApplicability.bind(this)('key', member, path);
87
+ });
88
+
89
+ // check if the annotations are assigned more than once in the scope of the current artifact
90
+ this.checkMultipleAssignments(valid.from, '@cds.valid.from', artifact, artifactName);
91
+ this.checkMultipleAssignments(valid.to, '@cds.valid.to', artifact, artifactName);
92
+ this.checkMultipleAssignments(valid.key, '@cds.valid.key', artifact, artifactName);
93
+
94
+ // if @cds.valid.key is defined, check whether @cds.valid.from and @cds.valid.to are also there
95
+ if (valid.key.length && !(valid.from.length && valid.to.length))
96
+ this.error(null, [ 'definitions', artifactName ], 'Annotation “@cds.valid.key” was used but “@cds.valid.from” and “@cds.valid.to” are missing');
97
+
98
+ /**
99
+ * Check if the given annotation is assigned to the current member and collect the path if so.
100
+ * Also determine whether the annotation is applicable for the member type. @cds.valid.from/to.key annotations
101
+ * are NOT allowed for elements which are: association/composition, structured or leaf element of a structure
102
+ *
103
+ * @param {string} annoIdentifier
104
+ * @param {CSN.Element} member
105
+ * @param {CSN.Path} path
106
+ */
107
+ function checkForAnnoAssignmentAndApplicability( annoIdentifier, member, path ) {
108
+ if (this.csnUtils.hasAnnotationValue(member, `@cds.valid.${ annoIdentifier }`)) {
109
+ valid[annoIdentifier].push(path);
110
+ // check whether annotation is not assigned to not allowed element type, these are: association, structured elements, leaf element of a structure
111
+ if (this.csnUtils.isAssocOrComposition(member) || this.csnUtils.isStructured(member) || path.length > 5)
112
+ this.error(null, member.$path, { anno: `@cds.valid.${ annoIdentifier }` }, 'Element can\'t be annotated with $(ANNO)');
113
+ }
114
+ }
115
+ }
116
+
69
117
  module.exports = {
70
118
  checkCoreMediaTypeAllowance,
71
119
  checkAnalytics,
72
120
  checkAtSapAnnotations,
73
121
  checkReadOnlyAndInsertOnly,
122
+ checkTemporalAnnotationsAssignment,
74
123
  };
@@ -112,45 +112,43 @@ function checkVirtualElement( member ) {
112
112
  * Checks whether managed associations
113
113
  * with cardinality 'to many' have an on-condition.
114
114
  *
115
- * @param {CSN.Artifact} art The artifact
116
- * @todo this is a member validator, is it not?
115
+ * @param {CSN.Artifact} member The member (e.g. element artifact)
117
116
  */
118
- function checkManagedAssoc( art ) {
119
- forEachMemberRecursively(art, (member) => {
120
- if (this.csnUtils.isAssocOrComposition(member) &&
121
- !isManagedComposition.bind(this)(member)) {
122
- // Implementation note: Imported services (i.e. external ones) may contain to-many associations
123
- // with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
124
- // foreign key array, we won't emit a warning to avoid spamming the user.
125
- const max = member.cardinality?.max ? member.cardinality.max : 1;
126
- if (max !== 1 && !member.on && (!member.keys || member.keys.length > 0)) {
127
- const isNoDb = art['@cds.persistence.skip'] || art['@cds.persistence.exists'];
128
- this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path, {
129
- value: cardinality2str(member, false),
130
- '#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
131
- }, {
132
- std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
133
- comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
134
- });
135
- }
136
- }
137
- });
117
+ function checkManagedAssoc( member ) {
118
+ if (!member.target || isManagedComposition.bind(this)(member))
119
+ return;
138
120
 
139
- /**
140
- *
141
- * @param {CSN.Element} member The member
142
- * @returns {boolean} Whether the member is managed composition
143
- */
144
- function isManagedComposition( member ) {
145
- if (member.targetAspect)
146
- return true;
147
- if (!member.target)
148
- return false;
149
- const target = typeof member.target === 'object' ? member.target : this.csnUtils.getCsnDef(member.target);
150
- return target.kind !== 'entity';
121
+ // Implementation note: Imported services (i.e. external ones) may contain to-many associations
122
+ // with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
123
+ // foreign key array, we won't emit a warning to avoid spamming the user.
124
+ const max = member.cardinality?.max ? member.cardinality.max : 1;
125
+ if (max !== 1 && !member.on && (!member.keys || member.keys.length > 0)) {
126
+ const isNoDb = this.artifact['@cds.persistence.skip'] || this.artifact['@cds.persistence.exists'];
127
+ this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path, {
128
+ value: cardinality2str(member, false),
129
+ '#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
130
+ }, {
131
+ std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
132
+ comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
133
+ });
151
134
  }
152
135
  }
153
136
 
137
+ /**
138
+ *
139
+ * @param {CSN.Element} member The member
140
+ * @returns {boolean} Whether the member is managed composition
141
+ */
142
+ function isManagedComposition( member ) {
143
+ if (member.targetAspect)
144
+ return true;
145
+ if (!member.target)
146
+ return false;
147
+ const target = typeof member.target === 'object' ? member.target : this.csnUtils.getCsnDef(member.target);
148
+ return target.kind !== 'entity';
149
+ }
150
+
151
+
154
152
  /**
155
153
  * All DB & OData flat mode must reject recursive type usages in entities
156
154
  * 'items' break recursion as 'items' will turn into an NCLOB and the path
@@ -6,6 +6,8 @@
6
6
 
7
7
  const { csnRefs } = require('../model/csnRefs');
8
8
  const { setProp } = require('../base/model');
9
+ const { xprInAnnoProperties } = require('../compiler/builtins');
10
+
9
11
  /**
10
12
  * The following properties are attached as non-enumerable where appropriate:
11
13
  *
@@ -16,9 +18,10 @@ const { setProp } = require('../base/model');
16
18
  *- `$path` has the csnPath to reach that property.
17
19
  *
18
20
  * @param {CSN.Model} csn CSN to enrich in-place
21
+ * @param {enrichCsnOptions} [options={}]
19
22
  * @returns {{ csn: CSN.Model, cleanup: () => void }} CSN with all ref's pre-resolved
20
23
  */
21
- function enrichCsn( csn ) {
24
+ function enrichCsn( csn, options ) {
22
25
  const transformers = {
23
26
  elements: dictionary,
24
27
  definitions: dictionary,
@@ -31,8 +34,7 @@ function enrichCsn( csn ) {
31
34
  target,
32
35
  includes: simpleRef,
33
36
  columns,
34
- // Annotations are ignored.
35
- '@': () => { /* ignore annotations */ },
37
+ '@': annotation,
36
38
  };
37
39
  let cleanupCallbacks = [];
38
40
 
@@ -83,6 +85,35 @@ function enrichCsn( csn ) {
83
85
  csnPath.pop();
84
86
  }
85
87
 
88
+ /**
89
+ * Transformer for things that are annotations. When we have a "=" plus an expression of some sorts,
90
+ * we treat it like a "standard" thing.
91
+ *
92
+ * @param {object | Array} _parent the thing that has _prop
93
+ * @param {string|number} _prop the name of the current property or index
94
+ * @param {object} node The value of node[_prop]
95
+ */
96
+ function annotation( _parent, _prop, node ) {
97
+ if (options.processAnnotations) {
98
+ if (node?.['='] !== undefined && xprInAnnoProperties.some(xProp => node[xProp] !== undefined)) {
99
+ standard(_parent, _prop, node);
100
+ }
101
+ else if (node && typeof node === 'object') {
102
+ csnPath.push(_prop);
103
+
104
+ if (Array.isArray(node)) {
105
+ node.forEach( (n, i) => annotation( node, i, n ) );
106
+ }
107
+ else {
108
+ for (const name of Object.getOwnPropertyNames( node ))
109
+ annotation( node, name, node[name] );
110
+ }
111
+
112
+ csnPath.pop();
113
+ }
114
+ }
115
+ }
116
+
86
117
  // eslint-disable-next-line jsdoc/require-jsdoc
87
118
  function columns( parent, prop, node ) {
88
119
  // Establish the link relationships
@@ -178,3 +209,8 @@ function enrichCsn( csn ) {
178
209
  }
179
210
 
180
211
  module.exports = enrichCsn;
212
+
213
+ /**
214
+ * @typedef {object} enrichCsnOptions
215
+ * @property {boolean} [processAnnotations=false] Wether to process annotations and call custom transformers on them
216
+ */
@@ -22,6 +22,7 @@ const { checkActionOrFunction } = require('./actionsFunctions');
22
22
  const {
23
23
  checkCoreMediaTypeAllowance, checkAnalytics,
24
24
  checkAtSapAnnotations, checkReadOnlyAndInsertOnly,
25
+ checkTemporalAnnotationsAssignment,
25
26
  } = require('./annotationsOData');
26
27
  // both
27
28
  const { validateOnCondition, validateMixinOnCondition } = require('./onConditions');
@@ -120,12 +121,11 @@ const forOdataQueryValidators = [];
120
121
  const commonMemberValidators
121
122
  = [ validateOnCondition, validateForeignKeys,
122
123
  validateAssociationsInItems, checkForInvalidTarget,
123
- checkVirtualElement ];
124
+ checkVirtualElement, checkManagedAssoc ];
124
125
 
125
126
  // TODO: checkManagedAssoc is a forEachMemberRecursively!
126
127
  const commonArtifactValidators = [
127
128
  checkTypeDefinitionHasType,
128
- checkManagedAssoc,
129
129
  checkRecursiveTypeUsage,
130
130
  ];
131
131
  // TODO: Does it make sense to run the on-condition check as part of a CSN validator?
@@ -136,10 +136,10 @@ const commonQueryValidators = [ validateMixinOnCondition ];
136
136
  *
137
137
  * @param {CSN.Model} csn CSN to check
138
138
  * @param {object} that Will be provided to the validators via "this"
139
- * @param {object[]} [csnValidators=[]] Validations on whole CSN using applyTransformations
140
- * @param {Function[]} [memberValidators=[]] Validations on member-level
141
- * @param {Function[]} [artifactValidators=[]] Validations on artifact-level
142
- * @param {Function[]} [queryValidators=[]] Validations on query-level
139
+ * @param {object[]} [csnValidators] Validations on whole CSN using applyTransformations
140
+ * @param {Function[]} [memberValidators] Validations on member-level
141
+ * @param {Function[]} [artifactValidators] Validations on artifact-level
142
+ * @param {Function[]} [queryValidators] Validations on query-level
143
143
  * @param {object} iterateOptions can be used to skip certain kinds from being iterated e.g. 'action' and 'function' for hana
144
144
  * @returns {Function} Function taking no parameters, that cleans up the attached helpers
145
145
  */
@@ -149,7 +149,7 @@ function _validate( csn, that,
149
149
  artifactValidators = [],
150
150
  queryValidators = [],
151
151
  iterateOptions = {} ) {
152
- const { cleanup } = enrich(csn);
152
+ const { cleanup } = enrich(csn, { processAnnotations: that.options.tranformation === 'odata' });
153
153
 
154
154
  applyTransformations(csn, mergeCsnValidators(csnValidators, that), [], { drillRef: true });
155
155
 
@@ -267,6 +267,7 @@ function forOdata( csn, that ) {
267
267
  checkAtSapAnnotations.bind(that),
268
268
  ]);
269
269
  }
270
+ checkTemporalAnnotationsAssignment.bind(that)(artifact, artifactName);
270
271
  }
271
272
  ),
272
273
  // eslint-disable-next-line sonarjs/no-empty-collection