@sap/cds-dk 8.3.0 → 8.4.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 (54) hide show
  1. package/CHANGELOG.md +59 -13
  2. package/bin/build.js +1 -1
  3. package/bin/cds.js +12 -0
  4. package/bin/compile/index.js +12 -2
  5. package/bin/deploy/index.js +6 -1
  6. package/bin/env.js +3 -2
  7. package/bin/init.js +8 -4
  8. package/bin/repl.js +3 -3
  9. package/lib/build/util.js +32 -20
  10. package/lib/compile/to-xsuaa.js +39 -26
  11. package/lib/deploy/to-hana/hana.js +62 -74
  12. package/lib/env/schemas/cds-rc.js +174 -2
  13. package/lib/import/asyncapi/parserContext.js +4 -30
  14. package/lib/import/odata/v2parser.js +3 -2
  15. package/lib/import/openapi/importOpenAPI.js +616 -569
  16. package/lib/import/openapi/utilities.js +7 -2
  17. package/lib/init/constants.js +2 -1
  18. package/lib/init/index.js +4 -0
  19. package/lib/init/projectReader.js +1 -2
  20. package/lib/init/registries/mta.js +8 -1
  21. package/lib/init/template/application-logging/files/package.json.hbs +6 -2
  22. package/lib/init/template/application-logging/index.js +4 -4
  23. package/lib/init/template/approuter/files/package.json +2 -2
  24. package/lib/init/template/cf-manifest/files/manifest.yml.hbs +3 -1
  25. package/lib/init/template/data/as-csv.js +1 -1
  26. package/lib/init/template/data/as-json.js +46 -42
  27. package/lib/init/template/enterprise-messaging-shared/index.js +1 -1
  28. package/lib/init/template/file-based-messaging/index.js +1 -1
  29. package/lib/init/template/hana/files/db/package.json +1 -1
  30. package/lib/init/template/hana/files/package.json.hbs +2 -0
  31. package/lib/init/template/hana/index.js +1 -1
  32. package/lib/init/template/handler/index.js +1 -1
  33. package/lib/init/template/html5-repo/files/app-package.json.hbs +2 -2
  34. package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +2 -2
  35. package/lib/init/template/http/index.js +6 -7
  36. package/lib/init/template/local-messaging/index.js +1 -1
  37. package/lib/init/template/nodejs/files/.vscode/extensions.json +0 -5
  38. package/lib/init/template/nodejs/files/.vscode/settings.json +1 -1
  39. package/lib/init/template/nodejs/files/.vscode/tasks.json +0 -2
  40. package/lib/init/template/nodejs/files/package.json.hbs +1 -1
  41. package/lib/init/template/postgres/index.js +1 -1
  42. package/lib/init/template/redis-messaging/index.js +1 -1
  43. package/lib/init/template/side-by-side-extensibility/files/mta.yaml.hbs +40 -0
  44. package/lib/init/template/side-by-side-extensibility/files/package.json.hbs +17 -0
  45. package/lib/init/template/side-by-side-extensibility/index.js +34 -0
  46. package/lib/init/template/typer/index.js +0 -1
  47. package/lib/init/template/typescript/files/package.json +2 -2
  48. package/lib/init/template/xsuaa/index.js +1 -1
  49. package/lib/mtx/push.js +0 -1
  50. package/lib/util/fs.js +1 -1
  51. package/lib/util/jsonc.js +1 -1
  52. package/lib/util/jsonc.pegjs +6 -2
  53. package/npm-shrinkwrap.json +74 -151
  54. package/package.json +1 -1
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const fs = require('fs').promises;
4
4
 
5
5
  const cds = require('../../../lib/cds');
6
- const { exists, read } = cds.utils;
6
+ const { exists, read, write, mkdirp } = cds.utils;
7
7
 
8
8
  const { build } = require('../../build');
9
9
 
@@ -38,7 +38,7 @@ module.exports = new class HanaDeployer {
38
38
  async deploy(options = {}) {
39
39
  let { model, serviceName: pServiceName, tunnelAddress, vcapFile, undeployWhitelist, hdiOptions = {}, dry, storeCredentials } = options;
40
40
 
41
- options.for = options.for || 'hybrid';
41
+ options.for ??= 'hybrid';
42
42
 
43
43
  if (model) {
44
44
  throw `Specifying a CDS model for HANA deployment is not supported.
@@ -67,68 +67,73 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
67
67
  }
68
68
  }
69
69
 
70
- const buildResults = await this._build();
70
+ let buildResult
71
+ if (cds.cli.options?.['no-build']) {
72
+ console.log(`Provided flag '--no-build', skipping cds build`)
73
+ } else {
74
+ buildResult = await this._build();
75
+ }
71
76
 
72
77
  if (process.env.TARGET_CONTAINER) {
73
78
  vcapEnv.TARGET_CONTAINER = process.env.TARGET_CONTAINER;
74
79
  }
75
80
 
76
- for (const { task, result } of buildResults) {
77
- if (dry) {
78
- const sortedResult = Array.from(result.hana).sort();
79
- for (const file of sortedResult) {
80
- const srcPath = path.join(result.dest, file);
81
- console.log(`-- ${path.relative(cds.root, srcPath)}`);
82
- console.log(await read(srcPath));
83
- console.log();
84
- }
81
+ if (dry) {
82
+ const sortedResult = Array.from(buildResult.hana).sort();
83
+ for (const file of sortedResult) {
84
+ const srcPath = path.join(buildResult.dest, file);
85
+ console.log(`-- ${path.relative(cds.root, srcPath)}`);
86
+ console.log(await read(srcPath));
87
+ console.log();
88
+ }
89
+ } else {
90
+ // the primary database is always built and deployed
91
+ const dest = path.join(cds.env.build.target, cds.env.folders.db);
92
+
93
+ let serviceName = pServiceName;
94
+ let serviceKeyName;
95
+ const { instance, key } = cds.requires.db?.binding ?? {};
96
+ if (!serviceName && instance) {
97
+ serviceName = instance;
98
+ serviceKeyName = key;
99
+ }
100
+
101
+ if (undeployWhitelist) {
102
+ console.log('Writing undeploy.json');
103
+ await write(undeployWhitelist).to(dest, 'undeploy.json')
104
+ }
105
+
106
+ const hasVCAPEnv = Object.keys(vcapEnv).length > 0;
107
+ if (!serviceName && hasVCAPEnv) {
108
+ await mkdirp(dest);
85
109
  } else {
86
- const dest = task.dest;
110
+ const { cfServiceInstanceName, cfServiceInstanceKeyName, serviceKey } =
111
+ await this._getOrCreateCFService(dest, serviceName, serviceKeyName, tunnelAddress);
87
112
 
88
- let serviceName = pServiceName;
89
- let serviceKeyName;
90
- if (!serviceName && cds.env.requires.db?.binding?.instance) {
91
- serviceName = cds.env.requires.db.binding.instance;
92
- serviceKeyName = cds.env.requires.db.binding.key;
113
+ serviceName ??= cfServiceInstanceName;
114
+ serviceKeyName ??= cfServiceInstanceKeyName
115
+ if (!vcapEnv.VCAP_SERVICES || serviceName) {
116
+ vcapEnv.VCAP_SERVICES = this._getVCAPServicesEntry(cfServiceInstanceName, serviceKey);
93
117
  }
94
118
 
95
- if (undeployWhitelist) {
96
- console.log('Writing undeploy.json');
97
- await fs.write(path.join(dest, 'undeploy.json'), JSON.stringify(undeployWhitelist, null, 2));
119
+ if (storeCredentials) {
120
+ await this._addInstanceToDefaultEnvJson([dest, cds.root], cfServiceInstanceName, serviceKey);
98
121
  }
122
+ }
99
123
 
100
- const hasVCAPEnv = Object.keys(vcapEnv).length > 0;
101
- if (!serviceName && hasVCAPEnv) {
102
- await fs.mkdir(dest, { recursive: true });
103
- } else {
104
- const { cfServiceInstanceName, cfServiceInstanceKeyName, serviceKey } =
105
- await this._getOrCreateCFService(dest, serviceName, serviceKeyName, tunnelAddress);
106
-
107
- serviceName ??= cfServiceInstanceName;
108
- serviceKeyName ??= cfServiceInstanceKeyName
109
- if (!vcapEnv.VCAP_SERVICES || serviceName) {
110
- vcapEnv.VCAP_SERVICES = this._getVCAPServicesEntry(cfServiceInstanceName, serviceKey);
111
- }
112
-
113
- if (storeCredentials) {
114
- await this._addInstanceToDefaultEnvJson([dest, cds.root], cfServiceInstanceName, serviceKey);
115
- }
116
- }
124
+ console.log(`${bold('Starting deployment to SAP HANA ...')}`);
125
+ DEBUG?.({ dest, vcapEnv, hdiOptions })
126
+ await hdi.deploy(dest, vcapEnv, hdiOptions);
117
127
 
118
- console.log(`${bold('Starting deployment to SAP HANA ...')}`);
119
- DEBUG?.({ dest, vcapEnv, hdiOptions })
120
- await hdi.deploy(dest, vcapEnv, hdiOptions);
121
-
122
- if (!hasVCAPEnv) {
123
- const bindOptions = {
124
- ...options,
125
- to: `${serviceName}:${serviceKeyName}`,
126
- for: options.for,
127
- kind: task.for
128
- }
129
- const services = await bind(bindOptions);
130
- await storeBindings(services, bindOptions);
128
+ if (!hasVCAPEnv) {
129
+ const bindOptions = {
130
+ ...options,
131
+ to: `${serviceName}:${serviceKeyName}`,
132
+ for: options.for,
133
+ kind: BUILD_TASK_HANA
131
134
  }
135
+ const services = await bind(bindOptions);
136
+ await storeBindings(services, bindOptions);
132
137
  }
133
138
 
134
139
  if (storeCredentials) {
@@ -145,7 +150,7 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
145
150
  console.warn(warn(warnings.join('\n')));
146
151
  }
147
152
 
148
- return { buildResults };
153
+ return buildResult;
149
154
  }
150
155
 
151
156
 
@@ -168,7 +173,7 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
168
173
  console.log(`Using container ${bold(cfServiceInstanceName)}`);
169
174
 
170
175
  const cfConfig = cfServiceInstanceMta?.parameters?.config;
171
- const serviceInstance = await this.createHanaService(cfServiceInstanceName, cfConfig);
176
+ const serviceInstance = await cfUtil.getOrCreateService('hana', 'hdi-shared', cfServiceInstanceName, cfConfig);
172
177
 
173
178
  const cfServiceInstanceKeyName = serviceKeyName ?? `${cfServiceInstanceName}-key`;
174
179
  let credentials = await cfUtil.getOrCreateServiceKey(serviceInstance, cfServiceInstanceKeyName, { permissions: 'development' });
@@ -182,25 +187,6 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
182
187
  return { cfServiceInstanceName, cfServiceInstanceKeyName, serviceKey: credentials, serviceInstance }
183
188
  }
184
189
 
185
-
186
- async createHanaService(instanceName, cfConfig) {
187
- // hana or hanatrial, error if neither found
188
- try {
189
- return await cfUtil.getOrCreateService('hana', 'hdi-shared', instanceName, cfConfig);
190
- } catch (error) {
191
- if (error.command && /offering .* not found/i.test(error.command.stderr)) {
192
- console.log(`Falling back to 'hanatrial'`);
193
- return await cfUtil.getOrCreateService('hanatrial', 'hdi-shared', instanceName, cfConfig);
194
- }
195
- else if (error.command && /no database/i.test(error.command.stderr)) {
196
- console.log(`No database connected to 'hana' service. Falling back to 'hanatrial'`);
197
- return await cfUtil.getOrCreateService('hanatrial', 'hdi-shared', instanceName, cfConfig);
198
- }
199
- throw error;
200
- }
201
- }
202
-
203
-
204
190
  _validateCredentials(credentials, cfServiceInstanceKey) {
205
191
  if (!credentials) {
206
192
  throw `Could not create service key ${bold(cfServiceInstanceKey)}.`;
@@ -225,9 +211,11 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
225
211
  src: cds.env.folders.db,
226
212
  [OUTPUT_MODE]: OUTPUT_MODE_FILESYSTEM | OUTPUT_MODE_RESULT
227
213
  }
228
-
229
214
  console.log(`Running build`);
230
- return build(buildTaskOptions);
215
+
216
+ // cds build throws an error if the src folder does not exist
217
+ const buildResult = await build(buildTaskOptions);
218
+ return buildResult[0].result;
231
219
  }
232
220
 
233
221
  async _addInstanceToDefaultEnvJson(currentFolders, serviceInstanceName, serviceKey) {
@@ -131,6 +131,141 @@ module.exports = {
131
131
  }
132
132
  },
133
133
 
134
+ import: {
135
+ type: "object",
136
+ description: "Set CDS import configurations.",
137
+ additionalProperties: false,
138
+ properties: {
139
+ as: {
140
+ type: "string",
141
+ description: "Converts the input file into one of these supported formats: cds, csn or json.",
142
+ $ref: "#/$defs/import-options/as"
143
+ },
144
+ from: {
145
+ type: "string",
146
+ description: "Imports the model from the one of the given options: edmx, openapi or asyncapi.",
147
+ $ref: "#/$defs/import-options/from"
148
+ },
149
+ force: {
150
+ type: "boolean",
151
+ description: "Forcefully overwrites the contents in the CDS file and updates the checksum",
152
+ },
153
+ no_save: {
154
+ type: "boolean",
155
+ description: "Skips updating ./package.json.",
156
+ },
157
+ out: {
158
+ type: "string",
159
+ description: "Output file written to the specified location."
160
+ },
161
+ dry: {
162
+ type: "boolean",
163
+ description: "Writes the converted csn to stdout only.",
164
+ },
165
+ keepNamespace: {
166
+ type: "boolean",
167
+ description: "Keeps the original namespace from the edmx content instead of using the filename by default.",
168
+ default: false
169
+ },
170
+ includeNamespaces: {
171
+ type: "string",
172
+ description: "Imports only entities matching to the given list of namespaces."
173
+ }
174
+ }
175
+ },
176
+
177
+ export: {
178
+ type: "object",
179
+ description: "Customize cds compile settings",
180
+ additionalProperties: false,
181
+ properties: {
182
+ asyncapi: {
183
+ type: "object",
184
+ description: "AsyncAPI settings for cds compile",
185
+ additionalProperties: false,
186
+ properties: {
187
+ application_namespace: {
188
+ type: "string",
189
+ description: "The registered namespace of the application"
190
+ },
191
+ merged: {
192
+ type: "object",
193
+ description: "properties that needs to be there when merged option is used",
194
+ additionalProperties: false,
195
+ properties: {
196
+ title: {
197
+ type: "string",
198
+ description: "Title to be used for merged doc"
199
+ },
200
+ version: {
201
+ type: "string",
202
+ description: "Version to be used for merged doc"
203
+ },
204
+ short_text: {
205
+ type: "string",
206
+ description: "Short decription of the AsyncAPIs"
207
+ },
208
+ description: {
209
+ type: "string",
210
+ description: "Description to be used for the merged doc"
211
+ }
212
+ }
213
+ },
214
+ event_spec_version: {
215
+ type: "string",
216
+ description: "Mentions the version of AsyncAPI spec"
217
+ },
218
+ event_source: {
219
+ type: "string",
220
+ description: "Event source information",
221
+ default: "{region}/{applicationNamespace}/{instanceId}",
222
+ },
223
+ event_source_params: {
224
+ type: "object",
225
+ description: "Parameters related to the source of event",
226
+ default: {
227
+ region: {
228
+ "description": "The regional context of the application.",
229
+ "schema": {
230
+ "type": "string"
231
+ }
232
+ },
233
+ applicationNamespace: {
234
+ "description": "The registered namespace of the application.",
235
+ "schema": {
236
+ "type": "string"
237
+ }
238
+ },
239
+ instanceId: {
240
+ "description": "The instance id (tenant, installation, ...) of the application.",
241
+ "schema": {
242
+ "type": "string"
243
+ }
244
+ }
245
+ },
246
+ additionalProperties: false,
247
+ region: {
248
+ type: "string",
249
+ description: "The regional context of the application"
250
+ },
251
+ applicationNamespace: {
252
+ type: "string",
253
+ description: "The registered namespace of the application"
254
+ },
255
+ instanceId: {
256
+ type: "string",
257
+ description: "The instance id (tenant, installation, ...) of the application."
258
+ }
259
+ },
260
+ event_characteristics: {
261
+ type: "object",
262
+ description: "The characteristics defined for events",
263
+ additionalProperties: true
264
+ }
265
+ }
266
+ }
267
+ }
268
+ },
134
269
  // deploy specific schema
135
270
  deploy: {
136
271
  type: "object",
@@ -164,7 +299,8 @@ module.exports = {
164
299
  "mtx-extension",
165
300
  "mtx",
166
301
  "node-cf",
167
- "java-cf"
302
+ "java-cf",
303
+ "helm"
168
304
  ],
169
305
  enumDescriptions: [
170
306
  "Node.js build plugin",
@@ -174,7 +310,8 @@ module.exports = {
174
310
  "MTX extension build plugin",
175
311
  "MTX build plugin",
176
312
  "Node.js Cloud Foundry build plugin",
177
- "Java Cloud Foundry build plugin"
313
+ "Java Cloud Foundry build plugin",
314
+ "Helm chart build plugin"
178
315
  ]
179
316
  },
180
317
 
@@ -193,6 +330,41 @@ module.exports = {
193
330
  }
194
331
  }
195
332
  ]
333
+ },
334
+
335
+ import_options: {
336
+ from: {
337
+ oneOf: [
338
+ {
339
+ const: "asyncapi",
340
+ description: "AsyncAPI specification"
341
+ },
342
+ {
343
+ const: "edmx",
344
+ description: "OData specification"
345
+ },
346
+ {
347
+ const: "openapi",
348
+ description: "OpenAPI specification"
349
+ }
350
+ ]
351
+ },
352
+ as: {
353
+ oneOf: [
354
+ {
355
+ const: "cds",
356
+ description: "Converts the input file into cds format"
357
+ },
358
+ {
359
+ const: "csn",
360
+ description: "Converts the input file into csn format"
361
+ },
362
+ {
363
+ const: "json",
364
+ description: "Converts the input file into json format"
365
+ }
366
+ ]
367
+ }
196
368
  }
197
369
  }
198
370
  }
@@ -6,12 +6,11 @@ module.exports = class ParserContext {
6
6
  * @constructor
7
7
  */
8
8
  constructor(content) {
9
- this.topics = this.getTopics(content);
10
9
  this.services = this.getServicesFromContent(content);
11
10
  }
12
11
 
13
- getTopicName(channelName) {
14
- return channelName.startsWith('ce/') ? channelName.substring(3) : channelName;
12
+ getTopicName(eventName) {
13
+ return eventName.startsWith('ce/') ? eventName.substring(3) : eventName;
15
14
  }
16
15
 
17
16
  getEventName(messageRefString) {
@@ -19,28 +18,6 @@ module.exports = class ParserContext {
19
18
  return lastSlashIndex == -1 ? '' : messageRefString.substring(lastSlashIndex + 1);
20
19
  }
21
20
 
22
- /**
23
- * Return an obect with topic annotations for messages
24
- */
25
- getTopics(content) {
26
- const topics = {};
27
-
28
- Object.entries(content.channels).forEach(([key, value]) => {
29
- const topic = this.getTopicName(key);
30
- if (value.subscribe && value.subscribe.message && value.subscribe.message.$ref) {
31
- const event = this.getEventName(value.subscribe.message.$ref);
32
- topics[event] = topic;
33
- }
34
-
35
- if (value.publish && value.publish.message && value.publish.message.$ref) {
36
- const event = this.getEventName(value.publish.message.$ref);
37
- topics[event] = topic;
38
- }
39
- });
40
-
41
- return topics;
42
- }
43
-
44
21
  /**
45
22
  * Returns services from the asyncapi json object along with the events and annotations.
46
23
  * @returns an object with all the services.
@@ -49,7 +26,7 @@ module.exports = class ParserContext {
49
26
  const services = {};
50
27
  const serviceAnnotations = this.getServiceAnnotations(content);
51
28
 
52
- Object.entries(content.components.messages).forEach(([key, value]) => {
29
+ Object.entries(content.components.messages).forEach(([, value]) => {
53
30
  let eventName;
54
31
  // Message name should be used to capture the event name
55
32
  if (value.name) {
@@ -71,10 +48,7 @@ module.exports = class ParserContext {
71
48
  const eventAnnotations = this.getEventAnnotations(value);
72
49
 
73
50
  // add topic annotation
74
- if (this.topics[key]) {
75
- eventAnnotations['@topic'] = this.topics[key];
76
- }
77
-
51
+ eventAnnotations['@topic'] = this.getTopicName(eventName);
78
52
  let properties = {};
79
53
  let types = {};
80
54
 
@@ -1650,8 +1650,9 @@ function _generateEDMXJSON2CSN(jsonObj, ignorePersistenceSkip, parserContext) {
1650
1650
  }
1651
1651
 
1652
1652
  // Construct CSN entity based on EDMX entity
1653
- let entityJson = jsonObj["edmx:Edmx"]["edmx:DataServices"].Schema.EntityType;
1654
- if (!Array.isArray(entityJson)) entityJson = [entityJson];
1653
+ let entityJson = jsonObj["edmx:Edmx"]["edmx:DataServices"].Schema.EntityType;
1654
+ // if entityJson exists(ignoring the case of no entity) and is not an array, convert it to an array to handle the case of single entity
1655
+ if (entityJson && !Array.isArray(entityJson)) entityJson = [entityJson];
1655
1656
  const definitions = [];
1656
1657
 
1657
1658
  if (parserContext.mockServerUc) {