@sap/cds-dk 8.4.2 → 8.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 (45) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/bin/debug.js +90 -0
  3. package/bin/watch.js +12 -2
  4. package/lib/bind/userCdsrcJsonStorageProvider.js +0 -19
  5. package/lib/build/buildTaskEngine.js +1 -1
  6. package/lib/build/provider/hana/2migration.js +34 -20
  7. package/lib/build/provider/hana/index.js +74 -41
  8. package/lib/build/provider/internalBuildTaskProvider.js +1 -1
  9. package/lib/build/provider/mtx-extension/index.js +4 -4
  10. package/lib/build/provider/mtx-sidecar/index.js +2 -2
  11. package/lib/build/provider/nodejs/index.js +3 -19
  12. package/lib/build/util.js +41 -17
  13. package/lib/deploy/to-hana/hdiDeployUtil.js +14 -10
  14. package/lib/import/message.js +2 -1
  15. package/lib/import/odata/metadataconverter/MetadataConverter.js +5 -0
  16. package/lib/import/odata/v4parser.js +32 -43
  17. package/lib/init/add.js +26 -0
  18. package/lib/init/constants.js +4 -5
  19. package/lib/init/index.js +17 -25
  20. package/lib/init/merge.js +1 -4
  21. package/lib/init/projectReader.js +75 -9
  22. package/lib/init/registries/mta.js +0 -1
  23. package/lib/init/template/approuter/files/package.json +1 -1
  24. package/lib/init/template/cloud-logging/files/package.json.hbs +1 -12
  25. package/lib/init/template/cloud-logging/files/values.yaml.hbs +16 -0
  26. package/lib/init/template/cloud-logging/index.js +14 -10
  27. package/lib/init/template/data/as-json.js +1 -1
  28. package/lib/init/template/data/index.js +1 -26
  29. package/lib/init/template/esm/files/package.json.hbs +3 -0
  30. package/lib/init/template/esm/index.js +16 -0
  31. package/lib/init/template/handler/files/handler.xs.hbs +56 -0
  32. package/lib/init/template/handler/index.js +84 -9
  33. package/lib/init/template/helm/index.js +4 -1
  34. package/lib/init/template/http/index.js +102 -35
  35. package/lib/init/template/sample/files/nodejs/{admin-service.js → admin-service.js.hbs} +8 -1
  36. package/lib/init/template/sample/files/nodejs/{cat-service.js → cat-service.js.hbs} +8 -3
  37. package/lib/init/template/sample/index.js +4 -2
  38. package/lib/init/template/telemetry/files/package.json.hbs +18 -0
  39. package/lib/init/template/telemetry/index.js +8 -2
  40. package/lib/init/template/typescript/files/package.json +1 -1
  41. package/lib/init/template/workzone-standard/index.js +1 -1
  42. package/lib/init/types.d.ts +0 -6
  43. package/lib/watch/node-watch.js +25 -9
  44. package/npm-shrinkwrap.json +26 -31
  45. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ This project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
9
9
 
10
+ ## Version 8.5.0 - 2024-11-26
11
+
12
+ ### Changed
13
+
14
+ - `cds import` now generates generic action/function in case of bound action/function collision and unbound function collision with @cds.validate = false.
15
+ - `cds add typescript` creates projects with `cds-tsx` to run apps instead of former `cds-ts`.
16
+ - `cds deploy` writes error output to `stderr`.
17
+ - `cds add http` now evaluates mocks users when creating `Authorization` headers
18
+
19
+ ### Added
20
+
21
+ - `cds debug` lets you debug Node.js applications running locally or in Cloud Foundry.
22
+ - `cds watch --inspect` and `cds watch --inspect-brk` activate the debugger in the same way as the standard Node.js CLI options, i.e. they accept a `host:port` combination and `0` as values.
23
+ - The existing `cds watch --debug` is now an alias to `cds watch --inspect`.
24
+ - `cds add cloud-logging` adds cloud-logging as an alternative for application logging in Kyma.
25
+ - `cds add cloud-logging --with-telemetry` or `cds add telemetry` adds cloud-logging as well as support for `cap-js/telemetry` plugin in Kyma.
26
+ - `cds add handler` now also works for Node.js projects. It creates an implementation file for each service with event handlers for all entites and actions.
27
+ - `cds add esm` creates ESM-compatible Node.js projects. Sample code added by `add sample` and `add handler` will respect this setting if added aftwards. Existing code will not be adjusted by `cds add esm`, though.
28
+
29
+ ### Fixed
30
+
31
+ - `cds import` throws proper error message if Annotation element doesn't have Term attribute.
32
+ - `cds add approuter` no longer enforces Node.js 22, which is not supporter by current `@sap/approuter` version 17. The previous Node.js version 20 is used again.
33
+ - `cds add http` works for actions without parameters.
34
+ - `cds add workzone` includes the `updateManifestJson` task on initial generation.
35
+ - `cds build --ws` no longer creates migration tables in shared database or wrong workspace.
36
+
10
37
  ## Version 8.4.2 - 2024-11-19
11
38
 
12
39
  ### Changed
@@ -23,7 +50,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
23
50
  - `cds add hana` adds the `requires.db` entry to `.cdsrc.json` for Java projects.
24
51
  - `cds add data` creates decimal values with correct scale `0` if only precision is given, like in `Decimal(15)`.
25
52
 
26
-
27
53
  ## Version 8.4.1 - 2024-11-08
28
54
 
29
55
  ### Added
package/bin/debug.js ADDED
@@ -0,0 +1,90 @@
1
+ module.exports = Object.assign(debug, {
2
+ options: ['--port', '--host'],
3
+ shortcuts: [],
4
+ flags: [/*'--vscode', '--k8s'*/],
5
+ help: `
6
+ # SYNOPSIS
7
+
8
+ *cds debug* [<app>]
9
+
10
+ Debugs a CAP-based application in the Chrome Developer Tools.
11
+
12
+ If <app> is given, it's assumed to be running on the currently logged-in Cloud Foundry space.
13
+
14
+ If no <app> is given, the app in the current working is started with 'cds watch --debug'.
15
+
16
+ # OPTIONS
17
+
18
+ *--host* the debug host (default: '127.0.0.1')
19
+
20
+ *--port* the debug port (default: 9229)
21
+
22
+ # EXAMPLES
23
+
24
+ *cds debug*
25
+ *cds debug* bookshop-srv
26
+ `})
27
+
28
+ async function debug() {
29
+ const cds = require('../lib/cds')
30
+ const { fork, spawn, execSync, exec } = require('node:child_process')
31
+ const os = require('node:os').platform()
32
+
33
+ const [appName] = cds.cli.argv
34
+ const { port = 9229, host = '127.0.0.1', vscode } = cds.cli.options
35
+ const ide = vscode ? 'vscode' : 'chrome'
36
+
37
+ if (!appName) { // local app
38
+ const script = process.argv[1] // `.../cds` or `.../cds-tsx`
39
+ console.log(`Starting '${cds.utils.path.basename(script)} watch --debug'`)
40
+ fork(script, ['watch', '--debug']).on('error', e => { throw 'Failed to start debug process:' + e.message })
41
+ } else if (cds.cli.options.k8s) { // Kyma/Kubernetes
42
+ // to be implemented
43
+ } else { // Cloud Foundry
44
+ const script = `ps aux | grep '[n]ode' | awk '{print \\$2}' | head -n 1`
45
+ const pid = execSync(`cf ssh ${appName} -c "${script}"`, { encoding: 'utf-8', shell: true }).trim()
46
+ if (!pid) throw `Node.js process for app '${appName}' not found`
47
+ execSync(`cf ssh ${appName} -c "kill -usr1 ${pid}"`, { stdio: 'inherit' })
48
+ console.log(`Opening SSH tunnel for CF app '${appName}'`)
49
+ spawn('cf', ['ssh', '-N', '-L', `${port}:${host}:${port}`, appName], { stdio: 'inherit' })
50
+ }
51
+
52
+ const maxAttempts = 15
53
+ let attempts = 0
54
+
55
+ const openDevTools = async () => {
56
+ attempts++
57
+ try {
58
+ switch (ide) {
59
+ case 'chrome': {
60
+ const res = await fetch(`http://localhost:${port}/json/list`)
61
+ const data = await res.json()
62
+ if (!data?.length) throw 'No debugger found on port ' + port
63
+ const wsUrl = data[0].webSocketDebuggerUrl
64
+ let command
65
+ const url = `devtools://devtools/bundled/inspector.html?ws=${wsUrl.split('ws://')[1]}`
66
+ console.log(`Opening Chrome DevTools at ${url}`)
67
+ if (os === 'darwin') {
68
+ command = `open -a "Google Chrome" '${url}'`
69
+ } else if (os === 'win32') {
70
+ command = `start chrome "${url}"`
71
+ } else {
72
+ command = `google-chrome '${url}' || chromium-browser '${url}' || chromium '${url}'`
73
+ }
74
+ exec(command, (error) => { if (error) throw 'Failed to open DevTools: ' + error.message })
75
+ break
76
+ }
77
+ case 'vscode': {
78
+ // to be implemented
79
+ }
80
+ }
81
+ } catch (err) {
82
+ if (attempts < maxAttempts) {
83
+ setTimeout(openDevTools, 1000) // connection fails sometimes, retry
84
+ } else {
85
+ throw `Failed to connect to debugger after ${maxAttempts} attempts: ${err}`
86
+ }
87
+ }
88
+ }
89
+ openDevTools()
90
+ }
package/bin/watch.js CHANGED
@@ -1,6 +1,6 @@
1
- const watchOnlyOptions = ['--ext', '--livereload', '--open']
1
+ const watchOnlyOptions = ['--ext', '--livereload', '--open', '--debug', '--inspect', '--inspect-brk']
2
2
  module.exports = Object.assign ( watch, {
3
- flags: [ '--debug', '--in-memory', '--in-memory?', '--with-mocks' ],
3
+ flags: [ '--in-memory', '--in-memory?', '--with-mocks' ],
4
4
  options: [ '--port', ...watchOnlyOptions ],
5
5
  help: `
6
6
  # SYNOPSIS
@@ -42,6 +42,16 @@ module.exports = Object.assign ( watch, {
42
42
  Specify from which profile(s) the binding information is taken.
43
43
  *Example:* cds w --profile hybrid,production
44
44
 
45
+ *--debug* / *--inspect* <host:port | 127.0.0.1:9229>
46
+
47
+ Activate debugger on the given host:port.
48
+ If port 0 is specified, a random available port will be used.
49
+
50
+ *--inspect-brk* <host:port | 127.0.0.1:9229>
51
+
52
+ Activate debugger on the given host:port and break at start of user script.
53
+ If port 0 is specified, a random available port will be used.
54
+
45
55
  # SEE ALSO
46
56
 
47
57
  *cds serve --help* for the different start options.
@@ -1,6 +1,5 @@
1
1
  const os = require('os');
2
2
  const path = require('path');
3
- const readline = require('readline');
4
3
 
5
4
  const fs = require('fs').promises;
6
5
  const { bold } = require('../util/term');
@@ -11,24 +10,6 @@ const gitUtil = require('../util/git');
11
10
  const CDSRC_FILENAME = '.cdsrc-private.json';
12
11
 
13
12
  class UserCdsrcJsonStorageProvider {
14
- async askBooleanQuestion(question) {
15
- const rl = readline.createInterface({
16
- input: process.stdin,
17
- output: process.stdout
18
- });
19
-
20
- question = question.trim() + ' ';
21
- return new Promise(resolve => {
22
- rl.question(question, answer => {
23
- if (answer === 'y' || answer === 'Y') {
24
- resolve(true);
25
- } else {
26
- resolve(false);
27
- }
28
- rl.close();
29
- });
30
- });
31
- }
32
13
 
33
14
  async storeServices(services, options = {}) {
34
15
  let jsonPath = options.out || CDSRC_FILENAME;
@@ -39,7 +39,7 @@ class BuildTaskEngine {
39
39
  // validate required @sap namespace models - log only
40
40
  const unresolved = BuildTaskEngine._resolveRequiredSapServices(tasks)
41
41
  if (unresolved.length > 0) {
42
- throw new BuildError(`Required CDS models [${unresolved.join(', ')}] cannot be resolved. Use the 'npm install' command to install up-to-date versions of the missing packages.`)
42
+ throw new BuildError(`Required CDS models [${unresolved.join(', ')}] cannot be resolved. Run the 'npm install' command to install up-to-date versions of the missing packages.`)
43
43
  }
44
44
 
45
45
  // create build plugins
@@ -7,15 +7,15 @@ const cdscVersion = `-- generated by cds-compiler version ${cdsc.version()}`
7
7
  const { getArtifactCdsPersistenceName } = cdsc
8
8
  const LOG = cds.log('cli|build'), DEBUG = cds.debug('cli|build')
9
9
 
10
- module.exports = async (model, beforeImage, srcPath, options = {}) => {
11
- const { definitions, migrations, afterImage, journalFileNames } = _compileToHana(model, options, beforeImage)
10
+ async function compileToHdbMigration(model, beforeImage, srcPath, options = {}) {
11
+ const { definitions, migrations, afterImage, migrationTableNames } = _compileToHana(model, options, beforeImage)
12
12
  const definitionResult = []
13
13
 
14
14
  for (const { name, suffix, sql } of definitions) {
15
15
  let definitionEntry = { name, suffix, content: sql }
16
16
 
17
17
  if (suffix === '.hdbtable') {
18
- if (journalFileNames.has(name)) {
18
+ if (migrationTableNames.has(name)) {
19
19
  const migration = migrations.find(migration => migration.name === name)
20
20
  definitionEntry = await _2migrationtable(srcPath, migration || _emptyMigration(name), sql)
21
21
  }
@@ -27,6 +27,22 @@ module.exports = async (model, beforeImage, srcPath, options = {}) => {
27
27
  return { definitions: definitionResult, afterImage }
28
28
  }
29
29
 
30
+ function getJournalEntities(model, options = {}) {
31
+ return cds.reflect(model).all(item => {
32
+ if (item.kind === 'entity' && item['@cds.persistence.journal'] === true) {
33
+ if (item['@cds.persistence.skip'] === true || item['@cds.persistence.exists'] === true) {
34
+ options.messages?.push(new BuildMessage(`[hdbmigrationtable] annotation @cds.persistence.journal skipped for entity '${item.name}' as persistence exists`, SEVERITY_WARNING))
35
+ }
36
+ return true
37
+ }
38
+ return false
39
+ })
40
+ }
41
+
42
+ function getMigrationTableName(entity, model) {
43
+ return getArtifactCdsPersistenceName(entity.name, 'quoted', model, 'hana')
44
+ }
45
+
30
46
  function _compileToHana(model, options, beforeImage) {
31
47
  let definitions = []
32
48
  let migrations = []
@@ -58,20 +74,20 @@ function _compileToHana(model, options, beforeImage) {
58
74
  migrations = result.migrations
59
75
  afterImage = result.afterImage
60
76
  }
61
- let journalFileNames = new Set()
77
+ let migrationTableNames = new Set()
62
78
  if (afterImage) {
63
79
  // 1. determine journal file names before _filterJournalArtifacts is called as getArtifactCdsPersistenceName
64
80
  // might no longer return the proper file name for the reduced CSN, e.g. for draft entities
65
- journalFileNames = _getJournalFileNames(afterImage, options)
81
+ migrationTableNames = _getMigrationTableNames(afterImage, options)
66
82
 
67
83
  // 2. leave only persisted journal-related entities in afterImage
68
84
  afterImage = _filterJournalArtifacts(afterImage)
69
85
  }
70
86
 
71
- _debug(definitions, migrations, journalFileNames)
87
+ _debug(definitions, migrations, migrationTableNames)
72
88
 
73
89
  return {
74
- definitions, migrations, afterImage, journalFileNames
90
+ definitions, migrations, afterImage, migrationTableNames
75
91
  }
76
92
  }
77
93
 
@@ -158,21 +174,13 @@ function _emptyMigration(name) {
158
174
  return { name, suffix: ".hdbmigrationtable", changeset: [] }
159
175
  }
160
176
 
161
- function _getJournalFileNames(model, options) {
162
- const journalNames = new Set(cds.reflect(model).all(item => {
163
- if (item.kind === 'entity' && item['@cds.persistence.journal'] === true) {
164
- if (item['@cds.persistence.skip'] === true || item['@cds.persistence.exists'] === true) {
165
- options.messages?.push(new BuildMessage(`[hdbmigrationtable] annotation @cds.persistence.journal skipped for entity '${item.name}' as persistence exists`, SEVERITY_WARNING))
166
- }
167
- return true
168
- }
169
- return false
170
- }).map(entity => getArtifactCdsPersistenceName(entity.name, 'quoted', model, 'hana')))
177
+ function _getMigrationTableNames(model, options) {
178
+ const migrationTableNames = getJournalEntities(model, options).map(entity => getMigrationTableName(entity, model))
171
179
 
172
- DEBUG?.(`\n[hdbmigrationtable] found ${journalNames.size} model entities annotated with '@cds.persistence.journal`)
173
- DEBUG?.(`[hdbmigrationtable] ${[...journalNames].join(', ')}\n`)
180
+ DEBUG?.(`\n[hdbmigrationtable] found ${migrationTableNames.length} model entities annotated with '@cds.persistence.journal`)
181
+ DEBUG?.(`[hdbmigrationtable] ${migrationTableNames.join(', ')}\n`)
174
182
 
175
- return journalNames
183
+ return new Set(migrationTableNames)
176
184
  }
177
185
 
178
186
  // only keep journal related entities
@@ -217,3 +225,9 @@ ${migration ? migration.changeset.map(change => change.sql).join('\n') : 'Empty
217
225
  }
218
226
  }
219
227
  }
228
+
229
+ module.exports = {
230
+ compileToHdbMigration,
231
+ getJournalEntities,
232
+ getMigrationTableName
233
+ }
@@ -1,9 +1,9 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
  const cds = require('../../../cds')
4
- const to_hdbmigration = require('./2migration')
4
+ const { compileToHdbMigration, getJournalEntities, getMigrationTableName } = require('./2migration')
5
5
  const InternalBuildPlugin = require('../internalBuildPlugin')
6
- const { BuildError, relativePaths, BuildMessage, getWorkspaces, hasOptionValue, getFiles } = require('../../util')
6
+ const { BuildError, relativePaths, BuildMessage, hasOptionValue, getFiles, getWorkspacePaths } = require('../../util')
7
7
  const { OUTPUT_MODE_NONE, OUTPUT_MODE, CONTENT_PACKAGE_JSON, CONTENT_HDBTABLEDATA, CSV_FILE_DETECTION,
8
8
  CONTENT_ENV, CONTENT_DEFAULT_ENV_JSON, CONTENT_NODE_MODULES, OUTPUT_MODE_RESULT, CONTINUE_UNRESOLVED_SCHEMA_CHANGES,
9
9
  CSV_FILE_TARGET, OPTION_WS } = require('../../constants')
@@ -38,14 +38,15 @@ class HanaBuildPlugin extends InternalBuildPlugin {
38
38
  if (!model) {
39
39
  return
40
40
  }
41
- let workspaceFiles
42
- if (cds.cli.options?.[OPTION_WS] || hasOptionValue(this.task.options[OPTION_WS], true)) {
43
- workspaceFiles = await this._getWorkspaceFiles()
41
+ let wsPaths = []
42
+ if(getJournalEntities(model).length || cds.cli.options?.[OPTION_WS] || hasOptionValue(this.task.options[OPTION_WS], true)) {
43
+ // workspace paths don't include the current project root
44
+ wsPaths = await getWorkspacePaths()
44
45
  }
45
46
 
46
47
  // the order of 1 and 2 is important
47
48
  // 1. compile
48
- const hdiPlugins = await this._compileToHana(model, workspaceFiles)
49
+ const hdiPlugins = await this._compileToHana(model, wsPaths)
49
50
 
50
51
  if (!this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_NONE)) {
51
52
  // 2. copy resources
@@ -54,16 +55,6 @@ class HanaBuildPlugin extends InternalBuildPlugin {
54
55
  } else {
55
56
  await this._copyResourcesExt(src, dest, model)
56
57
  }
57
- // copy native hana artifacts from workspaces
58
- if (workspaceFiles) {
59
- const dest = path.join(this.task.dest, this.isStagingBuild() ? 'src' : 'src/gen')
60
- await Promise.all(
61
- workspaceFiles.map((src) => {
62
- const { dir, filePath } = src
63
- return this.copy(path.join(dir, filePath)).to(path.join(dest, filePath))
64
- })
65
- )
66
- }
67
58
 
68
59
  // create additional stuff in dest
69
60
  await this._writeHdiConfig(hdiPlugins)
@@ -78,6 +69,28 @@ class HanaBuildPlugin extends InternalBuildPlugin {
78
69
  if (!this.hasBuildOption(CONTENT_PACKAGE_JSON, false)) {
79
70
  await this._writePackageJson()
80
71
  }
72
+
73
+ // copy native hana artifacts from other workspaces including .hdbmigrationtable files defined for
74
+ // common model artifacts that are not part of the current project
75
+ if (wsPaths.length || cds.cli.options?.[OPTION_WS] || hasOptionValue(this.task.options[OPTION_WS], true)) {
76
+ // first is winning, e.g. a .hdiconfig file might exist in multiple workspaces
77
+ const wsFiles = (await this._getWorkspaceFiles(wsPaths)).reduce((acc, src) => {
78
+ const { filePath } = src;
79
+ if (!acc.some(file => file.filePath === filePath)) {
80
+ acc.push(src);
81
+ }
82
+ return acc;
83
+ }, []);
84
+
85
+ // copy artifacts to the src/gen folder in inplace mode
86
+ const dest = path.join(this.task.dest, this.isStagingBuild() ? 'src' : 'src/gen')
87
+ await Promise.all(
88
+ wsFiles.map((src) => {
89
+ const { dir, filePath } = src
90
+ return this.copy(path.join(dir, filePath)).to(path.join(dest, filePath))
91
+ })
92
+ )
93
+ }
81
94
  }
82
95
  return this._result
83
96
  }
@@ -264,20 +277,18 @@ class HanaBuildPlugin extends InternalBuildPlugin {
264
277
  *
265
278
  * @param {object} model The compiled csn model
266
279
  */
267
- async _compileToHana(model, workspaceFiles) {
280
+ async _compileToHana(model, wsPaths) {
268
281
  // compile to old format (.hdbcds) or new format (.hdbtable / .hdbview)
269
282
  const format = this.getBuildOption(DEPLOY_FORMAT) || cds.env.requires.db?.[DEPLOY_FORMAT] || cds.env.hana?.[DEPLOY_FORMAT]
270
283
  if (!cds.compile.to[format]) {
271
284
  return Promise.reject(new Error(`Invalid deploy-format defined: ${format}`))
272
285
  }
273
286
 
274
- if (cds.env.features.journal === false
275
- || format === 'hdbcds'
276
- || (!cds.compile.to.hana && Object.values(model.definitions).every(def => !def['@cds.persistence.journal']))) {
287
+ if (cds.env.features.journal === false || format === 'hdbcds') {
277
288
  // compatibility with existing cds.compile.to.hdbtable plugins cds < 8.0.0
278
289
  return await this._compileToHdb(model, format)
279
290
  } else {
280
- return await this._compileToHdbmigration(model, format, workspaceFiles)
291
+ return await this._compileToHdbmigration(model, format, wsPaths)
281
292
  }
282
293
  }
283
294
 
@@ -313,7 +324,7 @@ class HanaBuildPlugin extends InternalBuildPlugin {
313
324
  return hdiPlugins
314
325
  }
315
326
 
316
- async _compileToHdbmigration(model, format, workspaceFiles) {
327
+ async _compileToHdbmigration(model, format, wsPaths) {
317
328
  const hdiPlugins = new Set()
318
329
  const relDestDir = path.relative(this.task.dest, this.task.options.compileDest)
319
330
  const relDbDestDir = path.relative(cds.root, this.task.options.compileDest)
@@ -328,19 +339,30 @@ class HanaBuildPlugin extends InternalBuildPlugin {
328
339
  if (fs.existsSync(lastDevCsnDir)) {
329
340
  lastDev = JSON.parse((await fs.promises.readFile(lastDevCsnDir, 'utf-8')).toString())
330
341
  }
342
+
331
343
  // enforces sqlNames option for compiler in tests, pass options from cds env, ensures that the correct format is taken
332
344
  const options = { ...this.options(), messages: this.messages, sql_mapping: cds.env.sql.names, "deploy-format": format }
333
-
334
- const { definitions, afterImage } = await to_hdbmigration(model, lastDev, dbSrcDir, options)
345
+ const { definitions, afterImage } = await compileToHdbMigration(model, lastDev, dbSrcDir, options)
335
346
  let validationError
336
347
 
348
+ // determine the migration tables originating from a different workspaces
349
+ const journalEntities = getJournalEntities(model)
350
+ const wsMigrationTables = new Set()
351
+ if (journalEntities.length) {
352
+ for (const entity of journalEntities) {
353
+ if (entity['$location']?.file && wsPaths.some(wsPath => path.resolve(cds.root, entity['$location'].file).startsWith(wsPath))) {
354
+ wsMigrationTables.add(getMigrationTableName(entity, model))
355
+ }
356
+ }
357
+ }
358
+
337
359
  for (const { name, suffix, content, changed } of definitions) {
338
360
  hdiPlugins.add(suffix)
339
361
  const fileName = name + suffix
340
362
  if (suffix === FILE_EXT_HDBMIGRATIONTABLE) {
341
- // Skip the .hdbmigrationtable file if its origin is a different workspace.
342
- // The workspace build copies artifacts located in 'db/src/**' workspace folders.
343
- if (!workspaceFiles?.find(wsFile => wsFile.fileName === fileName)) {
363
+ // do not create .hdbmigrationtable files originating from a different workspace
364
+ // the --ws build option will copy these artifacts to either gen/db/src or db/src/gen
365
+ if (!wsMigrationTables.has(name)) {
344
366
  migrationTableFiles.push(path.join(relDbSrcDir, fileName))
345
367
  if (changed) {
346
368
  if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
@@ -350,6 +372,8 @@ class HanaBuildPlugin extends InternalBuildPlugin {
350
372
  } else {
351
373
  DEBUG?.(`no change, keep existing ${fileName}`)
352
374
  }
375
+ } else {
376
+ DEBUG?.(`skip generation of migration table [${fileName}] as it originates from a different workspace`)
353
377
  }
354
378
  } else {
355
379
  if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
@@ -547,21 +571,26 @@ class HanaBuildPlugin extends InternalBuildPlugin {
547
571
 
548
572
  /**
549
573
  * Returns all hana native artifacts located in db/src/** workspace folders.
574
+ * @param {Array} workspacePaths Array of workspace paths, 'cds.root' is excluded from the list
550
575
  * @returns {Array} Array of workspace files, each entry contains the db src directory,
551
576
  * the relative file path and the file name.
552
577
  */
553
- async _getWorkspaceFiles() {
578
+ async _getWorkspaceFiles(workspacePaths) {
579
+ if (!workspacePaths || !workspacePaths.length) {
580
+ return []
581
+ }
554
582
  const dbPaths = new Set()
555
- const workspaces = await getWorkspaces()
556
-
557
- workspaces.forEach(workspace => {
558
- const dbSrc = path.join(cds.root, workspace, 'db/src')
559
- const dbCfg = path.join(cds.root, workspace, 'db/cfg')
560
- if (fs.existsSync(dbSrc)) {
561
- dbPaths.add(dbSrc)
562
- }
563
- if (fs.existsSync(dbCfg)) {
564
- dbPaths.add(dbCfg)
583
+ workspacePaths.forEach(workspacePath => {
584
+ // skip workspace matching the current project if cds build is executed in a workspace folder
585
+ if (cds.root !== workspacePath) {
586
+ const dbSrc = path.join(workspacePath, 'db/src')
587
+ const dbCfg = path.join(workspacePath, 'db/cfg')
588
+ if (fs.existsSync(dbSrc)) {
589
+ dbPaths.add(dbSrc)
590
+ }
591
+ if (fs.existsSync(dbCfg)) {
592
+ dbPaths.add(dbCfg)
593
+ }
565
594
  }
566
595
  })
567
596
  const workspaceFiles = [...dbPaths].reduce((acc, src) => {
@@ -572,11 +601,15 @@ class HanaBuildPlugin extends InternalBuildPlugin {
572
601
  }
573
602
  return true
574
603
  })
575
- files.forEach(file => acc.add({ dir: src, filePath: path.relative(src, file), fileName: path.basename(file) }))
604
+ files.forEach(file => acc.push({ dir: src, filePath: path.relative(src, file), fileName: path.basename(file) }))
576
605
  return acc
577
- }, new Set())
606
+ }, [])
607
+
608
+ return workspaceFiles
609
+ }
578
610
 
579
- return [...workspaceFiles]
611
+ _hasJournalDefinitions(model) {
612
+ return Object.values(model.definitions).some(def => def['@cds.persistence.journal'])
580
613
  }
581
614
 
582
615
  static async _readTemplateAsJson(template) {
@@ -167,7 +167,7 @@ class InternalBuildTaskProvider extends BuildTaskProvider {
167
167
  if (!sidecarEnv.requires["cds.xt.ModelProviderService"]?._in_sidecar) {
168
168
  if (sidecarEnv.profiles?.includes('mtx-sidecar')) {
169
169
  // profile has been set, but cannot be resolved
170
- throw new BuildError("MTX configuration cannot be resolved. Use the 'npm install' command to install up-to-date versions of @sap/cds-mtxs and @sap/cds.")
170
+ throw new BuildError("MTX configuration cannot be resolved. Run the 'npm install' command to install up-to-date versions of @sap/cds-mtxs and @sap/cds.")
171
171
  }
172
172
  throw new BuildError(`Invalid MTX sidecar configuration - profile 'mtx-sidecar' not set.`)
173
173
  }
@@ -85,13 +85,13 @@ class MtxExtensionBuildPlugin extends InternalBuildPlugin {
85
85
  // existence already checked
86
86
  await this.copy(path.join(cds.root, 'package.json')).to(path.join(destExt, 'package.json'))
87
87
 
88
- // copy handlers
89
- const folders = [path.join(cds.root, cds.env.folders.srv, 'handlers')]
88
+ // copy handlers from srv folder
89
+ const folder = path.join(cds.root, cds.env.folders.srv).replace(/[/\\]$/, '')
90
90
  await this.copyNativeContent(cds.root, destExt, res => {
91
91
  if (fs.statSync(res).isDirectory()) {
92
- return folders.some(folder => folder.startsWith(res))
92
+ return res.startsWith(folder)
93
93
  }
94
- if (folders.includes(path.dirname(res)) && /\.js$/.test(res)) {
94
+ if (path.dirname(res).startsWith(folder) && /\.js$/.test(res)) {
95
95
  return true
96
96
  }
97
97
  })
@@ -118,7 +118,7 @@ class MtxSidecarBuildPlugin extends NodejsBuildPlugin {
118
118
  const modelFilePaths = cds.resolve(modelPaths)
119
119
 
120
120
  if (!modelFilePaths || modelFilePaths.length === 0) {
121
- throw new BuildError("No CDS models found in MTX sidecar. Make sure up-to-date versions of the required npm modules are installed.")
121
+ throw new BuildError("No CDS models found in MTX sidecar. Run the 'npm install' command to install up-to-date versions of the required packages.")
122
122
  }
123
123
  DEBUG?.(`sidecar model: ${relativePaths(cds.root, modelFilePaths).join(", ")}`)
124
124
 
@@ -126,7 +126,7 @@ class MtxSidecarBuildPlugin extends NodejsBuildPlugin {
126
126
  const unresolved = resolveRequiredSapModels(modelPaths)
127
127
  if (unresolved.length > 0) {
128
128
  // log error, but don't fail
129
- this.pushMessage(`CDS models [${unresolved.join(', ')}] required by MTX sidecar cannot be resolved. Make sure up-to-date versions of the missing npm modules are installed.`, ERROR)
129
+ this.pushMessage(`CDS models [${unresolved.join(', ')}] required by MTX sidecar cannot be resolved. Run the 'npm install' command to install up-to-date versions of the required packages.`, ERROR)
130
130
  }
131
131
 
132
132
  // using synchronous compilation as cds.root and cds.env have been switched to mtx/sidecar
@@ -3,7 +3,7 @@ const path = require('path')
3
3
  const cds = require('../../../cds')
4
4
  const cmd = require('../../../util/command');
5
5
  const EdmxBuildPlugin = require('../edmxBuildPlugin')
6
- const { BuildError, getWorkspaces } = require('../../util')
6
+ const { BuildError, getWorkspaces, findWorkspaceRoot } = require('../../util')
7
7
  const { OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM, ODATA_VERSION_V2, FOLDER_GEN, CONTENT_EDMX, CONTENT_PACKAGELOCK_JSON,
8
8
  CONTENT_NPMRC, CONTENT_CDSRC_JSON, CONTENT_ENV, CONTENT_DEFAULT_ENV_JSON, FLAVOR_LOCALIZED_EDMX,
9
9
  OPTION_WS_PACK} = require('../../constants')
@@ -160,14 +160,14 @@ class NodejsBuildPlugin extends EdmxBuildPlugin {
160
160
  }
161
161
 
162
162
  async _packWorkspaceDependencies(dest) {
163
- const wsRoot = NodejsBuildPlugin._findWorkspaceRoot(cds.root)
163
+ const wsRoot = findWorkspaceRoot(cds.root)
164
164
  const appPkg = require(path.join(dest, 'package.json'))
165
165
  let workspaces
166
166
  if (this.task.options.workspaces) {
167
167
  workspaces = Array.isArray(this.task.options.workspaces) ? this.task.options.workspaces : [this.task.options.workspaces]
168
168
  } else {
169
169
  // devDependencies are not considered
170
- workspaces = await getWorkspaces(true, false, wsRoot);
170
+ workspaces = await getWorkspaces(wsRoot, true, false);
171
171
  }
172
172
  if (workspaces.length) {
173
173
  let pkgDescriptors
@@ -197,22 +197,6 @@ class NodejsBuildPlugin extends EdmxBuildPlugin {
197
197
  }
198
198
  }
199
199
 
200
- static _findWorkspaceRoot(dir) {
201
- try {
202
- if (require(path.join(dir, 'package.json')).workspaces) {
203
- return dir
204
- }
205
- } catch (err) {
206
- // ignore
207
- }
208
- if (fs.existsSync(path.join(dir, '.gitmodules'))) {
209
- return // project root reached
210
- }
211
- if (path.dirname(dir) !== dir) {
212
- return this._findWorkspaceRoot(path.dirname(dir))
213
- }
214
- }
215
-
216
200
  static async _execNpmPack(dest, wsRoot, workspaces) {
217
201
  const args = ['pack', '-ws', '--json', `--pack-destination=${dest}`]
218
202
  workspaces.forEach(w => args.push(`-w=${w}`))