@cap-js/cds-typer 0.26.0 → 0.27.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/CHANGELOG.md +12 -1
- package/cds-plugin.js +11 -4
- package/lib/cli.js +172 -40
- package/lib/compile.js +10 -16
- package/lib/components/basedefs.js +6 -0
- package/lib/components/inline.js +2 -1
- package/lib/components/javascript.js +2 -2
- package/lib/components/wrappers.js +16 -0
- package/lib/config.js +117 -0
- package/lib/file.js +5 -14
- package/lib/resolution/resolver.js +10 -6
- package/lib/typedefs.d.ts +49 -38
- package/lib/util.js +17 -64
- package/lib/visitor.js +26 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 0.
|
|
7
|
+
## Version 0.28.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.27.0 - 2024-10-02
|
|
10
|
+
### Changed
|
|
11
|
+
- Any configuration variable (via CLI or `cds.env`) can now be passed in snake_case in addition to camelCase
|
|
12
|
+
- Action parameters are now generated as optional by default, which is how the runtime treats them. Mandatory parameters have to be marked as `not null` in CDS/CDL, or `notNull` in CSN.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Fix build task for projects with spaces
|
|
16
|
+
- Fixa bug where cds-typer would produce redundant type declarations when the model contains an associations to another entity's property
|
|
8
17
|
|
|
9
18
|
## Version 0.26.0 - 2024-09-11
|
|
10
19
|
### Added
|
|
20
|
+
- Added a static `.keys` property in all entities. That property is dictionary which holds all properties as keys that are marked as `key` in CDS
|
|
11
21
|
- Added a CLI option `--useEntitiesProxy`. When set to `true`, all entities are wrapped into `Proxy` objects during runtime, allowing top level imports of entity types.
|
|
12
22
|
- Added a static `.kind` property for entities and types, which contains `'entity'` or `'type'` respectively
|
|
13
23
|
- Apps need to provide `@sap/cds` version `8.2` or higher.
|
|
@@ -47,6 +57,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
## Version 0.22.0 - 2024-06-20
|
|
60
|
+
|
|
50
61
|
### Fixed
|
|
51
62
|
- Fixed a bug where keys would sometimes inconsistently become nullable
|
|
52
63
|
|
package/cds-plugin.js
CHANGED
|
@@ -36,7 +36,7 @@ const rmDirIfExists = dir => {
|
|
|
36
36
|
* Remove files with given extensions from a directory recursively.
|
|
37
37
|
* @param {string} dir - The directory to start from.
|
|
38
38
|
* @param {string[]} exts - The extensions to remove.
|
|
39
|
-
* @returns {Promise<
|
|
39
|
+
* @returns {Promise<unknown>}
|
|
40
40
|
*/
|
|
41
41
|
const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
42
42
|
? Promise.all(
|
|
@@ -68,10 +68,15 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
68
68
|
|
|
69
69
|
get #appFolder () { return cds?.env?.folders?.app ?? 'app' }
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* cds.env > tsconfig.compilerOptions.paths > '@cds-models' (default)
|
|
73
|
+
*/
|
|
71
74
|
get #modelDirectoryName () {
|
|
75
|
+
const outputDirectory = cds.env.typer?.outputDirectory
|
|
76
|
+
if (outputDirectory) return outputDirectory
|
|
72
77
|
try {
|
|
73
78
|
// expected format: { '#cds-models/*': [ './@cds-models/*/index.ts' ] }
|
|
74
|
-
//
|
|
79
|
+
// ^^^^^^^^^^^
|
|
75
80
|
// relevant part - may be changed by user
|
|
76
81
|
const config = JSON.parse(fs.readFileSync ('tsconfig.json', 'utf8'))
|
|
77
82
|
const alias = config.compilerOptions.paths['#cds-models/*'][0]
|
|
@@ -89,7 +94,9 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
89
94
|
|
|
90
95
|
async #runCdsTyper () {
|
|
91
96
|
DEBUG?.('running cds-typer')
|
|
92
|
-
|
|
97
|
+
cds.env.typer ??= {}
|
|
98
|
+
cds.env.typer.outputDirectory ??= this.#modelDirectoryName
|
|
99
|
+
await typer.compileFromFile('*')
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
async #buildWithConfig () {
|
|
@@ -103,7 +110,7 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
103
110
|
DEBUG?.('building without config')
|
|
104
111
|
// this will include gen/ that was created by the nodejs task
|
|
105
112
|
// _within_ the project directory. So we need to remove it afterwards.
|
|
106
|
-
await exec(`npx tsc --outDir ${this.task.dest}`)
|
|
113
|
+
await exec(`npx tsc --outDir "${this.task.dest}"`)
|
|
107
114
|
rmDirIfExists(path.join(this.task.dest, cds.env.build.target))
|
|
108
115
|
rmDirIfExists(path.join(this.task.dest, this.#appFolder))
|
|
109
116
|
}
|
package/lib/cli.js
CHANGED
|
@@ -2,20 +2,147 @@
|
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./typedefs').config.cli.ParameterSchema[number]} Parameter
|
|
7
|
+
*/
|
|
8
|
+
|
|
5
9
|
const cds = require('@sap/cds')
|
|
6
10
|
const { compileFromFile } = require('./compile')
|
|
7
|
-
const {
|
|
11
|
+
const { camelToSnake } = require('./util')
|
|
8
12
|
const { deprecated, _keyFor } = require('./logging')
|
|
9
13
|
const path = require('path')
|
|
10
14
|
const { EOL } = require('node:os')
|
|
15
|
+
const { camelSnakeHybrid, configuration } = require('./config')
|
|
11
16
|
|
|
12
17
|
const EOL2 = EOL + EOL
|
|
13
18
|
const toolName = 'cds-typer'
|
|
14
|
-
|
|
15
19
|
// @ts-expect-error - nope, it is actually there. Types just seem to be out of sync.
|
|
16
20
|
const lls = cds.log.levels
|
|
21
|
+
const parameterTypes = {
|
|
22
|
+
boolean:
|
|
23
|
+
/**
|
|
24
|
+
* @param {Parameter} props - additional parameter properties
|
|
25
|
+
* @returns {Parameter}
|
|
26
|
+
*/
|
|
27
|
+
props => ({...{
|
|
28
|
+
allowed: ['true', 'false'],
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
postprocess: (/** @type {string} */ value) => value === 'true'
|
|
31
|
+
},
|
|
32
|
+
...props})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adds additional properties to the CLI parameter schema.
|
|
37
|
+
* @param {import('./typedefs').config.cli.ParameterSchema } flags - The CLI parameter schema.
|
|
38
|
+
* @returns {import('./typedefs').config.cli.ParameterSchema} - The enriched schema.
|
|
39
|
+
*/
|
|
40
|
+
const enrichFlagSchema = flags => {
|
|
41
|
+
const flagKeys = Object.keys(flags)
|
|
42
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
43
|
+
/** @type {Parameter} */(value).camel = key;
|
|
44
|
+
/** @type {Parameter} */(value).snake = camelToSnake(key)
|
|
45
|
+
}
|
|
46
|
+
// non-enumerable utilities
|
|
47
|
+
Object.defineProperties(flags, {
|
|
48
|
+
hasFlag: {
|
|
49
|
+
value: (/** @type {string} **/ flag) => Object.values(flags).some(f => f.snake === flag || f.camel === flag)
|
|
50
|
+
},
|
|
51
|
+
keys: {
|
|
52
|
+
value: flagKeys
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return camelSnakeHybrid(flags)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parses command line arguments into named and positional parameters.
|
|
60
|
+
* Named parameters are expected to start with a double dash (--).
|
|
61
|
+
* If the next argument `B` after a named parameter `A` is not a named parameter itself,
|
|
62
|
+
* `B` is used as value for `A`.
|
|
63
|
+
* If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
|
|
64
|
+
* Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
|
|
65
|
+
* will cause an error.
|
|
66
|
+
* Named parameters that are either not specified or do not have a value assigned to them may draw a default value
|
|
67
|
+
* from their definition in validFlags.
|
|
68
|
+
* @param {string[]} argv - list of command line arguments
|
|
69
|
+
* @param {import('./typedefs').config.cli.ParameterSchema} schema - allowed flags. May specify default values.
|
|
70
|
+
* @returns {import('./typedefs').config.cli.ParsedParameters} - parsed arguments
|
|
71
|
+
*/
|
|
72
|
+
const parseCommandlineArgs = (argv, schema) => {
|
|
73
|
+
const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
|
|
74
|
+
const positional = []
|
|
75
|
+
/** @type {import('./typedefs').config.cli.ParsedParameters['named']} */
|
|
76
|
+
const named = {}
|
|
77
|
+
|
|
78
|
+
let i = 0
|
|
79
|
+
while (i < argv.length) {
|
|
80
|
+
const originalArgName = argv[i] // so our feedback to the user is less confusing
|
|
81
|
+
let arg = originalArgName
|
|
82
|
+
if (isFlag(arg)) {
|
|
83
|
+
arg = camelToSnake(arg.slice(2))
|
|
84
|
+
// @ts-expect-error - cba to add hasFlag to the general dictionary
|
|
85
|
+
if (!schema.hasFlag(arg)) {
|
|
86
|
+
throw new Error(`invalid named flag '${originalArgName}'`)
|
|
87
|
+
}
|
|
88
|
+
const next = argv[i + 1]
|
|
89
|
+
if (next && !isFlag(next)) {
|
|
90
|
+
named[arg] = { value: next, isDefault: false }
|
|
91
|
+
i++
|
|
92
|
+
} else {
|
|
93
|
+
named[arg] = { value: schema[arg].default, isDefault: true }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { allowed, allowedHint } = schema[arg]
|
|
97
|
+
if (allowed && !allowed.includes(named[arg].value)) {
|
|
98
|
+
throw new Error(`invalid value '${named[arg]?.value ?? named[arg]}' for flag ${originalArgName}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
positional.push(arg)
|
|
102
|
+
}
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// enrich with defaults
|
|
107
|
+
/** @type {import('./typedefs').config.cli.ParameterSchema} */
|
|
108
|
+
const defaults = Object.entries(schema)
|
|
109
|
+
.filter(e => !!e[1].default)
|
|
110
|
+
.reduce((dict, [k, v]) => {
|
|
111
|
+
// @ts-expect-error - can't infer type of initial {}
|
|
112
|
+
dict[camelToSnake(k)] = { value: v.default, isDefault: true }
|
|
113
|
+
return dict
|
|
114
|
+
}, {})
|
|
115
|
+
|
|
116
|
+
const namedWithDefaults = {...defaults, ...named}
|
|
117
|
+
|
|
118
|
+
// apply postprocessing
|
|
119
|
+
for (const [key, {value}] of Object.entries(namedWithDefaults)) {
|
|
120
|
+
const { postprocess } = schema[key]
|
|
121
|
+
if (typeof postprocess === 'function') {
|
|
122
|
+
namedWithDefaults[key].value = postprocess(value)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
named: namedWithDefaults,
|
|
128
|
+
positional,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Adds CLI parameters to the configuration object.
|
|
134
|
+
* Precedence: CLI > env > default.
|
|
135
|
+
* @param {ReturnType<parseCommandlineArgs>['named']} params - CLI parameters.
|
|
136
|
+
*/
|
|
137
|
+
const addCLIParamsToConfig = params => {
|
|
138
|
+
for (const [key, value] of Object.entries(params)) {
|
|
139
|
+
if (!value.isDefault || Object.hasOwn(configuration, key)) {
|
|
140
|
+
configuration[key] = value.value
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
17
144
|
|
|
18
|
-
const flags = {
|
|
145
|
+
const flags = enrichFlagSchema({
|
|
19
146
|
outputDirectory: {
|
|
20
147
|
desc: 'Root directory to write the generated files to.',
|
|
21
148
|
default: './',
|
|
@@ -30,16 +157,24 @@ const flags = {
|
|
|
30
157
|
allowedHint: Object.keys(lls).join(' | '), // FIXME: remove once old levels are faded out
|
|
31
158
|
defaultHint: _keyFor(lls.ERROR),
|
|
32
159
|
default: cds?.env?.log?.levels?.['cds-typer'] ?? _keyFor(lls.ERROR),
|
|
160
|
+
postprocess: level => {
|
|
161
|
+
const newLogLevel = deprecated[level]
|
|
162
|
+
if (newLogLevel) {
|
|
163
|
+
console.warn(`deprecated log level '${level}', use '${newLogLevel}' instead (changing this automatically for now).`)
|
|
164
|
+
return newLogLevel
|
|
165
|
+
}
|
|
166
|
+
return level
|
|
167
|
+
}
|
|
33
168
|
},
|
|
34
169
|
jsConfigPath: {
|
|
35
170
|
desc: `Path to where the jsconfig.json should be written.${EOL}If specified, ${toolName} will create a jsconfig.json file and${EOL}set it up to restrict property usage in types entities to${EOL}existing properties only.`,
|
|
36
|
-
type: 'string'
|
|
171
|
+
type: 'string',
|
|
172
|
+
postprocess: file => file && !file.endsWith('jsconfig.json') ? path.join(file, 'jsconfig.json') : path
|
|
37
173
|
},
|
|
38
|
-
useEntitiesProxy: {
|
|
174
|
+
useEntitiesProxy: parameterTypes.boolean({
|
|
39
175
|
desc: `If set to true the 'cds.entities' exports in the generated 'index.js'${EOL}files will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.${EOL}${EOL}WARNING: entity properties can still only be accessed after${EOL}'cds.entities' has been loaded`,
|
|
40
|
-
allowed: ['true', 'false'],
|
|
41
176
|
default: 'false'
|
|
42
|
-
},
|
|
177
|
+
}),
|
|
43
178
|
version: {
|
|
44
179
|
desc: 'Prints the version of this tool.'
|
|
45
180
|
},
|
|
@@ -48,17 +183,15 @@ const flags = {
|
|
|
48
183
|
allowed: ['flat', 'structured'],
|
|
49
184
|
default: 'structured'
|
|
50
185
|
},
|
|
51
|
-
propertiesOptional: {
|
|
186
|
+
propertiesOptional: parameterTypes.boolean({
|
|
52
187
|
desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
|
|
53
|
-
allowed: ['true', 'false'],
|
|
54
188
|
default: 'true'
|
|
55
|
-
},
|
|
56
|
-
IEEE754Compatible: {
|
|
189
|
+
}),
|
|
190
|
+
IEEE754Compatible: parameterTypes.boolean({
|
|
57
191
|
desc: `If set to true, floating point properties are generated${EOL}as IEEE754 compatible '(number | string)' instead of 'number'.`,
|
|
58
|
-
allowed: ['true', 'false'],
|
|
59
192
|
default: 'false'
|
|
60
|
-
}
|
|
61
|
-
}
|
|
193
|
+
})
|
|
194
|
+
})
|
|
62
195
|
|
|
63
196
|
const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
|
|
64
197
|
/**
|
|
@@ -74,19 +207,22 @@ const help = () => `SYNOPSIS${EOL2}` +
|
|
|
74
207
|
indent('cds-typer [cds file | "*"]', ' ') + EOL2 +
|
|
75
208
|
indent(`Generates type information based on a CDS model.${EOL}Call with at least one positional parameter pointing${EOL}to the (root) CDS file you want to compile.`, ' ') + EOL2 +
|
|
76
209
|
`OPTIONS${EOL2}` +
|
|
77
|
-
|
|
210
|
+
flags.keys
|
|
78
211
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
79
|
-
.map(
|
|
212
|
+
.map(key => {
|
|
213
|
+
const value = flags[key]
|
|
80
214
|
let s = indent(`--${key}`, ' ')
|
|
81
|
-
|
|
215
|
+
const snake = camelToSnake(key)
|
|
216
|
+
if (key !== snake) s += EOL + indent(`--${snake}`, ' ')
|
|
217
|
+
// ts-expect-error - not going to check presence of each property. Same for the following expect-errors.
|
|
82
218
|
if (value.allowedHint) s += ` ${value.allowedHint}`
|
|
83
|
-
//
|
|
219
|
+
// ts-expect-error
|
|
84
220
|
else if (value.allowed) s += `: <${value.allowed.join(' | ')}>`
|
|
85
221
|
else if ('type' in value && value.type) s += `: <${value.type}>`
|
|
86
|
-
//
|
|
222
|
+
// ts-expect-error
|
|
87
223
|
if (value.defaultHint || value.default) {
|
|
88
224
|
s += EOL
|
|
89
|
-
//
|
|
225
|
+
// ts-expect-error
|
|
90
226
|
s += indent(`(default: ${value.defaultHint ?? value.default})`, ' ')
|
|
91
227
|
}
|
|
92
228
|
s += `${EOL2}${indent(value.desc, ' ')}`
|
|
@@ -96,7 +232,9 @@ const help = () => `SYNOPSIS${EOL2}` +
|
|
|
96
232
|
|
|
97
233
|
const version = () => require('../package.json').version
|
|
98
234
|
|
|
99
|
-
const
|
|
235
|
+
const prepareParameters = (/** @type {any[]} */ argv) => {
|
|
236
|
+
const args = parseCommandlineArgs(argv, flags)
|
|
237
|
+
|
|
100
238
|
if ('help' in args.named) {
|
|
101
239
|
console.log(help())
|
|
102
240
|
process.exit(0)
|
|
@@ -109,27 +247,21 @@ const main = async (/** @type {any} */ args) => {
|
|
|
109
247
|
console.log(hint())
|
|
110
248
|
process.exit(1)
|
|
111
249
|
}
|
|
112
|
-
if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
|
|
113
|
-
args.named.jsConfigPath = path.join(args.named.jsConfigPath, 'jsconfig.json')
|
|
114
|
-
}
|
|
115
|
-
const newLogLevel = deprecated[args.named.logLevel]
|
|
116
|
-
if (newLogLevel) {
|
|
117
|
-
console.warn(`deprecated log level '${args.named.logLevel}', use '${newLogLevel}' instead (changing this automatically for now).`)
|
|
118
|
-
args.named.logLevel = newLogLevel
|
|
119
|
-
}
|
|
120
250
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
propertiesOptional: args.named.propertiesOptional === 'true',
|
|
129
|
-
IEEE754Compatible: args.named.IEEE754Compatible === 'true'
|
|
130
|
-
})
|
|
251
|
+
addCLIParamsToConfig(args.named)
|
|
252
|
+
return args
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const main = async (/** @type {any[]} */ argv) => {
|
|
256
|
+
const { positional } = prepareParameters(argv)
|
|
257
|
+
compileFromFile(positional)
|
|
131
258
|
}
|
|
132
259
|
|
|
133
260
|
if (require.main === module) {
|
|
134
|
-
main(
|
|
261
|
+
main(process.argv.slice(2))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
flags,
|
|
266
|
+
prepareParameters
|
|
135
267
|
}
|
package/lib/compile.js
CHANGED
|
@@ -7,10 +7,7 @@ const util = require('./util')
|
|
|
7
7
|
const { writeout } = require('./file')
|
|
8
8
|
const { Visitor } = require('./visitor')
|
|
9
9
|
const { LOG, setLevel } = require('./logging')
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters
|
|
13
|
-
*/
|
|
10
|
+
const { configuration } = require('./config')
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* Writes the accompanying jsconfig.json file to the specified paths.
|
|
@@ -40,31 +37,28 @@ const writeJsConfig = path => {
|
|
|
40
37
|
/**
|
|
41
38
|
* Compiles a CSN object to Typescript types.
|
|
42
39
|
* @param {{xtended: import('./typedefs').resolver.CSN, inferred: import('./typedefs').resolver.CSN}} csn - csn tuple
|
|
43
|
-
* @param {CompileParameters} parameters - path to root directory for all generated files, min log level
|
|
44
40
|
*/
|
|
45
|
-
const compileFromCSN = async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
writeJsConfig(parameters.jsConfigPath)
|
|
41
|
+
const compileFromCSN = async csn => {
|
|
42
|
+
|
|
43
|
+
setLevel(configuration.logLevel)
|
|
44
|
+
if (configuration.jsConfigPath) {
|
|
45
|
+
writeJsConfig(configuration.jsConfigPath)
|
|
51
46
|
}
|
|
52
47
|
return writeout(
|
|
53
|
-
|
|
54
|
-
Object.values(new Visitor(csn
|
|
48
|
+
configuration.outputDirectory,
|
|
49
|
+
Object.values(new Visitor(csn).getWriteoutFiles())
|
|
55
50
|
)
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
54
|
* Compiles a .cds file to Typescript types.
|
|
60
55
|
* @param {string | string[]} inputFile - path to input .cds file
|
|
61
|
-
* @param {CompileParameters} parameters - path to root directory for all generated files, min log level, etc.
|
|
62
56
|
*/
|
|
63
|
-
const compileFromFile = async
|
|
57
|
+
const compileFromFile = async inputFile => {
|
|
64
58
|
const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
|
|
65
59
|
const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
|
|
66
60
|
const inferred = await cds.linked(await cds.load(paths, { docs: true }))
|
|
67
|
-
return compileFromCSN({xtended, inferred}
|
|
61
|
+
return compileFromCSN({xtended, inferred})
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
module.exports = {
|
|
@@ -42,6 +42,12 @@ export type DeepRequired<T> = {
|
|
|
42
42
|
[K in keyof T]: DeepRequired<T[K]>
|
|
43
43
|
} & Exclude<Required<T>, null>;
|
|
44
44
|
|
|
45
|
+
const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
|
|
46
|
+
export type Key<T> = T & {[key]?: true}
|
|
47
|
+
|
|
48
|
+
export type KeysOf<T> = {
|
|
49
|
+
[K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* Dates and timestamps are strings during runtime, so cds-typer represents them as such.
|
package/lib/components/inline.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { configuration } = require('../config')
|
|
1
2
|
const { SourceFile, Buffer } = require('../file')
|
|
2
3
|
const { normalise } = require('./identifier')
|
|
3
4
|
const { docify } = require('./wrappers')
|
|
@@ -94,7 +95,7 @@ class InlineDeclarationResolver {
|
|
|
94
95
|
* @returns {'?:'|':'}
|
|
95
96
|
*/
|
|
96
97
|
getPropertyTypeSeparator() {
|
|
97
|
-
return
|
|
98
|
+
return configuration.propertiesOptional ? '?:' : ':'
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
const proxyAccessFunction = function (fqParts, opts = {}) {
|
|
5
5
|
const { target, customProps } = { target: {}, customProps: [], ...opts }
|
|
6
|
-
const fq = fqParts.join('.')
|
|
6
|
+
const fq = fqParts.filter(p => !!p).join('.')
|
|
7
7
|
return new Proxy(target, {
|
|
8
8
|
get: function (target, prop) {
|
|
9
9
|
if (cds.entities) {
|
|
10
|
-
target.__proto__ = cds.entities[
|
|
10
|
+
target.__proto__ = cds.entities(fqParts[0])[fqParts[1]]
|
|
11
11
|
// overwrite/simplify getter after cds.entities is accessible
|
|
12
12
|
this.get = (target, prop) => target[prop]
|
|
13
13
|
return target[prop]
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
// this was derived from baseDefinitions before, but caused a circular dependency
|
|
4
4
|
const base = '__'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Wraps type into the Key type.
|
|
8
|
+
* @param {string} t - the type name.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
const createKey = t => `${base}.Key<${t}>`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps type into KeysOf type.
|
|
15
|
+
* @param {string} t - the type name.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
const createKeysOf = t => `${base}.KeysOf<${t}>`
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* Wraps type into association to scalar.
|
|
8
22
|
* @param {string} t - the singular type name.
|
|
@@ -86,6 +100,8 @@ const docify = doc => {
|
|
|
86
100
|
|
|
87
101
|
module.exports = {
|
|
88
102
|
createArrayOf,
|
|
103
|
+
createKey,
|
|
104
|
+
createKeysOf,
|
|
89
105
|
createObjectOf,
|
|
90
106
|
createPromiseOf,
|
|
91
107
|
createUnionOf,
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const { camelToSnake } = require('./util')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Makes properties of an object accessible in both camelCase and snake_case.
|
|
6
|
+
* Snake_case gets precedence over camelCase.
|
|
7
|
+
* @template T
|
|
8
|
+
* @param {T} target - The object to proxy.
|
|
9
|
+
* @returns {T} - The proxied object.
|
|
10
|
+
*/
|
|
11
|
+
const camelSnakeHybrid = target => {
|
|
12
|
+
// @ts-expect-error - expecting target to be of type {}, which is not T (same for following)
|
|
13
|
+
const proxy = new Proxy(target, {
|
|
14
|
+
get(target, prop) {
|
|
15
|
+
// @ts-expect-error
|
|
16
|
+
return target[camelToSnake(prop)] ?? target[prop]
|
|
17
|
+
},
|
|
18
|
+
set(target, p, v) {
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
target[camelToSnake(p)] = v
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
// need to make sure all properties are initially available in snake_case
|
|
25
|
+
// @ts-expect-error
|
|
26
|
+
for (const [k,v] of Object.entries(target)) {
|
|
27
|
+
// @ts-expect-error
|
|
28
|
+
proxy[k] = v
|
|
29
|
+
}
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
return proxy
|
|
32
|
+
}
|
|
33
|
+
class Config {
|
|
34
|
+
static #defaults = {
|
|
35
|
+
propertiesOptional: true,
|
|
36
|
+
useEntitiesProxy: false,
|
|
37
|
+
inlineDeclarations: 'flat'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
values = undefined
|
|
41
|
+
proxy = undefined
|
|
42
|
+
|
|
43
|
+
init () {
|
|
44
|
+
this.values = {...Config.#defaults, ...(cds.env.typer ?? {})}
|
|
45
|
+
this.proxy = camelSnakeHybrid(this.values)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constructor() {
|
|
49
|
+
// proxy around config still allows arbitrary property access:
|
|
50
|
+
// require('config').configuration.logLevel = 'warn' will work
|
|
51
|
+
// eslint-disable-next-line no-constructor-return
|
|
52
|
+
return new Proxy(this, {
|
|
53
|
+
get(target, prop) {
|
|
54
|
+
// lazy loading of cds.env
|
|
55
|
+
// if we don't do this, configuration will load cds.env whenever it is
|
|
56
|
+
// first imported anywhere (even by proxy from, say, cli.js).
|
|
57
|
+
// So we don't get to modify cds.env before that, which is important
|
|
58
|
+
// in cds-build.js.
|
|
59
|
+
// FIXME: revisit. This is horrible.
|
|
60
|
+
if (target.values === undefined) target.init()
|
|
61
|
+
return target[prop] ?? target.proxy[prop]
|
|
62
|
+
},
|
|
63
|
+
set(target, p, v) {
|
|
64
|
+
if (target.values === undefined) target.init()
|
|
65
|
+
|
|
66
|
+
// this.value, this.proxy etc should not be forwarded to the wrapped values
|
|
67
|
+
if (target[p]) {
|
|
68
|
+
target[p] = v
|
|
69
|
+
} else {
|
|
70
|
+
target.proxy[p] = v
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} key - The key to set.
|
|
79
|
+
* @param {any} value - The value to set
|
|
80
|
+
*/
|
|
81
|
+
setOne (key, value) {
|
|
82
|
+
this.proxy[key] = value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {object} props - The properties to set.
|
|
87
|
+
*/
|
|
88
|
+
setMany (props) {
|
|
89
|
+
for (const [k,v] of Object.entries(props)) {
|
|
90
|
+
this.proxy[k] = v
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resets the config value and sets all its values from another passed
|
|
96
|
+
* config object. This allows to keep the reference to the same object.
|
|
97
|
+
* @param {Config} config - Another config object to set all config entries from.
|
|
98
|
+
*/
|
|
99
|
+
setFrom (config) {
|
|
100
|
+
this.values = camelSnakeHybrid({})
|
|
101
|
+
this.setMany(config.values)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
clone () {
|
|
105
|
+
const res = new Config()
|
|
106
|
+
res.init()
|
|
107
|
+
res.setMany(this.values)
|
|
108
|
+
return res
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
camelSnakeHybrid,
|
|
114
|
+
/** @type {import('./typedefs').config.Configuration} */
|
|
115
|
+
// @ts-ignore
|
|
116
|
+
configuration: new Config()
|
|
117
|
+
}
|
package/lib/file.js
CHANGED
|
@@ -8,11 +8,11 @@ const { normalise } = require('./components/identifier')
|
|
|
8
8
|
const { empty } = require('./components/typescript')
|
|
9
9
|
const { proxyAccessFunction } = require('./components/javascript')
|
|
10
10
|
const { createObjectOf } = require('./components/wrappers')
|
|
11
|
+
const { configuration } = require('./config')
|
|
11
12
|
|
|
12
13
|
const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
|
|
13
14
|
|
|
14
15
|
/** @typedef {import('./typedefs').file.Namespace} Namespace */
|
|
15
|
-
/** @typedef {import('./typedefs').file.FileOptions} FileOptions */
|
|
16
16
|
|
|
17
17
|
class File {
|
|
18
18
|
/**
|
|
@@ -109,11 +109,9 @@ class Library extends File {
|
|
|
109
109
|
class SourceFile extends File {
|
|
110
110
|
/**
|
|
111
111
|
* @param {string | Path} path - path to the file
|
|
112
|
-
* @param {FileOptions} [options] - options for file output
|
|
113
112
|
*/
|
|
114
|
-
constructor(path
|
|
113
|
+
constructor(path) {
|
|
115
114
|
super()
|
|
116
|
-
this.options = options ?? { useEntitiesProxy: false }
|
|
117
115
|
/** @type {Path} */
|
|
118
116
|
this.path = path instanceof Path ? path : new Path(path.split('.'))
|
|
119
117
|
/** @type {{[key:string]: any}} */
|
|
@@ -437,7 +435,7 @@ class SourceFile extends File {
|
|
|
437
435
|
const namespace = this.path.asNamespace()
|
|
438
436
|
|
|
439
437
|
const boilerplate = [AUTO_GEN_NOTE]
|
|
440
|
-
if (
|
|
438
|
+
if (configuration.useEntitiesProxy) {
|
|
441
439
|
if (namespace === '_') {
|
|
442
440
|
boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
|
|
443
441
|
} else {
|
|
@@ -460,7 +458,7 @@ class SourceFile extends File {
|
|
|
460
458
|
* @returns {{singularRhs: string, pluralRhs: string}}
|
|
461
459
|
*/
|
|
462
460
|
#getEntityExportsRhs(singular, original) {
|
|
463
|
-
if (
|
|
461
|
+
if (configuration.useEntitiesProxy) {
|
|
464
462
|
const namespace = this.path.asNamespace()
|
|
465
463
|
// determine the custom properties for the proxy function call
|
|
466
464
|
const customProps = this.entityProxies[singular] ?? []
|
|
@@ -697,13 +695,6 @@ class FileRepository {
|
|
|
697
695
|
/** @type {{[key:string]: SourceFile}} */
|
|
698
696
|
#files = {}
|
|
699
697
|
|
|
700
|
-
/**
|
|
701
|
-
* @param {FileOptions} options - options to control file
|
|
702
|
-
*/
|
|
703
|
-
constructor(options) {
|
|
704
|
-
this.options = options
|
|
705
|
-
}
|
|
706
|
-
|
|
707
698
|
/**
|
|
708
699
|
* @param {string} name - file name
|
|
709
700
|
* @param {SourceFile} file - the file
|
|
@@ -720,7 +711,7 @@ class FileRepository {
|
|
|
720
711
|
*/
|
|
721
712
|
getNamespaceFile(path) {
|
|
722
713
|
const key = path instanceof Path ? path.asNamespace() : path
|
|
723
|
-
return (this.#files[key] ??= new SourceFile(path
|
|
714
|
+
return (this.#files[key] ??= new SourceFile(path))
|
|
724
715
|
}
|
|
725
716
|
|
|
726
717
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const util = require('../util')
|
|
4
4
|
// eslint-disable-next-line no-unused-vars
|
|
5
5
|
const { Buffer, SourceFile, Path, Library } = require('../file')
|
|
6
|
-
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('../components/wrappers')
|
|
6
|
+
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne, createKey } = require('../components/wrappers')
|
|
7
7
|
const { StructuredInlineDeclarationResolver } = require('../components/inline')
|
|
8
8
|
const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
|
|
9
9
|
const { isReferenceType } = require('../components/reference')
|
|
@@ -13,6 +13,7 @@ const { BuiltinResolver } = require('./builtin')
|
|
|
13
13
|
const { LOG } = require('../logging')
|
|
14
14
|
const { last } = require('../components/identifier')
|
|
15
15
|
const { getPropertyModifiers } = require('../components/property')
|
|
16
|
+
const { configuration } = require('../config')
|
|
16
17
|
|
|
17
18
|
/** @typedef {import('../visitor').Visitor} Visitor */
|
|
18
19
|
/** @typedef {import('../typedefs').resolver.CSN} CSN */
|
|
@@ -30,7 +31,7 @@ class Resolver {
|
|
|
30
31
|
this.visitor = visitor
|
|
31
32
|
|
|
32
33
|
/** @type {BuiltinResolver} */
|
|
33
|
-
this.builtinResolver = new BuiltinResolver(
|
|
34
|
+
this.builtinResolver = new BuiltinResolver({ IEEE754Compatible: configuration.IEEE754Compatible })
|
|
34
35
|
|
|
35
36
|
/** @type {Library[]} */
|
|
36
37
|
this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
|
|
@@ -63,8 +64,7 @@ class Resolver {
|
|
|
63
64
|
* @returns {boolean} whether the type is configured to be optional
|
|
64
65
|
*/
|
|
65
66
|
isOptional(type) {
|
|
66
|
-
|
|
67
|
-
return Object.keys(type).some(k => k.startsWith('@Core.OptionalParameter'))
|
|
67
|
+
return !type.notNull
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -159,8 +159,8 @@ class Resolver {
|
|
|
159
159
|
if (parts.length <= 1) return []
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* @param {string} property
|
|
163
|
-
* @param {import('../typedefs').resolver.EntityCSN} entity
|
|
162
|
+
* @param {string} property - the property to check
|
|
163
|
+
* @param {import('../typedefs').resolver.EntityCSN} entity - the entity to check the property against
|
|
164
164
|
*/
|
|
165
165
|
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
|
|
166
166
|
|
|
@@ -385,6 +385,10 @@ class Resolver {
|
|
|
385
385
|
plural: typeName
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
if (element.key === true) {
|
|
389
|
+
typeName = createKey(typeName)
|
|
390
|
+
}
|
|
391
|
+
|
|
388
392
|
// FIXME: typeName could probably just become part of typeInfo
|
|
389
393
|
return { typeName, typeInfo }
|
|
390
394
|
}
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -99,47 +99,13 @@ export module resolver {
|
|
|
99
99
|
|
|
100
100
|
export module util {
|
|
101
101
|
export type Annotations = {
|
|
102
|
-
name
|
|
102
|
+
name: string,
|
|
103
103
|
'@singular'?: string,
|
|
104
104
|
'@plural'?: string
|
|
105
105
|
}
|
|
106
|
-
|
|
107
|
-
export type CommandLineFlags = {
|
|
108
|
-
desc: string,
|
|
109
|
-
default?: any
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export type ParsedFlag = {
|
|
113
|
-
positional: string[],
|
|
114
|
-
named: { [key: string]: any }
|
|
115
|
-
}
|
|
116
106
|
}
|
|
117
107
|
|
|
118
108
|
export module visitor {
|
|
119
|
-
export type CompileParameters = {
|
|
120
|
-
outputDirectory: string,
|
|
121
|
-
logLevel: number,
|
|
122
|
-
useEntitiesProxy: boolean,
|
|
123
|
-
jsConfigPath?: string,
|
|
124
|
-
inlineDeclarations: 'flat' | 'structured',
|
|
125
|
-
propertiesOptional: boolean,
|
|
126
|
-
IEEE754Compatible: boolean,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export type VisitorOptions = {
|
|
130
|
-
/** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
|
|
131
|
-
propertiesOptional: boolean,
|
|
132
|
-
/**
|
|
133
|
-
* `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
|
|
134
|
-
* `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
|
|
135
|
-
*/
|
|
136
|
-
inlineDeclarations: 'flat' | 'structured',
|
|
137
|
-
/**
|
|
138
|
-
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
|
139
|
-
*/
|
|
140
|
-
useEntitiesProxy: boolean
|
|
141
|
-
}
|
|
142
|
-
|
|
143
109
|
export type Inflection = {
|
|
144
110
|
typeName?: string,
|
|
145
111
|
singular: string,
|
|
@@ -158,9 +124,54 @@ export module visitor {
|
|
|
158
124
|
}
|
|
159
125
|
}
|
|
160
126
|
|
|
127
|
+
export module config {
|
|
128
|
+
export module cli {
|
|
129
|
+
export type CLIFlags = 'version' | 'help'
|
|
130
|
+
export type ParameterSchema = {
|
|
131
|
+
[key: string]: {
|
|
132
|
+
desc: string,
|
|
133
|
+
allowed?: string[],
|
|
134
|
+
allowedHint?: string,
|
|
135
|
+
type?: 'string' | 'boolean' | 'number',
|
|
136
|
+
default?: string,
|
|
137
|
+
defaultHint?: string,
|
|
138
|
+
postprocess?: (value: string) => any,
|
|
139
|
+
camel?: string,
|
|
140
|
+
snake?: string
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type ParsedParameters = {
|
|
145
|
+
positional: string[],
|
|
146
|
+
named: { [key: keyof RuntimeParameters]: {
|
|
147
|
+
value: any,
|
|
148
|
+
isDefault: boolean,
|
|
149
|
+
} }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type Configuration = {
|
|
154
|
+
outputDirectory: string,
|
|
155
|
+
logLevel: number,
|
|
156
|
+
/**
|
|
157
|
+
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
|
158
|
+
*/
|
|
159
|
+
useEntitiesProxy: boolean,
|
|
160
|
+
jsConfigPath?: string,
|
|
161
|
+
/**
|
|
162
|
+
* `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
|
|
163
|
+
* `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
|
|
164
|
+
*/
|
|
165
|
+
inlineDeclarations: 'flat' | 'structured',
|
|
166
|
+
/** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
|
|
167
|
+
propertiesOptional: boolean,
|
|
168
|
+
/**
|
|
169
|
+
* `IEEE754Compatible = true` -> any cds.Decimal will become `number | string`
|
|
170
|
+
*/
|
|
171
|
+
IEEE754Compatible: boolean
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
export module file {
|
|
162
176
|
export type Namespace = Object<string, Buffer>
|
|
163
|
-
export type FileOptions = {
|
|
164
|
-
useEntitiesProxy: boolean
|
|
165
|
-
}
|
|
166
177
|
}
|
package/lib/util.js
CHANGED
|
@@ -19,6 +19,16 @@ const annotations = {
|
|
|
19
19
|
plural: ['@plural'],
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Converts a camelCase string to snake_case.
|
|
24
|
+
* @param {string} camel - The camelCase string.
|
|
25
|
+
* @returns {string} - The snake_case string.
|
|
26
|
+
*/
|
|
27
|
+
const camelToSnake = camel => camel
|
|
28
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2') // Handle camelCase
|
|
29
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') // Handle sequences of uppercase letters
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Tries to retrieve an annotation that specifies the singular name
|
|
24
34
|
* from a CSN. Valid annotations are listed in util.annotations
|
|
@@ -27,6 +37,7 @@ const annotations = {
|
|
|
27
37
|
* @param {EntityCSN} csn - the CSN of an entity to check
|
|
28
38
|
* @returns {string | undefined} the singular annotation or undefined
|
|
29
39
|
*/
|
|
40
|
+
// @ts-expect-error - can not use possible undefined from find as key
|
|
30
41
|
const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
|
|
31
42
|
|
|
32
43
|
/**
|
|
@@ -37,6 +48,7 @@ const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.h
|
|
|
37
48
|
* @param {EntityCSN} csn - the CSN of an entity to check
|
|
38
49
|
* @returns {string | undefined} the plural annotation or undefined
|
|
39
50
|
*/
|
|
51
|
+
// @ts-expect-error - can not use possible undefined from find as key
|
|
40
52
|
const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
|
|
41
53
|
|
|
42
54
|
/**
|
|
@@ -62,9 +74,9 @@ const unlocalize = name => {
|
|
|
62
74
|
* @param {boolean?} stripped - if true, leading namespace will be stripped
|
|
63
75
|
*/
|
|
64
76
|
const singular4 = (dn, stripped = false) => {
|
|
65
|
-
let n = dn.name
|
|
77
|
+
let n = dn.name ?? dn
|
|
66
78
|
if (stripped) {
|
|
67
|
-
n = n.match(last)[0]
|
|
79
|
+
n = n.match(last)?.[0] ?? ''
|
|
68
80
|
}
|
|
69
81
|
return (
|
|
70
82
|
getSingularAnnotation(dn) ??
|
|
@@ -94,9 +106,9 @@ const singular4 = (dn, stripped = false) => {
|
|
|
94
106
|
* @param {boolean} stripped - if true, leading namespace will be stripped
|
|
95
107
|
*/
|
|
96
108
|
const plural4 = (dn, stripped) => {
|
|
97
|
-
let n = dn.name
|
|
109
|
+
let n = dn.name ?? dn
|
|
98
110
|
if (stripped) {
|
|
99
|
-
n = n.match(last)[0]
|
|
111
|
+
n = n.match(last)?.[0]
|
|
100
112
|
}
|
|
101
113
|
return (
|
|
102
114
|
getPluralAnnotation(dn) ??
|
|
@@ -123,72 +135,13 @@ const deepMerge = (target, source) => {
|
|
|
123
135
|
Object.assign(target, source)
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
/**
|
|
127
|
-
* Parses command line arguments into named and positional parameters.
|
|
128
|
-
* Named parameters are expected to start with a double dash (--).
|
|
129
|
-
* If the next argument `B` after a named parameter `A` is not a named parameter itself,
|
|
130
|
-
* `B` is used as value for `A`.
|
|
131
|
-
* If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
|
|
132
|
-
* Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
|
|
133
|
-
* will cause an error.
|
|
134
|
-
* Named parameters that are either not specified or do not have a value assigned to them may draw a default value
|
|
135
|
-
* from their definition in validFlags.
|
|
136
|
-
* @param {string[]} argv - list of command line arguments
|
|
137
|
-
* @param {{[key: string]: CommandlineFlag}} validFlags - allowed flags. May specify default values.
|
|
138
|
-
* @returns {ParsedFlags}
|
|
139
|
-
*/
|
|
140
|
-
const parseCommandlineArgs = (argv, validFlags) => {
|
|
141
|
-
const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
|
|
142
|
-
const positional = []
|
|
143
|
-
const named = {}
|
|
144
|
-
|
|
145
|
-
let i = 0
|
|
146
|
-
while (i < argv.length) {
|
|
147
|
-
let arg = argv[i]
|
|
148
|
-
if (isFlag(arg)) {
|
|
149
|
-
arg = arg.slice(2)
|
|
150
|
-
if (!(arg in validFlags)) {
|
|
151
|
-
throw new Error(`invalid named flag '${arg}'`)
|
|
152
|
-
} else {
|
|
153
|
-
const next = argv[i + 1]
|
|
154
|
-
if (next && !isFlag(next)) {
|
|
155
|
-
named[arg] = next
|
|
156
|
-
i++
|
|
157
|
-
} else {
|
|
158
|
-
named[arg] = validFlags[arg].default
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const { allowed, allowedHint } = validFlags[arg]
|
|
162
|
-
if (allowed && !allowed.includes(named[arg])) {
|
|
163
|
-
throw new Error(`invalid value '${named[arg]}' for flag ${arg}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
positional.push(arg)
|
|
168
|
-
}
|
|
169
|
-
i++
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const defaults = Object.entries(validFlags)
|
|
173
|
-
.filter(e => !!e[1].default)
|
|
174
|
-
.reduce((dict, [k, v]) => {
|
|
175
|
-
dict[k] = v.default
|
|
176
|
-
return dict
|
|
177
|
-
}, {})
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
named: Object.assign(defaults, named),
|
|
181
|
-
positional,
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
138
|
module.exports = {
|
|
186
139
|
annotations,
|
|
140
|
+
camelToSnake,
|
|
187
141
|
getSingularAnnotation,
|
|
188
142
|
getPluralAnnotation,
|
|
189
143
|
unlocalize,
|
|
190
144
|
singular4,
|
|
191
145
|
plural4,
|
|
192
|
-
parseCommandlineArgs,
|
|
193
146
|
deepMerge
|
|
194
147
|
}
|
package/lib/visitor.js
CHANGED
|
@@ -8,7 +8,7 @@ const { SourceFile, FileRepository, Buffer, Path } = require('./file')
|
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
9
|
const { Resolver } = require('./resolution/resolver')
|
|
10
10
|
const { LOG } = require('./logging')
|
|
11
|
-
const { docify, createPromiseOf, createUnionOf } = require('./components/wrappers')
|
|
11
|
+
const { docify, createPromiseOf, createUnionOf, createKeysOf } = require('./components/wrappers')
|
|
12
12
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
13
13
|
const { isReferenceType } = require('./components/reference')
|
|
14
14
|
const { empty } = require('./components/typescript')
|
|
@@ -16,23 +16,15 @@ const { baseDefinitions } = require('./components/basedefs')
|
|
|
16
16
|
const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
17
17
|
const { last } = require('./components/identifier')
|
|
18
18
|
const { getPropertyModifiers } = require('./components/property')
|
|
19
|
+
const { configuration } = require('./config')
|
|
19
20
|
|
|
20
21
|
/** @typedef {import('./file').File} File */
|
|
21
22
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
22
|
-
/** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
|
|
23
|
-
/** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
|
|
24
23
|
/** @typedef {import('./typedefs').visitor.Inflection} Inflection */
|
|
25
24
|
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
26
25
|
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
27
26
|
/** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
|
|
28
27
|
|
|
29
|
-
const defaults = {
|
|
30
|
-
// FIXME: add defaults for remaining parameters
|
|
31
|
-
propertiesOptional: true,
|
|
32
|
-
useEntitiesProxy: false,
|
|
33
|
-
inlineDeclarations: 'flat'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
class Visitor {
|
|
37
29
|
/**
|
|
38
30
|
* Gathers all files that are supposed to be written to
|
|
@@ -46,12 +38,10 @@ class Visitor {
|
|
|
46
38
|
|
|
47
39
|
/**
|
|
48
40
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
49
|
-
* @param {VisitorOptions | {}} options - the options
|
|
50
41
|
*/
|
|
51
|
-
constructor(csn
|
|
42
|
+
constructor(csn) {
|
|
52
43
|
amendCSN(csn.xtended)
|
|
53
44
|
propagateForeignKeys(csn.inferred)
|
|
54
|
-
this.options = { ...defaults, ...options }
|
|
55
45
|
this.csn = csn
|
|
56
46
|
|
|
57
47
|
/** @type {Context[]} **/
|
|
@@ -64,12 +54,10 @@ class Visitor {
|
|
|
64
54
|
this.entityRepository = new EntityRepository(this.resolver)
|
|
65
55
|
|
|
66
56
|
/** @type {FileRepository} */
|
|
67
|
-
this.fileRepository = new FileRepository(
|
|
68
|
-
// REVISIT: better way to pass options to base source file ???
|
|
69
|
-
baseDefinitions.options = this.options
|
|
57
|
+
this.fileRepository = new FileRepository()
|
|
70
58
|
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
71
59
|
this.inlineDeclarationResolver =
|
|
72
|
-
|
|
60
|
+
configuration.inlineDeclarations === 'structured'
|
|
73
61
|
? new StructuredInlineDeclarationResolver(this)
|
|
74
62
|
: new FlatInlineDeclarationResolver(this)
|
|
75
63
|
|
|
@@ -105,7 +93,13 @@ class Visitor {
|
|
|
105
93
|
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
|
|
106
94
|
// this should be revisted once we settle on a single flavour.
|
|
107
95
|
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
|
|
108
|
-
|
|
96
|
+
if (target.kind !== 'type') {
|
|
97
|
+
// skip if the target is a property, like in:
|
|
98
|
+
// books: Association to many Author.books ...
|
|
99
|
+
// as this would result in a type definition that
|
|
100
|
+
// name-clashes with the actual declaration of Author
|
|
101
|
+
this.visitEntity(name, target)
|
|
102
|
+
}
|
|
109
103
|
} else {
|
|
110
104
|
LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
|
|
111
105
|
}
|
|
@@ -160,6 +154,14 @@ class Visitor {
|
|
|
160
154
|
}
|
|
161
155
|
}
|
|
162
156
|
|
|
157
|
+
/**
|
|
158
|
+
* @param {Buffer} buffer - the buffer to write the keys into
|
|
159
|
+
* @param {string} clean - the clean name of the entity
|
|
160
|
+
*/
|
|
161
|
+
#printStaticKeys(buffer, clean) {
|
|
162
|
+
buffer.add(`declare static readonly keys: ${createKeysOf(clean)}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
163
165
|
/**
|
|
164
166
|
* Transforms an entity or CDS aspect into a JS aspect (aka mixin).
|
|
165
167
|
* That is, for an element A we get:
|
|
@@ -272,6 +274,8 @@ class Visitor {
|
|
|
272
274
|
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
|
|
273
275
|
}
|
|
274
276
|
this.#printStaticActions(entity, buffer, ancestorInfos, file)
|
|
277
|
+
this.#printStaticKeys(buffer, clean)
|
|
278
|
+
|
|
275
279
|
})
|
|
276
280
|
}, '};') // end of generated class
|
|
277
281
|
}, '}') // end of aspect
|
|
@@ -488,12 +492,13 @@ class Visitor {
|
|
|
488
492
|
buffer.add('// event')
|
|
489
493
|
// only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
|
|
490
494
|
buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
|
|
491
|
-
const propOpt =
|
|
492
|
-
|
|
495
|
+
const propOpt = configuration.propertiesOptional
|
|
496
|
+
// FIXME: shouldn't need to change config here! Idea: init Visitor with .options fed from config, then manipulate that
|
|
497
|
+
configuration.propertiesOptional = false
|
|
493
498
|
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
494
499
|
this.visitElement(ename, element, file, buffer)
|
|
495
500
|
}
|
|
496
|
-
|
|
501
|
+
configuration.propertiesOptional = propOpt
|
|
497
502
|
}, '}')
|
|
498
503
|
}
|
|
499
504
|
|