@sap/cds-dk 8.6.1 → 8.7.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 +36 -0
  2. package/bin/cds.js +11 -6
  3. package/bin/compile/index.js +1 -1
  4. package/bin/debug.js +122 -26
  5. package/bin/deploy/index.js +7 -0
  6. package/bin/import.js +54 -29
  7. package/bin/repl.js +2 -2
  8. package/bin/util/fuzzySearch.js +10 -10
  9. package/bin/watch.js +47 -16
  10. package/lib/build/buildTaskProviderFactory.js +8 -5
  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 +3 -2
  13. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.yaml +1 -1
  14. package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -1
  15. package/lib/build/plugins/helm/files/subcharts/web-application/templates/_helpers.tpl +3 -3
  16. package/lib/build/plugins/helm/files/subcharts/web-application/templates/api-rule.yaml +1 -1
  17. package/lib/build/plugins/helm/files/subcharts/web-application/templates/deployment.yaml +13 -5
  18. package/lib/build/plugins/helm/files/subcharts/web-application/templates/service-binding.yaml +1 -0
  19. package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +28 -15
  20. package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +22 -1
  21. package/lib/import/common.js +174 -44
  22. package/lib/import/importapi.js +1 -1
  23. package/lib/import/message.js +1 -1
  24. package/lib/import/odata/ParserContext.js +46 -0
  25. package/lib/import/odata/index.js +20 -11
  26. package/lib/import/odata/v4parser.js +25 -51
  27. package/lib/init/add.js +0 -2
  28. package/lib/init/constants.js +3 -3
  29. package/lib/init/index.js +1 -1
  30. package/lib/init/merge.js +67 -66
  31. package/lib/init/mvn.js +16 -12
  32. package/lib/init/projectReader.js +2 -2
  33. package/lib/init/template/approuter/files/mta.yaml.hbs +2 -4
  34. package/lib/init/template/hana/index.js +12 -8
  35. package/lib/init/template/ias/files/mta.yaml.hbs +15 -0
  36. package/lib/init/template/ias/index.js +27 -7
  37. package/lib/init/template/mta/files/mta.yaml.hbs +4 -17
  38. package/lib/init/template/multitenancy/files/mta.yaml.hbs +1 -0
  39. package/lib/init/template/multitenancy/index.js +5 -0
  40. package/lib/init/template/nodejs/files/package.json.hbs +1 -1
  41. package/lib/init/template/portal/files/portal-site/i18n/catalog.properties +1 -1
  42. package/lib/init/template/portal/files/portal-site/i18n/catalog_de.properties +1 -1
  43. package/lib/init/template/portal/files/portal-site/i18n/group.properties +1 -1
  44. package/lib/init/template/portal/files/portal-site/i18n/group_de.properties +1 -1
  45. package/lib/init/template/sample/files/typescript/admin-service.ts.hbs +16 -0
  46. package/lib/init/template/sample/files/typescript/cat-service.ts.hbs +38 -0
  47. package/lib/init/template/sample/index.js +16 -12
  48. package/lib/init/template/typescript/files/package.json +1 -1
  49. package/lib/lint/io.js +6 -16
  50. package/lib/watch/node-watch.js +3 -3
  51. package/npm-shrinkwrap.json +54 -38
  52. package/package.json +1 -1
  53. /package/lib/init/template/sample/files/{nodejs → javascript}/admin-service.js.hbs +0 -0
  54. /package/lib/init/template/sample/files/{nodejs → javascript}/cat-service.js.hbs +0 -0
@@ -5,4 +5,4 @@ name: web-application
5
5
  sources:
6
6
  - https://github.tools.sap/unified-runtime/web-application
7
7
  type: application
8
- version: 1.20.0
8
+ version: 1.22.0
@@ -208,11 +208,11 @@ If release name contains chart name it will be used as service instance name.
208
208
  {{ .checkName }}:
209
209
  {{- if .checkValues.path }}
210
210
  httpGet:
211
- path: {{ .checkValues.path }}
212
- port: http
211
+ path: {{ tpl .checkValues.path .context }}
212
+ port: {{ .values.port | default .context.Values.port }}
213
213
  {{- else }}
214
214
  tcpSocket:
215
- port: {{ .context.Values.port }}
215
+ port: {{ .values.port | default .context.Values.port}}
216
216
  {{- end }}
217
217
  {{- if eq .checkName "startupProbe" }}
218
218
  periodSeconds: 2
@@ -20,7 +20,7 @@ spec:
20
20
  name: {{ include "web-application.fullname" . }}
21
21
  port: {{ .Values.port }}
22
22
  rules:
23
- {{- toYaml .Values.expose.rules | nindent 4 }}
23
+ {{- tpl (toYaml .Values.expose.rules) $ | nindent 4 }}
24
24
  {{- if .Values.expose.corsPolicy }}
25
25
  corsPolicy:
26
26
  {{- toYaml .Values.expose.corsPolicy | nindent 4 }}
@@ -19,9 +19,8 @@ spec:
19
19
  {{- if .Values.istio }}
20
20
  sidecar.istio.io/inject: {{ .Values.istio.enabled | quote }}
21
21
  {{ end }}
22
- annotations:
23
- container.apparmor.security.beta.kubernetes.io/{{ include "web-application.name" . }}: runtime/default
24
22
  {{- if or .Values.annotations.deployment }}
23
+ annotations:
25
24
  {{- tpl (toYaml .Values.annotations.deployment) $ | nindent 8 }}
26
25
  {{- end }}
27
26
  spec:
@@ -73,9 +72,9 @@ spec:
73
72
  {{- if not (eq (len $envVars.vars) 0) }}
74
73
  {{- $envVars.vars | toYaml | nindent 8}}
75
74
  {{- end }}
76
- {{ include "web-application.internal.healthCheck" (dict "checkName" "livenessProbe" "checkValues" .Values.health.liveness "context" $) | nindent 8}}
77
- {{ include "web-application.internal.healthCheck" (dict "checkName" "readinessProbe" "checkValues" .Values.health.readiness "context" $) | nindent 8}}
78
- {{ include "web-application.internal.healthCheck" (dict "checkName" "startupProbe" "checkValues" .Values.health.liveness "context" $) | nindent 8}}
75
+ {{ include "web-application.internal.healthCheck" (dict "checkName" "livenessProbe" "checkValues" .Values.health.liveness "values" .Values.health "context" $) | nindent 8}}
76
+ {{ include "web-application.internal.healthCheck" (dict "checkName" "readinessProbe" "checkValues" .Values.health.readiness "values" .Values.health "context" $) | nindent 8}}
77
+ {{ include "web-application.internal.healthCheck" (dict "checkName" "startupProbe" "checkValues" .Values.health.liveness "values" .Values.health "context" $) | nindent 8}}
79
78
  volumeMounts:
80
79
  - name: tmp
81
80
  mountPath: /tmp
@@ -101,6 +100,8 @@ spec:
101
100
  readOnlyRootFilesystem: {{ .Values.readOnlyRootFilesystem | default false }}
102
101
  seccompProfile:
103
102
  type: RuntimeDefault
103
+ appArmorProfile:
104
+ type: RuntimeDefault
104
105
  {{- range $key, $sidecar := .Values.sidecars }}
105
106
  - image: {{ include "web-application.internal.imageName" (dict "image" $sidecar.image "context" $) }}
106
107
  {{- if ( or $.Values.global.imagePullPolicy $sidecar.imagePullPolicy ) }}
@@ -143,6 +144,11 @@ spec:
143
144
  resources:
144
145
  {{- toYaml $sidecar.resources | nindent 10 }}
145
146
  {{- end }}
147
+ {{- if $sidecar.health }}
148
+ {{ include "web-application.internal.healthCheck" (dict "checkName" "livenessProbe" "checkValues" $sidecar.health.liveness "values" $sidecar.health "context" $) | nindent 8}}
149
+ {{ include "web-application.internal.healthCheck" (dict "checkName" "readinessProbe" "checkValues" $sidecar.health.readiness "values" $sidecar.health "context" $) | nindent 8}}
150
+ {{ include "web-application.internal.healthCheck" (dict "checkName" "startupProbe" "checkValues" $sidecar.health.liveness "values" $sidecar.health "context" $) | nindent 8}}
151
+ {{- end }}
146
152
  securityContext:
147
153
  allowPrivilegeEscalation: false
148
154
  capabilities:
@@ -156,6 +162,8 @@ spec:
156
162
  readOnlyRootFilesystem: {{ $.Values.readOnlyRootFilesystem | default false }}
157
163
  seccompProfile:
158
164
  type: RuntimeDefault
165
+ appArmorProfile:
166
+ type: RuntimeDefault
159
167
  {{- end }}
160
168
  restartPolicy: Always
161
169
  terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
@@ -28,6 +28,7 @@ spec:
28
28
  {{- end }}
29
29
  {{- if $mergedParams.parametersFrom }}
30
30
  parametersFrom: {{ tpl ($mergedParams.parametersFrom | toYaml) $ | nindent 4 }}
31
+ watchParametersFromChanges: true
31
32
  {{- end }}
32
33
  {{- if $mergedParams.credentialsRotationPolicy }}
33
34
  credentialsRotationPolicy: {{ $mergedParams.credentialsRotationPolicy | toYaml | nindent 4 }}
@@ -72,9 +72,7 @@
72
72
  },
73
73
  "port": {
74
74
  "$id": "#/properties/port",
75
- "type": "integer",
76
- "minimum": 1,
77
- "maximum": 65535,
75
+ "$ref": "#/definitions/Port",
78
76
  "default": 8080
79
77
  },
80
78
  "serviceAccountName": {
@@ -333,18 +331,7 @@
333
331
  },
334
332
  "health": {
335
333
  "$id": "#/properties/health",
336
- "type": "object",
337
- "additionalProperties": false,
338
- "properties": {
339
- "liveness": {
340
- "$id": "#/properties/health/properties/liveness",
341
- "$ref": "#/definitions/Probe"
342
- },
343
- "readiness": {
344
- "$id": "#/properties/health/properties/readiness",
345
- "$ref": "#/definitions/Probe"
346
- }
347
- }
334
+ "$ref": "#/definitions/Health"
348
335
  },
349
336
  "startupTimeoutSeconds": {
350
337
  "$id": "#/properties/startupTimeoutSeconds",
@@ -1193,6 +1180,10 @@
1193
1180
  "title": "User to be used for running the container.",
1194
1181
  "type": "integer",
1195
1182
  "minimum": 1
1183
+ },
1184
+ "health": {
1185
+ "$id": "#/properties/health",
1186
+ "$ref": "#/definitions/Health"
1196
1187
  }
1197
1188
  }
1198
1189
  }
@@ -1313,6 +1304,22 @@
1313
1304
  "type": "string",
1314
1305
  "enum": ["Always", "IfNotPresent", "Never"]
1315
1306
  },
1307
+ "Health": {
1308
+ "title": "Health Configuration",
1309
+ "type": "object",
1310
+ "additionalProperties": false,
1311
+ "properties": {
1312
+ "port": {
1313
+ "$ref": "#/definitions/Port"
1314
+ },
1315
+ "liveness": {
1316
+ "$ref": "#/definitions/Probe"
1317
+ },
1318
+ "readiness": {
1319
+ "$ref": "#/definitions/Probe"
1320
+ }
1321
+ }
1322
+ },
1316
1323
  "Probe": {
1317
1324
  "title": "Probe configuration",
1318
1325
  "type": "object",
@@ -1339,6 +1346,12 @@
1339
1346
  "minimum": 1
1340
1347
  }
1341
1348
  }
1349
+ },
1350
+ "Port": {
1351
+ "title": "Port configuration",
1352
+ "type": "integer",
1353
+ "minimum": 1,
1354
+ "maximum": 65535
1342
1355
  }
1343
1356
  }
1344
1357
  }
@@ -14,7 +14,7 @@ global:
14
14
  _internal:
15
15
  web-application:
16
16
  versions:
17
- 1.20.0: true
17
+ 1.22.0: true
18
18
  ## Will be used instead of the `.Chart.Name`, e.g. when generating the Deployment name.
19
19
  # nameOverride: name
20
20
 
@@ -94,6 +94,8 @@ availability:
94
94
  - pod-template-hash
95
95
  # Health-check configuration
96
96
  health:
97
+ ## If the probes are using ports other than the application port, specify them here.
98
+ # port: 8081
97
99
  # Liveness-probe configuration.
98
100
  liveness:
99
101
  # HTTP path used by Kubernetes, to perform liveness-probe calls.
@@ -286,6 +288,20 @@ annotations:
286
288
  # envFrom:
287
289
  # - configMapRef:
288
290
  # name: my-configmap1
291
+ # health:
292
+ # port: 8082
293
+ # liveness:
294
+ # path: /healthz
295
+ # periodSeconds: 3
296
+ # failureThreshold: 5
297
+ # successThreshold: 1
298
+ # timeoutSeconds: 1
299
+ # readiness:
300
+ # path: /healthz
301
+ # periodSeconds: 3
302
+ # failureThreshold: 5
303
+ # successThreshold: 1
304
+ # timeoutSeconds: 1
289
305
  # sidecar-2:
290
306
  # image:
291
307
  # repository: gcr.io/my-sidecar-2
@@ -305,7 +321,12 @@ annotations:
305
321
  # envFrom:
306
322
  # - configMapRef:
307
323
  # name: my-configmap1
324
+
325
+ # Optional configuration for creating Horizontal Pod Autoscaling
308
326
  hpa:
309
327
  enabled: false
310
328
  minReplicas: 1
311
329
  maxReplicas: 3
330
+ ## See https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/horizontal-pod-autoscaler-v2/#HorizontalPodAutoscalerSpec
331
+ # behavior: {}
332
+ # metrics: {}
@@ -1,13 +1,15 @@
1
1
  /* eslint-disable no-prototype-builtins */
2
2
  'use strict';
3
3
 
4
+ /** @type { import('@sap/cds') } */
5
+ const cds = require('../cds');
4
6
  let convert = require('xml-js');
5
7
  let axios = require('axios');
6
8
  const messages = require("./message").getMessages();
7
- const cds = require('../cds');
8
9
  const { fs, path, write, mkdirp, exists, read } = cds.utils;
9
10
  const cdsCompiler = require('..').compile.to;
10
11
  const md5 = data => require('crypto').createHash('md5').update(data).digest('hex');
12
+ const { info } = require("../util/term")
11
13
 
12
14
  let optionsMapping = {
13
15
  'keep-namespace': 'keepNamespace',
@@ -16,8 +18,9 @@ let optionsMapping = {
16
18
  'no-save': 'no_save'
17
19
  };
18
20
 
19
- let apiOptions = ['includeNamespaces', 'keepNamespace', 'beta'];
20
- let commonOptions = ['no_copy', 'no_save', 'out', 'dry', 'as', 'from', 'force', 'config', 'destination', 'group', 'name'];
21
+ let apiOptions = ['includeNamespaces', 'keepNamespace'];
22
+ let commonOptions = ['for', 'no_copy', 'no_save', 'out', 'dry', 'as', 'from', 'force', 'config', 'destination', 'group', 'name'];
23
+ let usingStatements = [];
21
24
 
22
25
  async function generateEDMX2JSON(edmx) {
23
26
  let cjson = convert.xml2json(edmx, {
@@ -163,6 +166,7 @@ async function processOptions(cliOptions, options) {
163
166
  delete cliOptions[key];
164
167
  }
165
168
  }
169
+
166
170
  // segregating cli/odata options
167
171
  apiOptions.forEach(function (item) {
168
172
  if (cliOptions[item]) {
@@ -268,7 +272,18 @@ async function _generateChecksumValidate(dest, output, force) {
268
272
  return "/* checksum : " + currentChecksum + " */\n" + output;
269
273
  }
270
274
 
271
- async function _getResult(output, as, dest, force, kind) {
275
+ // Modify the output to replace the using statements with the dependency services
276
+ function _replaceUsingStatementsForDependent(output) {
277
+ let using = usingStatements.join('\n');
278
+ usingStatements = [];
279
+ let startIndex = output.indexOf('using');
280
+ let endIndex = output.indexOf(';', startIndex);
281
+ let usingStatement = output.substring(startIndex, endIndex+1);
282
+ output = output.replace(usingStatement, using);
283
+ return output;
284
+ }
285
+
286
+ async function _getResult(output, as, dest, force, kind, isDependent) {
272
287
  let extension = dest.substring(dest.lastIndexOf(".") + 1);
273
288
  as = (as === "cds") ? "cdl" : as;
274
289
 
@@ -286,6 +301,9 @@ async function _getResult(output, as, dest, force, kind) {
286
301
  if (as && as != "csn" && as != "json") {
287
302
  // cdsCompiler[as] will convert the csn output into various output formats
288
303
  output = cdsCompiler[as](output);
304
+ if (isDependent && kind === 'V4') {
305
+ output = _replaceUsingStatementsForDependent(output);
306
+ }
289
307
  }
290
308
 
291
309
  switch (as) {
@@ -304,52 +322,64 @@ async function _getResult(output, as, dest, force, kind) {
304
322
  return [output, dest];
305
323
  }
306
324
 
307
- async function _write(result, service, cwd) {
308
- let dest, output, using, message, extension;
309
- output = result[0];
310
- dest = result[1];
311
- await write(dest, output, "utf-8");
312
- extension = dest.substring(dest.lastIndexOf('.'));
325
+ function _printUsageHint(dest, service, cwd) {
326
+ const extension = dest.substring(dest.lastIndexOf('.'));
313
327
  dest = dest.replace(extension, '');
314
- if (service.length > 1){
328
+ let using
329
+ if (service.length > 1){
315
330
  using = `using { ${service.join(', ')}} from './${path.relative(cds.env.folders.srv, dest).replace(/\\/g, '/')}'`;
316
- }
317
- else{
331
+ } else {
318
332
  using = `using { ${service} as external } from './${path.relative(cds.env.folders.srv, dest).replace(/\\/g, '/')}'`;
319
- }
320
- using = require('../util/term').info(using);
321
- message = `[cds] - imported API to ${path.relative(cwd, dest)}
333
+ }
334
+ const message = `\n[cds] - imported API to ${path.relative(cwd, dest)}
322
335
  > use it in your CDS models through the like of:
323
336
 
324
- ${using}
337
+ ${require('../util/term').info(using)}
325
338
  `;
326
-
327
339
  console.log(message);
328
340
  }
329
341
 
330
- async function _updatePackageJson(dest, services, kind, cwd, config, destination) {
342
+ async function _updateNodeConfig(dest, services, kind, cwd, options) {
343
+ let config
344
+ if (typeof options.config === 'string') {
345
+ if (options.config.trim().startsWith('{')) {
346
+ config = JSON.parse(options.config.trim());
347
+ } else {
348
+ config = {};
349
+ options.config.trim().split(',').forEach(kv => {
350
+ const [key, val] = kv.split('=');
351
+ config[key] = val;
352
+ })
353
+ normalizeToDeepObject(config);
354
+ }
355
+ }
356
+
331
357
  // try to find package.json in the srv folder
332
358
  let package_json = _findPackageJson(path.resolve(cwd, dest), cwd);
333
- let conf = {};
359
+ let pkgConf = {};
334
360
  if (package_json) {
335
- conf = JSON.parse(await read(package_json, "utf-8"));
361
+ pkgConf = JSON.parse(await read(package_json, "utf-8"));
336
362
  } else {
337
363
  package_json = path.resolve(cwd, 'package.json');
338
364
  }
339
- const requires = ['cds', 'requires'].reduce((p, n) => p[n] || (p[n] = {}), conf);
365
+
366
+ const requires = ['cds', 'requires'].reduce((p, n) => p[n] || (p[n] = {}), pkgConf);
340
367
  if (kind !== 'rest' && kind !== 'rfc') {
341
368
  kind = (kind === "V2") ? 'odata-v2' : 'odata';
342
369
  }
370
+
343
371
  // add destination configuration
344
- if (!config && destination) {
345
- config = { "[production]": { credentials: { destination } } };
372
+ if (!config && options.destination) {
373
+ const profile = options.for || 'production';
374
+ config = { [`[${profile}]`]: { credentials: { destination: options.destination } }};
346
375
  }
347
376
 
348
377
  let package_json_updated = false;
349
378
  for (let service of services) {
350
379
  // existing service configuration data can be updated, e.g. in case of delta imports
351
380
  const model = path.relative(cwd, dest).replace(/\\/g, '/'); // cds env must not be platform specific
352
- const serviceConfig = { kind, model, ...config }
381
+ let serviceConfig = { kind, model, ...config }
382
+ if (options.for) serviceConfig = { [`[${options.for}]`]: serviceConfig };
353
383
  if (!requires[service]) {
354
384
  cds.env.requires[service] = requires[service] = serviceConfig;
355
385
  package_json_updated = true;
@@ -359,12 +389,44 @@ async function _updatePackageJson(dest, services, kind, cwd, config, destination
359
389
  }
360
390
  }
361
391
  }
392
+
362
393
  if (package_json_updated) {
363
- await write(package_json, JSON.stringify(conf, null, ' '));
394
+ await write(package_json, JSON.stringify(pkgConf, null, ' '));
364
395
  console.log(`[cds] - updated ./package.json`);
365
396
  }
366
397
  }
367
398
 
399
+ async function _updateJavaConfig(kind, serviceNames, csn, options={}) {
400
+ const mvn = require('../init/mvn')
401
+ for (let name of serviceNames) {
402
+ let type = kind
403
+ if (!options.from || options.from === 'edmx') type = `odata-${kind.toLowerCase()}`
404
+ const javaOptions = {
405
+ name, type,
406
+ 'destination.name': options.destination ?? name,
407
+ }
408
+ // if the name is not in the model, set the first/only service there as 'model', assuming only one was imported
409
+ if (!csn.definitions[name]) {
410
+ const srv = cds.reflect(csn).find('service')
411
+ if (srv) javaOptions['service.model'] = srv.name
412
+ }
413
+
414
+ // normalize config object into our flat list of key-value pairs
415
+ if (typeof options.config === 'string') {
416
+ if (options.config.trim().startsWith('{')) {
417
+ const cfg = JSON.parse(options.config)
418
+ _flattenConfigInto(cfg, javaOptions)
419
+ } else {
420
+ options.config.trim().split(',').forEach(kv => {
421
+ const [key, val] = kv.split('=')
422
+ javaOptions[key] = val
423
+ })
424
+ }
425
+ }
426
+ await mvn.add('remote-service', javaOptions)
427
+ }
428
+ }
429
+
368
430
  function _findPackageJson(dir, cwd) {
369
431
  const packageJsonPath = path.join(dir, 'package.json');
370
432
  if (fs.existsSync(packageJsonPath)) {
@@ -380,29 +442,96 @@ function _findPackageJson(dir, cwd) {
380
442
  return _findPackageJson(parentDir, cwd);
381
443
  }
382
444
 
383
- function _parseValidJsonConfig(config) {
384
- try {
385
- if (config) {
386
- return JSON.parse(config);
445
+ async function postProcess(filePath, options, csn, cwd) {
446
+ const services = _getServiceNames(csn);
447
+ let destFilePath;
448
+
449
+ if (options.isDependency || options.isDependent) {
450
+ destFilePath = path.resolve(cwd, options.out ? path.join(options.out, path.basename(filePath)) : path.join(cds.env.folders.srv, 'external', path.basename(filePath)));
451
+ } else {
452
+ destFilePath = path.resolve(cwd, options.out ? path.join(path.dirname(options.out), path.basename(filePath)) : path.join(cds.env.folders.srv, 'external', path.basename(filePath)));
453
+ }
454
+
455
+ if (options.out) {
456
+ options.out = _validateAndComputeOutputFilePath(options.out, options.as, destFilePath);
457
+ if (options.isDependency || options.isDependent) {
458
+ // output is a dirname in case of import with dependencies, then append the filename to it
459
+ let fileName = path.basename(filePath);
460
+ let index = fileName.lastIndexOf(".");
461
+ options.out = path.join(options.out, fileName.substring(0, index));
387
462
  }
388
- } catch (error) {
389
- throw new Error(messages.SPECIFY_CONFIG);
390
463
  }
391
- }
392
464
 
393
- async function postProcess(filePath, options, csn, cwd) {
394
- const services = _getServiceNames(csn);
395
- let config = _parseValidJsonConfig(options.config);
396
- const destFilePath = path.resolve(cwd, options.out ? path.join(options.out, path.basename(filePath)) : path.join(cds.env.folders.srv, 'external', path.basename(filePath)));
397
- if (options.out) options.out = _validateAndComputeOutputFilePath(options.out, options.as, destFilePath);
398
465
  const kind = options.inputFileKind;
399
- const result = await _getResult(csn, options.as, options.out || destFilePath.replace(/\.[^.]+$/, ''), options.force, kind);
400
- const written = options.dry || await _write(result, services, cwd);
401
- const registered = options.dry || options.no_save || await _updatePackageJson(result[1].replace(/\.[^.]+$/, ''), services, kind, cwd, config, options.destination);
466
+ let result;
467
+ try {
468
+ result = await _getResult(csn, options.as, options.out || destFilePath.replace(/\.[^.]+$/, ''), options.force, kind, options.isDependent);
469
+ } catch (error) {
470
+ if (options.isDependency && error.message === messages.FILE_MODIFIED) {
471
+ return console.log(info(`[cds] - skipping import of ${path.basename(filePath)} as it is already imported`));
472
+ } else {
473
+ throw error;
474
+ }
475
+ }
476
+
477
+ const [serviceCsn, csnFile] = result;
478
+
479
+ if (!options.dry && !options.no_save) {
480
+ if (cds.env['project-nature'] === 'java') {
481
+ await _updateJavaConfig(kind, services, csn, options)
482
+ } else {
483
+ await _updateNodeConfig(result[1].replace(/\.[^.]+$/, ''), services, kind, cwd, options);
484
+ }
485
+ }
486
+ if (!options.dry)
487
+ await write(csnFile, serviceCsn, "utf-8");
488
+
489
+ if (options.isDependency) {
490
+ if (services.length > 1){
491
+ usingStatements.push(`using { ${services.join(', ')}} from './${path.basename(csnFile)}';`);
492
+ } else {
493
+ usingStatements.push(`using { ${services} } from './${path.basename(csnFile)}';`);
494
+ }
495
+ }
496
+
497
+ _printUsageHint(csnFile, services, cwd);
498
+
402
499
  if (!options.dry && !options.no_copy)
403
500
  await _copyToSrvExternal(filePath, destFilePath, cwd);
404
- if (options.dry) return console.log(result[0]);
405
- return Promise.all([written, registered]);
501
+
502
+ if (options.dry)
503
+ return console.log(result[0]);
504
+ }
505
+
506
+ function _flattenConfigInto (config, target, prefix='') {
507
+ for (let each in config) {
508
+ if (typeof config[each] === 'object') _flattenConfigInto (config[each], target, prefix + each + '.')
509
+ else target[prefix + each] = config[each]
510
+ }
511
+ return target
512
+ }
513
+
514
+ /**
515
+ * Normalizes an dot-separated keys in the object to a deep structure.
516
+ * Example is the SpringBoot config that allows dots in keys to express nested objects.
517
+ */
518
+ function normalizeToDeepObject(obj) {
519
+ if (typeof obj !== 'object') return obj
520
+ Object.keys(obj).forEach(k => {
521
+ const prop = k.split('.')
522
+ const last = prop.pop()
523
+ // and define the object if not already defined
524
+ const res = prop.reduce((o, key) => {
525
+ // define the object if not defined and return
526
+ return o[key] = o[key] ?? {}
527
+ }, obj)
528
+ res[last] = obj[k]
529
+ // recursively normalize
530
+ normalizeToDeepObject(obj[k])
531
+ // delete the original property from object if it was rewritten
532
+ if (prop.length) delete obj[k]
533
+ })
534
+ return obj
406
535
  }
407
536
 
408
537
  async function identifyFile(filepath) {
@@ -454,5 +583,6 @@ module.exports = {
454
583
  postProcess,
455
584
  identifyFile,
456
585
  readEnvVariables,
457
- processOptions
586
+ processOptions,
587
+ normalizeToDeepObject
458
588
  };
@@ -64,7 +64,7 @@ async function cds_import_from_edmx(filepath, options = { keepNamespace: false }
64
64
  filepath = await common.isValidInputFile(path.resolve(process.cwd(), filepath));
65
65
  const src = await common.getContentFromFile(filepath);
66
66
  // takes the api specific options and reads it from env
67
- let odataOptions = { include_namespaces: options.includeNamespaces, keep_namespace: options.keepNamespace, beta: options.beta};
67
+ let odataOptions = { include_namespaces: options.includeNamespaces, keep_namespace: options.keepNamespace};
68
68
  if (cds.env.import) {
69
69
  common.readEnvVariables(odataOptions, false);
70
70
  }
@@ -37,8 +37,8 @@ const messages = {
37
37
  FIXED_PRECISION_VARIABLE_SCALE: 'WARNING: Fixed Precision with Variable or Floating Scale is not supported in CDS.',
38
38
  NULLABLE_KEY: 'WARNING: Key element cannot be Nullable.',
39
39
  COLLISION_DETECTED: `INFO: Element name collision detected. Adding suffix to affected type category.`,
40
- SPECIFY_CONFIG: 'For --config option, please specify correct JSON Object.',
41
40
  INVALID_ANNOTATION: 'Annotation element must have a Term attribute',
41
+ DEPENDENCY_NOT_SUPPORTED: '--dependencies option is only supported for OData V4 files, not for other imports.',
42
42
  };
43
43
 
44
44
  function getMessages() {
@@ -0,0 +1,46 @@
1
+ // Singleton class to hold the parser context
2
+ const MultiMap = require("./util/MultiMap");
3
+
4
+ module.exports = class ParserContext {
5
+
6
+ static #instance = null;
7
+
8
+ static getInstance(mockServerUc) {
9
+ if (!ParserContext.#instance) {
10
+ ParserContext.#instance = new ParserContext(mockServerUc);
11
+ }
12
+ return ParserContext.#instance;
13
+ }
14
+
15
+ static resetInstance() {
16
+ ParserContext.#instance = null;
17
+ }
18
+
19
+ constructor(mockServerUc) {
20
+ this.#initialize(mockServerUc);
21
+ }
22
+
23
+ #initialize(mockServerUc) {
24
+ this.unboundActions = {};
25
+ this.unboundFunctions = {};
26
+ this.entityTypes = [];
27
+ this.entityTypeOpenEntries = [];
28
+ this.complexTypeOpenEntries = [];
29
+ this.complexTypes = [];
30
+ this.enumTypes = [];
31
+ this.typeDefinitions = [];
32
+ this.entityContainerName = "";
33
+ this.entityToEntitySetMap = new MultiMap();
34
+ this.entityToSingletonMap = new MultiMap();
35
+ this.actionToActionImportMap = new MultiMap();
36
+ this.functionToFunctionImportMap = new MultiMap();
37
+ this.primarySchema = "";
38
+ this.schemaAliasToNamespace = {};
39
+ this.schemaToSchemaDataIndex = {};
40
+ this.vocabNamespaceToAlias = {};
41
+ this.entityToBoundActions = {};
42
+ this.entityToBoundFunctions = {};
43
+ this.entityContainerAnnotations = {};
44
+ this.mockServerUc = mockServerUc;
45
+ }
46
+ }
@@ -6,6 +6,8 @@ const messages = require("../message").getMessages();
6
6
  let common = require("../common");
7
7
  const { path } = require('../../cds').utils;
8
8
 
9
+ let namespaceFileObject = {};
10
+
9
11
  function getNamespaces(context) {
10
12
  if (context.include_namespaces === "*") context.include_all_namespaces = true;
11
13
  else if (typeof context.include_namespaces === 'string' && !context.include_namespaces.startsWith('-')) context.namespaces = context.include_namespaces.replace(/ /g, "").split(',');
@@ -73,9 +75,11 @@ async function edmx2csn(edmx, filename, context) {
73
75
  edmx = _convertEDMXWithMultipleSchemas(edmx, multipleSchemas, namespace);
74
76
  }
75
77
  let csn = await _generateCSN(edmx, false, true, context);
76
- const regex = /^[\p{Letter}\p{Nl}_]{1}[_\p{Letter}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}]{0,127}$/gu
77
- // Regex checks for this condition “A-B” must start with a letter or underscore, followed by at most 127 letters, underscores or digits in the fileName
78
+
78
79
  if (filename) {
80
+ // Regex checks for this condition “A-B” must start with a letter or underscore, followed by at most 127 letters, underscores or digits in the fileName
81
+ const regex = /^[\p{Letter}\p{Nl}_]{1}[_\p{Letter}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}]{0,127}$/gu
82
+
79
83
  /**
80
84
  * V4: <Schema Namespace="Full.Schema.Name" xmlns="http://docs.oasis-open.org/odata/ns/edm" Alias="aliasedValue">
81
85
  * V2: <Schema Namespace="Full.Schema.Name" xmlns="http://schemas.microsoft.com/ado/2008/09/edm" Alias="aliasedValue">
@@ -83,17 +87,22 @@ async function edmx2csn(edmx, filename, context) {
83
87
  * context.schemaNamespace is set to correct namespace value by the respective parsers
84
88
  */
85
89
  if (context.schemaNamespace) {
86
- namespace = context.schemaNamespace;
90
+ namespaceFileObject[context.schemaNamespace] = filename;
91
+ } else {
92
+ namespaceFileObject[namespace] = filename;
87
93
  }
88
- // Replace namespace by given filename in the csn file instead of edmx file
89
- const re = RegExp(`([("/])${namespace}([/").])`, 'g');
90
- let name = path.parse(filename).name;
91
- if (!(name.match(regex))) { // check if the filename is in the right format with the regex if not replace unaccepted characters with underscore (_)
92
- name = name.replace(/^[^a-z\p{Nl}_]/gui, '_');
93
- name = name.replace(/[^a-z0-9_\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}]/gui, '_');
94
+
95
+ for (const [namespace, filename] of Object.entries(namespaceFileObject)) {
96
+ // Replace namespace by given filename in the csn file instead of edmx file
97
+ const re = RegExp(`([("/])${namespace}([/").])`, 'g');
98
+ let name = path.parse(filename).name;
99
+ if (!(name.match(regex))) { // check if the filename is in the right format with the regex if not replace unaccepted characters with underscore (_)
100
+ name = name.replace(/^[^a-z\p{Nl}_]/gui, '_');
101
+ name = name.replace(/[^a-z0-9_\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}]/gui, '_');
102
+ }
103
+ const fn = `$1${name}$2`;
104
+ csn = csn.replace(re, fn);
94
105
  }
95
- const fn = `$1${name}$2`;
96
- csn = csn.replace(re, fn);
97
106
  }
98
107
  return JSON.parse(csn);
99
108
  }