@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/CHANGELOG.md
CHANGED
|
@@ -4,14 +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
|
|
8
24
|
|
|
9
25
|
## Version 0.19.0 - 2024-03-28
|
|
10
26
|
### Added
|
|
11
27
|
- Support for `cds.Vector`, which will be represented as `string`
|
|
12
28
|
|
|
13
29
|
## Version 0.18.2 - 2024-03-21
|
|
14
|
-
###
|
|
30
|
+
### Fixed
|
|
15
31
|
- Resolving `@sap/cds` will now look in the CWD first to ensure a consistent use the same CDS version across different setups
|
|
16
32
|
- Types of function parameters starting with `cds.` are not automatically considered builtin anymore and receive a more thorough check against an allow-list
|
|
17
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,
|
|
@@ -58,10 +61,10 @@ const Builtins = {
|
|
|
58
61
|
Double: 'number',
|
|
59
62
|
Boolean: 'boolean',
|
|
60
63
|
// note: the date-related types are strings on purpose, which reflects their runtime behaviour
|
|
61
|
-
Date: '
|
|
62
|
-
DateTime: '
|
|
63
|
-
Time: '
|
|
64
|
-
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)
|
|
65
68
|
//
|
|
66
69
|
Composition: 'Array',
|
|
67
70
|
Association: 'Array'
|
|
@@ -88,7 +91,7 @@ class Resolver {
|
|
|
88
91
|
*/
|
|
89
92
|
namespaces: {},
|
|
90
93
|
/**
|
|
91
|
-
* @type {{ [qualifier: string]: string }}
|
|
94
|
+
* @type {{ [qualifier: string]: string[] }}
|
|
92
95
|
*/
|
|
93
96
|
propertyAccesses: {}
|
|
94
97
|
}
|
|
@@ -103,12 +106,28 @@ class Resolver {
|
|
|
103
106
|
|
|
104
107
|
/**
|
|
105
108
|
* @param {string} qualifier
|
|
106
|
-
* @
|
|
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[]?}
|
|
107
118
|
*/
|
|
108
119
|
#getCachedPropertyAccess (qualifier) {
|
|
109
120
|
return this.#caches.propertyAccesses[qualifier]
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} qualifier
|
|
125
|
+
* @param {string[]} propertyAccess
|
|
126
|
+
*/
|
|
127
|
+
#cachePropertyAccess (qualifier, propertyAccess) {
|
|
128
|
+
this.#caches.propertyAccesses[qualifier] = propertyAccess
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
get csn() { return this.visitor.csn.inferred }
|
|
113
132
|
|
|
114
133
|
/** @param {Visitor} visitor */
|
|
@@ -118,6 +137,12 @@ class Resolver {
|
|
|
118
137
|
|
|
119
138
|
/** @type {Library[]} */
|
|
120
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)
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
/**
|
|
@@ -128,17 +153,40 @@ class Resolver {
|
|
|
128
153
|
return this.libraries.filter(l => l.referenced)
|
|
129
154
|
}
|
|
130
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
|
+
|
|
131
166
|
/**
|
|
132
167
|
* Conveniently combines resolveNamespace and trimNamespace
|
|
133
168
|
* to end up with both the resolved Path of the namespace,
|
|
134
169
|
* and the clean name of the class.
|
|
135
170
|
* @param {string} fq the fully qualified name of an entity.
|
|
136
|
-
* @returns {
|
|
171
|
+
* @returns {Untangled} untangled qualifier
|
|
137
172
|
*/
|
|
138
173
|
untangle(fq) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|
|
142
190
|
}
|
|
143
191
|
|
|
144
192
|
/**
|
|
@@ -152,7 +200,7 @@ class Resolver {
|
|
|
152
200
|
* @returns {string} the entity name without leading namespace.
|
|
153
201
|
*/
|
|
154
202
|
trimNamespace(p) {
|
|
155
|
-
if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
|
|
203
|
+
//if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
|
|
156
204
|
const parts = p.split('.')
|
|
157
205
|
if (parts.length <= 1) return p
|
|
158
206
|
|
|
@@ -172,6 +220,7 @@ class Resolver {
|
|
|
172
220
|
* From a fully qualified path, finds the parts that are property accesses.
|
|
173
221
|
* This are specifically used in CDS' `typeof` syntax, where a property can
|
|
174
222
|
* refer to another entity's property type.
|
|
223
|
+
* @param {string} p path
|
|
175
224
|
* @example
|
|
176
225
|
* ```
|
|
177
226
|
* namespace namespace;
|
|
@@ -198,19 +247,24 @@ class Resolver {
|
|
|
198
247
|
const parts = p.split('.')
|
|
199
248
|
if (parts.length <= 1) return []
|
|
200
249
|
|
|
201
|
-
|
|
202
|
-
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
203
|
-
// sadly we have to use the extended flavour here, as inferred csn contains artificial entities for
|
|
204
|
-
// this kind of property access
|
|
205
|
-
const defs = this.visitor.csn.xtended.definitions
|
|
206
|
-
const properties = []
|
|
207
|
-
let qualifier = parts.join('.')
|
|
208
|
-
while (!defs[qualifier] && parts.length) {
|
|
209
|
-
properties.unshift(parts.pop())
|
|
210
|
-
qualifier = parts.join('.')
|
|
211
|
-
}
|
|
250
|
+
const isPropertyOf = (property, entity) => entity && property && Object.hasOwn(entity?.elements, property)
|
|
212
251
|
|
|
213
|
-
|
|
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
|
|
214
268
|
}
|
|
215
269
|
|
|
216
270
|
/**
|
|
@@ -252,7 +306,7 @@ class Resolver {
|
|
|
252
306
|
// If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
|
|
253
307
|
// piece of code instead to reduce overhead.
|
|
254
308
|
const into = new Buffer()
|
|
255
|
-
|
|
309
|
+
this.structuredInlineResolver.printInlineType(undefined, { typeInfo }, into, '')
|
|
256
310
|
typeName = into.join(' ')
|
|
257
311
|
singular = typeName
|
|
258
312
|
plural = createArrayOf(typeName)
|
|
@@ -318,10 +372,29 @@ class Resolver {
|
|
|
318
372
|
const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
|
|
319
373
|
/** set `notNull = true` to avoid repeated `| not null` TS construction */
|
|
320
374
|
target.notNull = true
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
cardinality > 1 ? toMany(
|
|
324
|
-
|
|
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
|
+
}
|
|
325
398
|
}
|
|
326
399
|
} else {
|
|
327
400
|
// TODO: this could go into resolve type
|
|
@@ -344,6 +417,7 @@ class Resolver {
|
|
|
344
417
|
const [, ...members] = element.type.ref
|
|
345
418
|
const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
|
|
346
419
|
typeName = deepRequire(typeInfo.inflection.singular, lookup)
|
|
420
|
+
typeInfo.isDeepRequire = true
|
|
347
421
|
file.addImport(baseDefinitions.path)
|
|
348
422
|
}
|
|
349
423
|
}
|
|
@@ -360,11 +434,17 @@ class Resolver {
|
|
|
360
434
|
typeInfo.inflection = this.inflect(typeInfo)
|
|
361
435
|
}
|
|
362
436
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
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
|
+
}
|
|
368
448
|
}
|
|
369
449
|
|
|
370
450
|
// add fallback inflection. Mainly needed for array-of with builtin types.
|
|
@@ -410,9 +490,9 @@ class Resolver {
|
|
|
410
490
|
* @returns {string} the namespace's name, i.e. 'a.b.c'.
|
|
411
491
|
*/
|
|
412
492
|
resolveNamespace(pathParts) {
|
|
413
|
-
if (typeof pathParts === 'string')
|
|
414
|
-
|
|
415
|
-
|
|
493
|
+
if (typeof pathParts === 'string') pathParts = pathParts.split('.')
|
|
494
|
+
const fq = pathParts.join('.')
|
|
495
|
+
if (this.#getCachedNamespace(fq)) return this.#getCachedNamespace(fq)
|
|
416
496
|
let result
|
|
417
497
|
while (result === undefined) {
|
|
418
498
|
const path = pathParts.join('.')
|
|
@@ -425,6 +505,7 @@ class Resolver {
|
|
|
425
505
|
pathParts = pathParts.slice(0, -1)
|
|
426
506
|
}
|
|
427
507
|
}
|
|
508
|
+
this.#cacheNamespace(fq, result)
|
|
428
509
|
return result
|
|
429
510
|
}
|
|
430
511
|
|
|
@@ -457,40 +538,38 @@ class Resolver {
|
|
|
457
538
|
// later on with an inline declaration
|
|
458
539
|
result.type = '{}'
|
|
459
540
|
result.isInlineDeclaration = true
|
|
460
|
-
} else {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
result.plainName = enumName
|
|
480
|
-
} else {
|
|
481
|
-
// FIXME: this is the case where users have arrays of enums as action parameter type.
|
|
482
|
-
// Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
|
|
483
|
-
// the encasing type (e.g. `string` here)
|
|
484
|
-
// We should instead aim for a proper type, i.e.
|
|
485
|
-
// this.#resolveInlineDeclarationType(element.enum, result, file)
|
|
486
|
-
// or
|
|
487
|
-
// stringifyEnumType(csnToEnumPairs(element))
|
|
488
|
-
this.#resolveTypeName(element.type, result)
|
|
489
|
-
}
|
|
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
|
|
490
560
|
} else {
|
|
491
|
-
this
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|
|
494
573
|
|
|
495
574
|
// objects and arrays
|
|
496
575
|
if (element?.items) {
|
|
@@ -506,9 +585,7 @@ class Resolver {
|
|
|
506
585
|
}
|
|
507
586
|
|
|
508
587
|
if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
|
|
509
|
-
this.logger.warning(
|
|
510
|
-
`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`
|
|
511
|
-
)
|
|
588
|
+
this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
|
|
512
589
|
}
|
|
513
590
|
return result
|
|
514
591
|
}
|
|
@@ -585,7 +662,7 @@ class Resolver {
|
|
|
585
662
|
result.plainName = 'this'
|
|
586
663
|
} else {
|
|
587
664
|
// type offered by some library
|
|
588
|
-
const lib = this.libraries.find(
|
|
665
|
+
const lib = this.libraries.find(lib => lib.offers(t))
|
|
589
666
|
if (lib) {
|
|
590
667
|
// only use the last name of the (fully qualified) type name in this case.
|
|
591
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,
|