@sap/cds-dk 7.8.2 → 7.9.2

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 (78) 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 +31 -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 +8 -4
  17. package/lib/bind/bindingUtil.js +10 -8
  18. package/lib/bind/cfKeyProvider.js +58 -153
  19. package/lib/bind/index.js +49 -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/index.js +5 -0
  27. package/lib/deploy/to-hana/hana.js +25 -7
  28. package/lib/env/schemas/cds-rc.js +29 -2
  29. package/lib/import/odata/v4parser.js +78 -25
  30. package/lib/init/constants.js +2 -1
  31. package/lib/init/index.js +18 -13
  32. package/lib/init/merge.js +1 -0
  33. package/lib/init/mvn.js +8 -47
  34. package/lib/init/plugin.js +0 -4
  35. package/lib/init/projectReader.js +17 -10
  36. package/lib/init/template/approuter/files/values.yaml.hbs +0 -12
  37. package/lib/init/template/completion/completionSetup.js +184 -0
  38. package/lib/init/template/completion/index.js +22 -0
  39. package/lib/init/template/data/as-csv.js +25 -17
  40. package/lib/init/template/data/as-json.js +148 -44
  41. package/lib/init/template/data/index.js +28 -15
  42. package/lib/init/template/hana/files/undeploy.json +2 -1
  43. package/lib/init/template/helm/files/chart/values.schema.json +1 -1
  44. package/lib/init/template/helm/files/content-deployment/Chart.yaml +0 -2
  45. package/lib/init/template/helm/files/content-deployment/values.schema.json +0 -1
  46. package/lib/init/template/helm/files/service-instance/Chart.yaml +0 -2
  47. package/lib/init/template/helm/files/service-instance/values.schema.json +0 -1
  48. package/lib/init/template/helm/files/web-application/Chart.yaml +0 -2
  49. package/lib/init/template/helm/files/web-application/values.schema.json +0 -1
  50. package/lib/init/template/helm-unified-runtime/files/values.schema.json +1 -1
  51. package/lib/init/template/http/index.js +150 -104
  52. package/lib/init/template/kafka/index.js +6 -0
  53. package/lib/init/template/lint/index.js +12 -13
  54. package/lib/init/template/mta/files/mta.yaml.hbs +2 -2
  55. package/lib/init/template/multitenancy/files/values.yaml.hbs +0 -5
  56. package/lib/init/template/multitenancy/index.js +13 -5
  57. package/lib/init/template/nodejs/files/.vscode/settings.json +11 -0
  58. package/lib/init/template/nodejs/files/eslint.config.js +13 -0
  59. package/lib/init/template/nodejs/files/package.json.hbs +3 -1
  60. package/lib/init/template/sample/index.js +18 -20
  61. package/lib/init/template/tiny-sample/index.js +1 -2
  62. package/lib/init/template/typer/index.js +2 -2
  63. package/lib/lint/Linter.js +133 -55
  64. package/lib/lint/checks.js +5 -6
  65. package/lib/lint/io.js +119 -10
  66. package/lib/mtx/auth_manager.js +5 -5
  67. package/lib/mtx/params.js +4 -2
  68. package/lib/mtx/settings_manager.js +1 -1
  69. package/lib/mtx/tenant_action.js +12 -2
  70. package/lib/mtx/util/logging.js +10 -4
  71. package/lib/util/axios.js +2 -1
  72. package/lib/util/cf.js +32 -37
  73. package/lib/util/git.js +1 -1
  74. package/lib/util/term.js +1 -1
  75. package/{bin → lib/watch}/watched.js +2 -2
  76. package/npm-shrinkwrap.json +35 -52
  77. package/package.json +2 -2
  78. package/lib/init/template/nodejs/files/.eslintrc +0 -26
@@ -1,5 +1,6 @@
1
1
  const cds = require('../../../cds')
2
2
  const { exists, fs } = cds.utils
3
+ const { dim } = require('../../../util/term')
3
4
  const { resolve, join, relative } = require('path')
4
5
  const { URLSearchParams } = require('url')
5
6
  const { env4 } = require('../../projectReader')
@@ -14,8 +15,8 @@ module.exports = class DataTemplate extends require('../../plugin') {
14
15
  type: 'string',
15
16
  short: 'f',
16
17
  help: `Filter for entities matching the given pattern. If it contains meta
17
- characters like '^' or '*', it is treated like a regular expression,
18
- otherwise as a prefix.`
18
+ characters like '^' or '*', it is treated as a regular expression,
19
+ otherwise as an include pattern, i.e /.*pattern.*/i`
19
20
  },
20
21
  'data:for': {
21
22
  type: 'string',
@@ -52,8 +53,9 @@ module.exports = class DataTemplate extends require('../../plugin') {
52
53
  if (Number.isNaN(records) || records < 1) records = 1
53
54
 
54
55
  const [filterName, filterQueryStr] = (cds.cli.options['filter'] ?? cds.cli.options['data:for'] ?? '').split(':')
55
- const nameFilter = asRegex(filterName)
56
+ const nameFilter = filterStringAsRegex(filterName)
56
57
  const dataForParams = parseQueryStr(filterQueryStr)
58
+ const { queries } = dataForParams
57
59
 
58
60
  const env = env4('production')
59
61
  let dest = (typeof out === 'string') // target folder
@@ -68,33 +70,43 @@ module.exports = class DataTemplate extends require('../../plugin') {
68
70
 
69
71
  const data = csn.all('entity')
70
72
  .filter (e => e.name.match(nameFilter)) // --for prefix|regex
73
+ .filter (e => queries || !e.query) // remove all projection-like entities unless specified differently
71
74
  .reduce((all, e) => { all[e.name] = []; return all }, {})
72
75
 
73
76
  await asJson(data, csn, records)
74
77
 
75
78
  if (contentType === 'csv') {
76
79
  const headerOnly = !cds.cli.options['records'] // compat to old behavior which only creates headers
77
- const queries = false // old behavior excluded queries
78
- await asCsv(data, csn, Object.assign({ headerOnly, queries }, dataForParams))
80
+ await asCsv(data, csn, Object.assign({ headerOnly }, dataForParams))
79
81
  }
80
82
 
81
83
  // write files
82
- for (const name of Object.keys(data)) {
84
+ for (const name of Object.keys(data).sort()) {
83
85
  writeFileFor(csn.definitions[name], data[name], csn, dest, force)
84
86
  }
85
87
  }
86
88
  }
87
89
  module.exports.asJson = asJson
88
90
  module.exports.asCsv = asCsv
89
-
90
- function asRegex(spec) {
91
+ module.exports.filterStringAsRegex = filterStringAsRegex
92
+
93
+ /**
94
+ * Turns a filter string with optional meta characters into a regular expression.
95
+ * If no meta characters are found, the string is treated as an include pattern.
96
+ *
97
+ * @param {string} spec the filter string
98
+ * @returns {RegExp} the regular expression
99
+ */
100
+ function filterStringAsRegex(spec) {
91
101
  if (typeof spec === 'string') {
92
102
  try {
93
- if (spec.match(/[\^$|*]/))
94
- return new RegExp(spec)
95
- else { // no meta chars -> prefix semantics
96
- spec = spec.replace(/\./g, '\\.') // escape dot
97
- return new RegExp('^'+spec+'.*')
103
+ if (spec.match(/[\^$|*]/)) {
104
+ spec = spec.replace(/^\*/, '.*') // * at start would be invalid, so make it a .*
105
+ return new RegExp(spec, 'i')
106
+ }
107
+ else { // no meta chars -> include semantics
108
+ spec = spec.replace(/\./g, '\\.') // escape literal dot
109
+ return new RegExp(spec, 'i')
98
110
  }
99
111
  } catch (err) {
100
112
  throw err.message // user error, so cut off stack trace
@@ -149,7 +161,7 @@ function writeFileFor (entity, data, csn, dest, force) {
149
161
 
150
162
  // continue only if file not already exists, or '--force' option provided
151
163
  if (fileExists && !force) {
152
- console.log(`Skipping ${relFilePath}`)
164
+ console.log(` ${dim('Skipping ' + relFilePath)}`)
153
165
  return
154
166
  }
155
167
 
@@ -158,7 +170,8 @@ function writeFileFor (entity, data, csn, dest, force) {
158
170
  if (data.length) {
159
171
  if (!exists(dest)) fs.mkdirSync(dest, {recursive: true})
160
172
  fs.writeFileSync(dataFilePath, data)
161
- fileExists ? console.log(`Overwriting `+relFilePath) : console.log(`Creating `+relFilePath)
173
+ if (fileExists) console.log(` ${dim('Overwriting ' + relFilePath)}`)
174
+ else console.log(` ${dim('Creating ' + relFilePath)}`)
162
175
  }
163
176
  }
164
177
 
@@ -2,5 +2,6 @@
2
2
  "src/gen/**/*.hdbview",
3
3
  "src/gen/**/*.hdbindex",
4
4
  "src/gen/**/*.hdbconstraint",
5
- "src/gen/**/*_drafts.hdbtable"
5
+ "src/gen/**/*_drafts.hdbtable",
6
+ "src/gen/**/*.hdbcalculationview"
6
7
  ]
@@ -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,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.6.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
6
  version: 1.13.1
@@ -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",
@@ -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) {
@@ -27,13 +27,13 @@ modules:
27
27
  {{/isNodejs}}
28
28
  {{#isJava}}
29
29
  parameters:
30
- buildpack: sap_java_buildpack
30
+ buildpack: sap_java_buildpack_jakarta
31
31
  readiness-health-check-type: http
32
32
  readiness-health-check-http-endpoint: /
33
33
  properties:
34
34
  SPRING_PROFILES_ACTIVE: cloud,sandbox
35
35
  JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']"
36
- JBP_CONFIG_SAP_MACHINE_JRE: '{ use_offline_repository: false, version: 17.+ }'
36
+ JBP_CONFIG_SAP_MACHINE_JRE: '{ version: {{jdkVersion}}.+ }'
37
37
  build-parameters:
38
38
  builder: custom
39
39
  commands:
@@ -7,11 +7,6 @@ srv:
7
7
  {{/isJava}}
8
8
  service-manager:
9
9
  serviceInstanceName: service-manager
10
- {{#hasXsuaa}}
11
- xsuaa:
12
- parameters:
13
- tenant-mode: shared
14
- {{/hasXsuaa}}
15
10
  service-manager:
16
11
  serviceOfferingName: service-manager
17
12
  servicePlanName: container