@cap-js/cds-typer 0.24.0 → 0.26.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 +28 -2
- package/README.md +19 -0
- package/lib/cli.js +30 -14
- package/lib/compile.js +3 -3
- package/lib/components/enum.js +19 -10
- package/lib/components/identifier.js +1 -1
- package/lib/components/inline.js +81 -26
- package/lib/components/javascript.js +28 -0
- package/lib/components/property.js +12 -0
- package/lib/components/wrappers.js +27 -4
- package/lib/csn.js +90 -26
- package/lib/file.js +166 -44
- 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 +60 -20
- package/lib/typedefs.d.ts +83 -13
- package/lib/util.js +4 -3
- package/lib/visitor.js +199 -79
- package/package.json +10 -6
|
@@ -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 }
|
|
@@ -55,6 +58,15 @@ class Resolver {
|
|
|
55
58
|
return this.existsInCsn(fq) || Boolean(this.builtinResolver.resolveBuiltin(fq))
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {EntityCSN} type - a CSN type
|
|
63
|
+
* @returns {boolean} whether the type is configured to be optional
|
|
64
|
+
*/
|
|
65
|
+
isOptional(type) {
|
|
66
|
+
// TODO temporary solution to determine optional parameters. Align w/ compiler/importer.
|
|
67
|
+
return Object.keys(type).some(k => k.startsWith('@Core.OptionalParameter'))
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
/**
|
|
59
71
|
* Returns all libraries that have been referenced at least once.
|
|
60
72
|
* @returns {Library[]}
|
|
@@ -86,7 +98,7 @@ class Resolver {
|
|
|
86
98
|
return {
|
|
87
99
|
namespace: new Path(ns.split('.')),
|
|
88
100
|
scope: nameParts.slice(0, -1),
|
|
89
|
-
name: nameParts.at(-1),
|
|
101
|
+
name: nameParts.at(-1) ?? nameAndProperty,
|
|
90
102
|
property
|
|
91
103
|
}
|
|
92
104
|
}
|
|
@@ -146,10 +158,16 @@ class Resolver {
|
|
|
146
158
|
const parts = p.split('.')
|
|
147
159
|
if (parts.length <= 1) return []
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} property
|
|
163
|
+
* @param {import('../typedefs').resolver.EntityCSN} entity
|
|
164
|
+
*/
|
|
165
|
+
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
|
|
150
166
|
|
|
151
167
|
const defs = this.visitor.csn.inferred.definitions
|
|
152
168
|
// assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
|
|
169
|
+
/** @type {string} */
|
|
170
|
+
// @ts-expect-error - nope, we know there is at least one element
|
|
153
171
|
let qualifier = parts.shift()
|
|
154
172
|
// find first entity from left (Entity1)
|
|
155
173
|
while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
|
|
@@ -175,7 +193,7 @@ class Resolver {
|
|
|
175
193
|
* - inline type definitions, which don't really have a linguistic plural,
|
|
176
194
|
* but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
|
|
177
195
|
* @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
|
|
196
|
+
* @param {string | import('../file').Path} [namespace] - namespace the type occurs in. If passed, will be shaved off from the name
|
|
179
197
|
* @returns {Inflection}
|
|
180
198
|
*/
|
|
181
199
|
inflect(typeInfo, namespace) {
|
|
@@ -189,7 +207,11 @@ class Resolver {
|
|
|
189
207
|
}
|
|
190
208
|
}
|
|
191
209
|
|
|
192
|
-
|
|
210
|
+
if (namespace instanceof Path) {
|
|
211
|
+
namespace = namespace.asNamespace()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let typeName = ''
|
|
193
215
|
let singular
|
|
194
216
|
let plural
|
|
195
217
|
|
|
@@ -204,7 +226,13 @@ class Resolver {
|
|
|
204
226
|
// If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
|
|
205
227
|
// piece of code instead to reduce overhead.
|
|
206
228
|
const into = new Buffer()
|
|
207
|
-
this.structuredInlineResolver.printInlineType(
|
|
229
|
+
this.structuredInlineResolver.printInlineType({
|
|
230
|
+
fq: '',
|
|
231
|
+
type: { typeInfo, typeName: '' },
|
|
232
|
+
buffer: into,
|
|
233
|
+
statementEnd: '',
|
|
234
|
+
modifiers: getPropertyModifiers(typeInfo.csn)
|
|
235
|
+
})
|
|
208
236
|
typeName = into.join(' ')
|
|
209
237
|
singular = typeName
|
|
210
238
|
plural = createArrayOf(typeName)
|
|
@@ -246,9 +274,9 @@ class Resolver {
|
|
|
246
274
|
* 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
|
|
247
275
|
* 2. resolve any singular/ plural issues and association/ composition around it
|
|
248
276
|
* 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
|
|
249
|
-
* @param {
|
|
277
|
+
* @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
|
|
250
278
|
* @param {SourceFile} file - source file for context.
|
|
251
|
-
* @returns {
|
|
279
|
+
* @returns {ResolveAndRequireInfo} info about the resolved type
|
|
252
280
|
*/
|
|
253
281
|
resolveAndRequire(element, file) {
|
|
254
282
|
const typeInfo = this.resolveType(element, file)
|
|
@@ -266,9 +294,15 @@ class Resolver {
|
|
|
266
294
|
}[element.constructor.name] ?? []
|
|
267
295
|
|
|
268
296
|
if (toOne && toMany) {
|
|
269
|
-
|
|
297
|
+
/** @type { EntityCSN | { type: string } } */
|
|
298
|
+
// @ts-expect-error - nope, it is not undefined
|
|
299
|
+
const target = element.items ?? (typeof element.target === 'string'
|
|
300
|
+
? { type: element.target }
|
|
301
|
+
: element.target)
|
|
270
302
|
/** set `notNull = true` to avoid repeated `| not null` TS construction */
|
|
303
|
+
// @ts-expect-error - yes, we know that notNull is not part of the type in some cases
|
|
271
304
|
target.notNull = true
|
|
305
|
+
// @ts-expect-error - yes, target is a valid parameter
|
|
272
306
|
const targetTypeInfo = this.resolveAndRequire(target, file)
|
|
273
307
|
if (targetTypeInfo.typeInfo.isDeepRequire === true) {
|
|
274
308
|
typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
|
|
@@ -280,7 +314,7 @@ class Resolver {
|
|
|
280
314
|
// But we can't just fix it in inflection(...), as that would break several other things
|
|
281
315
|
// So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
|
|
282
316
|
if (target.type) {
|
|
283
|
-
const untangled = this.visitor.entityRepository.
|
|
317
|
+
const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
|
|
284
318
|
const scope = untangled.scope.join('.')
|
|
285
319
|
if (scope && !singular.startsWith(scope)) {
|
|
286
320
|
singular = `${scope}.${singular}`
|
|
@@ -359,7 +393,7 @@ class Resolver {
|
|
|
359
393
|
* Resolves the fully qualified name of an entity to its parent entity.
|
|
360
394
|
* resolveParent(a.b.c.D) -> CSN {a.b.c}
|
|
361
395
|
* @param {string} name - fully qualified name of the entity to resolve the parent of.
|
|
362
|
-
* @returns {
|
|
396
|
+
* @returns {import('../typedefs').resolver.EntityCSN} the resolved parent CSN.
|
|
363
397
|
*/
|
|
364
398
|
resolveParent(name) {
|
|
365
399
|
return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
|
|
@@ -394,14 +428,20 @@ class Resolver {
|
|
|
394
428
|
/**
|
|
395
429
|
* Resolves an element's type to either a builtin or a user defined type.
|
|
396
430
|
* Enriched with additional information for improved printout (see return type).
|
|
397
|
-
* @param {
|
|
431
|
+
* @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
|
|
398
432
|
* @param {SourceFile} file - source file for context.
|
|
399
433
|
* @returns {TypeResolveInfo} description of the resolved type
|
|
400
434
|
*/
|
|
401
435
|
resolveType(element, file) {
|
|
402
436
|
// while resolving inline declarations, it can happen that we land here
|
|
403
437
|
// with an already resolved type. In that case, just return the type we have.
|
|
404
|
-
|
|
438
|
+
// type guard check purely to satisfy return statement
|
|
439
|
+
/**
|
|
440
|
+
* @param {any} e - the element to check
|
|
441
|
+
* @returns {e is TypeResolveInfo}
|
|
442
|
+
*/
|
|
443
|
+
const isBuiltin = e => Object.hasOwn(e ?? {}, 'isBuiltin')
|
|
444
|
+
if (isBuiltin(element)) return element
|
|
405
445
|
|
|
406
446
|
const cardinality = getMaxCardinality(element)
|
|
407
447
|
|
|
@@ -447,7 +487,7 @@ class Resolver {
|
|
|
447
487
|
// this.#resolveInlineDeclarationType(element.enum, result, file)
|
|
448
488
|
// or
|
|
449
489
|
// stringifyEnumType(csnToEnumPairs(element))
|
|
450
|
-
this
|
|
490
|
+
this.resolveTypeName(element.type, result)
|
|
451
491
|
}
|
|
452
492
|
} else {
|
|
453
493
|
this.resolvePotentialReferenceType(element.type, result, file)
|
|
@@ -482,7 +522,7 @@ class Resolver {
|
|
|
482
522
|
* }
|
|
483
523
|
*
|
|
484
524
|
* These have to be resolved to a new type.
|
|
485
|
-
* @param {
|
|
525
|
+
* @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
|
|
486
526
|
* @param {TypeResolveInfo} into - @see resolveType()
|
|
487
527
|
* @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
|
|
488
528
|
* This is important to correctly detect when a field in the inline declaration is referencing
|
|
@@ -503,11 +543,11 @@ class Resolver {
|
|
|
503
543
|
if (val.elements) {
|
|
504
544
|
this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
|
|
505
545
|
} else if (val.constructor === Object && 'ref' in val) {
|
|
506
|
-
this
|
|
546
|
+
this.resolveTypeName(val.ref[0], into)
|
|
507
547
|
into.isForeignKeyReference = true
|
|
508
548
|
} else {
|
|
509
549
|
// val is string
|
|
510
|
-
this
|
|
550
|
+
this.resolveTypeName(val, into)
|
|
511
551
|
}
|
|
512
552
|
}
|
|
513
553
|
|
|
@@ -516,11 +556,11 @@ class Resolver {
|
|
|
516
556
|
* String is supposed to refer to either a builtin type
|
|
517
557
|
* or any type defined in CSN.
|
|
518
558
|
* @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()
|
|
559
|
+
* @param {TypeResolveInfo} [into] - optional dictionary to fill by reference, see resolveType()
|
|
520
560
|
* @returns @see resolveType
|
|
521
561
|
*/
|
|
522
|
-
|
|
523
|
-
const result = into
|
|
562
|
+
resolveTypeName(t, into) {
|
|
563
|
+
const result = into ?? {}
|
|
524
564
|
const path = t.split('.')
|
|
525
565
|
const builtin = this.builtinResolver.resolveBuiltin(path)
|
|
526
566
|
if (builtin === undefined) {
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
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
|
+
operations?: OperationCSN[],
|
|
12
|
+
cardinality?: { max?: '*' | string }
|
|
13
|
+
compositions?: { target: string }[]
|
|
14
|
+
doc?: string,
|
|
15
|
+
elements?: { [key: string]: EntityCSN }
|
|
16
|
+
key?: string // custom!!
|
|
17
|
+
keys?: { [key:string]: any }
|
|
18
|
+
kind: string,
|
|
19
|
+
includes?: string[]
|
|
20
|
+
items?: EntityCSN
|
|
21
|
+
notNull?: boolean, // custom!
|
|
22
|
+
on?: string,
|
|
23
|
+
parent?: EntityCSN
|
|
24
|
+
projection?: { from: ref, columns: (ref | '*')[]}
|
|
25
|
+
target?: string,
|
|
26
|
+
type: string | ref,
|
|
27
|
+
name: string,
|
|
28
|
+
'@odata.draft.enabled'?: boolean // custom!
|
|
29
|
+
_unresolved?: boolean
|
|
30
|
+
isRefNotNull?: boolean // custom!
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type OperationCSN = EntityCSN & {
|
|
34
|
+
params: {[key:string]: EntityCSN},
|
|
35
|
+
returns?: any,
|
|
36
|
+
kind: 'action' | 'function'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ProjectionCSN = EntityCSN & {
|
|
40
|
+
projection: any
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ViewCSN = EntityCSN & {
|
|
44
|
+
query?: any
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export type EnumCSN = EntityCSN & {
|
|
49
|
+
enum: {[key:name]: string}
|
|
4
50
|
}
|
|
5
51
|
|
|
6
52
|
export type CSN = {
|
|
7
|
-
definitions
|
|
53
|
+
definitions: { [key: string]: EntityCSN },
|
|
8
54
|
}
|
|
9
55
|
|
|
10
56
|
/**
|
|
@@ -19,19 +65,25 @@ export module resolver {
|
|
|
19
65
|
* ```
|
|
20
66
|
*/
|
|
21
67
|
export type TypeResolveInfo = {
|
|
22
|
-
isBuiltin
|
|
23
|
-
isDeepRequire
|
|
24
|
-
isNotNull
|
|
25
|
-
isInlineDeclaration
|
|
26
|
-
isForeignKeyReference
|
|
27
|
-
isArray
|
|
28
|
-
type
|
|
68
|
+
isBuiltin?: boolean,
|
|
69
|
+
isDeepRequire?: boolean,
|
|
70
|
+
isNotNull?: boolean,
|
|
71
|
+
isInlineDeclaration?: boolean,
|
|
72
|
+
isForeignKeyReference?: boolean,
|
|
73
|
+
isArray?: boolean,
|
|
74
|
+
type?: string,
|
|
29
75
|
path?: Path,
|
|
30
|
-
csn?:
|
|
31
|
-
imports
|
|
32
|
-
inner
|
|
76
|
+
csn?: EntityCSNCSN,
|
|
77
|
+
imports?: Path[]
|
|
78
|
+
inner?: TypeResolveInfo,
|
|
79
|
+
structuredType?: {[key: string]: {typeName: string, typeInfo: TypeResolveInfo}} // FIXME: same as inner?
|
|
80
|
+
plainName: string,
|
|
81
|
+
typeName?: string // FIXME: same as plainName?
|
|
82
|
+
inflection?: visitor.Inflection
|
|
33
83
|
}
|
|
34
84
|
|
|
85
|
+
export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
|
|
86
|
+
|
|
35
87
|
// TODO: this will be completely replaced by EntityInfo
|
|
36
88
|
export type Untangled = {
|
|
37
89
|
// scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
|
|
@@ -67,7 +119,11 @@ export module visitor {
|
|
|
67
119
|
export type CompileParameters = {
|
|
68
120
|
outputDirectory: string,
|
|
69
121
|
logLevel: number,
|
|
122
|
+
useEntitiesProxy: boolean,
|
|
70
123
|
jsConfigPath?: string,
|
|
124
|
+
inlineDeclarations: 'flat' | 'structured',
|
|
125
|
+
propertiesOptional: boolean,
|
|
126
|
+
IEEE754Compatible: boolean,
|
|
71
127
|
}
|
|
72
128
|
|
|
73
129
|
export type VisitorOptions = {
|
|
@@ -78,10 +134,14 @@ export module visitor {
|
|
|
78
134
|
* `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
|
|
79
135
|
*/
|
|
80
136
|
inlineDeclarations: 'flat' | 'structured',
|
|
137
|
+
/**
|
|
138
|
+
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
|
139
|
+
*/
|
|
140
|
+
useEntitiesProxy: boolean
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
export type Inflection = {
|
|
84
|
-
typeName
|
|
144
|
+
typeName?: string,
|
|
85
145
|
singular: string,
|
|
86
146
|
plural: string
|
|
87
147
|
}
|
|
@@ -89,8 +149,18 @@ export module visitor {
|
|
|
89
149
|
export type Context = {
|
|
90
150
|
entity: string
|
|
91
151
|
}
|
|
152
|
+
|
|
153
|
+
export type ParamInfo = {
|
|
154
|
+
name: string,
|
|
155
|
+
modifier: '' | '?',
|
|
156
|
+
type: string,
|
|
157
|
+
doc?: string
|
|
158
|
+
}
|
|
92
159
|
}
|
|
93
160
|
|
|
94
161
|
export module file {
|
|
95
162
|
export type Namespace = Object<string, Buffer>
|
|
163
|
+
export type FileOptions = {
|
|
164
|
+
useEntitiesProxy: boolean
|
|
165
|
+
}
|
|
96
166
|
}
|
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
|
|