@sap/cds-dk 8.7.3 → 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.
- package/CHANGELOG.md +27 -1
- package/bin/bind.js +7 -8
- package/bin/cds.js +2 -4
- package/bin/compile/index.js +1 -0
- package/bin/deploy/index.js +10 -5
- package/bin/watch.js +48 -7
- package/lib/bind/{cfKeyProvider.js → cf.js} +43 -53
- package/lib/bind/k8s.js +138 -0
- package/lib/bind/plugin.js +48 -0
- package/lib/bind/shared.js +267 -0
- package/lib/build/buildTaskEngine.js +1 -1
- package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +0 -2
- package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -3
- package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml +3 -0
- package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +3 -0
- package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +2 -1
- package/lib/build/util.js +1 -1
- package/lib/compile/to-mermaid.js +1 -2
- package/lib/deploy/to-hana/hana.js +1 -1
- package/lib/deploy/to-hana/mtaUtil.js +0 -13
- package/lib/import/common.js +6 -7
- package/lib/init/merge.js +41 -32
- package/lib/init/projectReader.js +15 -7
- package/lib/init/registries/mta.js +7 -1
- package/lib/init/template/approuter/files/package.json +1 -1
- package/lib/init/template/approuter/files/xs-app.json.hbs +0 -11
- package/lib/init/template/cloud-logging/files/package.json.hbs +12 -1
- package/lib/init/template/cloud-logging/files/values.yaml.hbs +0 -2
- package/lib/init/template/cloud-logging/index.js +8 -15
- package/lib/init/template/destination/files/values.yaml +4 -4
- package/lib/init/template/destination/index.js +1 -1
- package/lib/init/template/html5-repo/files/app-package.json.hbs +0 -2
- package/lib/init/template/html5-repo/files/containerize.yaml.hbs +25 -0
- package/lib/init/template/html5-repo/files/mta.yaml.hbs +1 -2
- package/lib/init/template/html5-repo/files/package.json.hbs +1 -1
- package/lib/init/template/html5-repo/files/ui-resources/package.json.hbs +11 -0
- package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +4 -7
- package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -4
- package/lib/init/template/html5-repo/index.js +22 -10
- package/lib/init/template/http/index.js +1 -1
- package/lib/init/template/multitenancy/files/mta.yaml.hbs +0 -2
- package/lib/init/template/multitenancy/index.js +1 -1
- package/lib/init/template/portal/files/mta.yaml.hbs +9 -0
- package/lib/init/template/portal/index.js +3 -1
- package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +119 -119
- package/lib/init/template/sample/files/app/browse/webapp/manifest.json +1 -1
- package/lib/init/template/telemetry/index.js +0 -13
- package/lib/init/template/typer/files/package.json +3 -0
- package/lib/init/template/workzone-standard/files/cdm.json.hbs +97 -0
- package/lib/init/template/workzone-standard/files/mta.yaml.hbs +23 -0
- package/lib/init/template/workzone-standard/index.js +70 -39
- package/lib/init/template/xsuaa/files/mta.yaml.hbs +2 -2
- package/lib/init/template/xsuaa/files/values.yaml.hbs +0 -2
- package/lib/init/template/xsuaa/index.js +22 -10
- package/lib/mtx/util/cf.js +1 -2
- package/lib/util/cf.js +104 -84
- package/lib/util/command.js +1 -2
- package/lib/util/git.js +1 -1
- package/npm-shrinkwrap.json +31 -31
- package/package.json +1 -1
- package/bin/deploy/to-cf.js +0 -0
- package/bin/deploy/via-mta.js +0 -0
- package/lib/bind/KubernetesApiAccessor.js +0 -81
- package/lib/bind/bindingManager.js +0 -89
- package/lib/bind/bindingResolver.js +0 -54
- package/lib/bind/bindingUtil.js +0 -129
- package/lib/bind/index.js +0 -127
- package/lib/bind/kubernetesCredentialsProvider.js +0 -84
- package/lib/bind/userCdsrcJsonStorageProvider.js +0 -92
- package/lib/init/template/destination/files/package.sidecar.json +0 -7
- package/lib/init/template/html5-repo/files/package.sidecar.json +0 -7
- 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
|
+
}
|
package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml
CHANGED
|
@@ -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 }}
|
|
@@ -14,7 +14,7 @@ global:
|
|
|
14
14
|
_internal:
|
|
15
15
|
web-application:
|
|
16
16
|
versions:
|
|
17
|
-
1.
|
|
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.
|
|
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
|
}
|
package/lib/import/common.js
CHANGED
|
@@ -194,25 +194,24 @@ function _getServiceNames(csn) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function _validateAndComputeOutputFilePath(outputFileLoc, as, destFilePath) {
|
|
197
|
-
let
|
|
197
|
+
let extension, expectedExtension, supportedExtension;
|
|
198
198
|
if (typeof (outputFileLoc) !== 'boolean') {
|
|
199
|
-
|
|
200
|
-
extension = outputFileLoc.substring(index + 1);
|
|
199
|
+
extension = path.extname(outputFileLoc).substring(1);
|
|
201
200
|
expectedExtension = (as) ? _getSuffix(as) : "csn";
|
|
202
|
-
supportedExtension =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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("/"))) {
|
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'
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
151
|
+
for (const entry of additions ?? []) {
|
|
147
152
|
entryMap.set(entry, undefined)
|
|
148
153
|
templateEntryMap.set(entry, undefined)
|
|
149
|
-
}
|
|
150
|
-
|
|
154
|
+
}
|
|
155
|
+
for (const deletion of deletions ?? []) {
|
|
151
156
|
entryMap.set(deletion.item, undefined)
|
|
152
|
-
}
|
|
153
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
|
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: () =>
|
|
168
|
-
|
|
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
|
|
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
|
|
@@ -12,9 +12,14 @@ const approuter = { in: 'modules', where: { type: 'approuter.nodejs' } }
|
|
|
12
12
|
|
|
13
13
|
const xsuaa = {
|
|
14
14
|
in: 'resources',
|
|
15
|
-
where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'xsuaa' }
|
|
15
|
+
where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'xsuaa', 'parameters.service-plan': 'application' }
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const xsuaa4 = plan => ({
|
|
19
|
+
in: 'resources',
|
|
20
|
+
where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'xsuaa', 'parameters.service-plan': plan }
|
|
21
|
+
})
|
|
22
|
+
|
|
18
23
|
const destination = {
|
|
19
24
|
in: 'resources',
|
|
20
25
|
where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'destination' }
|
|
@@ -194,6 +199,7 @@ module.exports = {
|
|
|
194
199
|
srv4,
|
|
195
200
|
approuter,
|
|
196
201
|
xsuaa,
|
|
202
|
+
xsuaa4,
|
|
197
203
|
destination,
|
|
198
204
|
destinations,
|
|
199
205
|
destinationSideBySideExtensibility,
|
|
@@ -17,17 +17,6 @@
|
|
|
17
17
|
"authenticationType": "none"
|
|
18
18
|
},
|
|
19
19
|
{{/hasMTXRouteJava}}
|
|
20
|
-
{
|
|
21
|
-
"source": "^/app/(.*)$",
|
|
22
|
-
"target": "$1",
|
|
23
|
-
"localDir": ".",
|
|
24
|
-
"cacheControl": "no-cache, no-store, must-revalidate"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"source": "^/appconfig/",
|
|
28
|
-
"localDir": ".",
|
|
29
|
-
"cacheControl": "no-cache, no-store, must-revalidate"
|
|
30
|
-
},
|
|
31
20
|
{
|
|
32
21
|
"source": "^/(.*)$",
|
|
33
22
|
"target": "$1",
|