@sap/cds-dk 7.7.1 → 7.8.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 (57) hide show
  1. package/bin/add.js +42 -17
  2. package/bin/bind.js +21 -12
  3. package/bin/build.js +14 -2
  4. package/bin/deploy/to-sqlite.js +1 -1
  5. package/bin/help.js +4 -1
  6. package/bin/import.js +5 -1
  7. package/bin/version.js +54 -22
  8. package/lib/bind/userCdsrcJsonStorageProvider.js +1 -1
  9. package/lib/build/buildTaskFactory.js +2 -2
  10. package/lib/build/provider/hana/index.js +1 -1
  11. package/lib/build/provider/internalBuildTaskProvider.js +19 -5
  12. package/lib/build/provider/java/index.js +4 -4
  13. package/lib/build/provider/mtx/resourcesTarBuilder.js +16 -4
  14. package/lib/build/provider/mtx-extension/extensionCompilation.js +13 -2
  15. package/lib/build/provider/mtx-extension/index.js +6 -2
  16. package/lib/build/provider/nodejs/index.js +4 -4
  17. package/lib/build/util.js +1 -12
  18. package/lib/compile/openapi/index.js +2 -1
  19. package/lib/deploy/to-hana/hana.js +19 -17
  20. package/lib/import/common.js +1 -1
  21. package/lib/import/importapi.js +1 -1
  22. package/lib/import/odata/v4parser.js +18 -11
  23. package/lib/index.js +2 -2
  24. package/lib/init/constants.js +2 -1
  25. package/lib/init/index.js +9 -23
  26. package/lib/init/mvn.js +1 -5
  27. package/lib/init/plugin.js +8 -0
  28. package/lib/init/registries/mta.js +6 -0
  29. package/lib/init/template/data/as-csv.js +60 -0
  30. package/lib/init/template/data/as-json.js +441 -0
  31. package/lib/init/template/data/index.js +115 -86
  32. package/lib/init/template/helm/files/content-deployment/Chart.yaml +1 -1
  33. package/lib/init/template/helm/files/content-deployment/templates/job.yaml +7 -0
  34. package/lib/init/template/helm/files/content-deployment/values.schema.json +17 -0
  35. package/lib/init/template/helm/files/content-deployment/values.yaml +6 -1
  36. package/lib/init/template/helm/files/service-instance/Chart.yaml +1 -1
  37. package/lib/init/template/helm/files/service-instance/templates/serviceinstance.yaml +4 -0
  38. package/lib/init/template/helm/files/service-instance/values.schema.json +17 -1
  39. package/lib/init/template/helm/files/service-instance/values.yaml +4 -1
  40. package/lib/init/template/helm/files/values.yaml.hbs +2 -2
  41. package/lib/init/template/helm/files/web-application/Chart.yaml +1 -1
  42. package/lib/init/template/helm/files/web-application/templates/api-rule.yaml +4 -0
  43. package/lib/init/template/helm/files/web-application/templates/network-policy.yaml +15 -0
  44. package/lib/init/template/helm/files/web-application/values.schema.json +41 -0
  45. package/lib/init/template/helm/files/web-application/values.yaml +19 -1
  46. package/lib/init/template/helm-unified-runtime/files/values.yaml.hbs +2 -2
  47. package/lib/init/template/helm-unified-runtime/index.js +9 -6
  48. package/lib/init/template/http/index.js +182 -0
  49. package/lib/init/template/kafka/files/mta.yaml.hbs +14 -0
  50. package/lib/init/template/kafka/files/package.json.hbs +22 -0
  51. package/lib/init/template/kafka/index.js +40 -0
  52. package/lib/init/template/lint/index.js +4 -4
  53. package/lib/init/template/mta/files/mta.yaml.hbs +4 -0
  54. package/lib/init/template/multitenancy/files/values.yaml.hbs +2 -2
  55. package/lib/util/cf.js +6 -2
  56. package/npm-shrinkwrap.json +300 -276
  57. package/package.json +1 -1
package/lib/build/util.js CHANGED
@@ -118,6 +118,7 @@ function resolveRequiredSapModels(modelPaths) {
118
118
  }
119
119
 
120
120
  async function getDefaultModelOptions(ws = false) {
121
+ // Note: requires.toggles holds the actual kind representation and not the value 'true'
121
122
  const wildcards = cds.env.requires.toggles && cds.env.features.folders ? ['*', cds.env.features.folders] : '*'
122
123
  let paths = _filterModelPaths(cds.resolve(wildcards, false))
123
124
 
@@ -165,17 +166,6 @@ function getI18nDefaultFolder() {
165
166
  return cds.env.i18n.folders[0] ?? 'i18n'
166
167
  }
167
168
 
168
- function isClassicMtx() {
169
- function _hasMtxDependency() {
170
- try {
171
- return !!require(path.join(cds.root, 'package.json'))?.dependencies?.['@sap/cds-mtx']
172
- } catch (e) {
173
- return false
174
- }
175
- }
176
- return /true/i.test(process.env.OLD_MTX) || _hasMtxDependency()
177
- }
178
-
179
169
  async function pathExists(path) {
180
170
  return fs.promises.access(path).then(() => true).catch(() => false);
181
171
  }
@@ -378,7 +368,6 @@ module.exports = {
378
368
  getDefaultModelOptions,
379
369
  flatten,
380
370
  getI18nDefaultFolder,
381
- isClassicMtx,
382
371
  normalizePath,
383
372
  pathExists,
384
373
  getWorkspaces,
@@ -3,7 +3,8 @@ const cds = require('../../cds')
3
3
 
4
4
  module.exports = function processor(csn, options = {}) {
5
5
  const edmOptions = Object.assign({
6
- odataOpenapiHints: true, // hint to compiler
6
+ odataOpenapiHints: true, // hint to cds-compiler
7
+ edm4OpenAPI: true, // downgrades certain OData errors to warnings in cds-compiler
7
8
  to: 'openapi' // hint to cds.compile.to.edm (usually set by CLI, but also do this in programmatic usages)
8
9
  }, options)
9
10
 
@@ -57,7 +57,7 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
57
57
 
58
58
  const fromFile = vcapFile && exists(vcapFile) ? await read(vcapFile) : {};
59
59
  const fromEnv = this.getFromEnv('VCAP_SERVICES', 'SERVICE_REPLACEMENTS');
60
- let vcapEnv = { ...fromFile, ...fromEnv };
60
+ let vcapEnv = { ...fromEnv, ...fromFile };
61
61
  if (process.env.TARGET_CONTAINER) {
62
62
  vcapEnv.TARGET_CONTAINER = process.env.TARGET_CONTAINER;
63
63
  }
@@ -82,30 +82,32 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
82
82
  }
83
83
 
84
84
  const hasVCAPEnv = Object.keys(vcapEnv).length > 0;
85
+ let serviceKeyName
85
86
  if (!serviceName && hasVCAPEnv) {
86
87
  await fs.mkdir(dest, { recursive: true });
87
- }
88
- const { cfServiceInstanceName, cfServiceInstanceKeyName, serviceKey } =
88
+ } else {
89
+ const { cfServiceInstanceName, cfServiceInstanceKeyName, serviceKey } =
89
90
  await this._getOrCreateCFService(projectPath, dest, serviceName, tunnelAddress);
90
91
 
91
- serviceName ??= cfServiceInstanceName;
92
- if (!vcapEnv.VCAP_SERVICES || serviceName) {
93
- vcapEnv.VCAP_SERVICES = this._getVCAPServicesEntry(cfServiceInstanceName, serviceKey);
94
- }
92
+ serviceName ??= cfServiceInstanceName;
93
+ serviceKeyName ??= cfServiceInstanceKeyName
94
+ if (!vcapEnv.VCAP_SERVICES || serviceName) {
95
+ vcapEnv.VCAP_SERVICES = this._getVCAPServicesEntry(cfServiceInstanceName, serviceKey);
96
+ }
95
97
 
96
- if (storeCredentials) {
97
- await this._addInstanceToDefaultEnvJson([dest, projectPath], cfServiceInstanceName, serviceKey);
98
+ if (storeCredentials) {
99
+ await this._addInstanceToDefaultEnvJson([dest, projectPath], cfServiceInstanceName, serviceKey);
100
+ }
98
101
  }
99
102
 
100
103
  console.log(`${bold('Starting deployment to SAP HANA ...')}`);
101
-
102
104
  DEBUG?.({ dest, vcapEnv, hdiOptions })
103
105
  await hdi.deploy(dest, vcapEnv, hdiOptions);
104
106
 
105
107
  if (!hasVCAPEnv) {
106
108
  const bindOptions = {
107
109
  ...options,
108
- to: `${serviceName}:${cfServiceInstanceKeyName}`,
110
+ to: `${serviceName}:${serviceKeyName}`,
109
111
  for: options.for,
110
112
  kind: task.for
111
113
  }
@@ -227,12 +229,12 @@ Therefore, specifying the CDS model is not required when calling 'cds deploy'.`;
227
229
 
228
230
  _getVCAPServicesEntry(serviceInstanceName, serviceKey) {
229
231
  return {
230
- hana: [
231
- {
232
- name: serviceInstanceName,
233
- tags: ['hana'],
234
- credentials: serviceKey
235
- }
232
+ hana: [
233
+ {
234
+ name: serviceInstanceName,
235
+ tags: ['hana'],
236
+ credentials: serviceKey
237
+ }
236
238
  ]
237
239
  };
238
240
  }
@@ -16,7 +16,7 @@ let optionsMapping = {
16
16
  'no-save': 'no_save'
17
17
  };
18
18
 
19
- let apiOptions = ['includeNamespaces', 'keepNamespace'];
19
+ let apiOptions = ['includeNamespaces', 'keepNamespace', 'beta'];
20
20
  let commonOptions = ['no_copy', 'no_save', 'out', 'dry', 'as', 'from', 'force', 'config'];
21
21
 
22
22
  async function generateEDMX2JSON(edmx) {
@@ -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 };
67
+ let odataOptions = { include_namespaces: options.includeNamespaces, keep_namespace: options.keepNamespace, beta: options.beta};
68
68
  if (cds.env.import) {
69
69
  common.readEnvVariables(odataOptions, false);
70
70
  }
@@ -82,7 +82,7 @@ const known_vocabularies = {
82
82
  'com.sap.vocabularies.UI.v1': 'UI',
83
83
  };
84
84
 
85
- function _initialize(parserContext, mockServerUc) {
85
+ function _initialize(parserContext) {
86
86
  parserContext.unboundedActions = {};
87
87
  parserContext.unboundedFunctions = {};
88
88
  parserContext.entityTypes = [];
@@ -103,7 +103,8 @@ function _initialize(parserContext, mockServerUc) {
103
103
  parserContext.entityToBoundedActions = {};
104
104
  parserContext.entityToBoundedFunctions = {};
105
105
  parserContext.entityContainerAnnotations = {};
106
- parserContext.mockServerUc = mockServerUc;
106
+ parserContext.mockServerUc = true;
107
+ parserContext.beta = false;
107
108
  }
108
109
  function _isJson(edmx) {
109
110
  let isJson = false;
@@ -290,7 +291,7 @@ function _replaceSpecialCharacters(text) {
290
291
  return text.replace(/(?:\\[rn]|[\r\n]+)+/gm, "\n").replace(/\s+/g, ' ').replace(/"/g, """).trim();
291
292
  }
292
293
 
293
- function _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObject) {
294
+ function _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObject, parserContext) {
294
295
  if (annotationTerm === "@Core.Description" || annotationTerm === "@Core.LongDescription") {
295
296
  // we don't capture `null` value for doc
296
297
  if (annotationTermObject === null) return;
@@ -313,9 +314,10 @@ function _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObj
313
314
  if (annotationTermObject === null) annotationTermObject = true;
314
315
 
315
316
  if (annotationTerm === "@Core.OptionalParameter") {
316
- _updateElementObjectForDefaultValue(csnDefsTargetObject, annotationTermObject.DefaultValue);
317
- }
318
- else {
317
+ if (parserContext.beta) {
318
+ _updateElementObjectForDefaultValue(csnDefsTargetObject, annotationTermObject.DefaultValue);
319
+ }
320
+ } else {
319
321
  _captureAnnotationInCSN(annotationTerm, annotationTermObject, csnDefsTargetObject);
320
322
  }
321
323
  }
@@ -403,13 +405,13 @@ function _parseValidationTerm(annotationTerm, annotationTermObject, csnDefsTarge
403
405
  _captureAnnotationInCSN(annotationTerm, annotationTermObject, csnDefsTargetObject);
404
406
  }
405
407
 
406
- function _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTargetObject) {
408
+ function _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTargetObject, parserContext) {
407
409
  const term = annotationTerm.split('.');
408
410
  const vocabAlias = term[0];
409
411
 
410
412
  switch (vocabAlias) {
411
413
  case "@Core":
412
- _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObject);
414
+ _resolveCoreTerm(annotationTerm, annotationTermObject, csnDefsTargetObject, parserContext);
413
415
  break;
414
416
  case "@Capabilities":
415
417
  _parseCapabilitiesTerm(annotationTerm, annotationTermObject, csnDefsTargetObject);
@@ -458,7 +460,7 @@ function _captureAnnotationWithinElement(sourceObject, targetObject, parserConte
458
460
  Object.keys(sourceObject).forEach(key => {
459
461
  if (key.startsWith('@')) {
460
462
  element = _resolveAnotationTerm(JSON.stringify(key), parserContext);
461
- _parseAnnotationTerms(element, sourceObject[key], targetObject);
463
+ _parseAnnotationTerms(element, sourceObject[key], targetObject, parserContext);
462
464
  }
463
465
  });
464
466
  }
@@ -1210,7 +1212,7 @@ function _parseAnnotations(schemaAnnotations, csnDefs, parserContext) {
1210
1212
  let annotationTerms = schemaAnnotations[annotationTargetPath];
1211
1213
  Object.keys(annotationTerms).forEach(annotationTerm => {
1212
1214
  let annotationTermObject = schemaAnnotations[annotationTargetPath][annotationTerm];
1213
- _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTargetObject);
1215
+ _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTargetObject, parserContext);
1214
1216
  });
1215
1217
  }
1216
1218
  });
@@ -1684,7 +1686,12 @@ async function getEdmxv4CSN(edmx, ignorePersistenceSkip, mockServerUc, importerC
1684
1686
  let name_space = [];
1685
1687
 
1686
1688
  const parserContext = {};
1687
- _initialize(parserContext, mockServerUc);
1689
+ _initialize(parserContext);
1690
+ parserContext.mockServerUc = mockServerUc;
1691
+
1692
+ if (importerContext.beta) {
1693
+ parserContext.beta = importerContext.beta
1694
+ }
1688
1695
 
1689
1696
  if (importerContext.include_all_namespaces) {
1690
1697
  name_space.push(true);
package/lib/index.js CHANGED
@@ -46,9 +46,9 @@ class CdsDkAdd {
46
46
  class CdsDkBuild {
47
47
  get build() { return super.build = require('./build') }
48
48
  }
49
- if (process.argv[2] in { 'add':1, 'init':1 }) {
49
+ if (process.argv[2] in { add:1, init:1, help:1 }) {
50
50
  cds.extend (cds.constructor) .with (CdsDkAdd)
51
- } else if (process.argv[2] in { 'build':1, 'deploy':1, 'push':1 }) {
51
+ } else if (process.argv[2] in { build:1, deploy:1, push:1 }) {
52
52
  cds.extend (cds.constructor) .with (CdsDkBuild)
53
53
  }
54
54
 
@@ -4,7 +4,7 @@ module.exports = {
4
4
  COMMAND_INIT: 'init',
5
5
  COMMAND_ADD: 'add',
6
6
 
7
- MAVEN_ARCHETYPE_VERSION: '2.7.0',
7
+ MAVEN_ARCHETYPE_VERSION: '2.7.1',
8
8
 
9
9
  OPTIONS: Object.freeze({
10
10
  JAVA: 'java',
@@ -35,6 +35,7 @@ module.exports = {
35
35
  ENTERPRISE_MESSAGING: 'enterprise-messaging',
36
36
  ENTERPRISE_MESSAGING_SHARED: 'enterprise-messaging-shared',
37
37
  REDIS_MESSAGING: 'redis-messaging',
38
+ KAFKA: 'kafka',
38
39
  APPROUTER: 'approuter',
39
40
  DESTINATION: 'destination',
40
41
  DATA: 'data',
package/lib/init/index.js CHANGED
@@ -100,11 +100,11 @@ module.exports = class CDSGenerator {
100
100
  }
101
101
 
102
102
  /**
103
- * @param {string} facets, comma separated list of templates, no blanks
103
+ * @param {string} facets, comma separated list of facets
104
104
  * @param {any} options, additional options
105
105
  */
106
- async addCmd(facets) {
107
- if (!facets) throw 'You must specify a facet to add to the project.'
106
+ async add(facets) {
107
+ if (!facets?.length) throw 'You must specify a facet to add to the project.'
108
108
 
109
109
  this._initialize(null);
110
110
  this._greetings();
@@ -113,7 +113,7 @@ module.exports = class CDSGenerator {
113
113
  throw `The current folder doesn't seem to contain a project. None of the following files found: ${PROJECT_FILES.join(', ')}.`
114
114
  }
115
115
 
116
- await this._process();
116
+ await this._process(facets);
117
117
  await this.stepEnd();
118
118
  }
119
119
 
@@ -170,11 +170,11 @@ module.exports = class CDSGenerator {
170
170
  }
171
171
  }
172
172
 
173
- async _process() {
173
+ async _process(facets) {
174
174
  DEBUG?.(`Project path: ${cds.root}`);
175
175
 
176
176
  await this._validateOptions();
177
- await this._fillTemplateList();
177
+ await this._fillTemplateList(facets);
178
178
 
179
179
  for (const template of this.templateList) {
180
180
  console.log(`Adding feature '${template.name}'...`);
@@ -216,11 +216,11 @@ module.exports = class CDSGenerator {
216
216
  }
217
217
  }
218
218
 
219
- async _fillTemplateList() {
219
+ async _fillTemplateList(facets) {
220
220
  const templates = new Map
221
- const { command, options, argv } = cds.cli
221
+ const { command, options } = cds.cli
222
222
 
223
- const plugins = command === COMMAND_INIT ? options.add : new Set(argv[0].split(','))
223
+ const plugins = command === COMMAND_INIT ? options.add : new Set(facets)
224
224
 
225
225
  for (let plugin of plugins) {
226
226
  // Compat
@@ -299,20 +299,6 @@ module.exports = class CDSGenerator {
299
299
  if (cds.cli.command === COMMAND_INIT && !options.add.has(JAVA)) {
300
300
  options.add.add(NODEJS);
301
301
  }
302
-
303
- this._checkAdditionalOptions();
304
- }
305
-
306
- _checkAdditionalOptions() {
307
- const { options } = cds.cli
308
- for (const [key] of Object.entries(options)) {
309
- const keySplit = key.split(/:/)
310
- if (keySplit.length === 2) {
311
- if (!options.add.has(keySplit[0])) {
312
- console.log(`ignoring option ${key} since ${keySplit[0]} is not part of specified feature list: ${Array.from(options.add).join(', ')}`)
313
- }
314
- }
315
- }
316
302
  }
317
303
 
318
304
  async _validateOptions() {
package/lib/init/mvn.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- const { URLS, MAVEN_ARCHETYPE_VERSION, OPTIONS: { JAVA_MVN, JAVA_PACKAGE, SAMPLES, HANA }, REGEX_JAVA_PACKAGE } = require('./constants');
2
+ const { URLS, MAVEN_ARCHETYPE_VERSION, OPTIONS: { JAVA_MVN, JAVA_PACKAGE, HANA }, REGEX_JAVA_PACKAGE } = require('./constants');
3
3
 
4
4
  const cp = require('child_process');
5
5
  const util = require('util');
@@ -143,10 +143,6 @@ class MvnArchetypeUtil {
143
143
  params.set(`-DtargetPlatform`, `cloudfoundry`);
144
144
  }
145
145
 
146
- const hasSamples = !!(options.add && options.add.has(SAMPLES));
147
- params.set(`-DincludeModel`, hasSamples);
148
- params.set(`-DincludeIntegrationTest`, hasSamples);
149
-
150
146
  if (options[JAVA_MVN]) {
151
147
  this._scanMvnParams(options[JAVA_MVN], params);
152
148
  }
@@ -10,6 +10,14 @@ module.exports = class Plugin {
10
10
  this.name = this.constructor.name.replace(/template/i, '').replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
11
11
  }
12
12
 
13
+ /**
14
+ * Provides command-line options for the plugin.
15
+ * @returns {Array.<{option: string, flag: string, shortcut?: string, help: string}>}
16
+ */
17
+ options() {
18
+ return []
19
+ }
20
+
13
21
  /**
14
22
  * Gets the required templates.
15
23
  * @returns {string[]} An array of strings representing required plugins.
@@ -32,6 +32,11 @@ const enterpriseMessaging = {
32
32
  where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'enterprise-messaging' }
33
33
  }
34
34
 
35
+ const kafkaMessaging = {
36
+ in: 'resources',
37
+ where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'kafka' }
38
+ }
39
+
35
40
  const applicationLogging = {
36
41
  in: 'resources',
37
42
  where: { type: 'org.cloudfoundry.managed-service', 'parameters.service': 'application-logs' }
@@ -144,6 +149,7 @@ module.exports = {
144
149
  destination,
145
150
  connectivity,
146
151
  enterpriseMessaging,
152
+ kafkaMessaging,
147
153
  hdbDeployer,
148
154
  hdiContainer,
149
155
  notificationTypesDeployer,
@@ -0,0 +1,60 @@
1
+ // const cds = require('../../../cds')
2
+
3
+ module.exports = async (data={}, csn, { headerOnly=false, queries=false, separator=',' }={}) => {
4
+ if (!queries) { // remove all projection-like entities
5
+ Object.keys(data)
6
+ .map(name => csn.definitions[name])
7
+ .filter (entity => entity.query)
8
+ .forEach(entity => { delete data[entity.name] })
9
+ }
10
+
11
+ // We might need multiple passes if data for associations/compositions is created on the fly.
12
+ // So loop as long as there is some non-string data left.
13
+ while (Object.values(data).some(d => typeof d === 'object')) {
14
+ Object.entries(data)
15
+ .filter(([_, value]) => typeof value === 'object')
16
+ .map(([name]) => csn.definitions[name])
17
+ .forEach(entity => {
18
+ const entityData = data[entity.name]
19
+ const header = []
20
+ const records = []
21
+ for (let i = 0; i < entityData.length; i++) {
22
+ let record = []
23
+ for (const element of Object.values(entity.elements).sort(keyFirstSorter)) {
24
+ const name = element.name
25
+ let value = entityData[i][name]
26
+ if (value === undefined)
27
+ continue
28
+ if (element._type === 'cds.Composition') {
29
+ data[element.target] = value // Add object for a new entity. Next pass will process it.
30
+ }
31
+ else if (element._type === 'cds.Association') {
32
+ if (i === 0) {
33
+ header.push(...Object.keys(value).map(foreignKey => name + '_' + foreignKey))
34
+ }
35
+ record.push(...Object.values(value))
36
+ }
37
+ else {
38
+ if (i === 0) header.push(name)
39
+ if (typeof value === 'string' && (value.includes('"') || value.includes(separator)))
40
+ value = `"${value.replace(/"/g, '""')}"` // escape double quotes, then quote the value
41
+ record.push(value)
42
+ }
43
+ }
44
+ records.push(record.join(separator))
45
+ }
46
+ let res = header.join(separator)
47
+ if (!headerOnly)
48
+ res += '\n' + records.join('\n')
49
+
50
+ data[entity.name] = res
51
+ })
52
+ }
53
+ return data
54
+ }
55
+
56
+ function keyFirstSorter(e1, e2) {
57
+ if (e1.key && !e2.key) return -1
58
+ if (!e1.key && e2.key) return 1
59
+ return 0 // preserve original order otherwise
60
+ }