@sap/cds-dk 7.8.1 → 7.9.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.
- package/bin/add.js +28 -7
- package/bin/bind.js +64 -21
- package/bin/build.js +35 -3
- package/bin/cds.js +32 -13
- package/bin/compile/index.js +44 -3
- package/bin/completion/completionFs.js +61 -0
- package/bin/completion/index.js +212 -0
- package/bin/completion/scripts/cds-bash.sh +17 -0
- package/bin/completion/scripts/cds-ps.ps1 +18 -0
- package/bin/completion/scripts/cds-zsh.sh +12 -0
- package/bin/help.js +8 -2
- package/bin/init.js +34 -4
- package/bin/subscribe.js +5 -1
- package/bin/watch.js +4 -2
- package/lib/bind/KubernetesApiAccessor.js +3 -2
- package/lib/bind/bindingResolver.js +3 -2
- package/lib/bind/bindingUtil.js +10 -8
- package/lib/bind/cfKeyProvider.js +58 -153
- package/lib/bind/index.js +46 -36
- package/lib/bind/kubernetesCredentialsProvider.js +6 -5
- package/lib/bind/userCdsrcJsonStorageProvider.js +25 -3
- package/lib/build/buildTaskEngine.js +1 -1
- package/lib/build/provider/hana/index.js +1 -1
- package/lib/build/provider/hana/template/undeploy.json +3 -1
- package/lib/build/provider/nodejs/index.js +1 -1
- package/lib/compile/openapi/csdl2openapi.js +4 -0
- package/lib/compile/openapi/index.js +5 -0
- package/lib/deploy/to-hana/hana.js +25 -7
- package/lib/env/schemas/cds-rc.js +29 -2
- package/lib/import/odata/v4parser.js +78 -25
- package/lib/init/constants.js +2 -1
- package/lib/init/index.js +18 -13
- package/lib/init/merge.js +1 -0
- package/lib/init/mvn.js +8 -47
- package/lib/init/plugin.js +0 -4
- package/lib/init/projectReader.js +17 -10
- package/lib/init/template/approuter/files/values.yaml.hbs +6 -12
- package/lib/init/template/approuter/index.js +1 -7
- package/lib/init/template/completion/completionSetup.js +184 -0
- package/lib/init/template/completion/index.js +22 -0
- package/lib/init/template/data/as-csv.js +25 -17
- package/lib/init/template/data/as-json.js +115 -30
- package/lib/init/template/data/index.js +28 -15
- package/lib/init/template/hana/files/undeploy.json +2 -1
- package/lib/init/template/helm/files/chart/values.schema.json +1 -1
- package/lib/init/template/helm/files/content-deployment/Chart.yaml +0 -2
- package/lib/init/template/helm/files/content-deployment/values.schema.json +0 -1
- package/lib/init/template/helm/files/service-instance/Chart.yaml +0 -2
- package/lib/init/template/helm/files/service-instance/values.schema.json +0 -1
- package/lib/init/template/helm/files/web-application/Chart.yaml +1 -3
- package/lib/init/template/helm/files/web-application/templates/_helpers.tpl +23 -0
- package/lib/init/template/helm/files/web-application/templates/deployment.yaml +3 -29
- package/lib/init/template/helm/files/web-application/values.schema.json +17 -2
- package/lib/init/template/helm/files/web-application/values.yaml +10 -2
- package/lib/init/template/helm-unified-runtime/files/templates/NOTES.txt +2 -0
- package/lib/init/template/helm-unified-runtime/files/templates/_deployment_helpers.tpl +10 -2
- package/lib/init/template/helm-unified-runtime/files/values.schema.json +1 -1
- package/lib/init/template/http/index.js +150 -104
- package/lib/init/template/kafka/index.js +6 -0
- package/lib/init/template/lint/index.js +12 -13
- package/lib/init/template/mta/files/mta.yaml.hbs +1 -1
- package/lib/init/template/multitenancy/files/mtxs-configmap-java.yaml +1 -1
- package/lib/init/template/multitenancy/files/values.yaml.hbs +0 -5
- package/lib/init/template/multitenancy/index.js +13 -5
- package/lib/init/template/nodejs/files/.vscode/settings.json +11 -0
- package/lib/init/template/nodejs/files/eslint.config.js +13 -0
- package/lib/init/template/nodejs/files/package.json.hbs +3 -1
- package/lib/init/template/sample/index.js +18 -20
- package/lib/init/template/tiny-sample/index.js +1 -2
- package/lib/init/template/typer/index.js +2 -2
- package/lib/lint/Linter.js +133 -55
- package/lib/lint/checks.js +5 -6
- package/lib/lint/io.js +119 -10
- package/lib/mtx/auth_manager.js +5 -5
- package/lib/mtx/params.js +4 -2
- package/lib/mtx/settings_manager.js +1 -1
- package/lib/mtx/tenant_action.js +12 -2
- package/lib/mtx/util/logging.js +10 -4
- package/lib/util/axios.js +2 -1
- package/lib/util/cf.js +32 -37
- package/lib/util/fs.js +1 -1
- package/lib/util/git.js +1 -1
- package/lib/util/term.js +1 -1
- package/{bin → lib/watch}/watched.js +2 -2
- package/npm-shrinkwrap.json +317 -293
- package/package.json +2 -2
- package/lib/init/template/approuter/files/approuter-mtxs-configmap.yaml +0 -19
- package/lib/init/template/nodejs/files/.eslintrc +0 -26
- /package/lib/init/template/{approuter/files/mtxs-configmap-java.yaml → multitenancy/files/mtxs-configmap-java-approuter.yaml} +0 -0
- /package/lib/init/template/{approuter/files/mtxs-configmap-nodejs.yaml → multitenancy/files/mtxs-configmap-nodejs-approuter.yaml} +0 -0
|
@@ -189,3 +189,26 @@ If release name contains chart name it will be used as service instance name.
|
|
|
189
189
|
{{- $_ := set $result "vars" $variables }}
|
|
190
190
|
{{- (fromYaml (tpl (toYaml $result) .context)) | mustToJson }}
|
|
191
191
|
{{- end }}
|
|
192
|
+
|
|
193
|
+
{{- define "web-application.internal.healthCheck" }}
|
|
194
|
+
{{ .checkName }}:
|
|
195
|
+
{{- if .checkValues.path }}
|
|
196
|
+
httpGet:
|
|
197
|
+
path: {{ .checkValues.path }}
|
|
198
|
+
port: http
|
|
199
|
+
{{- else }}
|
|
200
|
+
tcpSocket:
|
|
201
|
+
port: {{ .context.Values.port }}
|
|
202
|
+
{{- end }}
|
|
203
|
+
{{- if eq .checkName "startupProbe" }}
|
|
204
|
+
periodSeconds: 2
|
|
205
|
+
failureThreshold: {{ div (add1 .context.Values.startupTimeoutSeconds) 2 }}
|
|
206
|
+
{{- else }}
|
|
207
|
+
{{- if .checkValues.periodSeconds }}
|
|
208
|
+
periodSeconds: {{ .checkValues.periodSeconds }}
|
|
209
|
+
{{- end }}
|
|
210
|
+
{{- if .checkValues.failureThreshold }}
|
|
211
|
+
failureThreshold: {{ .checkValues.failureThreshold }}
|
|
212
|
+
{{- end }}
|
|
213
|
+
{{- end }}
|
|
214
|
+
{{- end }}
|
|
@@ -71,35 +71,9 @@ spec:
|
|
|
71
71
|
{{- if not (eq (len $envVars.vars) 0) }}
|
|
72
72
|
{{- $envVars.vars | toYaml | nindent 8}}
|
|
73
73
|
{{- end }}
|
|
74
|
-
livenessProbe
|
|
75
|
-
{{-
|
|
76
|
-
|
|
77
|
-
path: {{ .Values.health.liveness.path }}
|
|
78
|
-
port: http
|
|
79
|
-
{{- else }}
|
|
80
|
-
tcpSocket:
|
|
81
|
-
port: {{ .Values.port }}
|
|
82
|
-
{{- end }}
|
|
83
|
-
readinessProbe:
|
|
84
|
-
{{- if .Values.health.readiness.path }}
|
|
85
|
-
httpGet:
|
|
86
|
-
path: {{ .Values.health.readiness.path }}
|
|
87
|
-
port: http
|
|
88
|
-
{{- else }}
|
|
89
|
-
tcpSocket:
|
|
90
|
-
port: {{ .Values.port }}
|
|
91
|
-
{{- end }}
|
|
92
|
-
startupProbe:
|
|
93
|
-
{{- if .Values.health.liveness.path }}
|
|
94
|
-
httpGet:
|
|
95
|
-
path: {{ .Values.health.liveness.path }}
|
|
96
|
-
port: http
|
|
97
|
-
{{- else }}
|
|
98
|
-
tcpSocket:
|
|
99
|
-
port: {{ .Values.port }}
|
|
100
|
-
{{- end }}
|
|
101
|
-
failureThreshold: {{ div (add1 .Values.startupTimeoutSeconds) 2 }}
|
|
102
|
-
periodSeconds: 2
|
|
74
|
+
{{ include "web-application.internal.healthCheck" (dict "checkName" "livenessProbe" "checkValues" .Values.health.liveness "context" $) | nindent 8}}
|
|
75
|
+
{{ include "web-application.internal.healthCheck" (dict "checkName" "readinessProbe" "checkValues" .Values.health.readiness "context" $) | nindent 8}}
|
|
76
|
+
{{ include "web-application.internal.healthCheck" (dict "checkName" "startupProbe" "checkValues" .Values.health.liveness "context" $) | nindent 8}}
|
|
103
77
|
volumeMounts:
|
|
104
78
|
- name: tmp
|
|
105
79
|
mountPath: /tmp
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
-
"$id": "https://github.tools.sap/cki/web-application/blob/main/values.schema.json",
|
|
4
3
|
"type": "object",
|
|
5
4
|
"required": [
|
|
6
5
|
"image",
|
|
@@ -354,6 +353,14 @@
|
|
|
354
353
|
"path": {
|
|
355
354
|
"type": "string",
|
|
356
355
|
"default": "/healthz"
|
|
356
|
+
},
|
|
357
|
+
"periodSeconds": {
|
|
358
|
+
"type": "integer",
|
|
359
|
+
"minimum": 1
|
|
360
|
+
},
|
|
361
|
+
"failureThreshold": {
|
|
362
|
+
"type": "integer",
|
|
363
|
+
"minimum": 1
|
|
357
364
|
}
|
|
358
365
|
}
|
|
359
366
|
},
|
|
@@ -365,6 +372,14 @@
|
|
|
365
372
|
"path": {
|
|
366
373
|
"type": "string",
|
|
367
374
|
"default": "/healthz"
|
|
375
|
+
},
|
|
376
|
+
"periodSeconds": {
|
|
377
|
+
"type": "integer",
|
|
378
|
+
"minimum": 1
|
|
379
|
+
},
|
|
380
|
+
"failureThreshold": {
|
|
381
|
+
"type": "integer",
|
|
382
|
+
"minimum": 1
|
|
368
383
|
}
|
|
369
384
|
}
|
|
370
385
|
}
|
|
@@ -599,7 +614,7 @@
|
|
|
599
614
|
"gateway": {
|
|
600
615
|
"$id": "#/properties/expose/properties/gateway",
|
|
601
616
|
"type": "string",
|
|
602
|
-
"default": "kyma-
|
|
617
|
+
"default": "kyma-system/kyma-gateway"
|
|
603
618
|
},
|
|
604
619
|
"tenantGateway": {
|
|
605
620
|
"$id": "#/properties/expose/properties/gateway",
|
|
@@ -15,7 +15,7 @@ global:
|
|
|
15
15
|
_internal:
|
|
16
16
|
web-application:
|
|
17
17
|
versions:
|
|
18
|
-
1.
|
|
18
|
+
1.13.1: true
|
|
19
19
|
## Will be used instead of the `.Chart.Name`, e.g. when generating the Deployment name.
|
|
20
20
|
# nameOverride: name
|
|
21
21
|
|
|
@@ -97,10 +97,18 @@ health:
|
|
|
97
97
|
liveness:
|
|
98
98
|
# HTTP path used by Kubernetes, to perform liveness-probe calls.
|
|
99
99
|
path: /healthz
|
|
100
|
+
## How often (in seconds) to perform the probe.
|
|
101
|
+
# periodSeconds: 3
|
|
102
|
+
## Number of probes to fail in a row, in order to consider the check failed.
|
|
103
|
+
# failureThreshold: 5
|
|
100
104
|
# Readiness-probe configuration.
|
|
101
105
|
readiness:
|
|
102
106
|
# HTTP path used by Kubernetes, to perform readiness-probe calls.
|
|
103
107
|
path: /healthz
|
|
108
|
+
## How often (in seconds) to perform the probe.
|
|
109
|
+
# periodSeconds: 3
|
|
110
|
+
## Number of probes to fail in a row, in order to consider the check failed.
|
|
111
|
+
# failureThreshold: 5
|
|
104
112
|
# Initial timeout in seconds, during which the app must start giving the response to the liveness-probe.
|
|
105
113
|
startupTimeoutSeconds: 30
|
|
106
114
|
## Use read only filesystem for the Deployment.
|
|
@@ -141,7 +149,7 @@ expose:
|
|
|
141
149
|
# Expose the application to the internet.
|
|
142
150
|
enabled: true
|
|
143
151
|
# Use this istio gateway for incoming traffic.
|
|
144
|
-
gateway: kyma-
|
|
152
|
+
gateway: kyma-system/kyma-gateway
|
|
145
153
|
## Custom Istio Gateway to use with the VirtualService created using the TENANT_HOST_PATTERN.
|
|
146
154
|
## If your TENANT_HOST_PATTERN uses top level domain for the tenants (like tenant-name.<provider>.<cluster-domain>), you need to create custom Certificate and Gateway since the default Kyma Gateway will not work.
|
|
147
155
|
# tenantGateway: tenants-gateway.my-ns.svc.cluster.local
|
|
@@ -6,7 +6,9 @@ Your services are available at:
|
|
|
6
6
|
{{- $root := . }}
|
|
7
7
|
{{- range $name := include "cap.deploymentNames" . | split ";" }}
|
|
8
8
|
{{- $deployment := (get $root.Values $name) }}
|
|
9
|
+
{{- if $deployment.expose.enabled }}
|
|
9
10
|
{{- $ := merge (dict "name" $name "deployment" $deployment) $root }}
|
|
10
11
|
{{- $url := include "cap.deploymentHostFull" $ }}
|
|
11
12
|
{{ printf "%s - https://%s" $name $url }}
|
|
13
|
+
{{- end -}}
|
|
12
14
|
{{- end }}
|
|
@@ -51,7 +51,11 @@ copied from web-application: web-application.fullname
|
|
|
51
51
|
Get FQDN of a deployment
|
|
52
52
|
*/}}
|
|
53
53
|
{{- define "cap.deploymentHostFull" -}}
|
|
54
|
-
{{
|
|
54
|
+
{{- if not .deployment.expose.enabled }}
|
|
55
|
+
{{- printf "%s-%s.%s.svc.cluster.local:8080" .Release.Name .name .Release.Namespace }}
|
|
56
|
+
{{- else }}
|
|
57
|
+
{{- include "cap.deploymentHost" $ }}.{{ $.Values.global.domain }}
|
|
58
|
+
{{- end -}}
|
|
55
59
|
{{- end -}}
|
|
56
60
|
|
|
57
61
|
{{/*
|
|
@@ -73,7 +77,11 @@ Backend Destinations
|
|
|
73
77
|
{{- $deployment := (get $root.Values $destination.service) -}}
|
|
74
78
|
{{- $srv := merge (dict "name" $destination.service "destination" $destination "deployment" $deployment) $root -}}
|
|
75
79
|
{{- $destinationHost := include "cap.deploymentHostFull" $srv -}}
|
|
76
|
-
{{- $
|
|
80
|
+
{{- $protocol := "https" -}}
|
|
81
|
+
{{- if not $deployment.expose.enabled }}
|
|
82
|
+
{{- $protocol = "http" -}}
|
|
83
|
+
{{- end -}}
|
|
84
|
+
{{- $currentDestination := dict $nameKey $destination.name $urlKey (print $protocol "://" $destinationHost ) -}}
|
|
77
85
|
{{- $currentDestination := merge $currentDestination $.defaultParameters -}}
|
|
78
86
|
{{- $destinations = (append $destinations $currentDestination) -}}
|
|
79
87
|
{{- end -}}
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
],
|
|
188
188
|
"properties": {
|
|
189
189
|
"fromSecret": {
|
|
190
|
-
"description": "Name of a Kubernetes Secret, with the binding content, compliant to the SAP Kubernetes Service Binding spec
|
|
190
|
+
"description": "Name of a Kubernetes Secret, with the binding content, compliant to the SAP Kubernetes Service Binding spec",
|
|
191
191
|
"$ref": "#/definitions/KubernetesName"
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
const https = require('node:https')
|
|
2
|
+
const axios = require('axios')
|
|
3
|
+
const asJson = require("../data/as-json")
|
|
1
4
|
const cds = require('../../../cds')
|
|
2
5
|
const cf = require("../../../util/cf")
|
|
3
6
|
const { write, exists } = cds.utils
|
|
7
|
+
const { filterStringAsRegex } = require("../data/index")
|
|
4
8
|
|
|
5
9
|
module.exports = class HttpTemplate extends require('../../plugin') {
|
|
6
10
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#defaultDirName = "*"
|
|
11
|
+
#nodeHost = "http://localhost:4004"
|
|
12
|
+
#javaHost = "http://localhost:8080"
|
|
10
13
|
#defaultOutputDir = "http"
|
|
11
|
-
#defaultAuth = `Authorization: Basic
|
|
14
|
+
#defaultAuth = `Authorization: Basic alice:`
|
|
12
15
|
|
|
13
16
|
options() {
|
|
14
17
|
return {
|
|
15
18
|
'filter': {
|
|
16
19
|
type: 'string',
|
|
17
20
|
short: 'f',
|
|
18
|
-
help: `
|
|
21
|
+
help: `Filter for services or entities or actions matching the given pattern. If it contains meta
|
|
22
|
+
characters like '^' or '*', it is treated as a regular expression,
|
|
23
|
+
otherwise as an include pattern, i.e /.*pattern.*/i`
|
|
19
24
|
},
|
|
20
25
|
'for-app': {
|
|
21
26
|
type: 'string',
|
|
@@ -26,146 +31,187 @@ module.exports = class HttpTemplate extends require('../../plugin') {
|
|
|
26
31
|
type: 'string',
|
|
27
32
|
short: 'o',
|
|
28
33
|
help: 'The output directory. By default, an `http` dir will be created in one of the following: test/tests/__tests__, whichever already exists. If none exists, an `http` dir will be created at the root level.'
|
|
34
|
+
},
|
|
35
|
+
'dry': {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
help: 'Print the generated requests to the console instead of writing them to a file.'
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
async run() {
|
|
34
|
-
|
|
35
|
-
const path = cds.cli.options['filter'] || this.#defaultDirName
|
|
36
|
-
const csn = await cds.load(path)
|
|
43
|
+
const csn = await cds.load(cds.env.roots).then(cds.minify)
|
|
37
44
|
cds.linked(csn)
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let
|
|
46
|
+
const serviceInfo = cds.compile.to.serviceinfo(csn)
|
|
47
|
+
let [hostname, auth] = await this.#getHostnameAndAuth(serviceInfo)
|
|
48
|
+
let services = this.#collectServiceInfo(csn, serviceInfo)
|
|
49
|
+
if (!services) return
|
|
42
50
|
|
|
43
|
-
this.#
|
|
51
|
+
const payload = await this.#generatePayload(csn, services)
|
|
52
|
+
this.#generateRequests(services, hostname, auth, payload)
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
async #getHostnameAndAuth() {
|
|
55
|
+
async #getHostnameAndAuth(serviceInfo) {
|
|
47
56
|
if (!cds.cli.options['for-app']) {
|
|
48
|
-
|
|
57
|
+
if (serviceInfo[0].runtime === 'Java') {
|
|
58
|
+
return [this.#javaHost, null]
|
|
59
|
+
} else {
|
|
60
|
+
return [this.#nodeHost, this.#defaultAuth]
|
|
61
|
+
}
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
const
|
|
64
|
+
const app = cds.cli.options['for-app']
|
|
65
|
+
// Later: differentiate between CF and Kyma
|
|
66
|
+
const env = await cf.getEnv(app)
|
|
67
|
+
if (!cds.cli.options.dry) console.error('Fetching credentials from Cloud Foundry app \'' + app + '\'...')
|
|
52
68
|
const hostname = `https://${env.application_env_json.VCAP_APPLICATION.application_uris[0]}`
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const { access_token } = await response.json()
|
|
66
|
-
const auth = `x-approuter-authorization: bearer ` + access_token
|
|
67
|
-
return [hostname, auth]
|
|
68
|
-
} else {
|
|
69
|
-
throw new Error('Error authenticating ', { url, response })
|
|
70
|
-
}
|
|
69
|
+
const xsuaaCredentials = env.system_env_json.VCAP_SERVICES?.xsuaa[0]?.credentials ?? {}
|
|
70
|
+
const { url, clientid, clientsecret, certurl, certificate, key } = xsuaaCredentials
|
|
71
|
+
if (!url) throw new Error('No XSUAA service found in the environment of ' + app)
|
|
72
|
+
|
|
73
|
+
const auth = certificate ? { maxRedirects: 0, httpsAgent: new https.Agent({ cert: certificate, key }) }
|
|
74
|
+
: { auth: { username: clientid, password: clientsecret } }
|
|
75
|
+
const authUrl = `${certurl ?? url}/oauth/token`
|
|
76
|
+
const data = `grant_type=client_credentials&client_id=${encodeURI(clientid)}`
|
|
77
|
+
const config = { method: 'POST', timeout: 5000, data, ...auth }
|
|
78
|
+
const { access_token } = (await axios(authUrl, config)).data
|
|
79
|
+
const approuterAuth = `x-approuter-authorization: bearer ${access_token}`
|
|
80
|
+
return [hostname, approuterAuth]
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
#
|
|
74
|
-
const
|
|
83
|
+
#collectServiceInfo(csn, serviceInfo) {
|
|
84
|
+
const services = new Map()
|
|
85
|
+
|
|
86
|
+
const filter = cds.cli.options['filter']
|
|
75
87
|
|
|
76
88
|
csn.services.map((s) => {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
const sInfo = serviceInfo.find(info => info.name === s.name)
|
|
90
|
+
const urlPaths = sInfo?.endpoints?.map(e => e.path) ?? [sInfo?.urlPath]
|
|
91
|
+
if (!urlPaths) return
|
|
92
|
+
let entities = []
|
|
93
|
+
let actions = []
|
|
94
|
+
|
|
95
|
+
for (const c of csn.childrenOf(s.name)) {
|
|
96
|
+
if (c.name.match(filterStringAsRegex(filter))) {
|
|
97
|
+
if (c.name.includes('.texts')) continue
|
|
98
|
+
c.kind === 'entity' ? entities.push(this.#addNameAndReadOnly(c)) : c.kind === 'action' ? actions.push(c) : null
|
|
85
99
|
}
|
|
86
|
-
entityInfo.push(this.#addNameAndReadOnly(entities[e]))
|
|
87
100
|
}
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
if (entities.length === 0 && actions.length === 0) return
|
|
102
|
+
services.set(s.name, { entities, urlPaths, actions })
|
|
90
103
|
})
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
if (services.size === 0) {
|
|
106
|
+
console.error(`No services found for filter ${filter}`)
|
|
107
|
+
process.exitCode = 1
|
|
108
|
+
return
|
|
109
|
+
}
|
|
95
110
|
|
|
96
|
-
return
|
|
111
|
+
return services
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
#
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
: ["odata"]
|
|
114
|
+
#addNameAndReadOnly(e) {
|
|
115
|
+
return {
|
|
116
|
+
name: e.name,
|
|
117
|
+
readOnly: (e["@readonly"] || e["@cds.autoexpose"] || e['@odata.singleton']) ?? false,
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const serviceName = s.name.replace("Service", "").toLowerCase()
|
|
113
|
-
|
|
114
|
-
let prefix = ""
|
|
115
|
-
if (protocols) {
|
|
116
|
-
for (const p of protocols) {
|
|
117
|
-
if (p === "odata-v2") {
|
|
118
|
-
prefix = "/odata/v2/"
|
|
119
|
-
} else if (p === "rest") {
|
|
120
|
-
prefix = "/rest/"
|
|
121
|
-
} else {
|
|
122
|
-
prefix = this.#defaultPathPrefix
|
|
123
|
-
}
|
|
124
|
-
paths.push(prefix + serviceName)
|
|
125
|
-
}
|
|
121
|
+
async #generatePayload(csn, services) {
|
|
122
|
+
const payload = {}
|
|
123
|
+
for (const [, service] of services) {
|
|
124
|
+
for (const e of service.entities) {
|
|
125
|
+
payload[e.name] = []
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
name: e.name.split(".")[1],
|
|
134
|
-
readOnly: (e["@readonly"] || e["@cds.autoexpose"] || e['@odata.singleton']) ?? false,
|
|
128
|
+
// csn.all('entity').reduce((all, e) => { all[e.name] = []; return all }, {})
|
|
129
|
+
await asJson(payload, csn, 1)
|
|
130
|
+
for (const p in payload) {
|
|
131
|
+
delete payload[p][0].texts
|
|
132
|
+
delete payload[p][0].localized
|
|
135
133
|
}
|
|
134
|
+
return payload
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
#generateRequests(
|
|
139
|
-
for
|
|
137
|
+
#generateRequests(services, hostname, auth, payload) {
|
|
138
|
+
let varsDefined = false // when writing requests for multiple services to stdout, define vars only once
|
|
139
|
+
for (const [serviceName, service] of services) {
|
|
140
140
|
let requests = []
|
|
141
|
+
if (!cds.cli.options.dry || !varsDefined) {
|
|
142
|
+
requests.push(auth ? `@server = ${hostname}\n@auth = ${auth}\n` : `@server = ${hostname}\n`)
|
|
143
|
+
}
|
|
144
|
+
varsDefined = true
|
|
145
|
+
const headers = auth ? `Content-Type: application/json\n{{auth}}` : `Content-Type: application/json`
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const url = `{{server}}${path}/${e.name}`
|
|
147
|
-
const contentType = `Content-Type: application/json`
|
|
148
|
-
const authHeader = `{{auth}}`
|
|
149
|
-
|
|
150
|
-
requests.push(`GET ${url}\n${authHeader}\n${contentType}\n`)
|
|
147
|
+
service.urlPaths.forEach((urlPath) => {
|
|
148
|
+
if (service.urlPaths.length > 1) {
|
|
149
|
+
requests.push(`\n# Service endpoint: ${urlPath}`)
|
|
150
|
+
}
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
service.entities.forEach((e) => {
|
|
153
|
+
const entityNameSimple = e.name.slice(e.name.lastIndexOf('.') + 1)
|
|
154
|
+
const url = `{{server}}/${urlPath}${entityNameSimple}`
|
|
155
|
+
const requestTitle = `\n### ${e.name}`
|
|
156
|
+
requests.push(`${requestTitle}\nGET ${url}\n${headers}\n`)
|
|
157
|
+
if (e.readOnly) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
const entityData = payload[`${e.name}`] ? payload[`${e.name}`][0] : null
|
|
162
|
+
if (!entityData) return
|
|
163
|
+
// TODO handle ids that are not named ID
|
|
164
|
+
if (!Object.keys(entityData).some(k => k === "ID")) return // no need to write requests for entities without ids
|
|
165
|
+
|
|
166
|
+
const postBody = JSON.stringify(entityData, null, 2)
|
|
167
|
+
requests.push(`${requestTitle}\nPOST ${url}\n${headers}\n\n${postBody}\n`)
|
|
168
|
+
requests.push(`${requestTitle}\nPATCH ${url}/${entityData.ID}\n${headers}\n\n${postBody}\n`)
|
|
169
|
+
requests.push(`${requestTitle}\nDELETE ${url}/${entityData.ID}\n${headers}\n`)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
service.actions.forEach((a) => {
|
|
173
|
+
const url = `{{server}}/${urlPath}${a.name.split(".")[1]}`
|
|
174
|
+
|
|
175
|
+
let actionPayload = {}
|
|
176
|
+
if (a.params) {
|
|
177
|
+
for (const [key, value] of Object.entries(a.params)) {
|
|
178
|
+
actionPayload[key] = value.type === 'cds.Integer' ? 1
|
|
179
|
+
: value.type === 'string' ? 'sample string'
|
|
180
|
+
// TODO reference an object that has been generated
|
|
181
|
+
: typeof value.type === 'object' && value.type.ref ? payload[value.type.ref[0]][0][value.type.ref[1]]
|
|
182
|
+
: null
|
|
183
|
+
}
|
|
158
184
|
}
|
|
159
|
-
|
|
185
|
+
requests.push(`\n### ${a.name}\nPOST ${url}\n${headers}\n\n${JSON.stringify(actionPayload, null, 2)}\n`)
|
|
186
|
+
})
|
|
187
|
+
|
|
160
188
|
})
|
|
189
|
+
this.#write(requests, serviceName)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
161
192
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
#write(requests, serviceName) {
|
|
194
|
+
const http = requests.join('\n')
|
|
195
|
+
if (cds.cli.options.dry) {
|
|
196
|
+
console.log(http)
|
|
197
|
+
} else {
|
|
198
|
+
const term = require('../../../util/term')
|
|
199
|
+
const outputPath = this.#determineOutputPath(serviceName)
|
|
200
|
+
if (exists(outputPath)) {
|
|
201
|
+
if (cds.cli.options.force) {
|
|
202
|
+
console.log(term.dim(` Overwriting ${outputPath}`))
|
|
203
|
+
} else {
|
|
204
|
+
console.log(term.dim(` Skipping ${outputPath}`))
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
console.log(term.dim(` Creating ${outputPath}`))
|
|
209
|
+
}
|
|
210
|
+
write(http).to(outputPath)
|
|
165
211
|
}
|
|
166
212
|
}
|
|
167
213
|
|
|
168
|
-
#determineOutputPath(
|
|
214
|
+
#determineOutputPath(serviceName) {
|
|
169
215
|
let outputDir
|
|
170
216
|
const { out } = cds.cli.options
|
|
171
217
|
if (out) {
|
|
@@ -175,7 +221,7 @@ module.exports = class HttpTemplate extends require('../../plugin') {
|
|
|
175
221
|
} else {
|
|
176
222
|
outputDir = this.#defaultOutputDir
|
|
177
223
|
}
|
|
178
|
-
return `${outputDir}/${
|
|
224
|
+
return `${outputDir}/${serviceName}.http`
|
|
179
225
|
}
|
|
180
226
|
}
|
|
181
227
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../cds')
|
|
2
|
+
const { exists, fs, path } = cds.utils
|
|
1
3
|
const { readProject } = require('../../projectReader')
|
|
2
4
|
const { merge } = require('../../merge')
|
|
3
5
|
const { srv4, kafkaMessaging } = require('../../registries/mta')
|
|
@@ -6,6 +8,10 @@ const mvn = require('../../mvn')
|
|
|
6
8
|
module.exports = class KafkaTemplate extends require('../../plugin') {
|
|
7
9
|
|
|
8
10
|
static hasInProduction(env) {
|
|
11
|
+
if (exists('pom.xml')) {
|
|
12
|
+
if (!exists('srv/pom.xml')) return false
|
|
13
|
+
return fs.readFileSync(path.resolve(cds.root, 'srv/pom.xml'), 'utf8').includes('cds-feature-kafka')
|
|
14
|
+
}
|
|
9
15
|
return !!(env.requires?.messaging?.kind === 'kafka')
|
|
10
16
|
}
|
|
11
17
|
|
|
@@ -11,7 +11,7 @@ const cds = require("../../../cds");
|
|
|
11
11
|
const { mkdirp, exists } = cds.utils;
|
|
12
12
|
const IS_WIN = os.platform() === "win32";
|
|
13
13
|
const VSCODE_FILE_ASSOCIATIONS = {
|
|
14
|
-
"csv": [
|
|
14
|
+
"csv": ["csv", "csv (semicolon)", "tsv", "tab"]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
module.exports = class LintTemplate extends require('../../plugin') {
|
|
@@ -43,7 +43,7 @@ module.exports = class LintTemplate extends require('../../plugin') {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async run() {
|
|
46
|
-
// Require ESLint v>=
|
|
46
|
+
// Require ESLint v>=8.0.0
|
|
47
47
|
if (!this.isTest) {
|
|
48
48
|
this._checkEslint();
|
|
49
49
|
}
|
|
@@ -69,21 +69,20 @@ module.exports = class LintTemplate extends require('../../plugin') {
|
|
|
69
69
|
// Generate VS Code extension settings
|
|
70
70
|
const { getFileExtensions } = require("@sap/eslint-plugin-cds/lib/api");
|
|
71
71
|
const ALLOWED_FILE_EXTENSIONS = getFileExtensions();
|
|
72
|
-
|
|
72
|
+
const extensions = new Set(ALLOWED_FILE_EXTENSIONS.map((ext) => {
|
|
73
73
|
return path.extname(ext).replace('.', '');
|
|
74
|
-
})
|
|
74
|
+
}))
|
|
75
75
|
extensions.forEach((ext) => {
|
|
76
76
|
if (ext in VSCODE_FILE_ASSOCIATIONS) {
|
|
77
77
|
VSCODE_FILE_ASSOCIATIONS[ext].forEach((assoc) => {
|
|
78
|
-
extensions.
|
|
78
|
+
extensions.add(assoc)
|
|
79
79
|
})
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
await io.sanitizeVscodeSettings(
|
|
83
|
-
cds.root,
|
|
84
83
|
path.join(cds.root, ".vscode/settings.json"),
|
|
85
84
|
this.lintType,
|
|
86
|
-
extensions,
|
|
85
|
+
Array.from(extensions),
|
|
87
86
|
this.customRuleExample
|
|
88
87
|
);
|
|
89
88
|
}
|
|
@@ -124,8 +123,8 @@ module.exports = class LintTemplate extends require('../../plugin') {
|
|
|
124
123
|
_checkEslint() {
|
|
125
124
|
const eslintCmd = checks.resolveEslint(cds.root, '');
|
|
126
125
|
if (!eslintCmd) {
|
|
127
|
-
this.missingNpmDependencies["names"].push("eslint");
|
|
128
|
-
this.missingNpmDependencies["msgs"].push("ESLint v>=
|
|
126
|
+
this.missingNpmDependencies["names"].push("eslint@^9");
|
|
127
|
+
this.missingNpmDependencies["msgs"].push("ESLint v>=9.0.0");
|
|
129
128
|
}
|
|
130
129
|
return;
|
|
131
130
|
}
|
|
@@ -135,10 +134,11 @@ module.exports = class LintTemplate extends require('../../plugin') {
|
|
|
135
134
|
"@sap/eslint-plugin-cds",
|
|
136
135
|
cds.root
|
|
137
136
|
);
|
|
137
|
+
|
|
138
138
|
// Require locally installed ESLint CDS plugin
|
|
139
139
|
if (!eslintPluginLocal) {
|
|
140
|
-
this.missingNpmDependencies["names"].push("@sap/eslint-plugin-cds");
|
|
141
|
-
this.missingNpmDependencies["msgs"].push("ESLint plugin for CDS");
|
|
140
|
+
this.missingNpmDependencies["names"].push("@sap/eslint-plugin-cds@^3.0.0");
|
|
141
|
+
this.missingNpmDependencies["msgs"].push("ESLint plugin for CDS v>=3.0.0");
|
|
142
142
|
}
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
@@ -153,10 +153,9 @@ module.exports = class LintTemplate extends require('../../plugin') {
|
|
|
153
153
|
const { getConfigPath } = require("@sap/eslint-plugin-cds/lib/api");
|
|
154
154
|
let configPath = getConfigPath(cds.root);
|
|
155
155
|
if (!configPath) {
|
|
156
|
-
configPath = path.join(cds.root, ".
|
|
156
|
+
configPath = path.join(cds.root, "eslint.config.js");
|
|
157
157
|
}
|
|
158
158
|
await io.sanitizeEslintConfig(configPath, this.customRuleExample);
|
|
159
|
-
return;
|
|
160
159
|
}
|
|
161
160
|
|
|
162
161
|
async _promptInstallDependencies(task) {
|