@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.
- package/bin/add.js +42 -17
- package/bin/bind.js +21 -12
- package/bin/build.js +14 -2
- package/bin/deploy/to-sqlite.js +1 -1
- package/bin/help.js +4 -1
- package/bin/version.js +54 -22
- package/lib/bind/userCdsrcJsonStorageProvider.js +1 -1
- package/lib/build/buildTaskFactory.js +2 -2
- package/lib/build/provider/hana/index.js +1 -1
- package/lib/build/provider/internalBuildTaskProvider.js +19 -5
- package/lib/build/provider/mtx/resourcesTarBuilder.js +16 -4
- package/lib/build/provider/mtx-extension/extensionCompilation.js +13 -2
- package/lib/build/provider/mtx-extension/index.js +6 -2
- package/lib/build/provider/nodejs/index.js +4 -4
- package/lib/build/util.js +1 -12
- package/lib/compile/openapi/index.js +2 -1
- package/lib/index.js +2 -2
- package/lib/init/constants.js +2 -1
- package/lib/init/index.js +9 -23
- package/lib/init/plugin.js +8 -0
- package/lib/init/registries/mta.js +6 -0
- package/lib/init/template/data/as-csv.js +60 -0
- package/lib/init/template/data/as-json.js +441 -0
- package/lib/init/template/data/index.js +115 -86
- package/lib/init/template/helm/files/content-deployment/Chart.yaml +1 -1
- package/lib/init/template/helm/files/content-deployment/templates/job.yaml +7 -0
- package/lib/init/template/helm/files/content-deployment/values.schema.json +17 -0
- package/lib/init/template/helm/files/content-deployment/values.yaml +6 -1
- package/lib/init/template/helm/files/service-instance/Chart.yaml +1 -1
- package/lib/init/template/helm/files/service-instance/templates/serviceinstance.yaml +4 -0
- package/lib/init/template/helm/files/service-instance/values.schema.json +17 -1
- package/lib/init/template/helm/files/service-instance/values.yaml +4 -1
- package/lib/init/template/helm/files/values.yaml.hbs +2 -2
- package/lib/init/template/helm/files/web-application/Chart.yaml +1 -1
- package/lib/init/template/helm/files/web-application/templates/api-rule.yaml +4 -0
- package/lib/init/template/helm/files/web-application/templates/network-policy.yaml +15 -0
- package/lib/init/template/helm/files/web-application/values.schema.json +41 -0
- package/lib/init/template/helm/files/web-application/values.yaml +19 -1
- package/lib/init/template/helm-unified-runtime/files/values.yaml.hbs +2 -2
- package/lib/init/template/helm-unified-runtime/index.js +9 -6
- package/lib/init/template/http/index.js +182 -0
- package/lib/init/template/kafka/files/mta.yaml.hbs +14 -0
- package/lib/init/template/kafka/files/package.json.hbs +22 -0
- package/lib/init/template/kafka/index.js +40 -0
- package/lib/init/template/lint/index.js +4 -4
- package/lib/init/template/mta/files/mta.yaml.hbs +4 -0
- package/lib/init/template/multitenancy/files/values.yaml.hbs +2 -2
- package/lib/util/cf.js +6 -2
- package/npm-shrinkwrap.json +13 -13
- 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 {
|
|
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 {
|
|
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
|
|
package/lib/init/constants.js
CHANGED
|
@@ -4,7 +4,7 @@ module.exports = {
|
|
|
4
4
|
COMMAND_INIT: 'init',
|
|
5
5
|
COMMAND_ADD: 'add',
|
|
6
6
|
|
|
7
|
-
MAVEN_ARCHETYPE_VERSION: '2.7.
|
|
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
|
|
103
|
+
* @param {string} facets, comma separated list of facets
|
|
104
104
|
* @param {any} options, additional options
|
|
105
105
|
*/
|
|
106
|
-
async
|
|
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
|
|
221
|
+
const { command, options } = cds.cli
|
|
222
222
|
|
|
223
|
-
const plugins = command === COMMAND_INIT ? options.add : new Set(
|
|
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/plugin.js
CHANGED
|
@@ -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
|
+
}
|