@cap-js/cds-typer 0.23.0 → 0.25.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 +23 -1
- package/cds-plugin.js +8 -3
- package/lib/cli.js +24 -14
- package/lib/compile.js +3 -3
- package/lib/components/enum.js +16 -9
- package/lib/components/identifier.js +1 -1
- package/lib/components/inline.js +81 -26
- package/lib/components/property.js +12 -0
- package/lib/components/wrappers.js +1 -1
- package/lib/csn.js +90 -26
- package/lib/file.js +43 -19
- package/lib/logging.js +5 -1
- package/lib/resolution/builtin.js +3 -2
- package/lib/resolution/entity.js +46 -7
- package/lib/resolution/resolver.js +51 -20
- package/lib/typedefs.d.ts +67 -13
- package/lib/util.js +4 -3
- package/lib/visitor.js +167 -61
- package/package.json +4 -2
package/lib/csn.js
CHANGED
|
@@ -1,31 +1,65 @@
|
|
|
1
1
|
const annotation = '@odata.draft.enabled'
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
4
|
+
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
5
|
+
/** @typedef {import('./typedefs').resolver.ProjectionCSN} ProjectionCSN */
|
|
6
|
+
/** @typedef {import('./typedefs').resolver.ViewCSN} ViewCSN */
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* FIXME: this is pretty handwavey: we are looking for view-entities,
|
|
5
10
|
* i.e. ones that have a query, but are not a cds level projection.
|
|
6
11
|
* Those are still not expanded and we have to retrieve their definition
|
|
7
12
|
* with all properties from the inferred model.
|
|
8
13
|
* @param {any} entity - the entity
|
|
14
|
+
* @returns {entity is ViewCSN}
|
|
9
15
|
*/
|
|
10
16
|
const isView = entity => entity.query && !entity.projection
|
|
11
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param {EntityCSN} entity - the entity
|
|
20
|
+
* @returns {entity is ProjectionCSN | ViewCSN}
|
|
21
|
+
*/
|
|
22
|
+
const isViewOrProjection = entity => Object.hasOwn(entity, 'query') || Object.hasOwn(entity, 'projection')
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {EntityCSN | ProjectionCSN} entity - the entity
|
|
26
|
+
* @returns {entity is ProjectionCSN}
|
|
27
|
+
*/
|
|
12
28
|
const isProjection = entity => entity.projection
|
|
13
29
|
|
|
14
30
|
/**
|
|
15
|
-
* @param {
|
|
31
|
+
* @param {EntityCSN} entity - the entity
|
|
16
32
|
* @see isView
|
|
17
33
|
* Unresolved entities have to be looked up from inferred csn.
|
|
18
34
|
*/
|
|
19
35
|
const isUnresolved = entity => entity._unresolved === true
|
|
20
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @param {EntityCSN} entity - the entity
|
|
39
|
+
*/
|
|
21
40
|
const isCsnAny = entity => entity?.constructor?.name === 'any'
|
|
22
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {EntityCSN} entity - the entity
|
|
44
|
+
*/
|
|
23
45
|
const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
|
|
24
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {EntityCSN} entity - the entity
|
|
49
|
+
*/
|
|
25
50
|
const isType = entity => entity?.kind === 'type'
|
|
26
51
|
|
|
52
|
+
/**
|
|
53
|
+
* @param {EntityCSN} entity - the entity
|
|
54
|
+
*/
|
|
27
55
|
const isEntity = entity => entity?.kind === 'entity'
|
|
28
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @param {EntityCSN | undefined} entity - the entity
|
|
59
|
+
* @returns {entity is import("./typedefs").resolver.EnumCSN}
|
|
60
|
+
*/
|
|
61
|
+
const isEnum = entity => Boolean(entity && Object.hasOwn(entity, 'enum'))
|
|
62
|
+
|
|
29
63
|
/**
|
|
30
64
|
* Attempts to retrieve the max cardinality of a CSN for an entity.
|
|
31
65
|
* @param {EntityCSN} element - csn of entity to retrieve cardinality for
|
|
@@ -34,13 +68,24 @@ const isEntity = entity => entity?.kind === 'entity'
|
|
|
34
68
|
* If it is set to '*', result is Infinity.
|
|
35
69
|
*/
|
|
36
70
|
const getMaxCardinality = element => {
|
|
37
|
-
const cardinality = element?.cardinality?.max ?? 1
|
|
71
|
+
const cardinality = element?.cardinality?.max ?? '1'
|
|
38
72
|
return cardinality === '*' ? Infinity : parseInt(cardinality)
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
|
|
75
|
+
/**
|
|
76
|
+
* @param {EntityCSN} entity - the entity
|
|
77
|
+
*/
|
|
78
|
+
const getViewTarget = entity => isView(entity)
|
|
79
|
+
? entity.query?.SELECT?.from?.ref?.[0]
|
|
80
|
+
: undefined
|
|
42
81
|
|
|
43
|
-
|
|
82
|
+
/**
|
|
83
|
+
* @param {EntityCSN} entity - the entity
|
|
84
|
+
* @returns {string | undefined}
|
|
85
|
+
*/
|
|
86
|
+
const getProjectionTarget = entity => isProjection(entity)
|
|
87
|
+
? entity.projection?.from?.ref?.[0]
|
|
88
|
+
: undefined
|
|
44
89
|
|
|
45
90
|
class DraftUnroller {
|
|
46
91
|
/** @type {Set<string>} */
|
|
@@ -48,15 +93,18 @@ class DraftUnroller {
|
|
|
48
93
|
/** @type {{[key: string]: boolean}} */
|
|
49
94
|
#draftable = {}
|
|
50
95
|
/** @type {{[key: string]: string}} */
|
|
51
|
-
#projections
|
|
52
|
-
/** @type {
|
|
53
|
-
#entities
|
|
96
|
+
#projections = {}
|
|
97
|
+
/** @type {EntityCSN[]} */
|
|
98
|
+
#entities = []
|
|
99
|
+
/** @type {CSN | undefined} */
|
|
54
100
|
#csn
|
|
55
101
|
set csn(c) {
|
|
56
102
|
this.#csn = c
|
|
103
|
+
if (c === undefined) return
|
|
57
104
|
this.#entities = Object.values(c.definitions)
|
|
58
105
|
this.#projections = this.#entities.reduce((pjs, entity) => {
|
|
59
106
|
if (isProjection(entity)) {
|
|
107
|
+
// @ts-ignore - we know that entity is a projection here
|
|
60
108
|
pjs[entity.name] = getProjectionTarget(entity)
|
|
61
109
|
}
|
|
62
110
|
return pjs
|
|
@@ -65,11 +113,13 @@ class DraftUnroller {
|
|
|
65
113
|
get csn() { return this.#csn }
|
|
66
114
|
|
|
67
115
|
/**
|
|
68
|
-
* @param {
|
|
116
|
+
* @param {EntityCSN | string} entityOrFq - entity to set draftable annotation for.
|
|
69
117
|
* @param {boolean} value - whether the entity is draftable.
|
|
70
118
|
*/
|
|
71
|
-
#setDraftable(
|
|
72
|
-
|
|
119
|
+
#setDraftable(entityOrFq, value) {
|
|
120
|
+
const entity = typeof entityOrFq === 'string'
|
|
121
|
+
? this.#getDefinition(entityOrFq)
|
|
122
|
+
: entityOrFq
|
|
73
123
|
if (!entity) return // inline definition -- not found in definitions
|
|
74
124
|
entity[annotation] = value
|
|
75
125
|
this.#draftable[entity.name] = value
|
|
@@ -81,32 +131,38 @@ class DraftUnroller {
|
|
|
81
131
|
}
|
|
82
132
|
|
|
83
133
|
/**
|
|
84
|
-
* @param {
|
|
134
|
+
* @param {EntityCSN | string} entityOrFq - entity to look draftability up for.
|
|
85
135
|
* @returns {boolean}
|
|
86
136
|
*/
|
|
87
|
-
#getDraftable(
|
|
88
|
-
const entity =
|
|
89
|
-
? this.#getDefinition(
|
|
90
|
-
:
|
|
137
|
+
#getDraftable(entityOrFq) {
|
|
138
|
+
const entity = typeof entityOrFq === 'string'
|
|
139
|
+
? this.#getDefinition(entityOrFq)
|
|
140
|
+
: entityOrFq
|
|
91
141
|
// assert(typeof entity !== 'string')
|
|
92
|
-
const name = entity?.name ??
|
|
142
|
+
const name = entity?.name ?? entityOrFq
|
|
143
|
+
// @ts-expect-error - .name not being present means entityOrFq is a string, so name is always a string and therefore a valid index
|
|
93
144
|
return this.#draftable[name] ??= this.#propagateInheritance(entity)
|
|
94
145
|
}
|
|
95
146
|
|
|
96
147
|
/**
|
|
148
|
+
* FIXME: could use EntityRepository here
|
|
97
149
|
* @param {string} name - name of the entity.
|
|
150
|
+
* @returns {EntityCSN}
|
|
98
151
|
*/
|
|
99
|
-
#
|
|
152
|
+
// @ts-expect-error - poor man's #getDefinitionOrThrow. We are always sure name is a valid key
|
|
153
|
+
#getDefinition(name) { return this.csn?.definitions[name] }
|
|
100
154
|
|
|
101
155
|
/**
|
|
102
156
|
* Propagate draft annotations through inheritance (includes).
|
|
103
157
|
* The latest annotation through the inheritance chain "wins".
|
|
104
158
|
* Annotations on the entity itself are always queued last, so they will always be decisive over ancestors.
|
|
105
|
-
* @param {
|
|
159
|
+
* @param {EntityCSN | undefined} entity - entity to pull draftability from its parents.
|
|
106
160
|
*/
|
|
107
161
|
#propagateInheritance(entity) {
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
if (!entity) return
|
|
163
|
+
/** @type {(boolean | undefined)[]} */
|
|
164
|
+
const annotations = (entity.includes ?? []).map(parent => this.#getDraftable(parent))
|
|
165
|
+
annotations.push(entity[annotation])
|
|
110
166
|
this.#setDraftable(entity, annotations.filter(a => a !== undefined).at(-1) ?? false)
|
|
111
167
|
}
|
|
112
168
|
|
|
@@ -114,6 +170,10 @@ class DraftUnroller {
|
|
|
114
170
|
* Propagate draft-enablement through projections.
|
|
115
171
|
*/
|
|
116
172
|
#propagateProjections() {
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} from - entity to propagate draftability from.
|
|
175
|
+
* @param {string} to - entity to propagate draftability to.
|
|
176
|
+
*/
|
|
117
177
|
const propagate = (from, to) => {
|
|
118
178
|
do {
|
|
119
179
|
this.#setDraftable(to, this.#getDraftable(to) || this.#getDraftable(from))
|
|
@@ -131,7 +191,7 @@ class DraftUnroller {
|
|
|
131
191
|
/**
|
|
132
192
|
* If an entity E is draftable and contains any composition of entities,
|
|
133
193
|
* then those entities also become draftable. Recursively.
|
|
134
|
-
* @param {
|
|
194
|
+
* @param {EntityCSN} entity - entity to propagate all compositions from.
|
|
135
195
|
*/
|
|
136
196
|
#propagateCompositions(entity) {
|
|
137
197
|
if (!this.#getDraftable(entity)) return
|
|
@@ -146,6 +206,7 @@ class DraftUnroller {
|
|
|
146
206
|
}
|
|
147
207
|
}
|
|
148
208
|
|
|
209
|
+
/** @param {CSN} csn - the full csn */
|
|
149
210
|
unroll(csn) {
|
|
150
211
|
this.csn = csn
|
|
151
212
|
|
|
@@ -241,7 +302,7 @@ function propagateForeignKeys(csn) {
|
|
|
241
302
|
// foreign key fields from Associations/ Compositions.
|
|
242
303
|
if (!Object.hasOwn(this, '__keys')) {
|
|
243
304
|
const ownKeys = Object.entries(this.elements ?? {}).filter(([,el]) => el.key === true)
|
|
244
|
-
const inheritedKeys = this.includes?.flatMap(parent => Object.entries(csn.definitions[parent].keys)) ?? []
|
|
305
|
+
const inheritedKeys = this.includes?.flatMap((/** @type {string} */ parent) => Object.entries(csn.definitions[parent].keys)) ?? []
|
|
245
306
|
// not sure why, but .associations contains both Associations, as well as Compositions in CSN.
|
|
246
307
|
// (.compositions contains only Compositions, if any)
|
|
247
308
|
const remoteKeys = Object.entries(this.associations ?? {})
|
|
@@ -249,9 +310,7 @@ function propagateForeignKeys(csn) {
|
|
|
249
310
|
.flatMap(([kname, key]) => Object.entries(csn.definitions[key.target].keys)
|
|
250
311
|
.map(([ckname, ckey]) => [`${kname}_${ckname}`, ckey]))
|
|
251
312
|
|
|
252
|
-
this.__keys = Object.fromEntries(ownKeys
|
|
253
|
-
.concat(inheritedKeys)
|
|
254
|
-
.concat(remoteKeys)
|
|
313
|
+
this.__keys = Object.fromEntries([...ownKeys, ...inheritedKeys, ...remoteKeys]
|
|
255
314
|
.filter(([,ckey]) => !ckey.target) // discard keys that are Associations. Those are already part of .elements
|
|
256
315
|
)
|
|
257
316
|
}
|
|
@@ -270,8 +329,11 @@ function amendCSN(csn) {
|
|
|
270
329
|
propagateForeignKeys(csn)
|
|
271
330
|
}
|
|
272
331
|
|
|
273
|
-
|
|
332
|
+
/**
|
|
333
|
+
* @param {EntityCSN} entity - the entity
|
|
334
|
+
*/
|
|
274
335
|
const getProjectionAliases = entity => {
|
|
336
|
+
/** @type {Record<string, string[]>} */
|
|
275
337
|
const aliases = {}
|
|
276
338
|
let all = false
|
|
277
339
|
for (const col of entity?.projection?.columns ?? []) {
|
|
@@ -290,8 +352,10 @@ module.exports = {
|
|
|
290
352
|
amendCSN,
|
|
291
353
|
isView,
|
|
292
354
|
isProjection,
|
|
355
|
+
isViewOrProjection,
|
|
293
356
|
isDraftEnabled,
|
|
294
357
|
isEntity,
|
|
358
|
+
isEnum,
|
|
295
359
|
isUnresolved,
|
|
296
360
|
isType,
|
|
297
361
|
getMaxCardinality,
|
package/lib/file.js
CHANGED
|
@@ -13,6 +13,13 @@ const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not
|
|
|
13
13
|
/** @typedef {import('./typedefs').file.Namespace} Namespace */
|
|
14
14
|
|
|
15
15
|
class File {
|
|
16
|
+
/**
|
|
17
|
+
* The Path for this library file, which is constructed from its namespace.
|
|
18
|
+
* @type {Path}
|
|
19
|
+
*/
|
|
20
|
+
// @ts-expect-error - not initialised, but will be done in subclasses (can't make File abstract in JS)
|
|
21
|
+
path
|
|
22
|
+
|
|
16
23
|
/**
|
|
17
24
|
* Creates one string from the buffers representing the type definitions.
|
|
18
25
|
* @returns {string} complete file contents.
|
|
@@ -42,6 +49,9 @@ class Library extends File {
|
|
|
42
49
|
return this.contents
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} file - path to the file
|
|
54
|
+
*/
|
|
45
55
|
constructor(file) {
|
|
46
56
|
super()
|
|
47
57
|
this.contents = readFileSync(file, 'utf-8')
|
|
@@ -102,7 +112,7 @@ class SourceFile extends File {
|
|
|
102
112
|
super()
|
|
103
113
|
/** @type {Path} */
|
|
104
114
|
this.path = path instanceof Path ? path : new Path(path.split('.'))
|
|
105
|
-
/** @type {
|
|
115
|
+
/** @type {{[key:string]: any}} */
|
|
106
116
|
this.imports = {}
|
|
107
117
|
/** @type {Buffer} */
|
|
108
118
|
this.preamble = new Buffer()
|
|
@@ -110,7 +120,7 @@ class SourceFile extends File {
|
|
|
110
120
|
this.events = { buffer: new Buffer(), fqs: []}
|
|
111
121
|
/** @type {Buffer} */
|
|
112
122
|
this.types = new Buffer()
|
|
113
|
-
/** @type {{ buffer: Buffer, data: {kvs: [string[]
|
|
123
|
+
/** @type {{ buffer: Buffer, data: {kvs: [string, string][], name: string, fq: string, property?: string}[]}} */
|
|
114
124
|
this.enums = { buffer: new Buffer(), data: [] }
|
|
115
125
|
/** @type {{ buffer: Buffer }} */
|
|
116
126
|
this.inlineEnums = { buffer: new Buffer() }
|
|
@@ -134,8 +144,14 @@ class SourceFile extends File {
|
|
|
134
144
|
|
|
135
145
|
/**
|
|
136
146
|
* Stringifies a lambda expression.
|
|
137
|
-
* @param {
|
|
138
|
-
* @
|
|
147
|
+
* @param {object} options - options
|
|
148
|
+
* @param {string} options.name - name of the lambda
|
|
149
|
+
* @param {[string, string][]} [options.parameters] - list of parameters, passed as [name, type] pairs
|
|
150
|
+
* @param {string} [options.returns] - the return type of the function
|
|
151
|
+
* @param {'action' | 'function'} options.kind - kind of the lambda
|
|
152
|
+
* @param {string} [options.initialiser] - the initialiser expression
|
|
153
|
+
* @param {boolean} [options.isStatic] - whether the lambda is static
|
|
154
|
+
* @returns {string} the stringified lambda
|
|
139
155
|
* @example
|
|
140
156
|
* ```js
|
|
141
157
|
* // note: these samples are actually simplified! See below.
|
|
@@ -187,7 +203,7 @@ class SourceFile extends File {
|
|
|
187
203
|
/**
|
|
188
204
|
* Adds a function definition in form of a arrow function to the file.
|
|
189
205
|
* @param {string} name - name of the function
|
|
190
|
-
* @param {
|
|
206
|
+
* @param {[string, string][]} parameters - list of parameters, passed as [name, type] pairs
|
|
191
207
|
* @param {string} returns - the return type of the function
|
|
192
208
|
* @param {'function' | 'action'} kind - kind of the node
|
|
193
209
|
*/
|
|
@@ -223,11 +239,8 @@ class SourceFile extends File {
|
|
|
223
239
|
* @param {string} fq - fully qualified name of the enum (entity name within CSN)
|
|
224
240
|
* @param {string} name - local name of the enum
|
|
225
241
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
226
|
-
* @param {string?} _property - property to which the enum is attached.
|
|
227
|
-
* If given, the enum is considered to be an inline definition of an enum.
|
|
228
|
-
* If not, it is considered to be regular, named enum.
|
|
229
242
|
*/
|
|
230
|
-
addEnum(fq, name, kvs
|
|
243
|
+
addEnum(fq, name, kvs) {
|
|
231
244
|
this.enums.data.push({ name, fq, kvs })
|
|
232
245
|
printEnum(this.enums.buffer, name, kvs)
|
|
233
246
|
}
|
|
@@ -318,10 +331,14 @@ class SourceFile extends File {
|
|
|
318
331
|
* @param {string} fq - fully qualified name of the enum
|
|
319
332
|
* @param {string} clean - local name of the enum
|
|
320
333
|
* @param {string} rhs - the right hand side of the assignment
|
|
334
|
+
* @param {boolean} exportValueLevel - whether to export the value level of the type (relevant to enums)
|
|
321
335
|
*/
|
|
322
|
-
addType(fq, clean, rhs) {
|
|
336
|
+
addType(fq, clean, rhs, exportValueLevel = false) {
|
|
323
337
|
this.typeNames[clean] = fq
|
|
324
338
|
this.types.add(`export type ${clean} = ${rhs};`)
|
|
339
|
+
if (exportValueLevel) {
|
|
340
|
+
this.types.add(`export const ${clean} = ${rhs};`)
|
|
341
|
+
}
|
|
325
342
|
}
|
|
326
343
|
|
|
327
344
|
/**
|
|
@@ -438,6 +455,10 @@ class Buffer {
|
|
|
438
455
|
* @type {string}
|
|
439
456
|
*/
|
|
440
457
|
this.currentIndent = ''
|
|
458
|
+
/**
|
|
459
|
+
* @type {boolean}
|
|
460
|
+
*/
|
|
461
|
+
this.closed = false
|
|
441
462
|
}
|
|
442
463
|
|
|
443
464
|
/**
|
|
@@ -483,7 +504,7 @@ class Buffer {
|
|
|
483
504
|
|
|
484
505
|
/**
|
|
485
506
|
* Adds an element to the buffer with one level of indent.
|
|
486
|
-
* @param {string | (() => void)} part - either a string or a function. If it is a string, it is added to the buffer.
|
|
507
|
+
* @param {string | string[] | (() => void)} part - either a string or a function. If it is a string, it is added to the buffer.
|
|
487
508
|
* If not, it is expected to be a function that manipulates the buffer as a side effect.
|
|
488
509
|
*/
|
|
489
510
|
addIndented(part) {
|
|
@@ -492,14 +513,16 @@ class Buffer {
|
|
|
492
513
|
part()
|
|
493
514
|
} else if (Array.isArray(part)) {
|
|
494
515
|
part.forEach(p => { this.add(p) })
|
|
516
|
+
} else if (typeof part === 'string') {
|
|
517
|
+
this.add(part)
|
|
495
518
|
}
|
|
496
519
|
this.outdent()
|
|
497
520
|
}
|
|
498
521
|
|
|
499
522
|
/**
|
|
500
|
-
* Adds an element to a buffer with one level of indent and
|
|
523
|
+
* Adds an element to a buffer with one level of indent and opener and closer surrounding it.
|
|
501
524
|
* @param {string} opener - the string to put before the indent
|
|
502
|
-
* @param {string} content - the content to indent (see {@link addIndented})
|
|
525
|
+
* @param {string | string[] | (() => void)} content - the content to indent (see {@link addIndented})
|
|
503
526
|
* @param {string} closer - the string to put after the indent
|
|
504
527
|
*/
|
|
505
528
|
addIndentedBlock(opener, content, closer) {
|
|
@@ -518,7 +541,7 @@ class Path {
|
|
|
518
541
|
* @param {string[]} parts - parts of the path. 'a.b.c' -> ['a', 'b', 'c']
|
|
519
542
|
* @param {string} kind - FIXME: currently unused
|
|
520
543
|
*/
|
|
521
|
-
constructor(parts, kind) {
|
|
544
|
+
constructor(parts, kind = '') {
|
|
522
545
|
this.parts = parts
|
|
523
546
|
this.kind = kind
|
|
524
547
|
}
|
|
@@ -533,9 +556,9 @@ class Path {
|
|
|
533
556
|
/**
|
|
534
557
|
* Transfoms the Path into a directory path.
|
|
535
558
|
* @param {object} params - parameters
|
|
536
|
-
* @param {string
|
|
537
|
-
* @param {boolean} params.local - if set to true, './' is prefixed to the directory
|
|
538
|
-
* @param {boolean} params.posix - if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
|
|
559
|
+
* @param {string} [params.relative] - if defined, the path is constructed relative to this directory
|
|
560
|
+
* @param {boolean} [params.local] - if set to true, './' is prefixed to the directory
|
|
561
|
+
* @param {boolean} [params.posix] - if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
|
|
539
562
|
* @returns {string} directory 'a.b.c'.asDirectory() -> 'a/b/c' (or a\b\c when on Windows without passing posix = true)
|
|
540
563
|
*/
|
|
541
564
|
asDirectory(params = {}) {
|
|
@@ -569,7 +592,7 @@ class Path {
|
|
|
569
592
|
}
|
|
570
593
|
|
|
571
594
|
/**
|
|
572
|
-
* @param {string} relative - directory to which we check relatively
|
|
595
|
+
* @param {string} [relative] - directory to which we check relatively
|
|
573
596
|
* @returns {boolean} true, iff the Path refers to the current working directory, aka './'
|
|
574
597
|
*/
|
|
575
598
|
isCwd(relative = undefined) {
|
|
@@ -580,6 +603,7 @@ class Path {
|
|
|
580
603
|
// TODO: having the repository pattern in place we can separate (some of) the printing logic from the visitor.
|
|
581
604
|
// Most of it hinges primarily on resolving specific files. We can now pass the repository and the resolver to a printer.
|
|
582
605
|
class FileRepository {
|
|
606
|
+
/** @type {{[key:string]: SourceFile}} */
|
|
583
607
|
#files = {}
|
|
584
608
|
|
|
585
609
|
/**
|
|
@@ -628,7 +652,7 @@ const writeout = async (root, sources) =>
|
|
|
628
652
|
fs.writeFile(path.join(dir, 'index.js'), source.toJSExports()),
|
|
629
653
|
])
|
|
630
654
|
|
|
631
|
-
} catch (err) {
|
|
655
|
+
} catch (/** @type {any} **/err) {
|
|
632
656
|
// eslint-disable-next-line no-console
|
|
633
657
|
console.error(`Could not create parent directory ${dir}: ${err}.`)
|
|
634
658
|
// eslint-disable-next-line no-console
|
package/lib/logging.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
2
|
|
|
3
|
+
/** @param {string} value - the value */
|
|
4
|
+
// @ts-expect-error - yes, cds.log.levels exists...
|
|
3
5
|
const _keyFor = value => Object.entries(cds.log.levels).find(([,val]) => val === value)?.[0]
|
|
4
6
|
|
|
5
7
|
// workaround until retroactively setting log level to 0 is possible
|
|
8
|
+
// @ts-expect-error - yes, cds.log.levels exists...
|
|
6
9
|
cds.log('cds-typer', _keyFor(cds.log.levels.SILENT))
|
|
7
10
|
module.exports = {
|
|
8
11
|
_keyFor,
|
|
9
|
-
setLevel: level => { cds.log('cds-typer', level) },
|
|
12
|
+
setLevel: (/** @type {string | number} */ level) => { cds.log('cds-typer', level) },
|
|
13
|
+
/** @type {Record<string, string>} */
|
|
10
14
|
deprecated: {
|
|
11
15
|
WARNING: 'WARN',
|
|
12
16
|
CRITICAL: 'ERROR',
|
|
@@ -2,6 +2,7 @@ class BuiltinResolver {
|
|
|
2
2
|
/**
|
|
3
3
|
* Builtin types defined by CDS.
|
|
4
4
|
*/
|
|
5
|
+
/** @type {Record<string, string>} */
|
|
5
6
|
#builtins = {
|
|
6
7
|
UUID: 'string',
|
|
7
8
|
String: 'string',
|
|
@@ -32,7 +33,7 @@ class BuiltinResolver {
|
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* @param {object} options - additional resolution options
|
|
35
|
-
* @param {boolean} options.IEEE754Compatible - if true, the Decimal, DecimalFloat, Float, and Double types are also allowed to be strings
|
|
36
|
+
* @param {boolean} [options.IEEE754Compatible] - if true, the Decimal, DecimalFloat, Float, and Double types are also allowed to be strings
|
|
36
37
|
*/
|
|
37
38
|
constructor ({ IEEE754Compatible } = {}) {
|
|
38
39
|
if (IEEE754Compatible) {
|
|
@@ -45,7 +46,7 @@ class BuiltinResolver {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* @param {string | string[]} t - name or parts of the type name split on dots
|
|
49
|
+
* @param {string | string[] | import("@sap/cds").ref} t - name or parts of the type name split on dots
|
|
49
50
|
* @returns {string | undefined | false} if t refers to a builtin, the name of the corresponding TS type is returned.
|
|
50
51
|
* If t _looks like_ a builtin (`cds.X`), undefined is returned.
|
|
51
52
|
* If t is obviously not a builtin, false is returned.
|
package/lib/resolution/entity.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { isType } = require('../csn')
|
|
2
|
+
|
|
1
3
|
class EntityInfo {
|
|
2
4
|
/**
|
|
3
5
|
* @example
|
|
@@ -6,7 +8,7 @@ class EntityInfo {
|
|
|
6
8
|
* // v
|
|
7
9
|
* Path(['n1', 'n2'])
|
|
8
10
|
* ```
|
|
9
|
-
* @type {Path}
|
|
11
|
+
* @type {import('../file').Path}
|
|
10
12
|
*/
|
|
11
13
|
namespace
|
|
12
14
|
|
|
@@ -44,7 +46,7 @@ class EntityInfo {
|
|
|
44
46
|
*/
|
|
45
47
|
propertyAccess
|
|
46
48
|
|
|
47
|
-
/** @type {{singular: string, plural: string}} */
|
|
49
|
+
/** @type {{singular: string, plural: string} | undefined} */
|
|
48
50
|
#inflection
|
|
49
51
|
|
|
50
52
|
/** @type {import('./resolver').Resolver} */
|
|
@@ -53,10 +55,10 @@ class EntityInfo {
|
|
|
53
55
|
/** @type {EntityRepository} */
|
|
54
56
|
#repository
|
|
55
57
|
|
|
56
|
-
/** @type {EntityInfo} */
|
|
57
|
-
#parent
|
|
58
|
+
/** @type {EntityInfo | null} */
|
|
59
|
+
#parent = null
|
|
58
60
|
|
|
59
|
-
/** @type {import('../typedefs').resolver.EntityCSN} */
|
|
61
|
+
/** @type {import('../typedefs').resolver.EntityCSN | undefined} */
|
|
60
62
|
#csn
|
|
61
63
|
|
|
62
64
|
get csn () {
|
|
@@ -124,7 +126,7 @@ class EntityInfo {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
class EntityRepository {
|
|
127
|
-
/** @type {{ [key: string]: EntityInfo }} */
|
|
129
|
+
/** @type {{ [key: string]: EntityInfo | null }} */
|
|
128
130
|
#cache = {}
|
|
129
131
|
|
|
130
132
|
/** @type {import('./resolver').Resolver} */
|
|
@@ -142,6 +144,19 @@ class EntityRepository {
|
|
|
142
144
|
return this.#cache[fq]
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Convenience for getByFq when you are 100% sure the entity exists.
|
|
149
|
+
* Serves to eliminate cumbersome null-handling where you know it's not necessary.
|
|
150
|
+
* For example when fq is derived from a reference to another entity.
|
|
151
|
+
* @param {string} fq - fully qualified name of the entity
|
|
152
|
+
* @returns {EntityInfo}
|
|
153
|
+
*/
|
|
154
|
+
getByFqOrThrow(fq) {
|
|
155
|
+
const entityInfo = this.getByFq(fq)
|
|
156
|
+
if (entityInfo === null) throw new Error(`Entity with fq "${fq}" is not part of the model`)
|
|
157
|
+
return entityInfo
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
/**
|
|
146
161
|
* @param {import('./resolver').Resolver} resolver - the resolver
|
|
147
162
|
*/
|
|
@@ -150,6 +165,30 @@ class EntityRepository {
|
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Derives an identifier from an entity info.
|
|
170
|
+
* That identifier can be used to refer to a specific entity within an index.ts file.
|
|
171
|
+
* By passing a relative file, the identifier will be preceeded with a scope if needed.
|
|
172
|
+
* @param {object} options - the options
|
|
173
|
+
* @param {EntityInfo} options.info - the entity info
|
|
174
|
+
* @param {function(string): string} [options.wrapper] - a function to wrap the identifier
|
|
175
|
+
* @param {import('../file').Path} [options.relative] - the path to resolve the identifier relative to
|
|
176
|
+
* @returns {string} the identifier
|
|
177
|
+
*/
|
|
178
|
+
function asIdentifier ({info, wrapper = undefined, relative = undefined}) {
|
|
179
|
+
const name = isType(info.csn)
|
|
180
|
+
? info.entityName
|
|
181
|
+
: info.inflection.singular
|
|
182
|
+
|
|
183
|
+
const wrapped = typeof wrapper === 'function'
|
|
184
|
+
? wrapper(name)
|
|
185
|
+
: name
|
|
186
|
+
return !relative || relative.isCwd(info.namespace.asDirectory())
|
|
187
|
+
? wrapped
|
|
188
|
+
: `${info.namespace.asIdentifier()}.${wrapped}`
|
|
189
|
+
}
|
|
190
|
+
|
|
153
191
|
module.exports = {
|
|
154
|
-
EntityRepository
|
|
192
|
+
EntityRepository,
|
|
193
|
+
asIdentifier
|
|
155
194
|
}
|