@sap/cds-dk 7.7.2 → 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 (50) 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/version.js +54 -22
  7. package/lib/bind/userCdsrcJsonStorageProvider.js +1 -1
  8. package/lib/build/buildTaskFactory.js +2 -2
  9. package/lib/build/provider/hana/index.js +1 -1
  10. package/lib/build/provider/internalBuildTaskProvider.js +19 -5
  11. package/lib/build/provider/mtx/resourcesTarBuilder.js +16 -4
  12. package/lib/build/provider/mtx-extension/extensionCompilation.js +13 -2
  13. package/lib/build/provider/mtx-extension/index.js +6 -2
  14. package/lib/build/provider/nodejs/index.js +4 -4
  15. package/lib/build/util.js +1 -12
  16. package/lib/compile/openapi/index.js +2 -1
  17. package/lib/index.js +2 -2
  18. package/lib/init/constants.js +2 -1
  19. package/lib/init/index.js +9 -23
  20. package/lib/init/plugin.js +8 -0
  21. package/lib/init/registries/mta.js +6 -0
  22. package/lib/init/template/data/as-csv.js +60 -0
  23. package/lib/init/template/data/as-json.js +441 -0
  24. package/lib/init/template/data/index.js +115 -86
  25. package/lib/init/template/helm/files/content-deployment/Chart.yaml +1 -1
  26. package/lib/init/template/helm/files/content-deployment/templates/job.yaml +7 -0
  27. package/lib/init/template/helm/files/content-deployment/values.schema.json +17 -0
  28. package/lib/init/template/helm/files/content-deployment/values.yaml +6 -1
  29. package/lib/init/template/helm/files/service-instance/Chart.yaml +1 -1
  30. package/lib/init/template/helm/files/service-instance/templates/serviceinstance.yaml +4 -0
  31. package/lib/init/template/helm/files/service-instance/values.schema.json +17 -1
  32. package/lib/init/template/helm/files/service-instance/values.yaml +4 -1
  33. package/lib/init/template/helm/files/values.yaml.hbs +2 -2
  34. package/lib/init/template/helm/files/web-application/Chart.yaml +1 -1
  35. package/lib/init/template/helm/files/web-application/templates/api-rule.yaml +4 -0
  36. package/lib/init/template/helm/files/web-application/templates/network-policy.yaml +15 -0
  37. package/lib/init/template/helm/files/web-application/values.schema.json +41 -0
  38. package/lib/init/template/helm/files/web-application/values.yaml +19 -1
  39. package/lib/init/template/helm-unified-runtime/files/values.yaml.hbs +2 -2
  40. package/lib/init/template/helm-unified-runtime/index.js +9 -6
  41. package/lib/init/template/http/index.js +182 -0
  42. package/lib/init/template/kafka/files/mta.yaml.hbs +14 -0
  43. package/lib/init/template/kafka/files/package.json.hbs +22 -0
  44. package/lib/init/template/kafka/index.js +40 -0
  45. package/lib/init/template/lint/index.js +4 -4
  46. package/lib/init/template/mta/files/mta.yaml.hbs +4 -0
  47. package/lib/init/template/multitenancy/files/values.yaml.hbs +2 -2
  48. package/lib/util/cf.js +6 -2
  49. package/npm-shrinkwrap.json +13 -13
  50. package/package.json +1 -1
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() {
@@ -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
+ }
@@ -0,0 +1,441 @@
1
+ const { randomUUID } = require('node:crypto')
2
+ const { dirname } = require('node:path')
3
+ const cds = require('../../../cds')
4
+
5
+ let counter = Math.round(Math.random() * 10000) // Default random seed // Non-deterministic behaviour
6
+ const MAX_ASSOC_DEPTH = 1
7
+
8
+ module.exports = async (data, csn, numRecords=1, existingData) => {
9
+ // if no entity restrictions are given, use all entities
10
+ if (!data) data = csn.all('entity').reduce((all, e) => { all[e.name] = []; return all }, {})
11
+
12
+ if (!existingData)
13
+ existingData = await cds.deploy.resources(csn)
14
+
15
+ addSemantics(csn)
16
+
17
+ // sort all entities first that are referenced by others, so that dependant entities can be created after them
18
+ const { definitions: defs } = csn
19
+ const entityNames = Object.keys(data).sort((n1, n2) => {
20
+ const n1HasComp = Object.values(defs[n1].elements).some(el => el.type === 'cds.Composition')
21
+ const n2HasComp = Object.values(defs[n2].elements).some(el => el.type === 'cds.Composition')
22
+ if (n1HasComp && !n2HasComp) return -1
23
+ if (!n1HasComp && n2HasComp) return 1
24
+ return 0
25
+ })
26
+
27
+ for (const name of entityNames) {
28
+ if (!data[name] // removed on purpose because records are created inline through compositions
29
+ || data[name].length >= numRecords) { // already created by following associations
30
+ continue
31
+ }
32
+
33
+ const records = data[name] = Array(numRecords).fill({}).map(() => ({}))
34
+ const entity = defs[name]
35
+
36
+ const idNum = randomNumber()
37
+ for (let i = 0; i < numRecords; i++) {
38
+ fillRecord(records[i], idNum+i, entity, csn, data, null, 0, existingData)
39
+ }
40
+ }
41
+ return data
42
+ }
43
+
44
+ function fillRecord(record, recordID, entity, csn, data, parentEntity, depth, existingData) {
45
+ Object.defineProperty(record, '$id', {value: recordID, enumerable: false})
46
+
47
+ 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)
57
+ })
58
+ return record
59
+ }
60
+
61
+ function dataForField(element, entity, csn, record, data, parentEntity, depth, existingData) {
62
+ if (element.virtual || element['@Core.Computed'])
63
+ return
64
+ switch (element._type) {
65
+ case 'cds.Time':
66
+ return '22:59:53'
67
+
68
+ case 'cds.Timestamp': {
69
+ const range = {from: new Date('2000-01-01'), to: new Date('2024-01-01')}
70
+ return randomDateInRange(range).toISOString()
71
+ }
72
+ case 'cds.Date': {
73
+ const range = {from: new Date('2000-01-01'), to: new Date('2024-01-01')}
74
+ annotatedDateRange(element, range)
75
+ const generatedDate = randomDateInRange(range)
76
+ const date = generatedDate.toISOString().split('T', 1)[0]
77
+ return date
78
+ }
79
+
80
+ case 'cds.DateTime': {
81
+ const range = {from: new Date('2000-01-01'), to: new Date('2024-01-01')}
82
+ annotatedDateRange(element, range)
83
+ const generatedDate = randomDateInRange(range)
84
+ return generatedDate.toISOString()
85
+ }
86
+
87
+ case 'cds.LargeString':
88
+ case 'cds.String': {
89
+ if (element['@data.gen']?.startsWith('faker.')) {
90
+ return fakerValue(element['@data.gen'], csn)
91
+ }
92
+ if (element.enum) {
93
+ const enums = Object.entries(element.enum).map(([name, val]) => val.value ?? name)
94
+ return randomFromList(enums)
95
+ }
96
+ if (entity['@Communication.Contact.email']?.some(mail => mail.address?.['='] === element.name)
97
+ || element['@Communication.IsEmailAddress']
98
+ || element['@cds.on.insert']?.['='] === '$user'
99
+ || element['@cds.on.update']?.['='] === '$user') {
100
+ return randomEmailAddress(element.name, record['$id'], csn)
101
+ }
102
+
103
+ let str = `${element.name}-${record['$id']}`
104
+
105
+ if (element.key) {
106
+ str = `${entity.name.split('.').pop()}-${record['$id']}`
107
+ if (element.name === 'locale' && entity.name.endsWith('texts')) {
108
+ const record = randomFromExistingData(entity, csn, existingData)
109
+ if (record) return record.locale
110
+ return randomLanguage()
111
+ }
112
+ }
113
+ if (element['@assert.format']) {
114
+ str = randomStrForRegex(element['@assert.format'], csn)
115
+ }
116
+
117
+ if (element.length && str.length > element.length) {
118
+ str = str.slice(-element.length) // truncate to length
119
+ }
120
+ return str
121
+ }
122
+
123
+ case 'cds.UUID': {
124
+ // create a pseudo-random UUID based on the record ID
125
+ const idStr = '' + record['$id']
126
+ return idStr + randomUUID().slice(idStr.length)
127
+ }
128
+
129
+ case 'cds.Boolean':
130
+ return (record['$id'].toString(2).at(-1)) < 0.5
131
+
132
+ case 'cds.Int16':
133
+ case 'cds.Integer64':
134
+ case 'cds.Integer': {
135
+ if (element.key)
136
+ return record['$id']
137
+ if (element.enum) {
138
+ const enums = Object.values(element.enum).map((value) => value.val)
139
+ return parseInt(randomFromList(enums))
140
+ }
141
+ const range = {from: 0, to: 100}
142
+ annotatedIntRange(element, range)
143
+ return randomIntInRange(range)
144
+ }
145
+ case 'cds.Decimal': {
146
+ const range = {from: 0, to: 100}
147
+ annotatedIntRange(element, range)
148
+ return +(randomDecimalInRange(range).toFixed(element.scale ?? 2))
149
+ }
150
+ case 'cds.Double': {
151
+ const range = {from: 0, to: 100}
152
+ return randomDecimalInRange(range)
153
+ }
154
+ case 'cds.Composition': {
155
+ const target = csn.definitions[element.target]
156
+ if (!target)
157
+ return
158
+ delete data[target.name] // as data for the target is created inline below, no need to have it separately
159
+ if (target === parentEntity || depth > MAX_ASSOC_DEPTH)
160
+ return
161
+
162
+ const targetRecords = []
163
+ const targetRecordCount = element.is2many ? 2 : 1 // TODO: make this configurable
164
+ const targetRecordID = randomNumber()
165
+ for (let i = 0; i < targetRecordCount; i++) {
166
+ targetRecords.push(fillRecord({}, targetRecordID+i, target, csn, data, entity, depth+1, existingData))
167
+ }
168
+
169
+ const [ownKey, ownKeyStructured, targetKey] = findKeysInOnCondition(element, target)
170
+ if (ownKey && targetKey) {
171
+ const value = ownKeyStructured ? { [ownKey]: record[ownKey] } : record[ownKey]
172
+ targetRecords.forEach(targetRecord => {
173
+ // Set parent in target as non-enumerable. Json serialization doesn't need it, but csv does.
174
+ Object.defineProperty(targetRecord, targetKey, { value, enumerable: false })
175
+ })
176
+ }
177
+
178
+ // set the entire target record inline
179
+ return element.is2many ? targetRecords : targetRecords[0]
180
+ }
181
+ case 'cds.Association': {
182
+ if (element.is2many)
183
+ return
184
+ const target = csn.definitions[element.target]
185
+ if (!target || target === parentEntity || depth > MAX_ASSOC_DEPTH)
186
+ return
187
+
188
+ let targetRecord = randomFromExistingData(target, csn, existingData)
189
+ if (targetRecord) {
190
+ delete data[target.name] // indicate that existing data is used and no new data shall be created
191
+ delete data[target.name + '.texts'] // also delete associated texts entity
192
+ }
193
+ else {
194
+ let targetRecords = data[target.name]
195
+ if (!targetRecords && (element['@mandatory']) || element['@assert.target'])
196
+ targetRecords = data[target.name] = []
197
+ else if (!targetRecords) // target entity is not in the list of entities to be created
198
+ return
199
+
200
+ targetRecord = fillRecord({}, randomNumber(), target, csn, data, entity, depth, existingData)
201
+ targetRecords.push(targetRecord)
202
+ }
203
+
204
+ // TODO: handle foreign keys correctly
205
+ const assoc = {}
206
+ Object.keys(target.elements)
207
+ .filter(keyName => target.elements[keyName].key)
208
+ .forEach(keyName => assoc[keyName] = targetRecord[keyName])
209
+ return assoc
210
+ }
211
+ default: {
212
+ throw new Error(`Unknown type '${element.type}' in '${entity.name}:${element.name}'`)
213
+ }
214
+ }
215
+ }
216
+
217
+ function randomFromExistingData (entity, csn, existingData) {
218
+ const files = Object.entries(existingData)
219
+ .filter(([, name]) => matchesEntityOrProjection(name, entity, csn))
220
+ .map(([file]) => file)
221
+ if (files.length === 0) return
222
+
223
+ if (files[0].endsWith('.csv')) {
224
+ const content = cds.utils.fs.readFileSync(files[0]).toString()
225
+ const records = cds.parse.csv(content)
226
+ if (records?.length > 1) {
227
+ const header = records[0] // 0 is header line
228
+ const randomRecord = records.length === 2 ? records[1] : records[randomIntInRange({from: 1, to: records.length-1} )]
229
+ let result = {}
230
+ for (let i = 0; i < records[0].length; i++) {
231
+ result[header[i]] = randomRecord[i]
232
+ }
233
+ return result
234
+ }
235
+ }
236
+ 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) {
244
+ 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} )]
249
+ }
250
+ }
251
+ }
252
+
253
+ /* whether the given name matches the entity or one of its projections */
254
+ function matchesEntityOrProjection(name, entity, csn) {
255
+ if (name === entity?.name) return true
256
+ // follow simle projections
257
+ const p = entity?.query?.SELECT || entity?.projection
258
+ if (p?.from?.ref?.length === 1) {
259
+ if (p.from.ref[0] === name) return true
260
+ return matchesEntityOrProjection(name, csn.definitions[p.from.ref[0]], csn)
261
+ }
262
+ }
263
+
264
+ function findKeysInOnCondition(assocElement, target) {
265
+ const { on } = assocElement
266
+ let ownKey, targetKey, ownKeyStructured
267
+ if (on?.length === 3 && on[1] === '=') {
268
+ for (let { ref } of [on[0], on[2]]) {
269
+ if (ref.length > 1 && ref[0] === assocElement.name && target.elements[ref[1]]) {
270
+ targetKey = ref[1]
271
+ }
272
+ else if (ref[0] === '$self') {
273
+ ownKey = Object.keys(assocElement.parent.keys)[0] // TODO composite keys
274
+ ownKeyStructured = true
275
+ }
276
+ else if (assocElement.parent.elements[ref[0]]) {
277
+ ownKey = ref[0]
278
+ }
279
+ }
280
+ }
281
+ return [ownKey, ownKeyStructured, targetKey]
282
+ }
283
+
284
+ function annotatedDateRange(el, dateRange) {
285
+ if (el['@assert.range']?.length === 2) {
286
+ const dateFrom = new Date(el['@assert.range'][0])
287
+ const dateTo = new Date(el['@assert.range'][1])
288
+ if (dateRange.from < dateFrom) {
289
+ dateRange.from = dateFrom
290
+ }
291
+ if (dateRange.to > dateTo) {
292
+ dateRange.to = dateTo
293
+ }
294
+ }
295
+ }
296
+
297
+ function annotatedIntRange(el, intRange) {
298
+ if (el['@assert.range']?.length === 2) {
299
+ const rangeFrom = el['@assert.range'][0]
300
+ const rangeTo = el['@assert.range'][1]
301
+ if (Number.isInteger(rangeFrom) && intRange.from < rangeFrom) {
302
+ intRange.from = rangeFrom
303
+ }
304
+ if (Number.isInteger(rangeTo) && intRange.to > rangeTo) {
305
+ intRange.to = rangeTo
306
+ }
307
+ }
308
+ //TODO : decimalRange
309
+ }
310
+
311
+ const langList = ['ar', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja', 'ko', 'ms', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'th', 'tr', 'zh']
312
+ function randomLanguage() {
313
+ return randomFromList(langList)
314
+
315
+ }
316
+ function randomFromList(list) {
317
+ return list[randomNumber() % list.length]
318
+ }
319
+
320
+ function randomEmailAddress(firstName, num, csn) {
321
+ let addr = fakerValue('faker.internet.email', csn, false)
322
+ if (addr) return addr
323
+ const provider = randomFromList(['example.com', 'example.org', 'example.net'])
324
+ return `${firstName}.${num.toString(36)}@${provider}`
325
+ }
326
+
327
+ function randomStrForRegex(regexStr, csn) {
328
+ // TODO make 'randexp' a hard dependency when this code is in a plugin
329
+ const randexp = requirePackage('randexp', csn, regexStr)
330
+ if (!randexp) return `Install package 'randexp' to create data for regex ${regexStr}`
331
+ return randexp.randexp(regexStr)
332
+ }
333
+
334
+ function fakerValue(expr, csn, useFallback=true, ...args) {
335
+ // `en` bundle is way smaller than full faker. TODO use different Faker locales?
336
+ const Faker = requirePackage('@faker-js/faker/locale/en', csn)
337
+ if (!Faker && !useFallback) return
338
+ if (!Faker) return `Install package '@faker-js/faker' to create data for ${expr}`
339
+
340
+ let resultPart
341
+ let [, category, func] = expr.split('.')
342
+ const cat = Faker.faker[category]
343
+ if (typeof cat !== 'object') return `Unknown faker category ${category} in ${expr}`
344
+ const funcParts = func.split('::')
345
+ if (funcParts.length === 2) {
346
+ resultPart = funcParts.pop()
347
+ func = funcParts.pop()
348
+ }
349
+ const fn = cat[func]
350
+ if (typeof fn !== 'function') return `Unknown faker function ${func} in ${expr}`
351
+ if (category === 'internet' && func === 'email') {
352
+ // only allow domains that cannot be registered
353
+ const provider = randomFromList(['example.com', 'example.org', 'example.net'])
354
+ return fn({ provider })
355
+ }
356
+ let res = args ? fn(...args) : fn()
357
+ if (res && resultPart) {
358
+ return res[resultPart]
359
+ }
360
+ return res
361
+ }
362
+
363
+ const packages = {}
364
+ function requirePackage(id, csn) {
365
+ try {
366
+ if (!packages[id]) {
367
+ const pkg = resolvePackage(id, csn)
368
+ if (pkg)
369
+ packages[id] = require(pkg)
370
+ }
371
+ return packages[id]
372
+ } catch (err) {
373
+ if (err.code !== 'MODULE_NOT_FOUND') throw err
374
+ }
375
+ }
376
+
377
+ function resolvePackage(id, csn) {
378
+ try {
379
+ return require.resolve(id, { paths: [...csn.$sources.map(dirname), cds.root, __dirname] })
380
+ } catch (err) {
381
+ if (err.code !== 'MODULE_NOT_FOUND') throw err
382
+
383
+ }
384
+ }
385
+
386
+ // add semantics to well-known entities
387
+ function addSemantics(csn) {
388
+ if (resolvePackage('@faker-js/faker', csn)) { // only if faker is installed
389
+ csn.all('entity').forEach(entity => {
390
+ const { elements } = entity
391
+ if (entity.name === 'sap.common.Countries') {
392
+ elements.code['@data.gen'] = 'faker.location.countryCode'
393
+ elements.name['@data.gen'] = elements.descr['@data.gen'] = 'faker.location.country'
394
+ }
395
+ else if (entity.name === 'sap.common.Currencies') {
396
+ elements.code['@data.gen'] = 'faker.finance.currency::code'
397
+ elements.name['@data.gen'] = elements.descr['@data.gen'] = 'faker.finance.currency::name'
398
+ elements.symbol['@data.gen'] = 'faker.finance.currency::symbol'
399
+ }
400
+ else if (entity.name === 'sap.common.Timezones') {
401
+ elements.code['@data.gen'] = 'faker.location.timeZone'
402
+ elements.name['@data.gen'] = elements.descr['@data.gen'] = 'faker.location.timeZone'
403
+ }
404
+ })
405
+ }
406
+ }
407
+
408
+ function randomDateInRange(dateRange) {
409
+ const dateFrom = dateRange.from
410
+ const returnDate = new Date(dateFrom.getUTCFullYear(), dateFrom.getUTCMonth(), dateFrom.getUTCDate())//needed to avoid overwriting the original dateFrom when generating a random Date
411
+ const diffTime = dateRange.to.valueOf() - dateRange.from.valueOf();
412
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
413
+ const step = randomNumber() % diffDays
414
+ returnDate.setUTCDate(returnDate.getUTCDate() + step);
415
+ return returnDate
416
+ }
417
+
418
+ function randomDecimalInRange(decimalRange) {
419
+ const max = decimalRange.to
420
+ const min = decimalRange.from
421
+ const num = min + (randomNumber() % (max - min))
422
+ let dec = randomNumber()
423
+ const divider = Math.pow(10, dec.toString().length)
424
+ dec = dec / divider
425
+ const returnValue = num + dec
426
+ return returnValue
427
+ }
428
+
429
+ function randomIntInRange(intRange) {
430
+ const max = intRange.to
431
+ const min = intRange.from
432
+ return min + (randomNumber() % max)
433
+ }
434
+
435
+ const p = 5651;
436
+ const q = 5623;
437
+ const M = p * q;
438
+ function randomNumber() {
439
+ counter = (counter * counter) % M
440
+ return counter;
441
+ }