@sap/cds-compiler 4.3.2 → 4.4.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 +39 -0
- package/lib/api/main.js +14 -24
- package/lib/api/options.js +1 -0
- package/lib/api/trace.js +38 -0
- package/lib/base/location.js +46 -1
- package/lib/base/message-registry.js +68 -16
- package/lib/base/messages.js +8 -3
- package/lib/checks/.eslintrc.json +1 -0
- package/lib/checks/actionsFunctions.js +1 -1
- package/lib/checks/annotationsOData.js +2 -2
- package/lib/checks/selectItems.js +4 -1
- package/lib/compiler/assert-consistency.js +3 -2
- package/lib/compiler/base.js +1 -1
- package/lib/compiler/builtins.js +25 -1
- package/lib/compiler/checks.js +6 -5
- package/lib/compiler/define.js +12 -10
- package/lib/compiler/extend.js +44 -23
- package/lib/compiler/finalize-parse-cdl.js +1 -1
- package/lib/compiler/generate.js +70 -53
- package/lib/compiler/kick-start.js +7 -5
- package/lib/compiler/populate.js +31 -22
- package/lib/compiler/propagator.js +6 -2
- package/lib/compiler/resolve.js +52 -17
- package/lib/compiler/shared.js +85 -39
- package/lib/compiler/tweak-assocs.js +64 -23
- package/lib/compiler/utils.js +40 -23
- package/lib/edm/.eslintrc.json +2 -0
- package/lib/edm/EdmPrimitiveTypeDefinitions.js +260 -0
- package/lib/edm/annotations/edmJson.js +994 -0
- package/lib/edm/annotations/genericTranslation.js +82 -423
- package/lib/edm/annotations/vocabularyDefinitions.js +160 -0
- package/lib/edm/csn2edm.js +12 -5
- package/lib/edm/edm.js +14 -73
- package/lib/edm/edmPreprocessor.js +6 -0
- package/lib/gen/Dictionary.json +187 -16
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageLexer.interp +1 -1
- package/lib/gen/languageLexer.js +1129 -671
- package/lib/gen/languageParser.js +4285 -4283
- package/lib/json/from-csn.js +13 -18
- package/lib/json/to-csn.js +11 -6
- package/lib/language/antlrParser.js +0 -1
- package/lib/language/docCommentParser.js +1 -1
- package/lib/language/errorStrategy.js +95 -30
- package/lib/language/genericAntlrParser.js +21 -1
- package/lib/main.js +13 -3
- package/lib/model/csnRefs.js +42 -8
- package/lib/model/csnUtils.js +14 -2
- package/lib/model/enrichCsn.js +33 -5
- package/lib/model/revealInternalProperties.js +5 -0
- package/lib/modelCompare/compare.js +76 -14
- package/lib/modelCompare/utils/filter.js +19 -12
- package/lib/optionProcessor.js +2 -0
- package/lib/render/.eslintrc.json +1 -1
- package/lib/render/manageConstraints.js +1 -0
- package/lib/render/toHdbcds.js +3 -0
- package/lib/render/toRename.js +3 -1
- package/lib/render/toSql.js +46 -92
- package/lib/render/utils/common.js +76 -0
- package/lib/render/utils/delta.js +17 -3
- package/lib/sql-identifier.js +1 -1
- package/lib/transform/db/.eslintrc.json +1 -0
- package/lib/transform/db/applyTransformations.js +30 -4
- package/lib/transform/db/associations.js +22 -10
- package/lib/transform/db/backlinks.js +6 -2
- package/lib/transform/db/expansion.js +2 -2
- package/lib/transform/db/transformExists.js +13 -39
- package/lib/transform/draft/db.js +14 -3
- package/lib/transform/draft/odata.js +5 -18
- package/lib/transform/effective/associations.js +46 -15
- package/lib/transform/effective/main.js +7 -2
- package/lib/transform/effective/misc.js +43 -24
- package/lib/transform/effective/queries.js +20 -22
- package/lib/transform/effective/types.js +6 -2
- package/lib/transform/forOdata.js +10 -3
- package/lib/transform/localized.js +1 -1
- package/lib/transform/parseExpr.js +73 -21
- package/lib/transform/translateAssocsToJoins.js +22 -15
- package/lib/utils/term.js +2 -2
- package/package.json +2 -1
|
@@ -333,6 +333,81 @@ function getDefaultTypeLengths( sqlDialect ) {
|
|
|
333
333
|
return { ...sqlDefaultLengths.default, ...sqlDefaultLengths[sqlDialect] };
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Maps $-variables per SQL dialect to a renderable expression.
|
|
338
|
+
* Callers can use `.fallback` in case the wanted dialect is not found.
|
|
339
|
+
*
|
|
340
|
+
* IMPORTANT: There is no sqlDialect better-sqlite. This "fake" dialect is
|
|
341
|
+
* set in variableForDialect() below.
|
|
342
|
+
*
|
|
343
|
+
* @type {object}
|
|
344
|
+
*/
|
|
345
|
+
const variablesToSql = {
|
|
346
|
+
fallback: {
|
|
347
|
+
// no fallback for $user.id and $user.tenant -> warning in call-site
|
|
348
|
+
'$user.locale': '\'en\'',
|
|
349
|
+
// $at.* are handled in all dialects -> there is no need for a fallback
|
|
350
|
+
},
|
|
351
|
+
hana: {
|
|
352
|
+
'$user.id': "SESSION_CONTEXT('APPLICATIONUSER')",
|
|
353
|
+
'$user.locale': "SESSION_CONTEXT('LOCALE')",
|
|
354
|
+
'$user.tenant': "SESSION_CONTEXT('TENANT')",
|
|
355
|
+
'$at.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))",
|
|
356
|
+
'$at.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))",
|
|
357
|
+
},
|
|
358
|
+
postgres: {
|
|
359
|
+
'$user.id': "current_setting('cap.applicationuser')",
|
|
360
|
+
'$user.locale': "current_setting('cap.locale')",
|
|
361
|
+
'$user.tenant': "current_setting('cap.tenant')",
|
|
362
|
+
'$at.from': "current_setting('cap.valid_from')::timestamp",
|
|
363
|
+
'$at.to': "current_setting('cap.valid_to')::timestamp",
|
|
364
|
+
},
|
|
365
|
+
'better-sqlite': {
|
|
366
|
+
'$user.id': "session_context( '$user.id' )",
|
|
367
|
+
'$user.locale': "session_context( '$user.locale' )",
|
|
368
|
+
'$user.tenant': "session_context( '$user.tenant' )",
|
|
369
|
+
'$at.from': "session_context( '$valid.from' )",
|
|
370
|
+
'$at.to': "session_context( '$valid.to' )",
|
|
371
|
+
},
|
|
372
|
+
sqlite: {
|
|
373
|
+
// For sqlite, we render the string-format-time (strftime) function.
|
|
374
|
+
// Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
|
|
375
|
+
// the format for timestamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
|
|
376
|
+
// --> Therefore the comparison in the temporal where clause doesn't work properly.
|
|
377
|
+
'$at.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')",
|
|
378
|
+
// + 1ms compared to $at.from
|
|
379
|
+
'$at.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')",
|
|
380
|
+
},
|
|
381
|
+
plain: {
|
|
382
|
+
'$at.from': 'current_timestamp',
|
|
383
|
+
'$at.to': 'current_timestamp',
|
|
384
|
+
},
|
|
385
|
+
h2: {
|
|
386
|
+
'$user.id': '@applicationuser',
|
|
387
|
+
'$user.locale': '@locale',
|
|
388
|
+
'$user.tenant': '@tenant',
|
|
389
|
+
'$at.from': '@valid_from',
|
|
390
|
+
'$at.to': '@valid_to',
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get a renderable string for given variable for the given options.sqlDialect.
|
|
396
|
+
* Note that this function does not handle `variableReplacements`. Callers should
|
|
397
|
+
* first check if the user has specified them and use them instead.
|
|
398
|
+
*
|
|
399
|
+
* @param {SqlOptions} options Used for `sqlDialect` and better-sqlite option.
|
|
400
|
+
* @param {string} variable Variable to render, e.g. `$user.id`.
|
|
401
|
+
* @return {string|null} `null` if the variable could not be found for the given dialect and in the fallback values.
|
|
402
|
+
*/
|
|
403
|
+
function variableForDialect( options, variable ) {
|
|
404
|
+
const dialect = options.sqlDialect === 'sqlite' && options.betterSqliteSessionVariables
|
|
405
|
+
? 'better-sqlite'
|
|
406
|
+
: options.sqlDialect;
|
|
407
|
+
return variablesToSql[dialect]?.[variable] || variablesToSql.fallback[variable] || null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
336
411
|
/**
|
|
337
412
|
* Get the element matching the column
|
|
338
413
|
*
|
|
@@ -614,6 +689,7 @@ module.exports = {
|
|
|
614
689
|
addContextMarkers,
|
|
615
690
|
cdsToSqlTypes,
|
|
616
691
|
cdsToHdbcdsTypes,
|
|
692
|
+
variableForDialect,
|
|
617
693
|
hasHanaComment,
|
|
618
694
|
getHanaComment,
|
|
619
695
|
findElement,
|
|
@@ -71,7 +71,7 @@ class DeltaRenderer {
|
|
|
71
71
|
/**
|
|
72
72
|
* Render column modifications as SQL.
|
|
73
73
|
*/
|
|
74
|
-
alterColumns(artifactName, columnName, delta, definitionsStr) {
|
|
74
|
+
alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
|
|
75
75
|
return [ `ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER (${definitionsStr});` ];
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -177,14 +177,28 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
177
177
|
/**
|
|
178
178
|
* Render column modifications as Postgres SQL - no ( ), special NOT NULL.
|
|
179
179
|
*/
|
|
180
|
-
alterColumns(artifactName, columnName, delta, definitionsStr) {
|
|
180
|
+
alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) {
|
|
181
181
|
const sqls = [];
|
|
182
182
|
if (delta.new.notNull === true || delta.new.key === true)
|
|
183
183
|
definitionsStr = definitionsStr.replace(' NOT NULL', ''); // TODO: Is this robust enough?
|
|
184
184
|
else if (delta.new.notNull === false || delta.new.$notNull === false)
|
|
185
185
|
definitionsStr = definitionsStr.replace(' NULL', ''); // TODO: Is this robust enough?
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
if (delta.old.default && !delta.old.value) // Drop old default if any exists
|
|
188
|
+
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} DROP DEFAULT;`);
|
|
189
|
+
|
|
190
|
+
if (delta.new.default && !delta.new.value ) { // Alter column with default
|
|
191
|
+
const df = delta.new.default;
|
|
192
|
+
delete delta.new.default;
|
|
193
|
+
const eltStrNoDefault = this.scopedFunctions.renderElement(eltName, delta.new, null, null, env);
|
|
194
|
+
delta.new.default = df;
|
|
195
|
+
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${eltStrNoDefault};`);
|
|
196
|
+
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} SET DEFAULT ${this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default'))};`);
|
|
197
|
+
}
|
|
198
|
+
else { // Alter column without default
|
|
199
|
+
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${definitionsStr};`);
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
if (delta.new.notNull && !delta.old.notNull)
|
|
189
203
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${columnName} SET NOT NULL;`);
|
|
190
204
|
else if (delta.old.notNull && !delta.new.notNull)
|
package/lib/sql-identifier.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Using the provided function smartId() instead of an identity function avoids
|
|
20
20
|
// this situation: it constructs delimited identifiers for the reserved names.
|
|
21
|
-
// Other names are returned directly to
|
|
21
|
+
// Other names are returned directly to avoid that people think that they
|
|
22
22
|
// had to use all-upper names in CDS.
|
|
23
23
|
|
|
24
24
|
// Please note that `.` to `_` replacements (and similar replacements for the
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"plugins": ["sonarjs", "jsdoc"],
|
|
4
4
|
"extends": ["plugin:jsdoc/recommended", "../../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended"],
|
|
5
5
|
"rules": {
|
|
6
|
+
"cds-compiler/message-no-quotes": "off",
|
|
6
7
|
"prefer-const": "error",
|
|
7
8
|
"quotes": ["error", "single", "avoid-escape"],
|
|
8
9
|
"prefer-template": "error",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
const { setProp } = require('../../base/model');
|
|
16
|
+
const { xprInAnnoProperties } = require('../../compiler/builtins');
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -40,6 +41,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
|
|
|
40
41
|
mixin: dictionary,
|
|
41
42
|
ref: pathRef,
|
|
42
43
|
$origin: () => {}, // no-op
|
|
44
|
+
'@': annotation,
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
const csnPath = [ ...path ];
|
|
@@ -48,7 +50,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
|
|
|
48
50
|
}
|
|
49
51
|
else if (options.directDict) {
|
|
50
52
|
for (const name of Object.getOwnPropertyNames( parent ))
|
|
51
|
-
|
|
53
|
+
dictEntry( parent, name, parent[name] );
|
|
52
54
|
}
|
|
53
55
|
else {
|
|
54
56
|
standard( parent, prop, parent[prop] );
|
|
@@ -67,7 +69,6 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
|
|
|
67
69
|
function standard( _parent, _prop, node ) {
|
|
68
70
|
if (!node || typeof node !== 'object' ||
|
|
69
71
|
!{}.propertyIsEnumerable.call( _parent, _prop ) ||
|
|
70
|
-
(typeof _prop === 'string' && _prop.startsWith('@')) ||
|
|
71
72
|
(options.skipIgnore && node.$ignore) ||
|
|
72
73
|
options.skipStandard?.[_prop]
|
|
73
74
|
)
|
|
@@ -81,7 +82,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
|
|
|
81
82
|
|
|
82
83
|
else {
|
|
83
84
|
for (const name of Object.getOwnPropertyNames( node )) {
|
|
84
|
-
const trans = transformers[name] || standard;
|
|
85
|
+
const trans = transformers[name] || transformers[name.charAt(0)] || standard;
|
|
85
86
|
if (customTransformers[name])
|
|
86
87
|
customTransformers[name](node, name, node[name], csnPath, _parent, _prop);
|
|
87
88
|
trans( node, name, node[name], csnPath );
|
|
@@ -105,7 +106,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
|
|
|
105
106
|
|
|
106
107
|
csnPath.push( entryName );
|
|
107
108
|
for (const name of Object.getOwnPropertyNames( node )) {
|
|
108
|
-
const trans = transformers[name] || standard;
|
|
109
|
+
const trans = transformers[name] || transformers[name.charAt(0)] || standard;
|
|
109
110
|
if (customTransformers[name])
|
|
110
111
|
customTransformers[name](node, name, node[name], csnPath, dict);
|
|
111
112
|
trans( node, name, node[name], csnPath );
|
|
@@ -133,6 +134,30 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
|
|
|
133
134
|
csnPath.pop();
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Transformer for things that are annotations. When we have a "=" plus an expression of some sorts,
|
|
139
|
+
* we treat it like a "standard" thing.
|
|
140
|
+
*
|
|
141
|
+
* @param {object | Array} _parent the thing that has _prop
|
|
142
|
+
* @param {string|number} _prop the name of the current property or index
|
|
143
|
+
* @param {object} node The value of node[_prop]
|
|
144
|
+
*/
|
|
145
|
+
function annotation( _parent, _prop, node ) {
|
|
146
|
+
if (options.processAnnotations) {
|
|
147
|
+
if (node?.['='] !== undefined && xprInAnnoProperties.some(xProp => node[xProp] !== undefined)) {
|
|
148
|
+
standard(_parent, _prop, node);
|
|
149
|
+
}
|
|
150
|
+
else if (node && typeof node === 'object') {
|
|
151
|
+
csnPath.push(_prop);
|
|
152
|
+
|
|
153
|
+
for (const name of Object.getOwnPropertyNames( node ))
|
|
154
|
+
annotation( node, name, node[name] );
|
|
155
|
+
|
|
156
|
+
csnPath.pop();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
/**
|
|
137
162
|
* Special version of "dictionary" to apply artifactTransformers.
|
|
138
163
|
*
|
|
@@ -279,4 +304,5 @@ module.exports = {
|
|
|
279
304
|
* @property {object} [skipDict] stop drill-down on certain "dictionary" props
|
|
280
305
|
* @property {boolean} [skipIgnore=true] Whether to skip $ignore elements or not
|
|
281
306
|
* @property {boolean} [directDict=false] Implicitly set via applyTransformationsOnDictionary
|
|
307
|
+
* @property {boolean} [processAnnotations=false] Wether to process annotations and call custom transformers on them
|
|
282
308
|
*/
|
|
@@ -13,9 +13,11 @@ const {
|
|
|
13
13
|
* @param {CSN.Model} csn
|
|
14
14
|
* @param {object} csnUtils
|
|
15
15
|
* @param {string} pathDelimiter
|
|
16
|
+
* @param {object} [iterateOptions={}]
|
|
17
|
+
* @param {CSN.Options} [options={}]
|
|
16
18
|
* @returns {CSN.Model} Return the input csn, with the transformations applied
|
|
17
19
|
*/
|
|
18
|
-
function attachOnConditions( csn, csnUtils, pathDelimiter ) {
|
|
20
|
+
function attachOnConditions( csn, csnUtils, pathDelimiter, iterateOptions = {}, options = {} ) {
|
|
19
21
|
const { isManagedAssociation } = csnUtils;
|
|
20
22
|
|
|
21
23
|
const alreadyHandled = new WeakMap();
|
|
@@ -27,8 +29,8 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
|
|
|
27
29
|
if (isManagedAssociation(elem))
|
|
28
30
|
transformManagedAssociation(elem, elemName);
|
|
29
31
|
}
|
|
30
|
-
},
|
|
31
|
-
}, [], { skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') });
|
|
32
|
+
}, /* only for views and entities */
|
|
33
|
+
}, [], Object.assign({ skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') }, iterateOptions));
|
|
32
34
|
|
|
33
35
|
return csn;
|
|
34
36
|
|
|
@@ -47,7 +49,7 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
|
|
|
47
49
|
// Assemble an ON-condition with the foreign keys created in earlier steps
|
|
48
50
|
const onCondParts = [];
|
|
49
51
|
let joinWithAnd = false;
|
|
50
|
-
if (elem.keys.length === 0) { // TODO: really kill instead of $ignore?
|
|
52
|
+
if (elem.keys.length === 0 && options.transformation !== 'effective') { // TODO: really kill instead of $ignore?
|
|
51
53
|
elem.$ignore = true;
|
|
52
54
|
}
|
|
53
55
|
else {
|
|
@@ -55,12 +57,15 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
|
|
|
55
57
|
// Assemble left hand side of 'assoc.key = fkey'
|
|
56
58
|
const assocKeyArg = {
|
|
57
59
|
ref: [
|
|
60
|
+
...elemName.startsWith('$') ? [ '$self' ] : [],
|
|
58
61
|
elemName,
|
|
59
|
-
|
|
62
|
+
...foreignKey.ref,
|
|
63
|
+
],
|
|
60
64
|
};
|
|
61
65
|
const fkName = `${elemName}${pathDelimiter}${foreignKey.as || implicitAs(foreignKey.ref)}`;
|
|
62
66
|
const fKeyArg = {
|
|
63
67
|
ref: [
|
|
68
|
+
...fkName.startsWith('$') ? [ '$self' ] : [],
|
|
64
69
|
fkName,
|
|
65
70
|
],
|
|
66
71
|
};
|
|
@@ -104,9 +109,10 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
|
|
|
104
109
|
* @param {CSN.Model} csn
|
|
105
110
|
* @param {object} csnUtils
|
|
106
111
|
* @param {string} pathDelimiter
|
|
112
|
+
* @param {boolean} [processOnInQueries=false] Wether to process on-conditions in queries (joins and mixins)
|
|
107
113
|
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
108
114
|
*/
|
|
109
|
-
function getFKAccessFinalizer( csn, csnUtils, pathDelimiter ) {
|
|
115
|
+
function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries = false ) {
|
|
110
116
|
const {
|
|
111
117
|
inspectRef,
|
|
112
118
|
} = csnUtils;
|
|
@@ -174,10 +180,16 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter ) {
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
if (artifact.query || artifact.projection) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
const transform = (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path);
|
|
184
|
+
const queryTransformers = {
|
|
185
|
+
orderBy: transform,
|
|
186
|
+
groupBy: transform,
|
|
187
|
+
where: transform,
|
|
188
|
+
having: transform,
|
|
189
|
+
};
|
|
190
|
+
if (processOnInQueries)
|
|
191
|
+
queryTransformers.on = transform;
|
|
192
|
+
applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', queryTransformers, {}, [ 'definitions', artifactName ]);
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
|
|
@@ -28,7 +28,7 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
|
|
|
28
28
|
function transformSelfInBacklinks( artifact, artifactName, dummy, path ) {
|
|
29
29
|
// Fixme: For toHana mixins must be transformed, for toSql -d hana
|
|
30
30
|
// mixin elements must be transformed, why can't toSql also use mixins?
|
|
31
|
-
if (artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
|
|
31
|
+
if (options.transformation === 'effective' && artifact.elements || artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
|
|
32
32
|
processDict(artifact.elements, path.concat([ 'elements' ]));
|
|
33
33
|
if (artifact.query?.SELECT?.mixin)
|
|
34
34
|
processDict(artifact.query.SELECT.mixin, path.concat([ 'query', 'SELECT', 'mixin' ]));
|
|
@@ -66,6 +66,9 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
|
|
|
66
66
|
// Don't add braces if it is a single expression (ignoring superfluous braces)
|
|
67
67
|
const multipleExprs = elem.on.filter(x => x !== '(' && x !== ')' ).length > 3;
|
|
68
68
|
elem.on = processExpressionArgs(elem.on, pathToOn);
|
|
69
|
+
const column = csnUtils.getColumn(elem);
|
|
70
|
+
if (column?.cast?.on) // avoid difference between column and element
|
|
71
|
+
column.cast.on = elem.on;
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
74
|
* Process the args
|
|
@@ -187,7 +190,8 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
|
|
|
187
190
|
if (assoc.keys.length)
|
|
188
191
|
return transformDollarSelfComparisonWithManagedAssoc(assocOp, assoc, assocName, elemName);
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
if (options.transformation !== 'effective')
|
|
194
|
+
elem.$ignore = true;
|
|
191
195
|
return [];
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -33,7 +33,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
33
33
|
},
|
|
34
34
|
columns: (parent, name, columns, path) => {
|
|
35
35
|
const artifact = csn.definitions[path[1]];
|
|
36
|
-
csnUtils.initDefinition(artifact); // potentially
|
|
36
|
+
csnUtils.initDefinition(artifact); // potentially not initialized, yet
|
|
37
37
|
if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
|
|
38
38
|
const root = csnUtils.get$combined({ SELECT: parent });
|
|
39
39
|
// TODO: replace with the correct options.transformation?
|
|
@@ -719,7 +719,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
719
719
|
}
|
|
720
720
|
}
|
|
721
721
|
else { // the thing is not shadowed - use the name from the base
|
|
722
|
-
const col = { ref: [ part ] };
|
|
722
|
+
const col = part.startsWith('$') ? { ref: [ base[part][0].parent, part ] } : { ref: [ part ] };
|
|
723
723
|
if (isComplexQuery) // $env: tableAlias
|
|
724
724
|
setProp(col, '$env', base[part][0].parent);
|
|
725
725
|
|
|
@@ -273,14 +273,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
273
273
|
ref, head, tail,
|
|
274
274
|
} = getFirstAssoc(current, exprPath.concat(i));
|
|
275
275
|
|
|
276
|
-
const lastAssoc = getLastAssoc(current, exprPath.concat(i));
|
|
277
|
-
// toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
|
|
278
|
-
// '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
|
|
279
|
-
// But the error might not be clear, since it could be because of our rewritten stuff. The later check
|
|
280
|
-
// checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
|
|
281
|
-
if (lastAssoc.tail.length > 0)
|
|
282
|
-
error(null, current.$path, { id: lastAssoc.tail[0].id ? lastAssoc.tail[0].id : lastAssoc.tail[0], name: lastAssoc.ref.id ? lastAssoc.ref.id : lastAssoc.ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
|
|
283
|
-
|
|
284
276
|
const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
|
|
285
277
|
expr[i].ref = newThing;
|
|
286
278
|
}
|
|
@@ -312,11 +304,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
312
304
|
const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
|
|
313
305
|
const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
|
|
314
306
|
|
|
315
|
-
if (!root.target) {
|
|
316
|
-
error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
|
|
317
|
-
return { result: [], leftovers: [] };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
307
|
const subselect = getSubselect(root.target, ref, sources);
|
|
321
308
|
|
|
322
309
|
const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
|
|
@@ -331,7 +318,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
331
318
|
|
|
332
319
|
newExpr.push('exists');
|
|
333
320
|
if (ref && ref.where) {
|
|
334
|
-
const remappedWhere = remapExistingWhere(target, ref.where);
|
|
321
|
+
const remappedWhere = remapExistingWhere(target, ref.where, exprPath, current);
|
|
335
322
|
if (remappedWhere.length > 3)
|
|
336
323
|
subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]);
|
|
337
324
|
else
|
|
@@ -381,7 +368,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
381
368
|
*/
|
|
382
369
|
function translateManagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
|
|
383
370
|
if (current.$scope === '$self') {
|
|
384
|
-
error('ref-unexpected-
|
|
371
|
+
error('ref-unexpected-self', current.$path, { '#': 'exists', id: current.ref[0], name: 'exists' });
|
|
385
372
|
return [];
|
|
386
373
|
}
|
|
387
374
|
|
|
@@ -572,27 +559,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
572
559
|
};
|
|
573
560
|
}
|
|
574
561
|
|
|
575
|
-
/**
|
|
576
|
-
* Get the last association from the expression part - similar to getFirstAssoc
|
|
577
|
-
*
|
|
578
|
-
* @param {object} xprPart
|
|
579
|
-
* @param {CSN.Path} path
|
|
580
|
-
* @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The last assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
|
|
581
|
-
*/
|
|
582
|
-
function getLastAssoc( xprPart, path ) {
|
|
583
|
-
const { links, art } = inspectRef(path);
|
|
584
|
-
for (let i = xprPart.ref.length - 1; i > -1; i--) {
|
|
585
|
-
if (links[i].art && links[i].art.target) {
|
|
586
|
-
return {
|
|
587
|
-
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return {
|
|
592
|
-
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
|
|
596
562
|
/**
|
|
597
563
|
* Check (using inspectRef -> links), whether the first path step is an entity or query source
|
|
598
564
|
*
|
|
@@ -713,13 +679,21 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
713
679
|
*
|
|
714
680
|
* This function does this by adding the assoc target before all the refs so that the refs are resolvable in the WHERE.
|
|
715
681
|
*
|
|
682
|
+
* This function also rejects $self paths in filter conditions.
|
|
683
|
+
*
|
|
716
684
|
* @param {string} target
|
|
717
685
|
* @param {TokenStream} where
|
|
718
|
-
* @
|
|
686
|
+
* @param {CSN.Path} path path to the part, used if error needs to be thrown
|
|
687
|
+
* @param {CSN.Artifact} parent the host of the `where`, used if error needs to be thrown
|
|
688
|
+
*
|
|
689
|
+
* @returns {TokenStream} where The input-where with the refs transformed to absolute ones
|
|
719
690
|
*/
|
|
720
|
-
function remapExistingWhere( target, where ) {
|
|
691
|
+
function remapExistingWhere( target, where, path, parent ) {
|
|
721
692
|
return where.map((part) => {
|
|
722
|
-
if (part
|
|
693
|
+
if (part.$scope === '$self') {
|
|
694
|
+
error('ref-unexpected-self', path, { '#': 'exists-filter', elemref: parent, id: part.ref[0] });
|
|
695
|
+
}
|
|
696
|
+
else if (part.ref && part.$scope !== '$magic') {
|
|
723
697
|
part.ref = [ target, ...part.ref ];
|
|
724
698
|
return part;
|
|
725
699
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
hasAnnotationValue, getServiceNames, forEachDefinition,
|
|
5
|
-
getResultingName, forEachMemberRecursively,
|
|
5
|
+
getResultingName, forEachMemberRecursively, applyAnnotationsFromExtensions,
|
|
6
6
|
} = require('../../model/csnUtils');
|
|
7
7
|
const { setProp, isDeprecatedEnabled } = require('../../base/model');
|
|
8
8
|
const { getTransformers } = require('../transformUtils');
|
|
@@ -29,9 +29,12 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
29
29
|
} = getTransformers(csn, options, pathDelimiter);
|
|
30
30
|
const { getCsnDef, isComposition } = csnUtils;
|
|
31
31
|
const { error, warning } = messageFunctions;
|
|
32
|
+
const generatedArtifacts = Object.create(null);
|
|
32
33
|
|
|
33
34
|
forEachDefinition(csn, generateDraft);
|
|
34
35
|
|
|
36
|
+
applyAnnotationsFromExtensions(csn, { filter: name => generatedArtifacts[name], applyToElements: false });
|
|
37
|
+
|
|
35
38
|
/**
|
|
36
39
|
* Generate the draft stuff for a given artifact
|
|
37
40
|
*
|
|
@@ -117,6 +120,8 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
117
120
|
// The name of the draft shadow entity we should generate
|
|
118
121
|
const draftsArtifactName = `${artifactName}${draftSuffix}`;
|
|
119
122
|
|
|
123
|
+
generatedArtifacts[draftsArtifactName] = true;
|
|
124
|
+
|
|
120
125
|
// extract keys for UUID inspection
|
|
121
126
|
const keys = [];
|
|
122
127
|
forEachMemberRecursively(artifact, (elt, name, prop, path) => {
|
|
@@ -132,7 +137,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
132
137
|
'Entity annotated with “@odata.draft.enabled” should have exactly one key element, but found $(COUNT)');
|
|
133
138
|
}
|
|
134
139
|
else {
|
|
135
|
-
const uuidCount = keys.reduce((acc, k) => ((k.type === 'cds.String' && k.$renamed === 'cds.UUID' && k.length === 36) ? acc + 1 : acc), 0);
|
|
140
|
+
const uuidCount = keys.reduce((acc, k) => ((k.type === 'cds.UUID' || k.type === 'cds.String' && k.$renamed === 'cds.UUID' && k.length === 36) ? acc + 1 : acc), 0);
|
|
136
141
|
if (uuidCount === 0)
|
|
137
142
|
warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have one key element of type “cds.UUID”');
|
|
138
143
|
}
|
|
@@ -143,10 +148,16 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
143
148
|
const draftAdminDataProjectionName = `${matchingService}.DraftAdministrativeData`;
|
|
144
149
|
let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
|
|
145
150
|
if (!draftAdminDataProjection) {
|
|
151
|
+
generatedArtifacts[draftAdminDataProjectionName] = true;
|
|
146
152
|
draftAdminDataProjection = createAndAddDraftAdminDataProjection(matchingService, true);
|
|
147
153
|
|
|
148
154
|
if (!draftAdminDataProjection.projection.columns && draftAdminDataProjection.elements.DraftUUID)
|
|
149
155
|
draftAdminDataProjection.projection.columns = Object.keys(draftAdminDataProjection.elements).map(e => (e === 'DraftUUID' ? { key: true, ref: [ 'DraftAdministrativeData', e ] } : { ref: [ 'DraftAdministrativeData', e ] }));
|
|
156
|
+
|
|
157
|
+
if (options.transformation === 'effective' && draftAdminDataProjection.projection) {
|
|
158
|
+
draftAdminDataProjection.query = { SELECT: draftAdminDataProjection.projection };
|
|
159
|
+
delete draftAdminDataProjection.projection;
|
|
160
|
+
}
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
// Barf if it is not an entity or not what we expect
|
|
@@ -159,7 +170,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
159
170
|
|
|
160
171
|
const persistenceName = getResultingName(csn, options.sqlMapping, draftsArtifactName);
|
|
161
172
|
// Duplicate the artifact as a draft shadow entity
|
|
162
|
-
if (csn.definitions[persistenceName]) {
|
|
173
|
+
if (csn.definitions[persistenceName] && !(options.transformation === 'effective' && csn.definitions[persistenceName].kind === 'entity' && csn.definitions[persistenceName].elements.DraftAdministrativeData_DraftUUID)) {
|
|
163
174
|
const definingDraftRoot = draftRoots.get(csn.definitions[persistenceName]);
|
|
164
175
|
if (!definingDraftRoot) {
|
|
165
176
|
error(null, [ 'definitions', artifactName ], { name: persistenceName },
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { forEachDefinition, getServiceNames } = require('../../model/csnUtils');
|
|
3
|
+
const { forEachDefinition, getServiceNames, applyAnnotationsFromExtensions } = require('../../model/csnUtils');
|
|
4
4
|
const { forEach } = require('../../utils/objectUtils');
|
|
5
5
|
const { isArtifactInSomeService, getServiceOfArtifact } = require('../odata/utils');
|
|
6
6
|
const { getTransformers } = require('../transformUtils');
|
|
7
|
-
const { ModelError } = require('../../base/error');
|
|
8
7
|
const { makeMessageFunction } = require('../../base/messages');
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -29,7 +28,6 @@ const { makeMessageFunction } = require('../../base/messages');
|
|
|
29
28
|
*/
|
|
30
29
|
function generateDrafts( csn, options, services ) {
|
|
31
30
|
const {
|
|
32
|
-
createForeignKeyElement,
|
|
33
31
|
createAndAddDraftAdminDataProjection, createScalarElement,
|
|
34
32
|
createAssociationElement, createAssociationPathComparison,
|
|
35
33
|
addElement, createAction, assignAction,
|
|
@@ -52,7 +50,7 @@ function generateDrafts( csn, options, services ) {
|
|
|
52
50
|
const externalServices = services.filter(serviceName => csn.definitions[serviceName]['@cds.external']);
|
|
53
51
|
// @ts-ignore
|
|
54
52
|
const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name));
|
|
55
|
-
|
|
53
|
+
const filterDict = Object.create(null);
|
|
56
54
|
forEachDefinition(csn, (def, defName) => {
|
|
57
55
|
// Generate artificial draft fields for entities/views if requested, ignore if not part of a service
|
|
58
56
|
if (def.kind === 'entity' && def['@odata.draft.enabled'] && isArtifactInSomeService(defName, services))
|
|
@@ -61,6 +59,7 @@ function generateDrafts( csn, options, services ) {
|
|
|
61
59
|
visitedArtifacts[defName] = true;
|
|
62
60
|
}, { skipArtifact: isExternalServiceMember });
|
|
63
61
|
|
|
62
|
+
applyAnnotationsFromExtensions(csn, { override: true, filter: name => filterDict[name] });
|
|
64
63
|
return csn;
|
|
65
64
|
/**
|
|
66
65
|
* Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact,
|
|
@@ -77,11 +76,6 @@ function generateDrafts( csn, options, services ) {
|
|
|
77
76
|
* @param {CSN.Artifact} rootArtifact artifact where composition traversal started
|
|
78
77
|
*/
|
|
79
78
|
function generateDraftForOdata( artifact, artifactName, rootArtifact ) {
|
|
80
|
-
// Sanity check
|
|
81
|
-
// @ts-ignore
|
|
82
|
-
if (!isArtifactInSomeService(artifactName, services))
|
|
83
|
-
throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
|
|
84
|
-
|
|
85
79
|
// Nothing to do if already draft-enabled (composition traversal may have circles)
|
|
86
80
|
if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) &&
|
|
87
81
|
artifact.actions && artifact.actions.draftPrepare)
|
|
@@ -106,9 +100,11 @@ function generateDrafts( csn, options, services ) {
|
|
|
106
100
|
resetAnnotation(artifact, '@Common.DraftRoot.ActivationAction', 'draftActivate', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
107
101
|
resetAnnotation(artifact, '@Common.DraftRoot.EditAction', 'draftEdit', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
108
102
|
resetAnnotation(artifact, '@Common.DraftRoot.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
103
|
+
filterDict[artifactName] = true;
|
|
109
104
|
}
|
|
110
105
|
else {
|
|
111
106
|
resetAnnotation(artifact, '@Common.DraftNode.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
|
|
107
|
+
filterDict[artifactName] = true;
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
Object.values(artifact.elements || {}).forEach( (elem) => {
|
|
@@ -141,15 +137,6 @@ function generateDrafts( csn, options, services ) {
|
|
|
141
137
|
draftAdministrativeData.DraftAdministrativeData['@UI.Hidden'] = true;
|
|
142
138
|
addElement(draftAdministrativeData, artifact, artifactName);
|
|
143
139
|
|
|
144
|
-
// Note that we need to do the ODATA transformation steps for managed associations
|
|
145
|
-
// (foreign key field generation, generatedFieldName) by hand, because the corresponding
|
|
146
|
-
// transformation steps have already been done on all artifacts when we come here)
|
|
147
|
-
let uuidDraftKey = draftAdministrativeData.DraftAdministrativeData.keys.filter(key => key.ref && key.ref.length === 1 && key.ref[0] === 'DraftUUID');
|
|
148
|
-
if (uuidDraftKey && uuidDraftKey[0]) {
|
|
149
|
-
uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element
|
|
150
|
-
const path = [ 'definitions', artifactName, 'elements', 'DraftAdministrativeData', 'keys', 0 ];
|
|
151
|
-
createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName, path);
|
|
152
|
-
}
|
|
153
140
|
// SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)
|
|
154
141
|
const siblingEntity = createAssociationElement('SiblingEntity', artifactName, false);
|
|
155
142
|
siblingEntity.SiblingEntity.cardinality = { max: 1 };
|