@sap/cds-dk 7.8.2 → 7.9.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.
- package/bin/add.js +28 -7
- package/bin/bind.js +64 -21
- package/bin/build.js +35 -3
- package/bin/cds.js +31 -13
- package/bin/compile/index.js +44 -3
- package/bin/completion/completionFs.js +61 -0
- package/bin/completion/index.js +212 -0
- package/bin/completion/scripts/cds-bash.sh +17 -0
- package/bin/completion/scripts/cds-ps.ps1 +18 -0
- package/bin/completion/scripts/cds-zsh.sh +12 -0
- package/bin/help.js +8 -2
- package/bin/init.js +34 -4
- package/bin/subscribe.js +5 -1
- package/bin/watch.js +4 -2
- package/lib/bind/KubernetesApiAccessor.js +3 -2
- package/lib/bind/bindingResolver.js +3 -2
- package/lib/bind/bindingUtil.js +10 -8
- package/lib/bind/cfKeyProvider.js +58 -153
- package/lib/bind/index.js +46 -36
- package/lib/bind/kubernetesCredentialsProvider.js +6 -5
- package/lib/bind/userCdsrcJsonStorageProvider.js +25 -3
- package/lib/build/buildTaskEngine.js +1 -1
- package/lib/build/provider/hana/index.js +1 -1
- package/lib/build/provider/hana/template/undeploy.json +3 -1
- package/lib/build/provider/nodejs/index.js +1 -1
- package/lib/compile/openapi/index.js +5 -0
- package/lib/deploy/to-hana/hana.js +25 -7
- package/lib/env/schemas/cds-rc.js +29 -2
- package/lib/import/odata/v4parser.js +78 -25
- package/lib/init/constants.js +2 -1
- package/lib/init/index.js +18 -13
- package/lib/init/merge.js +1 -0
- package/lib/init/mvn.js +8 -47
- package/lib/init/plugin.js +0 -4
- package/lib/init/projectReader.js +17 -10
- package/lib/init/template/approuter/files/values.yaml.hbs +0 -12
- package/lib/init/template/completion/completionSetup.js +184 -0
- package/lib/init/template/completion/index.js +22 -0
- package/lib/init/template/data/as-csv.js +25 -17
- package/lib/init/template/data/as-json.js +115 -30
- package/lib/init/template/data/index.js +28 -15
- package/lib/init/template/hana/files/undeploy.json +2 -1
- package/lib/init/template/helm/files/chart/values.schema.json +1 -1
- package/lib/init/template/helm/files/content-deployment/Chart.yaml +0 -2
- package/lib/init/template/helm/files/content-deployment/values.schema.json +0 -1
- package/lib/init/template/helm/files/service-instance/Chart.yaml +0 -2
- package/lib/init/template/helm/files/service-instance/values.schema.json +0 -1
- package/lib/init/template/helm/files/web-application/Chart.yaml +0 -2
- package/lib/init/template/helm/files/web-application/values.schema.json +0 -1
- package/lib/init/template/helm-unified-runtime/files/values.schema.json +1 -1
- package/lib/init/template/http/index.js +150 -104
- package/lib/init/template/kafka/index.js +6 -0
- package/lib/init/template/lint/index.js +12 -13
- package/lib/init/template/mta/files/mta.yaml.hbs +1 -1
- package/lib/init/template/multitenancy/files/values.yaml.hbs +0 -5
- package/lib/init/template/multitenancy/index.js +13 -5
- package/lib/init/template/nodejs/files/.vscode/settings.json +11 -0
- package/lib/init/template/nodejs/files/eslint.config.js +13 -0
- package/lib/init/template/nodejs/files/package.json.hbs +3 -1
- package/lib/init/template/sample/index.js +18 -20
- package/lib/init/template/tiny-sample/index.js +1 -2
- package/lib/init/template/typer/index.js +2 -2
- package/lib/lint/Linter.js +133 -55
- package/lib/lint/checks.js +5 -6
- package/lib/lint/io.js +119 -10
- package/lib/mtx/auth_manager.js +5 -5
- package/lib/mtx/params.js +4 -2
- package/lib/mtx/settings_manager.js +1 -1
- package/lib/mtx/tenant_action.js +12 -2
- package/lib/mtx/util/logging.js +10 -4
- package/lib/util/axios.js +2 -1
- package/lib/util/cf.js +32 -37
- package/lib/util/git.js +1 -1
- package/lib/util/term.js +1 -1
- package/{bin → lib/watch}/watched.js +2 -2
- package/npm-shrinkwrap.json +34 -34
- package/package.json +2 -2
- 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
|
-
|
|
1
|
+
const { elementsSorter } = require('./as-json')
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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 (
|
|
40
|
-
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
|
-
|
|
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
|
|
13
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
|
|
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 =
|
|
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
|
|
139
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
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,14 +269,22 @@ 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
|
-
|
|
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
|
|
218
|
-
const files = Object.entries(
|
|
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
|
|
@@ -281,6 +350,22 @@ function findKeysInOnCondition(assocElement, target) {
|
|
|
281
350
|
return [ownKey, ownKeyStructured, targetKey]
|
|
282
351
|
}
|
|
283
352
|
|
|
353
|
+
function uniqRecords(records, entity) {
|
|
354
|
+
const keys = Object.keys(entity.keys||{})
|
|
355
|
+
if (keys.length === 0) return records
|
|
356
|
+
const keySet = new Set()
|
|
357
|
+
for (let i = 0; i < records.length; i++) {
|
|
358
|
+
const key = keys.map(k => records[i][k]).join('|')
|
|
359
|
+
if (keySet.has(key)) {
|
|
360
|
+
// console.debug('Removing', keys, key, 'from', entity.name)
|
|
361
|
+
records.splice(i, 1)
|
|
362
|
+
i--
|
|
363
|
+
}
|
|
364
|
+
keySet.add(key)
|
|
365
|
+
}
|
|
366
|
+
return records
|
|
367
|
+
}
|
|
368
|
+
|
|
284
369
|
function annotatedDateRange(el, dateRange) {
|
|
285
370
|
if (el['@assert.range']?.length === 2) {
|
|
286
371
|
const dateFrom = new Date(el['@assert.range'][0])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../../../cds')
|
|
2
2
|
const { exists, fs } = cds.utils
|
|
3
|
+
const { dim } = require('../../../util/term')
|
|
3
4
|
const { resolve, join, relative } = require('path')
|
|
4
5
|
const { URLSearchParams } = require('url')
|
|
5
6
|
const { env4 } = require('../../projectReader')
|
|
@@ -14,8 +15,8 @@ module.exports = class DataTemplate extends require('../../plugin') {
|
|
|
14
15
|
type: 'string',
|
|
15
16
|
short: 'f',
|
|
16
17
|
help: `Filter for entities matching the given pattern. If it contains meta
|
|
17
|
-
characters like '^' or '*', it is treated
|
|
18
|
-
otherwise as
|
|
18
|
+
characters like '^' or '*', it is treated as a regular expression,
|
|
19
|
+
otherwise as an include pattern, i.e /.*pattern.*/i`
|
|
19
20
|
},
|
|
20
21
|
'data:for': {
|
|
21
22
|
type: 'string',
|
|
@@ -52,8 +53,9 @@ module.exports = class DataTemplate extends require('../../plugin') {
|
|
|
52
53
|
if (Number.isNaN(records) || records < 1) records = 1
|
|
53
54
|
|
|
54
55
|
const [filterName, filterQueryStr] = (cds.cli.options['filter'] ?? cds.cli.options['data:for'] ?? '').split(':')
|
|
55
|
-
const nameFilter =
|
|
56
|
+
const nameFilter = filterStringAsRegex(filterName)
|
|
56
57
|
const dataForParams = parseQueryStr(filterQueryStr)
|
|
58
|
+
const { queries } = dataForParams
|
|
57
59
|
|
|
58
60
|
const env = env4('production')
|
|
59
61
|
let dest = (typeof out === 'string') // target folder
|
|
@@ -68,33 +70,43 @@ module.exports = class DataTemplate extends require('../../plugin') {
|
|
|
68
70
|
|
|
69
71
|
const data = csn.all('entity')
|
|
70
72
|
.filter (e => e.name.match(nameFilter)) // --for prefix|regex
|
|
73
|
+
.filter (e => queries || !e.query) // remove all projection-like entities unless specified differently
|
|
71
74
|
.reduce((all, e) => { all[e.name] = []; return all }, {})
|
|
72
75
|
|
|
73
76
|
await asJson(data, csn, records)
|
|
74
77
|
|
|
75
78
|
if (contentType === 'csv') {
|
|
76
79
|
const headerOnly = !cds.cli.options['records'] // compat to old behavior which only creates headers
|
|
77
|
-
|
|
78
|
-
await asCsv(data, csn, Object.assign({ headerOnly, queries }, dataForParams))
|
|
80
|
+
await asCsv(data, csn, Object.assign({ headerOnly }, dataForParams))
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
// write files
|
|
82
|
-
for (const name of Object.keys(data)) {
|
|
84
|
+
for (const name of Object.keys(data).sort()) {
|
|
83
85
|
writeFileFor(csn.definitions[name], data[name], csn, dest, force)
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
module.exports.asJson = asJson
|
|
88
90
|
module.exports.asCsv = asCsv
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
module.exports.filterStringAsRegex = filterStringAsRegex
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Turns a filter string with optional meta characters into a regular expression.
|
|
95
|
+
* If no meta characters are found, the string is treated as an include pattern.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} spec the filter string
|
|
98
|
+
* @returns {RegExp} the regular expression
|
|
99
|
+
*/
|
|
100
|
+
function filterStringAsRegex(spec) {
|
|
91
101
|
if (typeof spec === 'string') {
|
|
92
102
|
try {
|
|
93
|
-
if (spec.match(/[\^$|*]/))
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
if (spec.match(/[\^$|*]/)) {
|
|
104
|
+
spec = spec.replace(/^\*/, '.*') // * at start would be invalid, so make it a .*
|
|
105
|
+
return new RegExp(spec, 'i')
|
|
106
|
+
}
|
|
107
|
+
else { // no meta chars -> include semantics
|
|
108
|
+
spec = spec.replace(/\./g, '\\.') // escape literal dot
|
|
109
|
+
return new RegExp(spec, 'i')
|
|
98
110
|
}
|
|
99
111
|
} catch (err) {
|
|
100
112
|
throw err.message // user error, so cut off stack trace
|
|
@@ -149,7 +161,7 @@ function writeFileFor (entity, data, csn, dest, force) {
|
|
|
149
161
|
|
|
150
162
|
// continue only if file not already exists, or '--force' option provided
|
|
151
163
|
if (fileExists && !force) {
|
|
152
|
-
console.log(`Skipping
|
|
164
|
+
console.log(` ${dim('Skipping ' + relFilePath)}`)
|
|
153
165
|
return
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -158,7 +170,8 @@ function writeFileFor (entity, data, csn, dest, force) {
|
|
|
158
170
|
if (data.length) {
|
|
159
171
|
if (!exists(dest)) fs.mkdirSync(dest, {recursive: true})
|
|
160
172
|
fs.writeFileSync(dataFilePath, data)
|
|
161
|
-
fileExists
|
|
173
|
+
if (fileExists) console.log(` ${dim('Overwriting ' + relFilePath)}`)
|
|
174
|
+
else console.log(` ${dim('Creating ' + relFilePath)}`)
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
],
|
|
188
188
|
"properties": {
|
|
189
189
|
"fromSecret": {
|
|
190
|
-
"description": "Name of a Kubernetes Secret, with the binding content, compliant to the SAP Kubernetes Service Binding spec
|
|
190
|
+
"description": "Name of a Kubernetes Secret, with the binding content, compliant to the SAP Kubernetes Service Binding spec",
|
|
191
191
|
"$ref": "#/definitions/KubernetesName"
|
|
192
192
|
}
|
|
193
193
|
}
|