@sap/cds-dk 7.7.2 → 7.8.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 (58) hide show
  1. package/bin/add.js +40 -17
  2. package/bin/bind.js +21 -12
  3. package/bin/build.js +14 -2
  4. package/bin/cds.js +8 -0
  5. package/bin/deploy/to-sqlite.js +1 -1
  6. package/bin/help.js +4 -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 +23 -11
  12. package/lib/build/provider/mtx/resourcesTarBuilder.js +16 -4
  13. package/lib/build/provider/mtx-extension/extensionCompilation.js +13 -2
  14. package/lib/build/provider/mtx-extension/index.js +6 -2
  15. package/lib/build/provider/nodejs/index.js +4 -4
  16. package/lib/build/util.js +7 -18
  17. package/lib/compile/openapi/index.js +2 -1
  18. package/lib/index.js +2 -2
  19. package/lib/init/constants.js +3 -2
  20. package/lib/init/index.js +9 -23
  21. package/lib/init/plugin.js +8 -0
  22. package/lib/init/registries/mta.js +6 -0
  23. package/lib/init/template/data/as-csv.js +60 -0
  24. package/lib/init/template/data/as-json.js +441 -0
  25. package/lib/init/template/data/index.js +116 -86
  26. package/lib/init/template/enterprise-messaging/files/package.json.hbs +2 -1
  27. package/lib/init/template/enterprise-messaging/index.js +16 -3
  28. package/lib/init/template/enterprise-messaging-shared/files/package.json.hbs +2 -1
  29. package/lib/init/template/enterprise-messaging-shared/index.js +16 -3
  30. package/lib/init/template/helm/files/content-deployment/Chart.yaml +1 -1
  31. package/lib/init/template/helm/files/content-deployment/templates/job.yaml +7 -0
  32. package/lib/init/template/helm/files/content-deployment/values.schema.json +17 -0
  33. package/lib/init/template/helm/files/content-deployment/values.yaml +6 -1
  34. package/lib/init/template/helm/files/service-instance/Chart.yaml +1 -1
  35. package/lib/init/template/helm/files/service-instance/templates/serviceinstance.yaml +4 -0
  36. package/lib/init/template/helm/files/service-instance/values.schema.json +17 -1
  37. package/lib/init/template/helm/files/service-instance/values.yaml +4 -1
  38. package/lib/init/template/helm/files/values.yaml.hbs +2 -2
  39. package/lib/init/template/helm/files/web-application/Chart.yaml +1 -1
  40. package/lib/init/template/helm/files/web-application/templates/api-rule.yaml +4 -0
  41. package/lib/init/template/helm/files/web-application/templates/network-policy.yaml +15 -0
  42. package/lib/init/template/helm/files/web-application/values.schema.json +41 -0
  43. package/lib/init/template/helm/files/web-application/values.yaml +19 -1
  44. package/lib/init/template/helm/index.js +5 -2
  45. package/lib/init/template/helm-unified-runtime/files/values.yaml.hbs +2 -2
  46. package/lib/init/template/helm-unified-runtime/index.js +13 -7
  47. package/lib/init/template/html5-repo/index.js +0 -1
  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/files/values.yaml +10 -0
  52. package/lib/init/template/kafka/index.js +39 -0
  53. package/lib/init/template/lint/index.js +4 -4
  54. package/lib/init/template/mta/files/mta.yaml.hbs +4 -0
  55. package/lib/init/template/multitenancy/files/values.yaml.hbs +2 -2
  56. package/lib/util/cf.js +6 -2
  57. package/npm-shrinkwrap.json +269 -293
  58. package/package.json +1 -1
@@ -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
+ }