@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.
Files changed (90) hide show
  1. package/bin/add.js +28 -7
  2. package/bin/bind.js +64 -21
  3. package/bin/build.js +35 -3
  4. package/bin/cds.js +32 -13
  5. package/bin/compile/index.js +44 -3
  6. package/bin/completion/completionFs.js +61 -0
  7. package/bin/completion/index.js +212 -0
  8. package/bin/completion/scripts/cds-bash.sh +17 -0
  9. package/bin/completion/scripts/cds-ps.ps1 +18 -0
  10. package/bin/completion/scripts/cds-zsh.sh +12 -0
  11. package/bin/help.js +8 -2
  12. package/bin/init.js +34 -4
  13. package/bin/subscribe.js +5 -1
  14. package/bin/watch.js +4 -2
  15. package/lib/bind/KubernetesApiAccessor.js +3 -2
  16. package/lib/bind/bindingResolver.js +3 -2
  17. package/lib/bind/bindingUtil.js +10 -8
  18. package/lib/bind/cfKeyProvider.js +58 -153
  19. package/lib/bind/index.js +46 -36
  20. package/lib/bind/kubernetesCredentialsProvider.js +6 -5
  21. package/lib/bind/userCdsrcJsonStorageProvider.js +25 -3
  22. package/lib/build/buildTaskEngine.js +1 -1
  23. package/lib/build/provider/hana/index.js +1 -1
  24. package/lib/build/provider/hana/template/undeploy.json +3 -1
  25. package/lib/build/provider/nodejs/index.js +1 -1
  26. package/lib/compile/openapi/csdl2openapi.js +4 -0
  27. package/lib/compile/openapi/index.js +5 -0
  28. package/lib/deploy/to-hana/hana.js +25 -7
  29. package/lib/env/schemas/cds-rc.js +29 -2
  30. package/lib/import/odata/v4parser.js +78 -25
  31. package/lib/init/constants.js +2 -1
  32. package/lib/init/index.js +18 -13
  33. package/lib/init/merge.js +1 -0
  34. package/lib/init/mvn.js +8 -47
  35. package/lib/init/plugin.js +0 -4
  36. package/lib/init/projectReader.js +17 -10
  37. package/lib/init/template/approuter/files/values.yaml.hbs +6 -12
  38. package/lib/init/template/approuter/index.js +1 -7
  39. package/lib/init/template/completion/completionSetup.js +184 -0
  40. package/lib/init/template/completion/index.js +22 -0
  41. package/lib/init/template/data/as-csv.js +25 -17
  42. package/lib/init/template/data/as-json.js +115 -30
  43. package/lib/init/template/data/index.js +28 -15
  44. package/lib/init/template/hana/files/undeploy.json +2 -1
  45. package/lib/init/template/helm/files/chart/values.schema.json +1 -1
  46. package/lib/init/template/helm/files/content-deployment/Chart.yaml +0 -2
  47. package/lib/init/template/helm/files/content-deployment/values.schema.json +0 -1
  48. package/lib/init/template/helm/files/service-instance/Chart.yaml +0 -2
  49. package/lib/init/template/helm/files/service-instance/values.schema.json +0 -1
  50. package/lib/init/template/helm/files/web-application/Chart.yaml +1 -3
  51. package/lib/init/template/helm/files/web-application/templates/_helpers.tpl +23 -0
  52. package/lib/init/template/helm/files/web-application/templates/deployment.yaml +3 -29
  53. package/lib/init/template/helm/files/web-application/values.schema.json +17 -2
  54. package/lib/init/template/helm/files/web-application/values.yaml +10 -2
  55. package/lib/init/template/helm-unified-runtime/files/templates/NOTES.txt +2 -0
  56. package/lib/init/template/helm-unified-runtime/files/templates/_deployment_helpers.tpl +10 -2
  57. package/lib/init/template/helm-unified-runtime/files/values.schema.json +1 -1
  58. package/lib/init/template/http/index.js +150 -104
  59. package/lib/init/template/kafka/index.js +6 -0
  60. package/lib/init/template/lint/index.js +12 -13
  61. package/lib/init/template/mta/files/mta.yaml.hbs +1 -1
  62. package/lib/init/template/multitenancy/files/mtxs-configmap-java.yaml +1 -1
  63. package/lib/init/template/multitenancy/files/values.yaml.hbs +0 -5
  64. package/lib/init/template/multitenancy/index.js +13 -5
  65. package/lib/init/template/nodejs/files/.vscode/settings.json +11 -0
  66. package/lib/init/template/nodejs/files/eslint.config.js +13 -0
  67. package/lib/init/template/nodejs/files/package.json.hbs +3 -1
  68. package/lib/init/template/sample/index.js +18 -20
  69. package/lib/init/template/tiny-sample/index.js +1 -2
  70. package/lib/init/template/typer/index.js +2 -2
  71. package/lib/lint/Linter.js +133 -55
  72. package/lib/lint/checks.js +5 -6
  73. package/lib/lint/io.js +119 -10
  74. package/lib/mtx/auth_manager.js +5 -5
  75. package/lib/mtx/params.js +4 -2
  76. package/lib/mtx/settings_manager.js +1 -1
  77. package/lib/mtx/tenant_action.js +12 -2
  78. package/lib/mtx/util/logging.js +10 -4
  79. package/lib/util/axios.js +2 -1
  80. package/lib/util/cf.js +32 -37
  81. package/lib/util/fs.js +1 -1
  82. package/lib/util/git.js +1 -1
  83. package/lib/util/term.js +1 -1
  84. package/{bin → lib/watch}/watched.js +2 -2
  85. package/npm-shrinkwrap.json +317 -293
  86. package/package.json +2 -2
  87. package/lib/init/template/approuter/files/approuter-mtxs-configmap.yaml +0 -19
  88. package/lib/init/template/nodejs/files/.eslintrc +0 -26
  89. /package/lib/init/template/{approuter/files/mtxs-configmap-java.yaml → multitenancy/files/mtxs-configmap-java-approuter.yaml} +0 -0
  90. /package/lib/init/template/{approuter/files/mtxs-configmap-nodejs.yaml → multitenancy/files/mtxs-configmap-nodejs-approuter.yaml} +0 -0
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema",
3
- "$id": "https://github.tools.sap/cki/content-deployment/blob/main/values.schema.json",
4
3
  "type": "object",
5
4
  "required": [
6
5
  "image", "bindings"
@@ -1,7 +1,5 @@
1
1
  apiVersion: v2
2
2
  description: A Helm chart for creating service instances on btp-service-operator
3
3
  name: service-instance
4
- sources:
5
- - https://github.tools.sap/unified-runtime/service-instance
6
4
  type: application
7
5
  version: 1.3.0
@@ -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-cf/blob/main/values.schema.json",
4
3
  "type": "object",
5
4
  "required": [
6
5
  "serviceOfferingName",
@@ -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.12.0
6
+ version: 1.13.1
@@ -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
- {{- if .Values.health.liveness.path }}
76
- httpGet:
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-gateway.kyma-system.svc.cluster.local"
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.12.0: true
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-gateway.kyma-system.svc.cluster.local
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
- {{ include "cap.deploymentHost" $ }}.{{ $.Values.global.domain }}
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
- {{- $currentDestination := dict $nameKey $destination.name $urlKey (print "https://" $destinationHost ) -}}
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 https://github.tools.sap/Kubernetes-Service-Bindings/doc/",
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
- #defaultHost = "http://localhost:4004"
8
- #defaultPathPrefix = "/odata/v4/"
9
- #defaultDirName = "*"
11
+ #nodeHost = "http://localhost:4004"
12
+ #javaHost = "http://localhost:8080"
10
13
  #defaultOutputDir = "http"
11
- #defaultAuth = `Authorization: Basic yves:`
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: `Specify the path to the service definition file or directory. Default is ${this.#defaultDirName}.`
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
- let [hostname, auth] = await this.#getHostnameAndAuth()
40
-
41
- let endpoints = this.#collectEndpointInfo(csn)
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.#generateRequests(endpoints, hostname, auth)
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
- return [this.#defaultHost, this.#defaultAuth]
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 env = await cf.getEnv(cds.cli.options['for-app'])
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 { url, clientid, clientsecret } = env.system_env_json.VCAP_SERVICES?.xsuaa[0]?.credentials || {}
54
-
55
- const response = await fetch(`${url}/oauth/token?${new URLSearchParams({
56
- grant_type: 'client_credentials',
57
- response_type: 'token',
58
- })}`, {
59
- method: 'GET',
60
- headers: {
61
- 'Authorization': `Basic ${Buffer.from(`${clientid}:${clientsecret}`).toString('base64')}`
62
- }
63
- })
64
- if (response.ok) {
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
- #collectEndpointInfo(csn) {
74
- const mapping = {}
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 protocols = this.#getProtocols(s)
78
- const paths = this.#getPaths(s, protocols)
79
-
80
- let entityInfo = []
81
- const entities = csn.entities(s.name)
82
- for (const e in entities) {
83
- if (entities[e].name.endsWith('.texts')) {
84
- continue
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
- mapping[s.name] = { entities: entityInfo, paths: paths, protocols: protocols, actions: [] }
101
+ if (entities.length === 0 && actions.length === 0) return
102
+ services.set(s.name, { entities, urlPaths, actions })
90
103
  })
91
104
 
92
- csn.foreach('action', (a) => {
93
- mapping[a.name.split(".")[0]].actions.push(a)
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 mapping
111
+ return services
97
112
  }
98
113
 
99
- #getProtocols(s) {
100
- return s["@protocol"]
101
- ? typeof s["@protocol"] === "string"
102
- ? [s["@protocol"]]
103
- : s["@protocol"]
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
- #getPaths(s, protocols) {
108
- let paths = []
109
- if (s["@path"]) {
110
- paths.push(s["@path"])
111
- } else {
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 paths
129
- }
130
-
131
- #addNameAndReadOnly(e) {
132
- return {
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(mapping, hostname, auth) {
139
- for (const service in mapping) {
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
- requests.push(`@server = ${hostname}\n@auth = ${auth}\n`)
143
-
144
- mapping[service].entities.forEach((e) => {
145
- for (const path of mapping[service].paths) {
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
- if (!e.readOnly) {
153
- const allHeaders = `${contentType}\n${authHeader}`
154
- requests.push(`POST ${url}\n${allHeaders}\n\n{}\n`)
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
- requests.push(`PATCH ${url}\n${allHeaders}\n\n{}\n`)
157
- requests.push(`DELETE ${url}\n${authHeader}\n`)
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
- const outputPath = this.#determineOutputPath(service)
163
- write(requests.join("\n###\n")).to(outputPath)
164
- console.log(`${outputPath} has been created.`)
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(service) {
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}/${service}.http`
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": [ "csv", "csv (semicolon)", "tsv", "tab" ]
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>=7.0.0
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
- const extensions = ALLOWED_FILE_EXTENSIONS.map((ext) => {
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.push(assoc)
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>=7.0.0");
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, ".eslintrc.json");
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) {