@cap-js/cds-typer 0.18.2 → 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 +22 -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 +152 -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/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 0.
|
|
7
|
+
## Version 0.21.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.20.0 - 2024-04-23
|
|
10
|
+
### Added
|
|
11
|
+
- Types for actions and functions now expose a `.kind` property which holds the string `'function'` or `'action'` respectively
|
|
12
|
+
- Added the CdsDate, CdsDateTime, CdsTime, CdsTimestamp types, which are each represented as a `string`.
|
|
13
|
+
- Plural types can now also contain an optional numeric `$count` property
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Empty `.actions` properties and operations without parameters are now typed as `Record<never, never>` to make it clear they contain nothing and also to satisfy overzealous linters
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Composition of aspects now properly resolve implicit `typeof` references in their properties
|
|
20
|
+
- Importing an enum into a service will now generate an alias to the original enum, instead of incorrectly duplicating the definition
|
|
21
|
+
- Returning entities from actions/ functions and using them as parameters will now properly use the singular inflection instead of returning an array thereof
|
|
22
|
+
- Aspects are now consistently named and called in their singular form
|
|
23
|
+
- Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
|
|
24
|
+
|
|
25
|
+
## Version 0.19.0 - 2024-03-28
|
|
26
|
+
### Added
|
|
27
|
+
- Support for `cds.Vector`, which will be represented as `string`
|
|
8
28
|
|
|
9
29
|
## Version 0.18.2 - 2024-03-21
|
|
10
|
-
###
|
|
30
|
+
### Fixed
|
|
11
31
|
- Resolving `@sap/cds` will now look in the CWD first to ensure a consistent use the same CDS version across different setups
|
|
12
32
|
- Types of function parameters starting with `cds.` are not automatically considered builtin anymore and receive a more thorough check against an allow-list
|
|
13
33
|
|
package/lib/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ const flags = {
|
|
|
21
21
|
desc: 'This text.',
|
|
22
22
|
},
|
|
23
23
|
logLevel: {
|
|
24
|
-
desc:
|
|
24
|
+
desc: 'Minimum log level that is printed.',
|
|
25
25
|
allowed: Object.keys(Levels),
|
|
26
26
|
default: Levels.ERROR,
|
|
27
27
|
},
|
|
@@ -48,7 +48,7 @@ const hint = () => 'Missing or invalid parameter(s). Call with --help for more d
|
|
|
48
48
|
const indent = (s, indentation) => s.split(EOL).map(line => `${indentation}${line}`).join(EOL)
|
|
49
49
|
|
|
50
50
|
const help = () => `SYNOPSIS${EOL2}` +
|
|
51
|
-
indent(
|
|
51
|
+
indent('cds-typer [cds file | "*"]', ' ') + EOL2 +
|
|
52
52
|
indent(`Generates type information based on a CDS model.${EOL}Call with at least one positional parameter pointing${EOL}to the (root) CDS file you want to compile.`, ' ') + EOL2 +
|
|
53
53
|
`OPTIONS${EOL2}` +
|
|
54
54
|
Object.entries(flags)
|
|
@@ -67,11 +67,11 @@ const help = () => `SYNOPSIS${EOL2}` +
|
|
|
67
67
|
s += `${EOL2}${indent(value.desc, ' ')}`
|
|
68
68
|
return s
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
).join(EOL2)
|
|
71
71
|
|
|
72
72
|
const version = () => require('../package.json').version
|
|
73
73
|
|
|
74
|
-
const main = async
|
|
74
|
+
const main = async args => {
|
|
75
75
|
if ('help' in args.named) {
|
|
76
76
|
console.log(help())
|
|
77
77
|
process.exit(0)
|
package/lib/compile.js
CHANGED
|
@@ -38,18 +38,6 @@ const writeJsConfig = (path, logger) => {
|
|
|
38
38
|
fs.writeFileSync(path, JSON.stringify(values, null, 2))
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* Compiles a .cds file to Typescript types.
|
|
43
|
-
* @param inputFile {string} path to input .cds file
|
|
44
|
-
* @param parameters {CompileParameters} path to root directory for all generated files, min log level, etc.
|
|
45
|
-
*/
|
|
46
|
-
const compileFromFile = async (inputFile, parameters) => {
|
|
47
|
-
const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
|
|
48
|
-
const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
|
|
49
|
-
const inferred = await cds.linked(await cds.load(paths, { docs: true }))
|
|
50
|
-
return compileFromCSN({xtended, inferred}, parameters)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
41
|
/**
|
|
54
42
|
* Compiles a CSN object to Typescript types.
|
|
55
43
|
* @param {{xtended: CSN, inferred: CSN}} csn
|
|
@@ -69,6 +57,18 @@ const compileFromCSN = async (csn, parameters) => {
|
|
|
69
57
|
)
|
|
70
58
|
}
|
|
71
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Compiles a .cds file to Typescript types.
|
|
62
|
+
* @param inputFile {string} path to input .cds file
|
|
63
|
+
* @param parameters {CompileParameters} path to root directory for all generated files, min log level, etc.
|
|
64
|
+
*/
|
|
65
|
+
const compileFromFile = async (inputFile, parameters) => {
|
|
66
|
+
const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
|
|
67
|
+
const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
|
|
68
|
+
const inferred = await cds.linked(await cds.load(paths, { docs: true }))
|
|
69
|
+
return compileFromCSN({xtended, inferred}, parameters)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
72
|
module.exports = {
|
|
73
73
|
compileFromFile
|
|
74
74
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { SourceFile } = require('../file')
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
4
|
+
const dateRegex = '`${number}${number}${number}${number}-${number}${number}-${number}${number}`'
|
|
5
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
6
|
+
const timeRegex = '`${number}${number}:${number}${number}:${number}${number}`'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base definitions used throughout the typing process,
|
|
10
|
+
* such as Associations and Compositions.
|
|
11
|
+
* @type {SourceFile}
|
|
12
|
+
*/
|
|
13
|
+
const baseDefinitions = new SourceFile('_')
|
|
14
|
+
// FIXME: this should be a library someday
|
|
15
|
+
baseDefinitions.addPreamble(`
|
|
16
|
+
export namespace Association {
|
|
17
|
+
export type to <T> = T;
|
|
18
|
+
export namespace to {
|
|
19
|
+
export type many <T extends readonly any[]> = T;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export namespace Composition {
|
|
24
|
+
export type of <T> = T;
|
|
25
|
+
export namespace of {
|
|
26
|
+
export type many <T extends readonly any[]> = T;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Entity {
|
|
31
|
+
static data<T extends Entity> (this:T, _input:Object) : T {
|
|
32
|
+
return {} as T // mock
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type EntitySet<T> = T[] & {
|
|
37
|
+
data (input:object[]) : T[]
|
|
38
|
+
data (input:object) : T
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type DeepRequired<T> = {
|
|
42
|
+
[K in keyof T]: DeepRequired<T[K]>
|
|
43
|
+
} & Exclude<Required<T>, null>;
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Dates and timestamps are strings during runtime, so cds-typer represents them as such.
|
|
48
|
+
*/
|
|
49
|
+
export type CdsDate = ${dateRegex};
|
|
50
|
+
/**
|
|
51
|
+
* @see {@link CdsDate}
|
|
52
|
+
*/
|
|
53
|
+
export type CdsDateTime = string;
|
|
54
|
+
/**
|
|
55
|
+
* @see {@link CdsDate}
|
|
56
|
+
*/
|
|
57
|
+
export type CdsTime = ${timeRegex};
|
|
58
|
+
/**
|
|
59
|
+
* @see {@link CdsDate}
|
|
60
|
+
*/
|
|
61
|
+
export type CdsTimestamp = string;
|
|
62
|
+
`)
|
|
63
|
+
|
|
64
|
+
module.exports = { baseDefinitions }
|
package/lib/components/enum.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
const { normalise } = require('./identifier')
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Extracts all unique values from a list of enum key-value pairs.
|
|
5
|
+
* If the value is an object, then the `.val` property is used.
|
|
6
|
+
* @param {[string, any | {val: any}][]} kvs
|
|
7
|
+
*/
|
|
8
|
+
const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stringifies a list of enum key-value pairs into the righthand side of a TS type.
|
|
12
|
+
* @param {[string, string][]} kvs list of key-value pairs
|
|
13
|
+
* @returns {string} a stringified type
|
|
14
|
+
* @example
|
|
15
|
+
* ```js
|
|
16
|
+
* ['A', 'B', 'A'] // -> '"A" | "B"'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
|
|
20
|
+
|
|
21
|
+
// in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
|
|
22
|
+
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
|
|
23
|
+
|
|
3
24
|
/**
|
|
4
25
|
* Prints an enum to a buffer. To be precise, it prints
|
|
5
26
|
* a constant object and a type which together form an artificial enum.
|
|
@@ -30,33 +51,12 @@ function printEnum(buffer, name, kvs, options = {}) {
|
|
|
30
51
|
const opts = {...{export: true}, ...options}
|
|
31
52
|
buffer.add('// enum')
|
|
32
53
|
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
33
|
-
kvs.forEach(([k, v]) => buffer.add(`${normalise(k)}: ${v},`))
|
|
54
|
+
kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
|
|
34
55
|
, '} as const;')
|
|
35
56
|
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
|
|
36
57
|
buffer.add('')
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
/**
|
|
40
|
-
* Stringifies a list of enum key-value pairs into the righthand side of a TS type.
|
|
41
|
-
* @param {[string, string][]} kvs list of key-value pairs
|
|
42
|
-
* @returns {string} a stringified type
|
|
43
|
-
* @example
|
|
44
|
-
* ```js
|
|
45
|
-
* ['A', 'B', 'A'] // -> '"A" | "B"'
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Extracts all unique values from a list of enum key-value pairs.
|
|
52
|
-
* If the value is an object, then the `.val` property is used.
|
|
53
|
-
* @param {[string, any | {val: any}][]} kvs
|
|
54
|
-
*/
|
|
55
|
-
const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
|
|
56
|
-
|
|
57
|
-
// in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
|
|
58
|
-
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
|
|
59
|
-
|
|
60
60
|
/**
|
|
61
61
|
* Converts a CSN type describing an enum into a list of kv-pairs.
|
|
62
62
|
* Values from CSN are unwrapped from their `.val` structure and
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @param {{type: any}} element
|
|
17
17
|
* @returns boolean
|
|
18
18
|
*/
|
|
19
|
-
const isReferenceType =
|
|
19
|
+
const isReferenceType = element => element.type && Object.hasOwn(element.type, 'ref')
|
|
20
20
|
|
|
21
21
|
module.exports = {
|
|
22
22
|
isReferenceType
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('../util')
|
|
4
4
|
// eslint-disable-next-line no-unused-vars
|
|
5
|
-
const { Buffer, SourceFile, Path, Library
|
|
5
|
+
const { Buffer, SourceFile, Path, Library } = require('../file')
|
|
6
6
|
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
|
|
7
7
|
const { StructuredInlineDeclarationResolver } = require('./inline')
|
|
8
8
|
const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
|
|
9
9
|
const { isReferenceType } = require('./reference')
|
|
10
|
+
const { isEntity } = require('../csn')
|
|
11
|
+
const { baseDefinitions } = require('./basedefs')
|
|
10
12
|
|
|
11
13
|
/** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
|
|
12
14
|
/** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
|
|
@@ -24,6 +26,7 @@ const { isReferenceType } = require('./reference')
|
|
|
24
26
|
* ```
|
|
25
27
|
* @typedef {{
|
|
26
28
|
* isBuiltin: boolean,
|
|
29
|
+
* isDeepRequire: boolean,
|
|
27
30
|
* isNotNull: boolean,
|
|
28
31
|
* isInlineDeclaration: boolean,
|
|
29
32
|
* isForeignKeyReference: boolean,
|
|
@@ -45,6 +48,7 @@ const Builtins = {
|
|
|
45
48
|
Binary: 'string',
|
|
46
49
|
LargeString: 'string',
|
|
47
50
|
LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
|
|
51
|
+
Vector: 'string',
|
|
48
52
|
Integer: 'number',
|
|
49
53
|
UInt8: 'number',
|
|
50
54
|
Int16: 'number',
|
|
@@ -57,10 +61,10 @@ const Builtins = {
|
|
|
57
61
|
Double: 'number',
|
|
58
62
|
Boolean: 'boolean',
|
|
59
63
|
// note: the date-related types are strings on purpose, which reflects their runtime behaviour
|
|
60
|
-
Date: '
|
|
61
|
-
DateTime: '
|
|
62
|
-
Time: '
|
|
63
|
-
Timestamp: '
|
|
64
|
+
Date: '__.CdsDate', // yyyy-mm-dd
|
|
65
|
+
DateTime: '__.CdsDateTime', // yyyy-mm-dd + time + TZ (precision: seconds)
|
|
66
|
+
Time: '__.CdsTime', // hh:mm:ss
|
|
67
|
+
Timestamp: '__.CdsTimestamp', // yyy-mm-dd + time + TZ (ms precision)
|
|
64
68
|
//
|
|
65
69
|
Composition: 'Array',
|
|
66
70
|
Association: 'Array'
|
|
@@ -87,7 +91,7 @@ class Resolver {
|
|
|
87
91
|
*/
|
|
88
92
|
namespaces: {},
|
|
89
93
|
/**
|
|
90
|
-
* @type {{ [qualifier: string]: string }}
|
|
94
|
+
* @type {{ [qualifier: string]: string[] }}
|
|
91
95
|
*/
|
|
92
96
|
propertyAccesses: {}
|
|
93
97
|
}
|
|
@@ -102,12 +106,28 @@ class Resolver {
|
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* @param {string} qualifier
|
|
105
|
-
* @
|
|
109
|
+
* @param {string} namespace
|
|
110
|
+
*/
|
|
111
|
+
#cacheNamespace (qualifier, namespace) {
|
|
112
|
+
this.#caches.namespaces[qualifier] = namespace
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} qualifier
|
|
117
|
+
* @returns {string[]?}
|
|
106
118
|
*/
|
|
107
119
|
#getCachedPropertyAccess (qualifier) {
|
|
108
120
|
return this.#caches.propertyAccesses[qualifier]
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} qualifier
|
|
125
|
+
* @param {string[]} propertyAccess
|
|
126
|
+
*/
|
|
127
|
+
#cachePropertyAccess (qualifier, propertyAccess) {
|
|
128
|
+
this.#caches.propertyAccesses[qualifier] = propertyAccess
|
|
129
|
+
}
|
|
130
|
+
|
|
111
131
|
get csn() { return this.visitor.csn.inferred }
|
|
112
132
|
|
|
113
133
|
/** @param {Visitor} visitor */
|
|
@@ -117,6 +137,12 @@ class Resolver {
|
|
|
117
137
|
|
|
118
138
|
/** @type {Library[]} */
|
|
119
139
|
this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @type {StructuredInlineDeclarationResolver}
|
|
143
|
+
* needed for inline declarations
|
|
144
|
+
*/
|
|
145
|
+
this.structuredInlineResolver = new StructuredInlineDeclarationResolver(this.visitor)
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
/**
|
|
@@ -127,17 +153,40 @@ class Resolver {
|
|
|
127
153
|
return this.libraries.filter(l => l.referenced)
|
|
128
154
|
}
|
|
129
155
|
|
|
156
|
+
/**
|
|
157
|
+
* TODO: this should probably be a class where we can also cache the properties
|
|
158
|
+
* and only retrieve them on demand
|
|
159
|
+
* @typedef {Object} Untangled
|
|
160
|
+
* @property {string[]} scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
|
|
161
|
+
* @property {string} name name of the leaf entity `a.b.C.D.E.f.g` -> `E`
|
|
162
|
+
* @property {string[]} property the property access path `a.b.C.D.E.f.g` -> `[f,g]`
|
|
163
|
+
* @property {Path} namespace the cds namespace of the entity `a.b.C.D.E.f.g` -> `a.b`
|
|
164
|
+
*/
|
|
165
|
+
|
|
130
166
|
/**
|
|
131
167
|
* Conveniently combines resolveNamespace and trimNamespace
|
|
132
168
|
* to end up with both the resolved Path of the namespace,
|
|
133
169
|
* and the clean name of the class.
|
|
134
170
|
* @param {string} fq the fully qualified name of an entity.
|
|
135
|
-
* @returns {
|
|
171
|
+
* @returns {Untangled} untangled qualifier
|
|
136
172
|
*/
|
|
137
173
|
untangle(fq) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
const builtin = resolveBuiltin(fq)
|
|
175
|
+
if (builtin) return { namespace: new Path([]), name: builtin, property: [], scope: [] }
|
|
176
|
+
|
|
177
|
+
const ns = this.resolveNamespace(fq)
|
|
178
|
+
const nameAndProperty = this.trimNamespace(fq)
|
|
179
|
+
const property = this.findPropertyAccess(fq)
|
|
180
|
+
const nameParts = (property.length
|
|
181
|
+
? nameAndProperty.slice(0, -(property.join('').length + property.length)) // +1 for each dot
|
|
182
|
+
: nameAndProperty
|
|
183
|
+
).split('.')//.at(-1) // nested entities would return Foo.Bar, so we only take the last part to get the actual entity name
|
|
184
|
+
return {
|
|
185
|
+
namespace: new Path(ns.split('.')),
|
|
186
|
+
scope: nameParts.slice(0, -1),
|
|
187
|
+
name: nameParts.at(-1),
|
|
188
|
+
property
|
|
189
|
+
}
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
/**
|
|
@@ -151,7 +200,7 @@ class Resolver {
|
|
|
151
200
|
* @returns {string} the entity name without leading namespace.
|
|
152
201
|
*/
|
|
153
202
|
trimNamespace(p) {
|
|
154
|
-
if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
|
|
203
|
+
//if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
|
|
155
204
|
const parts = p.split('.')
|
|
156
205
|
if (parts.length <= 1) return p
|
|
157
206
|
|
|
@@ -171,6 +220,7 @@ class Resolver {
|
|
|
171
220
|
* From a fully qualified path, finds the parts that are property accesses.
|
|
172
221
|
* This are specifically used in CDS' `typeof` syntax, where a property can
|
|
173
222
|
* refer to another entity's property type.
|
|
223
|
+
* @param {string} p path
|
|
174
224
|
* @example
|
|
175
225
|
* ```
|
|
176
226
|
* namespace namespace;
|
|
@@ -197,19 +247,24 @@ class Resolver {
|
|
|
197
247
|
const parts = p.split('.')
|
|
198
248
|
if (parts.length <= 1) return []
|
|
199
249
|
|
|
200
|
-
|
|
201
|
-
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
202
|
-
// sadly we have to use the extended flavour here, as inferred csn contains artificial entities for
|
|
203
|
-
// this kind of property access
|
|
204
|
-
const defs = this.visitor.csn.xtended.definitions
|
|
205
|
-
const properties = []
|
|
206
|
-
let qualifier = parts.join('.')
|
|
207
|
-
while (!defs[qualifier] && parts.length) {
|
|
208
|
-
properties.unshift(parts.pop())
|
|
209
|
-
qualifier = parts.join('.')
|
|
210
|
-
}
|
|
250
|
+
const isPropertyOf = (property, entity) => entity && property && Object.hasOwn(entity?.elements, property)
|
|
211
251
|
|
|
212
|
-
|
|
252
|
+
const defs = this.visitor.csn.inferred.definitions
|
|
253
|
+
// assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
|
|
254
|
+
let qualifier = parts.shift()
|
|
255
|
+
// find first entity from left (Entity1)
|
|
256
|
+
while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
|
|
257
|
+
qualifier += `.${parts.shift()}`
|
|
258
|
+
}
|
|
259
|
+
// skip forward to the last entity from left (Entity3), assuming that there is no name conflict between entities and properties
|
|
260
|
+
// i.e.: if there is a property "Entity2" in the entity Entity1, this will instead [Entity2, Entity3, property1, property2] as property access
|
|
261
|
+
while (!isPropertyOf(parts[0], defs[qualifier]) && isEntity(defs[qualifier + `.${parts[0]}`])) {
|
|
262
|
+
qualifier += `.${parts.shift()}`
|
|
263
|
+
}
|
|
264
|
+
// assuming Entity3 _does_ own a property "property1", return [property1, property2]
|
|
265
|
+
const propertyAccess = isPropertyOf(parts[0], defs[qualifier]) ? parts : []
|
|
266
|
+
this.#cachePropertyAccess(p, propertyAccess)
|
|
267
|
+
return propertyAccess
|
|
213
268
|
}
|
|
214
269
|
|
|
215
270
|
/**
|
|
@@ -251,7 +306,7 @@ class Resolver {
|
|
|
251
306
|
// If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
|
|
252
307
|
// piece of code instead to reduce overhead.
|
|
253
308
|
const into = new Buffer()
|
|
254
|
-
|
|
309
|
+
this.structuredInlineResolver.printInlineType(undefined, { typeInfo }, into, '')
|
|
255
310
|
typeName = into.join(' ')
|
|
256
311
|
singular = typeName
|
|
257
312
|
plural = createArrayOf(typeName)
|
|
@@ -317,10 +372,29 @@ class Resolver {
|
|
|
317
372
|
const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
|
|
318
373
|
/** set `notNull = true` to avoid repeated `| not null` TS construction */
|
|
319
374
|
target.notNull = true
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
cardinality > 1 ? toMany(
|
|
323
|
-
|
|
375
|
+
const targetTypeInfo = this.resolveAndRequire(target, file)
|
|
376
|
+
if (targetTypeInfo.typeInfo.isDeepRequire === true) {
|
|
377
|
+
typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
|
|
378
|
+
} else {
|
|
379
|
+
let { singular, plural } = targetTypeInfo.typeInfo.inflection
|
|
380
|
+
|
|
381
|
+
// FIXME: super hack!!
|
|
382
|
+
// Inflection currently does not retain the scope of the entity.
|
|
383
|
+
// But we can't just fix it in inflection(...), as that would break several other things
|
|
384
|
+
// So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
|
|
385
|
+
if (target.type) {
|
|
386
|
+
const untangled = this.untangle(target.type)
|
|
387
|
+
const scope = untangled.scope.join('.')
|
|
388
|
+
if (scope && !singular.startsWith(scope)) {
|
|
389
|
+
singular = `${scope}.${singular}`
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
typeName = cardinality > 1
|
|
394
|
+
? toMany(plural)
|
|
395
|
+
: toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
|
|
396
|
+
file.addImport(baseDefinitions.path)
|
|
397
|
+
}
|
|
324
398
|
}
|
|
325
399
|
} else {
|
|
326
400
|
// TODO: this could go into resolve type
|
|
@@ -343,6 +417,7 @@ class Resolver {
|
|
|
343
417
|
const [, ...members] = element.type.ref
|
|
344
418
|
const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
|
|
345
419
|
typeName = deepRequire(typeInfo.inflection.singular, lookup)
|
|
420
|
+
typeInfo.isDeepRequire = true
|
|
346
421
|
file.addImport(baseDefinitions.path)
|
|
347
422
|
}
|
|
348
423
|
}
|
|
@@ -359,11 +434,17 @@ class Resolver {
|
|
|
359
434
|
typeInfo.inflection = this.inflect(typeInfo)
|
|
360
435
|
}
|
|
361
436
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
437
|
+
// handle typeof (unless it has already been handled above)
|
|
438
|
+
const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
|
|
439
|
+
if (target && !typeInfo.isDeepRequire) {
|
|
440
|
+
const { property: propertyAccess } = this.untangle(target)
|
|
441
|
+
if (propertyAccess.length) {
|
|
442
|
+
const element = target.slice(0, -propertyAccess.join('.').length - 1)
|
|
443
|
+
const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
|
|
444
|
+
// singular, as we have to access the property of the entity
|
|
445
|
+
typeName = deepRequire(util.singular4(element)) + access
|
|
446
|
+
typeInfo.isDeepRequire = true
|
|
447
|
+
}
|
|
367
448
|
}
|
|
368
449
|
|
|
369
450
|
// add fallback inflection. Mainly needed for array-of with builtin types.
|
|
@@ -409,9 +490,9 @@ class Resolver {
|
|
|
409
490
|
* @returns {string} the namespace's name, i.e. 'a.b.c'.
|
|
410
491
|
*/
|
|
411
492
|
resolveNamespace(pathParts) {
|
|
412
|
-
if (typeof pathParts === 'string')
|
|
413
|
-
|
|
414
|
-
|
|
493
|
+
if (typeof pathParts === 'string') pathParts = pathParts.split('.')
|
|
494
|
+
const fq = pathParts.join('.')
|
|
495
|
+
if (this.#getCachedNamespace(fq)) return this.#getCachedNamespace(fq)
|
|
415
496
|
let result
|
|
416
497
|
while (result === undefined) {
|
|
417
498
|
const path = pathParts.join('.')
|
|
@@ -424,6 +505,7 @@ class Resolver {
|
|
|
424
505
|
pathParts = pathParts.slice(0, -1)
|
|
425
506
|
}
|
|
426
507
|
}
|
|
508
|
+
this.#cacheNamespace(fq, result)
|
|
427
509
|
return result
|
|
428
510
|
}
|
|
429
511
|
|
|
@@ -456,40 +538,38 @@ class Resolver {
|
|
|
456
538
|
// later on with an inline declaration
|
|
457
539
|
result.type = '{}'
|
|
458
540
|
result.isInlineDeclaration = true
|
|
459
|
-
} else {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
result.plainName = enumName
|
|
479
|
-
} else {
|
|
480
|
-
// FIXME: this is the case where users have arrays of enums as action parameter type.
|
|
481
|
-
// Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
|
|
482
|
-
// the encasing type (e.g. `string` here)
|
|
483
|
-
// We should instead aim for a proper type, i.e.
|
|
484
|
-
// this.#resolveInlineDeclarationType(element.enum, result, file)
|
|
485
|
-
// or
|
|
486
|
-
// stringifyEnumType(csnToEnumPairs(element))
|
|
487
|
-
this.#resolveTypeName(element.type, result)
|
|
488
|
-
}
|
|
541
|
+
} else if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
|
|
542
|
+
// element.parent is only set if the enum is attached to an entity's property.
|
|
543
|
+
// If it is missing then we are dealing with an inline parameter type of an action.
|
|
544
|
+
// Edge case: element.parent is set, but no .name property is attached. This happens
|
|
545
|
+
// for inline enums inside types:
|
|
546
|
+
// ```cds
|
|
547
|
+
// type T {
|
|
548
|
+
// x : String enum { ... }; // no element.name for x
|
|
549
|
+
// }
|
|
550
|
+
// ```
|
|
551
|
+
// In that case, we currently resolve to the more general type (cds.String, here)
|
|
552
|
+
if (element.parent?.name) {
|
|
553
|
+
result.isInlineDeclaration = true
|
|
554
|
+
// we use the singular as the initial declaration of these enums takes place
|
|
555
|
+
// while defining the singular class. Which therefore uses the singular over the plural name.
|
|
556
|
+
const cleanEntityName = util.singular4(element.parent, true)
|
|
557
|
+
const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
|
|
558
|
+
result.type = enumName
|
|
559
|
+
result.plainName = enumName
|
|
489
560
|
} else {
|
|
490
|
-
this
|
|
491
|
-
|
|
492
|
-
|
|
561
|
+
// FIXME: this is the case where users have arrays of enums as action parameter type.
|
|
562
|
+
// Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
|
|
563
|
+
// the encasing type (e.g. `string` here)
|
|
564
|
+
// We should instead aim for a proper type, i.e.
|
|
565
|
+
// this.#resolveInlineDeclarationType(element.enum, result, file)
|
|
566
|
+
// or
|
|
567
|
+
// stringifyEnumType(csnToEnumPairs(element))
|
|
568
|
+
this.#resolveTypeName(element.type, result)
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
this.resolvePotentialReferenceType(element.type, result, file)
|
|
572
|
+
}
|
|
493
573
|
|
|
494
574
|
// objects and arrays
|
|
495
575
|
if (element?.items) {
|
|
@@ -505,9 +585,7 @@ class Resolver {
|
|
|
505
585
|
}
|
|
506
586
|
|
|
507
587
|
if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
|
|
508
|
-
this.logger.warning(
|
|
509
|
-
`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`
|
|
510
|
-
)
|
|
588
|
+
this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
|
|
511
589
|
}
|
|
512
590
|
return result
|
|
513
591
|
}
|
|
@@ -584,7 +662,7 @@ class Resolver {
|
|
|
584
662
|
result.plainName = 'this'
|
|
585
663
|
} else {
|
|
586
664
|
// type offered by some library
|
|
587
|
-
const lib = this.libraries.find(
|
|
665
|
+
const lib = this.libraries.find(lib => lib.offers(t))
|
|
588
666
|
if (lib) {
|
|
589
667
|
// only use the last name of the (fully qualified) type name in this case.
|
|
590
668
|
// We can not use trimNamespace, as that actually does a semantic lookup within the CSN.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// this was derived from baseDefinitions before, but caused a circular dependency
|
|
4
|
+
const base = '__'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Wraps type into association to scalar.
|
|
@@ -37,6 +38,13 @@ const createCompositionOfMany = t => `${base}.Composition.of.many<${t}>`
|
|
|
37
38
|
*/
|
|
38
39
|
const createArrayOf = t => `Array<${t}>`
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Wraps type into object braces
|
|
43
|
+
* @param {string} t the properties, stringified and comma separated.
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
const createObjectOf = t => `{${t}}`
|
|
47
|
+
|
|
40
48
|
/**
|
|
41
49
|
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
|
42
50
|
* @param {string} t the singular type name.
|
|
@@ -52,11 +60,12 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
|
|
|
52
60
|
* concatenated to be properly indented by `buffer.add(...)`.
|
|
53
61
|
*/
|
|
54
62
|
const docify = doc => doc
|
|
55
|
-
? ['/**'].concat(doc.split('\n').map(
|
|
63
|
+
? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
|
|
56
64
|
: []
|
|
57
65
|
|
|
58
66
|
module.exports = {
|
|
59
67
|
createArrayOf,
|
|
68
|
+
createObjectOf,
|
|
60
69
|
createToOneAssociation,
|
|
61
70
|
createToManyAssociation,
|
|
62
71
|
createCompositionOfOne,
|