@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 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) {
@@ -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.entity',
326
- '@ValueList.type': '@Common.ValueList.type',
327
- '@Capabilities.Deletable': '@Capabilities.DeleteRestrictions.Deletable',
328
- '@Capabilities.Insertable': '@Capabilities.InsertRestrictions.Insertable',
329
- '@Capabilities.Updatable': '@Capabilities.UpdateRestrictions.Updatable',
330
- '@Capabilities.Readable': '@Capabilities.ReadRestrictions.Readable',
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) ? name : renameShortCuts.find(p => name.startsWith(p + '.'));
339
+ const renamePrefix = (name in renameMappings)
340
+ ? name
341
+ : renameShortCuts.find(p => name.startsWith(p + '.'));
340
342
  if(renamePrefix) {
341
- renameAnnotation(node, name, name.replace(renamePrefix, renameMappings[renamePrefix]));
342
- } else {
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) ? name : setShortCuts.find(p => name.startsWith(p + '.'));
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
- // Special case: '@readonly' becomes a triplet of capability restrictions for entities,
352
- // but '@Core.Immutable' for everything else.
353
- if (!(node['@readonly'] && node['@insertonly'])) {
354
- if (name === '@readonly' && node[name]) {
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, '@Capabilities.DeleteRestrictions.Deletable', false);
357
- setAnnotation(node, '@Capabilities.InsertRestrictions.Insertable', false);
358
- setAnnotation(node, '@Capabilities.UpdateRestrictions.Updatable', false);
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
- // @insertonly is effective on entities/queries only
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
- // Only on element level: translate @mandatory
373
- if (name === '@mandatory' && node[name] &&
374
- node.kind === undefined && node['@Common.FieldControl'] === undefined) {
375
- setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });
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
- if (name === '@assert.format' && node[name] !== null)
379
- setAnnotation(node, '@Validation.Pattern', node['@assert.format']);
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
- if (name === '@assert.range' && node[name] !== null) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "CDS (Core Data Services) compiler and backends",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "author": "SAP SE (https://www.sap.com)",