@cap-js/cds-typer 0.26.0 → 0.28.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 -1
- package/cds-plugin.js +11 -4
- package/lib/cli.js +172 -40
- package/lib/compile.js +10 -16
- package/lib/components/basedefs.js +20 -0
- package/lib/components/class.js +35 -0
- package/lib/components/enum.js +2 -2
- package/lib/components/inline.js +15 -9
- package/lib/components/javascript.js +2 -2
- package/lib/components/wrappers.js +49 -1
- package/lib/config.js +118 -0
- package/lib/csn.js +110 -147
- package/lib/file.js +37 -24
- package/lib/resolution/entity.js +1 -0
- package/lib/resolution/resolver.js +23 -14
- package/lib/typedefs.d.ts +82 -38
- package/lib/util.js +17 -64
- package/lib/visitor.js +119 -66
- package/library/cds.hana.ts +2 -2
- package/package.json +70 -2
package/lib/resolution/entity.js
CHANGED
|
@@ -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,11 +13,13 @@ 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 */
|
|
19
20
|
/** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
|
|
20
21
|
/** @typedef {import('../typedefs').resolver.TypeResolveInfo} TypeResolveInfo */
|
|
22
|
+
/** @typedef {import('../typedefs').resolver.TypeResolveOptions} TypeResolveOptions */
|
|
21
23
|
/** @typedef {import('../typedefs').visitor.Inflection} Inflection */
|
|
22
24
|
/** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
|
|
23
25
|
|
|
@@ -30,7 +32,7 @@ class Resolver {
|
|
|
30
32
|
this.visitor = visitor
|
|
31
33
|
|
|
32
34
|
/** @type {BuiltinResolver} */
|
|
33
|
-
this.builtinResolver = new BuiltinResolver(
|
|
35
|
+
this.builtinResolver = new BuiltinResolver({ IEEE754Compatible: configuration.IEEE754Compatible })
|
|
34
36
|
|
|
35
37
|
/** @type {Library[]} */
|
|
36
38
|
this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
|
|
@@ -63,8 +65,7 @@ class Resolver {
|
|
|
63
65
|
* @returns {boolean} whether the type is configured to be optional
|
|
64
66
|
*/
|
|
65
67
|
isOptional(type) {
|
|
66
|
-
|
|
67
|
-
return Object.keys(type).some(k => k.startsWith('@Core.OptionalParameter'))
|
|
68
|
+
return type.items ? !type.items.notNull : !type.notNull
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -159,8 +160,8 @@ class Resolver {
|
|
|
159
160
|
if (parts.length <= 1) return []
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
* @param {string} property
|
|
163
|
-
* @param {import('../typedefs').resolver.EntityCSN} entity
|
|
163
|
+
* @param {string} property - the property to check
|
|
164
|
+
* @param {import('../typedefs').resolver.EntityCSN} entity - the entity to check the property against
|
|
164
165
|
*/
|
|
165
166
|
const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
|
|
166
167
|
|
|
@@ -233,7 +234,7 @@ class Resolver {
|
|
|
233
234
|
statementEnd: '',
|
|
234
235
|
modifiers: getPropertyModifiers(typeInfo.csn)
|
|
235
236
|
})
|
|
236
|
-
typeName = into.join(
|
|
237
|
+
typeName = into.join()
|
|
237
238
|
singular = typeName
|
|
238
239
|
plural = createArrayOf(typeName)
|
|
239
240
|
} else {
|
|
@@ -276,10 +277,11 @@ class Resolver {
|
|
|
276
277
|
* 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
|
|
277
278
|
* @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
|
|
278
279
|
* @param {SourceFile} file - source file for context.
|
|
280
|
+
* @param {TypeResolveOptions} [options] - resolver options
|
|
279
281
|
* @returns {ResolveAndRequireInfo} info about the resolved type
|
|
280
282
|
*/
|
|
281
|
-
resolveAndRequire(element, file) {
|
|
282
|
-
const typeInfo = this.resolveType(element, file)
|
|
283
|
+
resolveAndRequire(element, file, options) {
|
|
284
|
+
const typeInfo = this.resolveType(element, file, options)
|
|
283
285
|
const cardinality = getMaxCardinality(element)
|
|
284
286
|
|
|
285
287
|
let typeName = typeInfo.plainName ?? typeInfo.type
|
|
@@ -385,6 +387,10 @@ class Resolver {
|
|
|
385
387
|
plural: typeName
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
if (element.key === true) {
|
|
391
|
+
typeName = createKey(typeName)
|
|
392
|
+
}
|
|
393
|
+
|
|
388
394
|
// FIXME: typeName could probably just become part of typeInfo
|
|
389
395
|
return { typeName, typeInfo }
|
|
390
396
|
}
|
|
@@ -430,9 +436,10 @@ class Resolver {
|
|
|
430
436
|
* Enriched with additional information for improved printout (see return type).
|
|
431
437
|
* @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
|
|
432
438
|
* @param {SourceFile} file - source file for context.
|
|
439
|
+
* @param {TypeResolveOptions} [options] - resolver options
|
|
433
440
|
* @returns {TypeResolveInfo} description of the resolved type
|
|
434
441
|
*/
|
|
435
|
-
resolveType(element, file) {
|
|
442
|
+
resolveType(element, file, options) {
|
|
436
443
|
// while resolving inline declarations, it can happen that we land here
|
|
437
444
|
// with an already resolved type. In that case, just return the type we have.
|
|
438
445
|
// type guard check purely to satisfy return statement
|
|
@@ -501,9 +508,10 @@ class Resolver {
|
|
|
501
508
|
result.isBuiltin = true
|
|
502
509
|
this.resolveType(element.items, file)
|
|
503
510
|
//delete element.items
|
|
504
|
-
} else if (element?.elements && !element?.type) {
|
|
511
|
+
} else if (element?.elements && (options?.forceInlineStructs || !element?.type)) {
|
|
505
512
|
// explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
|
|
506
|
-
|
|
513
|
+
// if the resolver option `forceInlineStructs` is `true`, named types in elements will be converted to inline
|
|
514
|
+
this.#resolveInlineDeclarationType(element.elements, result, file, options)
|
|
507
515
|
}
|
|
508
516
|
|
|
509
517
|
if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
|
|
@@ -525,11 +533,12 @@ class Resolver {
|
|
|
525
533
|
* @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
|
|
526
534
|
* @param {TypeResolveInfo} into - @see resolveType()
|
|
527
535
|
* @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
|
|
536
|
+
* @param {TypeResolveOptions} [options] - resolver options
|
|
528
537
|
* This is important to correctly detect when a field in the inline declaration is referencing
|
|
529
538
|
* types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
|
|
530
539
|
*/
|
|
531
|
-
#resolveInlineDeclarationType(items, into, relativeTo) {
|
|
532
|
-
return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
|
|
540
|
+
#resolveInlineDeclarationType(items, into, relativeTo, options) {
|
|
541
|
+
return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo, options)
|
|
533
542
|
}
|
|
534
543
|
|
|
535
544
|
/**
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -82,6 +82,39 @@ export module resolver {
|
|
|
82
82
|
inflection?: visitor.Inflection
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Custom options to be used during type resolvement
|
|
87
|
+
*/
|
|
88
|
+
export type TypeResolveOptions = {
|
|
89
|
+
/**
|
|
90
|
+
* Entity elements that have a custom type are not available when entity is accessed using CQL.
|
|
91
|
+
*
|
|
92
|
+
* They only exist in the original defined form in the CSN and LinkedCSN but not in the compiled
|
|
93
|
+
* OData or SQL models (i.e. `cds.compile(..).for.odata()`).
|
|
94
|
+
*
|
|
95
|
+
* Therefore they need to be flattened down like inline structs.
|
|
96
|
+
*
|
|
97
|
+
* ```cds
|
|
98
|
+
* // model.cds
|
|
99
|
+
* type Adress {
|
|
100
|
+
* street: String;
|
|
101
|
+
* zipCode: String;
|
|
102
|
+
* }
|
|
103
|
+
* entity Persons {
|
|
104
|
+
* title: String
|
|
105
|
+
* address: Adress
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* // service.js
|
|
110
|
+
* ```js
|
|
111
|
+
* const {title, address_street, address_zipCode} = await SELECT.from(Persons);
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
*/
|
|
115
|
+
forceInlineStructs?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
85
118
|
export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
|
|
86
119
|
|
|
87
120
|
// TODO: this will be completely replaced by EntityInfo
|
|
@@ -99,47 +132,13 @@ export module resolver {
|
|
|
99
132
|
|
|
100
133
|
export module util {
|
|
101
134
|
export type Annotations = {
|
|
102
|
-
name
|
|
135
|
+
name: string,
|
|
103
136
|
'@singular'?: string,
|
|
104
137
|
'@plural'?: string
|
|
105
138
|
}
|
|
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
139
|
}
|
|
117
140
|
|
|
118
141
|
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
142
|
export type Inflection = {
|
|
144
143
|
typeName?: string,
|
|
145
144
|
singular: string,
|
|
@@ -158,9 +157,54 @@ export module visitor {
|
|
|
158
157
|
}
|
|
159
158
|
}
|
|
160
159
|
|
|
160
|
+
export module config {
|
|
161
|
+
export module cli {
|
|
162
|
+
export type CLIFlags = 'version' | 'help'
|
|
163
|
+
export type ParameterSchema = {
|
|
164
|
+
[key: string]: {
|
|
165
|
+
desc: string,
|
|
166
|
+
allowed?: string[],
|
|
167
|
+
allowedHint?: string,
|
|
168
|
+
type?: 'string' | 'boolean' | 'number',
|
|
169
|
+
default?: string,
|
|
170
|
+
defaultHint?: string,
|
|
171
|
+
postprocess?: (value: string) => any,
|
|
172
|
+
camel?: string,
|
|
173
|
+
snake?: string
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type ParsedParameters = {
|
|
178
|
+
positional: string[],
|
|
179
|
+
named: { [key: keyof RuntimeParameters]: {
|
|
180
|
+
value: any,
|
|
181
|
+
isDefault: boolean,
|
|
182
|
+
} }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type Configuration = {
|
|
187
|
+
outputDirectory: string,
|
|
188
|
+
logLevel: number,
|
|
189
|
+
/**
|
|
190
|
+
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
|
191
|
+
*/
|
|
192
|
+
useEntitiesProxy: boolean,
|
|
193
|
+
jsConfigPath?: string,
|
|
194
|
+
/**
|
|
195
|
+
* `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
|
|
196
|
+
* `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
|
|
197
|
+
*/
|
|
198
|
+
inlineDeclarations: 'flat' | 'structured',
|
|
199
|
+
/** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
|
|
200
|
+
propertiesOptional: boolean,
|
|
201
|
+
/**
|
|
202
|
+
* `IEEE754Compatible = true` -> any cds.Decimal will become `number | string`
|
|
203
|
+
*/
|
|
204
|
+
IEEE754Compatible: boolean
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
161
208
|
export module file {
|
|
162
209
|
export type Namespace = Object<string, Buffer>
|
|
163
|
-
export type FileOptions = {
|
|
164
|
-
useEntitiesProxy: boolean
|
|
165
|
-
}
|
|
166
210
|
}
|
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
|
}
|