@cap-js/cds-typer 0.19.0 → 0.20.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 +18 -2
- package/lib/cli.js +4 -4
- package/lib/compile.js +12 -12
- package/lib/components/basedefs.js +64 -0
- package/lib/components/enum.js +22 -22
- package/lib/components/reference.js +1 -1
- package/lib/components/resolver.js +151 -74
- package/lib/components/typescript.js +3 -0
- package/lib/components/wrappers.js +11 -2
- package/lib/csn.js +31 -24
- package/lib/file.js +43 -87
- package/lib/util.js +27 -29
- package/lib/visitor.js +100 -89
- package/package.json +3 -3
package/lib/csn.js
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
const annotation = '@odata.draft.enabled'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* FIXME: this is pretty handwavey: we are looking for view-entities,
|
|
5
|
+
* i.e. ones that have a query, but are not a cds level projection.
|
|
6
|
+
* Those are still not expanded and we have to retrieve their definition
|
|
7
|
+
* with all properties from the inferred model.
|
|
8
|
+
*/
|
|
9
|
+
const isView = entity => entity.query && !entity.projection
|
|
10
|
+
|
|
11
|
+
const isProjection = entity => entity.projection
|
|
12
|
+
|
|
13
|
+
const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
|
|
14
|
+
|
|
15
|
+
const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
|
|
16
|
+
|
|
17
|
+
const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
|
|
18
|
+
|
|
19
|
+
const isType = entity => entity?.kind === 'type'
|
|
20
|
+
|
|
21
|
+
const isEntity = entity => entity?.kind === 'entity'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @see isView
|
|
25
|
+
* Unresolved entities have to be looked up from inferred csn.
|
|
26
|
+
*/
|
|
27
|
+
const isUnresolved = entity => entity._unresolved === true
|
|
28
|
+
|
|
29
|
+
const isCsnAny = entity => entity?.constructor?.name === 'any'
|
|
30
|
+
|
|
3
31
|
class DraftUnroller {
|
|
4
32
|
/** @type {Set<string>} */
|
|
5
33
|
#positives = new Set()
|
|
@@ -226,19 +254,6 @@ function amendCSN(csn) {
|
|
|
226
254
|
propagateForeignKeys(csn)
|
|
227
255
|
}
|
|
228
256
|
|
|
229
|
-
/**
|
|
230
|
-
* FIXME: this is pretty handwavey: we are looking for view-entities,
|
|
231
|
-
* i.e. ones that have a query, but are not a cds level projection.
|
|
232
|
-
* Those are still not expanded and we have to retrieve their definition
|
|
233
|
-
* with all properties from the inferred model.
|
|
234
|
-
*/
|
|
235
|
-
const isView = entity => entity.query && !entity.projection
|
|
236
|
-
|
|
237
|
-
const isProjection = entity => entity.projection
|
|
238
|
-
|
|
239
|
-
const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
|
|
240
|
-
|
|
241
|
-
const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
|
|
242
257
|
|
|
243
258
|
const getProjectionAliases = entity => {
|
|
244
259
|
const aliases = {}
|
|
@@ -255,25 +270,17 @@ const getProjectionAliases = entity => {
|
|
|
255
270
|
return { aliases, all }
|
|
256
271
|
}
|
|
257
272
|
|
|
258
|
-
const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
|
|
259
|
-
|
|
260
|
-
const isType = entity => entity?.kind === 'type'
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* @see isView
|
|
264
|
-
* Unresolved entities have to be looked up from inferred csn.
|
|
265
|
-
*/
|
|
266
|
-
const isUnresolved = entity => entity._unresolved === true
|
|
267
|
-
|
|
268
273
|
module.exports = {
|
|
269
274
|
amendCSN,
|
|
270
275
|
isView,
|
|
271
276
|
isProjection,
|
|
272
277
|
isDraftEnabled,
|
|
278
|
+
isEntity,
|
|
273
279
|
isUnresolved,
|
|
274
280
|
isType,
|
|
275
281
|
getProjectionTarget,
|
|
276
282
|
getProjectionAliases,
|
|
277
283
|
getViewTarget,
|
|
278
|
-
propagateForeignKeys
|
|
284
|
+
propagateForeignKeys,
|
|
285
|
+
isCsnAny
|
|
279
286
|
}
|
package/lib/file.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const fs = require('fs').promises
|
|
4
|
+
const path = require('path')
|
|
4
5
|
const { readFileSync } = require('fs')
|
|
5
6
|
const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
|
|
6
7
|
const { normalise } = require('./components/identifier')
|
|
7
|
-
const
|
|
8
|
+
const { empty } = require('./components/typescript')
|
|
9
|
+
const { createObjectOf } = require('./components/wrappers')
|
|
8
10
|
|
|
9
|
-
const AUTO_GEN_NOTE =
|
|
11
|
+
const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
|
|
10
12
|
|
|
11
13
|
/** @typedef {Object<string, Buffer>} Namespace */
|
|
12
14
|
|
|
@@ -115,7 +117,7 @@ class SourceFile extends File {
|
|
|
115
117
|
/** @type {Buffer} */
|
|
116
118
|
this.classes = new Buffer()
|
|
117
119
|
/** @type {{ buffer: Buffer, names: string[]}} */
|
|
118
|
-
this.
|
|
120
|
+
this.operations = { buffer: new Buffer(), names: [] }
|
|
119
121
|
/** @type {Buffer} */
|
|
120
122
|
this.aspects = new Buffer()
|
|
121
123
|
/** @type {Namespace} */
|
|
@@ -154,15 +156,19 @@ class SourceFile extends File {
|
|
|
154
156
|
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
|
|
155
157
|
* ```
|
|
156
158
|
*/
|
|
157
|
-
static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false}) {
|
|
159
|
+
static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false, kind}) {
|
|
158
160
|
const parameterTypes = parameters.map(([n, t]) => `${normalise(n)}: ${t}`).join(', ')
|
|
161
|
+
const parameterTypeAsObject = parameterTypes.length
|
|
162
|
+
? createObjectOf(parameterTypes)
|
|
163
|
+
: empty
|
|
159
164
|
const callableSignature = `(${parameterTypes}): ${returns}`
|
|
160
165
|
let prefix = name ? `${normalise(name)}: `: ''
|
|
161
166
|
if (prefix && isStatic) {
|
|
162
167
|
prefix = `static ${prefix}`
|
|
163
168
|
}
|
|
169
|
+
const kindDef = kind ? `, kind: '${kind}'` : ''
|
|
164
170
|
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
165
|
-
const lambda = `{ ${callableSignature}, __parameters:
|
|
171
|
+
const lambda = `{ ${callableSignature}, __parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
|
|
166
172
|
return prefix + lambda + suffix
|
|
167
173
|
}
|
|
168
174
|
|
|
@@ -183,25 +189,13 @@ class SourceFile extends File {
|
|
|
183
189
|
* Adds a function definition in form of a arrow function to the file.
|
|
184
190
|
* @param {string} name name of the function
|
|
185
191
|
* @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
|
|
192
|
+
* @param {'function' | 'action'} kind
|
|
186
193
|
* @param returns the return type of the function
|
|
187
194
|
*/
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
this.
|
|
191
|
-
this.
|
|
192
|
-
this.actions.names.push(name)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Adds an action definition in form of a arrow function to the file.
|
|
197
|
-
* @param {string} name name of the action
|
|
198
|
-
* @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
|
|
199
|
-
* @param returns the return type of the action
|
|
200
|
-
*/
|
|
201
|
-
addAction(name, parameters, returns) {
|
|
202
|
-
this.actions.buffer.add("// action")
|
|
203
|
-
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
|
|
204
|
-
this.actions.names.push(name)
|
|
195
|
+
addOperation(name, parameters, returns, kind) {
|
|
196
|
+
//this.operations.buffer.add("// operation")
|
|
197
|
+
this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind})};`)
|
|
198
|
+
this.operations.names.push(name)
|
|
205
199
|
}
|
|
206
200
|
|
|
207
201
|
/**
|
|
@@ -382,38 +376,38 @@ class SourceFile extends File {
|
|
|
382
376
|
this.aspects.join(), // needs to be before classes
|
|
383
377
|
this.classes.join(),
|
|
384
378
|
this.events.buffer.join(),
|
|
385
|
-
this.
|
|
379
|
+
this.operations.buffer.join(),
|
|
386
380
|
namespaces.join() // needs to be after classes for possible declaration merging
|
|
387
381
|
].filter(Boolean).join('\n')
|
|
388
382
|
}
|
|
389
383
|
|
|
390
384
|
toJSExports() {
|
|
391
|
-
return [AUTO_GEN_NOTE,
|
|
385
|
+
return [AUTO_GEN_NOTE, 'const cds = require(\'@sap/cds\')', `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
|
|
392
386
|
.concat(
|
|
393
387
|
// FIXME: move stringification of service into own module
|
|
394
388
|
this.services.names.map(name => `module.exports = { name: '${name}' }`)) // there should be only one
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
389
|
+
.concat(this.inflections
|
|
390
|
+
// sorting the entries based on the number of dots in their singular.
|
|
391
|
+
// that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
|
|
392
|
+
// "module.exports.Books" is defined before "module.exports.Books.text"
|
|
393
|
+
.sort(([a], [b]) => a.split('.').length - b.split('.').length)
|
|
394
|
+
// by using a temporary Set we make sure to catch cases where
|
|
395
|
+
// (1) plural is the same as original (default case) and
|
|
396
|
+
// (2) plural differs from original, i.e. when a @plural annotation is present
|
|
397
|
+
// or when plural4 produced weird inflection.
|
|
398
|
+
.flatMap(([singular, plural, original]) => Array.from(new Set([
|
|
399
|
+
`module.exports.${singular} = csn.${original}`,
|
|
400
|
+
/Array<.*>/.test(plural) ? undefined : `module.exports.${plural} = csn.${original}`,
|
|
401
|
+
// FIXME: we currently produce at most 3 entries.
|
|
402
|
+
// This could be an issue when the user re-used the original name in a @singular/@plural annotation.
|
|
403
|
+
// Seems unlikely, but we have to eliminate the original entry if users start running into this.
|
|
404
|
+
`module.exports.${original} = csn.${original}`
|
|
405
|
+
].filter(Boolean)))) // 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
406
|
) // singular -> plural aliases
|
|
413
407
|
.concat(['// events'])
|
|
414
408
|
.concat(this.events.fqs.map(({fq, name}) => `module.exports.${name} = '${fq}'`))
|
|
415
409
|
.concat(['// actions'])
|
|
416
|
-
.concat(this.
|
|
410
|
+
.concat(this.operations.names.map(name => `module.exports.${name} = '${name}'`))
|
|
417
411
|
.concat(['// enums'])
|
|
418
412
|
.concat(this.enums.data.map(({name, kvs}) => stringifyEnumImplementation(name, kvs)))
|
|
419
413
|
.join('\n') + '\n'
|
|
@@ -494,7 +488,7 @@ class Buffer {
|
|
|
494
488
|
if (typeof part === 'function') {
|
|
495
489
|
part()
|
|
496
490
|
} else if (Array.isArray(part)) {
|
|
497
|
-
part.forEach(p => this.add(p))
|
|
491
|
+
part.forEach(p => { this.add(p) })
|
|
498
492
|
}
|
|
499
493
|
this.outdent()
|
|
500
494
|
}
|
|
@@ -610,44 +604,6 @@ class FileRepository {
|
|
|
610
604
|
}
|
|
611
605
|
}
|
|
612
606
|
|
|
613
|
-
/**
|
|
614
|
-
* Base definitions used throughout the typing process,
|
|
615
|
-
* such as Associations and Compositions.
|
|
616
|
-
* @type {SourceFile}
|
|
617
|
-
*/
|
|
618
|
-
const baseDefinitions = new SourceFile('_')
|
|
619
|
-
// FIXME: this should be a library someday
|
|
620
|
-
baseDefinitions.addPreamble(`
|
|
621
|
-
export namespace Association {
|
|
622
|
-
export type to <T> = T;
|
|
623
|
-
export namespace to {
|
|
624
|
-
export type many <T extends readonly any[]> = T;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
export namespace Composition {
|
|
629
|
-
export type of <T> = T;
|
|
630
|
-
export namespace of {
|
|
631
|
-
export type many <T extends readonly any[]> = T;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
export class Entity {
|
|
636
|
-
static data<T extends Entity> (this:T, _input:Object) : T {
|
|
637
|
-
return {} as T // mock
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
export type EntitySet<T> = T[] & {
|
|
642
|
-
data (input:object[]) : T[]
|
|
643
|
-
data (input:object) : T
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
export type DeepRequired<T> = {
|
|
647
|
-
[K in keyof T]: DeepRequired<T[K]>
|
|
648
|
-
} & Exclude<Required<T>, null>;
|
|
649
|
-
`)
|
|
650
|
-
|
|
651
607
|
/**
|
|
652
608
|
* Writes the files to disk. For each source, a index.d.ts holding the type definitions
|
|
653
609
|
* and a index.js holding implementation stubs is generated at the appropriate directory.
|
|
@@ -658,18 +614,19 @@ export type DeepRequired<T> = {
|
|
|
658
614
|
*/
|
|
659
615
|
const writeout = async (root, sources) =>
|
|
660
616
|
Promise.all(
|
|
661
|
-
sources.map(async
|
|
617
|
+
sources.map(async source => {
|
|
662
618
|
const dir = path.join(root, source.path.asDirectory({local: false, posix: false}))
|
|
663
619
|
try {
|
|
664
620
|
await fs.mkdir(dir, { recursive: true })
|
|
665
621
|
await Promise.all([
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
622
|
+
fs.writeFile(path.join(dir, 'index.ts'), source.toTypeDefs()),
|
|
623
|
+
fs.writeFile(path.join(dir, 'index.js'), source.toJSExports()),
|
|
624
|
+
])
|
|
669
625
|
|
|
670
626
|
} catch (err) {
|
|
671
627
|
// eslint-disable-next-line no-console
|
|
672
628
|
console.error(`Could not create parent directory ${dir}: ${err}.`)
|
|
629
|
+
// eslint-disable-next-line no-console
|
|
673
630
|
console.error(err.stack)
|
|
674
631
|
}
|
|
675
632
|
return dir
|
|
@@ -683,6 +640,5 @@ module.exports = {
|
|
|
683
640
|
FileRepository,
|
|
684
641
|
SourceFile,
|
|
685
642
|
Path,
|
|
686
|
-
writeout
|
|
687
|
-
baseDefinitions,
|
|
643
|
+
writeout
|
|
688
644
|
}
|
package/lib/util.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable indent */
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @typedef { {name?: string, '@singular'?: string, '@plural'?: string,} } Annotations
|
|
5
3
|
* @typedef { {desc: string, default?: any} } CommandlineFlag
|
|
@@ -30,7 +28,7 @@ const annotations = {
|
|
|
30
28
|
* @param {Object} csn the CSN of an entity to check
|
|
31
29
|
* @returns {string | undefined} the singular annotation or undefined
|
|
32
30
|
*/
|
|
33
|
-
const getSingularAnnotation =
|
|
31
|
+
const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
|
|
34
32
|
|
|
35
33
|
/**
|
|
36
34
|
* Tries to retrieve an annotation that specifies the plural name
|
|
@@ -40,9 +38,9 @@ const getSingularAnnotation = (csn) => csn[annotations.singular.find(a => Object
|
|
|
40
38
|
* @param {Object} csn the CSN of an entity to check
|
|
41
39
|
* @returns {string | undefined} the plural annotation or undefined
|
|
42
40
|
*/
|
|
43
|
-
const getPluralAnnotation =
|
|
41
|
+
const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
/**
|
|
46
44
|
* Users can specify that they want to refer to localisation
|
|
47
45
|
* using the syntax {i18n>Foo}, where Foo is the name of the
|
|
48
46
|
* entity as found in the .cds file
|
|
@@ -53,9 +51,9 @@ const getPluralAnnotation = (csn) => csn[annotations.plural.find(a => Object.has
|
|
|
53
51
|
* @returns {string} the name without localisation syntax or untouched.
|
|
54
52
|
* @deprecated we have dropped this feature altogether, users specify custom names via @singular/@plural now
|
|
55
53
|
*/
|
|
56
|
-
const unlocalize =
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const unlocalize = name => {
|
|
55
|
+
const match = name.match(/\{i18n>(.*)\}/)
|
|
56
|
+
return match ? match[1] : name
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
/**
|
|
@@ -74,19 +72,19 @@ const singular4 = (dn, stripped = false) => {
|
|
|
74
72
|
(/.*species|news$/i.test(n)
|
|
75
73
|
? n
|
|
76
74
|
: /.*ess$/.test(n)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
? n // Address
|
|
76
|
+
: /.*ees$/.test(n)
|
|
77
|
+
? n.slice(0, -1) // Employees --> Employee
|
|
78
|
+
: /.*[sz]es$/.test(n)
|
|
79
|
+
? n.slice(0, -2)
|
|
80
|
+
: /.*[^aeiou]ies$/.test(n)
|
|
81
|
+
? n.slice(0, -3) + 'y' // Deliveries --> Delivery
|
|
82
|
+
: /.*s$/.test(n)
|
|
83
|
+
? n.slice(0, -1)
|
|
84
|
+
: /.*_$/.test(n) // special cdstyper case where we revert the _ suffix for when a plural can not be determined
|
|
85
|
+
? n.slice(0, -1)
|
|
86
|
+
: n
|
|
87
|
+
)
|
|
90
88
|
)
|
|
91
89
|
}
|
|
92
90
|
|
|
@@ -106,10 +104,10 @@ const plural4 = (dn, stripped) => {
|
|
|
106
104
|
(/.*analysis|status|species|news$/i.test(n)
|
|
107
105
|
? n
|
|
108
106
|
: /.*[^aeiou]y$/.test(n)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
? n.slice(0, -1) + 'ies'
|
|
108
|
+
: /.*(s|x|z|ch|sh)$/.test(n)
|
|
109
|
+
? n + 'es'
|
|
110
|
+
: n + 's')
|
|
113
111
|
)
|
|
114
112
|
}
|
|
115
113
|
|
|
@@ -121,8 +119,8 @@ const plural4 = (dn, stripped) => {
|
|
|
121
119
|
*/
|
|
122
120
|
const deepMerge = (target, source) => {
|
|
123
121
|
Object.keys(target)
|
|
124
|
-
.filter(
|
|
125
|
-
.forEach(
|
|
122
|
+
.filter(k => k in source)
|
|
123
|
+
.forEach(k => { deepMerge(target[k], source[k]) })
|
|
126
124
|
Object.assign(target, source)
|
|
127
125
|
}
|
|
128
126
|
|
|
@@ -141,7 +139,7 @@ const deepMerge = (target, source) => {
|
|
|
141
139
|
* @returns {ParsedFlags}
|
|
142
140
|
*/
|
|
143
141
|
const parseCommandlineArgs = (argv, validFlags) => {
|
|
144
|
-
const isFlag =
|
|
142
|
+
const isFlag = arg => arg.startsWith('--')
|
|
145
143
|
const positional = []
|
|
146
144
|
const named = {}
|
|
147
145
|
|
|
@@ -173,7 +171,7 @@ const parseCommandlineArgs = (argv, validFlags) => {
|
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
const defaults = Object.entries(validFlags)
|
|
176
|
-
.filter(
|
|
174
|
+
.filter(e => !!e[1].default)
|
|
177
175
|
.reduce((dict, [k, v]) => {
|
|
178
176
|
dict[k] = v.default
|
|
179
177
|
return dict
|