@cap-js/cds-typer 0.16.0 → 0.18.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 +12 -1
- package/lib/components/enum.js +3 -7
- package/lib/components/reference.js +23 -0
- package/lib/components/resolver.js +99 -25
- package/lib/csn.js +39 -3
- package/lib/file.js +65 -2
- package/lib/util.js +2 -2
- package/lib/visitor.js +114 -126
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,18 @@ 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.19.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.18.0 - 2024-03-12
|
|
10
|
+
### Added
|
|
11
|
+
- Improved support for projections, including projections on inline definitions, and on views, as well as support for explicit exclusion and selection of properties
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- [breaking] CDS `type` definitions will not be inflected. Whatever inflection you define them in will be assumed treated as a singular form and will not receive a plural form anymore
|
|
15
|
+
|
|
16
|
+
## Version 0.17.0 - 2024-03-05
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed a bug where refering to an externally defined enum via the `typeof` syntax would crash the type generation
|
|
8
19
|
|
|
9
20
|
## Version 0.16.0 - 2024-02-01
|
|
10
21
|
### Changed
|
package/lib/components/enum.js
CHANGED
|
@@ -29,13 +29,9 @@ const { normalise } = require('./identifier')
|
|
|
29
29
|
function printEnum(buffer, name, kvs, options = {}) {
|
|
30
30
|
const opts = {...{export: true}, ...options}
|
|
31
31
|
buffer.add('// enum')
|
|
32
|
-
buffer.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
buffer.add(`${normalise(k)}: ${v},`)
|
|
36
|
-
}
|
|
37
|
-
buffer.outdent()
|
|
38
|
-
buffer.add('} as const;')
|
|
32
|
+
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
33
|
+
kvs.forEach(([k, v]) => buffer.add(`${normalise(k)}: ${v},`))
|
|
34
|
+
, '} as const;')
|
|
39
35
|
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
|
|
40
36
|
buffer.add('')
|
|
41
37
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an element references another type.
|
|
3
|
+
* This happens for foreign key relationships
|
|
4
|
+
* and for the typeof syntax.
|
|
5
|
+
*
|
|
6
|
+
* ```cds
|
|
7
|
+
* entity E {
|
|
8
|
+
* x: Integer
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* entity F {
|
|
12
|
+
* y: E.x // <- ref
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param {{type: any}} element
|
|
17
|
+
* @returns boolean
|
|
18
|
+
*/
|
|
19
|
+
const isReferenceType = (element) => element.type && Object.hasOwn(element.type, 'ref')
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
isReferenceType
|
|
23
|
+
}
|
|
@@ -6,6 +6,7 @@ const { Buffer, SourceFile, Path, Library, baseDefinitions } = 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
|
+
const { isReferenceType } = require('./reference')
|
|
9
10
|
|
|
10
11
|
/** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
|
|
11
12
|
/** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
|
|
@@ -55,7 +56,7 @@ const Builtins = {
|
|
|
55
56
|
Float: 'number',
|
|
56
57
|
Double: 'number',
|
|
57
58
|
Boolean: 'boolean',
|
|
58
|
-
// note: the date-related types
|
|
59
|
+
// note: the date-related types are strings on purpose, which reflects their runtime behaviour
|
|
59
60
|
Date: 'string', // yyyy-mm-dd
|
|
60
61
|
DateTime: 'string', // yyyy-mm-dd + time + TZ (precision: seconds
|
|
61
62
|
Time: 'string',
|
|
@@ -66,6 +67,34 @@ const Builtins = {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
class Resolver {
|
|
70
|
+
|
|
71
|
+
#caches = {
|
|
72
|
+
/**
|
|
73
|
+
* @type {{ [qualifier: string]: string }}
|
|
74
|
+
*/
|
|
75
|
+
namespaces: {},
|
|
76
|
+
/**
|
|
77
|
+
* @type {{ [qualifier: string]: string }}
|
|
78
|
+
*/
|
|
79
|
+
propertyAccesses: {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} qualifier
|
|
84
|
+
* @returns {string?}
|
|
85
|
+
*/
|
|
86
|
+
#getCachedNamespace (qualifier) {
|
|
87
|
+
return this.#caches.namespaces[qualifier]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} qualifier
|
|
92
|
+
* @returns {string?}
|
|
93
|
+
*/
|
|
94
|
+
#getCachedPropertyAccess (qualifier) {
|
|
95
|
+
return this.#caches.propertyAccesses[qualifier]
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
get csn() { return this.visitor.csn.inferred }
|
|
70
99
|
|
|
71
100
|
/** @param {Visitor} visitor */
|
|
@@ -90,18 +119,18 @@ class Resolver {
|
|
|
90
119
|
* to end up with both the resolved Path of the namespace,
|
|
91
120
|
* and the clean name of the class.
|
|
92
121
|
* @param {string} fq the fully qualified name of an entity.
|
|
93
|
-
* @returns {[Path, string]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity
|
|
122
|
+
* @returns {[Path, string, string[]]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity, [2] holding chained property accesses
|
|
94
123
|
*/
|
|
95
124
|
untangle(fq) {
|
|
96
125
|
const ns = this.resolveNamespace(fq.split('.'))
|
|
97
|
-
const name = this.trimNamespace(fq)
|
|
98
|
-
return [new Path(ns.split('.')), name]
|
|
126
|
+
const name = this.trimNamespace(fq).split('.').at(-1) // nested entities would return Foo.Bar, so we only take the last part to get the actual entity name
|
|
127
|
+
return [new Path(ns.split('.')), name, this.findPropertyAccess(name)]
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
/**
|
|
102
131
|
* Convenience method to shave off the namespace of a fully qualified path.
|
|
103
132
|
* More specifically, only the parts (reading from right to left) that are of
|
|
104
|
-
* kind "entity" are retained.
|
|
133
|
+
* kind "entity" or something similar are retained.
|
|
105
134
|
* a.b.c.Foo -> Foo
|
|
106
135
|
* Bar -> Bar
|
|
107
136
|
* sap.cap.Book.text -> Book.text (assuming Book and text are both of kind "entity")
|
|
@@ -109,19 +138,15 @@ class Resolver {
|
|
|
109
138
|
* @returns {string} the entity name without leading namespace.
|
|
110
139
|
*/
|
|
111
140
|
trimNamespace(p) {
|
|
112
|
-
|
|
113
|
-
// start on right side, go up while we have an entity at hand
|
|
114
|
-
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
141
|
+
if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
|
|
115
142
|
const parts = p.split('.')
|
|
116
|
-
if (parts.length <= 1)
|
|
117
|
-
return p
|
|
118
|
-
}
|
|
143
|
+
if (parts.length <= 1) return p
|
|
119
144
|
|
|
145
|
+
// start on right side, go up while we have an entity at hand
|
|
146
|
+
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
147
|
+
const defs = this.csn.definitions
|
|
120
148
|
let qualifier = parts.join('.')
|
|
121
|
-
while (
|
|
122
|
-
this.csn.definitions[qualifier] &&
|
|
123
|
-
['entity', 'type', 'aspect', 'event'].includes(this.csn.definitions[qualifier].kind)
|
|
124
|
-
) {
|
|
149
|
+
while (defs[qualifier] && ['entity', 'type', 'aspect', 'event'].includes(defs[qualifier].kind)) {
|
|
125
150
|
parts.pop()
|
|
126
151
|
qualifier = parts.join('.')
|
|
127
152
|
}
|
|
@@ -129,6 +154,51 @@ class Resolver {
|
|
|
129
154
|
return qualifier ? p.substring(qualifier.length + 1) : p
|
|
130
155
|
}
|
|
131
156
|
|
|
157
|
+
/**
|
|
158
|
+
* From a fully qualified path, finds the parts that are property accesses.
|
|
159
|
+
* This are specifically used in CDS' `typeof` syntax, where a property can
|
|
160
|
+
* refer to another entity's property type.
|
|
161
|
+
* @example
|
|
162
|
+
* ```
|
|
163
|
+
* namespace namespace;
|
|
164
|
+
* entity Entity {
|
|
165
|
+
* x: Composition of { y: Composition of z: { a: Integer }}
|
|
166
|
+
* }
|
|
167
|
+
*
|
|
168
|
+
* // somewhere else
|
|
169
|
+
* entity Foo {
|
|
170
|
+
* x: namespace.Entity.x.y.z;
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```js
|
|
176
|
+
* findPropertyAccess('namespace') // []
|
|
177
|
+
* findPropertyAccess('namespace.Entity') // []
|
|
178
|
+
* findPropertyAccess('namespace.Entity.x') // ['x']
|
|
179
|
+
* findPropertyAccess('namespace.Entity.x.y.z') // ['x', 'y', 'z']
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
findPropertyAccess(p) {
|
|
183
|
+
if (this.#getCachedPropertyAccess(p)) return this.#getCachedPropertyAccess(p)
|
|
184
|
+
const parts = p.split('.')
|
|
185
|
+
if (parts.length <= 1) return []
|
|
186
|
+
|
|
187
|
+
// start on right side, go up while we have an entity at hand
|
|
188
|
+
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
189
|
+
// sadly we have to use the extended flavour here, as inferred csn contains artificial entities for
|
|
190
|
+
// this kind of property access
|
|
191
|
+
const defs = this.visitor.csn.xtended.definitions
|
|
192
|
+
const properties = []
|
|
193
|
+
let qualifier = parts.join('.')
|
|
194
|
+
while (!defs[qualifier] && parts.length) {
|
|
195
|
+
properties.unshift(parts.pop())
|
|
196
|
+
qualifier = parts.join('.')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return properties
|
|
200
|
+
}
|
|
201
|
+
|
|
132
202
|
/**
|
|
133
203
|
* Generates singular and plural inflection for the passed type.
|
|
134
204
|
* Several cases are covered here:
|
|
@@ -148,7 +218,7 @@ class Resolver {
|
|
|
148
218
|
if (typeInfo.csn?.kind === 'type') {
|
|
149
219
|
return {
|
|
150
220
|
singular: typeInfo.plainName,
|
|
151
|
-
plural: typeInfo.plainName,
|
|
221
|
+
plural: createArrayOf(typeInfo.plainName),
|
|
152
222
|
typeName: typeInfo.plainName,
|
|
153
223
|
}
|
|
154
224
|
}
|
|
@@ -174,14 +244,13 @@ class Resolver {
|
|
|
174
244
|
plural = createArrayOf(typeName)
|
|
175
245
|
} else {
|
|
176
246
|
// TODO: make sure the resolution still works. Currently, we only cut off the namespace!
|
|
177
|
-
|
|
178
|
-
|
|
247
|
+
plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName
|
|
248
|
+
singular = util.getSingularAnnotation(typeInfo.csn) ?? util.singular4(typeInfo.csn, true) // util.singular4(typeInfo.csn, true) // can not use `plural` to honor possible @singular annotation
|
|
179
249
|
|
|
180
250
|
// don't slice off namespace if it isn't part of the inflected name.
|
|
181
251
|
// This happens when the user adds an annotation and singular4 therefore
|
|
182
|
-
// already returns an identifier without namespace
|
|
252
|
+
// already returns an identifier without namespace. Plural has ns already sliced off.
|
|
183
253
|
if (namespace && singular.startsWith(namespace)) {
|
|
184
|
-
// TODO: not totally sure why plural doesn't have to be sliced
|
|
185
254
|
singular = singular.slice(namespace.length + 1)
|
|
186
255
|
}
|
|
187
256
|
|
|
@@ -191,7 +260,7 @@ class Resolver {
|
|
|
191
260
|
}
|
|
192
261
|
}
|
|
193
262
|
if (!singular || !plural) {
|
|
194
|
-
this.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
|
|
263
|
+
this.visitor.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
|
|
195
264
|
}
|
|
196
265
|
|
|
197
266
|
return { typeName, singular, plural }
|
|
@@ -277,6 +346,13 @@ class Resolver {
|
|
|
277
346
|
typeInfo.inflection = this.inflect(typeInfo)
|
|
278
347
|
}
|
|
279
348
|
|
|
349
|
+
const [,,propertyAccess] = this.untangle(typeName)
|
|
350
|
+
if (propertyAccess.length) {
|
|
351
|
+
const element = typeName.slice(0, -propertyAccess.join('.').length - 1)
|
|
352
|
+
const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
|
|
353
|
+
typeName = deepRequire(element) + access
|
|
354
|
+
}
|
|
355
|
+
|
|
280
356
|
// add fallback inflection. Mainly needed for array-of with builtin types.
|
|
281
357
|
// (array-of relies on inflection being present, which is not the case in builtin)
|
|
282
358
|
typeInfo.inflection ??= {
|
|
@@ -348,9 +424,7 @@ class Resolver {
|
|
|
348
424
|
resolveType(element, file) {
|
|
349
425
|
// while resolving inline declarations, it can happen that we land here
|
|
350
426
|
// with an already resolved type. In that case, just return the type we have.
|
|
351
|
-
if (element && Object.hasOwn(element, 'isBuiltin'))
|
|
352
|
-
return element
|
|
353
|
-
}
|
|
427
|
+
if (element && Object.hasOwn(element, 'isBuiltin')) return element
|
|
354
428
|
|
|
355
429
|
const cardinality = this.getMaxCardinality(element)
|
|
356
430
|
|
|
@@ -370,7 +444,7 @@ class Resolver {
|
|
|
370
444
|
result.type = '{}'
|
|
371
445
|
result.isInlineDeclaration = true
|
|
372
446
|
} else {
|
|
373
|
-
if (isInlineEnumType(element, this.csn)) {
|
|
447
|
+
if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
|
|
374
448
|
// element.parent is only set if the enum is attached to an entity's property.
|
|
375
449
|
// If it is missing then we are dealing with an inline parameter type of an action.
|
|
376
450
|
// Edge case: element.parent is set, but no .name property is attached. This happens
|
package/lib/csn.js
CHANGED
|
@@ -14,8 +14,8 @@ class DraftUnroller {
|
|
|
14
14
|
this.#csn = c
|
|
15
15
|
this.#entities = Object.values(c.definitions)
|
|
16
16
|
this.#projections = this.#entities.reduce((pjs, entity) => {
|
|
17
|
-
if (entity
|
|
18
|
-
pjs[entity.name] = entity
|
|
17
|
+
if (isProjection(entity)) {
|
|
18
|
+
pjs[entity.name] = getProjectionTarget(entity)
|
|
19
19
|
}
|
|
20
20
|
return pjs
|
|
21
21
|
}, {})
|
|
@@ -234,10 +234,46 @@ function amendCSN(csn) {
|
|
|
234
234
|
*/
|
|
235
235
|
const isView = entity => entity.query && !entity.projection
|
|
236
236
|
|
|
237
|
+
const isProjection = entity => entity.projection
|
|
238
|
+
|
|
239
|
+
const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
|
|
240
|
+
|
|
241
|
+
const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
|
|
242
|
+
|
|
243
|
+
const getProjectionAliases = entity => {
|
|
244
|
+
const aliases = {}
|
|
245
|
+
let all = false
|
|
246
|
+
for (const col of entity?.projection?.columns ?? []) {
|
|
247
|
+
if (col === '*') {
|
|
248
|
+
all = true
|
|
249
|
+
} else if (col.ref) {
|
|
250
|
+
(aliases[col.ref[0]] ??= []).push(col.as ?? col.ref[0])
|
|
251
|
+
} else {
|
|
252
|
+
// TODO: error, casting seems to miss ref...
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { aliases, all }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
|
|
259
|
+
|
|
260
|
+
const isType = entity => entity?.kind === 'type'
|
|
261
|
+
|
|
237
262
|
/**
|
|
238
263
|
* @see isView
|
|
239
264
|
* Unresolved entities have to be looked up from inferred csn.
|
|
240
265
|
*/
|
|
241
266
|
const isUnresolved = entity => entity._unresolved === true
|
|
242
267
|
|
|
243
|
-
module.exports = {
|
|
268
|
+
module.exports = {
|
|
269
|
+
amendCSN,
|
|
270
|
+
isView,
|
|
271
|
+
isProjection,
|
|
272
|
+
isDraftEnabled,
|
|
273
|
+
isUnresolved,
|
|
274
|
+
isType,
|
|
275
|
+
getProjectionTarget,
|
|
276
|
+
getProjectionAliases,
|
|
277
|
+
getViewTarget,
|
|
278
|
+
propagateForeignKeys
|
|
279
|
+
}
|
package/lib/file.js
CHANGED
|
@@ -93,10 +93,13 @@ class Library extends File {
|
|
|
93
93
|
* Source file containing several buffers.
|
|
94
94
|
*/
|
|
95
95
|
class SourceFile extends File {
|
|
96
|
+
/**
|
|
97
|
+
* @param {string | Path} path
|
|
98
|
+
*/
|
|
96
99
|
constructor(path) {
|
|
97
100
|
super()
|
|
98
101
|
/** @type {Path} */
|
|
99
|
-
this.path = new Path(path.split('.'))
|
|
102
|
+
this.path = path instanceof Path ? path : new Path(path.split('.'))
|
|
100
103
|
/** @type {Object} */
|
|
101
104
|
this.imports = {}
|
|
102
105
|
/** @type {Buffer} */
|
|
@@ -480,6 +483,33 @@ class Buffer {
|
|
|
480
483
|
add(part) {
|
|
481
484
|
this.parts.push(this.currentIndent + part)
|
|
482
485
|
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Adds an element to the buffer with one level of indent.
|
|
489
|
+
* @param {string | (() => void)} part either a string or a function. If it is a string, it is added to the buffer.
|
|
490
|
+
* If not, it is expected to be a function that manipulates the buffer as a side effect.
|
|
491
|
+
*/
|
|
492
|
+
addIndented(part) {
|
|
493
|
+
this.indent()
|
|
494
|
+
if (typeof part === 'function') {
|
|
495
|
+
part()
|
|
496
|
+
} else if (Array.isArray(part)) {
|
|
497
|
+
part.forEach(p => this.add(p))
|
|
498
|
+
}
|
|
499
|
+
this.outdent()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Adds an element to a buffer with one level of indent and and opener and closer surrounding it.
|
|
504
|
+
* @param {string} opener the string to put before the indent
|
|
505
|
+
* @param {string} content the content to indent (see {@link addIndented})
|
|
506
|
+
* @param {string} closer the string to put after the indent
|
|
507
|
+
*/
|
|
508
|
+
addIndentedBlock(opener, content, closer) {
|
|
509
|
+
this.add(opener)
|
|
510
|
+
this.addIndented(content)
|
|
511
|
+
this.add(closer)
|
|
512
|
+
}
|
|
483
513
|
}
|
|
484
514
|
|
|
485
515
|
/**
|
|
@@ -548,6 +578,38 @@ class Path {
|
|
|
548
578
|
}
|
|
549
579
|
}
|
|
550
580
|
|
|
581
|
+
// TODO: having the repository pattern in place we can separate (some of) the printing logic from the visitor.
|
|
582
|
+
// Most of it hinges primarily on resolving specific files. We can now pass the repository and the resolver to a printer.
|
|
583
|
+
class FileRepository {
|
|
584
|
+
#files = {}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* @param {string} name
|
|
588
|
+
* @param {SourceFile} file
|
|
589
|
+
*/
|
|
590
|
+
add(name, file) {
|
|
591
|
+
this.#files[name] = file
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Determines the file corresponding to the namespace.
|
|
596
|
+
* If no such file exists yet, it is created first.
|
|
597
|
+
* @param {string | Path} path the name of the namespace (foo.bar.baz)
|
|
598
|
+
* @returns {SourceFile} the file corresponding to that namespace name
|
|
599
|
+
*/
|
|
600
|
+
getNamespaceFile(path) {
|
|
601
|
+
const key = path instanceof Path ? path.asNamespace() : path
|
|
602
|
+
return (this.#files[key] ??= new SourceFile(path))
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* @returns {SourceFile[]}
|
|
607
|
+
*/
|
|
608
|
+
getFiles() {
|
|
609
|
+
return Object.values(this.#files)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
551
613
|
/**
|
|
552
614
|
* Base definitions used throughout the typing process,
|
|
553
615
|
* such as Associations and Compositions.
|
|
@@ -583,7 +645,7 @@ export type EntitySet<T> = T[] & {
|
|
|
583
645
|
|
|
584
646
|
export type DeepRequired<T> = {
|
|
585
647
|
[K in keyof T]: DeepRequired<T[K]>
|
|
586
|
-
} & Required<T>;
|
|
648
|
+
} & Exclude<Required<T>, null>;
|
|
587
649
|
`)
|
|
588
650
|
|
|
589
651
|
/**
|
|
@@ -618,6 +680,7 @@ module.exports = {
|
|
|
618
680
|
Library,
|
|
619
681
|
Buffer,
|
|
620
682
|
File,
|
|
683
|
+
FileRepository,
|
|
621
684
|
SourceFile,
|
|
622
685
|
Path,
|
|
623
686
|
writeout,
|
package/lib/util.js
CHANGED
|
@@ -70,7 +70,7 @@ const singular4 = (dn, stripped = false) => {
|
|
|
70
70
|
n = n.match(last)[0]
|
|
71
71
|
}
|
|
72
72
|
return (
|
|
73
|
-
getSingularAnnotation(dn)
|
|
73
|
+
getSingularAnnotation(dn) ??
|
|
74
74
|
(/.*species|news$/i.test(n)
|
|
75
75
|
? n
|
|
76
76
|
: /.*ess$/.test(n)
|
|
@@ -102,7 +102,7 @@ const plural4 = (dn, stripped) => {
|
|
|
102
102
|
n = n.match(last)[0]
|
|
103
103
|
}
|
|
104
104
|
return (
|
|
105
|
-
getPluralAnnotation(dn)
|
|
105
|
+
getPluralAnnotation(dn) ??
|
|
106
106
|
(/.*analysis|status|species|news$/i.test(n)
|
|
107
107
|
? n
|
|
108
108
|
: /.*[^aeiou]y$/.test(n)
|
package/lib/visitor.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const { amendCSN, isView, isUnresolved, propagateForeignKeys } = require('./csn')
|
|
5
|
+
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
|
-
const { SourceFile, baseDefinitions, Buffer } = require('./file')
|
|
7
|
+
const { SourceFile, FileRepository, baseDefinitions, Buffer } = require('./file')
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
9
|
const { Resolver } = require('./components/resolver')
|
|
10
10
|
const { Logger } = require('./logging')
|
|
11
11
|
const { docify } = require('./components/wrappers')
|
|
12
12
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
13
|
+
const { isReferenceType } = require('./components/reference')
|
|
13
14
|
|
|
14
15
|
/** @typedef {import('./file').File} File */
|
|
15
16
|
/** @typedef {{ entity: String }} Context */
|
|
@@ -54,7 +55,7 @@ class Visitor {
|
|
|
54
55
|
* @returns {File[]} a full list of files to be written
|
|
55
56
|
*/
|
|
56
57
|
getWriteoutFiles() {
|
|
57
|
-
return
|
|
58
|
+
return this.fileRepository.getFiles().concat(this.resolver.getUsedLibraries())
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -74,9 +75,9 @@ class Visitor {
|
|
|
74
75
|
/** @type {Resolver} */
|
|
75
76
|
this.resolver = new Resolver(this)
|
|
76
77
|
|
|
77
|
-
/** @type {
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
78
|
+
/** @type {FileRepository} */
|
|
79
|
+
this.fileRepository = new FileRepository()
|
|
80
|
+
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
80
81
|
this.inlineDeclarationResolver =
|
|
81
82
|
this.options.inlineDeclarations === 'structured'
|
|
82
83
|
? new StructuredInlineDeclarationResolver(this)
|
|
@@ -85,16 +86,6 @@ class Visitor {
|
|
|
85
86
|
this.visitDefinitions()
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
/**
|
|
89
|
-
* Determines the file corresponding to the namespace.
|
|
90
|
-
* If no such file exists yet, it is created first.
|
|
91
|
-
* @param {string} path the name of the namespace (foo.bar.baz)
|
|
92
|
-
* @returns {SourceFile} the file corresponding to that namespace name
|
|
93
|
-
*/
|
|
94
|
-
getNamespaceFile(path) {
|
|
95
|
-
return (this.files[path] ??= new SourceFile(path))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
89
|
/**
|
|
99
90
|
* Visits all definitions within the CSN definitions.
|
|
100
91
|
*/
|
|
@@ -102,7 +93,7 @@ class Visitor {
|
|
|
102
93
|
for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
|
|
103
94
|
if (isView(entity)) {
|
|
104
95
|
this.visitEntity(name, this.csn.inferred.definitions[name])
|
|
105
|
-
} else if (!isUnresolved(entity)) {
|
|
96
|
+
} else if (isProjection(entity) || !isUnresolved(entity)) {
|
|
106
97
|
this.visitEntity(name, entity)
|
|
107
98
|
} else {
|
|
108
99
|
this.logger.warning(`Skipping unresolved entity: ${name}`)
|
|
@@ -158,79 +149,68 @@ class Visitor {
|
|
|
158
149
|
* @param {string} name the name of the entity
|
|
159
150
|
* @param {CSN} element the pointer into the CSN to extract the elements from
|
|
160
151
|
* @param {Buffer} buffer the buffer to write the resulting definitions into
|
|
161
|
-
* @param {string
|
|
152
|
+
* @param {{cleanName?: string}} options
|
|
162
153
|
*/
|
|
163
|
-
#aspectify(name, entity, buffer,
|
|
164
|
-
const clean = cleanName ?? this.resolver.trimNamespace(name)
|
|
154
|
+
#aspectify(name, entity, buffer, options = {}) {
|
|
155
|
+
const clean = options?.cleanName ?? this.resolver.trimNamespace(name)
|
|
165
156
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
166
|
-
const file = this.getNamespaceFile(ns)
|
|
167
|
-
|
|
157
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
168
158
|
const identSingular = (name) => name
|
|
169
159
|
const identAspect = (name) => `_${name}Aspect`
|
|
170
160
|
|
|
171
161
|
this.contexts.push({ entity: name })
|
|
172
162
|
|
|
173
163
|
// CLASS ASPECT
|
|
174
|
-
buffer.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
164
|
+
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, function () {
|
|
165
|
+
buffer.addIndentedBlock(`return class ${clean} extends Base {`, function () {
|
|
166
|
+
const enums = []
|
|
167
|
+
for (let [ename, element] of Object.entries(entity.elements ?? {})) {
|
|
168
|
+
if (element.target && /\.texts?/.test(element.target)) {
|
|
169
|
+
this.logger.warning(`referring to .texts property in ${name}. This is currently not supported and will be ignored.`)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
this.visitElement(ename, element, file, buffer)
|
|
173
|
+
|
|
174
|
+
// make foreign keys explicit
|
|
175
|
+
if ('target' in element) {
|
|
176
|
+
// lookup in cds.definitions can fail for inline structs.
|
|
177
|
+
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
178
|
+
// the containing entity.
|
|
179
|
+
for (const [kname, kelement] of this.#keys(element.target)) {
|
|
180
|
+
if (this.resolver.getMaxCardinality(element) === 1) { // FIXME: kelement?
|
|
181
|
+
const foreignKey = `${ename}_${kname}`
|
|
182
|
+
if (Object.hasOwn(entity.elements, foreignKey)) {
|
|
183
|
+
this.logger.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${name}. But a property of that name is already defined explicitly. Consider renaming that property.`)
|
|
184
|
+
} else {
|
|
185
|
+
kelement.isRefNotNull = !!element.notNull || !!element.key
|
|
186
|
+
this.visitElement(foreignKey, kelement, file, buffer)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
198
189
|
}
|
|
199
190
|
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
191
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
buffer.indent()
|
|
210
|
-
for (const e of enums) {
|
|
211
|
-
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
212
|
-
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
213
|
-
}
|
|
214
|
-
buffer.add('static actions: {')
|
|
215
|
-
buffer.indent()
|
|
216
|
-
for (const [aname, action] of Object.entries(entity.actions ?? {})) {
|
|
217
|
-
buffer.add(
|
|
218
|
-
SourceFile.stringifyLambda({
|
|
219
|
-
name: aname,
|
|
220
|
-
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
221
|
-
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
222
|
-
//initialiser: `undefined as unknown as typeof ${clean}.${aname}`,
|
|
223
|
-
})
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
buffer.outdent()
|
|
227
|
-
buffer.outdent()
|
|
228
|
-
buffer.add('}') // end of actions
|
|
192
|
+
// store inline enums for later handling, as they have to go into one common "static elements" wrapper
|
|
193
|
+
if (isInlineEnumType(element, this.csn.xtended)) {
|
|
194
|
+
enums.push(element)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
229
197
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
198
|
+
buffer.addIndented(function() {
|
|
199
|
+
for (const e of enums) {
|
|
200
|
+
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
201
|
+
file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
202
|
+
}
|
|
203
|
+
const actions = Object.entries(entity.actions ?? {})
|
|
204
|
+
buffer.addIndentedBlock('static actions: {',
|
|
205
|
+
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
206
|
+
name: aname,
|
|
207
|
+
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
208
|
+
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
209
|
+
}))
|
|
210
|
+
, '}') // end of actions
|
|
211
|
+
}.bind(this))
|
|
212
|
+
}.bind(this), '};') // end of generated class
|
|
213
|
+
}.bind(this), '}') // end of aspect
|
|
234
214
|
|
|
235
215
|
// CLASS WITH ADDED ASPECTS
|
|
236
216
|
file.addImport(baseDefinitions.path)
|
|
@@ -255,35 +235,37 @@ class Visitor {
|
|
|
255
235
|
this.contexts.pop()
|
|
256
236
|
}
|
|
257
237
|
|
|
258
|
-
#isDraftEnabled(entity) {
|
|
259
|
-
return entity['@odata.draft.enabled'] === true
|
|
260
|
-
}
|
|
261
|
-
|
|
262
238
|
#staticClassContents(clean, entity) {
|
|
263
|
-
return
|
|
239
|
+
return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
264
240
|
}
|
|
265
241
|
|
|
266
242
|
#printEntity(name, entity) {
|
|
267
243
|
// static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
|
|
268
244
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
269
|
-
const clean = this.resolver.
|
|
270
|
-
const
|
|
271
|
-
const file = this.getNamespaceFile(ns)
|
|
245
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
246
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
272
247
|
// entities are expected to be in plural anyway, so we would favour the regular name.
|
|
273
248
|
// If the user decides to pass a @plural annotation, that gets precedence over the regular name.
|
|
249
|
+
|
|
250
|
+
/*
|
|
274
251
|
let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name)
|
|
275
252
|
const singular = this.resolver.trimNamespace(util.singular4(entity, true))
|
|
253
|
+
*/
|
|
254
|
+
let { singular, plural } = this.resolver.inflect({csn: entity, plainName: clean}, ns.asNamespace())
|
|
255
|
+
|
|
276
256
|
// trimNamespace does not properly detect scoped entities, like A.B where both A and B are
|
|
277
257
|
// entities. So to see if we would run into a naming collision, we forcefully take the last
|
|
278
258
|
// part of the name, so "A.B" and "A.Bs" just become "B" and "Bs" to be compared.
|
|
279
259
|
// FIXME: put this in a util function
|
|
280
|
-
if (singular.split('.').at(-1) === plural.split('.').at(-1)) {
|
|
281
|
-
|
|
260
|
+
//if (singular.split('.').at(-1) === plural.split('.').at(-1)) {
|
|
261
|
+
if (plural.split('.').at(-1) === `${singular.split('.').at(-1)}_`) {
|
|
262
|
+
//plural += '_'
|
|
282
263
|
this.logger.warning(
|
|
283
264
|
`Derived singular and plural forms for '${singular}' are the same. This usually happens when your CDS entities are named following singular flexion. Consider naming your entities in plural or providing '@singular:'/ '@plural:' annotations to have a clear distinction between the two. Plural form will be renamed to '${plural}' to avoid compilation errors within the output.`
|
|
284
265
|
)
|
|
285
266
|
}
|
|
286
|
-
|
|
267
|
+
// as types are not inflected, their singular will always clash and there is also no plural for them anyway
|
|
268
|
+
if (!isType(entity) && singular in this.csn.xtended.definitions) {
|
|
287
269
|
this.logger.error(
|
|
288
270
|
`Derived singular '${singular}' for your entity '${name}', already exists. The resulting types will be erronous. Consider using '@singular:'/ '@plural:' annotations in your model or move the offending declarations into different namespaces to resolve this collision.`
|
|
289
271
|
)
|
|
@@ -303,22 +285,33 @@ class Visitor {
|
|
|
303
285
|
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
|
|
304
286
|
// edge case: @singular annotation present. singular4 will take care of that.
|
|
305
287
|
file.addInflection(util.singular4(entity, true), plural, clean)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
288
|
+
docify(entity.doc).forEach(d => buffer.add(d))
|
|
289
|
+
|
|
290
|
+
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
291
|
+
const target = isProjection(entity) || isView(entity)
|
|
292
|
+
? this.csn.inferred.definitions[name]
|
|
293
|
+
: entity
|
|
294
|
+
|
|
295
|
+
// draft enablement is stored in csn.xtended. Iff we took the entity from csn.inferred, we have to carry the draft-enablement over at this point
|
|
296
|
+
target['@odata.draft.enabled'] = isDraftEnabled(entity)
|
|
309
297
|
|
|
310
|
-
this.#aspectify(name,
|
|
298
|
+
this.#aspectify(name, target, buffer, { cleanName: singular })
|
|
299
|
+
|
|
300
|
+
buffer.add(overrideNameProperty(singular, entity.name))
|
|
311
301
|
|
|
312
302
|
// PLURAL
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
303
|
+
|
|
304
|
+
// types do not receive a plural
|
|
305
|
+
if (!isType(entity)) {
|
|
306
|
+
if (plural.includes('.')) {
|
|
307
|
+
// Foo.text -> namespace Foo { class text { ... }}
|
|
308
|
+
plural = plural.split('.').at(-1)
|
|
309
|
+
}
|
|
310
|
+
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
311
|
+
// so it can get passed as value to CQL functions.
|
|
312
|
+
buffer.add(`export class ${plural} extends Array<${singular}> {${this.#staticClassContents(singular, entity).join('\n')}}`)
|
|
313
|
+
buffer.add(overrideNameProperty(plural, entity.name))
|
|
316
314
|
}
|
|
317
|
-
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
318
|
-
// so it can get passed as value to CQL functions.
|
|
319
|
-
buffer.add(`export class ${plural} extends Array<${singular}> {${this.#staticClassContents(singular, entity).join('\n')}}`)
|
|
320
|
-
buffer.add(overrideNameProperty(singular, entity.name))
|
|
321
|
-
buffer.add(overrideNameProperty(plural, entity.name))
|
|
322
315
|
buffer.add('')
|
|
323
316
|
}
|
|
324
317
|
|
|
@@ -354,7 +347,7 @@ class Visitor {
|
|
|
354
347
|
// FIXME: mostly duplicate of printAction -> reuse
|
|
355
348
|
this.logger.debug(`Printing function ${name}:\n${JSON.stringify(func, null, 2)}`)
|
|
356
349
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
357
|
-
const file = this.getNamespaceFile(ns)
|
|
350
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
358
351
|
const params = this.#stringifyFunctionParams(func.params, file)
|
|
359
352
|
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
360
353
|
this.resolver.resolveAndRequire(func.returns, file)
|
|
@@ -365,7 +358,7 @@ class Visitor {
|
|
|
365
358
|
#printAction(name, action) {
|
|
366
359
|
this.logger.debug(`Printing action ${name}:\n${JSON.stringify(action, null, 2)}`)
|
|
367
360
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
368
|
-
const file = this.getNamespaceFile(ns)
|
|
361
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
369
362
|
const params = this.#stringifyFunctionParams(action.params, file)
|
|
370
363
|
const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
|
|
371
364
|
this.resolver.resolveAndRequire(action.returns, file)
|
|
@@ -375,10 +368,9 @@ class Visitor {
|
|
|
375
368
|
|
|
376
369
|
#printType(name, type) {
|
|
377
370
|
this.logger.debug(`Printing type ${name}:\n${JSON.stringify(type, null, 2)}`)
|
|
378
|
-
const clean = this.resolver.
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
if ('enum' in type) {
|
|
371
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
372
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
373
|
+
if ('enum' in type && !isReferenceType(type)) { // skip references to enums
|
|
382
374
|
file.addEnum(name, clean, csnToEnumPairs(type))
|
|
383
375
|
} else {
|
|
384
376
|
// alias
|
|
@@ -389,41 +381,37 @@ class Visitor {
|
|
|
389
381
|
|
|
390
382
|
#printAspect(name, aspect) {
|
|
391
383
|
this.logger.debug(`Printing aspect ${name}`)
|
|
392
|
-
const clean = this.resolver.
|
|
393
|
-
const
|
|
394
|
-
const file = this.getNamespaceFile(ns)
|
|
384
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
385
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
395
386
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
396
387
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
397
388
|
// So we separate them into another buffer which is printed before the classes.
|
|
398
389
|
file.addClass(clean, name)
|
|
399
390
|
file.aspects.add(`// the following represents the CDS aspect '${clean}'`)
|
|
400
|
-
this.#aspectify(name, aspect, file.aspects, clean)
|
|
391
|
+
this.#aspectify(name, aspect, file.aspects, { cleanName: clean })
|
|
401
392
|
}
|
|
402
393
|
|
|
403
394
|
#printEvent(name, event) {
|
|
404
395
|
this.logger.debug(`Printing event ${name}`)
|
|
405
|
-
const clean = this.resolver.
|
|
406
|
-
const
|
|
407
|
-
const file = this.getNamespaceFile(ns)
|
|
396
|
+
const [ns, clean] = this.resolver.untangle(name)
|
|
397
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
408
398
|
file.addEvent(clean, name)
|
|
409
399
|
const buffer = file.events.buffer
|
|
410
400
|
buffer.add('// event')
|
|
411
|
-
buffer.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
this
|
|
419
|
-
buffer.outdent()
|
|
420
|
-
buffer.add('}')
|
|
401
|
+
buffer.addIndentedBlock(`export class ${clean} {`, function() {
|
|
402
|
+
const propOpt = this.options.propertiesOptional
|
|
403
|
+
this.options.propertiesOptional = false
|
|
404
|
+
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
405
|
+
this.visitElement(ename, element, file, buffer)
|
|
406
|
+
}
|
|
407
|
+
this.options.propertiesOptional = propOpt
|
|
408
|
+
}.bind(this), '}')
|
|
421
409
|
}
|
|
422
410
|
|
|
423
411
|
#printService(name, service) {
|
|
424
412
|
this.logger.debug(`Printing service ${name}:\n${JSON.stringify(service, null, 2)}`)
|
|
425
413
|
const ns = this.resolver.resolveNamespace(name)
|
|
426
|
-
const file = this.getNamespaceFile(ns)
|
|
414
|
+
const file = this.fileRepository.getNamespaceFile(ns)
|
|
427
415
|
// service.name is clean of namespace
|
|
428
416
|
file.services.buffer.add(`export default { name: '${service.name}' }`)
|
|
429
417
|
file.addService(service.name)
|
|
@@ -453,7 +441,7 @@ class Visitor {
|
|
|
453
441
|
// types like inline definitions can be used very similarly to entities.
|
|
454
442
|
// They can be extended, contain inline enums, etc., so we treat them as entities.
|
|
455
443
|
const handler = entity.elements ? this.#printEntity : this.#printType
|
|
456
|
-
handler.
|
|
444
|
+
handler.call(this, name, entity)
|
|
457
445
|
break
|
|
458
446
|
}
|
|
459
447
|
case 'event':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Generates .ts files for a CDS model to receive code completion in VS Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": "github:cap-js/cds-typer",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@babel/eslint-parser": "^7.23.3",
|
|
44
|
+
"@stylistic/eslint-plugin-js": "^1.6.3",
|
|
44
45
|
"acorn": "^8.10.0",
|
|
45
46
|
"eslint": "^8.15.0",
|
|
46
47
|
"jest": "^29",
|
|
@@ -48,8 +49,7 @@
|
|
|
48
49
|
},
|
|
49
50
|
"jest": {
|
|
50
51
|
"projects": [
|
|
51
|
-
"test/unit.jest.config.js"
|
|
52
|
-
"test/int.jest.config.js"
|
|
52
|
+
"test/unit.jest.config.js"
|
|
53
53
|
]
|
|
54
54
|
}
|
|
55
55
|
}
|