@cap-js/cds-typer 0.2.5-beta.1 → 0.3.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 +10 -1
- package/README.md +10 -21
- package/lib/compile.js +2 -859
- package/lib/components/inline.js +13 -12
- package/lib/components/resolver.js +471 -0
- package/lib/components/wrappers.js +66 -0
- package/lib/file.js +14 -0
- package/lib/logging.js +5 -4
- package/lib/util.js +47 -2
- package/lib/visitor.js +351 -0
- package/package.json +1 -1
package/lib/components/inline.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { SourceFile, Buffer } = require('../file')
|
|
2
|
+
const { docify } = require('./wrappers')
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Inline declarations of types can come in different flavours.
|
|
@@ -10,24 +11,24 @@ class InlineDeclarationResolver {
|
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @param {string} name
|
|
13
|
-
* @param {import('
|
|
14
|
+
* @param {import('./resolver').TypeResolveInfo} type
|
|
14
15
|
* @param {import('../file').Buffer} buffer
|
|
15
16
|
* @param {string} statementEnd
|
|
16
|
-
* @
|
|
17
|
+
* @protected
|
|
17
18
|
* @abstract
|
|
18
19
|
*/
|
|
19
20
|
printInlineType(name, type, buffer, statementEnd) { /* abstract */ }
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Attempts to resolve a type that could reference another type.
|
|
23
|
-
* @param {any}
|
|
24
|
-
* @param {import('
|
|
24
|
+
* @param {any} items
|
|
25
|
+
* @param {import('./resolver').TypeResolveInfo} into @see Visitor.resolveType
|
|
25
26
|
* @param {SourceFile} file temporary file to resolve dummy types into.
|
|
26
27
|
* @public
|
|
27
28
|
*/
|
|
28
29
|
resolveInlineDeclaration(items, into, relativeTo) {
|
|
29
30
|
const dummy = new SourceFile(relativeTo.path.asDirectory())
|
|
30
|
-
dummy.classes.
|
|
31
|
+
dummy.classes.currentIndent = relativeTo.classes.currentIndent
|
|
31
32
|
dummy.classes.add('{')
|
|
32
33
|
dummy.classes.indent()
|
|
33
34
|
|
|
@@ -36,7 +37,7 @@ class InlineDeclarationResolver {
|
|
|
36
37
|
// in inline definitions, we sometimes have to resolve first
|
|
37
38
|
// FIXME: does this tie in with how we sometimes end up with resolved typed in resolveType()?
|
|
38
39
|
const se = (typeof subelement === 'string')
|
|
39
|
-
? this.visitor.
|
|
40
|
+
? this.visitor.resolver.resolveTypeName(subelement)
|
|
40
41
|
: subelement
|
|
41
42
|
into.structuredType[subname] = this.visitor.visitElement(subname, se, dummy)
|
|
42
43
|
}
|
|
@@ -55,17 +56,17 @@ class InlineDeclarationResolver {
|
|
|
55
56
|
/**
|
|
56
57
|
* Visits a single element in an entity.
|
|
57
58
|
* @param {string} name name of the element
|
|
58
|
-
* @param {import('
|
|
59
|
+
* @param {import('./resolver').CSN} element CSN data belonging to the the element.
|
|
59
60
|
* @param {SourceFile} file the namespace file the surrounding entity is being printed into.
|
|
60
61
|
* @param {Buffer} [buffer] buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
61
62
|
* @public
|
|
62
63
|
*/
|
|
63
64
|
visitElement(name, element, file, buffer = file.classes) {
|
|
64
65
|
this.depth++
|
|
65
|
-
for (const d of
|
|
66
|
+
for (const d of docify(element.doc)) {
|
|
66
67
|
buffer.add(d)
|
|
67
68
|
}
|
|
68
|
-
const type = this.visitor.resolveAndRequire(element, file)
|
|
69
|
+
const type = this.visitor.resolver.resolveAndRequire(element, file)
|
|
69
70
|
this.depth--
|
|
70
71
|
if (this.depth === 0) {
|
|
71
72
|
this.printInlineType(name, type, buffer)
|
|
@@ -77,13 +78,13 @@ class InlineDeclarationResolver {
|
|
|
77
78
|
* Separator between value V and type T: `v : T`.
|
|
78
79
|
* Depending on the visitor's setting, this is may be `?:` for optional
|
|
79
80
|
* properties or `:` for required properties.
|
|
80
|
-
* @returns {'
|
|
81
|
+
* @returns {'?:'|':'}
|
|
81
82
|
*/
|
|
82
83
|
getPropertyTypeSeparator() {
|
|
83
84
|
return this.visitor.options.propertiesOptional ? '?:' : ':'
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/** @param {import('../
|
|
87
|
+
/** @param {import('../visitor').Visitor} visitor */
|
|
87
88
|
constructor(visitor) {
|
|
88
89
|
this.visitor = visitor
|
|
89
90
|
// type resolution might recurse. This indicator is used to determine
|
|
@@ -110,7 +111,7 @@ class InlineDeclarationResolver {
|
|
|
110
111
|
* @public
|
|
111
112
|
* @abstract
|
|
112
113
|
*/
|
|
113
|
-
getTypeLookup(members) { /* abstract */ }
|
|
114
|
+
getTypeLookup(members) { /* abstract */ return '' }
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const util = require('../util')
|
|
4
|
+
const { Buffer, SourceFile, Path, Library, baseDefinitions } = require("../file")
|
|
5
|
+
const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
|
|
6
|
+
const { Visitor } = require("../visitor")
|
|
7
|
+
const { StructuredInlineDeclarationResolver } = require("./inline")
|
|
8
|
+
|
|
9
|
+
/** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
|
|
10
|
+
/** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* When nested inline types require additional imports. E.g.:
|
|
14
|
+
* ```cds
|
|
15
|
+
* // mymodel.cds
|
|
16
|
+
* Foo {
|
|
17
|
+
* bar: {
|
|
18
|
+
* baz: a.b.c.Baz // need to require a.b.c in mymodel.cds!
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
* @typedef {{
|
|
23
|
+
* isBuiltin: boolean,
|
|
24
|
+
* isInlineDeclaration: boolean,
|
|
25
|
+
* isForeignKeyReference: boolean,
|
|
26
|
+
* isArray: boolean,
|
|
27
|
+
* type: string,
|
|
28
|
+
* path?: Path,
|
|
29
|
+
* csn?: CSN,
|
|
30
|
+
* imports: Path[]
|
|
31
|
+
* inner: TypeResolveInfo
|
|
32
|
+
* }} TypeResolveInfo
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builtin types defined by CDS.
|
|
37
|
+
*/
|
|
38
|
+
const Builtins = {
|
|
39
|
+
UUID: 'string',
|
|
40
|
+
String: 'string',
|
|
41
|
+
Binary: 'string',
|
|
42
|
+
LargeString: 'string',
|
|
43
|
+
LargeBinary: 'string',
|
|
44
|
+
Integer: 'number',
|
|
45
|
+
UInt8: 'number',
|
|
46
|
+
Int16: 'number',
|
|
47
|
+
Int32: 'number',
|
|
48
|
+
Int64: 'number',
|
|
49
|
+
Integer64: 'number',
|
|
50
|
+
Decimal: 'number',
|
|
51
|
+
DecimalFloat: 'number',
|
|
52
|
+
Float: 'number',
|
|
53
|
+
Double: 'number',
|
|
54
|
+
Boolean: 'boolean',
|
|
55
|
+
Date: 'Date',
|
|
56
|
+
DateTime: 'Date',
|
|
57
|
+
Time: 'Date',
|
|
58
|
+
Timestamp: 'Date',
|
|
59
|
+
//
|
|
60
|
+
Composition: 'Array',
|
|
61
|
+
Association: 'Array'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Resolver {
|
|
65
|
+
get csn() { return this.visitor.csn }
|
|
66
|
+
|
|
67
|
+
/** @param {Visitor} visitor */
|
|
68
|
+
constructor(visitor) {
|
|
69
|
+
/** @type {Visitor} */
|
|
70
|
+
this.visitor = visitor
|
|
71
|
+
|
|
72
|
+
/** @type {Library[]} */
|
|
73
|
+
this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns all libraries that have been referenced at least once.
|
|
78
|
+
* @returns {Library[]}
|
|
79
|
+
*/
|
|
80
|
+
getUsedLibraries() {
|
|
81
|
+
return this.libraries.filter(l => l.referenced)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Conveniently combines resolveNamespace and trimNamespace
|
|
86
|
+
* to end up with both the resolved Path of the namespace,
|
|
87
|
+
* and the clean name of the class.
|
|
88
|
+
* @param {string} fq the fully qualified name of an entity.
|
|
89
|
+
* @returns {[Path, string]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity.
|
|
90
|
+
*/
|
|
91
|
+
untangle(fq) {
|
|
92
|
+
const ns = this.resolveNamespace(fq.split('.'))
|
|
93
|
+
const name = this.trimNamespace(fq)
|
|
94
|
+
return [new Path(ns.split('.')), name]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Convenience method to shave off the namespace of a fully qualified path.
|
|
99
|
+
* More specifically, only the parts (reading from right to left) that are of
|
|
100
|
+
* kind "entity" are retained.
|
|
101
|
+
* a.b.c.Foo -> Foo
|
|
102
|
+
* Bar -> Bar
|
|
103
|
+
* sap.cap.Book.text -> Book.text (assuming Book and text are both of kind "entity")
|
|
104
|
+
* @param {string} p path
|
|
105
|
+
* @returns {string} the entity name without leading namespace.
|
|
106
|
+
*/
|
|
107
|
+
trimNamespace(p) {
|
|
108
|
+
// start on right side, go up while we have an entity at hand
|
|
109
|
+
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
110
|
+
const parts = p.split('.')
|
|
111
|
+
if (parts.length <= 1) {
|
|
112
|
+
return p
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let qualifier = parts.join('.')
|
|
116
|
+
while (
|
|
117
|
+
this.csn.definitions[qualifier] &&
|
|
118
|
+
['entity', 'type', 'aspect'].includes(this.csn.definitions[qualifier].kind)
|
|
119
|
+
) {
|
|
120
|
+
parts.pop()
|
|
121
|
+
qualifier = parts.join('.')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return qualifier ? p.substring(qualifier.length + 1) : p
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generates singular and plural inflection for the passed type.
|
|
129
|
+
* Several cases are covered here:
|
|
130
|
+
* - explicit annotation by the user in the CSN
|
|
131
|
+
* - implicitly derived inflection based on simple grammar rules
|
|
132
|
+
* - collisions between singular and plural name (resolved by appending a '_' suffix)
|
|
133
|
+
* - inline type definitions, which don't really have a linguistic plural,
|
|
134
|
+
* but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
|
|
135
|
+
* @param {import('./resolver').TypeResolveInfo} typeInfo information about the type gathered so far.
|
|
136
|
+
* @param {string} [namespace] namespace the type occurs in. If passed, will be shaved off from the name
|
|
137
|
+
* @returns {Inflection}
|
|
138
|
+
*/
|
|
139
|
+
inflect(typeInfo, namespace) {
|
|
140
|
+
let typeName
|
|
141
|
+
let singular
|
|
142
|
+
let plural
|
|
143
|
+
|
|
144
|
+
if (typeInfo.isInlineDeclaration) {
|
|
145
|
+
// if we detected an inline declaration, we take a quick detour via an InlineDeclarationResolver
|
|
146
|
+
// to rectify the typeName (which would be just '{' elsewise).
|
|
147
|
+
// The correct typename in string form is required in stringifyLambda(...)
|
|
148
|
+
// Note that whenever the typeName is relevant, it is assumed to be in structured form
|
|
149
|
+
// (i.e. a struct), so we always use a StructuredInlineDeclarationResolver here, regardless of
|
|
150
|
+
// what is configured for nested declarations in the visitor.
|
|
151
|
+
// FIXME: in most other places where we have an inline declaration, we actually don't need the typeName.
|
|
152
|
+
// If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
|
|
153
|
+
// piece of code instead to reduce overhead.
|
|
154
|
+
const into = new Buffer()
|
|
155
|
+
new StructuredInlineDeclarationResolver(this.visitor).printInlineType(undefined, { typeInfo }, into, '')
|
|
156
|
+
typeName = into.join(' ')
|
|
157
|
+
singular = typeName
|
|
158
|
+
plural = createArrayOf(typeName) //`Array<${typeName}>`
|
|
159
|
+
} else {
|
|
160
|
+
// TODO: make sure the resolution still works. Currently, we only cut off the namespace!
|
|
161
|
+
singular = util.singular4(typeInfo.csn)
|
|
162
|
+
plural = util.getPluralAnnotation(typeInfo.csn) ? util.plural4(typeInfo.csn) : typeInfo.plainName
|
|
163
|
+
|
|
164
|
+
// don't slice off namespace if it isn't part of the inflected name.
|
|
165
|
+
// This happens when the user adds an annotation and singular4 therefore
|
|
166
|
+
// already returns an identifier without namespace
|
|
167
|
+
if (namespace && singular.startsWith(namespace)) {
|
|
168
|
+
// TODO: not totally sure why plural doesn't have to be sliced
|
|
169
|
+
singular = singular.slice(namespace.length + 1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (singular === plural) {
|
|
173
|
+
// same as when creating the entity
|
|
174
|
+
plural += '_'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!singular || !plural) {
|
|
178
|
+
this.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { typeName, singular, plural }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convenient API to consume resolveType.
|
|
186
|
+
* Internally calls resolveType, determines how it has to be imported,
|
|
187
|
+
* used, etc. relative to file and just returns the name under
|
|
188
|
+
* which it will finally be known within file.
|
|
189
|
+
*
|
|
190
|
+
* For example:
|
|
191
|
+
* model1.cds contains entity Foo
|
|
192
|
+
* model2.cds references Foo
|
|
193
|
+
*
|
|
194
|
+
* calling resolveAndRequire({... Foo}, model2.d.ts) would then:
|
|
195
|
+
* 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
|
|
196
|
+
* 2. resolve any singular/ plural issues and association/ composition around it
|
|
197
|
+
* 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
|
|
198
|
+
*
|
|
199
|
+
* @param {CSN} element the CSN element to resolve the type for.
|
|
200
|
+
* @param {SourceFile} file source file for context.
|
|
201
|
+
* @returns {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} info about the resolved type
|
|
202
|
+
*/
|
|
203
|
+
resolveAndRequire(element, file) {
|
|
204
|
+
const typeInfo = this.resolveType(element, file)
|
|
205
|
+
const cardinality = this.getMaxCardinality(element)
|
|
206
|
+
|
|
207
|
+
let typeName = typeInfo.plainName ?? typeInfo.type
|
|
208
|
+
|
|
209
|
+
// only applies to builtin types, because the association/ composition _themselves_ are the (builtin) types we are checking, not their generic parameter!
|
|
210
|
+
if (typeInfo.isBuiltin === true) {
|
|
211
|
+
const [toOne, toMany] =
|
|
212
|
+
{
|
|
213
|
+
Association: [createToOneAssociation, createToManyAssociation],
|
|
214
|
+
Composition: [createCompositionOfOne, createCompositionOfMany],
|
|
215
|
+
}[element.constructor.name] ?? []
|
|
216
|
+
|
|
217
|
+
if (toOne && toMany) {
|
|
218
|
+
const target = typeof element.target === 'string' ? { type: element.target } : element.target
|
|
219
|
+
const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
|
|
220
|
+
typeName =
|
|
221
|
+
cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(element.target) ? 'this' : singular)
|
|
222
|
+
file.addImport(baseDefinitions.path)
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// TODO: this could go into resolve type
|
|
226
|
+
// resolve and maybe generate an import.
|
|
227
|
+
// Inline declarations don't have a corresponding path, etc., so skip those.
|
|
228
|
+
if (typeInfo.isInlineDeclaration === false) {
|
|
229
|
+
const namespace = this.resolveNamespace(typeInfo.path.parts)
|
|
230
|
+
const parent = new Path(namespace.split('.')) //t.path.getParent()
|
|
231
|
+
typeInfo.inflection = this.inflect(typeInfo, namespace)
|
|
232
|
+
|
|
233
|
+
if (!parent.isCwd(file.path.asDirectory())) {
|
|
234
|
+
file.addImport(parent)
|
|
235
|
+
// prepend namespace
|
|
236
|
+
typeName = `${parent.asIdentifier()}.${typeName}`
|
|
237
|
+
typeInfo.inflection.singular = `${parent.asIdentifier()}.${typeInfo.inflection.singular}`
|
|
238
|
+
typeInfo.inflection.plural = `${parent.asIdentifier()}.${typeInfo.inflection.plural}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (element.type.ref?.length > 1) {
|
|
242
|
+
const [entity, ...members] = element.type.ref
|
|
243
|
+
const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
|
|
244
|
+
typeName = deepRequire(typeInfo.inflection.singular, lookup)
|
|
245
|
+
file.addImport(baseDefinitions.path)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// FIXME NOW: inline declarations, aka structs go here!
|
|
249
|
+
|
|
250
|
+
for (const imp of typeInfo.imports ?? []) {
|
|
251
|
+
if (!imp.isCwd(file.path.asDirectory())) {
|
|
252
|
+
file.addImport(imp)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typeInfo.isInlineDeclaration === true) {
|
|
258
|
+
typeInfo.inflection = this.inflect(typeInfo)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeInfo.isArray === true) {
|
|
262
|
+
typeName = createArrayOf(typeName)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// FIXME: typeName could probably just become part of typeInfo
|
|
266
|
+
return { typeName, typeInfo }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Attempts to retrieve the max cardinality of a CSN for an entity.
|
|
271
|
+
* @param {EntityCSN} element csn of entity to retrieve cardinality for
|
|
272
|
+
* @returns {number} max cardinality of the element.
|
|
273
|
+
* If no cardinality is attached to the element, cardinality is 1.
|
|
274
|
+
* If it is set to '*', result is Infinity.
|
|
275
|
+
*/
|
|
276
|
+
getMaxCardinality(element) {
|
|
277
|
+
const cardinality = element?.cardinality?.max ?? 1
|
|
278
|
+
return cardinality === '*' ? Infinity : parseInt(cardinality)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolves the fully qualified name of an entity to its parent entity.
|
|
283
|
+
* resolveParent(a.b.c.D) -> CSN {a.b.c}
|
|
284
|
+
* @param {string} name fully qualified name of the entity to resolve the parent of.
|
|
285
|
+
* @returns {CSN} the resolved parent CSN.
|
|
286
|
+
*/
|
|
287
|
+
resolveParent(name) {
|
|
288
|
+
return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolves a fully qualified identifier to a namespace.
|
|
293
|
+
* In an identifier 'a.b.c.D.E', the namespace is the part of the identifier
|
|
294
|
+
* read from left to right which does not contain a kind 'context' or 'service'.
|
|
295
|
+
* That is, if in the above example 'D' is a context and 'E' is a service,
|
|
296
|
+
* the resulting namespace is 'a.b.c'.
|
|
297
|
+
* @param {string[]} pathParts the distinct parts of the namespace, i.e. ['a','b','c','D','E']
|
|
298
|
+
* @returns {string} the namespace's name, i.e. 'a.b.c'.
|
|
299
|
+
*/
|
|
300
|
+
resolveNamespace(pathParts) {
|
|
301
|
+
let result
|
|
302
|
+
while (result === undefined) {
|
|
303
|
+
const path = pathParts.join('.')
|
|
304
|
+
const def = this.csn.definitions[path]
|
|
305
|
+
if (!def) {
|
|
306
|
+
result = path
|
|
307
|
+
} else if (['context', 'service'].includes(def.kind)) {
|
|
308
|
+
result = path
|
|
309
|
+
} else {
|
|
310
|
+
pathParts = pathParts.slice(0, -1)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return result
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Resolves an element's type to either a builtin or a user defined type.
|
|
318
|
+
* Enriched with additional information for improved printout (see return type).
|
|
319
|
+
* @param {CSN} element the CSN element to resolve the type for.
|
|
320
|
+
* @param {SourceFile} file source file for context.
|
|
321
|
+
* @returns {TypeResolveInfo} description of the resolved type
|
|
322
|
+
*/
|
|
323
|
+
resolveType(element, file) {
|
|
324
|
+
// while resolving inline declarations, it can happen that we land here
|
|
325
|
+
// with an already resolved type. In that case, just return the type we have.
|
|
326
|
+
if (element && Object.hasOwn(element, 'isBuiltin')) {
|
|
327
|
+
return element
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = {
|
|
331
|
+
isBuiltin: false, // will be rectified in the corresponding handlers, if needed
|
|
332
|
+
isInlineDeclaration: false,
|
|
333
|
+
isForeignKeyReference: false,
|
|
334
|
+
isArray: false,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// FIXME: switch case
|
|
338
|
+
if (element?.type === undefined) {
|
|
339
|
+
// "fallback" type "empty object". May be overriden via #resolveInlineDeclarationType
|
|
340
|
+
// later on with an inline declaration
|
|
341
|
+
result.type = '{}'
|
|
342
|
+
result.isInlineDeclaration = true
|
|
343
|
+
} else {
|
|
344
|
+
this.resolvePotentialReferenceType(element.type, result, file)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// objects and arrays
|
|
348
|
+
if (element?.items) {
|
|
349
|
+
// FIXME: builtin = true? arrays are kinda builtin
|
|
350
|
+
result.isArray = true
|
|
351
|
+
this.resolveType(element.items, file)
|
|
352
|
+
//delete element.items
|
|
353
|
+
} else if (element?.elements && !element?.type) {
|
|
354
|
+
// explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
|
|
355
|
+
this.#resolveInlineDeclarationType(element.elements, result, file)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
|
|
359
|
+
this.logger.warning(
|
|
360
|
+
`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
return result
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Resolves an inline declaration of a type.
|
|
368
|
+
* We can encounter declarations like:
|
|
369
|
+
*
|
|
370
|
+
* record : array of {
|
|
371
|
+
* column : String;
|
|
372
|
+
* data : String;
|
|
373
|
+
* }
|
|
374
|
+
*
|
|
375
|
+
* These have to be resolved to a new type.
|
|
376
|
+
*
|
|
377
|
+
* @param {any[]} items the properties of the inline declaration.
|
|
378
|
+
* @param {TypeResolveInfo} into @see resolveType()
|
|
379
|
+
* @param {SourceFile} relativeTo the sourcefile in which we have found the reference to the type.
|
|
380
|
+
* This is important to correctly detect when a field in the inline declaration is referencing
|
|
381
|
+
* types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
|
|
382
|
+
*/
|
|
383
|
+
#resolveInlineDeclarationType(items, into, relativeTo) {
|
|
384
|
+
return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Attempts to resolve a type that could reference another type.
|
|
389
|
+
* @param {?} val
|
|
390
|
+
* @param {TypeResolveInfo} into see resolveType()
|
|
391
|
+
* @param {SourceFile} file only needed as we may call #resolveInlineDeclarationType from here. Will be expelled at some point.
|
|
392
|
+
*/
|
|
393
|
+
resolvePotentialReferenceType(val, into, file) {
|
|
394
|
+
// FIXME: get rid of file parameter! it is only used to pass to #resolveInlineDeclarationType
|
|
395
|
+
if (val.elements) {
|
|
396
|
+
this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
|
|
397
|
+
} else if (val.constructor === Object && 'ref' in val) {
|
|
398
|
+
this.#resolveTypeName(val.ref[0], into)
|
|
399
|
+
into.isForeignKeyReference = true
|
|
400
|
+
} else {
|
|
401
|
+
// val is string
|
|
402
|
+
this.#resolveTypeName(val, into)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Attempts to resolve a string to a type.
|
|
408
|
+
* String is supposed to refer to either a builtin type
|
|
409
|
+
* or any type defined in CSN.
|
|
410
|
+
* @param {string} t fully qualified type, like cds.String, or a.b.c.d.Foo
|
|
411
|
+
* @param {TypeResolveInfo} into optional dictionary to fill by reference, see resolveType()
|
|
412
|
+
* @returns @see resolveType
|
|
413
|
+
*/
|
|
414
|
+
#resolveTypeName(t, into) {
|
|
415
|
+
const result = into || {}
|
|
416
|
+
const path = t.split('.')
|
|
417
|
+
if (path.length === 2 && path[0] === 'cds') {
|
|
418
|
+
// builtin type
|
|
419
|
+
const resolvedBuiltin = Builtins[path[1]]
|
|
420
|
+
if (!resolvedBuiltin) {
|
|
421
|
+
throw new Error(`Can not resolve apparent builtin type '${t}' to any CDS type.`)
|
|
422
|
+
}
|
|
423
|
+
result.type = resolvedBuiltin
|
|
424
|
+
result.isBuiltin = true
|
|
425
|
+
} else if (t in this.csn.definitions) {
|
|
426
|
+
// user-defined type
|
|
427
|
+
result.type = this.trimNamespace(util.singular4(this.csn.definitions[t])) //(path[path.length - 1])
|
|
428
|
+
result.isBuiltin = false
|
|
429
|
+
result.path = new Path(path) // FIXME: relative to current file
|
|
430
|
+
result.csn = this.csn.definitions[t]
|
|
431
|
+
result.plainName = this.trimNamespace(t)
|
|
432
|
+
} else {
|
|
433
|
+
// type offered by some library
|
|
434
|
+
const lib = this.libraries.find((lib) => lib.offers(t))
|
|
435
|
+
if (lib) {
|
|
436
|
+
// only use the last name of the (fully qualified) type name in this case.
|
|
437
|
+
// We can not use trimNamespace, as that actually does a semantic lookup within the CSN.
|
|
438
|
+
// But entities that are found in libraries are not part of that CSN and have therefore be
|
|
439
|
+
// separated from their namespace in a more barbarian way.
|
|
440
|
+
// Luckily, this is not an issue, as libraries are supposed to be flat. So we can assume the
|
|
441
|
+
// last portion of the type to refer to the entity.
|
|
442
|
+
// We use this plain name as type name because consider:
|
|
443
|
+
//
|
|
444
|
+
// ```cds
|
|
445
|
+
// entity Book { title: hana.VARCHAR }
|
|
446
|
+
// ```
|
|
447
|
+
//
|
|
448
|
+
// ```ts
|
|
449
|
+
// import * as _cds_hana from '../../cds/hana'
|
|
450
|
+
// class Book { title: _cds_hana.cds.hana.VARCHAR } // <- how it would be without discarding the namespace
|
|
451
|
+
// class Book { title: _cds_hana.VARCHAR } // <- how we want it to look
|
|
452
|
+
// ```
|
|
453
|
+
const plain = t.split('.').at(-1)
|
|
454
|
+
lib.referenced = true
|
|
455
|
+
result.type = plain
|
|
456
|
+
result.isBuiltin = false
|
|
457
|
+
result.path = lib.path
|
|
458
|
+
result.csn = { name: t }
|
|
459
|
+
result.plainName = plain
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error(`Can not resolve '${t}' to any builtin, library-, or user defined type.`)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return result
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = {
|
|
470
|
+
Resolver
|
|
471
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const base = require('../file').baseDefinitions.path.asIdentifier()
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps type into association to scalar.
|
|
7
|
+
* @param {string} t the singular type name.
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
const createToOneAssociation = t => `${base}.Association.to<${t}>`
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps type into association to vector.
|
|
14
|
+
* @param {string} t the singular type name.
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
const createToManyAssociation = t => `${base}.Association.to.many<${t}>`
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wraps type into composition of scalar.
|
|
21
|
+
* @param {string} t the singular type name.
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
const createCompositionOfOne = t => `${base}.Composition.of<${t}>`
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wraps type into composition of vector.
|
|
28
|
+
* @param {string} t the singular type name.
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
const createCompositionOfMany = t => `${base}.Composition.of.many<${t}>`
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wraps type into an array.
|
|
35
|
+
* @param {string} t the singular type name.
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
const createArrayOf = t => `Array<${t}>`
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
|
42
|
+
* @param {string} t the singular type name.
|
|
43
|
+
* @param {string?} lookup a property lookup of the required type (`['Foo']`)
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Puts a passed string in docstring format.
|
|
50
|
+
* @param {string} doc raw string to docify. May contain linebreaks.
|
|
51
|
+
* @returns {string[]} an array of lines wrapped in doc format. The result is not
|
|
52
|
+
* concatenated to be properly indented by `buffer.add(...)`.
|
|
53
|
+
*/
|
|
54
|
+
const docify = doc => doc
|
|
55
|
+
? ['/**'].concat(doc.split('\n').map((line) => `* ${line}`)).concat(['*/'])
|
|
56
|
+
: []
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
createArrayOf,
|
|
60
|
+
createToOneAssociation,
|
|
61
|
+
createToManyAssociation,
|
|
62
|
+
createCompositionOfOne,
|
|
63
|
+
createCompositionOfMany,
|
|
64
|
+
deepRequire,
|
|
65
|
+
docify
|
|
66
|
+
}
|
package/lib/file.js
CHANGED
|
@@ -136,6 +136,19 @@ class SourceFile extends File {
|
|
|
136
136
|
this.inflections.push([singular, plural, original])
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Adds a function definition in form of a arrow function to the file.
|
|
141
|
+
* @param {string} name name of the function
|
|
142
|
+
* @param {{relative: string | undefined, local: boolean, posix: boolean}} params list of parameters, passed as [name, type] pairs
|
|
143
|
+
* @param returns the return type of the function
|
|
144
|
+
*/
|
|
145
|
+
addFunction(name, params, returns) {
|
|
146
|
+
// FIXME: use different buffers for buffers and actions, or at least rename buffer to the more general category "functions"?
|
|
147
|
+
this.actions.buffer.add("// function")
|
|
148
|
+
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name, params, returns)};`)
|
|
149
|
+
this.actions.names.push(name)
|
|
150
|
+
}
|
|
151
|
+
|
|
139
152
|
/**
|
|
140
153
|
* Adds an action definition in form of a arrow function to the file.
|
|
141
154
|
* @param {string} name name of the action
|
|
@@ -428,6 +441,7 @@ class Path {
|
|
|
428
441
|
* @type {SourceFile}
|
|
429
442
|
*/
|
|
430
443
|
const baseDefinitions = new SourceFile('_')
|
|
444
|
+
// FIXME: this should be a library someday
|
|
431
445
|
baseDefinitions.addPreamble(`
|
|
432
446
|
export namespace Association {
|
|
433
447
|
export type to <T> = T;
|
package/lib/logging.js
CHANGED
|
@@ -16,15 +16,16 @@ class Logger {
|
|
|
16
16
|
for (let i = 0; i < lvls.length - 1; i++) {
|
|
17
17
|
// -1 to ignore NONE
|
|
18
18
|
const level = lvls[i]
|
|
19
|
-
this[level.toLowerCase()] = function (message) {
|
|
20
|
-
this._log(level, message)
|
|
21
|
-
}.bind(this)
|
|
19
|
+
this[level.toLowerCase()] = function (message) { this._log(level, message) }.bind(this)
|
|
22
20
|
}
|
|
23
21
|
}
|
|
24
22
|
|
|
23
|
+
// only temporarily to disable those warnings...
|
|
24
|
+
//warning(s) {}; error(s) {}; info(s) {}; debug(s) {};
|
|
25
|
+
|
|
25
26
|
/**
|
|
26
27
|
* Add all log levels starting at level.
|
|
27
|
-
* @param {number}
|
|
28
|
+
* @param {number} baseLevel level to start from.
|
|
28
29
|
*/
|
|
29
30
|
addFrom(baseLevel) {
|
|
30
31
|
const vals = Object.values(Levels)
|