@sap/cds-dk 8.8.2 → 8.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 (64) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/bin/deploy/index.js +7 -3
  3. package/bin/init.js +5 -5
  4. package/bin/repl.js +4 -0
  5. package/bin/test.js +6 -4
  6. package/bin/up.js +77 -0
  7. package/bin/watch.js +0 -43
  8. package/lib/bind/shared.js +1 -1
  9. package/lib/build/buildTaskEngine.js +12 -9
  10. package/lib/build/buildTaskFactory.js +0 -45
  11. package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +1 -1
  12. package/lib/build/plugins/helm/files/subcharts/content-deployment/templates/job.yaml +8 -0
  13. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.schema.json +87 -0
  14. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.yaml +29 -1
  15. package/lib/build/plugins/helm/files/subcharts/service-instance/Chart.yaml +1 -1
  16. package/lib/build/plugins/helm/files/subcharts/service-instance/templates/serviceinstance.yaml +1 -0
  17. package/lib/build/plugins/helm/files/subcharts/service-instance/values.yaml +1 -1
  18. package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -1
  19. package/lib/build/plugins/helm/files/subcharts/web-application/templates/deployment.yaml +8 -0
  20. package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +87 -0
  21. package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +29 -1
  22. package/lib/build/provider/internalBuildTaskProvider.js +1 -1
  23. package/lib/build/provider/mtx-extension/index.js +5 -0
  24. package/lib/deploy/to-hana/hana.js +95 -43
  25. package/lib/deploy/to-hana/mtaUtil.js +1 -2
  26. package/lib/env/schema.js +12 -88
  27. package/lib/import/common.js +23 -15
  28. package/lib/import/odata.package.json +7 -0
  29. package/lib/init/constants.js +2 -1
  30. package/lib/init/index.js +13 -16
  31. package/lib/init/mvn.js +1 -1
  32. package/lib/init/plugin.js +1 -1
  33. package/lib/init/projectReader.js +5 -1
  34. package/lib/init/template/approuter/files/package.json +1 -4
  35. package/lib/init/template/attachments/files/package.json.hbs +1 -1
  36. package/lib/init/template/cloud-logging/files/java-cdsrc.json +5 -0
  37. package/lib/init/template/cloud-logging/index.js +9 -5
  38. package/lib/init/template/containerize/files/containerize.yaml.hbs +1 -1
  39. package/lib/init/template/data/index.js +3 -3
  40. package/lib/init/template/extension/files/.base/index.cds +15 -0
  41. package/lib/init/template/extension/files/app/extension.cds.hbs +47 -0
  42. package/lib/init/template/extension/files/test/data/Authors.csv +4 -0
  43. package/lib/init/template/extension/files/test/data/my.bookshop-Books.csv +8 -0
  44. package/lib/init/template/extension/index.js +99 -0
  45. package/lib/init/template/handler/files/extension/handler.xs.hbs +10 -0
  46. package/lib/init/template/handler/files/extension/package.json +10 -0
  47. package/lib/init/template/handler/index.js +83 -20
  48. package/lib/init/template/http/index.js +13 -10
  49. package/lib/init/template/java/index.js +3 -3
  50. package/lib/init/template/mta/index.js +1 -1
  51. package/lib/init/template/nodejs/files/package.json.hbs +1 -1
  52. package/lib/init/template/nodejs/index.js +3 -3
  53. package/lib/init/template/pipeline/index.js +1 -1
  54. package/lib/init/template/sample/index.js +6 -0
  55. package/lib/init/template/typer/files/package.json +0 -3
  56. package/lib/init/template/typer/index.js +2 -2
  57. package/lib/init/template/typescript/files/package.json +1 -1
  58. package/lib/init/validate.js +5 -7
  59. package/lib/lint/Linter.js +19 -0
  60. package/lib/mtx/pull.js +9 -1
  61. package/lib/util/question.js +24 -21
  62. package/npm-shrinkwrap.json +34 -24
  63. package/package.json +2 -2
  64. /package/lib/init/template/handler/files/{handler.xs.hbs → standard/handler.xs.hbs} +0 -0
package/lib/init/index.js CHANGED
@@ -60,7 +60,7 @@ module.exports = class CDSGenerator {
60
60
  * @param {any} options, additional options
61
61
  */
62
62
  async add(facets) {
63
- if (!facets?.length) throw 'You must specify a facet to add to the project.'
63
+ if (!facets?.length) throw 'you must specify a facet to add to the project'
64
64
 
65
65
  this._initialize(null);
66
66
  this._greetings();
@@ -110,7 +110,7 @@ module.exports = class CDSGenerator {
110
110
  _initialize(projectName) {
111
111
  // avoid config files to be created in the home dir
112
112
  if (cds.cli.command === COMMAND_INIT && !projectName && process.cwd() === require('os').homedir()) {
113
- throw `In your home directory, use 'cds init <project>'`
113
+ throw `in your home directory, use 'cds init <project>'`
114
114
  }
115
115
  const { options } = cds.cli
116
116
  this.cwd = options.cwd || process.cwd();
@@ -123,19 +123,19 @@ module.exports = class CDSGenerator {
123
123
  _greetings() {
124
124
  if (cds.cli.command === COMMAND_INIT) {
125
125
  const relativeProjectPath = path.relative(this.cwd, cds.root)
126
- const folderName = relativeProjectPath ? `.${path.sep}${relativeProjectPath}` : 'current folder'
127
- console.log(`Creating new CAP project in ${term.bold(folderName)}\n`)
126
+ const folderName = relativeProjectPath ? `.${path.sep}${relativeProjectPath}` : 'the current folder'
127
+ console.log(`creating new CAP project in ${term.bold(folderName)}\n`)
128
128
  }
129
129
  if (cds.cli.options.for && typeof cds.cli.options.for !== 'string') {
130
- throw 'The --for argument must not be empty.'
130
+ throw 'the --for argument must not be empty'
131
131
  }
132
132
  if (cds.cli.options.force && !cds.cli.options.dry) {
133
- console.log(`Using '--force' ... existing files will be overwritten`)
133
+ console.log(`using '--force' ... existing files will be overwritten`)
134
134
  }
135
135
  }
136
136
 
137
137
  async _process(facets) {
138
- DEBUG?.(`Project path: ${cds.root}`);
138
+ DEBUG?.(`project path: ${cds.root}`);
139
139
 
140
140
  const { options, command } = cds.cli
141
141
  if (command === COMMAND_INIT) {
@@ -149,7 +149,7 @@ module.exports = class CDSGenerator {
149
149
  const byPriority = (a, b) => cmds.indexOf(a) - cmds.indexOf(b)
150
150
  const sorted = Array.from(plugins).sort(byPriority)
151
151
  for (const [i, facet] of Object.entries(sorted)) {
152
- if (!cds.cli.options.dry) console.log(`Adding feature '${facet}'...`);
152
+ if (!cds.cli.options.dry) console.log(`adding ${term.bold(facet)}`)
153
153
  const template = this.templateList[i]
154
154
  await template.run();
155
155
  await template.combine();
@@ -157,19 +157,19 @@ module.exports = class CDSGenerator {
157
157
  }
158
158
 
159
159
  if (cds.env['project-nature'] === 'java') {
160
- console.log('Updating frozen dependencies in package-lock.json...')
160
+ console.log('updating frozen dependencies in package-lock.json…')
161
161
  await cmd.spawnCommand('npm' , ['i', '--package-lock-only'], { cwd:cds.root })
162
162
  }
163
163
 
164
164
  if (cds.cli.command === COMMAND_INIT) {
165
165
  const relativeProjectPath = path.relative(this.cwd, cds.root)
166
- let message = 'Successfully created project.'
166
+ let message = 'successfully created project'
167
167
  if (relativeProjectPath) {
168
- message = message + ` Continue with '${term.bold('cd ' + relativeProjectPath)}'.`
168
+ message += ` – continue with ${term.bold('cd ' + relativeProjectPath)}`
169
169
  }
170
170
  console.log('\n' + message)
171
171
  } else if (!cds.cli.options.dry) {
172
- console.log(`\nSuccessfully added features to your project.`)
172
+ console.log(`\nsuccessfully added features to your project`)
173
173
  }
174
174
  }
175
175
 
@@ -188,7 +188,7 @@ module.exports = class CDSGenerator {
188
188
  return `${term.bold(name) + ' '.repeat(max - name.length)} ${term.dim(help)}`
189
189
  }).join('\n')
190
190
  DEBUG?.(err)
191
- throw `Unknown facet '${term.bold(plugin)}'. Did you mean ${term.bold(`cds add ${bestMatch}`)}?\n\nHaven't found the proper facet yet? Here are all supported facets:\n\n${allFacetsText}\n`
191
+ throw `unknown facet ${term.bold(plugin)} – did you mean ${term.bold(`cds add ${bestMatch}`)}?\n\nall supported facets:\n\n${allFacetsText}\n`
192
192
  }
193
193
  throw err
194
194
  }
@@ -210,7 +210,6 @@ module.exports = class CDSGenerator {
210
210
  if (!templates.has(plugin)) {
211
211
  const template = await this._createTemplate(plugin)
212
212
  if (await template.canRun()) {
213
- DEBUG?.(`Adding feature '${plugin}'`)
214
213
  templates.set(plugin, template)
215
214
  const dependencies = await template.requires()
216
215
  dependencies?.forEach(d => plugins.add(d))
@@ -229,8 +228,6 @@ module.exports = class CDSGenerator {
229
228
  return a - b
230
229
  })
231
230
  .map(key => templates.get(key))
232
-
233
- DEBUG?.(`Features: ${this.templateList.map(({ name }) => name).join(', ')}`);
234
231
  }
235
232
 
236
233
  async stepEnd() { // Also used by CAP Generator
package/lib/init/mvn.js CHANGED
@@ -111,7 +111,7 @@ module.exports.generate = async function (feature, options = cds.cli.options) {
111
111
  const cwd = options.cwd ?? cds.root
112
112
 
113
113
  try {
114
- console.log(`Executing command: `, term.highlight('mvn ' + command.join(' ')))
114
+ console.log(`executing: `, term.highlight('mvn ' + command.join(' ')))
115
115
  await cmd.spawnCommand('mvn', command, { cwd: cwd })
116
116
  return command
117
117
  } catch (err) {
@@ -69,7 +69,7 @@ module.exports = class Plugin {
69
69
  async combineSupported() {
70
70
  const DEBUG = cds.log('cli|add')._debug
71
71
  DEBUG && console.log()
72
- DEBUG && console.log('Plugins active for production:')
72
+ DEBUG && console.log('plugins active for production:')
73
73
  const fromDk = await readdir(join(__dirname, './template'))
74
74
  const { registered } = require('./add')
75
75
  const { env4 } = require('./projectReader')
@@ -102,6 +102,7 @@ module.exports = new class ProjectReader {
102
102
  * @property {boolean} hasMalwareScanner Indicates if the project uses the SAP Malware Scanning service.
103
103
  * @property {boolean} hasDynatrace Indicates if the project uses Dynatrace.
104
104
  * @property {boolean} hasCloudLogging Indicates if the project uses Cloud Logging.
105
+ * @property {String} extendsApp Indicates which project is extended if any.
105
106
  */
106
107
 
107
108
  /**
@@ -125,8 +126,9 @@ module.exports = new class ProjectReader {
125
126
  const _uiEmbedded = () => exists(join(env.folders.app, 'xs-app.json'))
126
127
  const _uiModule = () => !_uiEmbedded() && isdir(env.folders.app) && fs.readdirSync(resolve(cds.root, env.folders.app)).length > 0
127
128
  const _isJava = () => this.isJava
129
+ const _extends = () => pkgJson().extends
128
130
  const isApp = name => {
129
- return name !== 'appconfig' && !name.startsWith('_') && name !== 'router' && name !== 'portal' && name !== 'workzone'
131
+ return name !== 'appconfig' && !name.match(/^[._]/) && name !== 'router' && name !== 'portal' && name !== 'workzone'
130
132
  }
131
133
  const reserved = {
132
134
  dependencies: () => pkgJson().dependencies,
@@ -192,6 +194,8 @@ module.exports = new class ProjectReader {
192
194
  hasUI5: _ui5,
193
195
  hasMTXRoute: () => _inProd('extensibility') || (_inProd('multitenancy') && !_isJava()),
194
196
  hasMTXRouteJava: () => (_inProd('helm-unified-runtime') || _inProd('helm')) && _isJava() && _inProd('multitenancy'),
197
+ imageRegistry: () => exists('chart/values.yaml') ? cds.load.yaml(join(cds.root, 'chart/values.yaml')).global?.image?.registry ?? '<your-container-registry>' : '<your-container-registry>',
198
+ extendsApp: _extends,
195
199
  }
196
200
 
197
201
  // Automatically creates availability checks for `cds add` commands.
@@ -1,10 +1,7 @@
1
1
  {
2
2
  "name": "approuter",
3
3
  "dependencies": {
4
- "@sap/approuter": "^19.0.0"
5
- },
6
- "engines": {
7
- "node": "^20"
4
+ "@sap/approuter": "^20.0.0"
8
5
  },
9
6
  "scripts": {
10
7
  "start": "node node_modules/@sap/approuter/approuter.js"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "dependencies": {
3
- "@cap-js/attachments": "^1"
3
+ "@cap-js/attachments": "^2"
4
4
  }
5
5
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "requires": {
3
+ "telemetry": "to-cloud-logging"
4
+ }
5
+ }
@@ -24,11 +24,15 @@ module.exports = class extends require('../../plugin') {
24
24
  }
25
25
 
26
26
  async run() {
27
- const project = readProject(), { hasTelemetry, isNodejs } = project
28
- if (isNodejs && (cds.cli.options['with-telemetry'] || hasTelemetry)) {
29
- project.profile ??= 'production'
30
- await merge(__dirname, 'files/package.json.hbs').into('package.json', { with: project })
31
- await sort('package.json', 'dependencies')
27
+ const project = readProject(), { hasTelemetry, isNodejs, isJava } = project
28
+ if (cds.cli.options['with-telemetry'] || hasTelemetry) {
29
+ if (isNodejs) {
30
+ project.profile ??= 'production'
31
+ await merge(__dirname, 'files/package.json.hbs').into('package.json', { with: project })
32
+ await sort('package.json', 'dependencies')
33
+ } else if (isJava) {
34
+ await merge(__dirname, 'files/java-cdsrc.json').into('.cdsrc.json')
35
+ }
32
36
  }
33
37
  }
34
38
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  _schema-version: '1.0'
3
- repository: <your-container-registry>
3
+ repository: {{& imageRegistry}}
4
4
  tag: latest
5
5
  before-all:
6
6
  - npx cds build --production
@@ -140,7 +140,7 @@ function writeFileFor (entity, data, csn, dest, force) {
140
140
 
141
141
  // continue only if file not already exists, or '--force' option provided
142
142
  if (fileExists && !force) {
143
- console.log(` ${dim('Skipping ' + relFilePath)}`)
143
+ console.log(` ${dim('skipping ' + relFilePath)}`)
144
144
  return
145
145
  }
146
146
 
@@ -149,8 +149,8 @@ function writeFileFor (entity, data, csn, dest, force) {
149
149
  if (data.length) {
150
150
  if (!exists(dest)) fs.mkdirSync(dest, {recursive: true})
151
151
  fs.writeFileSync(dataFilePath, data)
152
- if (fileExists) console.log(` ${dim('Overwriting ' + relFilePath)}`)
153
- else console.log(` ${dim('Creating ' + relFilePath)}`)
152
+ if (fileExists) console.log(` ${dim('overwriting ' + relFilePath)}`)
153
+ else console.log(` ${dim('creating ' + relFilePath)}`)
154
154
  }
155
155
  }
156
156
 
@@ -0,0 +1,15 @@
1
+ entity my.bookshop.Books {
2
+ key ID : Integer;
3
+ title : String @title : 'Title';
4
+ stock : Integer @title : 'Stock';
5
+ }
6
+
7
+ service CatalogService {
8
+ entity Books as projection on my.bookshop.Books;
9
+ }
10
+
11
+ annotate CatalogService.Books with @UI.LineItem: [
12
+ { Value: ID },
13
+ { Value: title },
14
+ { Value: stock },
15
+ ];
@@ -0,0 +1,47 @@
1
+ using { my.bookshop, CatalogService } from '{{{basePackage}}}';
2
+
3
+ namespace {{{extensionNamespace}}};
4
+
5
+ // new entity
6
+ entity x_Authors {
7
+ key ID : Integer;
8
+ name : String @title : 'Name';
9
+ books : Association to many bookshop.Books on books.x_author = $self;
10
+ }
11
+
12
+ // field extension incl. association
13
+ extend bookshop.Books with {
14
+ x_isbn : String @title : 'ISBN';
15
+ x_author : Association to x_Authors @title : 'Author';
16
+ }
17
+
18
+ // service extension
19
+ extend service CatalogService with {
20
+ @readonly entity Authors as projection on x_Authors;
21
+ }
22
+
23
+ // -------------------------------------------
24
+ // Fiori Annotations
25
+
26
+ annotate CatalogService.Books with @(
27
+ Common.SemanticKey: [x_isbn],
28
+ UI: {
29
+ LineItem: [
30
+ { Value: x_isbn },
31
+ ... up to { Value: title },
32
+ { Value: x_author_ID },
33
+ ...
34
+ ]
35
+ }
36
+ ) {
37
+ x_author @Common: { Text: x_author.name, TextArrangement: #TextOnly };
38
+ };
39
+
40
+ annotate CatalogService.Authors with @(
41
+ UI: {
42
+ LineItem: [
43
+ { Value: ID },
44
+ { Value: name },
45
+ ]
46
+ }
47
+ );
@@ -0,0 +1,4 @@
1
+ ID,name
2
+ 201,J.K. Rowling
3
+ 202,William Shakespeare
4
+ 203,Agatha Christie
@@ -0,0 +1,8 @@
1
+ ID,title,stock,x_isbn,x_author_ID
2
+ 101,Harry Potter and the Philosopher's Stone,7,9780747532743,201
3
+ 102,Harry Potter and the Chamber of Secrets,5,9780747538493,201
4
+ 103,Harry Potter and the Prisoner of Azkaban,12,9780747546290,201
5
+ 104,Harry Potter and the Goblet of Fire,3,9780747550990,201
6
+ 105,Harry Potter and the Order of the Phoenix,2,9780747551002,201
7
+ 106,Harry Potter and the Half-Blood Prince,1,9780747581085,201
8
+ 107,Harry Potter and the Deathly Hallows,0,9780747591053,201
@@ -0,0 +1,99 @@
1
+ const cds = require('../../../cds')
2
+ const { read, write, exists, copy, path:{join, relative}, readdir, rimraf } = cds.utils
3
+ const { mkdir, symlink } = require('fs/promises')
4
+ const { renderAndCopy } = require('../../template')
5
+ const projectReader = require('../../projectReader')
6
+ const npmPackageRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i
7
+
8
+ module.exports = class ExtensionProjectTemplate extends require('../../plugin') {
9
+
10
+ static help() {
11
+ return 'Extension project'
12
+ }
13
+
14
+ options() {
15
+ return {
16
+ 'base': {
17
+ type: 'string',
18
+ short: 'p',
19
+ help: `The project to extend.`
20
+ }
21
+ }
22
+ }
23
+
24
+
25
+ async run() {
26
+ let { base, force } = cds.cli.options
27
+ if (typeof base === 'string' && !base.match(npmPackageRegex)) throw `Illegal package name: ${base}`
28
+ if (typeof base !== 'string' || !base?.length) base = '@capire/bookshop'
29
+
30
+ const pkgJson = exists('package.json') ? await read('package.json') : { name: 'my-extension-project' }
31
+ if (!pkgJson.extends || force) {
32
+ pkgJson.extends = base
33
+ }
34
+
35
+ if (!pkgJson.workspaces?.includes('.base')) {
36
+ pkgJson.workspaces ??= []
37
+ pkgJson.workspaces.push('.base')
38
+ }
39
+ await write('package.json', pkgJson, { spaces: 2 })
40
+
41
+ if (!exists('.base/package.json') || force) {
42
+ await write('.base/package.json', {name: base})
43
+ }
44
+
45
+ await nodeModulesLink(base)
46
+ // remove empty and irrelevant folders
47
+ await Promise.all([ rmIfEmpty('db') ])
48
+ }
49
+
50
+ async addSample() {
51
+ const proj = projectReader.readProject()
52
+ const base = proj.extendsApp
53
+ if (!base) throw 'No project to extend found in package.json. Create a new extension project with "cds init extension"'
54
+
55
+ const projectSimple = base.replace(/^.*?\//, '')
56
+ const extensionNamespace = `${namespacify(proj.appName)}.${namespacify(projectSimple)}`
57
+ const { force } = cds.cli.options
58
+
59
+ if (!exists('.base/index.cds') || force) {
60
+ await copy(join(__dirname, 'files/.base/index.cds'), '.base/index.cds')
61
+ }
62
+
63
+ if (!exists('app/extension.cds') || force) {
64
+ await renderAndCopy(join(__dirname, 'files/app'), 'app', {
65
+ basePackage: base,
66
+ extensionNamespace
67
+ })
68
+ }
69
+ if (!exists('test/data') || force) {
70
+ await copy(join(__dirname, 'files/test/data/my.bookshop-Books.csv'), 'test/data/my.bookshop-Books.csv')
71
+ await copy(join(__dirname, 'files/test/data/Authors.csv'), `test/data/${extensionNamespace}-x_Authors.csv`)
72
+ }
73
+
74
+ }
75
+
76
+ // async finalize() {
77
+ // console.log(`For information on project "Piper" and its pipelines see ${URLS.PIPELINE_HELP}`);
78
+ // }
79
+ }
80
+
81
+ function namespacify(s) {
82
+ return s.replace(/[/.-]/g, '_').replace(/[@]/g, '').toLowerCase()
83
+ }
84
+ async function rmIfEmpty(s) {
85
+ exists(s) && !(await readdir(s)).length && await rimraf(s)
86
+ }
87
+
88
+ // Allow subsequent templates like `cds add handler` to resolve the model w/o `npm i`.
89
+ // So create a symlink from node_modules to .base, anticipating what `npm i` would do.
90
+ async function nodeModulesLink(base) {
91
+ const link = join(cds.root, 'node_modules', base)
92
+ if (exists(link)) return
93
+ const scope = base.indexOf('/') > 0 && base.split('/')[0] // @scope/pkg
94
+ const target = join(cds.root, 'node_modules', scope||'')
95
+ await mkdir(target, { recursive: true })
96
+ const relTarget = relative(target, join(cds.root, '.base'))
97
+ // console.log(`> linking ${relTarget} to ${link}`)
98
+ await symlink(relTarget, link, 'junction')
99
+ }
@@ -0,0 +1,10 @@
1
+ {{#hasTyper}}
2
+ const { {{entityNames}} } = require('#cds-models/{{{servicePath}}}')
3
+ {{/hasTyper}}
4
+ {{^hasTyper}}
5
+ const { {{entityNames}} } = this.entities
6
+ {{/hasTyper}}
7
+
8
+ module.exports = async function(req) {
9
+ {{#entityName}}console.log('{{when}} {{event}} {{entityName}}'){{/entityName}}{{^entityName}}console.log('{{when}} {{event}}'){{/entityName}}
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "devDependencies": {
3
+ "@sap/cds-oyster": "git+https://github.tools.sap/cap/cds-oyster"
4
+ },
5
+ "cds": {
6
+ "requires": {
7
+ "code-extensibility": true
8
+ }
9
+ }
10
+ }
@@ -1,5 +1,5 @@
1
1
  const cds = require('../../../cds')
2
- const { fs: { promises: { rename } }, path: { basename, dirname, join }, exists } = cds.utils
2
+ const { fs: { promises: { rename } }, read, write, path: { basename, dirname, join }, exists } = cds.utils
3
3
  const { readProject } = require('../../projectReader')
4
4
  const { renderAndCopy } = require('../../template')
5
5
  const mvn = require('../../mvn')
@@ -36,34 +36,28 @@ module.exports = class Handler extends require('../../plugin') {
36
36
  }
37
37
 
38
38
  async run() {
39
- const { isJava } = readProject()
40
- if (isJava) {
39
+ const proj = readProject()
40
+ if (proj.isJava) {
41
41
  await mvn.generate('handler')
42
42
  } else {
43
- await createNodeHandler()
43
+ await createNodeHandler(proj)
44
44
  }
45
45
  }
46
46
  }
47
47
 
48
- async function createNodeHandler() {
48
+ /**
49
+ * @param {ReturnType<readProject} proj
50
+ */
51
+ async function createNodeHandler(proj) {
49
52
  const { filter, force, out=cds.env.folders.srv } = cds.cli.options
50
- const proj = readProject()
51
53
  const csn = await loadModel()
52
- if (!csn) return console.log(dim(`> Skipping, no model found`))
54
+ if (!csn) return console.log(dim(`> skipping, no model found`))
53
55
 
54
56
  const nameFilter = filterStringAsRegex(filter)
55
57
 
56
58
  for (const service of csn.services) {
57
59
  if (!service.$location.file) continue
58
60
 
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
61
  proj.serviceName = service.name
68
62
  proj.serviceClass = service.name.split('.').pop()
69
63
  proj.servicePath = proj.serviceName ? (proj.serviceName.replace(/\./g, '/')) : ''
@@ -92,11 +86,67 @@ async function createNodeHandler() {
92
86
  else if (all.length)
93
87
  proj.allNames = all.map(e => e.name).join(', ')
94
88
 
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)
89
+ if (proj.extendsApp) {
90
+ await createExtCodeHandler(service, proj, out, force)
91
+ } else {
92
+ const destFileType = proj.isTypescript ? '.ts' : '.js'
93
+ const outPath = join(out, basename(service.$location.file.replace('.cds', destFileType)))
94
+ const destFile = join(cds.root, outPath)
95
+ if (!force && exists(destFile)) {
96
+ console.log(dim(`> skipping ${outPath}`))
97
+ continue
98
+ }
99
+ console.log(dim(`> writing ${outPath}`))
100
+ const destPath = dirname(destFile)
101
+ await renderAndCopy(join(__dirname, 'files/standard'), destPath, proj)
102
+ await rename(join(destPath, 'handler.xs'), destFile)
103
+ }
104
+ }
105
+ }
106
+
107
+ async function createExtCodeHandler(service, proj, out, force) {
108
+ const destFileType = '.js' // only JS for now, a TS toolchain in an ext. project would be too complex
109
+ const srvOutPath = join(cds.root, out, service.name)
110
+
111
+ // entities
112
+ const events = [['after', 'READ'], ['before', 'CREATE'], ['before', 'UPDATE']]
113
+ for (const ent of proj.entities) {
114
+ proj.entityName = ent.name
115
+ const entityOutPath = join(srvOutPath, ent.name)
116
+ for (const [when, event] of events) {
117
+ proj.when = when
118
+ proj.event = event
119
+ const outPath = join(entityOutPath, `${when}-${event}${destFileType}`)
120
+ if (!force && exists(outPath)) {
121
+ console.log(dim(`> skipping ${outPath}`))
122
+ continue
123
+ }
124
+ console.log(dim(`> writing ${outPath}`))
125
+ await renderAndCopy(join(__dirname, 'files/extension'), entityOutPath, proj)
126
+ await rename(join(entityOutPath, 'handler.xs'), outPath)
127
+ }
128
+ }
129
+
130
+ // unbound actions
131
+ proj.when = 'on'
132
+ proj.entityName = ''
133
+ for (const op of proj.operations) {
134
+ proj.event = op.name
135
+ const outPath = join(srvOutPath, `on-${op.name}${destFileType}`)
136
+ if (!force && exists(outPath)) {
137
+ console.log(dim(`> skipping ${outPath}`))
138
+ continue
139
+ }
140
+ console.log(dim(`> writing ${outPath}`))
141
+ await renderAndCopy(join(__dirname, 'files/extension'), dirname(outPath), proj)
142
+ await rename(join(dirname(outPath), 'handler.xs'), outPath)
99
143
  }
144
+
145
+ // package.json
146
+ const pkgJson = await read('package.json')
147
+ const pkgJsonDelta = require('./files/extension/package.json')
148
+ deepMerge(pkgJson, pkgJsonDelta)
149
+ await write('package.json', pkgJson, { spaces: 2 })
100
150
  }
101
151
 
102
152
  /**
@@ -107,6 +157,19 @@ async function loadModel() {
107
157
  return cds.linked(cds.minify(await cds.load([cds.env.roots])))
108
158
  } catch (err) {
109
159
  if (err.code === 'MODEL_NOT_FOUND') return null
110
- throw err
160
+ throw new Error(`Error compiling CDS files. Run 'npm install' and try again.`, {cause:err})
161
+ }
162
+ }
163
+
164
+ function deepMerge(obj1, obj2) {
165
+ for (const key in obj2) {
166
+ if (Object.prototype.hasOwnProperty.call(obj2, key)) {
167
+ if (obj2[key] instanceof Object && obj1[key] instanceof Object) {
168
+ obj1[key] = deepMerge(obj1[key], obj2[key])
169
+ } else {
170
+ obj1[key] = obj2[key]
171
+ }
172
+ }
111
173
  }
174
+ return obj1
112
175
  }
@@ -133,14 +133,15 @@ module.exports = class HttpTemplate extends require('../../plugin') {
133
133
  async #generateRequests(services, payload, csn) {
134
134
  let varsDefined = false // when writing requests for multiple services to stdout, define vars only once
135
135
  for (const [serviceName, service] of services) {
136
- let [hostname, auth] = await this.#getHostnameAndAuth(service, csn)
136
+ let [hostname, username, password] = await this.#getHostnameAndAuth(service, csn)
137
137
  let requests = []
138
138
  if (!cds.cli.options.dry || !varsDefined) {
139
- requests.push(auth ? `@server = ${hostname}\n@auth = ${auth}\n` : `@server = ${hostname}\n`)
139
+ const auth = username ? `@username=${username}\n@password=${password}\n` : ''
140
+ requests.push(`@server=${hostname}\n${auth}`)
140
141
  }
141
142
  varsDefined = true
142
- const headers = auth ? `Content-Type: application/json\n{{auth}}` : `Content-Type: application/json`
143
- const headerAuth = auth ? '{{auth}}' : ''
143
+ const headerAuth = username ? `Authorization: Basic {{username}}:{{password}}` : ''
144
+ const headers = headerAuth ? `Content-Type: application/json\n${headerAuth}` : `Content-Type: application/json`
144
145
 
145
146
  service.urlPaths.forEach((urlPath) => {
146
147
  if (service.urlPaths.length > 1) {
@@ -201,7 +202,7 @@ module.exports = class HttpTemplate extends require('../../plugin') {
201
202
  const nameDraftsCreate = slug(title+'_Draft_POST')
202
203
  requests.push(`${title} Drafts GET\n# @name ${slug(title+'_Drafts_GET')}\nGET ${url}?$filter=(IsActiveEntity eq false)\n${headerAuth}\n`) // unencoded URL for better readability (client can handle this)
203
204
  requests.push(`${title} Draft POST\n# @name ${nameDraftsCreate}\nPOST ${url}\n${headers}\n\n${postBody}\n`)
204
- requests.push(`\n### Result from POST request above\n@draftID = {{${nameDraftsCreate}.response.body.$.${key}}}\n`)
205
+ requests.push(`\n### Result from POST request above\n@draftID={{${nameDraftsCreate}.response.body.$.${key}}}\n`)
205
206
  requests.push(`${title} Draft PATCH\n# @name ${slug(title+'_Draft_Patch')}\nPATCH ${url}(${key}={{draftID}},IsActiveEntity=false)\n${headers}\n\n${postBody}\n`)
206
207
  requests.push(`${title} Draft Prepare\n# @name ${slug(title+'_Draft_Prepare')}\nPOST ${url}(${key}={{draftID}},IsActiveEntity=false)/AdminService.draftPrepare\n${headers}\n\n{}\n`)
207
208
  requests.push(`${title} Draft Activate\n# @name ${slug(title+'_Draft_Activate')}\nPOST ${url}(${key}={{draftID}},IsActiveEntity=false)/AdminService.draftActivate\n${headers}\n\n{}\n`)
@@ -216,8 +217,10 @@ module.exports = class HttpTemplate extends require('../../plugin') {
216
217
  if (!cds.cli.options['for-app']) {
217
218
  const host = service.isJava ? this.#javaHost : this.#nodeHost
218
219
  const users = Object.values(await this.#mockUsersForService(service, csn))
219
- const authStr = users.length ? `Authorization: Basic ${users[0].name}:${users[0].password||''}` : null
220
- return [host, authStr]
220
+ if (users.length) {
221
+ return [host, users[0].name, users[0].password || '']
222
+ }
223
+ return [host]
221
224
  }
222
225
 
223
226
  if (!this.approuterAuth) {
@@ -304,13 +307,13 @@ module.exports = class HttpTemplate extends require('../../plugin') {
304
307
  const outputPath = this.#determineOutputPath(serviceName)
305
308
  if (exists(outputPath)) {
306
309
  if (cds.cli.options.force) {
307
- console.log(term.dim(` Overwriting ${outputPath}`))
310
+ console.log(term.dim(` overwriting ${outputPath}`))
308
311
  } else {
309
- console.log(term.dim(` Skipping ${outputPath}`))
312
+ console.log(term.dim(` skipping ${outputPath}`))
310
313
  return
311
314
  }
312
315
  } else {
313
- console.log(term.dim(` Creating ${outputPath}`))
316
+ console.log(term.dim(` creating ${outputPath}`))
314
317
  }
315
318
  write(http).to(outputPath)
316
319
  }
@@ -22,7 +22,7 @@ module.exports = class JavaTemplate extends require('../../plugin') {
22
22
  throw `You can't change the runtime of an existing project.`
23
23
  }
24
24
  if (options.add?.has(NODEJS)) {
25
- throw `Only one runtime per project is supported. Specify either ${bold(JAVA)} or ${bold(NODEJS)}.`
25
+ throw `only one runtime per project is supported – specify either ${bold(JAVA)} or ${bold(NODEJS)}`
26
26
  }
27
27
  return true
28
28
  }
@@ -30,7 +30,7 @@ module.exports = class JavaTemplate extends require('../../plugin') {
30
30
  async run() {
31
31
  const projectName = path.basename(cds.root)
32
32
  const { params, artifactId, archetypeVersion } = await mvn.init(projectName)
33
- console.log(`Using Maven archetype version ${archetypeVersion}`)
33
+ console.log(`using Maven archetype version ${archetypeVersion}`)
34
34
 
35
35
  const temp = await mkdtemp(join(os.tmpdir(), `${path.basename(cds.root)}_`))
36
36
  try {
@@ -47,6 +47,6 @@ module.exports = class JavaTemplate extends require('../../plugin') {
47
47
  }
48
48
 
49
49
  async finalize() {
50
- console.log(`Learn about next steps at ${link(URLS.CAPIRE)}`)
50
+ console.log(`learn about next steps at ${link(URLS.CAPIRE)}`)
51
51
  }
52
52
  }