@cap-js/cds-typer 0.24.0 → 0.25.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 +14 -1
- package/lib/cli.js +24 -14
- package/lib/compile.js +3 -3
- package/lib/components/enum.js +16 -9
- package/lib/components/identifier.js +1 -1
- package/lib/components/inline.js +81 -26
- package/lib/components/property.js +12 -0
- package/lib/components/wrappers.js +1 -1
- package/lib/csn.js +90 -26
- package/lib/file.js +43 -19
- package/lib/logging.js +5 -1
- package/lib/resolution/builtin.js +3 -2
- package/lib/resolution/entity.js +46 -7
- package/lib/resolution/resolver.js +51 -20
- package/lib/typedefs.d.ts +67 -13
- package/lib/util.js +4 -3
- package/lib/visitor.js +163 -59
- package/package.json +4 -2
|
@@ -12,11 +12,14 @@ const { baseDefinitions } = require('../components/basedefs')
|
|
|
12
12
|
const { BuiltinResolver } = require('./builtin')
|
|
13
13
|
const { LOG } = require('../logging')
|
|
14
14
|
const { last } = require('../components/identifier')
|
|
15
|
+
const { getPropertyModifiers } = require('../components/property')
|
|
15
16
|
|
|
16
17
|
/** @typedef {import('../visitor').Visitor} Visitor */
|
|
17
18
|
/** @typedef {import('../typedefs').resolver.CSN} CSN */
|
|
19
|
+
/** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
|
|
18
20
|
/** @typedef {import('../typedefs').resolver.TypeResolveInfo} TypeResolveInfo */
|
|
19
|
-
/** @typedef {import('../typedefs').visitor.Inflection}
|
|
21
|
+
/** @typedef {import('../typedefs').visitor.Inflection} Inflection */
|
|
22
|
+
/** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
|
|
20
23
|
|
|
21
24
|
class Resolver {
|
|
22
25
|
get csn() { return this.visitor.csn.inferred }
|
|
@@ -86,7 +89,7 @@ class Resolver {
|
|
|
86
89
|
return {
|
|
87
90
|
namespace: new Path(ns.split('.')),
|
|
88
91
|
scope: nameParts.slice(0, -1),
|
|
89
|
-
name: nameParts.at(-1),
|
|
92
|
+
name: nameParts.at(-1) ?? nameAndProperty,
|
|
90
93
|
property
|
|
91
94
|
}
|
|
92
95
|
}
|
|
@@ -146,10 +149,16 @@ class Resolver {
|
|
|
146
149
|
const parts = p.split('.')
|
|
147
150
|
if (parts.length <= 1) return []
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} property
|
|
154
|
+
* @param {import('../typedefs').resolver.EntityCSN} entity
|
|
155
|
+
*/
|
|
156
|
+
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
|
|
150
157
|
|
|
151
158
|
const defs = this.visitor.csn.inferred.definitions
|
|
152
159
|
// assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
|
|
160
|
+
/** @type {string} */
|
|
161
|
+
// @ts-expect-error - nope, we know there is at least one element
|
|
153
162
|
let qualifier = parts.shift()
|
|
154
163
|
// find first entity from left (Entity1)
|
|
155
164
|
while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
|
|
@@ -175,7 +184,7 @@ class Resolver {
|
|
|
175
184
|
* - inline type definitions, which don't really have a linguistic plural,
|
|
176
185
|
* but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
|
|
177
186
|
* @param {import('./resolver').TypeResolveInfo} typeInfo - information about the type gathered so far.
|
|
178
|
-
* @param {string} [namespace] - namespace the type occurs in. If passed, will be shaved off from the name
|
|
187
|
+
* @param {string | import('../file').Path} [namespace] - namespace the type occurs in. If passed, will be shaved off from the name
|
|
179
188
|
* @returns {Inflection}
|
|
180
189
|
*/
|
|
181
190
|
inflect(typeInfo, namespace) {
|
|
@@ -189,7 +198,11 @@ class Resolver {
|
|
|
189
198
|
}
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
|
|
201
|
+
if (namespace instanceof Path) {
|
|
202
|
+
namespace = namespace.asNamespace()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let typeName = ''
|
|
193
206
|
let singular
|
|
194
207
|
let plural
|
|
195
208
|
|
|
@@ -204,7 +217,13 @@ class Resolver {
|
|
|
204
217
|
// If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
|
|
205
218
|
// piece of code instead to reduce overhead.
|
|
206
219
|
const into = new Buffer()
|
|
207
|
-
this.structuredInlineResolver.printInlineType(
|
|
220
|
+
this.structuredInlineResolver.printInlineType({
|
|
221
|
+
fq: '',
|
|
222
|
+
type: { typeInfo, typeName: '' },
|
|
223
|
+
buffer: into,
|
|
224
|
+
statementEnd: '',
|
|
225
|
+
modifiers: getPropertyModifiers(typeInfo.csn)
|
|
226
|
+
})
|
|
208
227
|
typeName = into.join(' ')
|
|
209
228
|
singular = typeName
|
|
210
229
|
plural = createArrayOf(typeName)
|
|
@@ -246,9 +265,9 @@ class Resolver {
|
|
|
246
265
|
* 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
|
|
247
266
|
* 2. resolve any singular/ plural issues and association/ composition around it
|
|
248
267
|
* 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
|
|
249
|
-
* @param {
|
|
268
|
+
* @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
|
|
250
269
|
* @param {SourceFile} file - source file for context.
|
|
251
|
-
* @returns {
|
|
270
|
+
* @returns {ResolveAndRequireInfo} info about the resolved type
|
|
252
271
|
*/
|
|
253
272
|
resolveAndRequire(element, file) {
|
|
254
273
|
const typeInfo = this.resolveType(element, file)
|
|
@@ -266,9 +285,15 @@ class Resolver {
|
|
|
266
285
|
}[element.constructor.name] ?? []
|
|
267
286
|
|
|
268
287
|
if (toOne && toMany) {
|
|
269
|
-
|
|
288
|
+
/** @type { EntityCSN | { type: string } } */
|
|
289
|
+
// @ts-expect-error - nope, it is not undefined
|
|
290
|
+
const target = element.items ?? (typeof element.target === 'string'
|
|
291
|
+
? { type: element.target }
|
|
292
|
+
: element.target)
|
|
270
293
|
/** set `notNull = true` to avoid repeated `| not null` TS construction */
|
|
294
|
+
// @ts-expect-error - yes, we know that notNull is not part of the type in some cases
|
|
271
295
|
target.notNull = true
|
|
296
|
+
// @ts-expect-error - yes, target is a valid parameter
|
|
272
297
|
const targetTypeInfo = this.resolveAndRequire(target, file)
|
|
273
298
|
if (targetTypeInfo.typeInfo.isDeepRequire === true) {
|
|
274
299
|
typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
|
|
@@ -280,7 +305,7 @@ class Resolver {
|
|
|
280
305
|
// But we can't just fix it in inflection(...), as that would break several other things
|
|
281
306
|
// So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
|
|
282
307
|
if (target.type) {
|
|
283
|
-
const untangled = this.visitor.entityRepository.
|
|
308
|
+
const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
|
|
284
309
|
const scope = untangled.scope.join('.')
|
|
285
310
|
if (scope && !singular.startsWith(scope)) {
|
|
286
311
|
singular = `${scope}.${singular}`
|
|
@@ -359,7 +384,7 @@ class Resolver {
|
|
|
359
384
|
* Resolves the fully qualified name of an entity to its parent entity.
|
|
360
385
|
* resolveParent(a.b.c.D) -> CSN {a.b.c}
|
|
361
386
|
* @param {string} name - fully qualified name of the entity to resolve the parent of.
|
|
362
|
-
* @returns {
|
|
387
|
+
* @returns {import('../typedefs').resolver.EntityCSN} the resolved parent CSN.
|
|
363
388
|
*/
|
|
364
389
|
resolveParent(name) {
|
|
365
390
|
return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
|
|
@@ -394,14 +419,20 @@ class Resolver {
|
|
|
394
419
|
/**
|
|
395
420
|
* Resolves an element's type to either a builtin or a user defined type.
|
|
396
421
|
* Enriched with additional information for improved printout (see return type).
|
|
397
|
-
* @param {
|
|
422
|
+
* @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
|
|
398
423
|
* @param {SourceFile} file - source file for context.
|
|
399
424
|
* @returns {TypeResolveInfo} description of the resolved type
|
|
400
425
|
*/
|
|
401
426
|
resolveType(element, file) {
|
|
402
427
|
// while resolving inline declarations, it can happen that we land here
|
|
403
428
|
// with an already resolved type. In that case, just return the type we have.
|
|
404
|
-
|
|
429
|
+
// type guard check purely to satisfy return statement
|
|
430
|
+
/**
|
|
431
|
+
* @param {any} e - the element to check
|
|
432
|
+
* @returns {e is TypeResolveInfo}
|
|
433
|
+
*/
|
|
434
|
+
const isBuiltin = e => Object.hasOwn(e ?? {}, 'isBuiltin')
|
|
435
|
+
if (isBuiltin(element)) return element
|
|
405
436
|
|
|
406
437
|
const cardinality = getMaxCardinality(element)
|
|
407
438
|
|
|
@@ -447,7 +478,7 @@ class Resolver {
|
|
|
447
478
|
// this.#resolveInlineDeclarationType(element.enum, result, file)
|
|
448
479
|
// or
|
|
449
480
|
// stringifyEnumType(csnToEnumPairs(element))
|
|
450
|
-
this
|
|
481
|
+
this.resolveTypeName(element.type, result)
|
|
451
482
|
}
|
|
452
483
|
} else {
|
|
453
484
|
this.resolvePotentialReferenceType(element.type, result, file)
|
|
@@ -482,7 +513,7 @@ class Resolver {
|
|
|
482
513
|
* }
|
|
483
514
|
*
|
|
484
515
|
* These have to be resolved to a new type.
|
|
485
|
-
* @param {
|
|
516
|
+
* @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
|
|
486
517
|
* @param {TypeResolveInfo} into - @see resolveType()
|
|
487
518
|
* @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
|
|
488
519
|
* This is important to correctly detect when a field in the inline declaration is referencing
|
|
@@ -503,11 +534,11 @@ class Resolver {
|
|
|
503
534
|
if (val.elements) {
|
|
504
535
|
this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
|
|
505
536
|
} else if (val.constructor === Object && 'ref' in val) {
|
|
506
|
-
this
|
|
537
|
+
this.resolveTypeName(val.ref[0], into)
|
|
507
538
|
into.isForeignKeyReference = true
|
|
508
539
|
} else {
|
|
509
540
|
// val is string
|
|
510
|
-
this
|
|
541
|
+
this.resolveTypeName(val, into)
|
|
511
542
|
}
|
|
512
543
|
}
|
|
513
544
|
|
|
@@ -516,11 +547,11 @@ class Resolver {
|
|
|
516
547
|
* String is supposed to refer to either a builtin type
|
|
517
548
|
* or any type defined in CSN.
|
|
518
549
|
* @param {string} t - fully qualified type, like cds.String, or a.b.c.d.Foo
|
|
519
|
-
* @param {TypeResolveInfo} into - optional dictionary to fill by reference, see resolveType()
|
|
550
|
+
* @param {TypeResolveInfo} [into] - optional dictionary to fill by reference, see resolveType()
|
|
520
551
|
* @returns @see resolveType
|
|
521
552
|
*/
|
|
522
|
-
|
|
523
|
-
const result = into
|
|
553
|
+
resolveTypeName(t, into) {
|
|
554
|
+
const result = into ?? {}
|
|
524
555
|
const path = t.split('.')
|
|
525
556
|
const builtin = this.builtinResolver.resolveBuiltin(path)
|
|
526
557
|
if (builtin === undefined) {
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
export module resolver {
|
|
2
|
+
type ref = {
|
|
3
|
+
ref: string[],
|
|
4
|
+
as?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type PropertyModifier = 'override' | 'declare'
|
|
8
|
+
|
|
2
9
|
export type EntityCSN = {
|
|
3
|
-
|
|
10
|
+
actions?: OperationCSN[],
|
|
11
|
+
cardinality?: { max?: '*' | string }
|
|
12
|
+
compositions?: { target: string }[]
|
|
13
|
+
doc?: string,
|
|
14
|
+
elements?: { [key: string]: EntityCSN }
|
|
15
|
+
key?: string // custom!!
|
|
16
|
+
keys?: { [key:string]: any }
|
|
17
|
+
kind: string,
|
|
18
|
+
includes?: string[]
|
|
19
|
+
items?: EntityCSN
|
|
20
|
+
notNull?: boolean, // custom!
|
|
21
|
+
on?: string,
|
|
22
|
+
parent?: EntityCSN
|
|
23
|
+
projection?: { from: ref, columns: (ref | '*')[]}
|
|
24
|
+
target?: string,
|
|
25
|
+
type: string | ref,
|
|
26
|
+
name: string,
|
|
27
|
+
'@odata.draft.enabled'?: boolean // custom!
|
|
28
|
+
_unresolved?: boolean
|
|
29
|
+
isRefNotNull?: boolean // custom!
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OperationCSN = EntityCSN & {
|
|
33
|
+
params: {[key:string]: EntityCSN},
|
|
34
|
+
returns?: any,
|
|
35
|
+
kind: 'action' | 'function'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ProjectionCSN = EntityCSN & {
|
|
39
|
+
projection: any
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ViewCSN = EntityCSN & {
|
|
43
|
+
query?: any
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export type EnumCSN = EntityCSN & {
|
|
48
|
+
enum: {[key:name]: string}
|
|
4
49
|
}
|
|
5
50
|
|
|
6
51
|
export type CSN = {
|
|
7
|
-
definitions
|
|
52
|
+
definitions: { [key: string]: EntityCSN },
|
|
8
53
|
}
|
|
9
54
|
|
|
10
55
|
/**
|
|
@@ -19,19 +64,25 @@ export module resolver {
|
|
|
19
64
|
* ```
|
|
20
65
|
*/
|
|
21
66
|
export type TypeResolveInfo = {
|
|
22
|
-
isBuiltin
|
|
23
|
-
isDeepRequire
|
|
24
|
-
isNotNull
|
|
25
|
-
isInlineDeclaration
|
|
26
|
-
isForeignKeyReference
|
|
27
|
-
isArray
|
|
28
|
-
type
|
|
67
|
+
isBuiltin?: boolean,
|
|
68
|
+
isDeepRequire?: boolean,
|
|
69
|
+
isNotNull?: boolean,
|
|
70
|
+
isInlineDeclaration?: boolean,
|
|
71
|
+
isForeignKeyReference?: boolean,
|
|
72
|
+
isArray?: boolean,
|
|
73
|
+
type?: string,
|
|
29
74
|
path?: Path,
|
|
30
|
-
csn?:
|
|
31
|
-
imports
|
|
32
|
-
inner
|
|
75
|
+
csn?: EntityCSNCSN,
|
|
76
|
+
imports?: Path[]
|
|
77
|
+
inner?: TypeResolveInfo,
|
|
78
|
+
structuredType?: {[key: string]: {typeName: string, typeInfo: TypeResolveInfo}} // FIXME: same as inner?
|
|
79
|
+
plainName: string,
|
|
80
|
+
typeName?: string // FIXME: same as plainName?
|
|
81
|
+
inflection?: visitor.Inflection
|
|
33
82
|
}
|
|
34
83
|
|
|
84
|
+
export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
|
|
85
|
+
|
|
35
86
|
// TODO: this will be completely replaced by EntityInfo
|
|
36
87
|
export type Untangled = {
|
|
37
88
|
// scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
|
|
@@ -68,6 +119,9 @@ export module visitor {
|
|
|
68
119
|
outputDirectory: string,
|
|
69
120
|
logLevel: number,
|
|
70
121
|
jsConfigPath?: string,
|
|
122
|
+
inlineDeclarations: 'flat' | 'structured',
|
|
123
|
+
propertiesOptional: boolean,
|
|
124
|
+
IEEE754Compatible: boolean,
|
|
71
125
|
}
|
|
72
126
|
|
|
73
127
|
export type VisitorOptions = {
|
|
@@ -81,7 +135,7 @@ export module visitor {
|
|
|
81
135
|
}
|
|
82
136
|
|
|
83
137
|
export type Inflection = {
|
|
84
|
-
typeName
|
|
138
|
+
typeName?: string,
|
|
85
139
|
singular: string,
|
|
86
140
|
plural: string
|
|
87
141
|
}
|
package/lib/util.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @typedef { import('./typedefs').util.Annotations} Annotations */
|
|
2
2
|
/** @typedef { import('./typedefs').util.CommandLineFlags } CommandlineFlag */
|
|
3
3
|
/** @typedef { import('./typedefs').util.ParsedFlag } ParsedFlags */
|
|
4
|
+
/** @typedef { import('./typedefs').resolver.EntityCSN } EntityCSN */
|
|
4
5
|
|
|
5
6
|
// inflection functions are stolen from github/cap/dev/blob/main/etc/inflect.js
|
|
6
7
|
|
|
@@ -23,7 +24,7 @@ const annotations = {
|
|
|
23
24
|
* from a CSN. Valid annotations are listed in util.annotations
|
|
24
25
|
* and their precedence is in order of definition.
|
|
25
26
|
* If no singular is specified at all, undefined is returned.
|
|
26
|
-
* @param {
|
|
27
|
+
* @param {EntityCSN} csn - the CSN of an entity to check
|
|
27
28
|
* @returns {string | undefined} the singular annotation or undefined
|
|
28
29
|
*/
|
|
29
30
|
const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
|
|
@@ -33,7 +34,7 @@ const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.h
|
|
|
33
34
|
* from a CSN. Valid annotations are listed in util.annotations
|
|
34
35
|
* and their precedence is in order of definition.
|
|
35
36
|
* If no plural is specified at all, undefined is returned.
|
|
36
|
-
* @param {
|
|
37
|
+
* @param {EntityCSN} csn - the CSN of an entity to check
|
|
37
38
|
* @returns {string | undefined} the plural annotation or undefined
|
|
38
39
|
*/
|
|
39
40
|
const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
|
|
@@ -137,7 +138,7 @@ const deepMerge = (target, source) => {
|
|
|
137
138
|
* @returns {ParsedFlags}
|
|
138
139
|
*/
|
|
139
140
|
const parseCommandlineArgs = (argv, validFlags) => {
|
|
140
|
-
const isFlag = arg => arg.startsWith('--')
|
|
141
|
+
const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
|
|
141
142
|
const positional = []
|
|
142
143
|
const named = {}
|
|
143
144
|
|