@cap-js/cds-typer 0.22.0 → 0.23.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 +9 -1
- package/cds-plugin.js +4 -4
- package/lib/cli.js +19 -8
- package/lib/compile.js +8 -10
- package/lib/components/enum.js +12 -12
- package/lib/components/identifier.js +9 -1
- package/lib/components/inline.js +15 -13
- package/lib/components/reference.js +1 -1
- package/lib/components/wrappers.js +8 -8
- package/lib/csn.js +38 -25
- package/lib/file.js +34 -30
- package/lib/logging.js +12 -70
- package/lib/resolution/builtin.js +64 -0
- package/lib/resolution/entity.js +155 -0
- package/lib/{components → resolution}/resolver.js +54 -163
- package/lib/typedefs.d.ts +21 -0
- package/lib/util.js +6 -6
- package/lib/visitor.js +116 -124
- package/package.json +2 -1
package/lib/visitor.js
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection } = require('./csn')
|
|
5
|
+
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
7
|
const { SourceFile, FileRepository, Buffer } = require('./file')
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
|
-
const { Resolver } = require('./
|
|
10
|
-
const {
|
|
9
|
+
const { Resolver } = require('./resolution/resolver')
|
|
10
|
+
const { LOG } = require('./logging')
|
|
11
11
|
const { docify } = require('./components/wrappers')
|
|
12
12
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
13
13
|
const { isReferenceType } = require('./components/reference')
|
|
14
14
|
const { empty } = require('./components/typescript')
|
|
15
15
|
const { baseDefinitions } = require('./components/basedefs')
|
|
16
|
+
const { EntityRepository } = require('./resolution/entity')
|
|
17
|
+
const { last } = require('./components/identifier')
|
|
16
18
|
|
|
17
19
|
/** @typedef {import('./file').File} File */
|
|
18
20
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
@@ -39,14 +41,12 @@ class Visitor {
|
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
42
|
-
* @param {VisitorOptions} options
|
|
43
|
-
* @param {Logger} logger
|
|
44
|
+
* @param {VisitorOptions} options - the options
|
|
44
45
|
*/
|
|
45
|
-
constructor(csn, options = {}
|
|
46
|
+
constructor(csn, options = {}) {
|
|
46
47
|
amendCSN(csn.xtended)
|
|
47
48
|
propagateForeignKeys(csn.inferred)
|
|
48
49
|
this.options = { ...defaults, ...options }
|
|
49
|
-
this.logger = logger
|
|
50
50
|
this.csn = csn
|
|
51
51
|
|
|
52
52
|
/** @type {Context[]} **/
|
|
@@ -55,6 +55,9 @@ class Visitor {
|
|
|
55
55
|
/** @type {Resolver} */
|
|
56
56
|
this.resolver = new Resolver(this)
|
|
57
57
|
|
|
58
|
+
/** @type {EntityRepository} */
|
|
59
|
+
this.entityRepository = new EntityRepository(this.resolver)
|
|
60
|
+
|
|
58
61
|
/** @type {FileRepository} */
|
|
59
62
|
this.fileRepository = new FileRepository()
|
|
60
63
|
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
@@ -76,7 +79,7 @@ class Visitor {
|
|
|
76
79
|
} else if (isProjection(entity) || !isUnresolved(entity)) {
|
|
77
80
|
this.visitEntity(name, entity)
|
|
78
81
|
} else {
|
|
79
|
-
|
|
82
|
+
LOG.warn(`Skipping unresolved entity: ${name}`)
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
// FIXME: optimise
|
|
@@ -91,13 +94,13 @@ class Visitor {
|
|
|
91
94
|
// instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
|
|
92
95
|
// The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
|
|
93
96
|
if (entity.projection) {
|
|
94
|
-
const targetName = entity.projection.from.ref[0]
|
|
97
|
+
const targetName = entity.projection.from.ref[0]
|
|
95
98
|
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
|
|
96
99
|
// this should be revisted once we settle on a single flavour.
|
|
97
100
|
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
|
|
98
101
|
this.visitEntity(name, target)
|
|
99
102
|
} else {
|
|
100
|
-
|
|
103
|
+
LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
}
|
|
@@ -105,10 +108,10 @@ class Visitor {
|
|
|
105
108
|
/**
|
|
106
109
|
* Retrieves all the keys from an entity.
|
|
107
110
|
* That is: all keys that are present in both inferred, as well as xtended flavour.
|
|
108
|
-
* @param {string} name
|
|
111
|
+
* @param {string} fq - fully qualified name of the entity
|
|
109
112
|
* @returns {[string, object][]} array of key name and key element pairs
|
|
110
113
|
*/
|
|
111
|
-
#keys(
|
|
114
|
+
#keys(fq) {
|
|
112
115
|
// FIXME: this is actually pretty bad, as not only have to propagate keys through
|
|
113
116
|
// both flavours of CSN (see constructor), but we are now also collecting them from
|
|
114
117
|
// both flavours and deduplicating them.
|
|
@@ -116,8 +119,8 @@ class Visitor {
|
|
|
116
119
|
// inferred contains keys from queried entities (thing `entity Foo as select from Bar`, where Bar has keys)
|
|
117
120
|
// So we currently need them both.
|
|
118
121
|
return Object.entries({
|
|
119
|
-
...this.csn.inferred.definitions[
|
|
120
|
-
...this.csn.xtended.definitions[
|
|
122
|
+
...this.csn.inferred.definitions[fq]?.keys ?? {},
|
|
123
|
+
...this.csn.xtended.definitions[fq]?.keys ?? {}
|
|
121
124
|
})
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -127,27 +130,47 @@ class Visitor {
|
|
|
127
130
|
* - the function A(B) to mix the aspect into another class B
|
|
128
131
|
* - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
|
|
129
132
|
* - the type A to use for external typing and is derived from AXtended.
|
|
130
|
-
* @param {string}
|
|
133
|
+
* @param {string} fq - the name of the entity
|
|
131
134
|
* @param {CSN} entity - the pointer into the CSN to extract the elements from
|
|
132
135
|
* @param {Buffer} buffer - the buffer to write the resulting definitions into
|
|
133
|
-
* @param {{cleanName?: string}} options
|
|
136
|
+
* @param {{cleanName?: string}} options - additional options
|
|
134
137
|
*/
|
|
135
|
-
#aspectify(
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
+
#aspectify(fq, entity, buffer, options = {}) {
|
|
139
|
+
const info = this.entityRepository.getByFq(fq)
|
|
140
|
+
const clean = options?.cleanName ?? info.withoutNamespace
|
|
141
|
+
const { namespace } = info
|
|
138
142
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
139
143
|
const identSingular = name => name
|
|
140
144
|
const identAspect = name => `_${name}Aspect`
|
|
145
|
+
const toAspectIdent = (wrapped, [ns, n, fq]) => {
|
|
146
|
+
// types are not inflected, so don't change those to singular
|
|
147
|
+
const refersToType = isType(this.csn.inferred.definitions[fq])
|
|
148
|
+
const ident = identAspect(refersToType
|
|
149
|
+
? n
|
|
150
|
+
: this.resolver.inflect({csn: this.csn.inferred.definitions[fq], plainName: n}).singular
|
|
151
|
+
)
|
|
152
|
+
return !ns || ns.isCwd(file.path.asDirectory())
|
|
153
|
+
? `${ident}(${wrapped})`
|
|
154
|
+
: `${ns.asIdentifier()}.${ident}(${wrapped})`
|
|
155
|
+
}
|
|
156
|
+
const ancestors = (entity.includes ?? [])
|
|
157
|
+
.map(parent => {
|
|
158
|
+
const { namespace, entityName } = this.entityRepository.getByFq(parent)
|
|
159
|
+
file.addImport(namespace)
|
|
160
|
+
return [namespace, entityName, parent]
|
|
161
|
+
})
|
|
162
|
+
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
163
|
+
.reduce(toAspectIdent, 'Base')
|
|
141
164
|
|
|
142
|
-
this.contexts.push({ entity:
|
|
165
|
+
this.contexts.push({ entity: fq })
|
|
143
166
|
|
|
144
167
|
// CLASS ASPECT
|
|
145
|
-
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`,
|
|
146
|
-
buffer.addIndentedBlock(`return class ${
|
|
168
|
+
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
|
|
169
|
+
buffer.addIndentedBlock(`return class extends ${ancestors} {`, () => {
|
|
147
170
|
const enums = []
|
|
148
171
|
for (let [ename, element] of Object.entries(entity.elements ?? {})) {
|
|
149
172
|
if (element.target && /\.texts?/.test(element.target)) {
|
|
150
|
-
|
|
173
|
+
LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
|
|
151
174
|
continue
|
|
152
175
|
}
|
|
153
176
|
this.visitElement(ename, element, file, buffer)
|
|
@@ -158,13 +181,13 @@ class Visitor {
|
|
|
158
181
|
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
159
182
|
// the containing entity.
|
|
160
183
|
for (const [kname, originalKeyElement] of this.#keys(element.target)) {
|
|
161
|
-
if (
|
|
184
|
+
if (getMaxCardinality(element) === 1 && typeof element.on !== 'object') { // FIXME: kelement?
|
|
162
185
|
const foreignKey = `${ename}_${kname}`
|
|
163
186
|
if (Object.hasOwn(entity.elements, foreignKey)) {
|
|
164
|
-
|
|
187
|
+
LOG.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${fq}. But a property of that name is already defined explicitly. Consider renaming that property.`)
|
|
165
188
|
} else {
|
|
166
|
-
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
167
|
-
isRefNotNull: !!element.notNull || !!element.key
|
|
189
|
+
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
190
|
+
isRefNotNull: !!element.notNull || !!element.key
|
|
168
191
|
})
|
|
169
192
|
this.visitElement(foreignKey, kelement, file, buffer)
|
|
170
193
|
}
|
|
@@ -178,14 +201,14 @@ class Visitor {
|
|
|
178
201
|
}
|
|
179
202
|
}
|
|
180
203
|
|
|
181
|
-
buffer.addIndented(
|
|
204
|
+
buffer.addIndented(() => {
|
|
182
205
|
for (const e of enums) {
|
|
183
206
|
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
184
|
-
file.addInlineEnum(clean,
|
|
207
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
185
208
|
}
|
|
186
209
|
const actions = Object.entries(entity.actions ?? {})
|
|
187
210
|
if (actions.length) {
|
|
188
|
-
buffer.addIndentedBlock('static readonly actions: {',
|
|
211
|
+
buffer.addIndentedBlock('static readonly actions: {',
|
|
189
212
|
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
190
213
|
name: aname,
|
|
191
214
|
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
@@ -196,37 +219,13 @@ class Visitor {
|
|
|
196
219
|
} else {
|
|
197
220
|
buffer.add(`static readonly actions: ${empty}`)
|
|
198
221
|
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
222
|
+
})
|
|
223
|
+
}, '};') // end of generated class
|
|
224
|
+
}, '}') // end of aspect
|
|
202
225
|
|
|
203
226
|
// CLASS WITH ADDED ASPECTS
|
|
204
227
|
file.addImport(baseDefinitions.path)
|
|
205
|
-
|
|
206
|
-
.map(parent => {
|
|
207
|
-
const { namespace, name } = this.resolver.untangle(parent)
|
|
208
|
-
file.addImport(namespace)
|
|
209
|
-
return [namespace, name, parent]
|
|
210
|
-
})
|
|
211
|
-
.concat([[undefined, clean, name]]) // add own aspect without namespace AFTER imports were created
|
|
212
|
-
//.concat([[undefined, clean, [namespace, clean].filter(Boolean).join('.')]]) // add own aspect without namespace AFTER imports were created
|
|
213
|
-
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
214
|
-
.reduce((wrapped, [ns, n, fq]) => {
|
|
215
|
-
// types are not inflected, so don't change those to singular
|
|
216
|
-
const refersToType = isType(this.csn.inferred.definitions[fq])
|
|
217
|
-
const ident = identAspect(refersToType
|
|
218
|
-
? n
|
|
219
|
-
: this.resolver.inflect({csn: this.csn.inferred.definitions[fq], plainName: n}).singular
|
|
220
|
-
)
|
|
221
|
-
return !ns || ns.isCwd(file.path.asDirectory())
|
|
222
|
-
? `${ident}(${wrapped})`
|
|
223
|
-
: `${ns.asIdentifier()}.${ident}(${wrapped})`
|
|
224
|
-
},
|
|
225
|
-
`${baseDefinitions.path.asIdentifier()}.Entity`
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
buffer.add(`export class ${identSingular(clean)} extends ${ancestors} {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
229
|
-
//buffer.add(`export type ${clean} = InstanceType<typeof ${identSingular(clean)}>`)
|
|
228
|
+
buffer.add(`export class ${identSingular(clean)} extends ${toAspectIdent(`${baseDefinitions.path.asIdentifier()}.Entity`, [undefined, clean, fq])} {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
230
229
|
this.contexts.pop()
|
|
231
230
|
}
|
|
232
231
|
|
|
@@ -234,37 +233,30 @@ class Visitor {
|
|
|
234
233
|
return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
235
234
|
}
|
|
236
235
|
|
|
237
|
-
#printEntity(
|
|
236
|
+
#printEntity(fq, entity) {
|
|
238
237
|
// static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
|
|
239
238
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
240
|
-
const { namespace: ns,
|
|
239
|
+
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFq(fq)
|
|
241
240
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
242
|
-
|
|
243
|
-
// If the user decides to pass a @plural annotation, that gets precedence over the regular name.
|
|
244
|
-
|
|
245
|
-
/*
|
|
246
|
-
let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name)
|
|
247
|
-
const singular = this.resolver.trimNamespace(util.singular4(entity, true))
|
|
248
|
-
*/
|
|
249
|
-
let { singular, plural } = this.resolver.inflect({csn: entity, plainName: clean}, ns.asNamespace())
|
|
241
|
+
let { singular, plural } = inflection
|
|
250
242
|
|
|
251
243
|
// trimNamespace does not properly detect scoped entities, like A.B where both A and B are
|
|
252
244
|
// entities. So to see if we would run into a naming collision, we forcefully take the last
|
|
253
245
|
// part of the name, so "A.B" and "A.Bs" just become "B" and "Bs" to be compared.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.logger.warning(
|
|
246
|
+
if (last(plural) === `${last(singular)}_`) {
|
|
247
|
+
LOG.warn(
|
|
257
248
|
`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.`
|
|
258
249
|
)
|
|
259
250
|
}
|
|
251
|
+
|
|
260
252
|
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
|
|
261
253
|
if (!isType(entity) && `${ns.asNamespace()}.${singular}` in this.csn.xtended.definitions) {
|
|
262
|
-
|
|
263
|
-
`Derived singular '${singular}' for your entity '${
|
|
254
|
+
LOG.error(
|
|
255
|
+
`Derived singular '${singular}' for your entity '${fq}', 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.`
|
|
264
256
|
)
|
|
265
257
|
}
|
|
266
|
-
file.addClass(singular,
|
|
267
|
-
file.addClass(plural,
|
|
258
|
+
file.addClass(singular, fq)
|
|
259
|
+
file.addClass(plural, fq)
|
|
268
260
|
|
|
269
261
|
const parent = this.resolver.resolveParent(entity.name)
|
|
270
262
|
const buffer = parent && parent.kind === 'entity'
|
|
@@ -282,13 +274,13 @@ class Visitor {
|
|
|
282
274
|
|
|
283
275
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
284
276
|
const target = isProjection(entity) || isView(entity)
|
|
285
|
-
? this.csn.inferred.definitions[
|
|
277
|
+
? this.csn.inferred.definitions[fq]
|
|
286
278
|
: entity
|
|
287
|
-
|
|
279
|
+
|
|
288
280
|
// 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
|
|
289
281
|
target['@odata.draft.enabled'] = isDraftEnabled(entity)
|
|
290
282
|
|
|
291
|
-
this.#aspectify(
|
|
283
|
+
this.#aspectify(fq, target, buffer, { cleanName: singular })
|
|
292
284
|
|
|
293
285
|
buffer.add(overrideNameProperty(singular, entity.name))
|
|
294
286
|
buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
|
|
@@ -299,7 +291,7 @@ class Visitor {
|
|
|
299
291
|
if (!isType(entity)) {
|
|
300
292
|
if (plural.includes('.')) {
|
|
301
293
|
// Foo.text -> namespace Foo { class text { ... }}
|
|
302
|
-
plural = plural
|
|
294
|
+
plural = last(plural)
|
|
303
295
|
}
|
|
304
296
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
305
297
|
// so it can get passed as value to CQL functions.
|
|
@@ -343,14 +335,14 @@ class Visitor {
|
|
|
343
335
|
}
|
|
344
336
|
|
|
345
337
|
/**
|
|
346
|
-
* @param {string} name
|
|
347
|
-
* @param {object} operation
|
|
348
|
-
* @param {'function' | 'action'} kind
|
|
338
|
+
* @param {string} fq - fully qualified name of the operation
|
|
339
|
+
* @param {object} operation - CSN
|
|
340
|
+
* @param {'function' | 'action'} kind - kind of operation
|
|
349
341
|
*/
|
|
350
|
-
#printOperation(
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
342
|
+
#printOperation(fq, operation, kind) {
|
|
343
|
+
LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
|
|
344
|
+
const { namespace } = this.entityRepository.getByFq(fq)
|
|
345
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
354
346
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
355
347
|
const returnType = operation.returns
|
|
356
348
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
@@ -359,59 +351,59 @@ class Visitor {
|
|
|
359
351
|
returnType,
|
|
360
352
|
returnType.typeInfo.isArray ? returnType.typeName : returnType.typeInfo.inflection.singular
|
|
361
353
|
)
|
|
362
|
-
file.addOperation(
|
|
354
|
+
file.addOperation(last(fq), params, returns, kind)
|
|
363
355
|
}
|
|
364
356
|
|
|
365
|
-
#printType(
|
|
366
|
-
|
|
367
|
-
const { namespace
|
|
368
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
357
|
+
#printType(fq, type) {
|
|
358
|
+
LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
|
|
359
|
+
const { namespace, entityName } = this.entityRepository.getByFq(fq)
|
|
360
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
369
361
|
// skip references to enums.
|
|
370
362
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
371
363
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
372
364
|
if ('enum' in type && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
373
|
-
file.addEnum(
|
|
365
|
+
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
374
366
|
} else {
|
|
375
367
|
// alias
|
|
376
|
-
file.addType(
|
|
368
|
+
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
|
|
377
369
|
}
|
|
378
370
|
// TODO: annotations not handled yet
|
|
379
371
|
}
|
|
380
372
|
|
|
381
|
-
#printAspect(
|
|
382
|
-
|
|
383
|
-
const { namespace
|
|
384
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
373
|
+
#printAspect(fq, aspect) {
|
|
374
|
+
LOG.debug(`Printing aspect ${fq}`)
|
|
375
|
+
const { namespace, entityName } = this.entityRepository.getByFq(fq)
|
|
376
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
385
377
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
386
378
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
387
379
|
// So we separate them into another buffer which is printed before the classes.
|
|
388
|
-
file.addClass(
|
|
389
|
-
file.aspects.add(`// the following represents the CDS aspect '${
|
|
390
|
-
this.#aspectify(
|
|
380
|
+
file.addClass(entityName, fq)
|
|
381
|
+
file.aspects.add(`// the following represents the CDS aspect '${entityName}'`)
|
|
382
|
+
this.#aspectify(fq, aspect, file.aspects, { cleanName: entityName })
|
|
391
383
|
}
|
|
392
384
|
|
|
393
|
-
#printEvent(
|
|
394
|
-
|
|
395
|
-
const { namespace
|
|
396
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
397
|
-
file.addEvent(
|
|
385
|
+
#printEvent(fq, event) {
|
|
386
|
+
LOG.debug(`Printing event ${fq}`)
|
|
387
|
+
const { namespace, entityName } = this.entityRepository.getByFq(fq)
|
|
388
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
389
|
+
file.addEvent(entityName, fq)
|
|
398
390
|
const buffer = file.events.buffer
|
|
399
391
|
buffer.add('// event')
|
|
400
392
|
// only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
|
|
401
|
-
buffer.addIndentedBlock(`export declare class ${
|
|
393
|
+
buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
|
|
402
394
|
const propOpt = this.options.propertiesOptional
|
|
403
395
|
this.options.propertiesOptional = false
|
|
404
396
|
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
405
397
|
this.visitElement(ename, element, file, buffer)
|
|
406
398
|
}
|
|
407
399
|
this.options.propertiesOptional = propOpt
|
|
408
|
-
}
|
|
400
|
+
}, '}')
|
|
409
401
|
}
|
|
410
402
|
|
|
411
|
-
#printService(
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
403
|
+
#printService(fq, service) {
|
|
404
|
+
LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
|
|
405
|
+
const { namespace } = this.entityRepository.getByFq(fq)
|
|
406
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
415
407
|
// service.name is clean of namespace
|
|
416
408
|
file.services.buffer.add(`export default { name: '${service.name}' }`)
|
|
417
409
|
file.addService(service.name)
|
|
@@ -420,36 +412,36 @@ class Visitor {
|
|
|
420
412
|
/**
|
|
421
413
|
* Visits a single entity from the CSN's definition field.
|
|
422
414
|
* Will call #printEntity or #printAction based on the entity's kind.
|
|
423
|
-
* @param {string}
|
|
415
|
+
* @param {string} fq - name of the entity, fully qualified as is used in the definition field.
|
|
424
416
|
* @param {CSN} entity - CSN data belonging to the entity to perform lookups in.
|
|
425
417
|
*/
|
|
426
|
-
visitEntity(
|
|
418
|
+
visitEntity(fq, entity) {
|
|
427
419
|
switch (entity.kind) {
|
|
428
420
|
case 'entity':
|
|
429
|
-
this.#printEntity(
|
|
421
|
+
this.#printEntity(fq, entity)
|
|
430
422
|
break
|
|
431
423
|
case 'action':
|
|
432
424
|
case 'function':
|
|
433
|
-
this.#printOperation(
|
|
425
|
+
this.#printOperation(fq, entity, entity.kind)
|
|
434
426
|
break
|
|
435
427
|
case 'aspect':
|
|
436
|
-
this.#printAspect(
|
|
428
|
+
this.#printAspect(fq, entity)
|
|
437
429
|
break
|
|
438
430
|
case 'type': {
|
|
439
431
|
// types like inline definitions can be used very similarly to entities.
|
|
440
432
|
// They can be extended, contain inline enums, etc., so we treat them as entities.
|
|
441
433
|
const handler = entity.elements ? this.#printEntity : this.#printType
|
|
442
|
-
handler.call(this,
|
|
434
|
+
handler.call(this, fq, entity)
|
|
443
435
|
break
|
|
444
436
|
}
|
|
445
437
|
case 'event':
|
|
446
|
-
this.#printEvent(
|
|
438
|
+
this.#printEvent(fq, entity)
|
|
447
439
|
break
|
|
448
440
|
case 'service':
|
|
449
|
-
this.#printService(
|
|
441
|
+
this.#printService(fq, entity)
|
|
450
442
|
break
|
|
451
443
|
default:
|
|
452
|
-
|
|
444
|
+
LOG.debug(`Unhandled entity kind '${entity.kind}'.`)
|
|
453
445
|
}
|
|
454
446
|
}
|
|
455
447
|
|
|
@@ -459,7 +451,7 @@ class Visitor {
|
|
|
459
451
|
* refer to types via their alias that hides the aspectification.
|
|
460
452
|
* If we attempt to directly refer to this alias while it has not been fully created,
|
|
461
453
|
* that will result in a TS error.
|
|
462
|
-
* @param {string}
|
|
454
|
+
* @param {string} fq - fully qualified name of the entity
|
|
463
455
|
* @returns {boolean} true, if `entityName` refers to the surrounding class
|
|
464
456
|
* @example
|
|
465
457
|
* ```ts
|
|
@@ -469,14 +461,14 @@ class Visitor {
|
|
|
469
461
|
* }
|
|
470
462
|
* ```
|
|
471
463
|
*/
|
|
472
|
-
isSelfReference(
|
|
473
|
-
return
|
|
464
|
+
isSelfReference(fq) {
|
|
465
|
+
return fq === this.contexts.at(-1)?.entity
|
|
474
466
|
}
|
|
475
467
|
|
|
476
468
|
/**
|
|
477
469
|
* Visits a single element in an entity.
|
|
478
470
|
* @param {string} name - name of the element
|
|
479
|
-
* @param {import('./
|
|
471
|
+
* @param {import('./resolution/resolver').CSN} element - CSN data belonging to the the element.
|
|
480
472
|
* @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
|
|
481
473
|
* @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
482
474
|
* @returns @see InlineDeclarationResolver.visitElement
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test:all": "jest",
|
|
20
20
|
"test": "npm run test:smoke && npm run test:unit",
|
|
21
21
|
"lint": "npx eslint .",
|
|
22
|
+
"lint:fix": "npx eslint . --fix",
|
|
22
23
|
"cli": "node lib/cli.js",
|
|
23
24
|
"doc:clean": "rm -rf ./doc",
|
|
24
25
|
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|