@sap/cds-dk 7.8.2 → 7.9.2

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 (78) hide show
  1. package/bin/add.js +28 -7
  2. package/bin/bind.js +64 -21
  3. package/bin/build.js +35 -3
  4. package/bin/cds.js +31 -13
  5. package/bin/compile/index.js +44 -3
  6. package/bin/completion/completionFs.js +61 -0
  7. package/bin/completion/index.js +212 -0
  8. package/bin/completion/scripts/cds-bash.sh +17 -0
  9. package/bin/completion/scripts/cds-ps.ps1 +18 -0
  10. package/bin/completion/scripts/cds-zsh.sh +12 -0
  11. package/bin/help.js +8 -2
  12. package/bin/init.js +34 -4
  13. package/bin/subscribe.js +5 -1
  14. package/bin/watch.js +4 -2
  15. package/lib/bind/KubernetesApiAccessor.js +3 -2
  16. package/lib/bind/bindingResolver.js +8 -4
  17. package/lib/bind/bindingUtil.js +10 -8
  18. package/lib/bind/cfKeyProvider.js +58 -153
  19. package/lib/bind/index.js +49 -36
  20. package/lib/bind/kubernetesCredentialsProvider.js +6 -5
  21. package/lib/bind/userCdsrcJsonStorageProvider.js +25 -3
  22. package/lib/build/buildTaskEngine.js +1 -1
  23. package/lib/build/provider/hana/index.js +1 -1
  24. package/lib/build/provider/hana/template/undeploy.json +3 -1
  25. package/lib/build/provider/nodejs/index.js +1 -1
  26. package/lib/compile/openapi/index.js +5 -0
  27. package/lib/deploy/to-hana/hana.js +25 -7
  28. package/lib/env/schemas/cds-rc.js +29 -2
  29. package/lib/import/odata/v4parser.js +78 -25
  30. package/lib/init/constants.js +2 -1
  31. package/lib/init/index.js +18 -13
  32. package/lib/init/merge.js +1 -0
  33. package/lib/init/mvn.js +8 -47
  34. package/lib/init/plugin.js +0 -4
  35. package/lib/init/projectReader.js +17 -10
  36. package/lib/init/template/approuter/files/values.yaml.hbs +0 -12
  37. package/lib/init/template/completion/completionSetup.js +184 -0
  38. package/lib/init/template/completion/index.js +22 -0
  39. package/lib/init/template/data/as-csv.js +25 -17
  40. package/lib/init/template/data/as-json.js +148 -44
  41. package/lib/init/template/data/index.js +28 -15
  42. package/lib/init/template/hana/files/undeploy.json +2 -1
  43. package/lib/init/template/helm/files/chart/values.schema.json +1 -1
  44. package/lib/init/template/helm/files/content-deployment/Chart.yaml +0 -2
  45. package/lib/init/template/helm/files/content-deployment/values.schema.json +0 -1
  46. package/lib/init/template/helm/files/service-instance/Chart.yaml +0 -2
  47. package/lib/init/template/helm/files/service-instance/values.schema.json +0 -1
  48. package/lib/init/template/helm/files/web-application/Chart.yaml +0 -2
  49. package/lib/init/template/helm/files/web-application/values.schema.json +0 -1
  50. package/lib/init/template/helm-unified-runtime/files/values.schema.json +1 -1
  51. package/lib/init/template/http/index.js +150 -104
  52. package/lib/init/template/kafka/index.js +6 -0
  53. package/lib/init/template/lint/index.js +12 -13
  54. package/lib/init/template/mta/files/mta.yaml.hbs +2 -2
  55. package/lib/init/template/multitenancy/files/values.yaml.hbs +0 -5
  56. package/lib/init/template/multitenancy/index.js +13 -5
  57. package/lib/init/template/nodejs/files/.vscode/settings.json +11 -0
  58. package/lib/init/template/nodejs/files/eslint.config.js +13 -0
  59. package/lib/init/template/nodejs/files/package.json.hbs +3 -1
  60. package/lib/init/template/sample/index.js +18 -20
  61. package/lib/init/template/tiny-sample/index.js +1 -2
  62. package/lib/init/template/typer/index.js +2 -2
  63. package/lib/lint/Linter.js +133 -55
  64. package/lib/lint/checks.js +5 -6
  65. package/lib/lint/io.js +119 -10
  66. package/lib/mtx/auth_manager.js +5 -5
  67. package/lib/mtx/params.js +4 -2
  68. package/lib/mtx/settings_manager.js +1 -1
  69. package/lib/mtx/tenant_action.js +12 -2
  70. package/lib/mtx/util/logging.js +10 -4
  71. package/lib/util/axios.js +2 -1
  72. package/lib/util/cf.js +32 -37
  73. package/lib/util/git.js +1 -1
  74. package/lib/util/term.js +1 -1
  75. package/{bin → lib/watch}/watched.js +2 -2
  76. package/npm-shrinkwrap.json +35 -52
  77. package/package.json +2 -2
  78. package/lib/init/template/nodejs/files/.eslintrc +0 -26
@@ -0,0 +1,184 @@
1
+
2
+ const cp = require('node:child_process');
3
+ const fsp = require('node:fs/promises');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const { bold } = require('../../../util/term');
8
+
9
+ const ENTRY_REGEX = /\n{0,2}# cds completion start(.+)# cds completion end/is;
10
+
11
+
12
+ module.exports = new class CompletionSetup {
13
+ async setup(addEntry, options) {
14
+ const shell = options.shell
15
+ || (process.env.MSYSTEM?.includes('MINGW64') ? 'gitbash' : process.env.SHELL?.match(/(bash|zsh)/)?.[0])
16
+ || (process.env.PSModulePath ? 'ps' : null);
17
+
18
+ switch (shell) {
19
+ case 'bash':
20
+ case 'gitbash':
21
+ case 'zsh':
22
+ return this.setupBashZsh(addEntry, shell);
23
+ case 'ps':
24
+ return this.setupPs(addEntry);
25
+ case null:
26
+ try {
27
+ const shellType = this.determineShellLinux();
28
+ if (!shellType) {
29
+ throw `Unsupported shell.\nSupported shells are: bash, zsh, gitbash, ps.`;
30
+ }
31
+ return this.setupBashZsh(addEntry, shellType);
32
+ } catch (err) {
33
+ throw `Error during completion setup: ${err}`;
34
+ }
35
+ default:
36
+ throw `Unsupported shell: ${shell}.\nSupported shells are: bash, zsh, gitbash, ps.`;
37
+ }
38
+ }
39
+
40
+ getProfilePathWindows() {
41
+ const supportedShells = ['pwsh.exe', 'powershell.exe'];
42
+ try {
43
+ const parentPID = process.ppid;
44
+ const parentProcessDetails = cp.spawnSync('tasklist', ['/fi', `PID eq ${parentPID}`, '/nh', '/fo', 'csv'], { encoding: 'utf8' }).stdout.trim();
45
+ const parentProcess = parentProcessDetails.split(',')[0].replace(/"/g, '').trim();
46
+ if (!supportedShells.includes(parentProcess)) {
47
+ throw `Shell '${parentProcess}' is not supported. Make sure you are using PowerShell.`;
48
+ }
49
+
50
+ return cp.spawnSync(parentProcess, ['-NoProfile', '-c', 'echo $PROFILE'], { encoding: 'utf8' }).stdout.trim();
51
+ } catch (err) {
52
+ throw `Error during getting Windows shell profile path:\n${err}`;
53
+ }
54
+ }
55
+
56
+ determineShellLinux() {
57
+ try {
58
+ const parentPID = process.ppid;
59
+ return cp.spawnSync('ps', ['--pid', parentPID, '-ocomm='], { encoding: 'utf8' }).stdout.trim();
60
+ }
61
+ catch (err) {
62
+ throw `Error during getting Linux parent shell: ${err}`;
63
+ }
64
+ }
65
+
66
+ async createBackup(fileName) {
67
+ await fsp.copyFile(fileName, fileName + '.cds.bak');
68
+ }
69
+
70
+ async setupBashZsh(addEntry, shell) {
71
+ const isBash = shell.includes('bash'); // also for gitbash
72
+ const isZsh = shell.includes('zsh');
73
+ const isGitBash = shell.includes('gitbash');
74
+
75
+ if (!isBash && !isZsh) {
76
+ throw 'Completion is only supported for bash, zsh or Git Bash shell.';
77
+ }
78
+
79
+ const profileFile = path.join(
80
+ os.homedir(),
81
+ isZsh
82
+ ? '.zshrc'
83
+ : os.platform() === 'darwin' || isGitBash
84
+ ? '.bash_profile'
85
+ : '.bashrc'
86
+ );
87
+
88
+ await this.changeProfile(profileFile, shell, addEntry);
89
+
90
+ const relProfilePath = path.relative(os.homedir(), profileFile);
91
+ console.log(`\nSource your shell using\n${bold(`source ~/${relProfilePath}`)}\nto activate the changes.\n`);
92
+ }
93
+
94
+ async setupPs(addEntry) {
95
+ const profilePath = this.getProfilePathWindows();
96
+
97
+ try {
98
+ await fsp.access(profilePath);
99
+ } catch (err) {
100
+ if (err.code === 'ENOENT') {
101
+ throw `Profile file ${bold(profilePath)} does not exist.\nMake sure you are using PowerShell. CMD shell is not supported.`
102
+ }
103
+ throw err;
104
+ }
105
+
106
+ await this.changeProfile(profilePath, 'ps', addEntry);
107
+ console.log(`\nSource your shell using\n${bold('. $PROFILE')}\nto activate the changes.`);
108
+ }
109
+
110
+ getScript(shell) {
111
+ switch (shell) {
112
+ case 'ps':
113
+ return `
114
+ try {
115
+ $CDS_PROFILE = cds completion --shell ps --profile 2> $null
116
+ if (Test-Path -Path "$CDS_PROFILE" -PathType Leaf) {
117
+ . $CDS_PROFILE
118
+ }
119
+ } catch {}
120
+ `
121
+ case 'bash':
122
+ case 'gitbash':
123
+ return `
124
+ CDS_PROFILE=$(cds completion --shell ${shell} --profile 2> /dev/null) || CDS_PROFILE=""
125
+ if [ -r "$CDS_PROFILE" ]; then
126
+ . "$CDS_PROFILE"
127
+ fi
128
+ `
129
+ case 'zsh':
130
+ return `
131
+ # compinit can be removed here if loaded elsewhere in the script
132
+ autoload -Uz compinit && compinit
133
+
134
+ CDS_PROFILE=$(cds completion --shell zsh --profile 2> /dev/null) || CDS_PROFILE=""
135
+ if [ -r "$CDS_PROFILE" ]; then
136
+ . "$CDS_PROFILE"
137
+ fi
138
+ `
139
+ default: throw `Unsupported shell: ${shell}`;
140
+ }
141
+ }
142
+
143
+ async changeProfile(profileFile, shell, addEntry) {
144
+ let profile = '';
145
+ let profileFileExists = true;
146
+ try {
147
+ profile = await fsp.readFile(profileFile, 'utf8');
148
+ } catch (err) {
149
+ profileFileExists = false;
150
+ await fsp.mkdir(path.dirname(profileFile), { recursive: true });
151
+ }
152
+
153
+ let changed;
154
+ if (profile.match(ENTRY_REGEX)) {
155
+ profile = profile.replace(ENTRY_REGEX, '');
156
+ changed = true;
157
+ }
158
+
159
+ if (addEntry) {
160
+ profile = profile + `
161
+
162
+ # cds completion start
163
+ ${this.getScript(shell)}
164
+ # cds completion end`;
165
+ changed = true;
166
+ }
167
+
168
+ console.log();
169
+
170
+ if (changed) {
171
+ const relProfilePath = path.relative(os.homedir(), profileFile);
172
+ if (profileFileExists) {
173
+ await this.createBackup(profileFile);
174
+ console.log(`Created backup ${bold(`~/${relProfilePath}.cds.bak`)}.`);
175
+ }
176
+ await fsp.writeFile(profileFile, profile);
177
+ if (addEntry) {
178
+ console.log(`Added entry to ${bold(`~/${relProfilePath}`)}.`);
179
+ } else {
180
+ console.log(`Removed entry from ${bold(`~/${relProfilePath}`)}.`);
181
+ }
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,22 @@
1
+ const cds = require('../../../cds');
2
+
3
+
4
+ module.exports = class CompletionTemplate extends require('../../plugin') {
5
+ options() {
6
+ return {
7
+ 'shell': {
8
+ type: 'string',
9
+ short: 's',
10
+ help: `<optional> Forces completion setup for a given shell and disables auto detection.
11
+ Usually the shell is determined automatically and this is only for cases where the automatic
12
+ detection fails. Valid values: bash, gitbash, zsh, ps`
13
+ }
14
+ }
15
+ }
16
+
17
+ async run() {
18
+ const completionSetup = require('./completionSetup');
19
+ await completionSetup.setup(true, cds.cli.options);
20
+ process.exit(0);
21
+ }
22
+ }
@@ -1,12 +1,13 @@
1
- // const cds = require('../../../cds')
1
+ const { elementsSorter } = require('./as-json')
2
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
- }
3
+ /**
4
+ * Serializes the given JSON data to CSV format
5
+ * @param {object} data JSON data to serialize
6
+ * @param {object} csn the model
7
+ * @param {{ headerOnly: boolean, separator: boolean }} options
8
+ * @returns {Promise<string>} CSV string
9
+ */
10
+ module.exports = async (data={}, csn, { headerOnly=false, separator=',' }={}) => {
10
11
 
11
12
  // We might need multiple passes if data for associations/compositions is created on the fly.
12
13
  // So loop as long as there is some non-string data left.
@@ -20,7 +21,8 @@ module.exports = async (data={}, csn, { headerOnly=false, queries=false, separat
20
21
  const records = []
21
22
  for (let i = 0; i < entityData.length; i++) {
22
23
  let record = []
23
- for (const element of Object.values(entity.elements).sort(keyFirstSorter)) {
24
+ const elements = Object.values(entity.elements).sort(elementsSorter(csn))
25
+ for (const element of elements) {
24
26
  const name = element.name
25
27
  let value = entityData[i][name]
26
28
  if (value === undefined)
@@ -29,6 +31,9 @@ module.exports = async (data={}, csn, { headerOnly=false, queries=false, separat
29
31
  data[element.target] = value // Add object for a new entity. Next pass will process it.
30
32
  }
31
33
  else if (element._type === 'cds.Association') {
34
+ if (element.on) {
35
+ continue // skip unmanaged associations with explicit on condition
36
+ }
32
37
  if (i === 0) {
33
38
  header.push(...Object.keys(value).map(foreignKey => name + '_' + foreignKey))
34
39
  }
@@ -36,8 +41,10 @@ module.exports = async (data={}, csn, { headerOnly=false, queries=false, separat
36
41
  }
37
42
  else {
38
43
  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
44
+ if (Array.isArray(value)) { // `array of` / `many`
45
+ value = JSON.stringify(value)
46
+ }
47
+ value = escape(value)
41
48
  record.push(value)
42
49
  }
43
50
  }
@@ -48,13 +55,14 @@ module.exports = async (data={}, csn, { headerOnly=false, queries=false, separat
48
55
  res += '\n' + records.join('\n')
49
56
 
50
57
  data[entity.name] = res
58
+
59
+ function escape(value) {
60
+ if (typeof value === 'string' && (value.includes('"') || value.includes(separator))) {
61
+ return `"${value.replace(/"/g, '""')}"` // escape double quotes, then quote the value
62
+ }
63
+ return value
64
+ }
51
65
  })
52
66
  }
53
67
  return data
54
68
  }
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
- }
@@ -5,14 +5,23 @@ const cds = require('../../../cds')
5
5
  let counter = Math.round(Math.random() * 10000) // Default random seed // Non-deterministic behaviour
6
6
  const MAX_ASSOC_DEPTH = 1
7
7
 
8
- module.exports = async (data, csn, numRecords=1, existingData) => {
8
+ /**
9
+ * Creates JSON test data for the given entities
10
+ *
11
+ * @param {Record<string, object[]>|undefined} data the data with entities and their records to be created
12
+ * @param {object} csn the model
13
+ * @param {Number} numRecords the number of records to be created for each entity
14
+ * @param {{referenceData? : Record<string, string}} options misc. options
15
+ * @returns {Record<string, object[]>} the filled data
16
+ */
17
+ const asJson = module.exports = async (data, csn, numRecords=1, options={}) => {
18
+ let { referenceData } = options
19
+
9
20
  // if no entity restrictions are given, use all entities
10
21
  if (!data) data = csn.all('entity').reduce((all, e) => { all[e.name] = []; return all }, {})
11
22
 
12
- if (!existingData)
13
- existingData = await cds.deploy.resources(csn)
14
-
15
- addSemantics(csn)
23
+ // if no reference data is given, scan for it using cds.deploy.resources
24
+ if (!referenceData) referenceData = await cds.deploy.resources(csn)
16
25
 
17
26
  // sort all entities first that are referenced by others, so that dependant entities can be created after them
18
27
  const { definitions: defs } = csn
@@ -24,6 +33,11 @@ module.exports = async (data, csn, numRecords=1, existingData) => {
24
33
  return 0
25
34
  })
26
35
 
36
+ // requested entities shall always be created, so remove it from reference data
37
+ Object.entries(referenceData).filter(([, name]) => entityNames.includes(name)).forEach(([file]) => delete referenceData[file])
38
+
39
+ addSemantics(csn)
40
+
27
41
  for (const name of entityNames) {
28
42
  if (!data[name] // removed on purpose because records are created inline through compositions
29
43
  || data[name].length >= numRecords) { // already created by following associations
@@ -35,33 +49,63 @@ module.exports = async (data, csn, numRecords=1, existingData) => {
35
49
 
36
50
  const idNum = randomNumber()
37
51
  for (let i = 0; i < numRecords; i++) {
38
- fillRecord(records[i], idNum+i, entity, csn, data, null, 0, existingData)
52
+ fillRecord(records[i], idNum+i, entity, csn, data, null, 0, referenceData)
39
53
  }
54
+ // Remove records w/ duplicate keys. This can happen if the key is an enum (and we have less enum values than requested records).
55
+ data[name] = uniqRecords(records, entity)
40
56
  }
41
57
  return data
42
58
  }
43
59
 
44
- function fillRecord(record, recordID, entity, csn, data, parentEntity, depth, existingData) {
60
+ function fillRecord(record, recordID, entity, csn, data, parentEntity, depth, referenceData) {
45
61
  Object.defineProperty(record, '$id', {value: recordID, enumerable: false})
46
62
 
47
63
  const { elements } = entity
48
-
49
- // fill in the keys first, to have them ready for foreign key refs
50
- const keys = Object.keys(entity.keys||{})
51
- for (const key of keys) {
52
- record[key] = dataForField(elements[key], entity, csn, record, data, parentEntity, depth, existingData)
53
- }
54
- // non-key elements
55
- Object.keys(elements).filter(name => !keys.includes(name)).forEach(name => {
56
- record[name] = dataForField(elements[name], entity, csn, record, data, parentEntity, depth, existingData)
64
+ Object.values(elements).sort(asJson.elementsSorter(csn)).forEach(el => {
65
+ record[el.name] = dataForField(el, entity, csn, record, data, parentEntity, depth, referenceData)
57
66
  })
58
67
  return record
59
68
  }
60
69
 
61
- function dataForField(element, entity, csn, record, data, parentEntity, depth, existingData) {
62
- if (element.virtual || element['@Core.Computed'])
70
+ asJson.elementsSorter = function (csn) {
71
+ return (el1, el2) => {
72
+ // keys first, need to have them ready first for foreign key resolution anyway
73
+ if ( el1.key && !el2.key) return -1
74
+ if (!el1.key && el2.key) return 1
75
+ // own elements first
76
+ if ( isOwnElement(el1, el1.parent) && !isOwnElement(el2, el2.parent)) return -1
77
+ if (!isOwnElement(el1, el1.parent) && isOwnElement(el2, el2.parent)) return 1
78
+ return 0
79
+ }
80
+ function isOwnElement(el, entity) {
81
+ if (entity && entity === el.parent && (!entity.includes || entity.includes.length === 0)) {
82
+ // shortcut if element's entity has no includes
83
+ return true
84
+ }
85
+ // look for elements in entity includes/parents
86
+ for (const inc of entity?.includes||[]) {
87
+ if (csn.definitions[inc]?.elements?.[el.name]) return false
88
+ if (!isOwnElement(el, csn.definitions[inc])) return false
89
+ }
90
+ return true
91
+ }
92
+ }
93
+
94
+
95
+ function dataForField(element, entity, csn, record, data, parentEntity, depth, referenceData) {
96
+ if (element.virtual || (element['@Core.Computed'] && element.value))
63
97
  return
64
- switch (element._type) {
98
+ if (record[element.name] !== undefined)
99
+ return record[element.name] // value is already set by previous run, e.g. CodeList.code sets .name and .descr
100
+
101
+ if (element.items?.elements) // `array of` / `many` with complex type
102
+ return [ fillRecord({}, record['$id'], element.items, csn, data, null, depth+1, referenceData) ]
103
+ else if (element.items?.type) // `array of` / `many` with simple type
104
+ return [ dataForField(element.items, entity, csn, record, data, null, depth+1, referenceData) ]
105
+
106
+ const type = element._type || element.type
107
+ switch (type) {
108
+ case undefined: return
65
109
  case 'cds.Time':
66
110
  return '22:59:53'
67
111
 
@@ -84,14 +128,22 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
84
128
  return generatedDate.toISOString()
85
129
  }
86
130
 
131
+ case 'cds.hana.CHAR':
132
+ case 'cds.hana.CLOB':
133
+ case 'cds.hana.NCHAR':
134
+ case 'cds.hana.VARCHAR':
87
135
  case 'cds.LargeString':
88
136
  case 'cds.String': {
89
137
  if (element['@data.gen']?.startsWith('faker.')) {
90
138
  return fakerValue(element['@data.gen'], csn)
91
139
  }
92
140
  if (element.enum) {
93
- const enums = Object.entries(element.enum).map(([name, val]) => val.value ?? name)
94
- return randomFromList(enums)
141
+ const enumName = randomFromList(Object.keys(element.enum))
142
+ const value = element.enum[enumName].val ?? element.enum[enumName].value ?? enumName
143
+ // key of common.CodeList, also fill name and descr with enum name
144
+ if (element.key && entity.elements.name && entity.elements.descr)
145
+ record.name = record.descr = enumName
146
+ return value
95
147
  }
96
148
  if (entity['@Communication.Contact.email']?.some(mail => mail.address?.['='] === element.name)
97
149
  || element['@Communication.IsEmailAddress']
@@ -105,7 +157,7 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
105
157
  if (element.key) {
106
158
  str = `${entity.name.split('.').pop()}-${record['$id']}`
107
159
  if (element.name === 'locale' && entity.name.endsWith('texts')) {
108
- const record = randomFromExistingData(entity, csn, existingData)
160
+ const record = randomFromReferenceData(entity, csn, referenceData)
109
161
  if (record) return record.locale
110
162
  return randomLanguage()
111
163
  }
@@ -129,19 +181,26 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
129
181
  case 'cds.Boolean':
130
182
  return (record['$id'].toString(2).at(-1)) < 0.5
131
183
 
184
+ case 'cds.hana.SMALLINT':
185
+ case 'cds.hana.TINYINT':
186
+ case 'cds.UInt8':
132
187
  case 'cds.Int16':
188
+ case 'cds.Int32':
189
+ case 'cds.Int64':
133
190
  case 'cds.Integer64':
134
191
  case 'cds.Integer': {
135
192
  if (element.key)
136
193
  return record['$id']
137
194
  if (element.enum) {
138
- const enums = Object.values(element.enum).map((value) => value.val)
139
- return parseInt(randomFromList(enums))
195
+ const enumName = randomFromList(Object.keys(element.enum))
196
+ const value = element.enum[enumName].val ?? element.enum[enumName].value ?? enumName
197
+ return parseInt(value)
140
198
  }
141
199
  const range = {from: 0, to: 100}
142
200
  annotatedIntRange(element, range)
143
201
  return randomIntInRange(range)
144
202
  }
203
+ case 'cds.hana.REAL':
145
204
  case 'cds.Decimal': {
146
205
  const range = {from: 0, to: 100}
147
206
  annotatedIntRange(element, range)
@@ -163,7 +222,7 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
163
222
  const targetRecordCount = element.is2many ? 2 : 1 // TODO: make this configurable
164
223
  const targetRecordID = randomNumber()
165
224
  for (let i = 0; i < targetRecordCount; i++) {
166
- targetRecords.push(fillRecord({}, targetRecordID+i, target, csn, data, entity, depth+1, existingData))
225
+ targetRecords.push(fillRecord({}, targetRecordID+i, target, csn, data, entity, depth+1, referenceData))
167
226
  }
168
227
 
169
228
  const [ownKey, ownKeyStructured, targetKey] = findKeysInOnCondition(element, target)
@@ -174,6 +233,7 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
174
233
  Object.defineProperty(targetRecord, targetKey, { value, enumerable: false })
175
234
  })
176
235
  }
236
+ uniqRecords(targetRecords, target)
177
237
 
178
238
  // set the entire target record inline
179
239
  return element.is2many ? targetRecords : targetRecords[0]
@@ -185,7 +245,7 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
185
245
  if (!target || target === parentEntity || depth > MAX_ASSOC_DEPTH)
186
246
  return
187
247
 
188
- let targetRecord = randomFromExistingData(target, csn, existingData)
248
+ let targetRecord = randomFromReferenceData(target, csn, referenceData)
189
249
  if (targetRecord) {
190
250
  delete data[target.name] // indicate that existing data is used and no new data shall be created
191
251
  delete data[target.name + '.texts'] // also delete associated texts entity
@@ -197,8 +257,9 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
197
257
  else if (!targetRecords) // target entity is not in the list of entities to be created
198
258
  return
199
259
 
200
- targetRecord = fillRecord({}, randomNumber(), target, csn, data, entity, depth, existingData)
260
+ targetRecord = fillRecord({}, randomNumber(), target, csn, data, entity, depth, referenceData)
201
261
  targetRecords.push(targetRecord)
262
+ uniqRecords(targetRecords, target)
202
263
  }
203
264
 
204
265
  // TODO: handle foreign keys correctly
@@ -208,44 +269,64 @@ function dataForField(element, entity, csn, record, data, parentEntity, depth, e
208
269
  .forEach(keyName => assoc[keyName] = targetRecord[keyName])
209
270
  return assoc
210
271
  }
272
+
273
+ case 'cds.hana.BINARY':
274
+ case 'cds.Binary':
275
+ case 'cds.LargeBinary': // TODO add binary
276
+ return
277
+ case 'cds.Vector': // TODO add Vector
278
+ return
211
279
  default: {
212
- throw new Error(`Unknown type '${element.type}' in '${entity.name}:${element.name}'`)
280
+ console.error(`Unknown type ${element._type} (${element.type}) in '${entity.name}:${element.name}'`)
281
+ return
213
282
  }
214
283
  }
215
284
  }
216
285
 
217
- function randomFromExistingData (entity, csn, existingData) {
218
- const files = Object.entries(existingData)
286
+ function randomFromReferenceData (entity, csn, referenceData) {
287
+ const files = Object.entries(referenceData)
219
288
  .filter(([, name]) => matchesEntityOrProjection(name, entity, csn))
220
289
  .map(([file]) => file)
221
290
  if (files.length === 0) return
222
291
 
223
- if (files[0].endsWith('.csv')) {
224
- const content = cds.utils.fs.readFileSync(files[0]).toString()
292
+ const toCSV = (content) => {
225
293
  const records = cds.parse.csv(content)
226
294
  if (records?.length > 1) {
227
295
  const header = records[0] // 0 is header line
228
296
  const randomRecord = records.length === 2 ? records[1] : records[randomIntInRange({from: 1, to: records.length-1} )]
229
297
  let result = {}
230
298
  for (let i = 0; i < records[0].length; i++) {
231
- result[header[i]] = randomRecord[i]
299
+ // For csv, which is all strings, convert to correct JSON type.
300
+ // For simplicity, try to convert the value instead of dispatching on the type.
301
+ const val = toNum(randomRecord[i]) ?? randomRecord[i] // boolean is already covered by CSV parser
302
+ result[header[i]] = val
232
303
  }
233
304
  return result
234
305
  }
235
306
  }
307
+ const toJSON = (records) => {
308
+ if (records.length === 1)
309
+ return records[0]
310
+ else if (records.length > 1)
311
+ return records[randomIntInRange({from: 0, to: records.length-1} )]
312
+ }
313
+
314
+ if (files[0].endsWith('.csv')) {
315
+ const content = cds.utils.fs.readFileSync(files[0]).toString()
316
+ return toCSV(content)
317
+ }
318
+ else if (files[0].endsWith('.json')) {
319
+ const content = cds.utils.fs.readFileSync(files[0]).toString()
320
+ const records = JSON.parse(content)
321
+ return toJSON(records)
322
+ }
236
323
  else {
237
- let content
238
- if (files[0].endsWith('.json')) {
239
- content = cds.utils.fs.readFileSync(files[0]).toString()
240
- } else {
241
- content = files[0] // for tests, assuming the content is given inline
242
- }
243
- if (content) {
324
+ const content = files[0] // for tests, assuming the content is given inline
325
+ try {
244
326
  const records = JSON.parse(content)
245
- if (records.length === 1)
246
- return records[0]
247
- else if (records.length > 1)
248
- return records[randomIntInRange({from: 0, to: records.length-1} )]
327
+ return toJSON(records)
328
+ } catch {
329
+ return toCSV(content)
249
330
  }
250
331
  }
251
332
  }
@@ -281,6 +362,22 @@ function findKeysInOnCondition(assocElement, target) {
281
362
  return [ownKey, ownKeyStructured, targetKey]
282
363
  }
283
364
 
365
+ function uniqRecords(records, entity) {
366
+ const keys = Object.keys(entity.keys||{})
367
+ if (keys.length === 0) return records
368
+ const keySet = new Set()
369
+ for (let i = 0; i < records.length; i++) {
370
+ const key = keys.map(k => records[i][k]).join('|')
371
+ if (keySet.has(key)) {
372
+ // console.debug('Removing', keys, key, 'from', entity.name)
373
+ records.splice(i, 1)
374
+ i--
375
+ }
376
+ keySet.add(key)
377
+ }
378
+ return records
379
+ }
380
+
284
381
  function annotatedDateRange(el, dateRange) {
285
382
  if (el['@assert.range']?.length === 2) {
286
383
  const dateFrom = new Date(el['@assert.range'][0])
@@ -439,3 +536,10 @@ function randomNumber() {
439
536
  counter = (counter * counter) % M
440
537
  return counter;
441
538
  }
539
+
540
+ function toNum(str){
541
+ if (typeof(str) === 'number')
542
+ return str
543
+ if (typeof(str) === 'string')
544
+ return +str
545
+ }