@sap/cds-dk 8.7.3 → 8.8.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 (76) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/bin/bind.js +7 -8
  3. package/bin/cds.js +2 -4
  4. package/bin/compile/index.js +1 -0
  5. package/bin/deploy/index.js +10 -5
  6. package/bin/watch.js +48 -7
  7. package/lib/bind/{cfKeyProvider.js → cf.js} +43 -53
  8. package/lib/bind/k8s.js +138 -0
  9. package/lib/bind/plugin.js +48 -0
  10. package/lib/bind/shared.js +267 -0
  11. package/lib/build/buildTaskEngine.js +1 -1
  12. package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +0 -2
  13. package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -3
  14. package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml +3 -0
  15. package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +3 -0
  16. package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +2 -1
  17. package/lib/build/util.js +1 -1
  18. package/lib/compile/to-mermaid.js +1 -2
  19. package/lib/deploy/to-hana/hana.js +1 -1
  20. package/lib/deploy/to-hana/mtaUtil.js +0 -13
  21. package/lib/env/schemas/cds-rc.js +17 -30
  22. package/lib/import/common.js +20 -10
  23. package/lib/init/constants.js +1 -1
  24. package/lib/init/merge.js +41 -32
  25. package/lib/init/projectReader.js +15 -7
  26. package/lib/init/registries/mta.js +7 -1
  27. package/lib/init/template/approuter/files/package.json +1 -1
  28. package/lib/init/template/approuter/files/xs-app.json.hbs +0 -11
  29. package/lib/init/template/cloud-logging/files/package.json.hbs +12 -1
  30. package/lib/init/template/cloud-logging/files/values.yaml.hbs +0 -2
  31. package/lib/init/template/cloud-logging/index.js +8 -15
  32. package/lib/init/template/destination/files/values.yaml +4 -4
  33. package/lib/init/template/destination/index.js +1 -1
  34. package/lib/init/template/html5-repo/files/app-package.json.hbs +0 -2
  35. package/lib/init/template/html5-repo/files/containerize.yaml.hbs +25 -0
  36. package/lib/init/template/html5-repo/files/mta.yaml.hbs +1 -2
  37. package/lib/init/template/html5-repo/files/package.json.hbs +1 -1
  38. package/lib/init/template/html5-repo/files/ui-resources/package.json.hbs +11 -0
  39. package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +4 -7
  40. package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -4
  41. package/lib/init/template/html5-repo/index.js +22 -10
  42. package/lib/init/template/http/index.js +1 -1
  43. package/lib/init/template/lint/index.js +7 -1
  44. package/lib/init/template/multitenancy/files/mta.yaml.hbs +0 -2
  45. package/lib/init/template/multitenancy/index.js +1 -1
  46. package/lib/init/template/portal/files/mta.yaml.hbs +9 -0
  47. package/lib/init/template/portal/index.js +3 -1
  48. package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +119 -119
  49. package/lib/init/template/sample/files/app/browse/webapp/manifest.json +1 -1
  50. package/lib/init/template/telemetry/index.js +0 -13
  51. package/lib/init/template/typer/files/package.json +3 -0
  52. package/lib/init/template/workzone-standard/files/cdm.json.hbs +97 -0
  53. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +23 -0
  54. package/lib/init/template/workzone-standard/index.js +70 -39
  55. package/lib/init/template/xsuaa/files/mta.yaml.hbs +2 -2
  56. package/lib/init/template/xsuaa/files/values.yaml.hbs +0 -2
  57. package/lib/init/template/xsuaa/index.js +22 -10
  58. package/lib/lint/io.js +8 -4
  59. package/lib/mtx/util/cf.js +1 -2
  60. package/lib/util/cf.js +104 -84
  61. package/lib/util/command.js +1 -2
  62. package/lib/util/git.js +1 -1
  63. package/npm-shrinkwrap.json +53 -45
  64. package/package.json +1 -1
  65. package/bin/deploy/to-cf.js +0 -0
  66. package/bin/deploy/via-mta.js +0 -0
  67. package/lib/bind/KubernetesApiAccessor.js +0 -81
  68. package/lib/bind/bindingManager.js +0 -89
  69. package/lib/bind/bindingResolver.js +0 -54
  70. package/lib/bind/bindingUtil.js +0 -129
  71. package/lib/bind/index.js +0 -127
  72. package/lib/bind/kubernetesCredentialsProvider.js +0 -84
  73. package/lib/bind/userCdsrcJsonStorageProvider.js +0 -92
  74. package/lib/init/template/destination/files/package.sidecar.json +0 -7
  75. package/lib/init/template/html5-repo/files/package.sidecar.json +0 -7
  76. package/lib/init/template/telemetry/files/package.json.hbs +0 -18
@@ -0,0 +1,267 @@
1
+ const os = require('node:os')
2
+ const cds = require('../cds')
3
+ const { exists, read, fs, path } = cds.utils
4
+ const git = require('../util/git')
5
+ const { bold } = require('../util/term')
6
+
7
+ const KindToRequiresNameMap = {
8
+ xsuaa: 'auth',
9
+ 'xsuaa-auth': 'auth',
10
+ 'ias-auth': 'auth',
11
+ 'hana-cloud': 'db',
12
+ hana: 'db',
13
+ 'hana-mt': 'db', // For compatibility only
14
+ 'sql-mt': 'db', // For compatibility only
15
+ destinations: 'destinations',
16
+ connectivity: 'connectivity',
17
+ 'audit-log': 'auditlog',
18
+ 'audit-log-to-restv2': 'auditlog',
19
+ 'enterprise-messaging': 'messaging',
20
+ 'enterprise-messaging-amqp': 'messaging',
21
+ 'enterprise-messaging-http': 'messaging',
22
+ 'enterprise-messaging-shared': 'messaging',
23
+ 'redis-messaging': 'messaging',
24
+ 'multitenancy': 'multitenancy'
25
+ }
26
+
27
+ const PreferredKinds = {
28
+ multitenancy: 1,
29
+ 'hana-cloud': 1,
30
+ xsuaa: 1,
31
+ 'xsuaa-auth': 1,
32
+ 'ias-auth': 1,
33
+ 'saas-registry': 1
34
+ }
35
+
36
+ async function bind(options) {
37
+ let on
38
+ switch (options.on) {
39
+ case 'k8s':
40
+ case 'kubernetes':
41
+ on = 'k8s'
42
+ break
43
+ case undefined:
44
+ case null:
45
+ case 'cf':
46
+ case 'cloudfoundry':
47
+ on = 'cf'
48
+ break
49
+ default:
50
+ throw `invalid value '${options.on}' for option --on.`
51
+ }
52
+ const plugin = require(`./${on}`)
53
+
54
+ if (!options['to-app-services']) console.log(`retrieving data from ${bold(plugin.displayName())}...`)
55
+
56
+ const services = options.to.split(/,/g)
57
+ const resolvedServices = await Promise.all(services.map(async service => {
58
+ const binding = { on, ...plugin.binding4(service) }
59
+ const resolved = await _resolveBinding(binding)
60
+ delete resolved.credentials
61
+ return resolved
62
+ }))
63
+ if (options.kind) {
64
+ resolvedServices.forEach(service => service.kind = options.kind)
65
+ } else {
66
+ _determineKinds(resolvedServices)
67
+ }
68
+ for (const resolved of resolvedServices) {
69
+ const { kind, binding, binding: { instance, secret } } = resolved
70
+ if (options.serviceArg) {
71
+ resolved.name = options.serviceArg
72
+ } else if (kind) {
73
+ const requiresName = KindToRequiresNameMap[kind]
74
+ if (!requiresName) throw new Error(`Unknown CDS service name for service ${bold(instance)}. Please specify as argument for ${bold('cds bind')}.`)
75
+ resolved.name = requiresName
76
+ }
77
+ resolved.name ??= `custom-service:${instance || secret}`
78
+ const kindText = kind ? ` with kind ${bold(kind)}` : ''
79
+ console.log(`binding ${bold(resolved.name)} to ${plugin.displayName()} ${_bindingText(binding)}${kindText}`)
80
+ }
81
+ return resolvedServices
82
+ }
83
+
84
+ async function storeBindings(resolvedServices, options) {
85
+ const cdsrcPrivate = '.cdsrc-private.json'
86
+ let jsonPath = options.out || cdsrcPrivate
87
+ if (jsonPath === cdsrcPrivate) {
88
+ await git.ensureFileIsGitignored(cdsrcPrivate)
89
+ }
90
+ jsonPath = jsonPath.replace(/~/g, os.homedir())
91
+ if (path.extname(jsonPath) !== '.json') {
92
+ jsonPath = path.join(jsonPath, '.cdsrc.json')
93
+ }
94
+ const conf = exists(jsonPath) ? await read(jsonPath) : {}
95
+ let cdsRoot
96
+ if (path.basename(jsonPath) === 'package.json') {
97
+ conf.cds = conf.cds || {}
98
+ cdsRoot = conf.cds
99
+ } else {
100
+ cdsRoot = conf
101
+ }
102
+ const profile = `[${options.for}]`
103
+ const features = cdsRoot.features?.[profile]
104
+ if (features) delete features.emulate_vcap_services
105
+ cdsRoot.requires ??= {}
106
+ const section = cdsRoot.requires[profile] ??= {}
107
+
108
+ for (const { name, kind, binding, credentials } of resolvedServices) {
109
+ const requireService = section[name] ??= {}
110
+ binding.credentials = _getServiceCredentials(binding, resolvedServices, options)
111
+ Object.assign(requireService, { binding: { ...binding, vcap: undefined }, credentials, kind, vcap: { name } })
112
+ }
113
+ await fs.promises.mkdir(path.dirname(jsonPath), { recursive: true })
114
+ await fs.promises.writeFile(jsonPath, JSON.stringify(conf, null, 2))
115
+ await fs.promises.chmod(jsonPath, 0o600)
116
+ console.log(`saving bindings to ${bold(jsonPath)} in profile ${bold(options.for)}`)
117
+ }
118
+
119
+ function mergeCredentials(credentials, customCredentials) {
120
+ if (!customCredentials) return credentials
121
+ for (const key in customCredentials) {
122
+ if (typeof customCredentials[key] === 'object') {
123
+ if (credentials[key] && typeof credentials[key] !== 'object') {
124
+ throw `Cannot merge credentials: '${key}' is not an object`
125
+ }
126
+ if (!credentials[key]) {
127
+ credentials[key] = {}
128
+ }
129
+ credentials[key] = mergeCredentials(credentials[key], customCredentials[key])
130
+ } else {
131
+ credentials[key] = customCredentials[key]
132
+ }
133
+ }
134
+ return credentials
135
+ }
136
+
137
+ async function bindingEnv(options) {
138
+ const bindings = _getUnresolvedBindings(options)
139
+ const resolvedBindings = await _resolveBindings({ ...options, bindings, cache: {} })
140
+ return resolvedBindings ? { VCAP_SERVICES: _toVcapServices(resolvedBindings) } : undefined
141
+ }
142
+
143
+ async function updateBindings(options, { onBeforeUpdate, onAfterUpdate } = {}) {
144
+ const bindings = _getUnresolvedBindings(options)
145
+ if (onBeforeUpdate) await onBeforeUpdate()
146
+ let resolvedBindings
147
+ try {
148
+ resolvedBindings = await _resolveBindings({ ...options, bindings, cache: {} })
149
+ } catch (error) {
150
+ console.error(error.message) // REVISIT: Why not throw here?
151
+ return
152
+ }
153
+ const env = resolvedBindings ? { VCAP_SERVICES: _toVcapServices(resolvedBindings) } : { VCAP_SERVICES: '{}' }
154
+ if (onAfterUpdate) await onAfterUpdate({ bindings: resolvedBindings, env })
155
+ return { bindings: resolvedBindings, env }
156
+ }
157
+
158
+ module.exports = { bind, storeBindings, mergeCredentials, bindingEnv, updateBindings }
159
+
160
+ /* Private helpers */
161
+
162
+ function _bindingText(binding) {
163
+ try { return require(`./${binding.on ?? binding.type}`).description4(binding) } catch (e) {
164
+ if (e.code === 'MODULE_NOT_FOUND') throw `unsupported binding type ${bold(binding.on ?? binding.type)}.`
165
+ }
166
+ }
167
+
168
+ function _resolveBinding(binding, name) {
169
+ try { return require(`./${binding.on ?? binding.type}`).resolve(name, binding) } catch (e) {
170
+ if (e.code === 'MODULE_NOT_FOUND') throw `unsupported binding type ${bold(binding.on ?? binding.type)} for service ${bold(name)}.`
171
+ }
172
+ }
173
+
174
+ function _determineKinds(services) {
175
+ const vcapServices = {}
176
+
177
+ // Build VCAP_SERVICES
178
+ for (const service of services) {
179
+ service.kindCandidates = []
180
+ const { vcap = {} } = service.binding || {}
181
+ const { label, type = label } = vcap
182
+ if (!type) continue
183
+ if (!vcapServices[type]) vcapServices[type] = []
184
+ vcapServices[type].push({ credentials: { dummy: 'dummy' }, ...vcap, service })
185
+ }
186
+
187
+ // Auto configure services based on VCAP_SERVICES
188
+ const cds = require('../cds')
189
+ const env = cds.env.for('cds', process.cwd())
190
+ if (!env._find_credentials_for_required_service) { // REVISIT: Proper API
191
+ throw new Error(`Please provide a service kind or update @sap/cds version to use the kind detection.`)
192
+ }
193
+ const requires = env.requires.kinds || {}
194
+ for (const [kind, service] of Object.entries(requires)) {
195
+ const vcapService = env._find_credentials_for_required_service(kind, service, vcapServices)
196
+ if (!vcapService) continue
197
+ const isUniqueKind = !Object.values(requires).some(otherService =>
198
+ otherService !== service && otherService.kind === kind
199
+ )
200
+ if (isUniqueKind) vcapService.service.kindCandidates.push(kind)
201
+ }
202
+ for (const service of services) {
203
+ const preferredKind = service.kindCandidates.find(kind => kind in PreferredKinds)
204
+ if (preferredKind) {
205
+ service.kind = preferredKind
206
+ } else if (service.kindCandidates.length === 1) {
207
+ service.kind = service.kindCandidates[0]
208
+ }
209
+ delete service.kindCandidates
210
+ }
211
+ }
212
+
213
+ function _getUnresolvedBindings({ cwd = process.cwd(), env } = {}) {
214
+ if (!env) {
215
+ env = cds.env.for('cds', path.resolve(cwd)) // REVISIT: This might load cds.env too early!
216
+ }
217
+ const bindings = {}
218
+ const requires = env.requires || {}
219
+ for (const name in requires) {
220
+ const service = requires[name]
221
+ if (service?.binding && !service.binding.resolved) {
222
+ bindings[name] = { ...service.binding, kind: service.kind }
223
+ }
224
+ }
225
+ return bindings
226
+ }
227
+
228
+ async function _resolveBindings({ bindings, cwd = process.cwd(), env, cache = {}, silent }) {
229
+ bindings ??= _getUnresolvedBindings({ cwd, env })
230
+ if (Object.keys(bindings).length === 0) return undefined
231
+ if (!silent) console.log(`resolving cloud service bindings...`)
232
+ const resolvedBindings = {}
233
+ await Promise.all(Object.keys(bindings).map(async name => {
234
+ const binding = bindings[name]
235
+ const cacheKey = JSON.stringify(binding)
236
+ let resolvedBinding
237
+ if (!cache[cacheKey]) {
238
+ resolvedBinding = cache[cacheKey] = await _resolveBinding(binding, name)
239
+ if (!silent) console.log(`bound ${bold(name)} to ${binding.type} ${_bindingText(binding)}`)
240
+ } else {
241
+ resolvedBinding = cache[cacheKey]
242
+ }
243
+ resolvedBindings[name] = resolvedBinding
244
+ }))
245
+ return Object.keys(resolvedBindings).length > 0 ? resolvedBindings : undefined
246
+ }
247
+
248
+ function _toVcapServices(bindings) {
249
+ const services = {}
250
+ for (const name in bindings) {
251
+ const { binding, credentials } = bindings[name]
252
+ const vcap = binding.vcap ?? {}
253
+ const type = vcap.type ?? vcap.label
254
+ services[type] ??= []
255
+ services[type].push({ ...vcap, name, credentials })
256
+ }
257
+ return JSON.stringify(services)
258
+ }
259
+
260
+ function _getServiceCredentials(binding, services, options) {
261
+ const { to, credentials } = options
262
+ const { type } = binding
263
+ if (!credentials) return
264
+ const plugin = require(`./${type}`)
265
+ const service = plugin.service4(binding, to ?? services)
266
+ return credentials[service] ?? credentials
267
+ }
@@ -261,7 +261,7 @@ The higher the value the earlier the plugin is run.`
261
261
  }
262
262
 
263
263
  _logTimer(start, end) {
264
- console.log(`build completed in ${end - start} ms\n`)
264
+ console.log(`build completed in ${end - start} ms`)
265
265
  }
266
266
 
267
267
  _logTaskHandler(plugin) {
@@ -1,7 +1,5 @@
1
1
  apiVersion: v2
2
2
  description: A Helm Chart for creating a Content Deployment job
3
3
  name: content-deployment
4
- sources:
5
- - https://github.tools.sap/unified-runtime/content-deployment
6
4
  type: application
7
5
  version: 1.7.1
@@ -2,7 +2,5 @@ apiVersion: v2
2
2
  description: A Helm chart for web app
3
3
  kubeVersion: '>= 1.21'
4
4
  name: web-application
5
- sources:
6
- - https://github.tools.sap/unified-runtime/web-application
7
5
  type: application
8
- version: 1.22.0
6
+ version: 1.23.0
@@ -33,5 +33,8 @@ spec:
33
33
  {{- if $mergedParams.credentialsRotationPolicy }}
34
34
  credentialsRotationPolicy: {{ $mergedParams.credentialsRotationPolicy | toYaml | nindent 4 }}
35
35
  {{- end }}
36
+ {{- if $mergedParams.secretTemplate }}
37
+ secretTemplate: {{ $mergedParams.secretTemplate | toYaml | nindent 2}}
38
+ {{- end }}
36
39
  {{- end }}
37
40
  {{- end }}
@@ -793,6 +793,9 @@
793
793
  "secretName": {
794
794
  "type": "string"
795
795
  },
796
+ "secretTemplate": {
797
+ "type": "string"
798
+ },
796
799
  "secretKey": {
797
800
  "type": "string",
798
801
  "default": "credentials"
@@ -14,7 +14,7 @@ global:
14
14
  _internal:
15
15
  web-application:
16
16
  versions:
17
- 1.22.0: true
17
+ 1.23.0: true
18
18
  ## Will be used instead of the `.Chart.Name`, e.g. when generating the Deployment name.
19
19
  # nameOverride: name
20
20
 
@@ -258,6 +258,7 @@ bindings:
258
258
  # enabled: false
259
259
  # hana:
260
260
  # serviceInstanceName: hana-manual
261
+ # secretTemplate: -> see https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#custom-formats
261
262
  # portal:
262
263
  # fromSecret: manual-secret # user-provided-service
263
264
 
package/lib/build/util.js CHANGED
@@ -134,7 +134,7 @@ async function getDefaultModelOptions(ws = false, filter) {
134
134
  workspaces.forEach(workspace => {
135
135
  cds.root = path.join(root, workspace)
136
136
  cds.env = cds.env.for('cds', cds.root)
137
- paths = paths.concat(_filterModelPaths(cds.resolve(cds.env.requires.toggles ? wildcards : '*', false), filter).map(p => path.join(workspace, p)))
137
+ paths = paths.concat(_filterModelPaths(cds.resolve(cds.env.requires.toggles ? wildcards : '*', false), filter).map(p => normalizePath(path.join(workspace, p))))
138
138
  })
139
139
  } finally {
140
140
  cds.root = root
@@ -114,7 +114,7 @@ module.exports = function to_mermaid (model, options={}) {
114
114
  }
115
115
 
116
116
  function resolveTargets (query) {
117
- if (query.target) return [ query.target.name ] // simple query, like projection on
117
+ if (query._target) return [ query._target.name ] // simple query, like projection on
118
118
 
119
119
  // SELECT: { from: { args: [{ ref: ['t1'] }, { ref: ['t2'] }], join: ..., on: ... } }
120
120
  const targets = query.SELECT?.from?.args?.filter(arg => arg.ref?.length).map (arg => arg.ref[0])
@@ -205,4 +205,3 @@ function to_mta_mermaid (file) {
205
205
 
206
206
  return diag
207
207
  }
208
-
@@ -14,7 +14,7 @@ const mtaUtil = require('./mtaUtil');
14
14
 
15
15
  const { bold, info, warn } = require('../../util/term');
16
16
 
17
- const { bind, storeBindings } = require('../../bind');
17
+ const { bind, storeBindings } = require('../../bind/shared');
18
18
  const { BUILD_TASK_HANA, OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM, OUTPUT_MODE_RESULT } = require('../../build/constants');
19
19
 
20
20
  const DEBUG = cds.debug('cli');
@@ -24,18 +24,6 @@ async function getHanaDbModuleDescriptor(projectPath, moduleName) {
24
24
  }
25
25
  }
26
26
 
27
- async function getServiceModuleDescriptor(projectPath, moduleName, moduleType) {
28
- // mta might be null if mta.yaml does not exist
29
- const mta = await _getMta(projectPath)
30
- const projectInfo = await _getProjectInfo(projectPath, mta)
31
- const appName = _getApplicationName(mta, moduleName, moduleType)
32
-
33
- return {
34
- project: projectInfo,
35
- appName: appName ? appName : `${projectInfo.name}-${moduleName}`
36
- }
37
- }
38
-
39
27
  async function _getProjectInfo(projectPath, mta) {
40
28
  const details = {
41
29
  }
@@ -166,6 +154,5 @@ function _findHdiResource(mta, moduleName) {
166
154
 
167
155
  module.exports = {
168
156
  getHanaDbModuleDescriptor,
169
- getServiceModuleDescriptor,
170
157
  MTA_YAML
171
158
  }
@@ -139,12 +139,12 @@ module.exports = {
139
139
  as: {
140
140
  type: "string",
141
141
  description: "Converts the input file into one of these supported formats: cds, csn or json.",
142
- $ref: "#/$defs/import-options/as"
142
+ $ref: "#/$defs/importOptions/as"
143
143
  },
144
144
  from: {
145
145
  type: "string",
146
146
  description: "Imports the model from the one of the given options: edmx, openapi or asyncapi.",
147
- $ref: "#/$defs/import-options/from"
147
+ $ref: "#/$defs/importOptions/from"
148
148
  },
149
149
  force: {
150
150
  type: "boolean",
@@ -173,7 +173,7 @@ module.exports = {
173
173
  }
174
174
  }
175
175
  },
176
-
176
+
177
177
  export: {
178
178
  type: "object",
179
179
  description: "Customize cds compile settings",
@@ -191,7 +191,7 @@ module.exports = {
191
191
  merged: {
192
192
  type: "object",
193
193
  description: "properties that needs to be there when merged option is used",
194
- additionalProperties: false,
194
+ additionalProperties: false,
195
195
  properties: {
196
196
  title: {
197
197
  type: "string",
@@ -224,37 +224,24 @@ module.exports = {
224
224
  type: "object",
225
225
  description: "Parameters related to the source of event",
226
226
  default: {
227
+ region: "the region",
228
+ applicationNamespace: "the namespace",
229
+ instanceId: "the instance id"
230
+ },
231
+ additionalProperties: false,
232
+ properties: {
227
233
  region: {
228
- "description": "The regional context of the application.",
229
- "schema": {
230
- "type": "string"
231
- }
234
+ type: "string",
235
+ description: "The regional context of the application"
232
236
  },
233
237
  applicationNamespace: {
234
- "description": "The registered namespace of the application.",
235
- "schema": {
236
- "type": "string"
237
- }
238
+ type: "string",
239
+ description: "The registered namespace of the application"
238
240
  },
239
241
  instanceId: {
240
- "description": "The instance id (tenant, installation, ...) of the application.",
241
- "schema": {
242
- "type": "string"
243
- }
242
+ type: "string",
243
+ description: "The instance id (tenant, installation, ...) of the application."
244
244
  }
245
- },
246
- additionalProperties: false,
247
- region: {
248
- type: "string",
249
- description: "The regional context of the application"
250
- },
251
- applicationNamespace: {
252
- type: "string",
253
- description: "The registered namespace of the application"
254
- },
255
- instanceId: {
256
- type: "string",
257
- description: "The instance id (tenant, installation, ...) of the application."
258
245
  }
259
246
  },
260
247
  event_characteristics: {
@@ -332,7 +319,7 @@ module.exports = {
332
319
  ]
333
320
  },
334
321
 
335
- import_options: {
322
+ importOptions: {
336
323
  from: {
337
324
  oneOf: [
338
325
  {
@@ -141,11 +141,22 @@ async function isValidInputFile(inputFileLoc) {
141
141
  }
142
142
 
143
143
  async function _copyToSrvExternal(file, dst, cwd) {
144
- const dstDir = path.dirname(dst), srcDir = path.dirname(file);
144
+ const dstDir = path.dirname(dst), srcDir = path.resolve(path.dirname(file));
145
145
  if (!file.startsWith(dstDir + path.sep)) {
146
146
  await mkdirp(dstDir);
147
- const copyOrMove = srcDir === cwd || srcDir == process.cwd() ? fs.renameSync : fs.copyFileSync;
148
- await copyOrMove(file, dst);
147
+ const copyOrMove = srcDir === cwd || srcDir === process.cwd() ? fs.renameSync : fs.copyFileSync;
148
+ try {
149
+ copyOrMove(file, dst);
150
+ } catch (err) {
151
+ // cross-device link error (might happen in docker containers), fall back to copy and delete
152
+ // see cap/issues#18004
153
+ if (err.code === 'EXDEV') {
154
+ fs.copyFileSync(file, dst);
155
+ fs.unlinkSync(file);
156
+ } else {
157
+ throw err;
158
+ }
159
+ }
149
160
  return dst;
150
161
  }
151
162
  return file;
@@ -194,25 +205,24 @@ function _getServiceNames(csn) {
194
205
  }
195
206
 
196
207
  function _validateAndComputeOutputFilePath(outputFileLoc, as, destFilePath) {
197
- let index, extension, expectedExtension, supportedExtension;
208
+ let extension, expectedExtension, supportedExtension;
198
209
  if (typeof (outputFileLoc) !== 'boolean') {
199
- index = outputFileLoc.lastIndexOf(".");
200
- extension = outputFileLoc.substring(index + 1);
210
+ extension = path.extname(outputFileLoc).substring(1);
201
211
  expectedExtension = (as) ? _getSuffix(as) : "csn";
202
- supportedExtension = (index > 0) ? _supportedAsFormats().includes(extension) : false;
212
+ supportedExtension = extension ? _supportedAsFormats().includes(extension) : false;
203
213
  }
204
214
 
205
215
  if (outputFileLoc === true || // if output file is not mentioned
206
216
  outputFileLoc[0] === "-" || // if output file is missing and "--as" is considered as output file
207
217
  outputFileLoc.trim() === "") {
208
218
  throw new Error(messages.SPECIFY_OUTPUT_FILE);
209
- } else if (index > 0 && !supportedExtension) {
219
+ } else if (extension && !supportedExtension) {
210
220
  // extension not supported, eg., "test.abs", "./sv/external.test.abcs", "./a."
211
221
  throw new Error(messages.INVALID_OUTPUT_FILE);
212
- } else if (index > 0 && !as && extension != "json" && extension != "csn") {
222
+ } else if (extension && !as && extension != "json" && extension != "csn") {
213
223
  // --as option is not provided but output file is like: "test.cds", "./srv/external/test.cds"
214
224
  throw new Error(messages.INCORRECT_EXTENSION);
215
- } else if (index > 0 && as && extension != expectedExtension) {
225
+ } else if (extension && as && extension != expectedExtension) {
216
226
  // extension not supported by --as option, e.g., "cds import <file> --out test.yml --as cds"
217
227
  throw new Error(messages.OUTPUT_FILE_MISMATCH);
218
228
  } else if (supportedExtension || (!outputFileLoc.endsWith("\\") && !outputFileLoc.endsWith("/"))) {
@@ -5,7 +5,7 @@ module.exports = {
5
5
  COMMAND_ADD: 'add',
6
6
 
7
7
  JAVA_LTS_VERSIONS: [17, 21],
8
- MAVEN_ARCHETYPE_VERSION: '3.7.2',
8
+ MAVEN_ARCHETYPE_VERSION: '3.8.0',
9
9
 
10
10
  OPTIONS: Object.freeze({
11
11
  COMPLETION: 'completion',
package/lib/init/merge.js CHANGED
@@ -129,12 +129,17 @@ async function _mergeJSON(into, from, project, semantics) {
129
129
  * @throws {Error} Throws an error if empty target YAML is passed in
130
130
  */
131
131
  async function _mergeYAML(into, from, project, semantics = {}) {
132
- const target = typeof into === 'string' ? await readYAML(into) : into instanceof YAML.Document ? into : new YAML.Document(into)
133
- const source = typeof from === 'string' ? await readYAML(from, project) : from instanceof YAML.Document ? from : new YAML.Document(from)
132
+ const target = typeof into === 'string' ? await readYAML(into) :
133
+ into instanceof YAML.Document ? into :
134
+ new YAML.Document(into)
135
+ const source = typeof from === 'string' ? await readYAML(from, project) :
136
+ from instanceof YAML.Document ? from :
137
+ new YAML.Document(from)
134
138
 
135
139
  if (!target?.contents) {
136
140
  if (typeof into === 'string' && typeof from === 'string') {
137
- project ? await writeYAML(into, source) : await copy(from, into)
141
+ if (project) await writeYAML(into, source)
142
+ else await copy(from, into)
138
143
  return source
139
144
  }
140
145
  throw new Error(`Target YAML doesn't exist`)
@@ -143,19 +148,19 @@ async function _mergeYAML(into, from, project, semantics = {}) {
143
148
  const entryMap = new Map, templateEntryMap = new Map
144
149
 
145
150
  const { additions, overwrites, deletions, relationships } = semantics
146
- additions?.forEach(entry => {
151
+ for (const entry of additions ?? []) {
147
152
  entryMap.set(entry, undefined)
148
153
  templateEntryMap.set(entry, undefined)
149
- })
150
- deletions?.forEach(deletion => {
154
+ }
155
+ for (const deletion of deletions ?? []) {
151
156
  entryMap.set(deletion.item, undefined)
152
- })
153
- relationships?.forEach(relationship => {
157
+ }
158
+ for (const relationship of relationships ?? []) {
154
159
  const [entry] = relationship.into
155
160
  const [insertEntry] = relationship.insert
156
161
  const hash = JSON.stringify(insertEntry) + ' -> ' + JSON.stringify(entry)
157
162
  entryMap.set(hash, undefined)
158
- })
163
+ }
159
164
 
160
165
  let collectionStack = [target.contents]
161
166
 
@@ -347,32 +352,36 @@ async function _mergeYAML(into, from, project, semantics = {}) {
347
352
  // 7. Create missing relationships
348
353
  _traverseYAMLNode(target.contents, null, {
349
354
  mergeCollection: (targetNode, templateNode) => {
350
- if (YAML.isSeq(templateNode)) {
351
- const targetJSON = YAML.parse(String(targetNode))
352
- const relationship = semantics.relationships?.filter(r => r)
353
- .find(relationship => {
354
- const [entry, keyPath] = relationship.into
355
- if (!entryMap.get(entry)) return false
356
- const existingNode = _getYAMLProperty(entryMap.get(entry).node, keyPath)
357
- return targetNode === existingNode
358
- })
359
- if (!relationship) return
360
-
361
- const intoKey = relationship.into[relationship.into.length - 1]
362
- const [entry, entryKeyPath] = relationship.insert
363
- const missingPairs = [entry]
364
- .filter(item =>
365
- !targetJSON.some(targetItem =>
366
- _getProperty(entryMap.get(item).json, entryKeyPath) === targetItem[intoKey]
367
- )
368
- )
369
- .map(item => entryMap.get(item).node.get(intoKey))
370
- missingPairs.forEach(pair => {
371
- targetNode.add({ [intoKey]: pair })
355
+ if (YAML.isSeq(templateNode)) {
356
+ const targetJSON = YAML.parse(String(targetNode))
357
+ const relationships = semantics.relationships?.filter(relationship => {
358
+ const [entry, keyPath] = relationship.into
359
+ if (!entryMap.get(entry)) return false
360
+ const existingNode = _getYAMLProperty(entryMap.get(entry).node, keyPath)
361
+ return targetNode === existingNode
362
+ }) ?? []
363
+
364
+ for (const { into, insert } of relationships) {
365
+ const intoKey = into[into.length - 1]
366
+ const [insEntry, entryKeyPath] = insert
367
+ const missingPairs = [insEntry]
368
+ .filter(item => {
369
+ if (!entryMap.get(item)) {
370
+ const spec = inspect(item, { colors, depth:11 })
371
+ throw new Error(`Did not find entry in template for ${spec} in ${from}:\n${source}`)
372
+ }
373
+ return !targetJSON.some(targetItem =>
374
+ _getProperty(entryMap.get(item).json, entryKeyPath) === targetItem[intoKey]
375
+ )
372
376
  })
377
+ .map(item => entryMap.get(item).node.get(intoKey))
378
+ for (const pair of missingPairs) {
379
+ targetNode.add({ [intoKey]: pair })
380
+ }
373
381
  }
382
+ }
374
383
  }
375
- }, source.contents)
384
+ }, source.contents)
376
385
 
377
386
  typeof into === 'string' && await writeYAML(into, target)
378
387
  return target