@sap/cds-dk 9.4.2 → 9.5.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/bin/compile/index.js +1 -1
  3. package/bin/data-products/index.js +10 -2
  4. package/bin/data-products/prepare-csn.js +63 -26
  5. package/bin/export.js +1 -1
  6. package/bin/help.js +2 -3
  7. package/bin/import.js +13 -3
  8. package/bin/repl.js +7 -11
  9. package/bin/up.js +164 -13
  10. package/bin/util/containerize.js +183 -0
  11. package/bin/version.js +4 -4
  12. package/lib/bind/k8s.js +1 -1
  13. package/lib/build/buildTaskEngine.js +5 -2
  14. package/lib/build/plugin.js +1 -1
  15. package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +1 -1
  16. package/lib/build/plugins/helm/index.js +7 -7
  17. package/lib/build/provider/hana/2migration.js +16 -24
  18. package/lib/build/provider/hana/index.js +11 -3
  19. package/lib/build/provider/hana/template/.hdiconfig-all +7 -1
  20. package/lib/build/provider/hana/template/package.json +1 -1
  21. package/lib/build/provider/internalBuildPlugin.js +15 -1
  22. package/lib/build/provider/java/index.js +12 -1
  23. package/lib/build/provider/mtx-sidecar/index.js +2 -1
  24. package/lib/build/provider/nodejs/index.js +2 -1
  25. package/lib/build/util.js +3 -3
  26. package/lib/compile/to_openapi.js +1 -5
  27. package/lib/import/common.js +5 -4
  28. package/lib/import/message.js +1 -1
  29. package/lib/import/odata/ParserContext.js +1 -0
  30. package/lib/import/odata/v4parser.js +48 -3
  31. package/lib/import/openapi/importOpenAPI.js +6 -1
  32. package/lib/init/constants.js +1 -1
  33. package/lib/init/index.js +15 -6
  34. package/lib/init/merge.js +16 -0
  35. package/lib/init/projectReader.js +54 -24
  36. package/lib/init/registries/mta.js +5 -2
  37. package/lib/init/registries/xs-app.js +7 -1
  38. package/lib/init/template/ams/index.js +1 -1
  39. package/lib/init/template/app-frontend/files/app-package.json.hbs +1 -7
  40. package/lib/init/template/app-frontend/files/ui5.yaml.hbs +20 -1
  41. package/lib/init/template/app-frontend/index.js +0 -4
  42. package/lib/init/template/application-logging/index.js +9 -14
  43. package/lib/init/template/approuter/files/mta.yaml.hbs +7 -2
  44. package/lib/init/template/approuter/index.js +21 -16
  45. package/lib/init/template/audit-logging/index.js +2 -2
  46. package/lib/init/template/cloud-logging/index.js +2 -3
  47. package/lib/init/template/containerize/files/containerize.yaml.hbs +17 -3
  48. package/lib/init/template/containerize/index.js +0 -4
  49. package/lib/init/template/data/as-json.js +18 -20
  50. package/lib/init/template/destination/index.js +0 -4
  51. package/lib/init/template/dynatrace/index.js +0 -2
  52. package/lib/init/template/github-actions/files/kyma-setup/action.yaml +0 -2
  53. package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +16 -1
  54. package/lib/init/template/github-actions/files/workflows/test.yaml.hbs +20 -0
  55. package/lib/init/template/github-actions/index.js +5 -8
  56. package/lib/init/template/hana/files/db/package.json +2 -2
  57. package/lib/init/template/hana/files/mta.yaml.hbs +0 -4
  58. package/lib/init/template/hana/index.js +5 -7
  59. package/lib/init/template/helm/index.js +16 -33
  60. package/lib/init/template/helm-unified-runtime/index.js +0 -4
  61. package/lib/init/template/html5-repo/files/app-package.json.hbs +1 -7
  62. package/lib/init/template/html5-repo/files/containerize.yaml.hbs +0 -9
  63. package/lib/init/template/html5-repo/files/mta.yaml.hbs +0 -7
  64. package/lib/init/template/html5-repo/files/ui5.yaml.hbs +20 -1
  65. package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -0
  66. package/lib/init/template/html5-repo/files/xs-app.json.hbs +5 -3
  67. package/lib/init/template/html5-repo/index.js +8 -14
  68. package/lib/init/template/ias/files/values.yaml.hbs +6 -6
  69. package/lib/init/template/ias/index.js +14 -23
  70. package/lib/init/template/kyma/index.js +14 -1
  71. package/lib/init/template/malware-scanner/files/mta.yaml.hbs +3 -1
  72. package/lib/init/template/malware-scanner/files/values.yaml +3 -1
  73. package/lib/init/template/multitenancy/files/mta.yaml.hbs +1 -3
  74. package/lib/init/template/multitenancy/files/values.yaml.hbs +8 -0
  75. package/lib/init/template/multitenancy/index.js +10 -5
  76. package/lib/init/template/nodejs/files/package.json.hbs +1 -1
  77. package/lib/init/template/pipeline/files/app-package.json.hbs +1 -7
  78. package/lib/init/template/pipeline/files/ui5.yaml.hbs +20 -1
  79. package/lib/init/template/pipeline/index.js +6 -3
  80. package/lib/init/template/portal/files/mta.yaml.hbs +5 -3
  81. package/lib/init/template/portal/index.js +3 -6
  82. package/lib/init/template/sample/files/_i18n/i18n_de.properties +12 -0
  83. package/lib/init/template/sample/files/_i18n/i18n_en.properties +21 -0
  84. package/lib/init/template/sample/files/_i18n/i18n_fr.properties +20 -0
  85. package/lib/init/template/sample/files/_i18n/messages_de.properties +34 -0
  86. package/lib/init/template/sample/files/_i18n/messages_en.properties +34 -0
  87. package/lib/init/template/sample/files/_i18n/messages_fr.properties +34 -0
  88. package/lib/init/template/sample/files/app/_i18n/i18n_de.properties +0 -26
  89. package/lib/init/template/sample/files/app/_i18n/i18n_en.properties +8 -0
  90. package/lib/init/template/sample/files/app/admin-authors/fiori-service.cds +52 -0
  91. package/lib/init/template/sample/files/app/admin-authors/webapp/Component.js.hbs +3 -0
  92. package/lib/init/template/sample/files/app/admin-authors/webapp/i18n/i18n.properties +11 -0
  93. package/lib/init/template/sample/files/app/admin-authors/webapp/manifest.json +130 -0
  94. package/lib/init/template/sample/files/app/admin-books/fiori-service.cds +81 -88
  95. package/lib/init/template/sample/files/app/admin-books/webapp/Component.js.hbs +3 -0
  96. package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n.properties +10 -2
  97. package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +24 -48
  98. package/lib/init/template/sample/files/app/appconfig/fioriSandboxConfig.json +54 -3
  99. package/lib/init/template/sample/files/app/browse/fiori-service.cds +30 -30
  100. package/lib/init/template/sample/files/app/browse/webapp/Component.js.hbs +3 -0
  101. package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n.properties +10 -3
  102. package/lib/init/template/sample/files/app/browse/webapp/manifest.json +18 -23
  103. package/lib/init/template/sample/files/app/common.cds +126 -158
  104. package/lib/init/template/sample/files/app/fiori-apps.html +30 -0
  105. package/lib/init/template/sample/files/app/genres/fiori-service.cds +33 -0
  106. package/lib/init/template/sample/files/app/genres/tree-view.cds +42 -0
  107. package/lib/init/template/sample/files/app/genres/value-help.cds +6 -0
  108. package/lib/init/template/sample/files/app/genres/webapp/Component.js.hbs +3 -0
  109. package/lib/init/template/sample/files/app/genres/webapp/i18n/i18n.properties +4 -0
  110. package/lib/init/template/sample/files/app/genres/webapp/i18n/i18n_de.properties +2 -0
  111. package/lib/init/template/sample/files/app/genres/webapp/manifest.json +126 -0
  112. package/lib/init/template/sample/files/app/services.cds +6 -2
  113. package/lib/init/template/sample/files/db/currencies.cds +13 -0
  114. package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Books.csv +5 -5
  115. package/lib/init/template/sample/files/db/data/{sap.capire.bookshop-Books_texts.csv → sap.capire.bookshop-Books.texts.csv} +4 -4
  116. package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Genres.csv +42 -15
  117. package/lib/init/template/sample/files/db/data/sap.common-Currencies.csv +12 -0
  118. package/lib/init/template/sample/files/db/data/sap.common-Currencies.texts.csv +15 -0
  119. package/lib/init/template/sample/files/db/hana/index.cds +10 -0
  120. package/lib/init/template/sample/files/db/schema.cds +35 -22
  121. package/lib/init/template/sample/files/db/sqlite/index.cds +10 -0
  122. package/lib/init/template/sample/files/package.json.hbs +14 -1
  123. package/lib/init/template/sample/files/srv/access-control.cds +3 -0
  124. package/lib/init/template/sample/files/srv/admin-service.cds +3 -2
  125. package/lib/init/template/sample/files/srv/cat-service.cds +7 -11
  126. package/lib/init/template/sample/index.js +26 -17
  127. package/lib/init/template/telemetry/index.js +1 -3
  128. package/lib/init/template/typescript/files/package.json +1 -1
  129. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +0 -5
  130. package/lib/init/template/workzone-standard/index.js +0 -1
  131. package/lib/init/template/xsuaa/files/mta.yaml.hbs +4 -0
  132. package/lib/init/template/xsuaa/index.js +5 -7
  133. package/lib/lint/Linter.js +10 -4
  134. package/lib/lint/eslintHelp.json +1 -1
  135. package/lib/mtx/tenant_action.js +12 -4
  136. package/lib/util/checksum.js +32 -0
  137. package/lib/util/question.js +60 -7
  138. package/npm-shrinkwrap.json +61 -37
  139. package/package.json +2 -2
  140. package/lib/init/template/app-frontend/files/ui5-deploy.yaml.hbs +0 -25
  141. package/lib/init/template/github-actions/files/cf-setup/action.yaml +0 -53
  142. package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +0 -25
  143. package/lib/init/template/pipeline/files/ui5-deploy.yaml.hbs +0 -25
  144. package/lib/init/template/sample/files/app/_i18n/i18n.properties +0 -29
  145. package/lib/init/template/sample/files/app/admin-books/webapp/Component.js +0 -8
  146. package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n_de.properties +0 -3
  147. package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n_en.properties +0 -3
  148. package/lib/init/template/sample/files/app/browse/webapp/Component.js +0 -7
  149. package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n_de.properties +0 -3
  150. package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n_en.properties +0 -4
  151. package/lib/init/template/sample/files/app/index.html +0 -32
package/CHANGELOG.md CHANGED
@@ -6,6 +6,64 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
8
8
 
9
+ ## Version 9.5.0 - 2025-12-01
10
+
11
+ ### Added
12
+
13
+ - `cds import` now accepts `--into-namespace <string>` to override the namespace entities are imported to, when importing OpenAPI definitions.
14
+
15
+ ### Changed
16
+
17
+ - `cds add portal` adds the correct service key configuration to the _mta.yaml_.
18
+ - `cds add sample` creates sample files aligned with recent versions of bookshop and bookstore in https://github.com/capire.
19
+ - `cds add sample` uses a matching ID in generated `manifest.json` and UI5 components.
20
+ - `cds build` uses `hdblogicalschema` and `hdblogicalschemaconfig` plugins in its default `.hdiconfig`.
21
+ - `cds add cloud-logging` adds required dependencies to _pom.xml_ for Java projects.
22
+ - `cds add kyma` is now the only suggested Kyma facet in `cds add help`.
23
+ - `cds add helm-unified-runtime` option is now deprecated; use `cds add kyma --unified-runtime` instead.
24
+ - `cds up --to k8s` now saves the checksum of buildpack images to determine if a rebuild is necessary.
25
+ - `cds up --to k8s` will interactively ask for registry credentials if they don't exist on the remote cluster.
26
+ - `cds build` automatically adds `contentLocalizedEdmx=true` to the java build task configuration if OData V2 is set.
27
+ - `cds add github-actions` has out-of-the-box support for Java projects.
28
+ - `cds add html5-repo` and `cds add app-frontend` do not create a separate _ui5-deploy.yaml_ but use the default _ui5.yaml_.
29
+ - `cds add` plugins allow merging into non-existing files with `cds.add.merge(<object>).into(<yaml-path>)`.
30
+ - `cds build` tasks that produce `csn.json` now emit the `compile.for.runtime` event, in alignment with other runtime CSN producers like `cds serve`. Plugins can use such events to hook into CSN creation.
31
+ - `cds add xsuaa` sets `credential-types: [ "binding-secret", "x509" ]` for XSUAA instances.
32
+ - `cds add hana` does not create an explicit `.hdiconfig` file any more as `cds build` provides defaults.
33
+ - `cds import` generates the on condition for a composition when the corresponding OData navigation property targets a collection, is contained (`$ContainsTarget` in OData EDM) and has a backlink (`$Partner` in OData EDM).
34
+ - `cds import` now considers OData contained navigation properties as compositions instead of associations.
35
+
36
+ ### Fixed
37
+
38
+ - `cds add ias` will always add a binding to the MTX sidecar or approuter if required.
39
+ - `cds add ias` has improved support for multitenancy scenarios on Kyma.
40
+ - `cds lint` no longer fails with error code 1 and no message when loading the config fails.
41
+ - `cds version` no longer prints an `undefined` value for `@sap/cds-dk`.
42
+ - `cds compile --flavor files` no longer crashes with a `TypeError`.
43
+ - `cds add workzone` does not add superfluous `destinations` configuration to the `srv-api` import.
44
+
45
+ ## Version 9.4.3 - 2025-10-29
46
+
47
+ ### Added
48
+
49
+ ### Changed
50
+
51
+ - `cds add github-actions` will use the `cap-js/cf-setup` action from GitHub marketplace instead of creating a local copy.
52
+ - `cds add containerize` omits the Unix-based `before_all` scripts in `containerize.yaml` in favor of a cross-platform compatible `cds up --to k8s` integration.
53
+ - `cds up --to k8s` now uses built-in containerization, eliminating the hard dependency on the `ctz` library.
54
+ - `cds up --to k8s` will interactively ask for a domain if it can't be determined from the current Kubernetes configuration.
55
+ - `cds build --for hana` no longer requires `.hdbgrants` and `.hdbrevokes` to be specified as a plugin in the `.hdiconfig` file.
56
+ - `cds import` no longer emits usage info when invoked with the dry option.
57
+
58
+ ### Fixed
59
+
60
+ - `cds add data` fixes an issue where randomly generated `Decimal` and `Integer` values could exceed their defined precision or scale.
61
+ - `cds add pipeline` does not add UI5 template files in some cases.
62
+ - `cds add ias` does not write to the `mta.yaml` for non-MTA projects.
63
+ - `cds add ias` automatically adds the `authenticationType` to `xs-app.json` if required.
64
+ - `cds add ias` adds a binding to the approuter component if required.
65
+ - `cds import --from ...` yields a better error message for unknown import kinds.
66
+
9
67
  ## Version 9.4.2 - 2025-10-23
10
68
 
11
69
  ### Changed
@@ -23,6 +81,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
23
81
  - `cds add test` creates correct OData URLs with unqualified entity names in service paths.
24
82
  - `cds add http` skips contained composition targets.
25
83
  - `cds add sample` no longer comes with an error when creating a Book through Fiori.
84
+ - `cds build` now places the `i18n` folder in the generated output root directory instead of nested locations.
26
85
 
27
86
  ## Version 9.4.1 - 2025-10-09
28
87
 
@@ -313,6 +372,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
313
372
  - `cds add ams` for Java adds a custom builder to the `mta.yaml` to circumvent the missing `srv/src/gen/policies`.
314
373
  - `cds add multitenancy` requires the `srv-api` instead of the `mtx-api` for Java projects.
315
374
 
375
+ ## Version 8.9.11 - 2025-11-21
376
+
377
+ ### Fixed
378
+
379
+ - Bump dependencies in shrinkwrap.
380
+
381
+ ## Version 8.9.10 - 2025-09-19
382
+
383
+ ### Fixed
384
+
385
+ - Updated shrinkwrap to include `@sap/cds-compiler` version 5.9.12.
386
+
387
+ ## Version 8.9.9 - 2025-09-16
388
+
389
+ ### Fixed
390
+
391
+ - CVE-2025-58754: vulnerability with `axios` versions <1.12.0.
392
+
393
+ ## Version 8.9.8 - 2025-07-24
394
+
395
+ ### Fixed
396
+
397
+ - CVE-2025-7783: vulnerability with `form-data` versions <4.0.4.
398
+
399
+ ## Version 8.9.7 - 2025-07-17
400
+
401
+ ### Fixed
402
+
403
+ - Bump dependencies in shrinkwrap.
404
+
405
+ ## Version 8.9.6 - 2025-06-05
406
+
407
+ ### Fixed
408
+
409
+ - Bump dependencies in shrinkwrap.
316
410
 
317
411
  ## Version 8.9.5 - 2025-06-05
318
412
 
@@ -297,7 +297,7 @@ async function compile (models, options={}) {
297
297
  throw `You must specify a model to compile.\nRun 'cds c -?' to learn more.`
298
298
  }
299
299
 
300
- let chain = model.then (m => model=m)
300
+ let chain = model.then?. (m => model=m) ?? Promise.resolve(model) // model could be a sync. value
301
301
 
302
302
  // add processors for compiling
303
303
  if (options.for) for (let each of options.for.split(',')) {
@@ -6,8 +6,16 @@ const path = require ('node:path')
6
6
 
7
7
  exports.load_data_product = async function (sources, o) {
8
8
  let service = /\/([^/]+)\//.exec(o.services)?.[1].replaceAll('\\.','.')
9
- if (!service.includes(':apiResource:')) {
10
- const [,namespace,name,version] = /^(.+)-([^-]+)-(v[0-9]+)\.json$/ .exec (path.basename(String(sources)))
9
+ if (service != 'all') {
10
+ // ORD ID has been provided as parameter "-s", valid example: "sap.s4com:apiResource:Product:v1"
11
+ if (!service.match(/^([^:]+):apiResource:([^:]+):(v[0-9]+)$/))
12
+ throw new Error(`"${service}" is not a valid output port ORD ID`)
13
+ }
14
+ else {
15
+ // try to deduce ORD ID from file name, valid example: "sap-s4com-Product-v1.json"
16
+ const m = /^(.+)-([^-]+)-(v[0-9]+)\.(json|csn)$/ .exec (path.basename(String(sources)))
17
+ if (m?.length != 5) throw new Error(`Output port ORD ID cannot be deduced from file name`)
18
+ const [,namespace,name,version] = m
11
19
  service = namespace.replace(/-/g,'.') + ':apiResource:' + name + ':' + version
12
20
  }
13
21
 
@@ -180,38 +180,75 @@ exports.prepareCsn = function (csn, srvName, ordId) {
180
180
  //
181
181
  // ---------- step 5: wrap everyting into service if not yet done -------------------------------
182
182
  //
183
- // Prefix all entity names with service name. Adapt association targets.
184
- // Add CAP specific annotations for DP handling.
183
+ // This function must be called with a service name "srvName" as argument.
184
+ if (!srvName) throw new Error('No service name has been provided.')
185
185
 
186
- if (srvName) {
187
- if (!csn.definitions[srvName] || csn.definitions[srvName].kind !== 'service') {
188
- // add the service
189
- csn.definitions[srvName] = {kind: 'service'}
190
- // loop over all entities and prefix names with service name
191
- for (let n in csn.definitions) {
192
- let entity = csn.definitions[n]
193
- if (entity.kind === 'entity') {
194
- // find association elements and rename the targets
195
- for (let en in entity.elements || {} ) {
196
- let element = entity.elements[en]
197
- if (element?.type == 'cds.Association' || element?.type == 'cds.Composition') {
198
- element.target = srvName + '.' + element.target
199
- }
200
- }
201
- csn.definitions[srvName + '.' + n] = entity
202
- delete csn.definitions[n]
186
+ // Assumptions (based on the rules for DP CSNs):
187
+ // 1) there is no or one context or service in the CSN
188
+ // 2) if there is a context or service, all objects in the CSN belong to it
189
+ //
190
+ // If there is no context/service (this is a DP CSN coming from BAH):
191
+ // put all entities into a new service "srvName", i.e.
192
+ // - prefix all object and association target names with "srvName"
193
+ // - add a service "srvName" with CAP specific annotations for DP handling
194
+ // If there is a context/service (DP CSN comes from UCL, context/service name is the schema name in the HDLFS share):
195
+ // move all entities out of the context/service and put them into a new service "srvName", i.e.
196
+ // - remove the context/service
197
+ // - change the prefix for all entity and association target names from
198
+ // the old context/service name to "srvName"
199
+ // - add a service "srvName" with CAP specific annotations for DP handling
200
+ //
201
+ // Note: Currently an existing context/service doesn't contain further info (e.g. annos),
202
+ // so deleting it is ok.
203
+
204
+ let ctxName = null;
205
+ const ctxs = Object.entries(csn.definitions).filter(([,o]) => o.kind === 'context' || o.kind === 'service')
206
+ if (ctxs.length > 0) {
207
+ // check assumption 1
208
+ if (ctxs.length > 1) throw new Error('Data Product CSN is invalid: it has more than one context or service.')
209
+ ctxName = ctxs[0][0]
210
+ delete csn.definitions[ctxName]
211
+ // check assumption 2
212
+ const bad = Object.keys(csn.definitions).filter((n) => !n.startsWith(ctxName + '.'))
213
+ if (bad.length > 0)
214
+ throw new Error(`Unexpected entity name "${bad[0]}" without prefix in Data product CSN with context "${ctxName}"`)
215
+ }
216
+
217
+ function changePrefix(name) {
218
+ if (ctxName) {
219
+ name = name.substring(ctxName.length + 1) // remove prefix incl. '.'
220
+ }
221
+ return srvName + '.' + name
222
+ }
223
+
224
+ // loop over all definitions and prefix names with service name
225
+ // if ctxName accidentially equals srvName, do nothing
226
+ if (ctxName != srvName) {
227
+ for (let n in csn.definitions) {
228
+ let def = csn.definitions[n]
229
+ // adapt assoc targets
230
+ for (let en in def.elements || {} ) {
231
+ let element = def.elements[en]
232
+ if (element?.type === 'cds.Association' || element?.type === 'cds.Composition') {
233
+ element.target = changePrefix(element.target)
203
234
  }
204
235
  }
236
+ csn.definitions[changePrefix(n)] = def
237
+ delete csn.definitions[n]
205
238
  }
206
- // add service annotations
207
- Object.assign(csn.definitions[srvName], {
208
- '@cds.dp.ordId': ordId,
209
- '@cds.external': true,
210
- '@data.product': true,
211
- '@protocol' : 'none' // don't serve via OData
212
- })
213
239
  }
214
240
 
241
+ // add the service
242
+ csn.definitions[srvName] = {
243
+ kind : 'service',
244
+ '@cds.dp.ordId': ordId,
245
+ '@cds.external': true,
246
+ '@data.product': true,
247
+ '@protocol' : 'none' // don't serve via OData
248
+ };
249
+ if (ctxName) csn.definitions[srvName]['@data.product.schema'] = ctxName;
250
+
251
+
215
252
  //
216
253
  // ---------- step 6: move entity/element annotations into extensions section -------------------
217
254
  //
package/bin/export.js CHANGED
@@ -111,7 +111,7 @@ class Export extends Minify {
111
111
  async export (minified,o) {
112
112
  if (this.options.to === '-') return super.print (minified)
113
113
  if (this.options.to.endsWith('/*')) this.options.to = this.export_to()
114
- console.log(`\n${this.name}ing APIs to`, this.options.to, '...\n')
114
+ console.log(`\n${this.name}ing APIs to`, cds.utils.local(this.options.to), '...\n')
115
115
  const exports = []; exports.push(...this.export_models (minified))
116
116
  if (o.texts) exports.push (...this.export_texts (minified))
117
117
  if (o.data) exports.push (this.export_data (minified))
package/bin/help.js CHANGED
@@ -12,7 +12,6 @@ module.exports = exports = Object.assign (help, {formatHelp, handleCompletion, h
12
12
 
13
13
  *i* | *init* jump-start cds-based projects
14
14
  *a* | *add* add a feature to an existing project
15
- * * | *gen* generate models/data using a descriptive prompt [beta]
16
15
  *m* | *import* add models from external sources
17
16
  *c* | *compile* compile cds models to different outputs
18
17
  *p* | *parse* parses given cds models
@@ -32,10 +31,10 @@ module.exports = exports = Object.assign (help, {formatHelp, handleCompletion, h
32
31
  * * | *logout* logout from extensible multitenant SaaS app
33
32
  * * | *pull* pull base model of extensible SaaS app
34
33
  * * | *push* push extension to extensible SaaS app
35
- *t* | *lint* run linter for env or model checks
34
+ * * | *lint* run linter for env or model checks
36
35
  *v* | *version* get detailed version information
37
36
  * * | *completion* add/remove cli completion for cds commands
38
- *?* | *help* get detailed usage information
37
+ *h* | *help* get detailed usage information
39
38
 
40
39
  Learn more about each command using:
41
40
  *cds help* <command> or
package/bin/import.js CHANGED
@@ -1,7 +1,7 @@
1
1
  module.exports = Object.assign(_import, {
2
2
  _name: 'import',
3
- options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group' ],
4
- shortcuts: [ '-s', '-4', '-o', '-as', '-n', '-ns', '-d', undefined, '-c', undefined, undefined, '-', '-f'],
3
+ options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group', '--into-namespace', '--data-product' ],
4
+ shortcuts: [ '-s', '-4', '-o', '-as', '-n', '-ns', '-d',undefined, '-c', undefined, undefined, '-2', '-', '-f'],
5
5
  flags: [ '--dry', '--force', '--no-copy', '--no-save', '--keep-namespace', '--data-product', '--edmx', '--rfc', '--odata', '--openapi', '--asyncapi' ],
6
6
  help: `
7
7
  # SYNOPSIS
@@ -55,6 +55,16 @@ module.exports = Object.assign(_import, {
55
55
  Destination name under which the remote service can be reached.
56
56
  Not supported by import types "edmx", "openapi" or "asyncapi".
57
57
 
58
+ *--data-product*
59
+ Tells cds import to import a data product. The source must be a valid
60
+ data product CSN file, e.g. as found on http://api.sap.com/dataproducts.
61
+ (Not released with general availability yet)
62
+
63
+ *-2 | --into-namespace* <string>
64
+ Specifies a custom namespace to be used for the imported service.
65
+ This will override the namespace that was specified via the the
66
+ @title annotation of the original service.
67
+
58
68
  *Below options are valid for OData only*
59
69
 
60
70
  *--keep-namespace*
@@ -153,7 +163,7 @@ async function _import(args, options = {}) {
153
163
  if (options.from) {
154
164
  if (!cds.import.from[options.from]) {
155
165
  // the required plugin might not be installed, e.g. rfc importer
156
- throw messages.UNSUPPORTED_FROM_OPTION;
166
+ throw messages.UNSUPPORTED_FROM_OPTION.replace('{0}', options.from).replace('{1}', Object.keys(cds.import.from).join(', '));
157
167
  }
158
168
  csn = await cds.import.from[options.from](input, apiOptions);
159
169
  } else {
package/bin/repl.js CHANGED
@@ -35,7 +35,7 @@ module.exports = Object.assign (repl, {
35
35
  *cds eval* to evaluate and execute JavaScript.
36
36
  `})
37
37
 
38
- async function repl (_, options={}) {
38
+ async function repl ([project], options={}) {
39
39
 
40
40
  const GREEN = '\x1b[32m', RESET = '\x1b[0m'
41
41
  const cds = require('../lib/cds')
@@ -46,9 +46,9 @@ async function repl (_, options={}) {
46
46
  inspect.defaultOptions.depth = 11
47
47
 
48
48
  Object.defineProperty (cds,'repl',{value:true})
49
- await cds.service.bindings
49
+ await cds.service.bindings // REVISIT: this pre-populates many of the cds facade's lazy properties
50
50
  let context={}, repl = { context, displayPrompt(){} }
51
- if (options.run) await _run (options.run)
51
+ if ((options.run ??= project)) await _run (options.run)
52
52
  if (options.use) _export (...options.use.split(','))
53
53
 
54
54
  repl = require('repl').start ({
@@ -94,14 +94,10 @@ async function repl (_, options={}) {
94
94
  process.on = (event, listener) => event !== 'uncaughtException' && event !== 'unhandledRejection' && process.addListener(event, listener)
95
95
 
96
96
  async function _run (project) {
97
- try {
98
- if (require.resolve('@cap-js/cds-test', {paths: [cds.root, cds.home]})) {
99
- await cds.test (project)
100
- }
101
- } catch (e) {
102
- if (e.code !== 'MODULE_NOT_FOUND') throw e
103
- await cds.exec ('--project', project, '--port', '0', '--in-memory?')
104
- }
97
+ await require('../bin/serve') ([project], { project, port: 0,
98
+ 'in-memory?': true,
99
+ 'with-mocks': true
100
+ })
105
101
  _export ('entities', 'services')
106
102
  }
107
103
 
package/bin/up.js CHANGED
@@ -28,31 +28,88 @@ module.exports = Object.assign(up, {
28
28
  `})
29
29
 
30
30
  const { execSync } = require('node:child_process')
31
+ const { tmpdir } = require('node:os')
32
+ const crypto = require('node:crypto')
31
33
  const cds = require('../lib/cds')
32
- const { highlight } = require('../lib/util/term')
33
- const { exists, fs, path, path: { join } } = cds.utils
34
+ const { highlight, warn, dim } = require('../lib/util/term')
35
+ const { exists, fs, path, path: { join }, read, write } = cds.utils
36
+ const { ask4 } = require('../lib/util/question')
34
37
  const { readProject } = require('../lib/init/projectReader')
38
+ const { mkdirp, copy } = cds.utils
39
+ const { containerize } = require('../bin/util/containerize')
40
+ const { calcDirChecksum } = require('../lib/util/checksum')
35
41
 
36
- const run = cmd => {
37
- console.log(highlight(cmd))
42
+ const DEBUG = /\b(y|all|cli)\b/.test(process.env.DEBUG) ? console.debug : undefined
43
+
44
+ const run = (cmd, { continueOnFail = false, silent = false } = {}) => {
45
+ const redactedCmd = cmd.replace(/(--docker-password=)(['"]?)[^'"\s]+(['"]?)/, '$1[REDACTED]$3')
46
+ console.log(highlight(redactedCmd))
38
47
  try {
39
48
  return execSync(cmd, {
40
- stdio: 'inherit',
49
+ stdio: silent ? 'pipe' : 'inherit',
41
50
  env: { ...process.env, FORCE_COLOR: cds.utils.colors.enabled }
42
51
  })
43
52
  } catch (error) {
53
+ if (continueOnFail) {
54
+ if (!silent)
55
+ console.error(error.message)
56
+ return
57
+ }
44
58
  throw error.message
45
59
  }
46
60
  }
47
61
 
62
+ /**
63
+ * Get or create checksum file path in temp directory for UI5 apps
64
+ * @param {string} appName - The name of the app
65
+ * @returns {string} - Path to checksum file in temp directory
66
+ */
67
+ function getChecksumFilePath(appName) {
68
+ const projectHash = crypto.createHash('md5').update(cds.root).digest('hex').substring(0, 8)
69
+ return join(tmpdir(), `cds-ui5-checksum-${projectHash}-${appName}.txt`)
70
+ }
71
+
72
+ /**
73
+ * Read stored checksum for an app from temp directory
74
+ * @param {string} appName - The name of the app
75
+ * @returns {string|null} - The stored checksum or null if not found
76
+ */
77
+ function readStoredChecksum(appName) {
78
+ const checksumFile = getChecksumFilePath(appName)
79
+ if (exists(checksumFile)) {
80
+ try {
81
+ return fs.readFileSync(checksumFile, 'utf-8').trim()
82
+ } catch (e) {
83
+ // Ignore read errors and assume no checksum is stored
84
+ }
85
+ }
86
+ return null
87
+ }
88
+
89
+ /**
90
+ * Write checksum for an app to temp directory
91
+ * @param {string} appName - The name of the app
92
+ * @param {string} checksum - The checksum to store
93
+ */
94
+ function writeStoredChecksum(appName, checksum) {
95
+ const checksumFile = getChecksumFilePath(appName)
96
+ fs.writeFileSync(checksumFile, checksum, 'utf-8')
97
+ }
98
+
48
99
  async function up() {
49
100
  const project = readProject()
50
- const { apps, appName, hasApprouter, hasXsuaa, isMonorepoMicroservice } = project
101
+ const { apps, appName, hasApprouter, hasXsuaa, isMonorepoMicroservice, k8s } = project
102
+
103
+ const html5DeployerPath = join(cds.root, cds.env.folders.app, 'html5-deployer', 'resources')
51
104
 
52
105
  for (const { app } of apps) {
53
106
  const appPath = path.join(cds.root, cds.env.folders.app, app)
54
- if (exists(path.join(appPath, 'package.json')) && !exists(path.join(appPath, 'package-lock.json'))) {
55
- run(`npm i --prefix ${appPath}`)
107
+ if (exists(path.join(appPath, 'package.json'))) {
108
+ if (!exists(path.join(appPath, 'package-lock.json'))) {
109
+ run(`npm i --prefix ${appPath}`)
110
+ } else if (process.env.CI) {
111
+ run(`npm ci --prefix ${appPath}`)
112
+ }
56
113
  }
57
114
  }
58
115
 
@@ -74,8 +131,37 @@ async function up() {
74
131
  }
75
132
 
76
133
  if (cds.cli.options.to === 'k8s') {
77
- if (!exists('chart')) run('cds add helm')
78
- if (!exists('containerize.yaml')) run('cds add containerize')
134
+ if (!exists('chart') || !exists('containerize.yaml')) run('cds add kyma')
135
+
136
+ if (project.hasHtml5Repo) {
137
+ if (apps.length > 0) {
138
+ await mkdirp(html5DeployerPath)
139
+ }
140
+
141
+ for (const { app } of apps) {
142
+ const appPath = path.join(cds.root, cds.env.folders.app, app)
143
+ const zipPath = path.join(appPath, `dist/${app}.zip`)
144
+
145
+ // Calculate checksum for the app source folder (excluding node_modules and dist)
146
+ const currentChecksum = await calcDirChecksum(
147
+ appPath,
148
+ (dir) => !dir.includes('node_modules') && !dir.includes('dist')
149
+ )
150
+
151
+ const storedChecksum = readStoredChecksum(app)
152
+ const needsBuild = currentChecksum !== storedChecksum || !exists(zipPath)
153
+
154
+ if (needsBuild) {
155
+ DEBUG?.(dim(`build required for '${app}' – checksums`, { old: storedChecksum, new: currentChecksum }))
156
+ run(`npm run build --prefix ${appPath}`)
157
+ writeStoredChecksum(app, currentChecksum)
158
+ } else {
159
+ console.log(highlight(`Skipping build for UI5 app '${app}' - no changes detected (checksum: ${currentChecksum.substring(0, 8)}...)`))
160
+ }
161
+
162
+ await copy(zipPath).to(html5DeployerPath, `${app}.zip`)
163
+ }
164
+ }
79
165
  } else {
80
166
  if (!exists('mta.yaml')) {
81
167
  run('cds add mta')
@@ -96,13 +182,78 @@ async function up() {
96
182
 
97
183
  if (cds.cli.options.to === 'k8s') {
98
184
  const ns = cds.cli.options.namespace
99
- const nsHelm = ns ? `--namespace ${ns} --create-namespace` : ''
185
+ const nsHelm = ns ? `--namespace ${ns}` : ''
100
186
  const nsKubectl = ns ? `-n ${ns}` : ''
101
187
 
102
188
  run('npm i --package-lock')
103
- run('ctz containerize.yaml --log --push')
189
+ run('cds build --production')
190
+
191
+ if (ns) {
192
+ const k8sNamespaceResult = run(`kubectl get namespace ${ns} -o jsonpath="{.metadata.name}"`, { continueOnFail: true, silent: true })
193
+ if (!k8sNamespaceResult) {
194
+ run(`kubectl create namespace ${ns}`)
195
+ }
196
+ }
197
+
198
+ const isValidDomain = str => /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(str)
199
+ const { image, imagePullSecret } = k8s.values.global ?? {}
200
+ let registry = image?.registry
201
+ if (!process.env.CI) {
202
+ if (!registry || !isValidDomain(registry)) {
203
+ const valuesPath = 'chart/values.yaml'
204
+ const [yn] = await ask4([warn(`invalid registry server '${registry}' detected, override in ${valuesPath}? (Y/n)`)])
205
+ if (yn?.toLowerCase() !== 'y' || process.env.CI) {
206
+ throw `registry parameter is invalid, please specify it in ${valuesPath}`
207
+ } else {
208
+ registry = image.registry = await ask4(['registry server (e.g. index.docker.io): '])
209
+ const values = (await read(valuesPath)).replace(/(registry:\s*).*/, `$1${registry}`)
210
+ await write(values).to(valuesPath)
211
+
212
+ const containerize = (await read('containerize.yaml')).replace(/(repository:\s*).*/, `$1${registry}`)
213
+ await write(containerize).to('containerize.yaml')
214
+ }
215
+ }
216
+ if (imagePullSecret?.name) {
217
+ const imagePullSecretResult = run(`kubectl get secret ${imagePullSecret.name} ${nsKubectl} -o jsonpath="{.metadata.name}"`, { continueOnFail: true, silent: true })
218
+ if (!imagePullSecretResult) {
219
+ const yn = await ask4([`image pull secret '${imagePullSecret.name}' not found in namespace '${ns || 'default'}' – do you want to create it? (Y/n) `])
220
+ if (yn?.toLowerCase() !== 'y' || process.env.CI) {
221
+ throw `image pull secret '${imagePullSecret.name}' is required to pull images from the registry`
222
+ } else {
223
+ const [username, email] = await ask4([
224
+ 'registry username: ',
225
+ 'registry email: ',
226
+ ])
227
+ const password = await ask4(['registry password: '], { redacted: true })
228
+ run(`kubectl create secret docker-registry ${imagePullSecret.name} ${nsKubectl} --docker-server=${registry} --docker-username=${username} --docker-password='${password}' --docker-email=${email}`)
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ const modules = await containerize()
235
+ const helmArgs = []
236
+ modules?.forEach(m => {
237
+ if (m.path === 'gen/srv') {
238
+ helmArgs.push(`--set srv.annotations.deployment.deployment-checksum=${m.checksum}`)
239
+ }
240
+ if (m.path === 'app/router') {
241
+ helmArgs.push(`--set approuter.annotations.deployment.deployment-checksum=${m.checksum}`)
242
+ }
243
+ if (m.path === 'app/html5-deployer') {
244
+ helmArgs.push(`--set html5-apps-deployer.annotations.job.job-checksum=${m.checksum}`)
245
+ }
246
+ if (m.path === 'gen/db') {
247
+ helmArgs.push(`--set hana-deployer.annotations.job.job-checksum=${m.checksum}`)
248
+ }
249
+ if (m.path === 'gen/mtx/sidecar') {
250
+ helmArgs.push(`--set sidecar.annotations.deployment.deployment-checksum=${m.checksum}`)
251
+ }
252
+ })
253
+
104
254
  let helmUpgrade = `helm upgrade --install ${appName} ./gen/chart ${nsHelm} --wait --wait-for-jobs --timeout=10m`
105
- if (hasXsuaa) helmUpgrade += ' --set-file xsuaa.jsonParameters=xs-security.json'
255
+ if (hasXsuaa) helmArgs.push(' --set-file xsuaa.jsonParameters=xs-security.json')
256
+ if (helmArgs.length > 0) helmUpgrade += ` ${helmArgs.join(' ')}`
106
257
  run(helmUpgrade)
107
258
  run(`kubectl rollout status deployment ${appName}-srv ${nsKubectl} --timeout=8m`)
108
259
  if (hasApprouter) run(`kubectl rollout status deployment ${appName}-approuter ${nsKubectl} --timeout=8m`)