@sap/cds-compiler 4.0.0 → 4.0.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 +15 -0
- package/lib/api/main.js +29 -1
- package/lib/edm/annotations/genericTranslation.js +29 -1
- package/lib/edm/edmPreprocessor.js +7 -1
- package/lib/model/sortViews.js +4 -2
- package/lib/transform/forOdataNew.js +51 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
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
|
+
|
|
11
|
+
## Version 4.0.2 - 2023-06-22
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- to.sql.migration: When drop-creating views, also drop-create (transitively) dependent views.
|
|
16
|
+
- to.edm(x):
|
|
17
|
+
+ Forward `@odata.singleton { nullable }` annotation to parameter entity.
|
|
18
|
+
+ Annotations assigned to a parameterized entity are propagated to the parameter entity if the annotation is
|
|
19
|
+
applicable to either an `edm.EntitySet` or `edm.Singleton`. This especially covers all `@Capabilities` and their
|
|
20
|
+
shortcut forms like `@readonly` and `@insertonly`. The original annotation is not removed from the original entity.
|
|
21
|
+
Annotations that should be rendered at the parameter `edm.EntityType` can be qualified with `$parameters`.
|
|
22
|
+
Explicitly qualified annotations are removed from the original entity allowing individual assignments.
|
|
23
|
+
|
|
24
|
+
|
|
10
25
|
## Version 4.0.0 - 2023-06-06
|
|
11
26
|
|
|
12
27
|
### Added
|
package/lib/api/main.js
CHANGED
|
@@ -384,6 +384,7 @@ function sqlMigration( csn, options, beforeImage ) {
|
|
|
384
384
|
}, {}),
|
|
385
385
|
};
|
|
386
386
|
|
|
387
|
+
const markedSkipByUs = {};
|
|
387
388
|
const cleanup = [];
|
|
388
389
|
// Delete artifacts that are already present in csn
|
|
389
390
|
if (beforeImage?.definitions) {
|
|
@@ -404,10 +405,38 @@ function sqlMigration( csn, options, beforeImage ) {
|
|
|
404
405
|
) { // don't render again, but need info for primary key extension
|
|
405
406
|
diffArtifact['@cds.persistence.skip'] = true;
|
|
406
407
|
cleanup.push(() => delete diffArtifact['@cds.persistence.skip']);
|
|
408
|
+
markedSkipByUs[artifactName] = true;
|
|
407
409
|
}
|
|
408
410
|
});
|
|
409
411
|
}
|
|
410
412
|
|
|
413
|
+
const sortOrder = sortViews({ sql: {}, csn: afterImage });
|
|
414
|
+
|
|
415
|
+
const dependentsDict = {};
|
|
416
|
+
sortOrder.forEach(({ name, dependents }) => {
|
|
417
|
+
dependentsDict[name] = dependents;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const stack = Object.keys(drops.creates);
|
|
421
|
+
while (stack.length > 0) {
|
|
422
|
+
const name = stack.pop();
|
|
423
|
+
const artifact = diff.definitions[name];
|
|
424
|
+
if (drops.creates[name] === undefined) {
|
|
425
|
+
if (artifact['@cds.persistence.skip'] && markedSkipByUs[name]) {
|
|
426
|
+
// Remove the skip so we render a CREATE VIEW
|
|
427
|
+
diff.definitions[name]['@cds.persistence.skip'] = false;
|
|
428
|
+
drops.creates[name] = `DROP VIEW ${ identifierUtils.renderArtifactName(name) };`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const dependents = dependentsDict[name];
|
|
433
|
+
if (dependents) { // schedule any dependents for processing that don't have a drop-create yet
|
|
434
|
+
for (const dependantName in dependents) {
|
|
435
|
+
if (!drops.creates[dependantName])
|
|
436
|
+
stack.push(dependantName);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
411
440
|
// Convert the diff to SQL.
|
|
412
441
|
if (!internalOptions.beta)
|
|
413
442
|
internalOptions.beta = {};
|
|
@@ -419,7 +448,6 @@ function sqlMigration( csn, options, beforeImage ) {
|
|
|
419
448
|
|
|
420
449
|
cleanup.forEach(fn => fn());
|
|
421
450
|
// TODO: Handle `ADD CONSTRAINT` etc!
|
|
422
|
-
const sortOrder = sortViews({ sql: {}, csn: afterImage });
|
|
423
451
|
|
|
424
452
|
const dropSqls = [];
|
|
425
453
|
const createAndAlterSqls = [];
|
|
@@ -267,6 +267,34 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName,
|
|
|
267
267
|
|
|
268
268
|
const v = options.v;
|
|
269
269
|
|
|
270
|
+
// Copy annotations from origin to parameter entity if it's
|
|
271
|
+
// qualified with #$parameters or if its applicable to an EntitySet or Singleton
|
|
272
|
+
forEachDefinition(reqDefs, (object) => {
|
|
273
|
+
if(object.$isParamEntity && object._origin) {
|
|
274
|
+
for(const attr in object._origin) {
|
|
275
|
+
if (attr[0] === '@') {
|
|
276
|
+
const [ prefix, innerAnnotation ] = attr.split('.@');
|
|
277
|
+
const ns = whatsMyTermNamespace(prefix);
|
|
278
|
+
if(ns) {
|
|
279
|
+
const steps = prefix.replace('@' + ns + '.', '').split('.');
|
|
280
|
+
const paramAnnoParts = steps[0].split('#$parameters');
|
|
281
|
+
const dictTerm = getDictTerm(ns + '.' + paramAnnoParts[0], options);
|
|
282
|
+
if(paramAnnoParts.length > 1 || ['Singleton', 'EntitySet'].some(y => dictTerm?.AppliesTo?.includes(y))) {
|
|
283
|
+
steps[0] = '@' + ns + '.' + paramAnnoParts.join('');
|
|
284
|
+
let newAnno = steps.join('.');
|
|
285
|
+
if(innerAnnotation)
|
|
286
|
+
newAnno += '.@' + innerAnnotation;
|
|
287
|
+
edmUtils.assignAnnotation(object, newAnno, object._origin[attr]);
|
|
288
|
+
// delete original annotation only if it was qualified with $parameters
|
|
289
|
+
if(paramAnnoParts.length > 1)
|
|
290
|
+
delete object._origin[attr];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
270
298
|
// Crawl over the csn and trigger the annotation translation for all kinds
|
|
271
299
|
// of annotated things.
|
|
272
300
|
// Note: only works for single service
|
|
@@ -771,7 +799,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName,
|
|
|
771
799
|
const [ prefix, innerAnnotation ] = a.split('.@');
|
|
772
800
|
const ns = whatsMyTermNamespace(prefix);
|
|
773
801
|
const steps = prefix.replace('@' + ns + '.', '').split('.');
|
|
774
|
-
steps.splice(0,0, ns);
|
|
802
|
+
steps.splice(0, 0, ns);
|
|
775
803
|
let i = steps.lastIndexOf('$edmJson');
|
|
776
804
|
if(i > -1) {
|
|
777
805
|
i = steps.findIndex(s => s.includes('@'), i+1);
|
|
@@ -495,7 +495,7 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
|
|
|
495
495
|
|
|
496
496
|
// create the Parameter Definition
|
|
497
497
|
const parameterCsn = createParameterEntity(entityCsn, entityName, false);
|
|
498
|
-
|
|
498
|
+
setProp(parameterCsn, '_origin', entityCsn);
|
|
499
499
|
// create the Type Definition
|
|
500
500
|
// modify the original parameter entity with backlink and new name
|
|
501
501
|
if(csn.definitions[typeEntityName])
|
|
@@ -609,6 +609,12 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
|
|
|
609
609
|
[ 'definitions', parameterEntityName, 'elements', parameterToTypeAssocName ] );
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
+
[ '@odata.singleton', '@odata.singleton.nullable' ].forEach(a => {
|
|
613
|
+
if(entityCsn[a] != null)
|
|
614
|
+
parameterCsn[a] = entityCsn[a];
|
|
615
|
+
delete entityCsn[a];
|
|
616
|
+
});
|
|
617
|
+
|
|
612
618
|
// initialize containment
|
|
613
619
|
// propagate containment information, if containment is recursive, use parameterCsn.name as $containerNames
|
|
614
620
|
if(entityCsn.$containerNames) {
|
package/lib/model/sortViews.js
CHANGED
|
@@ -93,13 +93,12 @@ function _findWithXPointers( definitionsArray, x, _dependents, _dependencies ) {
|
|
|
93
93
|
module.exports = function sortViews({ sql, csn }) {
|
|
94
94
|
const { cleanup, _dependents, _dependencies } = setDependencies(csn);
|
|
95
95
|
const { layers, leftover } = sortTopologically(csn, _dependents, _dependencies);
|
|
96
|
-
cleanup.forEach(fn => fn());
|
|
97
96
|
if (leftover.length > 0)
|
|
98
97
|
throw new ModelError('Unable to build a correct dependency graph! Are there cycles?');
|
|
99
98
|
|
|
100
99
|
const result = [];
|
|
101
100
|
// keep the "artifact name" - needed for to.hdi sorting
|
|
102
|
-
layers.forEach(layer => layer.forEach(objName => result.push({ name: objName, sql: sql[objName] })));
|
|
101
|
+
layers.forEach(layer => layer.forEach(objName => result.push({ name: objName, sql: sql[objName], dependents: csn.definitions[objName][_dependents] })));
|
|
103
102
|
// attach sql artifacts which are not considered during the view sorting algorithm
|
|
104
103
|
// --> this is the case for "ALTER TABLE ADD CONSTRAINT" statements,
|
|
105
104
|
// because their identifiers are not part of the csn.definitions
|
|
@@ -107,5 +106,8 @@ module.exports = function sortViews({ sql, csn }) {
|
|
|
107
106
|
if (!result.some( o => o.name === name )) // not in result but in incoming sql
|
|
108
107
|
result.push({ name, sql: sqlString });
|
|
109
108
|
});
|
|
109
|
+
|
|
110
|
+
cleanup.forEach(fn => fn());
|
|
111
|
+
|
|
110
112
|
return result;
|
|
111
113
|
};
|
|
@@ -312,79 +312,93 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
312
312
|
node['@Core.Computed'] = true;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
// Rename shorthand annotations within artifact or element 'node' according to a builtin
|
|
316
|
-
// list.
|
|
315
|
+
// Rename shorthand annotations within artifact or element 'node' according to a builtin list
|
|
317
316
|
function renameShorthandAnnotations(node) {
|
|
318
|
-
// FIXME: Verify this list - are they all still required? Do we need any more?
|
|
319
317
|
const setMappings = {
|
|
320
318
|
'@label': '@Common.Label',
|
|
321
319
|
'@title': '@Common.Label',
|
|
322
320
|
'@description': '@Core.Description',
|
|
323
321
|
};
|
|
324
322
|
const renameMappings = {
|
|
325
|
-
'@ValueList.entity': '@Common.ValueList
|
|
326
|
-
'@ValueList.type': '@Common.ValueList
|
|
327
|
-
'@Capabilities.Deletable': '@Capabilities.DeleteRestrictions
|
|
328
|
-
'@Capabilities.Insertable': '@Capabilities.InsertRestrictions
|
|
329
|
-
'@Capabilities.Updatable': '@Capabilities.UpdateRestrictions
|
|
330
|
-
'@Capabilities.Readable': '@Capabilities.ReadRestrictions
|
|
323
|
+
'@ValueList.entity': { val: '@Common.ValueList', op: 'entity' },
|
|
324
|
+
'@ValueList.type': { val: '@Common.ValueList', op: 'type' },
|
|
325
|
+
'@Capabilities.Deletable': { val: '@Capabilities.DeleteRestrictions', op: 'Deletable' },
|
|
326
|
+
'@Capabilities.Insertable': { val: '@Capabilities.InsertRestrictions', op: 'Insertable' },
|
|
327
|
+
'@Capabilities.Updatable': { val: '@Capabilities.UpdateRestrictions', op: 'Updatable' },
|
|
328
|
+
'@Capabilities.Readable': { val: '@Capabilities.ReadRestrictions', op: 'Readable' }
|
|
331
329
|
};
|
|
332
330
|
|
|
333
331
|
const setShortCuts = Object.keys(setMappings);
|
|
334
332
|
const renameShortCuts = Object.keys(renameMappings);
|
|
333
|
+
|
|
334
|
+
// Capabilities shortcuts have precedence over @readonly/@insertonly
|
|
335
335
|
Object.keys(node).forEach( name => {
|
|
336
336
|
if (!name.startsWith('@'))
|
|
337
337
|
return;
|
|
338
338
|
// Rename according to map above
|
|
339
|
-
const renamePrefix = (name in renameMappings)
|
|
339
|
+
const renamePrefix = (name in renameMappings)
|
|
340
|
+
? name
|
|
341
|
+
: renameShortCuts.find(p => name.startsWith(p + '.'));
|
|
340
342
|
if(renamePrefix) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
const mapping = renameMappings[renamePrefix];
|
|
344
|
+
renameAnnotation(node, name, name.replace(renamePrefix, `${mapping.val}.${mapping.op}`));
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
343
347
|
// The two mappings have no overlap, so no need to check for second map if first matched.
|
|
344
348
|
// Rename according to map above
|
|
345
|
-
const setPrefix = (name in setMappings)
|
|
349
|
+
const setPrefix = (name in setMappings)
|
|
350
|
+
? name
|
|
351
|
+
: setShortCuts.find(p => name.startsWith(p + '.') || name.startsWith(p + '#'));
|
|
346
352
|
if(setPrefix) {
|
|
347
353
|
setAnnotation(node, name.replace(setPrefix, setMappings[setPrefix]), node[name]);
|
|
348
354
|
}
|
|
349
355
|
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Special case: '@readonly' becomes a triplet of capability restrictions for entities,
|
|
359
|
+
// but '@Core.Computed' for everything else.
|
|
350
360
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if
|
|
354
|
-
|
|
361
|
+
// only if not both readonly/insertonly are true do the mapping
|
|
362
|
+
if(!(node['@readonly'] && node['@insertonly'])) {
|
|
363
|
+
if(node['@readonly']) {
|
|
364
|
+
const setRO = (qualifier) => {
|
|
355
365
|
if (node.kind === 'entity' || node.kind === 'aspect') {
|
|
356
|
-
setAnnotation(node,
|
|
357
|
-
setAnnotation(node,
|
|
358
|
-
setAnnotation(node,
|
|
366
|
+
setAnnotation(node, `@Capabilities.DeleteRestrictions${ qualifier ? '#' + qualifier : ''}.Deletable`, false);
|
|
367
|
+
setAnnotation(node, `@Capabilities.InsertRestrictions${ qualifier ? '#' + qualifier : ''}.Insertable`, false);
|
|
368
|
+
setAnnotation(node, `@Capabilities.UpdateRestrictions${ qualifier ? '#' + qualifier : ''}.Updatable`, false);
|
|
359
369
|
} else {
|
|
360
370
|
setAnnotation(node, '@Core.Computed', true);
|
|
361
371
|
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
else if (name === '@insertonly' && node[name]) {
|
|
365
|
-
if (node.kind === 'entity' || node.kind === 'aspect') {
|
|
366
|
-
setAnnotation(node, '@Capabilities.DeleteRestrictions.Deletable', false);
|
|
367
|
-
setAnnotation(node, '@Capabilities.ReadRestrictions.Readable', false);
|
|
368
|
-
setAnnotation(node, '@Capabilities.UpdateRestrictions.Updatable', false);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
372
|
+
};
|
|
373
|
+
setRO(undefined);
|
|
371
374
|
}
|
|
372
|
-
//
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
// @insertonly is effective on entities/queries only
|
|
376
|
+
if (node['@insertonly'] && (node.kind === 'entity' || node.kind === 'aspect')) {
|
|
377
|
+
const setIO = (qualifier) => {
|
|
378
|
+
setAnnotation(node, `@Capabilities.DeleteRestrictions${ qualifier ? '#' + qualifier : ''}.Deletable`, false);
|
|
379
|
+
setAnnotation(node, `@Capabilities.ReadRestrictions${ qualifier ? '#' + qualifier : ''}.Readable`, false);
|
|
380
|
+
setAnnotation(node, `@Capabilities.UpdateRestrictions${ qualifier ? '#' + qualifier : ''}.Updatable`, false);
|
|
381
|
+
}
|
|
382
|
+
setIO(undefined);
|
|
376
383
|
}
|
|
384
|
+
}
|
|
377
385
|
|
|
378
|
-
|
|
379
|
-
|
|
386
|
+
// @Validation.Pattern is applicable to "Term" => node.kind === annotation
|
|
387
|
+
if (node['@assert.format'] != null)
|
|
388
|
+
setAnnotation(node, '@Validation.Pattern', node['@assert.format']);
|
|
380
389
|
|
|
381
|
-
|
|
390
|
+
// Only on element level
|
|
391
|
+
if(node.kind == null) {
|
|
392
|
+
if (node['@mandatory']&& node['@Common.FieldControl'] === undefined) {
|
|
393
|
+
setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });
|
|
394
|
+
}
|
|
395
|
+
if (node['@assert.range'] != null) {
|
|
382
396
|
if (Array.isArray(node['@assert.range']) && node['@assert.range'].length === 2) {
|
|
383
397
|
setAnnotation(node, '@Validation.Minimum', node['@assert.range'][0]);
|
|
384
398
|
setAnnotation(node, '@Validation.Maximum', node['@assert.range'][1]);
|
|
385
399
|
}
|
|
386
400
|
}
|
|
387
|
-
}
|
|
401
|
+
}
|
|
388
402
|
}
|
|
389
403
|
|
|
390
404
|
// Apply default type facets to each type definition and every member
|