@sap/cds-compiler 5.0.6 → 5.1.2
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 +41 -0
- package/bin/cdsc.js +34 -8
- package/bin/cdshi.js +2 -1
- package/lib/api/main.js +10 -1
- package/lib/api/options.js +2 -3
- package/lib/base/message-registry.js +14 -0
- package/lib/base/messages.js +2 -1
- package/lib/base/meta.js +10 -0
- package/lib/base/optionProcessorHelper.js +11 -0
- package/lib/checks/dbFeatureFlags.js +5 -0
- package/lib/compiler/assert-consistency.js +1 -0
- package/lib/compiler/define.js +4 -2
- package/lib/compiler/extend.js +43 -7
- package/lib/compiler/index.js +4 -4
- package/lib/compiler/populate.js +58 -11
- package/lib/compiler/propagator.js +9 -4
- package/lib/compiler/resolve.js +115 -79
- package/lib/compiler/shared.js +2 -1
- package/lib/compiler/tweak-assocs.js +29 -5
- package/lib/edm/edm.js +8 -0
- package/lib/edm/edmPreprocessor.js +7 -3
- package/lib/gen/Dictionary.json +37 -0
- package/lib/json/to-csn.js +2 -0
- package/lib/main.js +2 -5
- package/lib/model/cloneCsn.js +1 -0
- package/lib/model/csnRefs.js +2 -1
- package/lib/model/csnUtils.js +0 -12
- package/lib/model/revealInternalProperties.js +0 -1
- package/lib/modelCompare/compare.js +12 -10
- package/lib/optionProcessor.js +2 -0
- package/lib/render/toCdl.js +8 -7
- package/lib/render/toHdbcds.js +1 -2
- package/lib/render/toSql.js +44 -8
- package/lib/transform/db/backlinks.js +20 -5
- package/lib/transform/db/killAnnotations.js +3 -0
- package/lib/transform/db/processSqlServices.js +63 -0
- package/lib/transform/draft/odata.js +6 -1
- package/lib/transform/forRelationalDB.js +9 -0
- package/lib/utils/file.js +77 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@
|
|
|
7
7
|
Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
|
|
8
8
|
The compiler behavior concerning `beta` features can change at any time without notice.
|
|
9
9
|
|
|
10
|
+
## Version 5.1.2 - 2024-08-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- compiler: In parseCdl mode, bound actions specifying the binding parameter with `$self` did not work.
|
|
15
|
+
|
|
16
|
+
## Version 5.1.0 - 2024-07-25
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- cdsc: Option `--stdin` was added to support input via standard input, e.g. `cat file.cds | cdsc --stdin`
|
|
21
|
+
- Allow to refer to draft state element `IsActiveEntity` via magic variable `$draft.IsActiveEntity` in annotation path expressions.
|
|
22
|
+
+ for.odata: During draft augmentation `$draft.IsActiveEntity` is rewritten to `$self.IsActiveEntity` for all draft enabled
|
|
23
|
+
entities (root and sub nodes but not for named types or entity parameters).
|
|
24
|
+
+ to.edm(x): (V4 only) Allow to refer to an entity element in a bound action via `$self` and not only via explicit binding parameter
|
|
25
|
+
in an annotation path expression. The API generator will prefix the path with the actual binding parameter name (explicit, annotation or
|
|
26
|
+
default).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Update OData vocabularies: 'Common', 'Core', 'HTML5', 'UI'.
|
|
31
|
+
- to.cdl|hdbcds|hdi|sql: Remove `generated by` comment.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- compiler: checks for associations now work for nested projections of the form `association.{ id }`
|
|
36
|
+
- to.edm(x): No `Nullable` attribute for `$ReturnType` of `Collection(<entity type>)` [OData V4 CSDL, section 12.8 Return Type](https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_ReturnType)
|
|
37
|
+
- to.sql|hdi|hdbcds: Detect and error on "cross-eyed" backlinks, where we cannot construct a valid on-condition.
|
|
38
|
+
- to.sql|hdi.migration: Correctly detect that a view was dropped - this was previously just silently ignored.
|
|
39
|
+
|
|
40
|
+
|
|
10
41
|
## Version 5.0.6 - 2024-07-10
|
|
11
42
|
|
|
12
43
|
### Fixed
|
|
@@ -73,6 +104,16 @@ This is a preview version for the major release and contains breaking changes. I
|
|
|
73
104
|
- API: Deprecated functions `preparedCsnToEdmx` and `preparedCsnToEdm` were removed.
|
|
74
105
|
Use `to.edm(x)` instead.
|
|
75
106
|
|
|
107
|
+
|
|
108
|
+
## Version 4.9.6 - 2024-07-15
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- for.seal: Don't generate DRAFT artifacts.
|
|
113
|
+
- for.odata: Propagate all `@odata { Type, MaxLength, Precision, Scale, SRID }` to generated foreign keys.
|
|
114
|
+
- to.edm(x): Respect `AppliesTo` specification in term definitions for actions and functions.
|
|
115
|
+
- to.sql: Conditions inside filters in combination with foreign key aliases were not properly translated in rare cases.
|
|
116
|
+
|
|
76
117
|
## Version 4.9.4 - 2024-05-21
|
|
77
118
|
|
|
78
119
|
### Fixed
|
package/bin/cdsc.js
CHANGED
|
@@ -35,6 +35,7 @@ const { addLocalizationViews } = require('../lib/transform/localized');
|
|
|
35
35
|
const { addTenantFields } = require('../lib/transform/addTenantFields');
|
|
36
36
|
const { availableBetaFlags } = require('../lib/base/model');
|
|
37
37
|
const { alterConstraintsWithCsn } = require('../lib/render/manageConstraints');
|
|
38
|
+
const { tmpFilePath, readStream } = require('../lib/utils/file');
|
|
38
39
|
|
|
39
40
|
// Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode),
|
|
40
41
|
// but that might truncate the output of stdout and stderr, both of which are async (or rather,
|
|
@@ -85,9 +86,12 @@ function remapCmdOptions( options, command ) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function cdsc_main() {
|
|
88
|
-
|
|
89
|
+
if (process.argv.some(arg => arg === '-i' || arg === '--stdin'))
|
|
90
|
+
optionProcessor.makePositionalArgumentsOptional();
|
|
89
91
|
|
|
92
|
+
// Parse the command line and translate it into options
|
|
90
93
|
const cmdLine = optionProcessor.processCmdLine(process.argv);
|
|
94
|
+
|
|
91
95
|
// Deal with '--version' explicitly
|
|
92
96
|
if (cmdLine.options.version) {
|
|
93
97
|
process.stdout.write(`${main.version()}\n`);
|
|
@@ -210,6 +214,9 @@ function cdsc_main() {
|
|
|
210
214
|
if (typeof cmdLine.options.moduleLookupDirectories === 'string')
|
|
211
215
|
cmdLine.options.moduleLookupDirectories = cmdLine.options.moduleLookupDirectories.split(',');
|
|
212
216
|
|
|
217
|
+
if (cmdLine.options.stdin)
|
|
218
|
+
cmdLine.options.fallbackParser ??= 'auto!';
|
|
219
|
+
|
|
213
220
|
parseSeverityOptions(cmdLine);
|
|
214
221
|
|
|
215
222
|
// Do the work for the selected command
|
|
@@ -230,12 +237,12 @@ function validateDirectBackendOption( command, options, args ) {
|
|
|
230
237
|
displayUsage(`Option '--direct-backend' can't be used with command '${command}'`,
|
|
231
238
|
optionProcessor.helpText, 2);
|
|
232
239
|
}
|
|
233
|
-
if (!args.files || args.files.length !== 1) {
|
|
234
|
-
displayUsage(`Option '--direct-backend' expects exactly one JSON file, but ${args.files
|
|
240
|
+
if (!options.stdin && (!args.files || args.files.length !== 1)) {
|
|
241
|
+
displayUsage(`Option '--direct-backend' expects exactly one JSON file, but ${args.files?.length || 'none'} given`,
|
|
235
242
|
optionProcessor.helpText, 2);
|
|
236
243
|
}
|
|
237
|
-
const filename = args.files[0];
|
|
238
|
-
if (!filename.endsWith('.csn') && !filename.endsWith('.json')) {
|
|
244
|
+
const filename = args.files?.[0];
|
|
245
|
+
if (filename && !filename.endsWith('.csn') && !filename.endsWith('.json')) {
|
|
239
246
|
displayUsage('Option \'--direct-backend\' expects a filename with a *.csn or *.json suffix',
|
|
240
247
|
optionProcessor.helpText, 2);
|
|
241
248
|
}
|
|
@@ -259,8 +266,21 @@ function displayUsage( error, helpText, code ) {
|
|
|
259
266
|
throw new ProcessExitError(code);
|
|
260
267
|
}
|
|
261
268
|
|
|
269
|
+
/**
|
|
270
|
+
* As the compiler is file-based and will always at least try to call `realpath()` on a file,
|
|
271
|
+
* we fake a "stdin" file for it.
|
|
272
|
+
*
|
|
273
|
+
* @returns {Promise<string>}
|
|
274
|
+
*/
|
|
275
|
+
async function createTemporaryFileFromStdin() {
|
|
276
|
+
const contents = await readStream(process.stdin);
|
|
277
|
+
const file = tmpFilePath('cds-compiler-stdin', 'cds');
|
|
278
|
+
await fs.promises.writeFile(file, contents);
|
|
279
|
+
return file;
|
|
280
|
+
}
|
|
281
|
+
|
|
262
282
|
// Executes a command line that has been translated to 'command' (what to do), 'options' (how) and 'args' (which files)
|
|
263
|
-
function executeCommandLine( command, options, args ) {
|
|
283
|
+
async function executeCommandLine( command, options, args ) {
|
|
264
284
|
const normalizeFilename = options.testMode && process.platform === 'win32';
|
|
265
285
|
const messageLevels = {
|
|
266
286
|
Error: 0, Warning: 1, Info: 2, Debug: 3,
|
|
@@ -304,17 +324,22 @@ function executeCommandLine( command, options, args ) {
|
|
|
304
324
|
}
|
|
305
325
|
|
|
306
326
|
options.messages = [];
|
|
327
|
+
args.files ??= [];
|
|
328
|
+
|
|
329
|
+
// Load a file from stdin if no explicit file is given and stdin is not a TTY.
|
|
330
|
+
if (options.stdin)
|
|
331
|
+
args.files.push(await createTemporaryFileFromStdin());
|
|
307
332
|
|
|
308
333
|
const fileCache = Object.create(null);
|
|
309
334
|
const compiled = options.directBackend
|
|
310
335
|
? util.promisify(fs.readFile)( args.files[0], 'utf-8' ).then(str => JSON.parse( str ))
|
|
311
336
|
: compiler.compileX( args.files, undefined, options, fileCache );
|
|
312
337
|
|
|
313
|
-
compiled.then( commands[command] )
|
|
338
|
+
await compiled.then( commands[command] )
|
|
314
339
|
.then( displayMessages, displayErrors )
|
|
315
340
|
.catch( catchErrors );
|
|
316
341
|
|
|
317
|
-
|
|
342
|
+
// below are only command implementations.
|
|
318
343
|
|
|
319
344
|
// Execute the command line option '--to-cdl' and display the results.
|
|
320
345
|
// Return the original model (for chaining)
|
|
@@ -724,3 +749,4 @@ function parseSeverityOptions({ options }) {
|
|
|
724
749
|
}
|
|
725
750
|
}
|
|
726
751
|
}
|
|
752
|
+
|
package/bin/cdshi.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
const compiler = require('../lib/compiler');
|
|
15
15
|
const fs = require('fs');
|
|
16
|
-
|
|
16
|
+
const stdinFd = 0;
|
|
17
|
+
fs.readFile( stdinFd, 'utf8', highlight );
|
|
17
18
|
|
|
18
19
|
const categoryChars = { // default: first char of category name
|
|
19
20
|
// first char lowercase = reference other than via extend/annotate:
|
package/lib/api/main.js
CHANGED
|
@@ -596,6 +596,15 @@ function sqlMigration( csn, options, messageFunctions, beforeImage ) {
|
|
|
596
596
|
if (constraintDeletions)
|
|
597
597
|
Object.values(constraintDeletions).forEach(constraint => dropSqls.push(constraint));
|
|
598
598
|
|
|
599
|
+
if (Object.keys(drops.final).length > 0) {
|
|
600
|
+
const order = sortViews({ sql: {}, csn: beforeImage });
|
|
601
|
+
|
|
602
|
+
for (const { name } of order) {
|
|
603
|
+
if (drops.final[name])
|
|
604
|
+
dropSqls.push(drops.final[name]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
599
608
|
// We need to drop the things without dependants first - so inversely sorted
|
|
600
609
|
dropSqls.reverse();
|
|
601
610
|
|
|
@@ -679,7 +688,7 @@ function createSqlDefinitions( hdbkinds, afterImage ) {
|
|
|
679
688
|
*/
|
|
680
689
|
function createSqlDeletions( deletions, beforeImage ) {
|
|
681
690
|
const result = [];
|
|
682
|
-
objectUtils.forEach(deletions, name => result.push({ name: getFileName(name, beforeImage), suffix: '.hdbtable' }));
|
|
691
|
+
objectUtils.forEach(deletions, name => result.push({ name: getFileName(name, beforeImage), suffix: beforeImage.definitions[name].query ? '.hdbview' : '.hdbtable' }));
|
|
683
692
|
return result;
|
|
684
693
|
}
|
|
685
694
|
/**
|
package/lib/api/options.js
CHANGED
|
@@ -29,7 +29,6 @@ const publicOptionsNewAPI = [
|
|
|
29
29
|
'magicVars', // deprecated, not removed in v3 as we have specific error messages for it
|
|
30
30
|
'variableReplacements',
|
|
31
31
|
'pre2134ReferentialConstraintNames',
|
|
32
|
-
'generatedByComment',
|
|
33
32
|
'betterSqliteSessionVariables',
|
|
34
33
|
'fewerLocalizedViews',
|
|
35
34
|
'withHanaAssociations',
|
|
@@ -156,7 +155,7 @@ module.exports = {
|
|
|
156
155
|
sql: (options) => {
|
|
157
156
|
const hardOptions = { src: 'sql', toSql: true, forHana: true };
|
|
158
157
|
const defaultOptions = {
|
|
159
|
-
sqlMapping: 'plain', sqlDialect: 'plain',
|
|
158
|
+
sqlMapping: 'plain', sqlDialect: 'plain', withHanaAssociations: true,
|
|
160
159
|
};
|
|
161
160
|
const processed = translateOptions(options, defaultOptions, hardOptions, undefined, [ 'sql-dialect-and-naming' ], 'to.sql');
|
|
162
161
|
|
|
@@ -166,7 +165,7 @@ module.exports = {
|
|
|
166
165
|
const hardOptions = { src: 'hdi', toSql: true, forHana: true };
|
|
167
166
|
// TODO: sqlDialect should be a hard option!
|
|
168
167
|
const defaultOptions = {
|
|
169
|
-
sqlMapping: 'plain', sqlDialect: 'hana',
|
|
168
|
+
sqlMapping: 'plain', sqlDialect: 'hana', withHanaAssociations: true,
|
|
170
169
|
};
|
|
171
170
|
return translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdi');
|
|
172
171
|
},
|
|
@@ -97,6 +97,7 @@ const centralMessages = {
|
|
|
97
97
|
'ref-expecting-foreign-key': { severity: 'Error' },
|
|
98
98
|
'ref-invalid-source': { severity: 'Error' },
|
|
99
99
|
'ref-invalid-target': { severity: 'Error' },
|
|
100
|
+
'ref-missing-self-counterpart': { severity: 'Error', configurableFor: true },
|
|
100
101
|
'ref-sloppy-target': { severity: 'Error', configurableFor: 'v4' },
|
|
101
102
|
|
|
102
103
|
'extend-repeated-intralayer': { severity: 'Warning' },
|
|
@@ -556,6 +557,10 @@ const centralMessageTexts = {
|
|
|
556
557
|
// Messages for erroneous references -----------------------------------------
|
|
557
558
|
// location at erroneous reference (if possible)
|
|
558
559
|
'ref-deprecated-orderby': 'Replace source element reference $(ID) by $(NEWCODE); auto-corrected',
|
|
560
|
+
'ref-missing-self-counterpart' : {
|
|
561
|
+
std: 'Expected to find a matching element in $self-comparison for foreign key $(PROP) of association $(NAME)',
|
|
562
|
+
unmanaged: 'Expected to find a matching element in $self-comparison for $(PROP) of association $(NAME)'
|
|
563
|
+
},
|
|
559
564
|
'ref-unexpected-self': {
|
|
560
565
|
std: 'Unexpected $(ID) reference; is valid only in ON-conditions of unmanaged associations',
|
|
561
566
|
on: 'Unexpected $(ID) reference; is valid only if compared to be equal to an association of the target side',
|
|
@@ -965,6 +970,15 @@ const centralMessageTexts = {
|
|
|
965
970
|
'managed': 'Ignoring managed association $(NAME) that is published in a UNION',
|
|
966
971
|
'std': 'Ignoring association $(NAME) that is published in a UNION'
|
|
967
972
|
},
|
|
973
|
+
'query-missing-element': {
|
|
974
|
+
std: 'Element $(ID) is missing in specified elements',
|
|
975
|
+
enum: 'Enum $(ID) is missing in specified enum values',
|
|
976
|
+
foreignKeys: 'Foreign key $(ID) is missing in specified foreign keys',
|
|
977
|
+
},
|
|
978
|
+
'query-unspecified-element': {
|
|
979
|
+
std: 'Element $(ID) does not result from the query',
|
|
980
|
+
foreignKeys: 'Foreign key $(ID) does not result from the query',
|
|
981
|
+
},
|
|
968
982
|
|
|
969
983
|
'ref-sloppy-target': 'An entity or an aspect (not type) is expected here',
|
|
970
984
|
|
package/lib/base/messages.js
CHANGED
|
@@ -13,6 +13,7 @@ const { analyseCsnPath, traverseQuery } = require('../model/csnRefs');
|
|
|
13
13
|
const { CompilerAssertion } = require('./error');
|
|
14
14
|
const { getArtifactName } = require('../compiler/base');
|
|
15
15
|
const { cdlNewLineRegEx } = require('../language/textUtils');
|
|
16
|
+
const meta = require('./meta');
|
|
16
17
|
|
|
17
18
|
const fs = require('fs');
|
|
18
19
|
const path = require('path');
|
|
@@ -117,7 +118,7 @@ class CompilationError extends Error {
|
|
|
117
118
|
// no proper message about _what_ the root cause of the exception was.
|
|
118
119
|
// To mitigate that, we serialize the first error in the message as well.
|
|
119
120
|
const firstError = messages.find( m => m.severity === 'Error' )?.toString() || '';
|
|
120
|
-
super( `CDS compilation failed\n${firstError}` );
|
|
121
|
+
super( `CDS compilation failed (@sap/cds-compiler v${ meta.version() })\n${firstError}` );
|
|
121
122
|
|
|
122
123
|
/** @since v4.0.0 */
|
|
123
124
|
this.code = 'ERR_CDS_COMPILATION_FAILURE';
|
package/lib/base/meta.js
ADDED
|
@@ -42,6 +42,7 @@ function createOptionProcessor() {
|
|
|
42
42
|
},
|
|
43
43
|
help,
|
|
44
44
|
processCmdLine,
|
|
45
|
+
makePositionalArgumentsOptional,
|
|
45
46
|
};
|
|
46
47
|
return optionProcessor;
|
|
47
48
|
|
|
@@ -291,6 +292,16 @@ function createOptionProcessor() {
|
|
|
291
292
|
};
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
function makePositionalArgumentsOptional() {
|
|
296
|
+
for (const arg of optionProcessor.positionalArguments || [])
|
|
297
|
+
arg.required = false;
|
|
298
|
+
|
|
299
|
+
for (const cmd in optionProcessor.commands) {
|
|
300
|
+
for (const arg of optionProcessor.commands[cmd].positionalArguments || [])
|
|
301
|
+
arg.required = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
294
305
|
// API: Let the option processor digest a command line 'argv'
|
|
295
306
|
// The expectation is to get a commandline like this:
|
|
296
307
|
// $ node cdsc.js -x 1 --foo toXyz -y --bar-wiz bla arg1 arg2
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { setProp } = require('../base/model');
|
|
4
4
|
const { featureFlags } = require('../transform/db/featureFlags');
|
|
5
|
+
const { isSqlService } = require('../transform/db/processSqlServices');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
*
|
|
@@ -25,4 +26,8 @@ module.exports = {
|
|
|
25
26
|
value: setFeatureFlag('$calculatedElements'),
|
|
26
27
|
expand: setFeatureFlag('$expandInline'),
|
|
27
28
|
inline: setFeatureFlag('$expandInline'),
|
|
29
|
+
kind: function setFeatureFlagForSqlService( artifact ) {
|
|
30
|
+
if (isSqlService(artifact))
|
|
31
|
+
setFeatureFlag( '$sqlService' ).call(this);
|
|
32
|
+
},
|
|
28
33
|
};
|
|
@@ -255,6 +255,7 @@ function assertConsistency( model, stage ) {
|
|
|
255
255
|
elements: { kind: true, inherits: 'definitions', also: [ 0 ] }, // 0 for cyclic expansions
|
|
256
256
|
// specified elements in query entities (TODO: introduce real "specified elements" instead):
|
|
257
257
|
elements$: { kind: true, enumerable: false, test: TODO },
|
|
258
|
+
foreignKeys$: { kind: true, enumerable: false, test: TODO },
|
|
258
259
|
enum$: { kind: true, enumerable: false, test: TODO },
|
|
259
260
|
typeProps$: { kind: true, enumerable: false, test: TODO },
|
|
260
261
|
// helper property for faster processing:
|
package/lib/compiler/define.js
CHANGED
|
@@ -216,7 +216,7 @@ function define( model ) {
|
|
|
216
216
|
|
|
217
217
|
const { $self } = model.definitions;
|
|
218
218
|
if ($self) {
|
|
219
|
-
message( 'name-deprecated-$self', [ $self.location, $self ], { name: '$self' },
|
|
219
|
+
message( 'name-deprecated-$self', [ $self.name.location, $self ], { name: '$self' },
|
|
220
220
|
'Do not use $(NAME) as name for an artifact definition' );
|
|
221
221
|
}
|
|
222
222
|
}
|
|
@@ -1203,7 +1203,9 @@ function define( model ) {
|
|
|
1203
1203
|
const type = first?.type || first?.items?.type; // this sequence = no derived type
|
|
1204
1204
|
const path = type?.path;
|
|
1205
1205
|
if (path?.length === 1 && path[0]?.id === '$self') { // TODO: no where: ?
|
|
1206
|
-
const
|
|
1206
|
+
const $self = main.$tableAliases?.$self ||
|
|
1207
|
+
main.kind === 'extend' && { name: { id: '$self' } };
|
|
1208
|
+
// remark: an extend has no "table alias" `$self` (relevant for parse-cdl)
|
|
1207
1209
|
setLink( type, '_artifact', $self );
|
|
1208
1210
|
setLink( path[0], '_artifact', $self );
|
|
1209
1211
|
}
|
package/lib/compiler/extend.js
CHANGED
|
@@ -27,6 +27,7 @@ const {
|
|
|
27
27
|
const layers = require('./moduleLayers');
|
|
28
28
|
const { CompilerAssertion } = require('../base/error');
|
|
29
29
|
const { Location } = require('../base/location');
|
|
30
|
+
const { typeParameters } = require('./builtins');
|
|
30
31
|
|
|
31
32
|
const $location = Symbol.for( 'cds.$location' );
|
|
32
33
|
|
|
@@ -69,6 +70,7 @@ function extend( model ) {
|
|
|
69
70
|
createRemainingAnnotateStatements,
|
|
70
71
|
extendArtifactBefore,
|
|
71
72
|
extendArtifactAfter,
|
|
73
|
+
extendForeignKeys,
|
|
72
74
|
applyIncludes, // TODO: re-check
|
|
73
75
|
} );
|
|
74
76
|
|
|
@@ -91,7 +93,8 @@ function extend( model ) {
|
|
|
91
93
|
//-----------------------------------------------------------------------------
|
|
92
94
|
// Extensions: general algorithm
|
|
93
95
|
//-----------------------------------------------------------------------------
|
|
94
|
-
// extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements
|
|
96
|
+
// extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements,
|
|
97
|
+
// extendForeignKeys
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
100
|
* Goes through all (applied) annotations in the given artifact and chooses one
|
|
@@ -179,22 +182,53 @@ function extend( model ) {
|
|
|
179
182
|
else {
|
|
180
183
|
let elementsProp = 'elements';
|
|
181
184
|
if (art.kind !== 'annotate')
|
|
182
|
-
elementsProp = art.enum && 'enum' || art.
|
|
183
|
-
|
|
185
|
+
elementsProp = art.enum && 'enum' || art.target && 'foreignKeys' || 'elements';
|
|
186
|
+
|
|
187
|
+
// keys are handled in tweak-assocs.js; don't push them down; see extendForeignKeys()
|
|
188
|
+
if (elementsProp !== 'foreignKeys')
|
|
189
|
+
moveDictExtensions( art, extensionsMap, elementsProp, 'elements' );
|
|
184
190
|
moveDictExtensions( art, extensionsMap, 'enum' );
|
|
185
191
|
}
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Apply foreign key extensions. Because foreign keys are handled late in the compiler
|
|
196
|
+
* (in tweak-assocs.js), we can't apply them in effectiveType(), yet.
|
|
197
|
+
* Instead, we postpone applying them until all foreign keys were generated.
|
|
198
|
+
*
|
|
199
|
+
* @param art
|
|
200
|
+
*/
|
|
201
|
+
function extendForeignKeys( art ) {
|
|
202
|
+
// See extendArtifactAfter() for targetAspect/items handling.
|
|
203
|
+
const sub = art.items || art.targetAspect?.elements && art.targetAspect;
|
|
204
|
+
if (!art._extensions || sub)
|
|
205
|
+
return;
|
|
206
|
+
|
|
207
|
+
// push down foreign keys
|
|
208
|
+
moveDictExtensions( art, art._extensions, 'foreignKeys', 'elements' );
|
|
209
|
+
if (!art.foreignKeys)
|
|
210
|
+
return;
|
|
211
|
+
|
|
212
|
+
forEachGeneric(art, 'foreignKeys', (key) => {
|
|
213
|
+
if (!key._effectiveType)
|
|
214
|
+
throw new CompilerAssertion('foreign key should have been processed');
|
|
215
|
+
extendArtifactBefore( key );
|
|
216
|
+
extendArtifactAfter( key );
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
188
220
|
/**
|
|
189
221
|
* Applying extensions is handled in extendArtifactAfter(). And only afterward,
|
|
190
222
|
* an effective sequence number is set. Meaning that if a sub-artifact already
|
|
191
223
|
* has a sequence number, then extensions would be lost.
|
|
224
|
+
*
|
|
225
|
+
* A special case are foreign keys, see extendForeignKeys().
|
|
192
226
|
*/
|
|
193
227
|
function ensureArtifactNotProcessed( art ) {
|
|
194
228
|
if (!model.options.testMode)
|
|
195
229
|
return;
|
|
196
230
|
|
|
197
|
-
if (art.$effectiveSeqNo !== 0 && art.$effectiveSeqNo !== undefined) {
|
|
231
|
+
if (art.kind !== 'key' && art.$effectiveSeqNo !== 0 && art.$effectiveSeqNo !== undefined) {
|
|
198
232
|
// if the artifact already has a sequence number, then
|
|
199
233
|
// extendArtifactAfter() was already called -> annotations would be lost.
|
|
200
234
|
throw new CompilerAssertion('artifact already processed; extensions would be lost');
|
|
@@ -357,7 +391,7 @@ function extend( model ) {
|
|
|
357
391
|
|
|
358
392
|
function extensionOverwrites( ext, prop ) {
|
|
359
393
|
return (prop.charAt(0) !== '@')
|
|
360
|
-
?
|
|
394
|
+
? (prop === 'doc' || typeParameters.list.includes(prop))
|
|
361
395
|
: !annotationHasEllipsis( ext[prop] );
|
|
362
396
|
}
|
|
363
397
|
|
|
@@ -437,7 +471,7 @@ function extend( model ) {
|
|
|
437
471
|
query.columns.push( ...ext.columns );
|
|
438
472
|
initSelectItems( query, ext.columns, query, true );
|
|
439
473
|
}
|
|
440
|
-
else if (
|
|
474
|
+
else if (typeParameters.list.includes( prop )) {
|
|
441
475
|
const typeExts = art.$typeExts || (art.$typeExts = {});
|
|
442
476
|
typeExts[prop] = ext;
|
|
443
477
|
}
|
|
@@ -647,6 +681,7 @@ function extend( model ) {
|
|
|
647
681
|
const extensions = extensionsMap[extProp];
|
|
648
682
|
if (!extensions)
|
|
649
683
|
return;
|
|
684
|
+
|
|
650
685
|
const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate
|
|
651
686
|
|
|
652
687
|
for (const ext of extensions) {
|
|
@@ -762,10 +797,11 @@ function extend( model ) {
|
|
|
762
797
|
const dict = parent[prop];
|
|
763
798
|
if (!dict) {
|
|
764
799
|
// TODO: check - for each name? - better locations
|
|
765
|
-
const location = ext._parent[prop][$location] || ext.name.location;
|
|
800
|
+
const location = ext._parent[prop]?.[$location] || ext.name.location;
|
|
766
801
|
// Remark: no `elements` dict location with `annotate Main:elem`
|
|
767
802
|
switch (prop) {
|
|
768
803
|
// TODO: change texts, somehow similar to checkDefinitions() ?
|
|
804
|
+
case 'foreignKeys':
|
|
769
805
|
case 'elements':
|
|
770
806
|
case 'enum': // TODO: extra?
|
|
771
807
|
warning( 'anno-unexpected-elements', [ location, ext._parent ],
|
package/lib/compiler/index.js
CHANGED
|
@@ -34,7 +34,7 @@ const { Location, emptyWeakLocation } = require('../base/location');
|
|
|
34
34
|
const { createMessageFunctions, deduplicateMessages } = require('../base/messages');
|
|
35
35
|
const { checkRemovedDeprecatedFlags } = require('../base/model');
|
|
36
36
|
const { promiseAllDoNotRejectImmediately } = require('../base/node-helpers');
|
|
37
|
-
const { cdsFs } = require('../utils/file');
|
|
37
|
+
const { cdsFs, fileExtension } = require('../utils/file');
|
|
38
38
|
|
|
39
39
|
const fs = require('fs');
|
|
40
40
|
const path = require('path');
|
|
@@ -82,7 +82,7 @@ class ArgumentError extends Error {
|
|
|
82
82
|
function parseX( source, filename, options = {}, messageFunctions = null ) {
|
|
83
83
|
if (!messageFunctions)
|
|
84
84
|
messageFunctions = createMessageFunctions( options, 'parse' );
|
|
85
|
-
const ext =
|
|
85
|
+
const ext = fileExtension( filename );
|
|
86
86
|
const parser = parserForFile( source, ext, options );
|
|
87
87
|
if (parser)
|
|
88
88
|
return parser( source, filename, options, messageFunctions );
|
|
@@ -166,10 +166,10 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
|
|
|
166
166
|
.then( () => promiseAllDoNotRejectImmediately( input.files.map( readAndParse ) ) )
|
|
167
167
|
.then( testInvocation, (reason) => {
|
|
168
168
|
// do not reject with PromiseAllError, use InvocationError:
|
|
169
|
-
const errs = reason.valuesOrErrors
|
|
169
|
+
const errs = reason.valuesOrErrors?.filter( e => e instanceof Error ) || [ reason ];
|
|
170
170
|
// internal error if no file IO error (has property `path`)
|
|
171
171
|
return Promise.reject( errs.find( e => !e.path ) ||
|
|
172
|
-
new InvocationError( [ ...input
|
|
172
|
+
new InvocationError( [ ...(input?.repeated || []), ...errs ]) );
|
|
173
173
|
} );
|
|
174
174
|
|
|
175
175
|
if (!options.parseOnly && !options.parseCdl)
|
package/lib/compiler/populate.js
CHANGED
|
@@ -83,6 +83,7 @@ function populate( model ) {
|
|
|
83
83
|
effectiveType,
|
|
84
84
|
getOrigin,
|
|
85
85
|
getInheritedProp,
|
|
86
|
+
mergeSpecifiedForeignKeys,
|
|
86
87
|
} );
|
|
87
88
|
// let depth = 100;
|
|
88
89
|
|
|
@@ -115,7 +116,10 @@ function populate( model ) {
|
|
|
115
116
|
|
|
116
117
|
/** Make sure that effectiveType() is called on all members and items */
|
|
117
118
|
function traverseElementEnvironments( art ) {
|
|
118
|
-
//
|
|
119
|
+
// We leave out foreign keys (as they are traversed via forEachMember).
|
|
120
|
+
// Keys are handled in tweak-assocs.js
|
|
121
|
+
if (art.kind === 'key')
|
|
122
|
+
return;
|
|
119
123
|
let type = effectiveType( art );
|
|
120
124
|
while (type?.items)
|
|
121
125
|
type = effectiveType( type.items );
|
|
@@ -529,9 +533,6 @@ function populate( model ) {
|
|
|
529
533
|
if (!selem) {
|
|
530
534
|
info( 'query-missing-element', [ ielem.name.location, art ], {
|
|
531
535
|
'#': ielem.kind === 'enum' ? 'enum' : 'std', id,
|
|
532
|
-
}, {
|
|
533
|
-
std: 'Element $(ID) is missing in specified elements',
|
|
534
|
-
enum: 'Enum $(ID) is missing in specified enum values',
|
|
535
536
|
} );
|
|
536
537
|
}
|
|
537
538
|
else {
|
|
@@ -560,6 +561,8 @@ function populate( model ) {
|
|
|
560
561
|
setLink( ielem, 'elements$', selem.elements );
|
|
561
562
|
if (selem.enum)
|
|
562
563
|
setLink( ielem, 'enum$', selem.enum );
|
|
564
|
+
if (selem.foreignKeys)
|
|
565
|
+
setLink( ielem, 'foreignKeys$', selem.foreignKeys );
|
|
563
566
|
}
|
|
564
567
|
}
|
|
565
568
|
|
|
@@ -574,8 +577,57 @@ function populate( model ) {
|
|
|
574
577
|
specifiedElement.$isSpecifiedElement = true;
|
|
575
578
|
if (!specifiedElement.$replacement) {
|
|
576
579
|
const loc = [ specifiedElement.name.location, specifiedElement ];
|
|
577
|
-
error( 'query-unspecified-element', loc, { id }
|
|
578
|
-
|
|
580
|
+
error( 'query-unspecified-element', loc, { id } );
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Merge _specified_ foreign keys with _inferred_ foreign keys in the given view/element,
|
|
587
|
+
* where specified elements can appear through CSN.
|
|
588
|
+
*
|
|
589
|
+
* We only copy annotations.
|
|
590
|
+
*
|
|
591
|
+
* This is important to ensure re-compilability.
|
|
592
|
+
*
|
|
593
|
+
* TODO: make this part of a revamped on-demand 'extend' functionality.
|
|
594
|
+
*
|
|
595
|
+
* @param art
|
|
596
|
+
*/
|
|
597
|
+
function mergeSpecifiedForeignKeys( art ) {
|
|
598
|
+
if (!art.foreignKeys)
|
|
599
|
+
return; // TODO: Warn if there are no foreign keys?
|
|
600
|
+
|
|
601
|
+
let wasAnnotated = false;
|
|
602
|
+
|
|
603
|
+
for (const id in art.foreignKeys) {
|
|
604
|
+
const ielem = art.foreignKeys[id]; // inferred element
|
|
605
|
+
const selem = art.foreignKeys$[id]; // specified element
|
|
606
|
+
if (!selem) {
|
|
607
|
+
info( 'query-missing-element', [ ielem.name.location, art ], { '#': 'foreignKeys', id } );
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
for (const prop in selem) {
|
|
611
|
+
// just annotation assignments and doc comments for foreign keys
|
|
612
|
+
if (prop.charAt(0) === '@' || prop === 'doc') {
|
|
613
|
+
ielem[prop] = selem[prop];
|
|
614
|
+
// required for gensrc mode of to-csn.js, otherwise the annotation
|
|
615
|
+
// may be lost during recompilation.
|
|
616
|
+
ielem[prop].$priority = 'annotate';
|
|
617
|
+
wasAnnotated = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
selem.$replacement = true;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (wasAnnotated)
|
|
624
|
+
setExpandStatusAnnotate( art, 'annotate' );
|
|
625
|
+
|
|
626
|
+
for (const id in art.foreignKeys$) {
|
|
627
|
+
const specifiedElement = art.foreignKeys$[id];
|
|
628
|
+
if (!specifiedElement.$replacement) {
|
|
629
|
+
const loc = [ specifiedElement.name.location, specifiedElement ];
|
|
630
|
+
error( 'query-unspecified-element', loc, { '#': 'foreignKeys', id } );
|
|
579
631
|
}
|
|
580
632
|
}
|
|
581
633
|
}
|
|
@@ -816,15 +868,10 @@ function populate( model ) {
|
|
|
816
868
|
const excludingDict = (colParent || query).excludingDict || Object.create( null );
|
|
817
869
|
|
|
818
870
|
const envParent = wildcard._pathHead; // TODO: rename _pathHead to _columnParent
|
|
819
|
-
// console.log('S1:',location.line,location.col,
|
|
820
|
-
// envParent&&!!envParent._origin&&envParent._origin.name)
|
|
821
871
|
const env = wildcardColumnEnv( wildcard, query );
|
|
822
872
|
if (!env)
|
|
823
873
|
return;
|
|
824
874
|
|
|
825
|
-
// if (envParent) console.log('S2:',location.line,location.col,
|
|
826
|
-
// envParent?.name,envParent?._origin?.name,
|
|
827
|
-
// Object.keys(env),Object.keys(elements))
|
|
828
875
|
for (const name in env) {
|
|
829
876
|
const navElem = env[name];
|
|
830
877
|
// TODO: remove all access to masked (use 'grep')
|
|
@@ -49,12 +49,17 @@ function propagate( model ) {
|
|
|
49
49
|
targetAspect,
|
|
50
50
|
cardinality: notWithExpand,
|
|
51
51
|
on: notWithExpand,
|
|
52
|
-
|
|
52
|
+
// "expensive" includes "notWithExpand"
|
|
53
|
+
// required for places where we don't handle associations, such as in parameters;
|
|
54
|
+
// otherwise already expanded and rewritten.
|
|
55
|
+
foreignKeys: expensive,
|
|
53
56
|
items,
|
|
57
|
+
// required for propagation in targetAspect; otherwise already expanded
|
|
54
58
|
elements: expensive,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
// already expanded if necessary
|
|
60
|
+
// enum: expensive,
|
|
61
|
+
// params: expensive, // actually only with parent action
|
|
62
|
+
// returns,
|
|
58
63
|
$enclosed: annotation,
|
|
59
64
|
};
|
|
60
65
|
const ruleToFunction = {
|