@sap/cds-dk 7.8.2 → 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 (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 +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/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 +115 -30
  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 +1 -1
  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 +34 -34
  77. package/package.json +2 -2
  78. package/lib/init/template/nodejs/files/.eslintrc +0 -26
@@ -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) {
@@ -33,7 +33,7 @@ modules:
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: '{ use_offline_repository: false, 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
@@ -78,7 +78,14 @@ module.exports = class MultitenancyTemplate extends require('../../plugin') {
78
78
  }
79
79
 
80
80
  if (hasHelm || hasHelmUnifiedRuntime) {
81
- await merge(__dirname, 'files/values.yaml.hbs').into('chart/values.yaml', { with: project })
81
+ const overwrites = hasXsuaa ? [{
82
+ item: [{
83
+ in: 'xsuaa',
84
+ where: { serviceOfferingName: 'xsuaa' }
85
+ }, 'parameters.tenant-mode'], withValue: 'shared'
86
+ }] : []
87
+
88
+ await merge(__dirname, 'files/values.yaml.hbs').into('chart/values.yaml', { with: project, overwrites })
82
89
 
83
90
  const envFromObject = {
84
91
  envFrom: [{
@@ -100,11 +107,12 @@ module.exports = class MultitenancyTemplate extends require('../../plugin') {
100
107
  }
101
108
  }
102
109
  ]
103
- }}).into('chart/values.yaml', { with: project })
110
+ }
111
+ }).into('chart/values.yaml', { with: project })
104
112
 
105
- await removeFromYAML(join('chart', 'values.yaml'), ['srv.expose', 'srv.networkSecurity'])
106
- await copy(join(__dirname, 'files', `mtxs-configmap-${isJava ? 'java' : 'nodejs'}${hasApprouter ? '-approuter' : ''}.yaml`)).to('chart', 'templates', 'mtxs-configmap.yaml') // REVISIT: Move to build task
107
- await copy(join(__dirname, 'files', 'saas-registry-secret.yaml')).to('chart', 'templates', 'saas-registry-secret.yaml') // REVISIT: Move to build task
113
+ await removeFromYAML(join('chart', 'values.yaml'), ['srv.expose', 'srv.networkSecurity'])
114
+ await copy(join(__dirname, 'files', `mtxs-configmap-${isJava ? 'java' : 'nodejs'}${hasApprouter ? '-approuter' : ''}.yaml`)).to('chart', 'templates', 'mtxs-configmap.yaml') // REVISIT: Move to build task
115
+ await copy(join(__dirname, 'files', 'saas-registry-secret.yaml')).to('chart', 'templates', 'saas-registry-secret.yaml') // REVISIT: Move to build task
108
116
  }
109
117
 
110
118
  if (hasXsuaa) {
@@ -0,0 +1,11 @@
1
+ {
2
+ // uncomment entries once all libraries have been installed via 'npm install'
3
+ "eslint.validate": [
4
+ // "cds",
5
+ // "csn",
6
+ // "csv",
7
+ // "csv (semicolon)",
8
+ // "tab",
9
+ // "tsv"
10
+ ]
11
+ }
@@ -0,0 +1,13 @@
1
+ const cds = require('@sap/eslint-plugin-cds')
2
+
3
+ module.exports = [
4
+ cds.configs.recommended,
5
+ {
6
+ plugins: {
7
+ '@sap/cds': cds
8
+ },
9
+ rules: {
10
+ ...cds.configs.recommended.rules
11
+ }
12
+ }
13
+ ]
@@ -10,7 +10,9 @@
10
10
  "express": "^4"
11
11
  },
12
12
  "devDependencies": {
13
- "@cap-js/sqlite": "^1"
13
+ "@cap-js/sqlite": "^1",
14
+ "@sap/eslint-plugin-cds": "^3",
15
+ "eslint": "^9"
14
16
  },
15
17
  "scripts": {
16
18
  "start": "cds-serve"
@@ -1,33 +1,31 @@
1
1
  const { join } = require('path')
2
2
  const cds = require('../../../cds'), { exists, read, write, copy } = cds.utils
3
3
  const { env4, readProject } = require('../../projectReader')
4
+ const mvn = require('../../mvn')
4
5
 
5
6
  module.exports = class SampleTemplate extends require('../../plugin') {
6
7
 
7
- async canRun() {
8
- const { isJava } = readProject()
9
- if (isJava) throw `'cds add sample' is not available for Java yet`
10
- return true
11
- }
12
-
13
8
  async run() {
14
9
  const { db, srv, app } = env4('production').folders
15
10
  const language = exists('pom.xml') ? 'java' : 'nodejs'
16
11
 
17
- await Promise.all([
18
- copy(join(__dirname, 'files', 'db')).to(db),
19
- copy(join(__dirname, 'files', 'srv')).to(srv),
20
- copy(join(__dirname, 'files', language)).to(srv)
21
- ])
22
-
23
- await copy(join(__dirname, 'files', 'app')).to(app)
24
- const { appName, appPath, appUIPaths } = readProject()
12
+ if (language === 'nodejs') {
13
+ await Promise.all([
14
+ copy(join(__dirname, 'files/db')).to(db),
15
+ copy(join(__dirname, 'files/srv')).to(srv)
16
+ ])
17
+ await copy(join(__dirname, 'files', language)).to(srv)
18
+ await copy(join(__dirname, 'files', 'app')).to(app)
19
+ const { appName, appPath, appUIPaths } = readProject()
25
20
 
26
- // manifest.json must be unique
27
- await Promise.all(appUIPaths.map(async p => {
28
- const manifest = await read(join(appPath, p, 'webapp/manifest.json'))
29
- manifest['sap.app'].id = appName + '.' + p
30
- await write(join(appPath, p, 'webapp/manifest.json'), manifest, { spaces: 2 })
31
- }))
21
+ // manifest.json must be unique // REVISIT: Check if relevant for `cds add sample` in Java scenario
22
+ await Promise.all(appUIPaths.map(async p => {
23
+ const manifest = await read(join(appPath, p, 'webapp/manifest.json'))
24
+ manifest['sap.app'].id = appName + '.' + p
25
+ await write(join(appPath, p, 'webapp/manifest.json'), manifest, { spaces: 2 })
26
+ }))
27
+ } else if (language === 'java') {
28
+ await mvn.add('sample')
29
+ }
32
30
  }
33
31
  }
@@ -11,8 +11,7 @@ module.exports = class TinySampleTemplate extends require('../../plugin') {
11
11
  const { isJava } = readProject()
12
12
  if (isJava) {
13
13
  if (!cds.cli.options.add.has(JAVA)) {
14
- await mvn.execMvn(mvn.getAddSamplesCmdArgs())
15
- await mvn.execMvn(mvn.getAddTestsCmdArgs())
14
+ await mvn.add('tiny_sample')
16
15
  }
17
16
  } else {
18
17
  const { db, srv } = env4('production').folders