@cap-js/ord 1.3.9 → 1.3.11

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/lib/build.js CHANGED
@@ -10,7 +10,9 @@ module.exports = class OrdBuildPlugin extends cds_dk.build.Plugin {
10
10
  static taskDefaults = { src: cds.env.folders.srv };
11
11
 
12
12
  init() {
13
- this.task.dest = path.join(cds.root, BUILD_DEFAULT_PATH);
13
+ if (this.task.dest === undefined) {
14
+ this.task.dest = path.join(cds.root, BUILD_DEFAULT_PATH);
15
+ }
14
16
  }
15
17
 
16
18
  async _writeResourcesFiles(resObj, model, promises) {
package/lib/constants.js CHANGED
@@ -44,6 +44,8 @@ const CONTENT_MERGE_KEY = "ordId";
44
44
 
45
45
  const DATA_PRODUCT_ANNOTATION = "@DataIntegration.dataProduct.type";
46
46
 
47
+ const DATA_PRODUCT_SIMPLE_ANNOTATION = "@data.product";
48
+
47
49
  const DATA_PRODUCT_TYPE = Object.freeze({
48
50
  primary: "primary",
49
51
  });
@@ -106,6 +108,7 @@ module.exports = {
106
108
  COMPILER_TYPES,
107
109
  CONTENT_MERGE_KEY,
108
110
  DATA_PRODUCT_ANNOTATION,
111
+ DATA_PRODUCT_SIMPLE_ANNOTATION,
109
112
  DATA_PRODUCT_TYPE,
110
113
  DESCRIPTION_PREFIX,
111
114
  ENTITY_RELATIONSHIP_ANNOTATION,
@@ -0,0 +1,127 @@
1
+ const localize = require("@sap/cds/lib/i18n/localize");
2
+
3
+ // turn effective CSN into interop CSN
4
+ function interopCSN(csn) {
5
+ if (typeof csn != "object" || csn === null) return csn; // handle non-object inputs early
6
+ add_i18n_texts(csn);
7
+ map_annotations(csn);
8
+ add_meta_info(csn);
9
+ return csn;
10
+ }
11
+
12
+ //
13
+ // add i18n texts
14
+ //
15
+ // First fetch all texts defined in the app,
16
+ // then remove those that are not referenced in the csn.
17
+ //
18
+ function add_i18n_texts(csn) {
19
+ // get all texts of the app
20
+ const i18n = [...(localize.bundles4(csn) || [])]
21
+ .filter(([locale]) => !!locale)
22
+ .reduce((all, [locale, value]) => ({ ...all, [locale]: value }), {});
23
+
24
+ // get all i18n keys referenced in the csn
25
+ let i18n_keys = new Set();
26
+ for (let n1 in csn.definitions) {
27
+ let def = csn.definitions[n1];
28
+ collect_i18n(def, i18n_keys);
29
+ if (def.kind === "entity")
30
+ for (let n2 in def.elements) {
31
+ let el = def.elements[n2];
32
+ collect_i18n(el, i18n_keys);
33
+ }
34
+ }
35
+
36
+ // delete from i18n array all entries not occuring in i18n_keys and add result to csn
37
+ for (let locale in i18n) {
38
+ let texts = i18n[locale];
39
+ for (let k in texts) {
40
+ if (!i18n_keys.has(k)) delete texts[k];
41
+ }
42
+ if (Object.keys(texts).length === 0) delete i18n[locale];
43
+ }
44
+ csn["i18n"] = i18n;
45
+
46
+ // helper function: find all i18n keys referenced in annotations
47
+ // for an entity or element
48
+ // to be improved: currently only considers annotations with scalar, string-like value;
49
+ // doesn't drill into structured or array-like annotations
50
+ function collect_i18n(o, keys) {
51
+ for (let n in o) {
52
+ if (n.startsWith("@")) {
53
+ let annoVal = o[n];
54
+ if (typeof annoVal === "string" && annoVal.startsWith("{i18n>")) {
55
+ let [, x] = annoVal.match(/^\{i18n>(.*)\}$/) || [];
56
+ if (x) keys.add(x);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ //
64
+ // annotation mapping/replacement
65
+ //
66
+ function map_annotations(csn) {
67
+ for (let n1 in csn.definitions) {
68
+ let def = csn.definitions[n1];
69
+ replaceAnnos(def);
70
+ if (def.kind === "entity")
71
+ for (let n2 in def.elements) {
72
+ let el = def.elements[n2];
73
+ replaceAnnos(el);
74
+ }
75
+ }
76
+
77
+ // helper function: do the actual anno replacement
78
+ function replaceAnnos(o) {
79
+ // rhs null => anno is removed
80
+ const annoReplacement = {
81
+ "@Common.Label": "@EndUserText.label",
82
+ "@title": "@EndUserText.label",
83
+ "@label": "@EndUserText.label",
84
+ "@description": "@EndUserText.quickInfo",
85
+ "@cds.autoexpose": null,
86
+ };
87
+
88
+ for (const [oldA, newA] of Object.entries(annoReplacement)) {
89
+ if (o[oldA]) {
90
+ if (newA) o[newA] ??= o[oldA];
91
+ delete o[oldA];
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ //
98
+ // add meta information
99
+ //
100
+ function add_meta_info(csn) {
101
+ if (typeof csn != "object") return csn; // needed to make tests pass
102
+ csn["csnInteropEffective"] = "1.0";
103
+ csn.meta ??= {};
104
+ csn.meta.flavor = "effective";
105
+
106
+ let services = Object.entries(csn.definitions).filter(([, def]) => def.kind === "service");
107
+ if (services.length === 1) {
108
+ // assumption: "short" service name contains no dots
109
+ let segments = services[0][0].split(".");
110
+ let v = "1";
111
+ let srv = segments.pop();
112
+ let m = srv.match(/^v(\d+)$/);
113
+ if (m) {
114
+ srv = segments.pop();
115
+ v = m[1];
116
+ }
117
+ csn.meta.document = { version: `${v}.0.0` };
118
+ csn.meta.__name = srv;
119
+ if (segments.length > 0) csn.meta.__namespace = segments.join(".");
120
+ }
121
+
122
+ return csn;
123
+ }
124
+
125
+ module.exports = {
126
+ interopCSN,
127
+ };
package/lib/metaData.js CHANGED
@@ -3,6 +3,7 @@ const { compile: openapi } = require("@cap-js/openapi");
3
3
  const { compile: asyncapi } = require("@cap-js/asyncapi");
4
4
  const { COMPILER_TYPES } = require("./constants");
5
5
  const { Logger } = require("./logger");
6
+ const { interopCSN } = require("./interopCsn.js");
6
7
  const cdsc = require("@sap/cds-compiler/lib/main");
7
8
 
8
9
  module.exports = async (url, model = null) => {
@@ -36,8 +37,9 @@ module.exports = async (url, model = null) => {
36
37
  break;
37
38
  case COMPILER_TYPES.csn:
38
39
  try {
39
- const opt2 = { beta: { effectiveCsn: true }, effectiveServiceName: serviceName };
40
- responseFile = cdsc.for.effective(csn, opt2);
40
+ const opt_eff = { beta: { effectiveCsn: true }, effectiveServiceName: serviceName };
41
+ let effCsn = cdsc.for.effective(csn, opt_eff);
42
+ responseFile = interopCSN(effCsn);
41
43
  } catch (error) {
42
44
  Logger.error("Csn error:", error.message);
43
45
  throw error;
@@ -3,6 +3,7 @@ const { ord, getMetadata, defaults, authentication, Logger } = require("./index.
3
3
 
4
4
  class OpenResourceDiscoveryService extends cds.ApplicationService {
5
5
  init() {
6
+ const logger = Logger.Logger;
6
7
  cds.app.get(`${this.path}`, cds.middlewares.before, (_, res) => {
7
8
  return res.status(200).send(defaults.baseTemplate);
8
9
  });
@@ -22,7 +23,7 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
22
23
  const { contentType, response } = await getMetadata(req.url);
23
24
  return res.status(200).contentType(contentType).send(response);
24
25
  } catch (error) {
25
- Logger.error(error, "Error while processing the resource definition document");
26
+ logger.error(error, "Error while processing the resource definition document");
26
27
  return res.status(500).send(error.message);
27
28
  }
28
29
  });
package/lib/ord.js CHANGED
@@ -101,7 +101,7 @@ function _triageCsnDefinitions(csn) {
101
101
  const result = _handleEntity(definitionKey, definitionObj);
102
102
  if (result) {
103
103
  if (result.apiEndpoint) apiEndpoints.add(result.apiEndpoint);
104
- if (result.entityTypeTarget) entityTypeTargets.push(result.entityTypeTarget);
104
+ if (result.entityTypeTarget) entityTypeTargets.push(...result.entityTypeTarget);
105
105
  }
106
106
  break;
107
107
  }
@@ -177,10 +177,12 @@ function _isValidService(key, definition) {
177
177
  function _handleEntity(key, keyDefinition) {
178
178
  if (!key.includes(".texts") && _shouldNotSkipIfServiceProtocolIsNone(keyDefinition)) {
179
179
  const apiEndpoint = key;
180
- const entityTypeTarget =
181
- keyDefinition[ORD_ODM_ENTITY_NAME_ANNOTATION] || keyDefinition[ENTITY_RELATIONSHIP_ANNOTATION]
182
- ? createEntityTypeMappingsItemTemplate(keyDefinition)
183
- : null;
180
+ let entityTypeTarget = null;
181
+ if (keyDefinition[ORD_ODM_ENTITY_NAME_ANNOTATION] || keyDefinition[ENTITY_RELATIONSHIP_ANNOTATION]) {
182
+ const mapping = createEntityTypeMappingsItemTemplate(keyDefinition);
183
+ if (Array.isArray(mapping)) entityTypeTarget = mapping;
184
+ else if (mapping) entityTypeTarget = [mapping];
185
+ }
184
186
  return { apiEndpoint, entityTypeTarget };
185
187
  }
186
188
  return null;
package/lib/templates.js CHANGED
@@ -5,6 +5,7 @@ const _ = require("lodash");
5
5
  const {
6
6
  AUTHENTICATION_TYPE,
7
7
  DATA_PRODUCT_ANNOTATION,
8
+ DATA_PRODUCT_SIMPLE_ANNOTATION,
8
9
  DATA_PRODUCT_TYPE,
9
10
  DESCRIPTION_PREFIX,
10
11
  ENTITY_RELATIONSHIP_ANNOTATION,
@@ -79,24 +80,28 @@ const _generatePaths = (srv, srvDefinition) => {
79
80
  * @returns {Object} An entry of the entityTypeMappings array.
80
81
  */
81
82
  const createEntityTypeMappingsItemTemplate = (entity) => {
83
+ const results = [];
82
84
  if (entity[ORD_ODM_ENTITY_NAME_ANNOTATION]) {
83
- return {
85
+ results.push({
84
86
  ordId: `sap.odm:entityType:${entity[ORD_ODM_ENTITY_NAME_ANNOTATION]}:v1`,
85
87
  entityName: entity[ORD_ODM_ENTITY_NAME_ANNOTATION],
86
88
  isODMMapping: true,
87
89
  ...entity,
88
- };
89
- } else if (entity[ENTITY_RELATIONSHIP_ANNOTATION]) {
90
+ });
91
+ }
92
+ if (entity[ENTITY_RELATIONSHIP_ANNOTATION]) {
90
93
  const ordIdParts = entity[ENTITY_RELATIONSHIP_ANNOTATION].split(":");
91
94
  const namespace = ordIdParts[0];
92
95
  const entityName = ordIdParts[1];
93
96
  const version = ordIdParts[2] || "v1";
94
- return {
97
+ results.push({
95
98
  ordId: `${namespace}:entityType:${entityName}:${version}`,
96
99
  entityName,
97
100
  ...entity,
98
- };
101
+ });
99
102
  }
103
+ if (results.length === 0) return;
104
+ return results;
100
105
  };
101
106
 
102
107
  function _getGroupID(serviceDefinition, groupTypeId = defaults.groupTypeId, appConfig) {
@@ -308,7 +313,34 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
308
313
 
309
314
  const paths = _generatePaths(serviceName, serviceDefinition);
310
315
  const apiResources = [];
311
- const ordId = `${appConfig.ordNamespace}:apiResource:${_getGroupNameWithNestedNamespace(serviceDefinition, appConfig)}:v1`;
316
+
317
+ // Handle version suffix extraction for primary data product services
318
+ let cleanServiceName,
319
+ version,
320
+ semanticVersion,
321
+ extracted = null;
322
+ if (isPrimaryDataProductService(serviceDefinition)) {
323
+ extracted = _extractVersionFromServiceName(serviceDefinition.name);
324
+ if (extracted) {
325
+ // Create a temporary service definition with the clean name for namespace processing
326
+ const cleanServiceDefinition = { ...serviceDefinition, name: extracted.cleanName };
327
+ cleanServiceName = _getGroupNameWithNestedNamespace(cleanServiceDefinition, appConfig);
328
+ version = extracted.version;
329
+ semanticVersion = extracted.semanticVersion;
330
+ } else {
331
+ // Invalid pattern - use current behavior
332
+ cleanServiceName = _getGroupNameWithNestedNamespace(serviceDefinition, appConfig);
333
+ version = "v1";
334
+ semanticVersion = "1.0.0";
335
+ }
336
+ } else {
337
+ // Non-data product - use current behavior
338
+ cleanServiceName = _getGroupNameWithNestedNamespace(serviceDefinition, appConfig);
339
+ version = "v1";
340
+ semanticVersion = "1.0.0";
341
+ }
342
+
343
+ const ordId = `${appConfig.ordNamespace}:apiResource:${cleanServiceName}:${version}`;
312
344
 
313
345
  paths.forEach((generatedPath) => {
314
346
  let resourceDefinitions = [
@@ -330,7 +362,7 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
330
362
  title: serviceDefinition["@title"] ?? serviceDefinition["@Common.Label"] ?? serviceName,
331
363
  shortDescription: SHORT_DESCRIPTION_PREFIX + serviceName,
332
364
  description: serviceDefinition["@Core.Description"] ?? DESCRIPTION_PREFIX + serviceName,
333
- version: "1.0.0",
365
+ version: semanticVersion,
334
366
  lastUpdate: appConfig.lastUpdate,
335
367
  visibility,
336
368
  partOfPackage: packageId,
@@ -352,6 +384,10 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
352
384
  obj.direction = "outbound";
353
385
  obj.implementationStandard = "sap.dp:data-subscription-api:v1";
354
386
  obj.entryPoints = [];
387
+ if (extracted) {
388
+ // Overwrite partOfGroups
389
+ obj.partOfGroups = [`${defaults.groupTypeId}:${appConfig.ordNamespace}:${cleanServiceName}`];
390
+ }
355
391
  obj.resourceDefinitions = [
356
392
  _getResourceDefinition(
357
393
  "sap-csn-interop-effective-v1",
@@ -425,7 +461,10 @@ const createEventResourceTemplate = (serviceName, serviceDefinition, appConfig,
425
461
  };
426
462
 
427
463
  function isPrimaryDataProductService(serviceDefinition) {
428
- return serviceDefinition[DATA_PRODUCT_ANNOTATION] === DATA_PRODUCT_TYPE.primary;
464
+ return (
465
+ serviceDefinition[DATA_PRODUCT_ANNOTATION] === DATA_PRODUCT_TYPE.primary ||
466
+ !!serviceDefinition[DATA_PRODUCT_SIMPLE_ANNOTATION]
467
+ );
429
468
  }
430
469
 
431
470
  function _getEntityTypeMappings(definitionObj) {
@@ -433,7 +472,9 @@ function _getEntityTypeMappings(definitionObj) {
433
472
  return;
434
473
  }
435
474
  const entities = Object.values(definitionObj.entities).flatMap((entity) => {
436
- const entityData = _flattenEntityGraph(entity).map(createEntityTypeMappingsItemTemplate);
475
+ const entityData = _flattenEntityGraph(entity)
476
+ .flatMap(createEntityTypeMappingsItemTemplate) // now returns arrays
477
+ .filter(Boolean);
437
478
  return _.uniqBy(entityData, CONTENT_MERGE_KEY);
438
479
  });
439
480
  const entityTypeTargets = _.uniqBy(entities, CONTENT_MERGE_KEY)
@@ -453,7 +494,7 @@ function _getExposedEntityTypes(definitionObj) {
453
494
  return;
454
495
  }
455
496
  const entities = Object.values(definitionObj.entities).flatMap((entity) => {
456
- const entityData = _flattenEntityGraph(entity).map(createEntityTypeMappingsItemTemplate);
497
+ const entityData = _flattenEntityGraph(entity).flatMap(createEntityTypeMappingsItemTemplate).filter(Boolean);
457
498
  return _.uniqBy(entityData, CONTENT_MERGE_KEY);
458
499
  });
459
500
  const exposedEntityTypes = _.uniqBy(entities, CONTENT_MERGE_KEY)
@@ -496,6 +537,32 @@ function _flattenEntityGraph(currentEntity, processedEntities = []) {
496
537
  return [currentEntity, ...assertionsTodo.flatMap((entity) => _flattenEntityGraph(entity, processedEntities))];
497
538
  }
498
539
 
540
+ /**
541
+ * Extracts version suffix from service name for data product services.
542
+ * Only accepts pattern: .v<number> (e.g., .v0, .v1, .v2, .v10)
543
+ * Rejects patterns like: .v1.1, .v1.0, .version1, .beta
544
+ *
545
+ * @param {string} serviceName The full service name
546
+ * @returns {Object|null} Object with cleanName, version, and semanticVersion, or null if invalid pattern
547
+ */
548
+ function _extractVersionFromServiceName(serviceName) {
549
+ // Only match pattern: .v<number> (where number is 1 or more digits)
550
+ const versionPattern = /\.v(\d+)$/;
551
+ const match = serviceName.match(versionPattern);
552
+
553
+ if (!match) {
554
+ return null; // No valid version suffix found
555
+ }
556
+
557
+ const versionNumber = parseInt(match[1], 10);
558
+
559
+ return {
560
+ cleanName: serviceName.replace(versionPattern, ""),
561
+ version: `v${versionNumber}`,
562
+ semanticVersion: `${versionNumber}.0.0`,
563
+ };
564
+ }
565
+
499
566
  function _getPackageID(namespace, packageIds, resourceType, visibility = RESOURCE_VISIBILITY.public) {
500
567
  if (!packageIds) return;
501
568
 
@@ -544,4 +611,5 @@ module.exports = {
544
611
  _getExposedEntityTypes,
545
612
  _propagateORDVisibility,
546
613
  _handleVisibility,
614
+ isPrimaryDataProductService,
547
615
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/ord",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "description": "CAP Plugin for generating ORD document.",
5
5
  "repository": "cap-js/ord",
6
6
  "author": "SAP SE (https://www.sap.com)",
@@ -25,8 +25,8 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "eslint": "^9.2.0",
28
- "jest": "^29.7.0",
29
- "prettier": "3.5.3"
28
+ "jest": "^30.0.0",
29
+ "prettier": "3.6.2"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@sap/cds": ">=8.9.4",