@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 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
- - [Replication Cache](#replication-cache)
19
- - [Migration Check](#migration-check)
20
- - [Rate Limiting](#rate-limiting)
21
- - [Redis Client](#redis-client)
22
- - [Local HTML5 Repository](#local-html5-repository)
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).
@@ -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",
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
- "lh5r": "bin/local-html5-repo.js",
31
- "local-html5-repo": "bin/local-html5-repo.js"
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.2",
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.6.1",
63
+ "@sap/cds": "^9.7.0",
61
64
  "@sap/cds-common-content": "^3.1.0",
62
- "@sap/cds-dk": "^9.6.0",
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.1",
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.7.4",
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;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ CDMBuilder: require("./CDMBuilder"),
5
+ };
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 PORT = process.env.PORT || 3001;
15
- const DEFAULT_ENV_PATH = path.join(process.cwd(), "approuter/default-env.json");
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/localHTML5Repo";
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 || DEFAULT_ENV_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
- console.error(`Cannot read default-env.json at ${this.path}`);
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
- console.log("Registering apps:");
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
- console.log(`- ${name} [${type}] -> ${path.join(APP_ROOT, appDirectory)}`);
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
- console.error("HTML5 Repo proxy error:", err.message);
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
- console.log(`Local HTML5 repository running on port ${this.port}`);
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
- console.log(`Rewriting HTML5 Repo URL in default-env.json of approuter: ${url}`);
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/migrationCheck";
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/rateLimiting";
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/rateLimiting";
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();
@@ -3,7 +3,7 @@
3
3
  const redis = require("redis");
4
4
  const cds = require("@sap/cds");
5
5
 
6
- const COMPONENT_NAME = "/cap-js-community-common/redisClient";
6
+ const COMPONENT_NAME = "/cap-js-community-common/redis-client";
7
7
  const LOG_AFTER_SEC = 5;
8
8
  const TIMEOUT_SHUTDOWN = 2500;
9
9