@sap/cds-dk 8.4.1 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +43 -2
  2. package/bin/add.js +2 -12
  3. package/bin/cds.js +5 -0
  4. package/bin/debug.js +90 -0
  5. package/bin/watch.js +12 -2
  6. package/lib/bind/index.js +1 -1
  7. package/lib/bind/userCdsrcJsonStorageProvider.js +0 -19
  8. package/lib/build/buildTaskEngine.js +1 -1
  9. package/lib/build/provider/hana/2migration.js +34 -20
  10. package/lib/build/provider/hana/index.js +74 -41
  11. package/lib/build/provider/internalBuildTaskProvider.js +1 -1
  12. package/lib/build/provider/mtx-extension/index.js +4 -4
  13. package/lib/build/provider/mtx-sidecar/index.js +2 -2
  14. package/lib/build/provider/nodejs/index.js +3 -19
  15. package/lib/build/util.js +41 -17
  16. package/lib/deploy/to-hana/hdiDeployUtil.js +14 -10
  17. package/lib/import/message.js +2 -1
  18. package/lib/import/odata/metadataconverter/MetadataConverter.js +5 -0
  19. package/lib/import/odata/v4parser.js +32 -43
  20. package/lib/index.js +14 -14
  21. package/lib/init/add.js +26 -0
  22. package/lib/init/constants.js +5 -6
  23. package/lib/init/index.js +17 -25
  24. package/lib/init/merge.js +1 -4
  25. package/lib/init/projectReader.js +75 -9
  26. package/lib/init/registries/mta.js +0 -1
  27. package/lib/init/template/approuter/files/package.json +1 -1
  28. package/lib/init/template/cloud-logging/files/package.json.hbs +1 -12
  29. package/lib/init/template/cloud-logging/files/values.yaml.hbs +16 -0
  30. package/lib/init/template/cloud-logging/index.js +14 -10
  31. package/lib/init/template/data/as-csv.js +1 -1
  32. package/lib/init/template/data/as-json.js +5 -5
  33. package/lib/init/template/data/index.js +1 -26
  34. package/lib/init/template/esm/files/package.json.hbs +3 -0
  35. package/lib/init/template/esm/index.js +16 -0
  36. package/lib/init/template/hana/files/package.json.hbs +18 -4
  37. package/lib/init/template/handler/files/handler.xs.hbs +56 -0
  38. package/lib/init/template/handler/index.js +84 -9
  39. package/lib/init/template/helm/index.js +4 -1
  40. package/lib/init/template/http/index.js +108 -39
  41. package/lib/init/template/sample/files/nodejs/{admin-service.js → admin-service.js.hbs} +8 -1
  42. package/lib/init/template/sample/files/nodejs/{cat-service.js → cat-service.js.hbs} +8 -3
  43. package/lib/init/template/sample/index.js +4 -2
  44. package/lib/init/template/side-by-side-extensibility/index.js +1 -0
  45. package/lib/init/template/telemetry/files/package.json.hbs +18 -0
  46. package/lib/init/template/telemetry/index.js +8 -2
  47. package/lib/init/template/typescript/files/package.json +1 -1
  48. package/lib/init/template/workzone-standard/index.js +1 -1
  49. package/lib/init/types.d.ts +0 -6
  50. package/lib/watch/node-watch.js +25 -9
  51. package/npm-shrinkwrap.json +28 -33
  52. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  const cds = require('../cds')
2
2
  const { join, resolve, basename } = require('path')
3
- const { fs, exists, isdir, path } = cds.utils
3
+ const { fs, exists, isfile, isdir, path } = cds.utils
4
4
  const { parseXml } = require('./xml')
5
5
  const { REGEX_JAVA_VERSION, JAVA_LTS_VERSIONS } = require('./constants')
6
6
  const TRACE = cds.debug('trace')
@@ -12,13 +12,15 @@ module.exports = new class ProjectReader {
12
12
  }
13
13
 
14
14
  #setPackageInfo() {
15
+ this.isJava = (exists('pom.xml') || cds.cli.options?.add?.has('java'))
16
+ && !cds.env.for('cds').profiles.includes('node') // opt-out for hybrid projects like sflight
17
+
15
18
  // 1. Use project name and static default values
16
- this.appVersion = cds.cli.options?.add?.has('java') ? '1.0.0-SNAPSHOT' : '1.0.0'
19
+ this.appVersion = this.isJava ? '1.0.0-SNAPSHOT' : '1.0.0'
17
20
  this.appName = this.appId = basename(cds.root)
18
21
  this.appDescription = 'A simple CAP project.'
19
22
 
20
- const isJava = exists('pom.xml') || cds.cli.options?.add?.has('java')
21
- if (isJava) {
23
+ if (this.isJava) {
22
24
  let artifactId, description, version, properties
23
25
  try {
24
26
  ({ artifactId, description, version, properties } = parseXml(resolve(cds.root, 'pom.xml')) ?? {})
@@ -74,6 +76,10 @@ module.exports = new class ProjectReader {
74
76
  * @property {boolean} isUI5 Indicates if the project is a UI5 project.
75
77
  * @property {boolean} isJava Indicates if the project is using Java.
76
78
  * @property {boolean} isNodejs Indicates if the project is using Node.js.
79
+ * @property {boolean} isESM Indicates if the project is an ESM project (type 'module')
80
+ * @property {boolean} hasEsmStyle Indicates if code supports ESM-style imports and exports. This is true for ESM and Typescript projects.
81
+ * @property {boolean} isTypescript Indicates if the project is a Typescript project.
82
+ * @property {boolean} hasTyper Indicates if code supports CDS typer.
77
83
  * @property {boolean} hasHelmUnifiedRuntime Indicates if the project uses Unified Runtime Helm deployment.
78
84
  * @property {boolean} hasHelm Indicates if the project uses CAP Helm deployment.
79
85
  * @property {boolean} hasMta Indicates if the project uses MTA deployment.
@@ -95,7 +101,7 @@ module.exports = new class ProjectReader {
95
101
  * @property {boolean} hasMTXRouteJava Indicates if the approuter should have a multitenant subscription route (java).
96
102
  * @property {boolean} hasMalwareScanner Indicates if the project uses the SAP Malware Scanning service.
97
103
  * @property {boolean} hasDynatrace Indicates if the project uses Dynatrace.
98
- *
104
+ * @property {boolean} hasCloudLogging Indicates if the project uses Cloud Logging.
99
105
  */
100
106
 
101
107
  /**
@@ -110,18 +116,27 @@ module.exports = new class ProjectReader {
110
116
  const { appVersion, appName, appId, appDescription, jdkVersion } = this
111
117
  TRACE?.({ env })
112
118
 
119
+ const pkgJson = () => exists('package.json') ? JSON.parse(fs.readFileSync(join(cds.root, 'package.json'))) : {}
120
+ const _isESM = () => pkgJson().type === 'module'
121
+ const _isTypescript = () => !!pkgJson().dependencies?.typescript || !!pkgJson().devDependencies?.typescript
122
+ const _hasTyper = () => !!pkgJson().dependencies?.['@cap-js/cds-typer'] || !!pkgJson().devDependencies?.['@cap-js/cds-typer']
123
+
113
124
  const _inProd = plugin => require(`./template/${plugin}`).hasInProduction(env)
114
- const _ui5 = () => exists('package.json') && !!JSON.parse(fs.readFileSync(path.resolve(cds.root, 'package.json'))).sapux
125
+ const _ui5 = () => pkgJson().sapux
115
126
  const _uiEmbedded = () => exists(join(env.folders.app, 'xs-app.json'))
116
127
  const _uiModule = () => !_uiEmbedded() && isdir(env.folders.app) && fs.readdirSync(resolve(cds.root, env.folders.app)).length > 0
117
- const _isJava = () => exists('pom.xml') || cds.cli.options?.add?.has('java')
128
+ const _isJava = () => this.isJava
118
129
  const isApp = name => name !== 'appconfig' && !name.startsWith('_') && name !== 'router' && name !== 'portal'
119
130
  const reserved = {
120
131
  has: x => _inProd(x),
121
132
  profile: () => cds.cli.options?.for,
122
- language: () => exists('pom.xml') ? 'java' : 'nodejs',
133
+ language: () => _isJava() ? 'java' : 'nodejs',
123
134
  isJava: () => _isJava(),
124
135
  isNodejs: () => !_isJava(),
136
+ isTypescript: () => _isTypescript(),
137
+ isESM: () => _isESM,
138
+ hasEsmStyle: () => _isTypescript() || _isESM(),
139
+ hasTyper: () => _hasTyper(),
125
140
  srvPath: () => join(env.build.target, env.folders.srv.replace(/\/$/, '')).replace(/\\/g, '/'),
126
141
  archiveName: () => {
127
142
  const pomXmlPath = resolve(cds.root, env.folders.srv, 'pom.xml')
@@ -135,7 +150,7 @@ module.exports = new class ProjectReader {
135
150
  const path = join(env.build.target, name).replace(/\\/g, '/')
136
151
  return { folder, name, path }
137
152
  },
138
- configFile: () => exists('pom.xml') ? '.cdsrc.json' : 'package.json',
153
+ configFile: () => _isJava() ? '.cdsrc.json' : 'package.json',
139
154
  apps: () => {
140
155
  const apps = fs.readdirSync(path.resolve(cds.root, cds.env.folders.app)).filter(e =>
141
156
  isdir(path.join(cds.root, cds.env.folders.app, e)) && isApp(e)
@@ -191,3 +206,54 @@ module.exports = new class ProjectReader {
191
206
  return project
192
207
  }
193
208
  }
209
+
210
+ /**
211
+ * Finds and reads SpringBoot configuration in `cds.root/srv` dir.
212
+ * This is done in a best-effort way. In no way can we replciate all of SpringBoot's
213
+ * config variants.
214
+ * @param {string[]?} roots array of directories to search for application.yaml, like `srv`
215
+ * @returns {Promise<object[]>} array of objects with `cds` property
216
+ */
217
+ module.exports._readSpringBootConfig = async function (roots=[cds.env.folders.srv]) {
218
+ const configNodes = []
219
+ const rootDirs = roots.map(d => join(cds.root, d))
220
+ for (let r of rootDirs) {
221
+ const file = isfile (join (r,'./src/main/resources/application.yaml'))
222
+ if (file) {
223
+ const yaml = cds.load.yaml(file)
224
+ for (let yamlDoc of Array.isArray(yaml) ? yaml : [yaml]) {
225
+ let cds = yamlDoc?.cds;
226
+ if (!cds) continue
227
+ cds = _normalizeSpringBootCfg(cds)
228
+ configNodes.push({cds})
229
+ }
230
+ }
231
+ }
232
+ return configNodes
233
+ }
234
+
235
+ /**
236
+ * Normalize SpringBoot's dots in keys to express nested objects
237
+ *
238
+ * @example: `cds.foo.bar = 1` --> `cds: { foo: { bar: 1 } }`
239
+ * @param {object} obj
240
+ * @returns {object}
241
+ */
242
+ function _normalizeSpringBootCfg(obj) {
243
+ if (typeof obj !== 'object') return obj
244
+ Object.keys(obj).forEach(k => {
245
+ const prop = k.split('.')
246
+ const last = prop.pop()
247
+ // and define the object if not already defined
248
+ const res = prop.reduce((o, key) => {
249
+ // define the object if not defined and return
250
+ return o[key] = o[key] ?? {}
251
+ }, obj)
252
+ res[last] = obj[k]
253
+ // recursively normalize
254
+ _normalizeSpringBootCfg(obj[k])
255
+ // delete the original property from object if it was rewritten
256
+ if (prop.length) delete obj[k]
257
+ })
258
+ return obj
259
+ }
@@ -1,4 +1,3 @@
1
- // Registry of sequence matchers in mta.yaml files
2
1
  const cds = require('../../cds')
3
2
 
4
3
  const srvNode4 = path => ({ in: 'modules', where: { type: 'nodejs', path } })
@@ -4,7 +4,7 @@
4
4
  "@sap/approuter": "^17.0.0"
5
5
  },
6
6
  "engines": {
7
- "node": "^22.0.0"
7
+ "node": "^20"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node node_modules/@sap/approuter/approuter.js"
@@ -1,18 +1,7 @@
1
1
  {
2
- "dependencies": {
3
- "@cap-js/telemetry": ">=0",
4
- "@grpc/grpc-js": "^1",
5
- "@opentelemetry/exporter-trace-otlp-grpc": "^0",
6
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0",
7
- "@opentelemetry/host-metrics": "^0"
8
- },
9
2
  "cds": {
10
3
  "requires": {
11
- "[{{profile}}]": {
12
- "telemetry": {
13
- "kind": "to-cloud-logging"
14
- }
15
- }
4
+ "cloud-logging": true
16
5
  }
17
6
  }
18
7
  }
@@ -0,0 +1,16 @@
1
+ ---
2
+ srv:
3
+ bindings:
4
+ cloud-logging:
5
+ serviceInstanceName: cloud-logging
6
+ credentialsRotationPolicy:
7
+ enabled: true
8
+ rotationFrequency: 168h
9
+ cloud-logging:
10
+ serviceOfferingName: cloud-logging
11
+ servicePlanName: standard
12
+ parameters:
13
+ ingest_otlp:
14
+ enabled: true
15
+
16
+
@@ -7,7 +7,7 @@ const { srv4, mtxSidecar4, approuter } = require('../../registries/mta')
7
7
  module.exports = class extends require('../../plugin') {
8
8
 
9
9
  static hasInProduction(env) {
10
- return env.requires?.telemetry?.kind === 'to-cloud-logging'
10
+ return env.requires?.['cloud-logging'];
11
11
  }
12
12
 
13
13
  options() {
@@ -24,23 +24,23 @@ module.exports = class extends require('../../plugin') {
24
24
  }
25
25
 
26
26
  async canRun() {
27
- const { hasMta, hasHelm, hasHelmUnifiedRuntime } = readProject()
28
- if ((hasHelmUnifiedRuntime || hasHelm) && !hasMta) throw `'cds add cloud-logging' is not available for Kyma yet`
29
27
  return true
30
28
  }
31
29
 
30
+ requires() {
31
+ const project = readProject(), { hasTelemetry } = project
32
+ if ((cds.cli.options['with-telemetry'] || hasTelemetry)) return ['telemetry']
33
+ }
34
+
32
35
  async run() {
33
- const project = readProject(), { hasTelemetry, isNodejs } = project
34
- if (isNodejs && (cds.cli.options['with-telemetry'] || hasTelemetry)) {
35
- project.profile ??= 'production'
36
- await merge(__dirname, 'files/package.json.hbs').into('package.json', { with: project })
37
- await sort('package.json', 'dependencies')
38
- }
36
+ const project = readProject();
37
+ await merge(__dirname, 'files/package.json.hbs').into('package.json', { with: project })
38
+ await sort('package.json', 'dependencies')
39
39
  }
40
40
 
41
41
  async combine() {
42
42
  const project = readProject()
43
- const { hasMta, hasApprouter, isJava, hasMultitenancy, srvPath } = project
43
+ const { hasMta, hasHelm, hasHelmUnifiedRuntime, hasApprouter, isJava, hasMultitenancy, srvPath } = project
44
44
 
45
45
  if (hasMta) {
46
46
  const cloudLogging = {
@@ -77,5 +77,9 @@ module.exports = class extends require('../../plugin') {
77
77
  with: project, additions, relationships
78
78
  })
79
79
  }
80
+ if (hasHelm || hasHelmUnifiedRuntime) {
81
+ await merge(__dirname, 'files/values.yaml.hbs').into('chart/values.yaml')
82
+ }
80
83
  }
81
84
  }
85
+
@@ -39,7 +39,7 @@ module.exports = async (data={}, csn, { headerOnly=false, separator=',' }={}) =>
39
39
  }
40
40
  record.push(...Object.values(value))
41
41
  }
42
- else if (element.is_struct) {
42
+ else if (element.is_struct && element.type !== 'cds.Map') {
43
43
  if (i === 0) {
44
44
  header.push(...Object.keys(value).map(structField => name + '_' + structField))
45
45
  }
@@ -80,7 +80,7 @@ function fillRecord(record, recordID, artifact, csn, data, parent, depth, refere
80
80
  Object.defineProperty(record, '$id', {value: recordID, enumerable: false})
81
81
  if (data[artifact.name]) data[artifact.name].push(record) // add id to data, to be referenced by associations to prevent circular dependencies
82
82
 
83
- const elems = Object.values(artifact.elements ?? artifact.params).sort(asJson.elementsSorter(csn))
83
+ const elems = Object.values(artifact.elements ?? artifact.params ?? {}).sort(asJson.elementsSorter(csn))
84
84
  elems.forEach(el => {
85
85
  record[el.name] = dataForField(el, artifact, csn, record, data, parent, depth, referenceData)
86
86
  })
@@ -229,9 +229,11 @@ function dataForField(element, artifact, csn, record, data, parent, depth, refer
229
229
  }
230
230
  case 'cds.hana.REAL':
231
231
  case 'cds.Decimal': {
232
- const range = {from: 0, to: 100}
232
+ const prec = element.precision ? Math.min(Math.pow(10, element.precision), Number.MAX_SAFE_INTEGER) : 100
233
+ const range = { from: 0, to: prec }
234
+ const scale = element.scale ?? (element.precision ? 0 : 2) // scale is 0 if only precision is given
233
235
  annotatedIntRange(element, range)
234
- return +(randomDecimalInRange(range).toFixed(element.scale ?? 2))
236
+ return +(randomDecimalInRange(range).toFixed(scale))
235
237
  }
236
238
  case 'cds.Double': {
237
239
  const range = {from: 0, to: 100}
@@ -309,8 +311,6 @@ function dataForField(element, artifact, csn, record, data, parent, depth, refer
309
311
  return
310
312
  case 'cds.Vector': // TODO add Vector
311
313
  return
312
- case 'cds.Map':
313
- return {}
314
314
  default: {
315
315
  console.error(`Unknown type ${element._type} (${element.type}) in '${artifact.name}:${element.name}'`)
316
316
  return
@@ -6,6 +6,7 @@ const { URLSearchParams } = require('url')
6
6
  const { env4 } = require('../../projectReader')
7
7
  const asJson = require('./as-json')
8
8
  const asCsv = require('./as-csv')
9
+ const { filterStringAsRegex } = require('../../add')
9
10
 
10
11
  module.exports = class DataTemplate extends require('../../plugin') {
11
12
 
@@ -92,32 +93,6 @@ module.exports = class DataTemplate extends require('../../plugin') {
92
93
  }
93
94
  module.exports.asJson = asJson
94
95
  module.exports.asCsv = asCsv
95
- module.exports.filterStringAsRegex = filterStringAsRegex
96
-
97
- /**
98
- * Turns a filter string with optional meta characters into a regular expression.
99
- * If no meta characters are found, the string is treated as an include pattern.
100
- *
101
- * @param {string} spec the filter string
102
- * @returns {RegExp} the regular expression
103
- */
104
- function filterStringAsRegex(spec) {
105
- if (typeof spec === 'string') {
106
- try {
107
- if (spec.match(/[\^$|*]/)) {
108
- spec = spec.replace(/^\*/, '.*') // * at start would be invalid, so make it a .*
109
- return new RegExp(spec, 'i')
110
- }
111
- else { // no meta chars -> include semantics
112
- spec = spec.replace(/\./g, '\\.') // escape literal dot
113
- return new RegExp(spec, 'i')
114
- }
115
- } catch (err) {
116
- throw err.message // user error, so cut off stack trace
117
- }
118
- }
119
- return /.*/
120
- }
121
96
 
122
97
  function parseQueryStr(queryStr) {
123
98
  const params = new URLSearchParams(queryStr) // e.g. key1&key2=false
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,16 @@
1
+ const { readProject } = require('../../projectReader')
2
+ const { merge } = require('../../merge')
3
+
4
+ module.exports = class EsmTemplate extends require('../../plugin') {
5
+
6
+ static help() {
7
+ return 'ESM-compatible Node.js project'
8
+ }
9
+
10
+ async run() {
11
+ const project = readProject()
12
+ const { configFile } = project
13
+ await merge(__dirname, 'files/package.json.hbs').into(configFile, { with: project, forceOverwrite: true })
14
+ }
15
+
16
+ }
@@ -3,10 +3,7 @@
3
3
  "dependencies": {
4
4
  "@cap-js/hana": "^1"
5
5
  },
6
- {{/isNodejs}}
7
- {{#isNodejs}}
8
6
  "cds": {
9
- {{/isNodejs}}
10
7
  "sql": {
11
8
  "native_hana_associations": false
12
9
  }{{#profile}},
@@ -22,7 +19,24 @@
22
19
  {{/shortcut}}
23
20
  }
24
21
  }{{/profile}}
25
- {{#isNodejs}}
26
22
  }
27
23
  {{/isNodejs}}
24
+ {{#isJava}}
25
+ "requires": {
26
+ {{#profile}}
27
+ "[{{profile}}]": {
28
+ {{/profile}}
29
+ {{#shortcut}}
30
+ "db": "hana"
31
+ {{/shortcut}}
32
+ {{^shortcut}}
33
+ "db": {
34
+ "kind": "hana"
35
+ }
36
+ {{/shortcut}}
37
+ {{#profile}}
38
+ }
39
+ {{/profile}}
40
+ }
41
+ {{/isJava}}
28
42
  }
@@ -0,0 +1,56 @@
1
+ {{! ESM }}
2
+ {{#hasEsmStyle}}
3
+ import cds from '@sap/cds'
4
+ {{#hasTyper}}
5
+ {{#allNames}}
6
+ import { {{allNames}} } from '#cds-models/{{{servicePath}}}'
7
+ {{/allNames}}
8
+ {{/hasTyper}}
9
+
10
+ {{! class declaration }}
11
+ export class {{serviceClass}} extends cds.ApplicationService { init() {
12
+ {{/hasEsmStyle}}
13
+ {{! CJS }}
14
+ {{^hasEsmStyle}}
15
+ const cds = require('@sap/cds')
16
+ {{#hasTyper}}
17
+ {{#allNames}}
18
+ const { {{allNames}} } = require('#cds-models/{{{servicePath}}}')
19
+ {{/allNames}}
20
+ {{/hasTyper}}
21
+
22
+ {{! class declaration }}
23
+ module.exports = class {{serviceClass}} extends cds.ApplicationService { init() {
24
+ {{/hasEsmStyle}}
25
+ {{! dynamic model imports }}
26
+ {{^hasTyper}}
27
+
28
+ {{#entityNames}}
29
+ const { {{entityNames}} } = cds.entities('{{serviceName}}')
30
+ {{/entityNames}}
31
+ {{/hasTyper}}
32
+
33
+ {{! entity handlers }}
34
+ {{#entities}}
35
+ this.before (['CREATE', 'UPDATE'], {{name}}, async (req) => {
36
+ console.log('Before CREATE/UPDATE {{name}}', req.data)
37
+ })
38
+ this.after ('READ', {{name}}, async ({{dataVar}}, req) => {
39
+ console.log('After READ {{name}}', {{dataVar}})
40
+ })
41
+ {{/entities}}
42
+
43
+ {{! action handlers }}
44
+ {{#operations}}
45
+ {{#hasTyper}}
46
+ this.on ({{name}}, async (req) => {
47
+ {{/hasTyper}}
48
+ {{^hasTyper}}
49
+ this.on ('{{name}}', async (req) => {
50
+ {{/hasTyper}}
51
+ console.log('On {{name}}', req.data)
52
+ })
53
+ {{/operations}}
54
+
55
+ return super.init()
56
+ }}
@@ -1,11 +1,15 @@
1
+ const cds = require('../../../cds')
2
+ const { fs: { promises: { rename } }, path: { basename, dirname, join }, exists } = cds.utils
1
3
  const { readProject } = require('../../projectReader')
4
+ const { renderAndCopy } = require('../../template')
2
5
  const mvn = require('../../mvn')
3
-
6
+ const { dim } = require('../../../util/term')
7
+ const { filterStringAsRegex } = require('../../add')
4
8
 
5
9
  module.exports = class Handler extends require('../../plugin') {
6
10
 
7
11
  static help() {
8
- return 'add handler stubs for actions and functions (Java)'
12
+ return 'handler stubs for service entities, actions and functions'
9
13
  }
10
14
 
11
15
  options() {
@@ -13,25 +17,96 @@ module.exports = class Handler extends require('../../plugin') {
13
17
  'filter': {
14
18
  type: 'string',
15
19
  short: 'f',
16
- help: `Filter actions or functions by name prefix. Regular expressions are not supported.`
20
+ help: `Filter for entities, actions or functions matching the given pattern.
21
+ For Node.js, if it contains meta characters like '^' or '*', it is treated as a regular expression,
22
+ otherwise as an include pattern, i.e /.*bookshop.*/i
23
+ For Java, only '*' and '**' as suffix wildcards are allowed, as in 'my.bookshop.*' or 'my.**'`
17
24
  },
18
25
  'out': {
19
26
  type: 'string',
20
27
  short: 'o',
21
- help: 'Specify a custom output directory inside the `handlers` directory. The default is `handlers`.'
28
+ help: `Custom output directory.
29
+ For Java, the default is 'handlers'. For Node.js, the default is 'srv'.`
22
30
  }
23
31
  }
24
32
  }
25
33
 
26
34
  canRun() {
27
- const { isJava } = readProject()
28
- if (!isJava) {
29
- throw `No Java project detected. 'cds add handler' is currently only supported for Java projects.\n`
30
- }
31
35
  return true
32
36
  }
33
37
 
34
38
  async run() {
35
- await mvn.generate('handler')
39
+ const { isJava } = readProject()
40
+ if (isJava) {
41
+ await mvn.generate('handler')
42
+ } else {
43
+ await createNodeHandler()
44
+ }
45
+ }
46
+ }
47
+
48
+ async function createNodeHandler() {
49
+ const { filter, force, out=cds.env.folders.srv } = cds.cli.options
50
+ const proj = readProject()
51
+ const csn = await loadModel()
52
+ if (!csn) return console.log(dim(`> Skipping, no model found`))
53
+
54
+ const nameFilter = filterStringAsRegex(filter)
55
+
56
+ for (const service of csn.services) {
57
+ if (!service.$location.file) continue
58
+
59
+ const destFileType = proj.isTypescript ? '.ts' : '.js'
60
+ const outPath = join(out, basename(service.$location.file.replace('.cds', destFileType)))
61
+ const destFile = join(cds.root, outPath)
62
+ if (!force && exists(destFile)) {
63
+ console.log(dim(`> Skipping ${outPath}`))
64
+ continue
65
+ }
66
+
67
+ proj.serviceName = service.name
68
+ proj.serviceClass = service.name.split('.').pop()
69
+ proj.servicePath = proj.serviceName ? (proj.serviceName.replace(/\./g, '/')) : ''
70
+
71
+ proj.entities = Object.entries(service.entities)
72
+ .filter(([name]) => name.match(nameFilter))
73
+ .filter(([name]) => !name.match(/[._]texts$/))
74
+ .filter(([, e]) => !e['@cds.autoexposed']) // only consider non-autoexposed entities to reduce clutter
75
+ .map(([name]) => ({
76
+ name,
77
+ dataVar: name.charAt(0).toLowerCase() + name.slice(1) // lower case first letter
78
+ }))
79
+ if (proj.entities.length)
80
+ proj.entityNames = proj.entities.map(e => e.name).join(', ')
81
+
82
+ proj.operations = Object.keys(service.actions)
83
+ .filter(name => name.match(nameFilter))
84
+ .map(name => ({
85
+ name,
86
+ dataVar: name.charAt(0).toLowerCase() + name.slice(1) // lower case first letter
87
+ }))
88
+
89
+ const all = [...proj.entities, ...proj.operations]
90
+ if (all.length > 4)
91
+ proj.allNames = `\n ${all.map(e => e.name).join(',\n ')}\n`
92
+ else if (all.length)
93
+ proj.allNames = all.map(e => e.name).join(', ')
94
+
95
+ console.log(dim(`> Writing ${outPath}`))
96
+ const destPath = dirname(destFile)
97
+ await renderAndCopy(join(__dirname, 'files'), destPath, proj)
98
+ await rename(join(destPath, 'handler.xs'), destFile)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @returns { Promise<import('@cap-js/cds-types').linked.LinkedCSN | null> }
104
+ */
105
+ async function loadModel() {
106
+ try {
107
+ return cds.linked(cds.minify(await cds.load([cds.env.roots])))
108
+ } catch (err) {
109
+ if (err.code === 'MODEL_NOT_FOUND') return null
110
+ throw err
36
111
  }
37
112
  }
@@ -95,7 +95,7 @@ module.exports = class HelmTemplate extends require('../../plugin') {
95
95
  }
96
96
 
97
97
  async combine() {
98
- const { hasDestination, hasEnterpriseMessaging, hasEnterpriseMessagingShared, hasHana, hasMultitenancy, hasXsuaa, hasHtml5Repo, hasApprouter, hasKafka, hasAttachments, hasPortal, hasMalwareScanner, hasDynatrace } = readProject()
98
+ const { hasDestination, hasEnterpriseMessaging, hasEnterpriseMessagingShared, hasHana, hasMultitenancy, hasXsuaa, hasHtml5Repo, hasApprouter, hasKafka, hasAttachments, hasPortal, hasMalwareScanner, hasDynatrace, hasCloudLogging } = readProject()
99
99
 
100
100
  if (hasApprouter) {
101
101
  await this._mergeDependency('web-application', 'approuter')
@@ -142,6 +142,9 @@ module.exports = class HelmTemplate extends require('../../plugin') {
142
142
  if (hasMalwareScanner) {
143
143
  await this._mergeDependency('service-instance', 'malware-scanner')
144
144
  }
145
+ if (hasCloudLogging) {
146
+ await this._mergeDependency('service-instance', 'cloud-logging')
147
+ }
145
148
  if (hasDynatrace) {
146
149
  await this._mergeDependency('service-instance', 'dynatrace')
147
150
  }