@sap/cds-dk 8.7.2 → 8.8.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 (81) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/bin/bind.js +7 -8
  3. package/bin/cds.js +2 -4
  4. package/bin/compile/index.js +2 -1
  5. package/bin/deploy/index.js +10 -5
  6. package/bin/lint.js +10 -4
  7. package/bin/watch.js +48 -7
  8. package/lib/bind/{cfKeyProvider.js → cf.js} +43 -53
  9. package/lib/bind/k8s.js +138 -0
  10. package/lib/bind/plugin.js +48 -0
  11. package/lib/bind/shared.js +267 -0
  12. package/lib/build/buildTaskEngine.js +1 -1
  13. package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +0 -2
  14. package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -3
  15. package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml +3 -0
  16. package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +3 -0
  17. package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +2 -1
  18. package/lib/build/util.js +1 -1
  19. package/lib/compile/to-mermaid.js +1 -2
  20. package/lib/deploy/to-hana/hana.js +4 -4
  21. package/lib/deploy/to-hana/mtaUtil.js +0 -13
  22. package/lib/import/common.js +6 -7
  23. package/lib/init/constants.js +1 -1
  24. package/lib/init/merge.js +42 -33
  25. package/lib/init/projectReader.js +15 -7
  26. package/lib/init/registries/mta.js +9 -3
  27. package/lib/init/template/approuter/files/mta.yaml.hbs +2 -2
  28. package/lib/init/template/approuter/files/package.json +1 -1
  29. package/lib/init/template/approuter/files/xs-app.json.hbs +0 -11
  30. package/lib/init/template/approuter/index.js +2 -2
  31. package/lib/init/template/cloud-logging/files/package.json.hbs +12 -1
  32. package/lib/init/template/cloud-logging/files/values.yaml.hbs +0 -2
  33. package/lib/init/template/cloud-logging/index.js +8 -15
  34. package/lib/init/template/destination/files/values.yaml +4 -4
  35. package/lib/init/template/destination/index.js +1 -1
  36. package/lib/init/template/html5-repo/files/app-package.json.hbs +0 -2
  37. package/lib/init/template/html5-repo/files/containerize.yaml.hbs +25 -0
  38. package/lib/init/template/html5-repo/files/mta.yaml.hbs +1 -2
  39. package/lib/init/template/html5-repo/files/package.json.hbs +1 -1
  40. package/lib/init/template/html5-repo/files/ui-resources/package.json.hbs +11 -0
  41. package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +4 -7
  42. package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -4
  43. package/lib/init/template/html5-repo/index.js +22 -10
  44. package/lib/init/template/http/index.js +1 -1
  45. package/lib/init/template/ias/files/mta.yaml.hbs +1 -1
  46. package/lib/init/template/ias/files/sidecar.package.json.hbs +12 -0
  47. package/lib/init/template/ias/index.js +1 -3
  48. package/lib/init/template/multitenancy/files/mta.yaml.hbs +8 -10
  49. package/lib/init/template/multitenancy/index.js +1 -1
  50. package/lib/init/template/portal/files/mta.yaml.hbs +9 -0
  51. package/lib/init/template/portal/index.js +3 -1
  52. package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +119 -119
  53. package/lib/init/template/sample/files/app/browse/webapp/manifest.json +1 -1
  54. package/lib/init/template/telemetry/index.js +0 -13
  55. package/lib/init/template/typer/files/package.json +3 -0
  56. package/lib/init/template/workzone-standard/files/cdm.json.hbs +97 -0
  57. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +23 -0
  58. package/lib/init/template/workzone-standard/index.js +70 -39
  59. package/lib/init/template/xsuaa/files/mta.yaml.hbs +2 -2
  60. package/lib/init/template/xsuaa/files/values.yaml.hbs +0 -2
  61. package/lib/init/template/xsuaa/index.js +22 -10
  62. package/lib/lint/Linter.js +242 -201
  63. package/lib/lint/io.js +4 -7
  64. package/lib/mtx/util/cf.js +1 -2
  65. package/lib/util/cf.js +104 -84
  66. package/lib/util/command.js +1 -2
  67. package/lib/util/git.js +1 -1
  68. package/npm-shrinkwrap.json +60 -43
  69. package/package.json +1 -1
  70. package/bin/deploy/to-cf.js +0 -0
  71. package/bin/deploy/via-mta.js +0 -0
  72. package/lib/bind/KubernetesApiAccessor.js +0 -81
  73. package/lib/bind/bindingManager.js +0 -89
  74. package/lib/bind/bindingResolver.js +0 -54
  75. package/lib/bind/bindingUtil.js +0 -129
  76. package/lib/bind/index.js +0 -127
  77. package/lib/bind/kubernetesCredentialsProvider.js +0 -84
  78. package/lib/bind/userCdsrcJsonStorageProvider.js +0 -92
  79. package/lib/init/template/destination/files/package.sidecar.json +0 -7
  80. package/lib/init/template/html5-repo/files/package.sidecar.json +0 -7
  81. 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');
@@ -79,6 +79,7 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
79
79
  }
80
80
 
81
81
  if (dry) {
82
+ if (!buildResult) return
82
83
  const sortedResult = Array.from(buildResult.hana).sort();
83
84
  for (const file of sortedResult) {
84
85
  const srcPath = path.join(buildResult.dest, file);
@@ -87,7 +88,7 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
87
88
  console.log();
88
89
  }
89
90
  } else {
90
- // the primary database is always built and deployed
91
+ // the primary database is always built and deployed
91
92
  const dest = path.join(cds.env.build.target, cds.env.folders.db);
92
93
 
93
94
  let serviceName = pServiceName;
@@ -211,8 +212,7 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
211
212
  src: cds.env.folders.db,
212
213
  [OUTPUT_MODE]: OUTPUT_MODE_FILESYSTEM | OUTPUT_MODE_RESULT
213
214
  }
214
- console.log(`Running build`);
215
-
215
+
216
216
  // cds build throws an error if the src folder does not exist
217
217
  const buildResult = await build(buildTaskOptions);
218
218
  return buildResult[0].result;
@@ -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
  }
@@ -194,25 +194,24 @@ function _getServiceNames(csn) {
194
194
  }
195
195
 
196
196
  function _validateAndComputeOutputFilePath(outputFileLoc, as, destFilePath) {
197
- let index, extension, expectedExtension, supportedExtension;
197
+ let extension, expectedExtension, supportedExtension;
198
198
  if (typeof (outputFileLoc) !== 'boolean') {
199
- index = outputFileLoc.lastIndexOf(".");
200
- extension = outputFileLoc.substring(index + 1);
199
+ extension = path.extname(outputFileLoc).substring(1);
201
200
  expectedExtension = (as) ? _getSuffix(as) : "csn";
202
- supportedExtension = (index > 0) ? _supportedAsFormats().includes(extension) : false;
201
+ supportedExtension = extension ? _supportedAsFormats().includes(extension) : false;
203
202
  }
204
203
 
205
204
  if (outputFileLoc === true || // if output file is not mentioned
206
205
  outputFileLoc[0] === "-" || // if output file is missing and "--as" is considered as output file
207
206
  outputFileLoc.trim() === "") {
208
207
  throw new Error(messages.SPECIFY_OUTPUT_FILE);
209
- } else if (index > 0 && !supportedExtension) {
208
+ } else if (extension && !supportedExtension) {
210
209
  // extension not supported, eg., "test.abs", "./sv/external.test.abcs", "./a."
211
210
  throw new Error(messages.INVALID_OUTPUT_FILE);
212
- } else if (index > 0 && !as && extension != "json" && extension != "csn") {
211
+ } else if (extension && !as && extension != "json" && extension != "csn") {
213
212
  // --as option is not provided but output file is like: "test.cds", "./srv/external/test.cds"
214
213
  throw new Error(messages.INCORRECT_EXTENSION);
215
- } else if (index > 0 && as && extension != expectedExtension) {
214
+ } else if (extension && as && extension != expectedExtension) {
216
215
  // extension not supported by --as option, e.g., "cds import <file> --out test.yml --as cds"
217
216
  throw new Error(messages.OUTPUT_FILE_MISMATCH);
218
217
  } 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.1',
8
+ MAVEN_ARCHETYPE_VERSION: '3.7.2',
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
 
@@ -330,7 +335,7 @@ async function _mergeYAML(into, from, project, semantics = {}) {
330
335
  })
331
336
  .filter(item => !entryMap.get(item))
332
337
  .forEach(item => {
333
- if (!templateEntryMap.get(item)) throw 'Error: did not find entry in template for ' + inspect(item, { colors }) + ' in ' + from
338
+ if (!templateEntryMap.get(item)) throw 'Error: did not find entry in template for ' + inspect(item, { colors, depth:11 }) + ' in ' + from
334
339
  const templateNode = templateEntryMap.get(item).node
335
340
  item.at !== undefined ? targetNode.items.splice(item.at, 0, templateNode) : targetNode.add(templateNode)
336
341
  })
@@ -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
@@ -125,7 +125,9 @@ module.exports = new class ProjectReader {
125
125
  const _uiEmbedded = () => exists(join(env.folders.app, 'xs-app.json'))
126
126
  const _uiModule = () => !_uiEmbedded() && isdir(env.folders.app) && fs.readdirSync(resolve(cds.root, env.folders.app)).length > 0
127
127
  const _isJava = () => this.isJava
128
- const isApp = name => name !== 'appconfig' && !name.startsWith('_') && name !== 'router' && name !== 'portal'
128
+ const isApp = name => {
129
+ return name !== 'appconfig' && !name.startsWith('_') && name !== 'router' && name !== 'portal' && name !== 'workzone'
130
+ }
129
131
  const reserved = {
130
132
  dependencies: () => pkgJson().dependencies,
131
133
  has: x => _inProd(x),
@@ -152,7 +154,9 @@ module.exports = new class ProjectReader {
152
154
  },
153
155
  configFile: () => _isJava() ? '.cdsrc.json' : 'package.json',
154
156
  apps: () => {
155
- const apps = fs.readdirSync(path.resolve(cds.root, cds.env.folders.app)).filter(e =>
157
+ const appPath = path.resolve(cds.root, cds.env.folders.app)
158
+ if (!exists(appPath)) return []
159
+ const apps = fs.readdirSync(appPath).filter(e =>
156
160
  isdir(path.join(cds.root, cds.env.folders.app, e)) && isApp(e)
157
161
  )
158
162
  return apps.map((app, i) => {
@@ -164,14 +168,18 @@ module.exports = new class ProjectReader {
164
168
  return { app, strippedApp: app.replace(/-/g, ''), vizId, isNotLast: i < apps.length - 1, manifestAppId: manifest?.['sap.app']?.id ?? app}
165
169
  })
166
170
  },
167
- appUIPaths: () => (fs.readdirSync(path.resolve(cds.root, cds.env.folders.app))).filter(e =>
168
- isdir(join(cds.root, cds.env.folders.app, e)) && isApp(e)
169
- ),
171
+ appUIPaths: () => {
172
+ const appPath = path.resolve(cds.root, cds.env.folders.app)
173
+ if (!exists(appPath)) return []
174
+ return fs.readdirSync(appPath).filter(e =>
175
+ isdir(path.join(cds.root, cds.env.folders.app, e)) && isApp(e)
176
+ )
177
+ },
170
178
  hasIndexHtml: () => {},
171
179
  appPath: () => env.folders.app,
172
180
  appVersion: () => appVersion,
173
181
  jdkVersion: () => jdkVersion,
174
- approuterPath: () => _uiEmbedded() ? join(env.folders.app) : join(env.folders.app, 'router'),
182
+ approuterPath: () => (_uiEmbedded() ? join(env.folders.app) : join(env.folders.app, 'router')).replace(/\\/g, '/'),
175
183
  appName: () => appName,
176
184
  cleanedAppName: () => appName.replaceAll('_', '-'),
177
185
  strippedAppName: () => appName.replace(/-/g, ''),
@@ -210,7 +218,7 @@ module.exports = new class ProjectReader {
210
218
 
211
219
  /**
212
220
  * Finds and reads SpringBoot configuration in `cds.root/srv` dir.
213
- * This is done in a best-effort way. In no way can we replciate all of SpringBoot's
221
+ * This is done in a best-effort way. In no way can we replicate all of SpringBoot's
214
222
  * config variants.
215
223
  * @param {string[]?} roots array of directories to search for application.yaml, like `srv`
216
224
  * @returns {Promise<object[]>} array of objects with `cds` property