@cap-js-community/common 0.3.3 → 0.3.5
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 +16 -0
- package/README.md +16 -5
- package/bin/cdm-build.js +31 -0
- package/package.json +16 -11
- package/src/cdm-build/CDMBuilder.js +496 -0
- package/src/cdm-build/index.js +5 -0
- package/src/index.js +1 -0
- package/src/local-html5-repo/LocalHTML5Repo.js +24 -15
- package/src/migration-check/MigrationCheck.js +8 -2
- package/src/rate-limiting/RateLimiting.js +1 -1
- package/src/rate-limiting/redis/common.js +1 -1
- package/src/redis-client/RedisClient.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## Version 0.3.5 - 2026-02-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Express 5 (with CDS 9.7)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- CDM Builder
|
|
17
|
+
|
|
18
|
+
## Version 0.3.4 - 2026-01-20
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fix journal migration check for draft entities
|
|
23
|
+
|
|
8
24
|
## Version 0.3.3 - 2026-01-07
|
|
9
25
|
|
|
10
26
|
### Added
|
package/README.md
CHANGED
|
@@ -15,11 +15,13 @@ This project provides common functionality for CDS services to be consumed with
|
|
|
15
15
|
|
|
16
16
|
## Table of Contents
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
18
|
+
- **Features:**
|
|
19
|
+
- [Replication Cache](#replication-cache)
|
|
20
|
+
- [Migration Check](#migration-check)
|
|
21
|
+
- [Rate Limiting](#rate-limiting)
|
|
22
|
+
- [Redis Client](#redis-client)
|
|
23
|
+
- [Local HTML5 Repository](#local-html5-repository)
|
|
24
|
+
- [CDM Builder](#cdm-builder)
|
|
23
25
|
- [Support, Feedback, Contributing](#support-feedback-contributing)
|
|
24
26
|
- [Code of Conduct](#code-of-conduct)
|
|
25
27
|
- [Licensing](#licensing)
|
|
@@ -424,6 +426,15 @@ Developing HTML5 apps against hybrid environments including Approuter component
|
|
|
424
426
|
All apps and libraries located in `app` folder and containing an `ui5.yaml` are redirected to local HTML5 repository
|
|
425
427
|
served from local file system. All other requests are proxied to the remote HTML5 repository.
|
|
426
428
|
|
|
429
|
+
## CDM Builder
|
|
430
|
+
|
|
431
|
+
The CDM Builder allows to build a CDM file `cdm.json` from apps, roles and portal content:
|
|
432
|
+
|
|
433
|
+
- Build CDM: `cdm-build`
|
|
434
|
+
|
|
435
|
+
The generated CDM is generated at `app/cdm.json` and can be included into HTML5 Repository automatically
|
|
436
|
+
when copied at `resources/cdm.json` during build time.
|
|
437
|
+
|
|
427
438
|
## Support, Feedback, Contributing
|
|
428
439
|
|
|
429
440
|
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js-community/common/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
package/bin/cdm-build.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-console */
|
|
5
|
+
/* eslint-disable n/no-process-exit */
|
|
6
|
+
|
|
7
|
+
const commander = require("commander");
|
|
8
|
+
const program = new commander.Command();
|
|
9
|
+
|
|
10
|
+
const packageJSON = require("../package.json");
|
|
11
|
+
|
|
12
|
+
const { CDMBuilder } = require("../src/cdm-build");
|
|
13
|
+
|
|
14
|
+
process.argv = process.argv.map((arg) => {
|
|
15
|
+
return arg.toLowerCase();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
program.version(packageJSON.version, "-v, --version").usage("[options]").option("-f, --force", "Force generation");
|
|
19
|
+
|
|
20
|
+
program.unknownOption = function () {};
|
|
21
|
+
program.parse(process.argv);
|
|
22
|
+
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const options = program.opts();
|
|
26
|
+
await new CDMBuilder(options).build();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(err);
|
|
29
|
+
process.exit(-1);
|
|
30
|
+
}
|
|
31
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/common",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "CAP Node.js Community Common",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"engines": {
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
"main": "index.js",
|
|
27
27
|
"types": "index.d.ts",
|
|
28
28
|
"bin": {
|
|
29
|
-
"cdsmc": "bin/cdsmc.js",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
29
|
+
"cdsmc": "./bin/cdsmc.js",
|
|
30
|
+
"migration-check": "./bin/cdsmc.js",
|
|
31
|
+
"cdm": "./bin/cdm-build.js",
|
|
32
|
+
"cdm-build": "./bin/cdm-build.js",
|
|
33
|
+
"lh5r": "./bin/local-html5-repo.js",
|
|
34
|
+
"local-html5-repo": "./bin/local-html5-repo.js"
|
|
32
35
|
},
|
|
33
36
|
"scripts": {
|
|
34
37
|
"start": "cds-serve",
|
|
@@ -47,8 +50,8 @@
|
|
|
47
50
|
"audit": "npm audit --only=prod"
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {
|
|
50
|
-
"commander": "^14.0.
|
|
51
|
-
"express": "^4.22.1",
|
|
53
|
+
"commander": "^14.0.3",
|
|
54
|
+
"express": "^4.22.1 || ^5.2.1",
|
|
52
55
|
"http-proxy-middleware": "^3.0.5",
|
|
53
56
|
"redis": "^4.7.1",
|
|
54
57
|
"verror": "^1.10.1"
|
|
@@ -57,17 +60,17 @@
|
|
|
57
60
|
"@cap-js-community/common": "./",
|
|
58
61
|
"@cap-js/cds-test": "^0.4.1",
|
|
59
62
|
"@cap-js/sqlite": "^2.1.2",
|
|
60
|
-
"@sap/cds": "^9.
|
|
63
|
+
"@sap/cds": "^9.7.0",
|
|
61
64
|
"@sap/cds-common-content": "^3.1.0",
|
|
62
|
-
"@sap/cds-dk": "^9.
|
|
65
|
+
"@sap/cds-dk": "^9.7.0",
|
|
63
66
|
"eslint": "^9.39.2",
|
|
64
67
|
"eslint-config-prettier": "^10.1.8",
|
|
65
68
|
"eslint-plugin-jest": "^29.12.1",
|
|
66
|
-
"eslint-plugin-n": "^17.23.
|
|
69
|
+
"eslint-plugin-n": "^17.23.2",
|
|
67
70
|
"jest": "^30.2.0",
|
|
68
71
|
"jest-html-reporters": "^3.1.7",
|
|
69
72
|
"jest-junit": "^16.0.0",
|
|
70
|
-
"prettier": "^3.
|
|
73
|
+
"prettier": "^3.8.1",
|
|
71
74
|
"shelljs": "^0.10.0"
|
|
72
75
|
},
|
|
73
76
|
"cds": {
|
|
@@ -76,7 +79,9 @@
|
|
|
76
79
|
"vcap": {
|
|
77
80
|
"label": "redis-cache"
|
|
78
81
|
}
|
|
79
|
-
}
|
|
82
|
+
},
|
|
83
|
+
"html5-repo": true,
|
|
84
|
+
"portal": true
|
|
80
85
|
},
|
|
81
86
|
"migrationCheck": {
|
|
82
87
|
"baseDir": "migration-check",
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const cds = require("@sap/cds");
|
|
6
|
+
|
|
7
|
+
const COMPONENT_NAME = "/cap-js-community-common/cdm-build";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ICON = "sap-icon://activity-2";
|
|
10
|
+
const DEFAULT_VERSION = "3.2.0";
|
|
11
|
+
|
|
12
|
+
const regexLanguageFile = /.*\.properties$/;
|
|
13
|
+
const regexLanguageSuffixFile = /.*?_([a-z]{2}(?:_[a-z]{2})?)\.properties/i;
|
|
14
|
+
|
|
15
|
+
class CDMBuilder {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.component = COMPONENT_NAME;
|
|
18
|
+
|
|
19
|
+
this.options = options;
|
|
20
|
+
this.rootPath = options.root || process.cwd();
|
|
21
|
+
|
|
22
|
+
const project = this.deriveProject();
|
|
23
|
+
|
|
24
|
+
this.appPath = options.app || path.join(this.rootPath, "app");
|
|
25
|
+
this.portalSitePath = options.portalSite || path.join(this.appPath, "portal", "portal-site");
|
|
26
|
+
this.cdmPath = options.cdm || path.join(this.portalSitePath, "CommonDataModel.json");
|
|
27
|
+
this.targetPath = options.target || path.join(this.appPath, "cdm.json");
|
|
28
|
+
this.xsSecurityPath = options.xsSecurity || path.join(this.rootPath, "xs-security.json");
|
|
29
|
+
|
|
30
|
+
this.icon = options.icon || DEFAULT_ICON;
|
|
31
|
+
this.version = options.version || DEFAULT_VERSION;
|
|
32
|
+
this.namespace = options.namespace || project.name;
|
|
33
|
+
this.description = options.description || project.description;
|
|
34
|
+
this.defaultLanguage = cds.env.i18n?.default_language || options.language || "en";
|
|
35
|
+
this.i18nPath = options.i18n || "i18n";
|
|
36
|
+
this.i18nData = this.loadI18n(path.join(this.portalSitePath, this.i18nPath));
|
|
37
|
+
this.i18nBundle = options.i18nBundle || false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async build() {
|
|
41
|
+
if (!this.options.skipWrite && !this.options.force && fs.existsSync(this.targetPath)) {
|
|
42
|
+
cds.log(this.component).info("Generation is skipped. CDM file already exists", this.targetPath);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const cdm = fs.existsSync(this.cdmPath)
|
|
46
|
+
? require(this.cdmPath)
|
|
47
|
+
: {
|
|
48
|
+
_version: this.version,
|
|
49
|
+
identification: {
|
|
50
|
+
id: `${this.namespace}-flp`,
|
|
51
|
+
title: this.namespace,
|
|
52
|
+
entityType: "bundle",
|
|
53
|
+
},
|
|
54
|
+
payload: {},
|
|
55
|
+
};
|
|
56
|
+
const apps = this.lookupApps(this.appPath);
|
|
57
|
+
const roles = this.lookupRoles(apps);
|
|
58
|
+
this.enhanceExisting(cdm);
|
|
59
|
+
this.addCatalogsAndGroups(cdm, apps);
|
|
60
|
+
this.addSitesAndPages(cdm);
|
|
61
|
+
this.addHomepageApp(cdm);
|
|
62
|
+
this.addBusinessApps(cdm, apps);
|
|
63
|
+
this.addUrlTemplates(cdm);
|
|
64
|
+
this.addRoles(cdm, roles);
|
|
65
|
+
if (!this.options.skipWrite) {
|
|
66
|
+
fs.mkdirSync(path.dirname(this.targetPath), { recursive: true });
|
|
67
|
+
fs.writeFileSync(this.targetPath, JSON.stringify(cdm, null, 2));
|
|
68
|
+
}
|
|
69
|
+
return cdm;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
deriveProject() {
|
|
73
|
+
const packageJSON = require(path.join(this.rootPath, "package.json"));
|
|
74
|
+
const rawName = packageJSON.name.split("/").pop();
|
|
75
|
+
const parts = rawName.split(/[-.]/);
|
|
76
|
+
const name = parts.join(".");
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
description: packageJSON.description || name,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
enhanceExisting(cdm) {
|
|
84
|
+
cdm._version = this.version;
|
|
85
|
+
cdm.payload.catalogs ||= [];
|
|
86
|
+
let index = 1;
|
|
87
|
+
for (const catalog of cdm.payload.catalogs) {
|
|
88
|
+
catalog._version = this.version;
|
|
89
|
+
catalog.identification.id = `${this.namespace}-catalog${cdm.payload.catalogs.length > 1 ? `-${index++}` : ""}`;
|
|
90
|
+
for (const viz of catalog.payload.viz || []) {
|
|
91
|
+
viz.appId ??= viz.id;
|
|
92
|
+
}
|
|
93
|
+
if (!this.i18nBundle) {
|
|
94
|
+
delete catalog.identification.i18n;
|
|
95
|
+
catalog.texts = this.buildTexts(this.i18nData);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cdm.payload.groups ||= [];
|
|
99
|
+
index = 1;
|
|
100
|
+
for (const group of cdm.payload.groups) {
|
|
101
|
+
group._version = this.version;
|
|
102
|
+
group.identification.id = `${this.namespace}-group${cdm.payload.groups.length > 1 ? `-${index++}` : ""}`;
|
|
103
|
+
if (!this.i18nBundle) {
|
|
104
|
+
delete group.identification.i18n;
|
|
105
|
+
group.texts = this.buildTexts(this.i18nData);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
delete cdm.payload.sites;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
addCatalogsAndGroups(cdm, apps) {
|
|
112
|
+
cdm.payload.catalogs ||= [];
|
|
113
|
+
if (!cdm.payload.catalogs.length) {
|
|
114
|
+
const catalog = {
|
|
115
|
+
_version: this.version,
|
|
116
|
+
identification: {
|
|
117
|
+
id: `${this.namespace}-catalog`,
|
|
118
|
+
title: "{{title}}",
|
|
119
|
+
entityType: "catalog",
|
|
120
|
+
...(this.i18nBundle ? { i18n: "i18n/catalog.properties" } : {}),
|
|
121
|
+
},
|
|
122
|
+
payload: { viz: [] },
|
|
123
|
+
...(!this.i18nBundle ? { texts: this.buildTexts(this.i18nData, ["title"], this.description) } : {}),
|
|
124
|
+
};
|
|
125
|
+
for (const app of apps) {
|
|
126
|
+
catalog.payload.viz.push({
|
|
127
|
+
appId: app.appId,
|
|
128
|
+
vizId: app.defaultViz,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
cdm.payload.catalogs.push(catalog);
|
|
132
|
+
}
|
|
133
|
+
cdm.payload.groups ||= [];
|
|
134
|
+
if (!cdm.payload.groups.length) {
|
|
135
|
+
const group = {
|
|
136
|
+
_version: this.version,
|
|
137
|
+
identification: {
|
|
138
|
+
id: `${this.namespace}-group`,
|
|
139
|
+
title: "{{title}}",
|
|
140
|
+
entityType: "group",
|
|
141
|
+
...(this.i18nBundle ? { i18n: "i18n/group.properties" } : {}),
|
|
142
|
+
},
|
|
143
|
+
payload: { viz: [] },
|
|
144
|
+
...(!this.i18nBundle ? { texts: this.buildTexts(this.i18nData, ["title"], this.description) } : {}),
|
|
145
|
+
};
|
|
146
|
+
for (const app of apps) {
|
|
147
|
+
group.payload.viz.push({
|
|
148
|
+
id: app.id,
|
|
149
|
+
appId: app.appId,
|
|
150
|
+
vizId: app.defaultViz,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
cdm.payload.groups.push(group);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
addSitesAndPages(cdm) {
|
|
158
|
+
cdm.payload.spaces ||= [];
|
|
159
|
+
cdm.payload.pages ||= [];
|
|
160
|
+
const space = {
|
|
161
|
+
_version: this.version,
|
|
162
|
+
identification: {
|
|
163
|
+
id: `${this.namespace}-space`,
|
|
164
|
+
title: "{{title}}",
|
|
165
|
+
entityType: "space",
|
|
166
|
+
...(this.i18nBundle ? { i18n: "i18n/space.properties" } : {}),
|
|
167
|
+
},
|
|
168
|
+
payload: { contentNodes: [] },
|
|
169
|
+
...(!this.i18nBundle ? { texts: this.buildTexts(this.i18nData, ["title"], this.description) } : {}),
|
|
170
|
+
};
|
|
171
|
+
cdm.payload.spaces.push(space);
|
|
172
|
+
let index = 1;
|
|
173
|
+
for (const group of cdm.payload.groups || []) {
|
|
174
|
+
const pageId = `${this.namespace}-page${cdm.payload.groups.length > 1 ? `-${index++}` : ""}\`;`;
|
|
175
|
+
cdm.payload.pages.push({
|
|
176
|
+
_version: this.version,
|
|
177
|
+
identification: {
|
|
178
|
+
id: pageId,
|
|
179
|
+
title: "{{title}}",
|
|
180
|
+
entityType: "page",
|
|
181
|
+
...(this.i18nBundle ? { i18n: "i18n/page.properties" } : {}),
|
|
182
|
+
},
|
|
183
|
+
payload: {
|
|
184
|
+
sections: [
|
|
185
|
+
{
|
|
186
|
+
id: `${this.namespace}-section`,
|
|
187
|
+
title: "{{title}}",
|
|
188
|
+
viz: group.payload.viz,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
...(!this.i18nBundle ? { texts: this.buildTexts(this.i18nData, ["title"], this.description) } : {}),
|
|
193
|
+
});
|
|
194
|
+
space.payload.contentNodes.push({
|
|
195
|
+
type: "page",
|
|
196
|
+
id: pageId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
addHomepageApp(cdm) {
|
|
202
|
+
cdm.payload.apps ||= [];
|
|
203
|
+
cdm.payload.apps.push({
|
|
204
|
+
_version: this.version,
|
|
205
|
+
identification: {
|
|
206
|
+
id: `${this.namespace}-shell-home`,
|
|
207
|
+
entityType: "businessapp",
|
|
208
|
+
title: "{{title}}",
|
|
209
|
+
...(this.i18nBundle ? { i18n: "i18n/businessapp.properties" } : {}),
|
|
210
|
+
},
|
|
211
|
+
payload: {
|
|
212
|
+
targetAppConfig: {
|
|
213
|
+
"sap.app": {
|
|
214
|
+
crossNavigation: {
|
|
215
|
+
inbounds: {
|
|
216
|
+
default: {
|
|
217
|
+
semanticObject: "Shell",
|
|
218
|
+
action: "home",
|
|
219
|
+
signature: {},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
tags: {
|
|
224
|
+
technicalAttributes: ["APPTYPE_HOMEPAGE"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
"sap.integration": {
|
|
228
|
+
urlTemplateId: `${this.namespace}-urltemplate.home`,
|
|
229
|
+
urlTemplateParams: { path: "" },
|
|
230
|
+
},
|
|
231
|
+
"sap.ui": {
|
|
232
|
+
icons: { icon: this.icon },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
visualizations: {
|
|
236
|
+
default: {
|
|
237
|
+
vizType: "sap.ushell.StaticAppLauncher",
|
|
238
|
+
vizConfig: {
|
|
239
|
+
"sap.app": { title: "{{title}}" },
|
|
240
|
+
"sap.flp": {
|
|
241
|
+
target: { type: "IBN", inboundId: "default" },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
defaultViz: "default",
|
|
247
|
+
},
|
|
248
|
+
...(!this.i18nBundle ? { texts: this.buildTexts(this.i18nData, ["title"], this.description) } : {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
addBusinessApps(cdm, apps) {
|
|
253
|
+
cdm.payload.apps ||= [];
|
|
254
|
+
for (const app of apps) {
|
|
255
|
+
const defaultViz = app.defaultViz;
|
|
256
|
+
if (!defaultViz) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const icon = app.crossNavigation.inbounds[defaultViz].icon || this.icon;
|
|
260
|
+
const texts = this.buildAppTexts(app.i18n, app.id);
|
|
261
|
+
cdm.payload.apps.push({
|
|
262
|
+
_version: this.version,
|
|
263
|
+
identification: {
|
|
264
|
+
id: app.id,
|
|
265
|
+
entityType: "businessapp",
|
|
266
|
+
title: "{{title}}",
|
|
267
|
+
...(this.i18nBundle ? { i18n: "i18n/app.properties" } : {}),
|
|
268
|
+
},
|
|
269
|
+
payload: {
|
|
270
|
+
targetAppConfig: {
|
|
271
|
+
"sap.app": { crossNavigation: app.crossNavigation },
|
|
272
|
+
"sap.integrations": [
|
|
273
|
+
{
|
|
274
|
+
navMode: "explace",
|
|
275
|
+
urlTemplateId: `${this.namespace}-urltemplate`,
|
|
276
|
+
urlTemplateParams: { path: "" },
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
"sap.ui": { icons: { icon } },
|
|
280
|
+
},
|
|
281
|
+
visualizations: {
|
|
282
|
+
[defaultViz]: {
|
|
283
|
+
vizType: "sap.ushell.StaticAppLauncher",
|
|
284
|
+
vizConfig: {
|
|
285
|
+
"sap.app": { title: "{{title}}" },
|
|
286
|
+
"sap.flp": {
|
|
287
|
+
target: { type: "IBN", inboundId: defaultViz },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
defaultViz,
|
|
293
|
+
},
|
|
294
|
+
...(!this.i18nBundle ? { texts } : {}),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
addUrlTemplates(cdm) {
|
|
300
|
+
cdm.payload.urlTemplates ||= [];
|
|
301
|
+
const templates = [
|
|
302
|
+
{
|
|
303
|
+
id: `${this.namespace}-urltemplate-home`,
|
|
304
|
+
template: "{+_baseUrl}{+path}#Shell-home{?intentParameters*}",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: `${this.namespace}-urltemplate`,
|
|
308
|
+
template: "{+_baseUrl}{+path}#{+semanticObject}-{+action}{?intentParameters*}",
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
for (const template of templates) {
|
|
312
|
+
cdm.payload.urlTemplates.push({
|
|
313
|
+
_version: this.version,
|
|
314
|
+
identification: {
|
|
315
|
+
id: template.id,
|
|
316
|
+
entityType: "urltemplate",
|
|
317
|
+
title: "{{title}}",
|
|
318
|
+
...(this.i18nBundle ? { i18n: "i18n/urltemplate.properties" } : {}),
|
|
319
|
+
},
|
|
320
|
+
payload: {
|
|
321
|
+
urlTemplate: template.template,
|
|
322
|
+
parameters: {
|
|
323
|
+
mergeWith: "/urlTemplates/urltemplate.base/payload/parameters/names",
|
|
324
|
+
names: {
|
|
325
|
+
path: "{./sap.integration/urlTemplateParams/path}",
|
|
326
|
+
intentParameters: "{*}",
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
capabilities: { navigationMode: "standalone" },
|
|
330
|
+
},
|
|
331
|
+
...(!this.i18nBundle ? { texts: this.buildTexts(this.i18nData) } : {}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
addRoles(cdm, roles) {
|
|
337
|
+
cdm.payload.roles ||= [];
|
|
338
|
+
let scopes = {};
|
|
339
|
+
if (fs.existsSync(this.xsSecurityPath)) {
|
|
340
|
+
const xsSecurity = require(this.xsSecurityPath);
|
|
341
|
+
scopes = Object.fromEntries(xsSecurity.scopes.map((scope) => [this.localScope(scope.name), scope]));
|
|
342
|
+
}
|
|
343
|
+
for (const role of Object.keys(roles)) {
|
|
344
|
+
const scope = scopes[role];
|
|
345
|
+
cdm.payload.roles.push({
|
|
346
|
+
_version: this.version,
|
|
347
|
+
identification: {
|
|
348
|
+
id: role,
|
|
349
|
+
entityType: "role",
|
|
350
|
+
title: "{{title}}",
|
|
351
|
+
...(this.i18nBundle ? { i18n: "i18n/role.properties" } : {}),
|
|
352
|
+
},
|
|
353
|
+
payload: {
|
|
354
|
+
catalogs: [{ id: `${this.namespace}-catalog` }],
|
|
355
|
+
groups: [{ id: `${this.namespace}-group` }],
|
|
356
|
+
spaces: [{ id: `${this.namespace}-space` }],
|
|
357
|
+
apps: [...roles[role].map((app) => ({ id: app.id })), { id: `${this.namespace}-shell-home` }],
|
|
358
|
+
},
|
|
359
|
+
...(!this.i18nBundle
|
|
360
|
+
? {
|
|
361
|
+
texts: [
|
|
362
|
+
{
|
|
363
|
+
locale: "",
|
|
364
|
+
textDictionary: {
|
|
365
|
+
title: scope?.description || role,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
}
|
|
370
|
+
: {}),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
lookupApps(appRoot) {
|
|
376
|
+
const apps = [];
|
|
377
|
+
const dirs = fs
|
|
378
|
+
.readdirSync(appRoot, { withFileTypes: true })
|
|
379
|
+
.filter((dir) => dir.isDirectory())
|
|
380
|
+
.map((dir) => dir.name);
|
|
381
|
+
for (const dir of dirs) {
|
|
382
|
+
const ui5Yaml = path.join(appRoot, dir, "ui5.yaml");
|
|
383
|
+
if (!fs.existsSync(ui5Yaml)) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const manifestPath = path.join(appRoot, dir, "webapp", "manifest.json");
|
|
387
|
+
if (!fs.existsSync(manifestPath)) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const manifest = require(manifestPath);
|
|
391
|
+
if (manifest["sap.flp"]?.type === "plugin") {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const appId = manifest["sap.app"]?.id;
|
|
395
|
+
if (!appId || appId.endsWith(".extension")) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const crossNavigation = manifest["sap.app"]?.crossNavigation || {};
|
|
399
|
+
const defaultViz = Object.keys(crossNavigation.inbounds || {})[0];
|
|
400
|
+
const scopes = manifest["sap.platform.cf"]?.oAuthScopes?.map((s) => this.localScope(s)) || [];
|
|
401
|
+
const i18n = this.loadI18n(path.join(appRoot, dir, "webapp", "i18n"));
|
|
402
|
+
apps.push({
|
|
403
|
+
id: appId,
|
|
404
|
+
appId,
|
|
405
|
+
defaultViz,
|
|
406
|
+
crossNavigation,
|
|
407
|
+
scopes,
|
|
408
|
+
i18n,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return apps;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
lookupRoles(apps) {
|
|
415
|
+
const roles = {};
|
|
416
|
+
for (const app of apps) {
|
|
417
|
+
for (const scope of app.scopes) {
|
|
418
|
+
roles[scope] ||= [];
|
|
419
|
+
roles[scope].push(app);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return roles;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
localScope(scope) {
|
|
426
|
+
return scope.startsWith("$XSAPPNAME.") ? scope.slice(11) : scope;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
loadI18n(dir) {
|
|
430
|
+
const result = {
|
|
431
|
+
[this.defaultLanguage]: {},
|
|
432
|
+
};
|
|
433
|
+
try {
|
|
434
|
+
for (const file of fs.readdirSync(dir)) {
|
|
435
|
+
if (!regexLanguageFile.test(file)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const texts = cds.load.properties(path.join(dir, file));
|
|
439
|
+
const match = file.match(regexLanguageSuffixFile);
|
|
440
|
+
const lang = match?.[1] || this.defaultLanguage;
|
|
441
|
+
result[lang] ||= {};
|
|
442
|
+
Object.assign(result[lang], texts);
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
/* ignore */
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
buildTexts(i18n, keys, defaultText = "") {
|
|
451
|
+
const texts = [];
|
|
452
|
+
for (const locale of Object.keys(i18n)) {
|
|
453
|
+
texts.push({
|
|
454
|
+
locale,
|
|
455
|
+
textDictionary: keys
|
|
456
|
+
? Object.fromEntries(keys.map((key) => [key, i18n[locale][key] || defaultText]))
|
|
457
|
+
: i18n[locale],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
texts.push({
|
|
461
|
+
locale: "",
|
|
462
|
+
textDictionary: keys
|
|
463
|
+
? Object.fromEntries(keys.map((key) => [key, i18n[this.defaultLanguage][key] || defaultText]))
|
|
464
|
+
: i18n[this.defaultLanguage],
|
|
465
|
+
});
|
|
466
|
+
return texts;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
buildAppTexts(i18n, app) {
|
|
470
|
+
const texts = [];
|
|
471
|
+
for (const locale of Object.keys(i18n)) {
|
|
472
|
+
const title =
|
|
473
|
+
i18n[locale].fioriAppTitle || i18n[locale].appTitle || i18n[locale].app_title || i18n[locale].appTitleReworked;
|
|
474
|
+
if (!title) {
|
|
475
|
+
throw new Error(`Missing title for locale ${locale} in app "${app}"`);
|
|
476
|
+
}
|
|
477
|
+
texts.push({
|
|
478
|
+
locale,
|
|
479
|
+
textDictionary: { title },
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
texts.push({
|
|
483
|
+
locale: "",
|
|
484
|
+
textDictionary: {
|
|
485
|
+
title:
|
|
486
|
+
i18n[this.defaultLanguage]?.fioriAppTitle ||
|
|
487
|
+
i18n[this.defaultLanguage]?.appTitle ||
|
|
488
|
+
i18n[this.defaultLanguage]?.app_title ||
|
|
489
|
+
i18n[this.defaultLanguage]?.appTitleReworked,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
return texts;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = CDMBuilder;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
+
CDMBuilder: require("./cdm-build").CDMBuilder,
|
|
4
5
|
LocalHTML5Repo: require("./local-html5-repo").LocalHTML5Repo,
|
|
5
6
|
MigrationCheck: require("./migration-check").MigrationCheck,
|
|
6
7
|
RateLimiting: require("./rate-limiting").RateLimiting,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
/* eslint-disable no-console */
|
|
4
3
|
/* eslint-disable n/no-process-exit */
|
|
5
4
|
|
|
6
5
|
// Suppress deprecation warning in Node 22 due to http-proxy using util._extend()
|
|
@@ -10,23 +9,35 @@ const fs = require("fs");
|
|
|
10
9
|
const path = require("path");
|
|
11
10
|
const express = require("express");
|
|
12
11
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
|
12
|
+
const cds = require("@sap/cds");
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const DEFAULT_ENV_NAME = "default-env.json";
|
|
15
|
+
const DEFAULT_ENV_PATHS = [
|
|
16
|
+
path.join(process.cwd(), "app", "router", DEFAULT_ENV_NAME),
|
|
17
|
+
path.join(process.cwd(), "approuter", DEFAULT_ENV_NAME),
|
|
18
|
+
];
|
|
16
19
|
const APP_ROOT = path.join(process.cwd(), "app");
|
|
20
|
+
const PORT = process.env.PORT || 3001;
|
|
17
21
|
|
|
18
|
-
const COMPONENT_NAME = "/cap-js-community-common/
|
|
22
|
+
const COMPONENT_NAME = "/cap-js-community-common/local-html5-repo";
|
|
19
23
|
|
|
20
24
|
class LocalHTML5Repo {
|
|
21
25
|
constructor(options) {
|
|
22
26
|
this.component = COMPONENT_NAME;
|
|
23
27
|
this.port = options?.port || PORT;
|
|
24
|
-
this.path = options?.path
|
|
25
|
-
|
|
28
|
+
this.path = options?.path;
|
|
29
|
+
if (!this.path) {
|
|
30
|
+
for (const path of DEFAULT_ENV_PATHS) {
|
|
31
|
+
if (fs.existsSync(path)) {
|
|
32
|
+
this.path = path;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
26
37
|
try {
|
|
27
38
|
this.defaultEnv = require(this.path);
|
|
28
39
|
} catch (err) {
|
|
29
|
-
|
|
40
|
+
cds.log(this.component).error(`Cannot read default-env.json at ${this.path}`);
|
|
30
41
|
throw err;
|
|
31
42
|
}
|
|
32
43
|
}
|
|
@@ -35,7 +46,7 @@ class LocalHTML5Repo {
|
|
|
35
46
|
return new Promise((resolve) => {
|
|
36
47
|
this.adjustDefaultEnv();
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
cds.log(this.component).info("Registering apps:");
|
|
39
50
|
const app = express();
|
|
40
51
|
|
|
41
52
|
// Serve every webapp
|
|
@@ -64,7 +75,7 @@ class LocalHTML5Repo {
|
|
|
64
75
|
}),
|
|
65
76
|
);
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
cds.log(this.component).info(`- ${name} [${type}] -> ${path.join(APP_ROOT, appDirectory)}`);
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
@@ -80,7 +91,7 @@ class LocalHTML5Repo {
|
|
|
80
91
|
ws: true,
|
|
81
92
|
logLevel: "warn",
|
|
82
93
|
onError(err, req, res) {
|
|
83
|
-
|
|
94
|
+
cds.log(this.component).error("HTML5 Repo proxy error:", err.message);
|
|
84
95
|
res.status(502).end("Bad Gateway");
|
|
85
96
|
},
|
|
86
97
|
});
|
|
@@ -89,7 +100,7 @@ class LocalHTML5Repo {
|
|
|
89
100
|
app.use("/", html5RepoProxy);
|
|
90
101
|
|
|
91
102
|
this.server = app.listen(this.port, () => {
|
|
92
|
-
|
|
103
|
+
cds.log(this.component).info(`Local HTML5 repository running on port ${this.port}`);
|
|
93
104
|
resolve(this.server);
|
|
94
105
|
});
|
|
95
106
|
});
|
|
@@ -118,10 +129,8 @@ class LocalHTML5Repo {
|
|
|
118
129
|
|
|
119
130
|
writeDefaultEnv() {
|
|
120
131
|
const url = this.defaultEnv.VCAP_SERVICES["html5-apps-repo"][0].credentials.uri;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
fs.writeFileSync(DEFAULT_ENV_PATH, JSON.stringify(this.defaultEnv, null, 2) + "\n");
|
|
132
|
+
cds.log(this.component).info(`Rewriting HTML5 Repo URL in default-env.json of approuter: ${url}`);
|
|
133
|
+
fs.writeFileSync(this.path, JSON.stringify(this.defaultEnv, null, 2) + "\n");
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
extractNameAndType(content) {
|
|
@@ -5,7 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
|
|
8
|
-
const COMPONENT_NAME = "/cap-js-community-common/
|
|
8
|
+
const COMPONENT_NAME = "/cap-js-community-common/migration-check";
|
|
9
9
|
const STRING_DEFAULT_LENGTH = 5000;
|
|
10
10
|
|
|
11
11
|
const Checks = [releasedEntityCheck, newEntityCheck, uniqueIndexCheck, journalModeCheck];
|
|
@@ -559,7 +559,10 @@ function journalModeCheck(csnBuild, csnProd, whitelist, options) {
|
|
|
559
559
|
}
|
|
560
560
|
visitPersistenceEntities(
|
|
561
561
|
csnBuild,
|
|
562
|
-
(definitionBuild) => {
|
|
562
|
+
(definitionBuild, { draft } = {}) => {
|
|
563
|
+
if (draft) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
563
566
|
const definitionProd = csnProd.definitions[definitionBuild.name];
|
|
564
567
|
if (definitionProd) {
|
|
565
568
|
if (definitionBuild["@cds.persistence.journal"] && !definitionProd["@cds.persistence.journal"]) {
|
|
@@ -616,6 +619,9 @@ function visitPersistenceEntities(csn, onEntity, filter) {
|
|
|
616
619
|
if (partOfService) {
|
|
617
620
|
const _compositeEntities = compositeEntities(csn.definitions, name);
|
|
618
621
|
_compositeEntities.forEach((name) => {
|
|
622
|
+
if (filter && !filter.includes(name)) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
619
625
|
const definition = csn.definitions[name];
|
|
620
626
|
definition.name = name;
|
|
621
627
|
onEntity(definition, { draft: true });
|
|
@@ -7,7 +7,7 @@ const redisResetTime = require("./redis/resetTime");
|
|
|
7
7
|
|
|
8
8
|
const { connectionCheck } = require("./redis/common");
|
|
9
9
|
|
|
10
|
-
const COMPONENT_NAME = "/cap-js-community-common/
|
|
10
|
+
const COMPONENT_NAME = "/cap-js-community-common/rate-limiting";
|
|
11
11
|
|
|
12
12
|
class RateLimiting {
|
|
13
13
|
constructor(service, { maxConcurrent, maxInWindow, window } = {}) {
|
|
@@ -4,7 +4,7 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
|
|
5
5
|
const { RedisClient } = require("../../redis-client");
|
|
6
6
|
|
|
7
|
-
const COMPONENT_NAME = "/cap-js-community-common/
|
|
7
|
+
const COMPONENT_NAME = "/cap-js-community-common/rate-limiting";
|
|
8
8
|
|
|
9
9
|
async function connectionCheck() {
|
|
10
10
|
return await RedisClient.create(COMPONENT_NAME).connectionCheck();
|