@cap-js/ord 1.8.0 → 1.9.1

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.
@@ -1,36 +1,36 @@
1
1
  const path = require("path");
2
2
  const cds = require("@sap/cds");
3
- const _ = require("lodash");
4
3
  const { readFileSync } = require("fs");
5
4
 
6
5
  const Logger = require("./logger");
7
- const { getRFC3339Date } = require("./date");
8
6
  const defaults = require("./defaults");
9
- const { createEntityTypeMappingsItemTemplate } = require("./templates");
10
- const { CONTENT_MERGE_KEY, CDS_ELEMENT_KIND } = require("./constants");
7
+ const { getRFC3339Date } = require("./common/utils");
8
+ const { createAuthConfig } = require("./auth/authentication");
9
+ const { resolveAccessStrategies } = require("./common/utils");
10
+ const { CDS_ELEMENT_KIND } = require("./constants");
11
11
 
12
12
  module.exports = class Configuration {
13
13
  constructor(csn) {
14
14
  const env = cds.env["ord"];
15
- const packageName = this._loadNameFromPackageJson();
15
+ const authConfig = createAuthConfig();
16
+ const packageName = env?.packageName ?? this._loadNameFromPackageJson();
16
17
  const eventApplicationNamespace = cds.env?.export?.asyncapi?.applicationNamespace;
17
- const ordNamespace = cds.env["ord"]?.namespace || `customer.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
18
- const internalNamespace = env?.internalNamespace;
18
+ const ordNamespace = env?.namespace || `customer.${packageName.replace(/[^a-zA-Z0-9]/g, "")}`;
19
19
 
20
20
  if (eventApplicationNamespace && ordNamespace !== eventApplicationNamespace) {
21
21
  Logger.warn("ORD and AsyncAPI namespaces should be the same.");
22
22
  }
23
23
 
24
+ if (authConfig.error) {
25
+ throw new Error(`Authentication configuration error: ${authConfig.error}`);
26
+ }
27
+
24
28
  this._env = env;
25
29
  this._packageName = packageName;
26
30
  this._ordNamespace = ordNamespace;
27
- this._internalNamespace = internalNamespace;
28
31
  this._lastUpdate = getRFC3339Date();
29
- this._serviceNames = this._resolveServiceNames(csn);
30
- this._existingProductORDId = env?.existingProductORDId;
31
- this._apiResourceNames = this._resolveApiResourceNames(csn);
32
- this._entityTypeTargets = this._resolveEntityTypeTargets(csn);
33
- this._eventServiceNames = this._resolveEventServiceNames(csn);
32
+ this._csn = this._propagateORDVisibility(cds.linked(csn));
33
+ this._accessStrategies = resolveAccessStrategies(authConfig);
34
34
  this._appName = packageName.replace(/^@/, "").replace(/[@/]/g, "-");
35
35
  this._policyLevels = env?.policyLevels || (env?.policyLevel && [env?.policyLevel]) || defaults.policyLevels;
36
36
  }
@@ -39,6 +39,10 @@ module.exports = class Configuration {
39
39
  return this._env;
40
40
  }
41
41
 
42
+ get csn() {
43
+ return this._csn;
44
+ }
45
+
42
46
  get appName() {
43
47
  return this._appName;
44
48
  }
@@ -51,32 +55,20 @@ module.exports = class Configuration {
51
55
  return this._packageName;
52
56
  }
53
57
 
54
- get serviceNames() {
55
- return this._serviceNames;
56
- }
57
-
58
58
  get ordNamespace() {
59
59
  return this._ordNamespace;
60
60
  }
61
61
 
62
- get internalNamespace() {
63
- return this._internalNamespace;
64
- }
65
-
66
62
  get policyLevels() {
67
63
  return this._policyLevels;
68
64
  }
69
65
 
70
- get apiResourceNames() {
71
- return this._apiResourceNames;
66
+ get accessStrategies() {
67
+ return this._accessStrategies;
72
68
  }
73
69
 
74
- get entityTypeTargets() {
75
- return this._entityTypeTargets;
76
- }
77
-
78
- get eventServiceNames() {
79
- return this._eventServiceNames;
70
+ get internalNamespace() {
71
+ return this._env?.internalNamespace;
80
72
  }
81
73
 
82
74
  get hasSAPPolicyLevel() {
@@ -84,12 +76,7 @@ module.exports = class Configuration {
84
76
  }
85
77
 
86
78
  get existingProductORDId() {
87
- return this._existingProductORDId;
88
- }
89
-
90
- _resolveServiceNames(csn) {
91
- return Object.keys(csn.definitions) //
92
- .filter((name) => this._isValidService(name, csn.definitions[name]));
79
+ return this._env?.existingProductORDId;
93
80
  }
94
81
 
95
82
  _loadNameFromPackageJson() {
@@ -102,60 +89,18 @@ module.exports = class Configuration {
102
89
  return JSON.parse(readFileSync(packageJsonPath, "utf-8")).name;
103
90
  }
104
91
 
105
- _isBlockedServiceName(name) {
106
- return ["cds.xt.MTXServices", "MtxOrdProviderService", "OpenResourceDiscoveryService"] //
107
- .some((blocked) => name.includes(blocked));
108
- }
109
-
110
- _resolveApiResourceNames(csn) {
111
- return Object.keys(csn.definitions)
112
- .filter((name) => this._isValidService(name, csn.definitions[name]))
113
- .filter((name) => !this._serviceOnlyContainsEvents(csn.definitions[name]));
114
- }
92
+ _propagateORDVisibility(csn) {
93
+ const annotation = "@ORD.Extensions.visibility";
115
94
 
116
- _resolveEntityTypeTargets(csn) {
117
- return _.uniqBy(
118
- Object.keys(csn.definitions)
119
- .filter((name) => !name.includes(".texts"))
120
- .filter((name) => !this._isBlockedServiceName(name))
121
- .filter((name) => csn.definitions[name].kind === CDS_ELEMENT_KIND.entity)
122
- .filter((name) => csn.definitions[name]["_service"]?.["@protocol"] !== "none")
123
- .flatMap((name) => createEntityTypeMappingsItemTemplate(csn.definitions[name]) || []),
124
- CONTENT_MERGE_KEY,
125
- );
126
- }
127
-
128
- _resolveEventServiceNames(csn) {
129
- const serviceNames = this._resolveServiceNames(csn);
130
-
131
- return [
132
- ...new Set(
133
- Object.keys(csn.definitions)
134
- .filter((name) => !this._isBlockedServiceName(name))
135
- .filter((name) => csn.definitions[name].kind === CDS_ELEMENT_KIND.event)
136
- .filter((name) => csn.definitions[name]["_service"]?.["@protocol"] !== "none")
137
- .filter((name) => serviceNames.some((serviceName) => name.startsWith(`${serviceName}.`)))
138
- .map((name) => serviceNames.find((serviceName) => name.startsWith(`${serviceName}.`))),
139
- ),
140
- ];
141
- }
142
-
143
- _isValidService(key, definition) {
144
- const isExternalService = Object.keys(cds).includes("requires") && Object.keys(cds.requires).includes(key);
145
-
146
- return (
147
- definition.kind === CDS_ELEMENT_KIND.service &&
148
- !definition["@cds.external"] &&
149
- definition["@protocol"] !== "none" &&
150
- !isExternalService &&
151
- !this._isBlockedServiceName(key)
152
- );
153
- }
95
+ Object.values(csn.definitions)
96
+ .filter((service) => Boolean(service[annotation]))
97
+ .filter((definition) => definition.kind === CDS_ELEMENT_KIND.service)
98
+ .forEach((service) => {
99
+ Object.values(csn.definitions)
100
+ .filter((definition) => definition.name.startsWith(`${service.name}.`))
101
+ .forEach((definition) => (definition[annotation] ??= service[annotation]));
102
+ });
154
103
 
155
- _serviceOnlyContainsEvents(definition) {
156
- return (
157
- Object.keys(definition.events || {}).length > 0 &&
158
- ["actions", "entities", "functions"].every((key) => Object.keys(definition[key] || {}).length === 0)
159
- );
104
+ return csn;
160
105
  }
161
106
  };
package/lib/constants.js CHANGED
@@ -1,9 +1,3 @@
1
- const AUTHENTICATION_TYPE = Object.freeze({
2
- Open: "open",
3
- Basic: "basic",
4
- CfMtls: "cf-mtls",
5
- });
6
-
7
1
  const BASIC_AUTH_HEADER_KEY = "authorization";
8
2
 
9
3
  const BUILD_DEFAULT_PATH = "gen/ord";
@@ -11,13 +5,8 @@ const BUILD_DEFAULT_PATH = "gen/ord";
11
5
  const ORD_ACCESS_STRATEGY = Object.freeze({
12
6
  Open: "open",
13
7
  Basic: "basic-auth",
14
- CfMtls: "sap:cmp-mtls:v1",
15
- });
16
-
17
- const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({
18
- [AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open,
19
- [AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic,
20
- [AUTHENTICATION_TYPE.CfMtls]: ORD_ACCESS_STRATEGY.CfMtls,
8
+ CmpMtls: "sap:cmp-mtls:v1",
9
+ BahMtls: "sap.businesshub:mtls:v1",
21
10
  });
22
11
 
23
12
  // CF mTLS default header names
@@ -42,15 +31,6 @@ const CDS_ELEMENT_KIND = Object.freeze({
42
31
  type: "type",
43
32
  });
44
33
 
45
- const COMPILER_TYPES = Object.freeze({
46
- oas3: "oas3",
47
- asyncapi2: "asyncapi2",
48
- edmx: "edmx",
49
- csn: "csn",
50
- mcp: "mcp",
51
- graphql: "graphql",
52
- });
53
-
54
34
  const CONTENT_MERGE_KEY = "ordId";
55
35
 
56
36
  const DATA_PRODUCT_ANNOTATION = "@DataIntegration.dataProduct.type";
@@ -116,8 +96,6 @@ const EXTERNAL_DP_ORD_ID_ANNOTATION = "@cds.dp.ordId";
116
96
 
117
97
  const EXTERNAL_SERVICE_ANNOTATION = "@cds.external";
118
98
 
119
- const INTEGRATION_DEPENDENCY_RESOURCE_NAME = "externalDependencies";
120
-
121
99
  // ORD apiProtocol values
122
100
  const ORD_API_PROTOCOL = Object.freeze({
123
101
  ODATA_V4: "odata-v4",
@@ -149,9 +127,6 @@ const ORD_ONLY_PROTOCOLS = Object.freeze({
149
127
  },
150
128
  });
151
129
 
152
- // Protocols that the ORD plugin cannot currently generate definitions for
153
- const PLUGIN_UNSUPPORTED_PROTOCOLS = Object.freeze([]);
154
-
155
130
  // CF mTLS Error Reasons
156
131
  const CF_MTLS_ERROR_REASON = Object.freeze({
157
132
  NO_HEADERS: "NO_HEADERS",
@@ -183,14 +158,11 @@ const DOCUMENT_PERSPECTIVES = Object.freeze({
183
158
  const LOCAL_TENANT_ID_HEADER_KEY = "local-tenant-id";
184
159
 
185
160
  module.exports = {
186
- AUTHENTICATION_TYPE,
187
- AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
188
161
  BASIC_AUTH_HEADER_KEY,
189
162
  BUILD_DEFAULT_PATH,
190
163
  CAP_TO_ORD_PROTOCOL_MAP,
191
164
  CDS_ELEMENT_KIND,
192
165
  CF_MTLS_HEADERS,
193
- COMPILER_TYPES,
194
166
  CONTENT_MERGE_KEY,
195
167
  DATA_PRODUCT_ANNOTATION,
196
168
  DATA_PRODUCT_SIMPLE_ANNOTATION,
@@ -199,7 +171,6 @@ module.exports = {
199
171
  ENTITY_RELATIONSHIP_ANNOTATION,
200
172
  EXTERNAL_DP_ORD_ID_ANNOTATION,
201
173
  EXTERNAL_SERVICE_ANNOTATION,
202
- INTEGRATION_DEPENDENCY_RESOURCE_NAME,
203
174
  LEVEL,
204
175
  MCP_RESOURCE_DEFINITION_TYPE,
205
176
  OPENAPI_SERVERS_ANNOTATION,
@@ -212,7 +183,6 @@ module.exports = {
212
183
  ORD_ONLY_PROTOCOLS,
213
184
  ORD_RESOURCE_TYPE,
214
185
  ORD_SERVICE_NAME,
215
- PLUGIN_UNSUPPORTED_PROTOCOLS,
216
186
  RESOURCE_VISIBILITY,
217
187
  ALLOWED_VISIBILITY,
218
188
  IMPLEMENTATIONSTANDARD_VERSIONS,
package/lib/defaults.js CHANGED
@@ -1,75 +1,12 @@
1
1
  const fs = require("fs");
2
- const _ = require("lodash");
3
2
  const cds = require("@sap/cds");
4
3
  const { join } = require("path");
5
4
 
6
- const {
7
- ORD_RESOURCE_TYPE,
8
- SHORT_DESCRIPTION_PREFIX,
9
- RESOURCE_VISIBILITY,
10
- DOCUMENT_PERSPECTIVES,
11
- CONTENT_MERGE_KEY,
12
- } = require("./constants");
5
+ const { slice } = require("./common/slice");
6
+ const { DOCUMENT_PERSPECTIVES } = require("./constants");
7
+ const { resolveAccessStrategies } = require("./common/utils");
13
8
 
14
- const stringTypeCheck = (value) => typeof value === "string";
15
- const arrayTypeCheck = (value) => Array.isArray(value);
16
-
17
- // see ref: https://pages.github.tools.sap/CentralEngineering/open-resource-discovery-specification/spec-v1/interfaces/document#package
18
- const packageTypeChecks = {
19
- policyLevels: arrayTypeCheck,
20
- packageLinks: arrayTypeCheck,
21
- links: arrayTypeCheck,
22
- licenseType: stringTypeCheck,
23
- supportInfo: stringTypeCheck,
24
- vendor: stringTypeCheck,
25
- countries: arrayTypeCheck,
26
- lineOfBusiness: arrayTypeCheck,
27
- industry: arrayTypeCheck,
28
- runtimeRestriction: stringTypeCheck,
29
- tags: arrayTypeCheck,
30
- labels: arrayTypeCheck,
31
- documentationLabels: arrayTypeCheck,
32
- };
33
-
34
- const regexWithRemoval = (name) => {
35
- return name?.replace(/[^a-zA-Z0-9]/g, "");
36
- };
37
-
38
- const nameWithDot = (name) => {
39
- return regexWithRemoval(name.charAt(0)) + name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g, ".");
40
- };
41
-
42
- const nameWithSpaces = (name) => {
43
- return regexWithRemoval(name.charAt(0)) + name.slice(1, name.length).replace(/[^a-zA-Z0-9]/g, " ");
44
- };
45
-
46
- const defaultProductOrdId = (name) => `customer:product:${nameWithDot(name)}:`;
47
-
48
- const generatePackageDescriptions = (name, type, visibility) => ({
49
- shortDescription: `Package containing ${visibility} ${type}`,
50
- description: `This package contains ${visibility} ${type} for ${nameWithSpaces(name)}.`,
51
- });
52
-
53
- const generateUniquePackageOrdId = (ordNamespace, name, tag, visibility) => {
54
- const visibilitySuffix = visibility === RESOURCE_VISIBILITY.public ? "" : `-${visibility}`;
55
- return `${ordNamespace}:package:${regexWithRemoval(name)}${tag}${visibilitySuffix}:v1`;
56
- };
57
-
58
- const mapEnvPackageInfo = (packageConfig) => {
59
- if (!packageConfig) {
60
- return { vendor: undefined };
61
- }
62
-
63
- const filteredEnv = {};
64
- for (const key in packageConfig) {
65
- const value = packageConfig[key];
66
- const checkFunction = packageTypeChecks[key];
67
- if (value && checkFunction && checkFunction(value)) {
68
- filteredEnv[key] = value;
69
- }
70
- }
71
- return filteredEnv;
72
- };
9
+ const sizeLimit = 2000000; // 2mb
73
10
 
74
11
  /**
75
12
  * Module containing default configuration for ORD Document.
@@ -77,75 +14,37 @@ const mapEnvPackageInfo = (packageConfig) => {
77
14
  */
78
15
  module.exports = {
79
16
  $schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
80
- openResourceDiscovery: "1.14",
17
+ openResourceDiscovery: "1.16",
81
18
  policyLevels: ["none"],
82
19
  groupTypeId: "sap.cds:service",
83
20
  description: "this is an application description",
84
- products: (name) => [
85
- {
86
- ordId: defaultProductOrdId(name),
87
- title: nameWithSpaces(name),
88
- shortDescription: SHORT_DESCRIPTION_PREFIX + nameWithSpaces(name),
89
- vendor: "customer:vendor:Customer:",
90
- },
91
- ],
92
- packages: (appConfig, products) => {
93
- const name = appConfig.appName;
94
- const ordNamespace = appConfig.ordNamespace;
95
- const productsOrdId = appConfig.existingProductORDId || products?.[0]?.ordId;
96
- const { vendor, ...envValues } = mapEnvPackageInfo(appConfig?.env?.packages?.[0]);
97
- const visibilities = !appConfig.hasSAPPolicyLevel
98
- ? [RESOURCE_VISIBILITY.public]
99
- : Object.values(RESOURCE_VISIBILITY);
100
- const packageTypes = !appConfig.hasSAPPolicyLevel
101
- ? [{ tag: "", type: "General" }]
102
- : [
103
- { tag: `-${ORD_RESOURCE_TYPE.api}`, type: "APIs" },
104
- { tag: `-${ORD_RESOURCE_TYPE.event}`, type: "Events" },
105
- { tag: `-${ORD_RESOURCE_TYPE.entityType}`, type: "Entity Types" },
106
- { tag: `-${ORD_RESOURCE_TYPE.dataProduct}`, type: "Data Products" },
107
- { tag: `-${ORD_RESOURCE_TYPE.integrationDependency}`, type: "Integration Dependencies" },
108
- ];
109
-
110
- return _.uniqBy(
111
- packageTypes.flatMap(({ tag, type }) =>
112
- visibilities.map((visibility) => ({
113
- ordId: generateUniquePackageOrdId(ordNamespace, name, tag, visibility),
114
- title: nameWithSpaces(name),
115
- ...generatePackageDescriptions(name, type, visibility),
116
- version: "1.0.0",
117
- ...(productsOrdId && { partOfProducts: [productsOrdId || defaultProductOrdId(name)] }),
118
- vendor: vendor || "customer:vendor:Customer:",
119
- ...envValues,
120
- })),
121
- ),
122
- CONTENT_MERGE_KEY,
123
- );
124
- },
125
- baseTemplate: (authConfig, tenant) => {
126
- // Get access strategies from the provided authConfig
127
- // If auth config is not available, fall back to empty array
128
- const toggles = cds.env.requires.toggles;
129
- const extensibility = cds.env.requires.extensibility;
130
- const accessStrategies = authConfig?.accessStrategies || [];
21
+ sizeLimit: sizeLimit,
22
+ baseTemplate: (authConfig, document, tenantDocument) => {
23
+ const accessStrategies = resolveAccessStrategies(authConfig);
131
24
 
132
25
  return {
133
26
  openResourceDiscoveryV1: {
134
27
  documents: [
135
- {
136
- url: "/ord/v1/documents/ord-document",
137
- perspective: DOCUMENT_PERSPECTIVES.SystemVersion,
138
- accessStrategies,
139
- },
140
- ...(!tenant || !(toggles || extensibility)
28
+ ...Array(slice(document, sizeLimit).length)
29
+ .fill(0)
30
+ .map((_, i) => {
31
+ return {
32
+ url: `/ord/v1/documents/ord-document?part=${i}`,
33
+ perspective: DOCUMENT_PERSPECTIVES.SystemVersion,
34
+ accessStrategies,
35
+ };
36
+ }),
37
+ ...(!tenantDocument
141
38
  ? []
142
- : [
143
- {
144
- url: `/ord/v1/documents/ord-document?perspective=${encodeURIComponent(DOCUMENT_PERSPECTIVES.SystemInstance)}`,
145
- perspective: DOCUMENT_PERSPECTIVES.SystemInstance,
146
- accessStrategies,
147
- },
148
- ]),
39
+ : Array(slice(tenantDocument, sizeLimit).length)
40
+ .fill(0)
41
+ .map((_, i) => {
42
+ return {
43
+ url: `/ord/v1/documents/ord-document?part=${i}&perspective=${encodeURIComponent(DOCUMENT_PERSPECTIVES.SystemInstance)}`,
44
+ perspective: DOCUMENT_PERSPECTIVES.SystemInstance,
45
+ accessStrategies,
46
+ };
47
+ })),
149
48
  ],
150
49
  },
151
50
  };
@@ -1,68 +1,99 @@
1
- const { CONTENT_MERGE_KEY } = require("./constants");
2
1
  const cds = require("@sap/cds");
3
2
  const fs = require("fs");
4
3
  const Logger = require("./logger");
5
4
  const path = require("path");
6
5
  const _ = require("lodash");
7
6
 
8
- function cleanNullProperties(obj) {
9
- for (const key in obj) {
10
- if (obj[key] === null) {
11
- delete obj[key];
12
- } else if (typeof obj[key] === "object") {
13
- cleanNullProperties(obj[key]);
14
- }
15
- }
16
- return obj;
17
- }
18
-
19
- function patchGeneratedOrdResources(destinationObj, sourceObj) {
20
- const destObj = _.keyBy(destinationObj, CONTENT_MERGE_KEY);
21
- const srcObj = _.keyBy(sourceObj, CONTENT_MERGE_KEY);
7
+ const SCALAR_TYPES = Object.freeze(["number", "string", "boolean"]);
22
8
 
23
- for (const ordId in srcObj) {
24
- destObj[ordId] =
25
- ordId in destObj
26
- ? _.assignWith(structuredClone(destObj[ordId]), structuredClone(srcObj[ordId]))
27
- : srcObj[ordId];
28
- }
9
+ const MERGE_STRATEGIES = Object.freeze({
10
+ // Directly replace scalar values and scalar value arrays
11
+ $schema: (current, custom) => custom,
12
+ baseUrl: (current, custom) => custom,
13
+ description: (current, custom) => custom,
14
+ perspective: (current, custom) => custom,
15
+ policyLevel: (current, custom) => custom,
16
+ customPolicyLevel: (current, custom) => custom,
17
+ policyLevels: (current, custom) => prune(custom),
18
+ openResourceDiscovery: (current, custom) => custom,
29
19
 
30
- return cleanNullProperties(Object.values(destObj));
31
- }
20
+ // Perform simple merge for objects
21
+ describedSystemType: (current, custom) => prune(_.assign(current, custom)),
22
+ describedSystemVersion: (current, custom) => prune(_.assign(current, custom)),
23
+ describedSystemInstance: (current, custom) => prune(_.assign(current, custom)),
32
24
 
33
- function compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent) {
34
- const clonedOrdContent = structuredClone(ordContent);
35
- for (const key in customORDContent) {
36
- const propertyType = typeof customORDContent[key];
37
- if (propertyType !== "object" && propertyType !== "array") {
38
- Logger.warn(
39
- "Found ord top level primitive ord property in customOrdFile:",
40
- key,
41
- ". Please define it in .cdsrc.json.",
42
- );
25
+ // Perform smart merge for arrays of objects
26
+ agents: (current, custom) => merge(current, custom, ["ordId"]),
27
+ vendors: (current, custom) => merge(current, custom, ["ordId"]),
28
+ overlays: (current, custom) => merge(current, custom, ["ordId"]),
29
+ products: (current, custom) => merge(current, custom, ["ordId"]),
30
+ packages: (current, custom) => merge(current, custom, ["ordId"]),
31
+ groups: (current, custom) => merge(current, custom, ["groupId"]),
32
+ entityTypes: (current, custom) => merge(current, custom, ["ordId"]),
33
+ apiResources: (current, custom) => merge(current, custom, ["ordId"]),
34
+ capabilities: (current, custom) => merge(current, custom, ["ordId"]),
35
+ dataProducts: (current, custom) => merge(current, custom, ["ordId"]),
36
+ eventResources: (current, custom) => merge(current, custom, ["ordId"]),
37
+ groupTypes: (current, custom) => merge(current, custom, ["groupTypeId"]),
38
+ consumptionBundles: (current, custom) => merge(current, custom, ["ordId"]),
39
+ integrationDependencies: (current, custom) => merge(current, custom, ["ordId"]),
40
+ tombstones: (current, custom) => merge(current, custom, ["ordId", "groupId", "groupTypeId"]),
41
+ });
43
42
 
44
- continue;
45
- }
46
- if (key in ordContent) {
47
- clonedOrdContent[key] = patchGeneratedOrdResources(clonedOrdContent[key], customORDContent[key]);
48
- } else {
49
- clonedOrdContent[key] = customORDContent[key];
50
- }
43
+ function prune(entity) {
44
+ if (Array.isArray(entity)) {
45
+ return entity
46
+ .filter((value) => ![null, undefined].includes(value))
47
+ .map((value) => (SCALAR_TYPES.includes(typeof value) ? value : prune(value)));
51
48
  }
52
- return clonedOrdContent;
49
+
50
+ return Object.fromEntries(
51
+ Object.entries(entity)
52
+ .filter(([, value]) => ![null, undefined].includes(value))
53
+ .map(([key, value]) => [key, SCALAR_TYPES.includes(typeof value) ? value : prune(value)]),
54
+ );
53
55
  }
54
56
 
55
- function getCustomORDContent(appConfig) {
56
- if (!appConfig.env?.customOrdContentFile) return;
57
- const pathToCustomORDContent = path.join(cds.root, appConfig.env?.customOrdContentFile);
58
- if (!fs.existsSync(pathToCustomORDContent)) {
59
- Logger.error("Custom ORD content file not found at", pathToCustomORDContent);
60
- return;
61
- }
62
- return require(pathToCustomORDContent);
57
+ function merge(target, source, keys) {
58
+ const iteratee = (entity) => keys.map((key) => entity[key] || "").join(".");
59
+ const sources = _.keyBy(source || [], iteratee);
60
+ const targets = _.keyBy(target || [], iteratee);
61
+
62
+ return Array.from(new Set([...Object.keys(targets), ...Object.keys(sources)])) //
63
+ .map((key) => _.assign(structuredClone(targets[key]), structuredClone(sources[key])))
64
+ .map(prune);
63
65
  }
64
66
 
65
67
  module.exports = {
66
- getCustomORDContent,
67
- compareAndHandleCustomORDContentWithExistingContent,
68
+ MERGE_STRATEGIES,
69
+ getCustomORDContent: (configuration) => {
70
+ if (!configuration.env?.customOrdContentFile) return {};
71
+
72
+ const file = path.join(cds.root, configuration.env?.customOrdContentFile);
73
+ if (!fs.existsSync(file)) {
74
+ Logger.error("Custom ORD content file not found at", file);
75
+ return {};
76
+ }
77
+
78
+ return JSON.parse(fs.readFileSync(file, "utf8"));
79
+ },
80
+ compareAndHandleCustomORDContentWithExistingContent: (ordContent, customORDContent) => {
81
+ return Object.fromEntries([
82
+ // Process elements found only in 'ordContent'
83
+ ...Object.entries(ordContent)
84
+ .filter(([key]) => !(key in customORDContent))
85
+ .map(([key, value]) => [key, structuredClone(value)]),
86
+
87
+ // Process elements found only in 'customORDContent'
88
+ ...Object.entries(customORDContent)
89
+ .filter(([key]) => !(key in ordContent))
90
+ .map(([key, value]) => [key, structuredClone(value)]),
91
+
92
+ // Process elements found in both 'ordContent' and 'customORDContent'
93
+ ...Object.entries(customORDContent)
94
+ .filter(([key]) => key in ordContent)
95
+ .filter(([, value]) => ![null, undefined].includes(value))
96
+ .map(([key, value]) => [key, MERGE_STRATEGIES[key](structuredClone(ordContent[key]), value)]),
97
+ ]);
98
+ },
68
99
  };