@cap-js/cds-typer 0.25.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 +26 -2
- package/README.md +19 -0
- package/cds-plugin.js +11 -4
- package/lib/cli.js +174 -36
- package/lib/compile.js +10 -16
- package/lib/components/basedefs.js +6 -0
- package/lib/components/enum.js +4 -2
- package/lib/components/inline.js +2 -1
- package/lib/components/javascript.js +28 -0
- package/lib/components/wrappers.js +42 -3
- package/lib/config.js +117 -0
- package/lib/file.js +115 -26
- package/lib/resolution/resolver.js +17 -4
- package/lib/typedefs.d.ts +54 -27
- package/lib/util.js +17 -64
- package/lib/visitor.js +78 -57
- package/package.json +8 -6
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
|
@@ -6,7 +6,9 @@ const { readFileSync } = require('fs')
|
|
|
6
6
|
const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
|
|
7
7
|
const { normalise } = require('./components/identifier')
|
|
8
8
|
const { empty } = require('./components/typescript')
|
|
9
|
+
const { proxyAccessFunction } = require('./components/javascript')
|
|
9
10
|
const { createObjectOf } = require('./components/wrappers')
|
|
11
|
+
const { configuration } = require('./config')
|
|
10
12
|
|
|
11
13
|
const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
|
|
12
14
|
|
|
@@ -140,50 +142,64 @@ class SourceFile extends File {
|
|
|
140
142
|
this.inflections = []
|
|
141
143
|
/** @type {{ buffer: Buffer, names: string[]}} */
|
|
142
144
|
this.services = { buffer: new Buffer(), names: [] }
|
|
145
|
+
/** @type {Record<string,string[]>} */
|
|
146
|
+
this.entityProxies = {}
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/**
|
|
146
150
|
* Stringifies a lambda expression.
|
|
147
151
|
* @param {object} options - options
|
|
148
152
|
* @param {string} options.name - name of the lambda
|
|
149
|
-
* @param {[
|
|
153
|
+
* @param {import('./typedefs').visitor.ParamInfo[]} [options.parameters] - list of parameters, passed as [name, modifier, type, doc] pairs
|
|
150
154
|
* @param {string} [options.returns] - the return type of the function
|
|
151
155
|
* @param {'action' | 'function'} options.kind - kind of the lambda
|
|
152
156
|
* @param {string} [options.initialiser] - the initialiser expression
|
|
153
157
|
* @param {boolean} [options.isStatic] - whether the lambda is static
|
|
158
|
+
* @param {{positional?: boolean, named?: boolean}} [options.callStyles] - whether to generate positional and/or named call styles
|
|
159
|
+
* @param {string[]?} [options.doc] - documentation for the operation
|
|
154
160
|
* @returns {string} the stringified lambda
|
|
155
161
|
* @example
|
|
156
162
|
* ```js
|
|
157
163
|
* // note: these samples are actually simplified! See below.
|
|
158
|
-
* stringifyLambda({parameters: [['p','T']]}) // f: { (p: T): any, ... }
|
|
159
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
160
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
161
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
164
|
+
* stringifyLambda({parameters: [['p','','T']]}) // f: { (p: T): any, ... }
|
|
165
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}]}) // f: { (p: T) => any, ... }
|
|
166
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',modifier:'?',type:'T',doc:'/** doc *\/'}], returns: 'number'}) // /** doc *\/f?: { (p: T) => number, ... }
|
|
167
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number', initialiser: '_ => 42'}) // f?: { (p: T): string = _ => 42, ... }
|
|
162
168
|
* ```
|
|
163
169
|
*
|
|
164
|
-
* The generated string will not be just the signature of the function. Instead, it will be an object offering
|
|
170
|
+
* The generated string will not be just the signature of the function. Instead, it will be an object offering callable signature(s).
|
|
165
171
|
* On top of that, it will also expose a property `__parameters`, which is an object reflecting the functions parameters.
|
|
166
172
|
* The reason for this is that the CDS runtime actually treats the function parameters as a named object. This can not be rectified via
|
|
167
173
|
* type magic, as parameter names do not exist on type level. So we can not use these names to reuse them as object properties.
|
|
168
174
|
* Instead, we generate this utility object for the runtime to use:
|
|
169
175
|
* @example
|
|
170
176
|
* ```js
|
|
171
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
177
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
|
|
172
178
|
* ```
|
|
173
179
|
*/
|
|
174
|
-
static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false,
|
|
175
|
-
|
|
180
|
+
static stringifyLambda({name, parameters=[], returns='any', kind, initialiser, isStatic=false, callStyles={positional:true, named:true}, doc}) {
|
|
181
|
+
let docStr = doc?.length ? doc.join('\n')+'\n' : ''
|
|
182
|
+
const parameterTypes = parameters.map(({name, modifier, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}${modifier}: ${type}`).join(', ')
|
|
176
183
|
const parameterTypeAsObject = parameterTypes.length
|
|
177
184
|
? createObjectOf(parameterTypes)
|
|
178
185
|
: empty
|
|
179
|
-
const
|
|
186
|
+
const callableSignatures = []
|
|
187
|
+
if (callStyles.positional) {
|
|
188
|
+
const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}: ${type}`).join(', ') // must not include ? modifiers
|
|
189
|
+
callableSignatures.push(`// positional\n${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
|
|
190
|
+
}
|
|
191
|
+
if (callStyles.named) {
|
|
192
|
+
const parameterNames = createObjectOf(parameters.map(({name}) => normalise(name)).join(', '))
|
|
193
|
+
callableSignatures.push(`// named\n${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
|
|
194
|
+
}
|
|
195
|
+
if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
|
|
180
196
|
let prefix = name ? `${normalise(name)}: `: ''
|
|
181
197
|
if (prefix && isStatic) {
|
|
182
198
|
prefix = `static ${prefix}`
|
|
183
199
|
}
|
|
184
200
|
const kindDef = kind ? `, kind: '${kind}'` : ''
|
|
185
201
|
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
186
|
-
const lambda = `{
|
|
202
|
+
const lambda = `{\n${callableSignatures.join('\n')}, \n// metadata (do not use)\n__parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
|
|
187
203
|
return prefix + lambda + suffix
|
|
188
204
|
}
|
|
189
205
|
|
|
@@ -203,13 +219,16 @@ class SourceFile extends File {
|
|
|
203
219
|
/**
|
|
204
220
|
* Adds a function definition in form of a arrow function to the file.
|
|
205
221
|
* @param {string} name - name of the function
|
|
206
|
-
* @param {[
|
|
222
|
+
* @param {import('./typedefs').visitor.ParamInfo[]} parameters - list of parameters, passed as [name, modifier, type] tuple
|
|
207
223
|
* @param {string} returns - the return type of the function
|
|
208
224
|
* @param {'function' | 'action'} kind - kind of the node
|
|
225
|
+
* @param {string[]} doc - documentation for the function
|
|
226
|
+
* @param {{positional?: boolean, named?: boolean}} callStyles - how the operation can be called
|
|
209
227
|
*/
|
|
210
|
-
addOperation(name, parameters, returns, kind) {
|
|
211
|
-
//this.operations.buffer.add(
|
|
212
|
-
this.operations.buffer.add(
|
|
228
|
+
addOperation(name, parameters, returns, kind, doc, callStyles) {
|
|
229
|
+
// this.operations.buffer.add(`// ${kind}`)
|
|
230
|
+
if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
|
|
231
|
+
this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})};`)
|
|
213
232
|
this.operations.names.push(name)
|
|
214
233
|
}
|
|
215
234
|
|
|
@@ -239,10 +258,11 @@ class SourceFile extends File {
|
|
|
239
258
|
* @param {string} fq - fully qualified name of the enum (entity name within CSN)
|
|
240
259
|
* @param {string} name - local name of the enum
|
|
241
260
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
261
|
+
* @param {string[]} doc - the enum docs
|
|
242
262
|
*/
|
|
243
|
-
addEnum(fq, name, kvs) {
|
|
263
|
+
addEnum(fq, name, kvs, doc) {
|
|
244
264
|
this.enums.data.push({ name, fq, kvs })
|
|
245
|
-
printEnum(this.enums.buffer, name, kvs)
|
|
265
|
+
printEnum(this.enums.buffer, name, kvs, {}, doc)
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
/**
|
|
@@ -251,6 +271,7 @@ class SourceFile extends File {
|
|
|
251
271
|
* @param {string} entityFqName - name of the entity the enum is attached to with namespace
|
|
252
272
|
* @param {string} propertyName - property to which the enum is attached.
|
|
253
273
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
274
|
+
* @param {string[]} doc - the enum docs
|
|
254
275
|
* If given, the enum is considered to be an inline definition of an enum.
|
|
255
276
|
* If not, it is considered to be regular, named enum.
|
|
256
277
|
* @example
|
|
@@ -274,14 +295,16 @@ class SourceFile extends File {
|
|
|
274
295
|
* }
|
|
275
296
|
* ```
|
|
276
297
|
*/
|
|
277
|
-
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs) {
|
|
298
|
+
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, doc=[]) {
|
|
278
299
|
this.enums.data.push({
|
|
279
300
|
name: `${entityCleanName}.${propertyName}`,
|
|
280
301
|
property: propertyName,
|
|
281
302
|
kvs,
|
|
282
303
|
fq: `${entityCleanName}.${propertyName}`
|
|
283
304
|
})
|
|
284
|
-
|
|
305
|
+
const entityProxy = this.entityProxies[entityCleanName] ?? (this.entityProxies[entityCleanName] = [])
|
|
306
|
+
entityProxy.push(propertyName)
|
|
307
|
+
printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
|
|
285
308
|
}
|
|
286
309
|
|
|
287
310
|
/**
|
|
@@ -363,11 +386,16 @@ class SourceFile extends File {
|
|
|
363
386
|
*/
|
|
364
387
|
getImports() {
|
|
365
388
|
const buffer = new Buffer()
|
|
389
|
+
if (this.services.names.length) {
|
|
390
|
+
// currently only needed to extend cds.Service and would trigger unused-variable-errors in strict configs
|
|
391
|
+
buffer.add('import cds from \'@sap/cds\'') // TODO should go to visitor#printService, but can't express this as Path
|
|
392
|
+
}
|
|
366
393
|
for (const imp of Object.values(this.imports)) {
|
|
367
394
|
if (!imp.isCwd(this.path.asDirectory())) {
|
|
368
395
|
buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
|
|
369
396
|
}
|
|
370
397
|
}
|
|
398
|
+
buffer.add('') // empty line after imports
|
|
371
399
|
return buffer
|
|
372
400
|
}
|
|
373
401
|
|
|
@@ -394,31 +422,92 @@ class SourceFile extends File {
|
|
|
394
422
|
namespaces.join() // needs to be after classes for possible declaration merging
|
|
395
423
|
].filter(Boolean).join('\n')
|
|
396
424
|
}
|
|
425
|
+
#getEntityProxyFunctionExport() {
|
|
426
|
+
return `module.exports.createEntityProxy = ${proxyAccessFunction}`
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Returns boilerplate code for `index.js` files
|
|
430
|
+
* - `useEntitiesProxy = true` -> import `createEntityProxy` function for entity proxy
|
|
431
|
+
* - `useEntitiesProxy = false` -> retrieve entities via `cds.entities(namespace)`
|
|
432
|
+
* @returns {string[]}
|
|
433
|
+
*/
|
|
434
|
+
#getJSExportBoilerplate() {
|
|
435
|
+
const namespace = this.path.asNamespace()
|
|
397
436
|
|
|
437
|
+
const boilerplate = [AUTO_GEN_NOTE]
|
|
438
|
+
if (configuration.useEntitiesProxy) {
|
|
439
|
+
if (namespace === '_') {
|
|
440
|
+
boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
|
|
441
|
+
} else {
|
|
442
|
+
boilerplate.push(`const { createEntityProxy } = require('${new Path(['_']).asDirectory({relative: this.path.asDirectory()})}')`)
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
boilerplate.push(
|
|
446
|
+
'const cds = require(\'@sap/cds\')',
|
|
447
|
+
`const csn = cds.entities('${namespace}')`
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
return boilerplate
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Returns RHS for entity `module.exports` assignments
|
|
454
|
+
* - `useEntitiesProxy = true` -> use function calls to create `Proxy` objects
|
|
455
|
+
* - `useEntitiesProxy = false` -> access entity from CSN directly
|
|
456
|
+
* @param {string} singular - singular name of entity
|
|
457
|
+
* @param {string} original - original name of entity
|
|
458
|
+
* @returns {{singularRhs: string, pluralRhs: string}}
|
|
459
|
+
*/
|
|
460
|
+
#getEntityExportsRhs(singular, original) {
|
|
461
|
+
if (configuration.useEntitiesProxy) {
|
|
462
|
+
const namespace = this.path.asNamespace()
|
|
463
|
+
// determine the custom properties for the proxy function call
|
|
464
|
+
const customProps = this.entityProxies[singular] ?? []
|
|
465
|
+
let customPropsStr = customProps.length ? `, customProps: ${JSON.stringify(customProps)}` : ''
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
|
|
469
|
+
pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
return {
|
|
473
|
+
singularRhs: `{ is_singular: true, __proto__: csn.${original} }`,
|
|
474
|
+
pluralRhs: `csn.${original}`
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
398
478
|
toJSExports() {
|
|
399
|
-
return
|
|
479
|
+
return this.#getJSExportBoilerplate() // boilerplate
|
|
400
480
|
.concat(
|
|
401
481
|
// FIXME: move stringification of service into own module
|
|
402
|
-
this.services.names.
|
|
482
|
+
this.services.names.flatMap(name => {
|
|
483
|
+
const nameSimple = name.split('.').pop()
|
|
484
|
+
return [
|
|
485
|
+
'// service',
|
|
486
|
+
`const ${nameSimple} = { name: '${name}' }`,
|
|
487
|
+
`module.exports = ${nameSimple}`, // there should be only one and must be the first
|
|
488
|
+
`module.exports.${nameSimple} = ${nameSimple}`
|
|
489
|
+
]
|
|
490
|
+
})
|
|
491
|
+
)
|
|
403
492
|
.concat(this.inflections
|
|
404
493
|
// sorting the entries based on the number of dots in their singular.
|
|
405
494
|
// that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
|
|
406
495
|
// "module.exports.Books" is defined before "module.exports.Books.text"
|
|
407
496
|
.sort(([a], [b]) => a.split('.').length - b.split('.').length)
|
|
408
497
|
.flatMap(([singular, plural, original]) => {
|
|
409
|
-
const
|
|
498
|
+
const { singularRhs, pluralRhs } = this.#getEntityExportsRhs(singular, original)
|
|
499
|
+
|
|
500
|
+
const exports = [`// ${original}`, `module.exports.${singular} = ${singularRhs}`]
|
|
410
501
|
if (!/Array<.*>/.test(plural) && plural !== original) {
|
|
411
502
|
// FIXME: this is a hack to support CDS types that will produce "Array<MyType>" as plural, which we do not want as export in the index.js files
|
|
412
|
-
exports.push(`module.exports.${plural} =
|
|
503
|
+
exports.push(`module.exports.${plural} = ${pluralRhs}`)
|
|
413
504
|
}
|
|
414
505
|
// FIXME: we currently produce at most 3 entries.
|
|
415
506
|
// This could be an issue when the user re-used the original name in a @singular/@plural annotation.
|
|
416
507
|
// Seems unlikely, but we have to eliminate the original entry if users start running into this.
|
|
417
508
|
if (singular !== original) {
|
|
418
509
|
// do not do the is_singular spiel if the original name is used for the plural
|
|
419
|
-
const rhs = plural === original
|
|
420
|
-
? `csn.${original}`
|
|
421
|
-
: `{ is_singular: true, __proto__: csn.${original} }`
|
|
510
|
+
const rhs = plural === original ? pluralRhs : singularRhs
|
|
422
511
|
exports.push(`module.exports.${original} = ${rhs}`)
|
|
423
512
|
}
|
|
424
513
|
return exports
|
|
@@ -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'))]
|
|
@@ -58,6 +59,14 @@ class Resolver {
|
|
|
58
59
|
return this.existsInCsn(fq) || Boolean(this.builtinResolver.resolveBuiltin(fq))
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* @param {EntityCSN} type - a CSN type
|
|
64
|
+
* @returns {boolean} whether the type is configured to be optional
|
|
65
|
+
*/
|
|
66
|
+
isOptional(type) {
|
|
67
|
+
return !type.notNull
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Returns all libraries that have been referenced at least once.
|
|
63
72
|
* @returns {Library[]}
|
|
@@ -150,8 +159,8 @@ class Resolver {
|
|
|
150
159
|
if (parts.length <= 1) return []
|
|
151
160
|
|
|
152
161
|
/**
|
|
153
|
-
* @param {string} property
|
|
154
|
-
* @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
|
|
155
164
|
*/
|
|
156
165
|
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
|
|
157
166
|
|
|
@@ -376,6 +385,10 @@ class Resolver {
|
|
|
376
385
|
plural: typeName
|
|
377
386
|
}
|
|
378
387
|
|
|
388
|
+
if (element.key === true) {
|
|
389
|
+
typeName = createKey(typeName)
|
|
390
|
+
}
|
|
391
|
+
|
|
379
392
|
// FIXME: typeName could probably just become part of typeInfo
|
|
380
393
|
return { typeName, typeInfo }
|
|
381
394
|
}
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export module resolver {
|
|
|
8
8
|
|
|
9
9
|
export type EntityCSN = {
|
|
10
10
|
actions?: OperationCSN[],
|
|
11
|
+
operations?: OperationCSN[],
|
|
11
12
|
cardinality?: { max?: '*' | string }
|
|
12
13
|
compositions?: { target: string }[]
|
|
13
14
|
doc?: string,
|
|
@@ -98,50 +99,76 @@ export module resolver {
|
|
|
98
99
|
|
|
99
100
|
export module util {
|
|
100
101
|
export type Annotations = {
|
|
101
|
-
name
|
|
102
|
+
name: string,
|
|
102
103
|
'@singular'?: string,
|
|
103
104
|
'@plural'?: string
|
|
104
105
|
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export module visitor {
|
|
109
|
+
export type Inflection = {
|
|
110
|
+
typeName?: string,
|
|
111
|
+
singular: string,
|
|
112
|
+
plural: string
|
|
113
|
+
}
|
|
105
114
|
|
|
106
|
-
export type
|
|
107
|
-
|
|
108
|
-
default?: any
|
|
115
|
+
export type Context = {
|
|
116
|
+
entity: string
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
export type
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
export type ParamInfo = {
|
|
120
|
+
name: string,
|
|
121
|
+
modifier: '' | '?',
|
|
122
|
+
type: string,
|
|
123
|
+
doc?: string
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
export module
|
|
118
|
-
export
|
|
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 = {
|
|
119
154
|
outputDirectory: string,
|
|
120
155
|
logLevel: number,
|
|
156
|
+
/**
|
|
157
|
+
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
|
158
|
+
*/
|
|
159
|
+
useEntitiesProxy: boolean,
|
|
121
160
|
jsConfigPath?: string,
|
|
122
|
-
inlineDeclarations: 'flat' | 'structured',
|
|
123
|
-
propertiesOptional: boolean,
|
|
124
|
-
IEEE754Compatible: boolean,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type VisitorOptions = {
|
|
128
|
-
/** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
|
|
129
|
-
propertiesOptional: boolean,
|
|
130
161
|
/**
|
|
131
162
|
* `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
|
|
132
163
|
* `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
|
|
133
164
|
*/
|
|
134
165
|
inlineDeclarations: 'flat' | 'structured',
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export type Context = {
|
|
144
|
-
entity: string
|
|
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
|
|
145
172
|
}
|
|
146
173
|
}
|
|
147
174
|
|