@cap-js/ord 1.7.0 → 1.9.0
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/README.md +10 -5
- package/lib/auth/authentication.js +30 -33
- package/lib/auth/cf-mtls.js +5 -5
- package/lib/build.js +1 -3
- package/lib/common/placeholders.js +6 -0
- package/lib/common/slice.js +72 -0
- package/lib/common/utils.js +190 -0
- package/lib/configuration.js +33 -88
- package/lib/constants.js +2 -32
- package/lib/defaults.js +27 -139
- package/lib/extend-ord-with-custom.js +82 -51
- package/lib/interop-csn.js +23 -11
- package/lib/logger.js +1 -3
- package/lib/meta-data.js +14 -26
- package/lib/ord.js +55 -123
- package/lib/protocol-resolver.js +26 -96
- package/lib/services/ord-service.js +30 -6
- package/lib/templates/api-resource.js +283 -0
- package/lib/templates/entity-type.js +121 -0
- package/lib/templates/event-resource.js +161 -0
- package/lib/templates/group.js +50 -0
- package/lib/templates/integration-dependency.js +98 -0
- package/lib/templates/package.js +91 -0
- package/lib/templates/product.js +57 -0
- package/package.json +7 -5
- package/data/well-known.json +0 -14
- package/lib/access-strategies.js +0 -172
- package/lib/date.js +0 -17
- package/lib/integration-dependency.js +0 -125
- package/lib/templates.js +0 -613
package/lib/interop-csn.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const assert = require("node:assert");
|
|
1
2
|
const localize = require("@sap/cds/lib/i18n/localize");
|
|
2
3
|
|
|
3
4
|
// turn effective CSN into interop CSN
|
|
@@ -20,7 +21,7 @@ function add_i18n_texts(csn) {
|
|
|
20
21
|
// get all texts of the app
|
|
21
22
|
const i18n = [...(localize.bundles4(csn) || [])]
|
|
22
23
|
.filter(([locale]) => !!locale)
|
|
23
|
-
.map(([locale, value]) => [locale.replaceAll(
|
|
24
|
+
.map(([locale, value]) => [locale.replaceAll("_", "-"), value]) // CSN interop uses '-' as separator
|
|
24
25
|
.reduce((all, [locale, value]) => ({ ...all, [locale]: value }), {});
|
|
25
26
|
|
|
26
27
|
// get all i18n keys referenced in the csn
|
|
@@ -119,9 +120,9 @@ function map_annotations(csn) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// delete all @assert.unique annotations
|
|
122
|
-
let assertUniqueAnnos = Object.keys(o).filter(x => x.startsWith(
|
|
123
|
+
let assertUniqueAnnos = Object.keys(o).filter((x) => x.startsWith("@assert.unique"));
|
|
123
124
|
for (let a of assertUniqueAnnos) {
|
|
124
|
-
delete o[a]
|
|
125
|
+
delete o[a];
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
|
@@ -132,7 +133,7 @@ function map_annotations(csn) {
|
|
|
132
133
|
function add_meta_info(csn) {
|
|
133
134
|
if (typeof csn != "object") return csn; // needed to make tests pass
|
|
134
135
|
csn["csnInteropEffective"] = "1.0";
|
|
135
|
-
csn.meta ??= {};
|
|
136
|
+
csn.meta ??= {}; // ensure there is a meta section
|
|
136
137
|
|
|
137
138
|
// Keep only the properties of csn.meta that are relevant for interop:
|
|
138
139
|
// "flavor", "document", "features", and private extension names (starting with "__")
|
|
@@ -143,27 +144,38 @@ function add_meta_info(csn) {
|
|
|
143
144
|
delete csn.meta[k];
|
|
144
145
|
}
|
|
145
146
|
}
|
|
147
|
+
|
|
146
148
|
csn.meta.flavor = "effective";
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
const services = Object.entries(csn.definitions).filter(([, def]) => def.kind === "service");
|
|
149
151
|
if (services.length === 1) {
|
|
152
|
+
const [name, definition] = services[0];
|
|
150
153
|
// assumption: "short" service name contains no dots
|
|
151
|
-
|
|
154
|
+
const segments = name.split(".");
|
|
155
|
+
|
|
152
156
|
let v = "1";
|
|
153
157
|
let srv = segments.pop();
|
|
154
|
-
|
|
158
|
+
const m = srv.match(/^v(\d+)$/);
|
|
155
159
|
if (m) {
|
|
156
160
|
srv = segments.pop();
|
|
157
161
|
v = m[1];
|
|
158
162
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
|
|
164
|
+
assert(
|
|
165
|
+
!definition["@ORD.Extensions.version"] || v === definition["@ORD.Extensions.version"].split(".").shift(),
|
|
166
|
+
`Major version in service name (${v}) does not match major version in @ORD.Extensions.version (${definition["@ORD.Extensions.version"]})`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
Object.assign(csn.meta, {
|
|
170
|
+
__name: srv,
|
|
171
|
+
document: { version: definition["@ORD.Extensions.version"] ?? `${v}.0.0` },
|
|
172
|
+
...(segments.length > 0 && { __namespace: segments.join(".") }),
|
|
173
|
+
});
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
return csn;
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
module.exports = {
|
|
168
|
-
interopCSN
|
|
180
|
+
interopCSN,
|
|
169
181
|
};
|
package/lib/logger.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const cds = require("@sap/cds");
|
|
2
2
|
|
|
3
3
|
// Unified INFO-level logging - simple and consistent across all environments
|
|
4
|
-
|
|
4
|
+
module.exports = cds.log("ord-plugin", {
|
|
5
5
|
level: cds.log?.levels?.INFO,
|
|
6
6
|
});
|
|
7
|
-
|
|
8
|
-
module.exports = Logger;
|
package/lib/meta-data.js
CHANGED
|
@@ -7,18 +7,10 @@ const { compile: asyncapi } = require("@cap-js/asyncapi");
|
|
|
7
7
|
|
|
8
8
|
const Logger = require("./logger");
|
|
9
9
|
const { interopCSN } = require("./interop-csn.js");
|
|
10
|
-
const {
|
|
10
|
+
const { OPENAPI_SERVERS_ANNOTATION } = require("./constants");
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function extractCompilerType(url) {
|
|
17
|
-
return path.basename(url, ".json").split(".").pop();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const compilers = Object.freeze({
|
|
21
|
-
[COMPILER_TYPES.csn]: function (csn, options) {
|
|
12
|
+
const COMPILERS = Object.freeze({
|
|
13
|
+
csn: function (csn, options) {
|
|
22
14
|
return {
|
|
23
15
|
contentType: "application/json",
|
|
24
16
|
response: interopCSN(
|
|
@@ -26,13 +18,13 @@ const compilers = Object.freeze({
|
|
|
26
18
|
),
|
|
27
19
|
};
|
|
28
20
|
},
|
|
29
|
-
|
|
21
|
+
mcp: async function (csn, options) {
|
|
30
22
|
return {
|
|
31
23
|
contentType: "application/json",
|
|
32
24
|
response: cds.compile(csn).to["mcp"]({ ...options, ...(cds.env.ord?.compileOptions?.mcp || {}) }),
|
|
33
25
|
};
|
|
34
26
|
},
|
|
35
|
-
|
|
27
|
+
oas3: function (csn, options) {
|
|
36
28
|
// Check for service-level @OpenAPI.servers annotation
|
|
37
29
|
const servers = csn?.definitions?.[options.service]?.[OPENAPI_SERVERS_ANNOTATION];
|
|
38
30
|
const openapiOptions = { ...options, ...(cds.env?.ord?.compileOptions?.openapi || {}) };
|
|
@@ -47,15 +39,13 @@ const compilers = Object.freeze({
|
|
|
47
39
|
response: openapi(csn, openapiOptions),
|
|
48
40
|
};
|
|
49
41
|
},
|
|
50
|
-
|
|
42
|
+
edmx: function (csn, options) {
|
|
51
43
|
return {
|
|
52
44
|
contentType: "application/xml",
|
|
53
|
-
response: cds
|
|
54
|
-
.compile(csn)
|
|
55
|
-
.to["edmx"]({ ...options, ...(cds.env?.ord?.compileOptions?.edmx || {}) }),
|
|
45
|
+
response: cds.compile(csn).to["edmx"]({ ...options, ...(cds.env?.ord?.compileOptions?.edmx || {}) }),
|
|
56
46
|
};
|
|
57
47
|
},
|
|
58
|
-
|
|
48
|
+
graphql: function (csn, options) {
|
|
59
49
|
const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
|
|
60
50
|
const { printSchema, lexicographicSortSchema } = require("graphql");
|
|
61
51
|
const srv = new cds.ApplicationService(options.service, cds.linked(csn));
|
|
@@ -65,7 +55,7 @@ const compilers = Object.freeze({
|
|
|
65
55
|
response: printSchema(lexicographicSortSchema(generateSchema4({ [options.service]: srv }))),
|
|
66
56
|
};
|
|
67
57
|
},
|
|
68
|
-
|
|
58
|
+
asyncapi2: function (csn, options) {
|
|
69
59
|
return {
|
|
70
60
|
contentType: "application/json",
|
|
71
61
|
response: asyncapi(csn, { ...options, ...(cds.env?.ord?.compileOptions?.asyncapi || {}) }),
|
|
@@ -74,16 +64,14 @@ const compilers = Object.freeze({
|
|
|
74
64
|
});
|
|
75
65
|
|
|
76
66
|
module.exports = async (url, model = null) => {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const csn = model || cds.services[name]?.model;
|
|
80
|
-
const options = { service: name, as: "str", messages: [] };
|
|
67
|
+
const type = path.basename(url, ".json").split(".").pop();
|
|
68
|
+
const name = path.basename(url, ".json").split(".").slice(0, -1).join(".");
|
|
81
69
|
|
|
82
|
-
assert(Object.hasOwn(
|
|
70
|
+
assert(Object.hasOwn(COMPILERS, type), `Unsupported format: ${type}`);
|
|
83
71
|
|
|
84
72
|
try {
|
|
85
|
-
return await
|
|
86
|
-
} catch(error) {
|
|
73
|
+
return await COMPILERS[type](model ?? cds.services[name]?.model, { service: name, as: "str", messages: [] });
|
|
74
|
+
} catch (error) {
|
|
87
75
|
Logger.error(`Compilation failed for service ${name} (compiler: ${type}) - ${error.message}`);
|
|
88
76
|
throw error;
|
|
89
77
|
}
|
package/lib/ord.js
CHANGED
|
@@ -1,135 +1,67 @@
|
|
|
1
|
-
const _ = require("lodash");
|
|
2
|
-
const cds = require("@sap/cds");
|
|
3
|
-
|
|
4
|
-
const Logger = require("./logger");
|
|
5
1
|
const defaults = require("./defaults");
|
|
6
|
-
const
|
|
2
|
+
const utils = require("./common/utils");
|
|
7
3
|
const Configuration = require("./configuration");
|
|
8
|
-
const {
|
|
4
|
+
const { createGroups } = require("./templates/group");
|
|
5
|
+
const { createPackages } = require("./templates/package");
|
|
6
|
+
const { createProducts } = require("./templates/product");
|
|
7
|
+
const { createEntityTypes } = require("./templates/entity-type");
|
|
8
|
+
const { createAPIResources } = require("./templates/api-resource");
|
|
9
|
+
const { createEventResources } = require("./templates/event-resource");
|
|
10
|
+
const { createIntegrationDependencies } = require("./templates/integration-dependency");
|
|
9
11
|
const {
|
|
10
12
|
getCustomORDContent,
|
|
11
13
|
compareAndHandleCustomORDContentWithExistingContent,
|
|
12
14
|
} = require("./extend-ord-with-custom");
|
|
13
|
-
const {
|
|
14
|
-
createAPIResourceTemplate,
|
|
15
|
-
createEntityTypeTemplate,
|
|
16
|
-
createEventResourceTemplate,
|
|
17
|
-
createGroupsTemplateForService,
|
|
18
|
-
_propagateORDVisibility,
|
|
19
|
-
} = require("./templates");
|
|
20
|
-
|
|
21
|
-
const _getGroups = (csn, appConfig) => {
|
|
22
|
-
return appConfig.serviceNames
|
|
23
|
-
.flatMap((serviceName) => createGroupsTemplateForService(serviceName, csn.definitions[serviceName], appConfig))
|
|
24
|
-
.filter((resource) => !!resource);
|
|
25
|
-
};
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
16
|
+
function prune(document) {
|
|
17
|
+
const usedPackageIds = new Set(
|
|
18
|
+
document.packages?.length === 0
|
|
19
|
+
? []
|
|
20
|
+
: [
|
|
21
|
+
...(document.entityTypes?.map((et) => et.partOfPackage) ?? []),
|
|
22
|
+
...(document.dataProducts?.map((dp) => dp.partOfPackage) ?? []),
|
|
23
|
+
...(document.apiResources?.map((ar) => ar.partOfPackage) ?? []),
|
|
24
|
+
...(document.eventResources?.map((er) => er.partOfPackage) ?? []),
|
|
25
|
+
...(document.integrationDependencies?.map((id) => id.partOfPackage) ?? []),
|
|
26
|
+
],
|
|
36
27
|
);
|
|
37
|
-
};
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
return utils.prune(
|
|
30
|
+
Object.assign(document, {
|
|
31
|
+
// remove unused packages
|
|
32
|
+
packages: document.packages?.filter((pkg) => usedPackageIds.has(pkg.ordId)),
|
|
33
|
+
}),
|
|
42
34
|
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const _getProducts = (appConfig) => {
|
|
46
|
-
const productsObj = defaults.products(appConfig.packageName);
|
|
47
|
-
|
|
48
|
-
if (appConfig.env?.products) {
|
|
49
|
-
const customProducts = appConfig.env.products[0];
|
|
50
|
-
if (customProducts?.ordId?.toLowerCase().startsWith("sap")) {
|
|
51
|
-
Logger.error(
|
|
52
|
-
"Detected sap product ordId, which should not be defined for custom products, use default value instead. Please check ord global registry.",
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
_.assign(productsObj[0], customProducts);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return productsObj;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const _createDefaultORDDocument = (linkedCsn, appConfig, authConfig) => {
|
|
63
|
-
const products = _getProducts(appConfig);
|
|
64
|
-
const packages = defaults.packages(appConfig, products);
|
|
65
|
-
const packageIds = packages?.map((pkg) => pkg.ordId) || [];
|
|
66
|
-
const entityTypes = _getEntityTypes(appConfig, packageIds);
|
|
67
|
-
const integrationDependencies = getIntegrationDependencies(linkedCsn, appConfig, packageIds);
|
|
68
|
-
const apiResources = _getAPIResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
|
|
69
|
-
const eventResources = _getEventResources(linkedCsn, appConfig, packageIds, authConfig.accessStrategies);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
// Unconditionally added top-level properties
|
|
73
|
-
$schema: "https://open-resource-discovery.github.io/specification/spec-v1/interfaces/Document.schema.json",
|
|
74
|
-
policyLevels: appConfig.policyLevels,
|
|
75
|
-
packages: packages,
|
|
76
|
-
description: appConfig.env?.description || defaults.description,
|
|
77
|
-
openResourceDiscovery: appConfig.env?.openResourceDiscovery || defaults.openResourceDiscovery,
|
|
78
|
-
consumptionBundles: appConfig.env?.consumptionBundles || defaults.consumptionBundles(appConfig),
|
|
79
|
-
|
|
80
|
-
// Conditionally added top-level properties
|
|
81
|
-
...(!entityTypes.length ? {} : { entityTypes: entityTypes }),
|
|
82
|
-
...(!apiResources.length ? {} : { apiResources: apiResources }),
|
|
83
|
-
...(!eventResources.length ? {} : { eventResources: eventResources }),
|
|
84
|
-
...(appConfig.existingProductORDId ? {} : { products: [products[0]] }),
|
|
85
|
-
...(!appConfig.serviceNames.length ? {} : { groups: _getGroups(linkedCsn, appConfig) }),
|
|
86
|
-
...(!integrationDependencies.length ? {} : { integrationDependencies: integrationDependencies }),
|
|
87
|
-
};
|
|
88
|
-
};
|
|
35
|
+
}
|
|
89
36
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return authConfig;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
module.exports = (csn, extensions = []) => {
|
|
124
|
-
const authConfig = _createAuthConfig();
|
|
125
|
-
const linkedCsn = _propagateORDVisibility(cds.linked(csn));
|
|
126
|
-
const appConfig = new Configuration(linkedCsn);
|
|
127
|
-
const ordDocument = [...(extensions || []), getCustomORDContent(appConfig)]
|
|
128
|
-
.filter((extension) => !!extension)
|
|
129
|
-
.reduce(
|
|
130
|
-
(document, extension) => compareAndHandleCustomORDContentWithExistingContent(document, extension),
|
|
131
|
-
_createDefaultORDDocument(linkedCsn, appConfig, authConfig),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
return Object.assign(ordDocument, { packages: _filterUnusedPackages(ordDocument) });
|
|
37
|
+
function extend(extensions, document) {
|
|
38
|
+
return extensions.reduce(
|
|
39
|
+
(ord, extension) => compareAndHandleCustomORDContentWithExistingContent(ord, extension),
|
|
40
|
+
document,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = (csn, extensions) => {
|
|
45
|
+
const appConfig = new Configuration(csn);
|
|
46
|
+
|
|
47
|
+
return prune(
|
|
48
|
+
extend(
|
|
49
|
+
[...(extensions || []), getCustomORDContent(appConfig)].filter(Boolean), //
|
|
50
|
+
{
|
|
51
|
+
$schema: defaults.$schema,
|
|
52
|
+
groups: createGroups(appConfig),
|
|
53
|
+
baseUrl: appConfig?.env?.baseUrl,
|
|
54
|
+
products: createProducts(appConfig),
|
|
55
|
+
packages: createPackages(appConfig),
|
|
56
|
+
policyLevels: appConfig.policyLevels,
|
|
57
|
+
entityTypes: createEntityTypes(appConfig),
|
|
58
|
+
apiResources: createAPIResources(appConfig),
|
|
59
|
+
eventResources: createEventResources(appConfig),
|
|
60
|
+
consumptionBundles: appConfig.env?.consumptionBundles,
|
|
61
|
+
description: appConfig.env?.description ?? defaults.description,
|
|
62
|
+
integrationDependencies: createIntegrationDependencies(appConfig),
|
|
63
|
+
openResourceDiscovery: appConfig.env?.openResourceDiscovery ?? defaults.openResourceDiscovery,
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
);
|
|
135
67
|
};
|
package/lib/protocol-resolver.js
CHANGED
|
@@ -1,37 +1,8 @@
|
|
|
1
1
|
const cds = require("@sap/cds");
|
|
2
|
-
const {
|
|
3
|
-
CAP_TO_ORD_PROTOCOL_MAP,
|
|
4
|
-
ORD_ONLY_PROTOCOLS,
|
|
5
|
-
ORD_API_PROTOCOL,
|
|
6
|
-
PLUGIN_UNSUPPORTED_PROTOCOLS,
|
|
7
|
-
} = require("./constants");
|
|
8
|
-
const Logger = require("./logger");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Gets CAP endpoints for a service using CDS endpoints4().
|
|
12
|
-
*
|
|
13
|
-
* @param {string} serviceName The service name.
|
|
14
|
-
* @param {Object} srvDefinition The service definition object.
|
|
15
|
-
* @returns {Array} Raw endpoints from CDS.
|
|
16
|
-
*/
|
|
17
|
-
function _getCapEndpoints(serviceName, srvDefinition) {
|
|
18
|
-
const srvObj = { name: serviceName, definition: srvDefinition };
|
|
19
|
-
return cds.service.protocols.endpoints4(srvObj);
|
|
20
|
-
}
|
|
21
2
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* @param {Object} srvDefinition The service definition object.
|
|
26
|
-
* @returns {string[]} Array of protocol names, or empty array if not explicitly set.
|
|
27
|
-
*/
|
|
28
|
-
function _getExplicitProtocol(srvDefinition) {
|
|
29
|
-
const protocol = srvDefinition["@protocol"];
|
|
30
|
-
if (!protocol) {
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
return Array.isArray(protocol) ? protocol : [protocol];
|
|
34
|
-
}
|
|
3
|
+
const Logger = require("./logger");
|
|
4
|
+
const { isPrimaryDataProductService } = require("./common/utils");
|
|
5
|
+
const { CAP_TO_ORD_PROTOCOL_MAP, ORD_ONLY_PROTOCOLS, ORD_API_PROTOCOL } = require("./constants");
|
|
35
6
|
|
|
36
7
|
/**
|
|
37
8
|
* Resolves protocol for ORD API Resource generation.
|
|
@@ -42,97 +13,56 @@ function _getExplicitProtocol(srvDefinition) {
|
|
|
42
13
|
* - Rule B: Only fallback to OData when no explicit protocol
|
|
43
14
|
* - Rule C: Never produce [null] in entryPoints
|
|
44
15
|
*
|
|
45
|
-
* @param {string} serviceName The service name.
|
|
46
16
|
* @param {Object} srvDefinition The service definition object.
|
|
47
|
-
* @param {Object} options Configuration options.
|
|
48
|
-
* @param {Function} options.isPrimaryDataProduct Strategy function to check if service is primary data product.
|
|
49
17
|
* @returns {Array} Array with single {apiProtocol, entryPoints, hasResourceDefinitions} object, or empty array.
|
|
50
18
|
*/
|
|
51
|
-
function resolveApiResourceProtocol(
|
|
52
|
-
const { isPrimaryDataProduct = () => false } = options;
|
|
53
|
-
|
|
19
|
+
function resolveApiResourceProtocol(srvDefinition) {
|
|
54
20
|
// 1. Primary Data Product - early return
|
|
55
|
-
if (
|
|
21
|
+
if (isPrimaryDataProductService(srvDefinition)) {
|
|
56
22
|
return [
|
|
57
23
|
{
|
|
58
|
-
apiProtocol: ORD_API_PROTOCOL.SAP_DATA_SUBSCRIPTION,
|
|
59
24
|
entryPoints: [],
|
|
60
25
|
hasResourceDefinitions: true,
|
|
26
|
+
apiProtocol: ORD_API_PROTOCOL.SAP_DATA_SUBSCRIPTION,
|
|
61
27
|
},
|
|
62
28
|
];
|
|
63
29
|
}
|
|
64
30
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const apiProtocol = CAP_TO_ORD_PROTOCOL_MAP[endpoint.kind] ?? endpoint.kind;
|
|
76
|
-
if (apiProtocol) {
|
|
77
|
-
ordProtocols.push({
|
|
78
|
-
apiProtocol,
|
|
79
|
-
entryPoints: endpoint.path ? [endpoint.path] : [],
|
|
80
|
-
hasResourceDefinitions: true,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
31
|
+
const ordProtocols = cds.service.protocols
|
|
32
|
+
.endpoints4({ name: srvDefinition.name, definition: srvDefinition })
|
|
33
|
+
.filter((endpoint) => Boolean(CAP_TO_ORD_PROTOCOL_MAP[endpoint.kind] ?? endpoint.kind))
|
|
34
|
+
.map((endpoint) => ({
|
|
35
|
+
hasResourceDefinitions: true,
|
|
36
|
+
entryPoints: endpoint.path ? [endpoint.path] : [],
|
|
37
|
+
apiProtocol: CAP_TO_ORD_PROTOCOL_MAP[endpoint.kind] ?? endpoint.kind,
|
|
38
|
+
}));
|
|
84
39
|
|
|
85
|
-
const
|
|
86
|
-
for (const protocol of
|
|
87
|
-
// 2. Handle explicit protocol
|
|
88
|
-
// 2a. Check if it's an ORD-only protocol (e.g., INA)
|
|
40
|
+
const cdsProtocols = Object.keys(cds.service.protocols.for(srvDefinition));
|
|
41
|
+
for (const protocol of cdsProtocols) {
|
|
42
|
+
// 2. Handle explicit protocol - check if it's an ORD-only protocol (e.g., INA)
|
|
89
43
|
if (ORD_ONLY_PROTOCOLS[protocol]) {
|
|
90
|
-
const
|
|
91
|
-
const path =
|
|
44
|
+
const { apiProtocol, hasEntryPoints, hasResourceDefinitions } = ORD_ONLY_PROTOCOLS[protocol];
|
|
45
|
+
const path = hasEntryPoints ? cds.service.protocols.path4(srvDefinition) : null;
|
|
46
|
+
|
|
92
47
|
ordProtocols.push({
|
|
93
|
-
apiProtocol:
|
|
48
|
+
apiProtocol: apiProtocol,
|
|
94
49
|
entryPoints: path ? [path] : [],
|
|
95
|
-
hasResourceDefinitions:
|
|
50
|
+
hasResourceDefinitions: hasResourceDefinitions,
|
|
96
51
|
});
|
|
97
52
|
continue;
|
|
98
53
|
}
|
|
99
54
|
|
|
100
|
-
//
|
|
101
|
-
if (
|
|
55
|
+
// 3. Handle explicit protocol with no CAP endpoint (Rule A)
|
|
56
|
+
if (!ordProtocols.some((p) => p.apiProtocol === protocol) && !CAP_TO_ORD_PROTOCOL_MAP[protocol]) {
|
|
102
57
|
Logger.warn(
|
|
103
|
-
`
|
|
58
|
+
`Unknown protocol '${protocol}' is not supported, and skipped for service '${srvDefinition.name}'.`,
|
|
104
59
|
);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 4. Handle explicit protocol with no CAP endpoint (Rule A)
|
|
109
|
-
if (!ordProtocols.some((p) => p.apiProtocol === protocol) && !CAP_TO_ORD_PROTOCOL_MAP[protocol]) {
|
|
110
|
-
Logger.warn(`Unknown protocol '${protocol}' is not supported, and skipped for service '${serviceName}'.`);
|
|
111
60
|
}
|
|
112
61
|
}
|
|
113
62
|
|
|
114
|
-
|
|
115
|
-
if (ordProtocols.length === 0 && explicit.length > 0) {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (ordProtocols.length > 0) {
|
|
120
|
-
return ordProtocols;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 5. No explicit protocol and no CAP endpoint - fallback to OData (Rule B)
|
|
124
|
-
const path = cds.service.protocols.path4(srvDefinition);
|
|
125
|
-
return [
|
|
126
|
-
{
|
|
127
|
-
apiProtocol: ORD_API_PROTOCOL.ODATA_V4,
|
|
128
|
-
entryPoints: path ? [path] : [],
|
|
129
|
-
hasResourceDefinitions: true,
|
|
130
|
-
},
|
|
131
|
-
];
|
|
63
|
+
return ordProtocols;
|
|
132
64
|
}
|
|
133
65
|
|
|
134
66
|
module.exports = {
|
|
135
67
|
resolveApiResourceProtocol,
|
|
136
|
-
// Exported for testing
|
|
137
|
-
_getExplicitProtocol,
|
|
138
68
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const cds = require("@sap/cds");
|
|
2
2
|
|
|
3
|
+
const { slice } = require("../common/slice");
|
|
3
4
|
const ord = require("../ord.js");
|
|
4
5
|
const Logger = require("../logger.js");
|
|
5
6
|
const defaults = require("../defaults.js");
|
|
6
|
-
const { LOCAL_TENANT_ID_HEADER_KEY, DOCUMENT_PERSPECTIVES } = require("../constants");
|
|
7
7
|
const compileMetadata = require("../meta-data.js");
|
|
8
8
|
const { createAuthConfig, createAuthMiddleware } = require("../auth/authentication.js");
|
|
9
|
+
const { LOCAL_TENANT_ID_HEADER_KEY, DOCUMENT_PERSPECTIVES } = require("../constants");
|
|
9
10
|
|
|
10
11
|
const validationMiddleware = (req, res, next) => {
|
|
11
12
|
const toggles = cds.env.requires.toggles;
|
|
@@ -13,7 +14,10 @@ const validationMiddleware = (req, res, next) => {
|
|
|
13
14
|
const extensibility = cds.env.requires.extensibility;
|
|
14
15
|
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
15
16
|
|
|
16
|
-
if (
|
|
17
|
+
if (
|
|
18
|
+
perspective &&
|
|
19
|
+
![DOCUMENT_PERSPECTIVES.SystemVersion, DOCUMENT_PERSPECTIVES.SystemInstance].includes(perspective)
|
|
20
|
+
) {
|
|
17
21
|
return res.status(400).send(`Required query parameter 'perspective' is invalid`);
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -79,19 +83,39 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
|
79
83
|
const authMiddleware = createAuthMiddleware(authConfig);
|
|
80
84
|
|
|
81
85
|
// Default: /.well-known/open-resource-discovery
|
|
82
|
-
cds.app.get(`${this.path}`, (req, res) => {
|
|
86
|
+
cds.app.get(`${this.path}`, async (req, res) => {
|
|
87
|
+
const toggles = cds.env.requires.toggles;
|
|
88
|
+
const extensibility = cds.env.requires.extensibility;
|
|
83
89
|
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
90
|
+
const extensions = Array.from(Object.values(this.extensions));
|
|
84
91
|
|
|
85
|
-
return res
|
|
92
|
+
return res
|
|
93
|
+
.status(200)
|
|
94
|
+
.send(
|
|
95
|
+
defaults.baseTemplate(
|
|
96
|
+
authConfig,
|
|
97
|
+
ord(cds.model, extensions),
|
|
98
|
+
!tenant || (!toggles && !extensibility)
|
|
99
|
+
? undefined
|
|
100
|
+
: ord(await resolveCdsModel(DOCUMENT_PERSPECTIVES.SystemInstance, tenant), extensions),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
86
103
|
});
|
|
87
104
|
|
|
88
105
|
cds.app.get(`/ord/v1/documents/ord-document`, [authMiddleware, validationMiddleware], async (req, res) => {
|
|
89
|
-
const
|
|
106
|
+
const part = req.query.part || 0;
|
|
90
107
|
const tenant = req.headers[LOCAL_TENANT_ID_HEADER_KEY];
|
|
108
|
+
const perspective = req.query.perspective || DOCUMENT_PERSPECTIVES.SystemVersion;
|
|
91
109
|
const model = await resolveCdsModel(perspective, tenant);
|
|
92
110
|
const extensions = Array.from(Object.values(this.extensions));
|
|
111
|
+
const document = defaults.adjustForPerspective(ord(model, extensions), perspective);
|
|
112
|
+
const slices = slice(document, defaults.sizeLimit);
|
|
113
|
+
|
|
114
|
+
if (isNaN(part) || part < 0 || part >= slices.length) {
|
|
115
|
+
return res.status(400).send(`Required query parameter 'part' is invalid`);
|
|
116
|
+
}
|
|
93
117
|
|
|
94
|
-
return res.status(200).send(
|
|
118
|
+
return res.status(200).send(slices[part]);
|
|
95
119
|
});
|
|
96
120
|
|
|
97
121
|
cds.app.get(`/ord/v1/documents/:id`, [authMiddleware, validationMiddleware], async (_, res) => {
|