@sap/cds-dk 9.2.1 → 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/bind.js +5 -8
  3. package/bin/up.js +19 -13
  4. package/lib/bind/cf.js +4 -0
  5. package/lib/bind/k8s.js +21 -0
  6. package/lib/bind/plugin.js +9 -0
  7. package/lib/build/buildTaskProviderFactory.js +8 -14
  8. package/lib/build/constants.js +1 -0
  9. package/lib/build/index.js +17 -2
  10. package/lib/build/plugin.js +5 -7
  11. package/lib/build/provider/hana/index.js +4 -4
  12. package/lib/env/schemas/cds-rc.js +2 -2
  13. package/lib/import/odata/metadataconverter/MetadataConverter.js +5 -0
  14. package/lib/init/constants.js +3 -2
  15. package/lib/init/projectReader.js +17 -5
  16. package/lib/init/template/ams/index.js +1 -2
  17. package/lib/init/template/approuter/files/containerize.yaml.hbs +1 -1
  18. package/lib/init/template/approuter/index.js +1 -1
  19. package/lib/init/template/data/as-csv.js +10 -7
  20. package/lib/init/template/data/as-json.js +7 -4
  21. package/lib/init/template/enterprise-messaging/index.js +3 -2
  22. package/lib/init/template/enterprise-messaging-shared/index.js +3 -2
  23. package/lib/init/template/extension/files/.base/index.cds +6 -6
  24. package/lib/init/template/extension/index.js +5 -5
  25. package/lib/init/template/github-actions/files/kyma-info/action.yaml +1 -1
  26. package/lib/init/template/github-actions/files/kyma-setup/action.yaml +1 -9
  27. package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +25 -12
  28. package/lib/init/template/github-actions/files/workflows/kyma.yaml.hbs +8 -5
  29. package/lib/init/template/github-actions/files/workflows/release.yaml.hbs +30 -0
  30. package/lib/init/template/github-actions/files/workflows/test.yaml.hbs +6 -3
  31. package/lib/init/template/github-actions/index.js +28 -14
  32. package/lib/init/template/hana/index.js +2 -1
  33. package/lib/init/template/html5-repo/files/mta.yaml.hbs +5 -1
  34. package/lib/init/template/html5-repo/files/package.json.hbs +1 -1
  35. package/lib/init/template/html5-repo/index.js +27 -19
  36. package/lib/init/template/ias/files/mta.yaml.hbs +5 -4
  37. package/lib/init/template/ias/files/values.yaml.hbs +41 -40
  38. package/lib/init/template/mta/files/mta.yaml.hbs +1 -1
  39. package/lib/init/template/mta/files/package.json +1 -1
  40. package/lib/init/template/multitenancy/files/mta.yaml.hbs +1 -1
  41. package/lib/init/template/multitenancy/files/values.yaml.hbs +1 -1
  42. package/lib/init/template/nodejs/files/.vscode/extensions.json +0 -1
  43. package/lib/init/template/nodejs/files/package.json.hbs +3 -9
  44. package/lib/init/template/portal/files/mta.yaml.hbs +5 -1
  45. package/lib/init/template/portal/index.js +0 -1
  46. package/lib/init/template/sample/files/app/_i18n/i18n.properties +15 -0
  47. package/lib/init/template/sample/files/app/_i18n/i18n_de.properties +15 -0
  48. package/lib/init/template/sample/files/app/admin-books/fiori-service.cds +14 -15
  49. package/lib/init/template/sample/files/app/browse/fiori-service.cds +44 -44
  50. package/lib/init/template/sample/files/app/common.cds +99 -84
  51. package/lib/init/template/sample/files/db/schema.cds +22 -17
  52. package/lib/init/template/sample/files/package.json.hbs +1 -3
  53. package/lib/init/template/sample/files/srv/admin-service.cds +4 -3
  54. package/lib/init/template/sample/files/srv/cat-service.cds +28 -11
  55. package/lib/init/template/tiny-sample/files/nodejs/db/schema.cds +3 -3
  56. package/lib/init/template/typescript/files/package.json +1 -1
  57. package/lib/init/template/ui5/index.js +22 -0
  58. package/lib/init/template/xsuaa/files/mta.yaml.hbs +2 -1
  59. package/lib/init/template/xsuaa/index.js +7 -5
  60. package/lib/lint/eslintHelp.json +5 -3
  61. package/npm-shrinkwrap.json +15 -15
  62. package/package.json +1 -1
  63. package/lib/init/template/github-actions/files/cf-github-deploy/action.yaml +0 -48
  64. package/lib/init/template/sample/files/javascript/admin-service.js.hbs +0 -24
  65. package/lib/init/template/sample/files/javascript/cat-service.js.hbs +0 -43
  66. package/lib/init/template/sample/files/typescript/admin-service.ts.hbs +0 -16
  67. package/lib/init/template/sample/files/typescript/cat-service.ts.hbs +0 -38
package/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ 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.3.1 - 2025-09-08
10
+
11
+ ### Fixed
12
+
13
+ - `cds add ias` correctly sets the `subscription-manager` dependencies endpoint for Node.js.
14
+ - `cds add data --records` respects the max length of a string field when it is annotated with `@Communication.IsEmailAddress`
15
+ - `cds add data --records --format csv` correctly escaping complex properties nested within structs.
16
+ - `cds add data --records` correctly generates values for structs in cases when the entity and the struct, used by the entity, have both a property with the same name.
17
+ - `cds add html5-repo` also binds the `html5-repo-host` service to the approuter.
18
+ - `cds up` has improved support for setups with multiple microservices.
19
+ - `cds add mta` will add no `role-collections` parameters if there are some specified in `xs-security.json`.
20
+ - `cds add github-actions` correctly generates a release workflow.
21
+ - `cds add github-actions` uses a simplified and more resilient Kyma setup script.
22
+ - `cds import` now correctly imports EDMX files with empty NavigationPropertyPath tags.
23
+
24
+ ## Version 9.3.0 - 2025-09-01
25
+
26
+ ### Added
27
+
28
+ - `cds bind -a` is now also supported for Kyma, where the app prefix can be passed for `-a`, e.g. `bookshop-srv`.
29
+ - `cds build --for hana` now trims leading and trailing whitespaces in csv-Files if build option `trimCsvWhitespaces` is set.
30
+ - `cds add github-actions` sets required `permissions`.
31
+ - `cds up` supports a deployment layout where approuter or portal service configuration is in a top-level `.deploy` folder.
32
+ - `cds add ias` sets the `access-token-format` to `jwt` by default.
33
+
34
+ ### Changed
35
+
36
+ - `cds add sample` provides more i18n translations.
37
+ - `cds add ias` sets the `xsuaa-cross-consumption` field to `true` by default.
38
+
39
+ ### Fixed
40
+
41
+ - `cds add workzone` with missing `sap.app` config in `manifest.json` does not throw a `TypeError`.
42
+ - `cds add ias` adds a subdomain-less application URL in `redirect_uris`.
43
+
9
44
  ## Version 9.2.1 - 2025-08-22
10
45
 
11
46
  ### Changed
package/bin/bind.js CHANGED
@@ -46,7 +46,7 @@ module.exports = Object.assign(cds_bind, {
46
46
 
47
47
  *-a | --to-app-services* <app>
48
48
 
49
- Bind services of a given application (Cloud Foundry only).
49
+ Bind services of a given application.
50
50
 
51
51
  *--no-create-service-key*
52
52
 
@@ -83,7 +83,7 @@ const { bind, storeBindings } = require('../lib/bind/shared');
83
83
 
84
84
 
85
85
  async function cds_bind(args, options) {
86
- const { on, 'to-app-services': app, to, out, 'output-file': outputFile, exec, credentials } = options
86
+ const { on = 'cf', 'to-app-services': app, to, out, 'output-file': outputFile, exec, credentials } = options
87
87
 
88
88
  if (exec) {
89
89
  process.env.CDS_ENV ??= 'hybrid'
@@ -103,10 +103,6 @@ async function cds_bind(args, options) {
103
103
  options.out ??= outputFile
104
104
  }
105
105
 
106
- if (on === 'k8s' && app) {
107
- throw `Option ${term.bold('--to-app-services')} is only supported for Cloud Foundry.`;
108
- }
109
-
110
106
  if (credentials === true) {
111
107
  throw `Option ${term.bold('--credentials')} requires a JSON object or a file path.`;
112
108
  }
@@ -155,9 +151,10 @@ async function cds_bind(args, options) {
155
151
  options.for ??= 'hybrid';
156
152
 
157
153
  if (app) {
158
- const cds = require('../lib/cds');
154
+ const cds = require('../lib/cds')
159
155
  const resolvedServices = [], unresolvedServices = {}
160
- const services = (await require('../lib/util/cf').services(app)).filter(s => Object.keys(s.credentials).length > 0)
156
+ const plugin = require(`../lib/bind/${on}`)
157
+ const services = await plugin.services4(app)
161
158
 
162
159
  if (!services) console.warn(`no services bound to app ${app} - nothing to bind`)
163
160
 
package/bin/up.js CHANGED
@@ -30,7 +30,7 @@ module.exports = Object.assign(up, {
30
30
  const { execSync, spawn } = require('node:child_process')
31
31
  const cds = require('../lib/cds')
32
32
  const { highlight, bold } = require('../lib/util/term')
33
- const { exists, fs, path } = cds.utils
33
+ const { exists, fs, path, path: { join } } = cds.utils
34
34
  const { readProject } = require('../lib/init/projectReader')
35
35
 
36
36
  const run = cmd => {
@@ -67,7 +67,8 @@ const runDetectingError = async (cmd, errorRegex) => {
67
67
  }
68
68
 
69
69
  async function up() {
70
- const { apps, appName, hasApprouter, hasXsuaa } = readProject()
70
+ const project = readProject()
71
+ const { apps, appName, hasApprouter, hasXsuaa, isMonorepoMicroservice } = project
71
72
 
72
73
  for (const { app } of apps) {
73
74
  const appPath = path.join(cds.root, cds.env.folders.app, app)
@@ -79,14 +80,18 @@ async function up() {
79
80
  if (exists('mtx/sidecar') && !exists('mtx/sidecar/package-lock.json')) {
80
81
  run('npm i --package-lock-only --prefix mtx/sidecar')
81
82
  }
82
- if (exists('app/router/package.json') && !exists('app/router/package-lock.json')) {
83
- run('npm i --package-lock-only --prefix app/router')
84
- }
85
- if (exists('app/portal/package.json') && !exists('app/portal/package-lock.json')) {
86
- run('npm i --package-lock-only --prefix app/portal')
83
+
84
+ const app = cds.env.folders.app
85
+ const legacyToPreferred = {
86
+ [join(app, 'router')]: '.deploy/app-router',
87
+ [join(app, 'portal')]: '.deploy/portal',
88
+ [join(app, 'html5-deployer')]: '.deploy/html5-deployer'
87
89
  }
88
- if (exists('app/html5-deployer/package.json') && !exists('app/html5-deployer/package-lock.json')) {
89
- run('npm i --package-lock-only --prefix app/html5-deployer')
90
+ for (const [legacy, preferred] of Object.entries(legacyToPreferred)) {
91
+ const root = exists(preferred) ? preferred : legacy
92
+ if (exists(join(root, 'package.json')) && !exists(join(root, 'package-lock.json'))) {
93
+ run(`npm i --package-lock-only --prefix ${root}`)
94
+ }
90
95
  }
91
96
 
92
97
  if (cds.cli.options.to === 'k8s') {
@@ -100,11 +105,12 @@ async function up() {
100
105
  run('npm i --package-lock-only')
101
106
  }
102
107
  }
103
- const prefix = execSync('npm prefix', { encoding: 'utf8' }).trim()
104
- if (prefix !== cds.root) {
105
- const source = path.join(prefix, 'package-lock.json')
108
+
109
+ if (isMonorepoMicroservice) {
110
+ const { npmWorkspaceRoot } = project
111
+ const source = path.join(npmWorkspaceRoot, 'package-lock.json')
106
112
  const target = path.join(process.cwd(), 'package-lock.json')
107
- if (exists(target) || fs.lstatSync(target).isSymbolicLink()) fs.unlinkSync(target)
113
+ if (exists(target) && fs.lstatSync(target).isSymbolicLink()) fs.unlinkSync(target)
108
114
  const relative = path.relative(process.cwd(), source)
109
115
  fs.symlinkSync(relative, target, 'junction')
110
116
  }
package/lib/bind/cf.js CHANGED
@@ -33,6 +33,10 @@ module.exports = new class CFBind extends require('./plugin') {
33
33
  return service
34
34
  }
35
35
 
36
+ async services4(app) {
37
+ return (await cf.services(app)).filter(s => Object.keys(s.credentials).length > 0)
38
+ }
39
+
36
40
  async resolve(name, binding) {
37
41
  this.target ??= await cf.target()
38
42
  this.#checkApiEndpoint(name, binding)
package/lib/bind/k8s.js CHANGED
@@ -30,6 +30,27 @@ module.exports = new class K8SBind extends require('./plugin') {
30
30
  return service
31
31
  }
32
32
 
33
+ services4(app) {
34
+ const { execSync } = require('node:child_process')
35
+ const json = cmd => JSON.parse(execSync(cmd, { encoding: 'utf8' }))
36
+
37
+ const bindings = json('kubectl get servicebindings -o json').items
38
+ .filter(b => b.metadata.name.startsWith(app))
39
+ if (!bindings.length) throw `cannot find services with metadata name starting with '${app}'`
40
+
41
+ const offerings = Object.fromEntries(
42
+ json('kubectl get serviceinstances -o json').items
43
+ .map(i => [i.metadata.name, i.spec.serviceOfferingName])
44
+ )
45
+
46
+ for (const b of bindings) {
47
+ const offering = offerings[b.spec.serviceInstanceName]
48
+ b.label = b.type = offering
49
+ b.name = b.metadata.name
50
+ }
51
+ return bindings
52
+ }
53
+
33
54
  async resolve(name, binding) {
34
55
  const { context, namespace, cluster } = await this.#context4(binding)
35
56
 
@@ -36,6 +36,15 @@ module.exports = class BindPlugin {
36
36
  throw new Error('credentials4 not implemented')
37
37
  }
38
38
 
39
+ /**
40
+ * Returns the services for the given app
41
+ * @param {string} app - The name of the app
42
+ * @returns {Promise<Array>} A promise that resolves to an array of bound service names
43
+ */
44
+ services4(_app) {
45
+ throw new Error('services4 not implemented')
46
+ }
47
+
39
48
  /**
40
49
  * Resolves the binding configuration for the given service name
41
50
  * @param {string} name - The name of the service
@@ -8,24 +8,13 @@ const BuildTaskProvider = require('./buildTaskProvider')
8
8
  const { BuildError, normalizePath } = require('./util')
9
9
  const DEBUG = cds.debug('cli|build')
10
10
 
11
- /**
12
- * @typedef {{
13
- * clean?: boolean,
14
- * outputMode?: number,
15
- * for?: string,
16
- * src?: string,
17
- * dest?: string,
18
- * } & {[key:string]:unknown}} BuildTaskProviderFactoryOptions
19
- */
20
-
21
11
  /**
22
- * TODO
23
- * @typedef {object} Task
12
+ * @typedef {import('./plugin').Task} Task
24
13
  */
25
14
 
26
15
  class BuildTaskProviderFactory {
27
16
  /**
28
- * @param {BuildTaskProviderFactoryOptions} options - build options
17
+ * @param {import('./index').BuildOptions} options - build options
29
18
  */
30
19
  constructor(options = {}) {
31
20
  options.outputMode = options.outputMode || OUTPUT_MODE_FILESYSTEM
@@ -43,7 +32,7 @@ class BuildTaskProviderFactory {
43
32
  }
44
33
  }
45
34
  })
46
- /** @type {{ options: BuildTaskProviderFactoryOptions, tasks: Task}} */
35
+ /** @type {{ options: import('./index').BuildOptions, tasks: Task[]}} */
47
36
  this._context = { options, tasks: [] } // resolved tasks
48
37
  }
49
38
  get options() {
@@ -112,6 +101,7 @@ class BuildTaskProviderFactory {
112
101
  * 'for' defines an alias for built-in plugins like 'hana', 'java', 'node', 'fiori' or 'mtx'.
113
102
  * 'use' defines the fully qualified module name of custom build plugins implementations.
114
103
  * @param {Task} task
104
+ * @returns {typeof import('./plugin')}
115
105
  */
116
106
  getPlugin(task) {
117
107
  const provider = this._getProvider(task)
@@ -227,6 +217,10 @@ class PluginBuildTaskProvider extends BuildTaskProvider {
227
217
  })
228
218
  }
229
219
 
220
+ /**
221
+ * @param {{for: string}} key
222
+ * @returns {Task}
223
+ */
230
224
  getTask(key) {
231
225
  // plugin entry is already validated, a taskDefaults default member exists
232
226
  const task = JSON.parse(JSON.stringify(this.plugins.get(key.for)?.taskDefaults ?? {}))
@@ -38,6 +38,7 @@ exports.OPTION_WS = "ws" // run a workspa
38
38
  exports.OPTION_WS_PACK = "ws-pack" // enable tarball based packaging of workspace dependencies
39
39
  exports.CSV_FILE_TARGET = "csvFileTarget" // target folder when copying CSV files to the deployment target folder ./db/src/gen/*, default is 'data'
40
40
  exports.CSV_FILE_DETECTION = "csvFileDetection" // detect CSV files based on CDS model location - ENABLED by default
41
+ exports.CSV_TRIM_WHITESPACES = "trimCsvWhitespaces" // trim whitespaces from CSV column names and values - DISABLED by default
41
42
  exports.EXTENSION_POINT_VALIDATION = "extensionPointValidation" // validate extension point restrictions defined by the extended SaaS app - ENABLED by default
42
43
  exports.MTX_SIDECAR_DB_VALIDATION = "mtxSidecarDbValidation" // validate consistency of app and sidecar db configuration - ENABLED by default
43
44
  exports.CONTINUE_UNRESOLVED_SCHEMA_CHANGES = "continueUnresolvedSchemaChanges" // build doesn't fail if unresolved HANA schema changes are detected
@@ -6,10 +6,25 @@ const { BuildError } = require('./util')
6
6
 
7
7
  module.exports = { build, register, Plugin, BuildError }
8
8
 
9
+ // reuse schema definition from cds-rc.js used for package.json code completion
10
+ /** @typedef{import('../env/schemas/cds-rc')['$defs']['buildTaskType']['enum'][number]} BuildFor */
11
+
12
+ // using (string & {}) prevents widening of `'x' | string` to just `string`
13
+ /**
14
+ * @typedef {{
15
+ * clean?: boolean,
16
+ * outputMode?: number,
17
+ * for?: BuildFor | (string & {}),
18
+ * src?: string,
19
+ * dest?: string,
20
+ * model?: string[]
21
+ * } & {[key:string]:unknown}} BuildOptions
22
+ */
23
+
9
24
  /**
10
25
  * Executes cds build in the directory defined by cds.root.
11
26
  *
12
- * @param {object} options - command options as defined by build command.
27
+ * @param {BuildOptions} options - command options as defined by build command.
13
28
  */
14
29
  async function build(options = {}) {
15
30
  if (!fs.existsSync(cds.root) || !cds.utils.isdir(cds.root)) {
@@ -31,4 +46,4 @@ async function build(options = {}) {
31
46
 
32
47
  function register(id, plugin) {
33
48
  return require('./plugins').register(id, plugin)
34
- }
49
+ }
@@ -21,7 +21,7 @@ const DEBUG = cds.debug('cli|build')
21
21
 
22
22
  /**
23
23
  * The build plugin creates the build output for a dedicated build task. It is uniquely identified
24
- * by the build task's `for<` or `use` property. The `use` property represents
24
+ * by the build task's `for<` or `use` property. The `use` property represents
25
25
  * the fully qualified node module path of the build plugin implementation.
26
26
  * The build task engine defines the following protocol. The methods are invoked in descending order:
27
27
  * - `init()` - optional
@@ -40,13 +40,11 @@ class Plugin {
40
40
 
41
41
  /**
42
42
  * @type {Task | undefined}
43
- * @private
44
43
  */
45
44
  _task
46
45
 
47
46
  /**
48
47
  * @type {Context | undefined}
49
- * @private
50
48
  */
51
49
  _context
52
50
 
@@ -69,7 +67,7 @@ class Plugin {
69
67
  */
70
68
  _messages = []
71
69
  /**
72
- * Returns a list of build and compiler messages created by this build plugin.
70
+ * Returns a list of build and compiler messages created by this build plugin.
73
71
  * Supported message severities are 'Info', 'Warning', and 'Error'.
74
72
  */
75
73
  get messages() {
@@ -176,9 +174,9 @@ class Plugin {
176
174
 
177
175
  /**
178
176
  * Asynchronously copies a single file or the entire directory structure from 'src' to 'dest', including subdirectories and files.
179
- *
177
+ *
180
178
  * Note: The file names are stored in the list of files written by this build plugin.
181
- *
179
+ *
182
180
  * @param {string} src - The absolute or relative source path of the file or directory to copy.
183
181
  * Relative paths will be resolved to this task's source path.
184
182
  * @returns {FileWriter}
@@ -216,7 +214,7 @@ class Plugin {
216
214
 
217
215
  /**
218
216
  * Adds the given user message and severity to the list of messages issued by this build task.
219
- *
217
+ *
220
218
  * User messages will be logged after CDS build has been finished based on the log-level that has been set.
221
219
  * By default messages with severity <em>warning</em> and <em>error</em> will be logged.
222
220
  * @param {string} message - the message text
@@ -74,7 +74,7 @@ class HanaBuildPlugin extends InternalBuildPlugin {
74
74
  await this._writePackageJson()
75
75
  }
76
76
 
77
- // copy native hana artifacts from other workspaces including .hdbmigrationtable files defined for
77
+ // copy native hana artifacts from other workspaces including .hdbmigrationtable files defined for
78
78
  // common model artifacts that are not part of the current project
79
79
  if (wsPaths.length || cds.cli.options?.[OPTION_WS] || hasOptionValue(this.task.options[OPTION_WS], true)) {
80
80
  // first is winning, e.g. a .hdiconfig file might exist in multiple workspaces
@@ -272,7 +272,7 @@ class HanaBuildPlugin extends InternalBuildPlugin {
272
272
 
273
273
  for (const file of csvFiles) {
274
274
  if (this.isStagingBuild() || file.startsWith(this.task.options.compileDest)) {
275
- await stripComments(file)
275
+ await stripComments(file, null, this.task.options.trimCsvWhitespaces)
276
276
  }
277
277
  }
278
278
  }
@@ -394,7 +394,7 @@ class HanaBuildPlugin extends InternalBuildPlugin {
394
394
  }
395
395
  }
396
396
  // The last-dev CSN shall only be updated, if all .hdbmigrationtable files could be successfully updated.
397
- // Of course, .hdbmigrationtable files already written would need to be manually reverted before the
397
+ // Of course, .hdbmigrationtable files already written would need to be manually reverted before the
398
398
  // cds build command is again executed.
399
399
  await Promise.all(promises)
400
400
 
@@ -578,7 +578,7 @@ class HanaBuildPlugin extends InternalBuildPlugin {
578
578
  /**
579
579
  * Returns all hana native artifacts located in db/src/** workspace folders.
580
580
  * @param {Array} workspacePaths Array of workspace paths, 'cds.root' is excluded from the list
581
- * @returns {Array} Array of workspace files, each entry contains the db src directory,
581
+ * @returns {Array} Array of workspace files, each entry contains the db src directory,
582
582
  * the relative file path and the file name.
583
583
  */
584
584
  async _getWorkspaceFiles(workspacePaths) {
@@ -278,7 +278,7 @@ module.exports = {
278
278
  },
279
279
 
280
280
  buildTaskType: {
281
- enum: [
281
+ enum: /**@type { const } */ ([
282
282
  "nodejs",
283
283
  "java",
284
284
  "hana",
@@ -288,7 +288,7 @@ module.exports = {
288
288
  "node-cf",
289
289
  "java-cf",
290
290
  "helm"
291
- ],
291
+ ]),
292
292
  enumDescriptions: [
293
293
  "Node.js build plugin",
294
294
  "Java build plugin",
@@ -1620,6 +1620,11 @@ class PropertyPathExpression extends Expression {
1620
1620
  const targetOption = this.getContext().getConverterOptions().target;
1621
1621
  if (targetOption === MetadataConverter.TARGETS.LIBRARY) {
1622
1622
  target = { [this._targetName]: this.getElement().elements[0].text };
1623
+ } else if (!this.getElement().elements && this._targetName === '$NavigationPropertyPath') {
1624
+ //elements are not given, when <NavigationPropertyPath/> is in EDMX, which is used to refresh the current context
1625
+ return {
1626
+ "=": "($self)"
1627
+ };
1623
1628
  } else {
1624
1629
  target = this.getElement().elements[0].text;
1625
1630
  }
@@ -5,7 +5,7 @@ module.exports = {
5
5
  COMMAND_ADD: 'add',
6
6
 
7
7
  JAVA_LTS_VERSIONS: [17, 21],
8
- MAVEN_ARCHETYPE_VERSION: '4.1.1',
8
+ MAVEN_ARCHETYPE_VERSION: '4.3.0',
9
9
 
10
10
  OPTIONS: Object.freeze({
11
11
  COMPLETION: 'completion',
@@ -60,6 +60,7 @@ module.exports = {
60
60
  NOTIFICATIONS: 'notifications',
61
61
  WORKZONE: 'workzone',
62
62
  WORKZONE_STANDARD: 'workzone-standard',
63
+ UI5: 'ui5',
63
64
  DATA: 'data',
64
65
  HTTP: 'http',
65
66
  LINT: 'lint',
@@ -74,7 +75,7 @@ module.exports = {
74
75
 
75
76
  URLS: Object.freeze({
76
77
  PIPELINE_HELP: 'https://www.project-piper.io/',
77
- SAMPLES: 'https://github.com/SAP-samples/cloud-cap-samples',
78
+ SAMPLES: 'https://github.com/capire/samples',
78
79
  CAPIRE: 'https://cap.cloud.sap',
79
80
  MAVEN_ARCHETYPE_HELP: `https://cap.cloud.sap/docs/java/developing-applications/building#the-maven-archetype`,
80
81
  MAVEN_INSTALL_HELP: `https://maven.apache.org/install.html`,
@@ -1,5 +1,6 @@
1
1
  const cds = require('../cds')
2
- const { join, resolve, basename } = require('path')
2
+ const { join, resolve, basename } = require('node:path')
3
+ const { execSync } = require('node:child_process')
3
4
  const { fs, exists, isfile, isdir, path } = cds.utils
4
5
  const { parseXml } = require('./xml')
5
6
  const { REGEX_JAVA_VERSION, JAVA_LTS_VERSIONS } = require('./constants')
@@ -9,6 +10,7 @@ module.exports = new class ProjectReader {
9
10
 
10
11
  constructor() {
11
12
  this.readProject = this.readProject.bind(this)
13
+ this.cachedWorkspaceRoot = null
12
14
  }
13
15
 
14
16
  #setPackageInfo() {
@@ -105,6 +107,9 @@ module.exports = new class ProjectReader {
105
107
  * @property {boolean} hasDynatrace Indicates if the project uses Dynatrace.
106
108
  * @property {boolean} hasCloudLogging Indicates if the project uses Cloud Logging.
107
109
  * @property {String} extendsApp Indicates which project is extended if any.
110
+ * @property {String} githubSlug GitHub org/repo.
111
+ * @property {boolean} isMonorepoMicroservice Indicates if the project is a monorepo microservice.
112
+ * @property {String} npmWorkspaceRoot The root path of the monorepo workspace.
108
113
  */
109
114
 
110
115
  /**
@@ -124,7 +129,6 @@ module.exports = new class ProjectReader {
124
129
  const _isTypescript = () => !!pkgJson().dependencies?.typescript || !!pkgJson().devDependencies?.typescript
125
130
  const _hasTyper = () => !!pkgJson().dependencies?.['@cap-js/cds-typer'] || !!pkgJson().devDependencies?.['@cap-js/cds-typer']
126
131
  const _inProd = plugin => require(`./template/${plugin}`).hasInProduction(env) || cds.cli.argv.includes(plugin) || cds.cli.options.add?.has(plugin)
127
- const _ui5 = () => pkgJson().sapux
128
132
  const _uiEmbedded = () => exists(join(env.folders.app, 'xs-app.json'))
129
133
  const _uiModule = () => !_uiEmbedded() && isdir(env.folders.app) && fs.readdirSync(resolve(cds.root, env.folders.app)).length > 0
130
134
  const _isJava = () => this.isJava
@@ -134,6 +138,7 @@ module.exports = new class ProjectReader {
134
138
  }
135
139
  const reserved = {
136
140
  dependencies: () => pkgJson().dependencies,
141
+ devDependencies: () => pkgJson().devDependencies,
137
142
  has: x => _inProd(x),
138
143
  profile: () => cds.cli.options?.for,
139
144
  language: () => _isJava() ? 'java' : 'nodejs',
@@ -183,7 +188,7 @@ module.exports = new class ProjectReader {
183
188
  appPath: () => env.folders.app,
184
189
  appVersion: () => appVersion,
185
190
  jdkVersion: () => jdkVersion,
186
- approuterPath: () => (_uiEmbedded() ? join(env.folders.app) : join(env.folders.app, 'router')).replace(/\\/g, '/'),
191
+ approuterPath: () => (_uiEmbedded() ? join(env.folders.app) : exists('.deploy/app-router') ? join('.deploy', 'app-router') : join(env.folders.app, 'router')).replace(/\\/g, '/'),
187
192
  appName: () => appName,
188
193
  cleanedAppName: () => appName.replaceAll('_', '-'),
189
194
  strippedAppName: () => appName.replace(/-/g, ''),
@@ -192,15 +197,22 @@ module.exports = new class ProjectReader {
192
197
  hasUIEmbedded: _uiEmbedded,
193
198
  hasUIModule: _uiModule,
194
199
  hasUI: () => _uiEmbedded() || _uiModule(),
195
- isUI5: _ui5,
196
- hasUI5: _ui5,
197
200
  literal: () => () => text => text,
198
201
  hasMTXRoute: () => _inProd('extensibility') || (_inProd('multitenancy') && !_isJava()),
199
202
  hasMTXRouteJava: () => (_inProd('helm-unified-runtime') || _inProd('helm')) && _isJava() && _inProd('multitenancy'),
200
203
  sap: () => cds.cli.options.sap,
201
204
  imageRegistry: () => exists('chart/values.yaml') ? cds.load.yaml(join(cds.root, 'chart/values.yaml')).global?.image?.registry ?? '<your-container-registry-server>' : '<your-container-registry-server>',
202
205
  extendsApp: _extends,
206
+ npmWorkspaceRoot: () => {
207
+ if (this.cachedWorkspaceRoot) return this.cachedWorkspaceRoot
208
+ this.cachedWorkspaceRoot = execSync('npm prefix', { encoding: 'utf8', cwd: cds.root, stdio: 'pipe' }).trim()
209
+ return this.cachedWorkspaceRoot
210
+ },
211
+ isMonorepoMicroservice: function () {
212
+ return this.npmWorkspaceRoot() !== cds.root
213
+ },
203
214
  hasRoles: () => {}, roles: () => {}, // for xs-security generation
215
+ hasCustomRoleCollections: () => exists('xs-security.json') && !!JSON.parse(fs.readFileSync(path.join(cds.root, 'xs-security.json')))['role-collections']
204
216
  }
205
217
 
206
218
  // Automatically creates availability checks for `cds add` commands.
@@ -1,4 +1,3 @@
1
- const cds = require('../../../cds')
2
1
  const { readProject } = require('../../projectReader')
3
2
  const { merge, sort } = require('../../merge')
4
3
  const { srv4 } = require('../../registries/mta')
@@ -15,7 +14,7 @@ module.exports = class AmsTemplate extends require('../../plugin') {
15
14
  }
16
15
 
17
16
  static hasInProduction(env) {
18
- return env.requires?.auth?.ams || cds.cli.options?.add?.has('ams') || cds.cli.argv?.some(arg => arg?.match?.(/(^|,)ams(,|$)/))
17
+ return env.requires?.auth?.ams || readProject().devDependencies?.['@sap/ams']
19
18
  }
20
19
 
21
20
  async run() {
@@ -10,6 +10,6 @@ modules:
10
10
  type: nodejs
11
11
  builder: builder-jammy-base
12
12
  {{/hasHelmUnifiedRuntime}}
13
- path: app/router
13
+ path: {{& approuterPath}}
14
14
  env:
15
15
  BP_NODE_RUN_SCRIPTS: ""
@@ -19,7 +19,7 @@ module.exports = class ApprouterTemplate extends require('../../plugin') {
19
19
  }
20
20
 
21
21
  static hasInProduction(env) {
22
- return exists(join(env.folders.app, 'xs-app.json')) || exists(join(env.folders.app, 'router', 'xs-app.json'))
22
+ return exists(join('.deploy', 'app-router')) || exists(join(env.folders.app, 'xs-app.json')) || exists(join(env.folders.app, 'router', 'xs-app.json'))
23
23
  }
24
24
 
25
25
  async run() {
@@ -48,11 +48,7 @@ module.exports = async (data={}, csn, { headerOnly=false, separator=',' }={}) =>
48
48
  }
49
49
  else {
50
50
  if (i === 0) header.push(name)
51
- if (Array.isArray(value) || typeof value === 'object') { // `array of`/`many` and `cds.Map`
52
- value = JSON.stringify(value)
53
- }
54
- value = escape(value)
55
- record.push(value)
51
+ record.push(processValue(value))
56
52
  }
57
53
  }
58
54
  records.push(record.join(separator))
@@ -71,16 +67,23 @@ module.exports = async (data={}, csn, { headerOnly=false, separator=',' }={}) =>
71
67
  return value
72
68
  }
73
69
 
70
+ function processValue(v) {
71
+ if (Array.isArray(v) || typeof v === 'object') { // `array of`/`many` and `cds.Map`
72
+ return escape(JSON.stringify(v))
73
+ }
74
+ return escape(v)
75
+ }
76
+
74
77
  function resolveStruct(v, prefix) {
75
78
  return Object.entries(v).reduce((acc, [key, val]) => {
76
79
  //structs can be within structs
77
- if (typeof val === 'object') {
80
+ if (typeof val === 'object' && !Array.isArray(val)) {
78
81
  const {headers, records} = resolveStruct(val, prefix + '_' + key);
79
82
  acc.headers = acc.headers.concat(headers);
80
83
  acc.records = acc.records.concat(records);
81
84
  } else {
82
85
  acc.headers.push(prefix + '_' + key);
83
- acc.records.push(val);
86
+ acc.records.push(processValue(val));
84
87
  }
85
88
  return acc;
86
89
  }, {headers: [], records: []});
@@ -125,8 +125,9 @@ function dataForField(element, artifact, csn, record, data, parent, depth, refer
125
125
 
126
126
  if (element.is_struct) {
127
127
  let struct = {}
128
+ Object.defineProperty(struct, '$id', {value: record['$id'], enumerable: false})
128
129
  for (const el of element.elements) {
129
- struct[el.name] = dataForField(el, artifact, csn, record, data, null, depth+1, referenceData)
130
+ struct[el.name] = dataForField(el, artifact, csn, struct, data, null, depth+1, referenceData)
130
131
  }
131
132
  return struct
132
133
  }
@@ -176,7 +177,7 @@ function dataForField(element, artifact, csn, record, data, parent, depth, refer
176
177
  || element['@Communication.IsEmailAddress']
177
178
  || element['@cds.on.insert']?.['='] === '$user'
178
179
  || element['@cds.on.update']?.['='] === '$user') {
179
- return randomEmailAddress(element.name, record['$id'], csn)
180
+ return randomEmailAddress(element, record['$id'], csn)
180
181
  }
181
182
 
182
183
  let str = `${element.name}-${record['$id']}`
@@ -450,11 +451,13 @@ function randomFromList(list) {
450
451
  return list[randomNumber() % list.length]
451
452
  }
452
453
 
453
- function randomEmailAddress(firstName, num, csn) {
454
+ function randomEmailAddress(element, num, csn) {
454
455
  let addr = fakerValue('faker.internet.email', csn, false)
455
456
  if (addr) return addr
456
457
  const provider = randomFromList(['example.com', 'example.org', 'example.net'])
457
- return `${firstName}.${num.toString(36)}@${provider}`
458
+ let localPart = `${element.name}.${num.toString(36)}`
459
+ // -1 in substring for @
460
+ return (element.length ? localPart.substring(0, element.length - 1 - provider.length) : localPart) + `@${provider}`
458
461
  }
459
462
 
460
463
  function randomStrForRegex(regexStr, csn) {
@@ -1,5 +1,5 @@
1
1
  const cds = require('../../../cds')
2
- const { read, yaml: YAML } = cds.utils
2
+ const { read, exists, yaml: YAML } = cds.utils
3
3
  const { readProject } = require('../../projectReader')
4
4
  const { merge } = require('../../merge')
5
5
  const { srv4, enterpriseMessaging, xsuaa } = require('../../registries/mta')
@@ -37,7 +37,6 @@ module.exports = class EnterpriseMessagingTemplate extends require('../../plugin
37
37
  project.cloudevents = cds.cli.options.cloudevents
38
38
  await merge(__dirname, 'files/package.json.hbs').into(configFile, { with: project })
39
39
  }
40
- await merge(__dirname, 'files/event-mesh.json.hbs').into('event-mesh.json', { project })
41
40
  }
42
41
 
43
42
  async combine() {
@@ -76,5 +75,7 @@ module.exports = class EnterpriseMessagingTemplate extends require('../../plugin
76
75
  if (hasHelm || hasHelmUnifiedRuntime) {
77
76
  await merge(__dirname, 'files', 'values.yaml').into('chart/values.yaml')
78
77
  }
78
+
79
+ if (!exists('event-mesh.json')) await merge(__dirname, 'files/event-mesh.json.hbs').into('event-mesh.json', { project })
79
80
  }
80
81
  }