@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.
- package/CHANGELOG.md +88 -0
- package/bin/cdsc.js +18 -11
- package/bin/cdsv2m.js +7 -5
- package/doc/CHANGELOG_BETA.md +22 -0
- package/lib/api/main.js +306 -144
- package/lib/api/options.js +18 -6
- package/lib/api/validate.js +1 -1
- package/lib/base/message-registry.js +45 -10
- package/lib/base/messages.js +33 -16
- package/lib/base/model.js +4 -0
- package/lib/base/optionProcessorHelper.js +45 -176
- package/lib/checks/annotationsOData.js +49 -0
- package/lib/checks/elements.js +32 -34
- package/lib/checks/enricher.js +39 -3
- package/lib/checks/validator.js +8 -7
- package/lib/compiler/assert-consistency.js +40 -17
- package/lib/compiler/builtins.js +30 -53
- package/lib/compiler/checks.js +46 -14
- package/lib/compiler/cycle-detector.js +1 -4
- package/lib/compiler/define.js +35 -10
- package/lib/compiler/extend.js +21 -7
- package/lib/compiler/generate.js +3 -0
- package/lib/compiler/populate.js +5 -1
- package/lib/compiler/propagator.js +46 -9
- package/lib/compiler/resolve.js +94 -35
- package/lib/compiler/shared.js +60 -33
- package/lib/compiler/tweak-assocs.js +188 -92
- package/lib/compiler/utils.js +11 -1
- package/lib/edm/annotations/edmJson.js +41 -66
- package/lib/edm/annotations/genericTranslation.js +27 -9
- package/lib/edm/annotations/preprocessAnnotations.js +2 -3
- package/lib/edm/csn2edm.js +28 -11
- package/lib/edm/edmInboundChecks.js +58 -15
- package/lib/edm/edmPreprocessor.js +12 -16
- package/lib/edm/edmUtils.js +5 -2
- package/lib/gen/Dictionary.json +10 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +15 -2
- package/lib/gen/language.tokens +1 -0
- package/lib/gen/languageParser.js +6557 -5618
- package/lib/json/from-csn.js +4 -5
- package/lib/json/to-csn.js +29 -4
- package/lib/language/antlrParser.js +19 -1
- package/lib/language/errorStrategy.js +28 -7
- package/lib/language/genericAntlrParser.js +118 -24
- package/lib/language/textUtils.js +16 -0
- package/lib/main.d.ts +28 -3
- package/lib/main.js +3 -0
- package/lib/model/csnRefs.js +4 -1
- package/lib/model/csnUtils.js +20 -14
- package/lib/model/revealInternalProperties.js +5 -2
- package/lib/optionProcessor.js +23 -22
- package/lib/render/manageConstraints.js +13 -29
- package/lib/render/toCdl.js +47 -26
- package/lib/render/toHdbcds.js +63 -42
- package/lib/render/toRename.js +6 -10
- package/lib/render/toSql.js +71 -117
- package/lib/render/utils/common.js +41 -6
- package/lib/transform/.eslintrc.json +9 -1
- package/lib/transform/addTenantFields.js +228 -0
- package/lib/transform/db/applyTransformations.js +57 -4
- package/lib/transform/db/assertUnique.js +4 -4
- package/lib/transform/db/backlinks.js +13 -1
- package/lib/transform/db/cdsPersistence.js +1 -1
- package/lib/transform/db/expansion.js +24 -3
- package/lib/transform/db/flattening.js +70 -71
- package/lib/transform/db/killAnnotations.js +37 -0
- package/lib/transform/db/rewriteCalculatedElements.js +46 -6
- package/lib/transform/db/temporal.js +1 -1
- package/lib/transform/draft/db.js +2 -16
- package/lib/transform/draft/odata.js +3 -3
- package/lib/transform/effective/associations.js +3 -5
- package/lib/transform/effective/main.js +6 -9
- package/lib/transform/forOdata.js +26 -55
- package/lib/transform/forRelationalDB.js +38 -18
- package/lib/transform/odata/toFinalBaseType.js +3 -3
- package/lib/transform/odata/typesExposure.js +14 -5
- package/lib/transform/transformUtils.js +47 -34
- package/lib/transform/translateAssocsToJoins.js +45 -11
- package/lib/transform/universalCsn/coreComputed.js +1 -1
- package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
- 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
|
|
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 {
|
|
56
|
-
* @param {
|
|
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,
|
|
59
|
-
return _addOption(optionProcessor, optString,
|
|
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
|
|
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,
|
|
103
|
-
return _addOption(cmd, optString,
|
|
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('<', '')
|
|
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,
|
|
161
|
-
const cliOpt = _parseOptionString(optString,
|
|
160
|
+
function _addOption( cmd, optString, options ) {
|
|
161
|
+
const cliOpt = _parseOptionString(optString, options);
|
|
162
162
|
Object.assign(cliOpt, options);
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
206
|
-
if (!
|
|
182
|
+
function _addOptionName( cmd, name, opt ) {
|
|
183
|
+
if (!name)
|
|
207
184
|
return;
|
|
208
|
-
if (cmd.options[
|
|
209
|
-
throw new Error(`Duplicate assignment for
|
|
185
|
+
if (cmd.options[name]) {
|
|
186
|
+
throw new Error(`Duplicate assignment for option ${ name }`);
|
|
210
187
|
}
|
|
211
|
-
else if (optionProcessor.options[
|
|
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:
|
|
215
|
-
description: `Command '${ cmd.longName }' has option clash with general options for: ${
|
|
191
|
+
option: name,
|
|
192
|
+
description: `Command '${ cmd.longName }' has option clash with general options for: ${ name }`,
|
|
216
193
|
});
|
|
217
194
|
}
|
|
218
|
-
cmd.options[
|
|
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
|
-
//
|
|
231
|
+
// optionName: 'fooBar', // or options.optionName if provided
|
|
255
232
|
// param: '<p>'
|
|
256
|
-
//
|
|
233
|
+
// valid
|
|
257
234
|
// }
|
|
258
|
-
function _parseOptionString( optString,
|
|
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 &&
|
|
274
|
+
if (!param && options?.valid)
|
|
298
275
|
throw new Error(`Option description has valid values but no param: ${ optString }`);
|
|
299
276
|
|
|
300
|
-
if (
|
|
301
|
-
|
|
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
|
-
|
|
287
|
+
optionName: options?.option ?? camelifyLongOption(longName),
|
|
311
288
|
param,
|
|
312
|
-
|
|
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.
|
|
500
|
+
result.options[currentCommand][currentOption.optionName] = val;
|
|
524
501
|
}
|
|
525
502
|
else {
|
|
526
|
-
result.options[currentOption.
|
|
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.
|
|
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.
|
|
563
|
+
if (!opt.valid?.length)
|
|
677
564
|
return true;
|
|
678
565
|
if (opt.ignoreCase)
|
|
679
|
-
return opt.
|
|
680
|
-
return opt.
|
|
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
|
};
|
package/lib/checks/elements.js
CHANGED
|
@@ -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}
|
|
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(
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
package/lib/checks/enricher.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
*/
|
package/lib/checks/validator.js
CHANGED
|
@@ -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
|
|
140
|
-
* @param {Function[]} [memberValidators
|
|
141
|
-
* @param {Function[]} [artifactValidators
|
|
142
|
-
* @param {Function[]} [queryValidators
|
|
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
|