@cap-js/cds-typer 0.21.2 → 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 +13 -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 +118 -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)
|
|
@@ -157,13 +180,15 @@ class Visitor {
|
|
|
157
180
|
// lookup in cds.definitions can fail for inline structs.
|
|
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
|
-
for (const [kname,
|
|
161
|
-
if (
|
|
183
|
+
for (const [kname, originalKeyElement] of this.#keys(element.target)) {
|
|
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
|
-
kelement
|
|
189
|
+
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
190
|
+
isRefNotNull: !!element.notNull || !!element.key
|
|
191
|
+
})
|
|
167
192
|
this.visitElement(foreignKey, kelement, file, buffer)
|
|
168
193
|
}
|
|
169
194
|
}
|
|
@@ -176,14 +201,14 @@ class Visitor {
|
|
|
176
201
|
}
|
|
177
202
|
}
|
|
178
203
|
|
|
179
|
-
buffer.addIndented(
|
|
204
|
+
buffer.addIndented(() => {
|
|
180
205
|
for (const e of enums) {
|
|
181
206
|
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
182
|
-
file.addInlineEnum(clean,
|
|
207
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
183
208
|
}
|
|
184
209
|
const actions = Object.entries(entity.actions ?? {})
|
|
185
210
|
if (actions.length) {
|
|
186
|
-
buffer.addIndentedBlock('static readonly actions: {',
|
|
211
|
+
buffer.addIndentedBlock('static readonly actions: {',
|
|
187
212
|
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
188
213
|
name: aname,
|
|
189
214
|
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
@@ -194,37 +219,13 @@ class Visitor {
|
|
|
194
219
|
} else {
|
|
195
220
|
buffer.add(`static readonly actions: ${empty}`)
|
|
196
221
|
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
222
|
+
})
|
|
223
|
+
}, '};') // end of generated class
|
|
224
|
+
}, '}') // end of aspect
|
|
200
225
|
|
|
201
226
|
// CLASS WITH ADDED ASPECTS
|
|
202
227
|
file.addImport(baseDefinitions.path)
|
|
203
|
-
|
|
204
|
-
.map(parent => {
|
|
205
|
-
const { namespace, name } = this.resolver.untangle(parent)
|
|
206
|
-
file.addImport(namespace)
|
|
207
|
-
return [namespace, name, parent]
|
|
208
|
-
})
|
|
209
|
-
.concat([[undefined, clean, name]]) // add own aspect without namespace AFTER imports were created
|
|
210
|
-
//.concat([[undefined, clean, [namespace, clean].filter(Boolean).join('.')]]) // add own aspect without namespace AFTER imports were created
|
|
211
|
-
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
212
|
-
.reduce((wrapped, [ns, n, fq]) => {
|
|
213
|
-
// types are not inflected, so don't change those to singular
|
|
214
|
-
const refersToType = isType(this.csn.inferred.definitions[fq])
|
|
215
|
-
const ident = identAspect(refersToType
|
|
216
|
-
? n
|
|
217
|
-
: this.resolver.inflect({csn: this.csn.inferred.definitions[fq], plainName: n}).singular
|
|
218
|
-
)
|
|
219
|
-
return !ns || ns.isCwd(file.path.asDirectory())
|
|
220
|
-
? `${ident}(${wrapped})`
|
|
221
|
-
: `${ns.asIdentifier()}.${ident}(${wrapped})`
|
|
222
|
-
},
|
|
223
|
-
`${baseDefinitions.path.asIdentifier()}.Entity`
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
buffer.add(`export class ${identSingular(clean)} extends ${ancestors} {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
227
|
-
//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')}}`)
|
|
228
229
|
this.contexts.pop()
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -232,37 +233,30 @@ class Visitor {
|
|
|
232
233
|
return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
#printEntity(
|
|
236
|
+
#printEntity(fq, entity) {
|
|
236
237
|
// static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
|
|
237
238
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
238
|
-
const { namespace: ns,
|
|
239
|
+
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFq(fq)
|
|
239
240
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
240
|
-
|
|
241
|
-
// If the user decides to pass a @plural annotation, that gets precedence over the regular name.
|
|
242
|
-
|
|
243
|
-
/*
|
|
244
|
-
let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name)
|
|
245
|
-
const singular = this.resolver.trimNamespace(util.singular4(entity, true))
|
|
246
|
-
*/
|
|
247
|
-
let { singular, plural } = this.resolver.inflect({csn: entity, plainName: clean}, ns.asNamespace())
|
|
241
|
+
let { singular, plural } = inflection
|
|
248
242
|
|
|
249
243
|
// trimNamespace does not properly detect scoped entities, like A.B where both A and B are
|
|
250
244
|
// entities. So to see if we would run into a naming collision, we forcefully take the last
|
|
251
245
|
// part of the name, so "A.B" and "A.Bs" just become "B" and "Bs" to be compared.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
this.logger.warning(
|
|
246
|
+
if (last(plural) === `${last(singular)}_`) {
|
|
247
|
+
LOG.warn(
|
|
255
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.`
|
|
256
249
|
)
|
|
257
250
|
}
|
|
251
|
+
|
|
258
252
|
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
|
|
259
253
|
if (!isType(entity) && `${ns.asNamespace()}.${singular}` in this.csn.xtended.definitions) {
|
|
260
|
-
|
|
261
|
-
`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.`
|
|
262
256
|
)
|
|
263
257
|
}
|
|
264
|
-
file.addClass(singular,
|
|
265
|
-
file.addClass(plural,
|
|
258
|
+
file.addClass(singular, fq)
|
|
259
|
+
file.addClass(plural, fq)
|
|
266
260
|
|
|
267
261
|
const parent = this.resolver.resolveParent(entity.name)
|
|
268
262
|
const buffer = parent && parent.kind === 'entity'
|
|
@@ -280,13 +274,13 @@ class Visitor {
|
|
|
280
274
|
|
|
281
275
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
282
276
|
const target = isProjection(entity) || isView(entity)
|
|
283
|
-
? this.csn.inferred.definitions[
|
|
277
|
+
? this.csn.inferred.definitions[fq]
|
|
284
278
|
: entity
|
|
285
|
-
|
|
279
|
+
|
|
286
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
|
|
287
281
|
target['@odata.draft.enabled'] = isDraftEnabled(entity)
|
|
288
282
|
|
|
289
|
-
this.#aspectify(
|
|
283
|
+
this.#aspectify(fq, target, buffer, { cleanName: singular })
|
|
290
284
|
|
|
291
285
|
buffer.add(overrideNameProperty(singular, entity.name))
|
|
292
286
|
buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
|
|
@@ -297,7 +291,7 @@ class Visitor {
|
|
|
297
291
|
if (!isType(entity)) {
|
|
298
292
|
if (plural.includes('.')) {
|
|
299
293
|
// Foo.text -> namespace Foo { class text { ... }}
|
|
300
|
-
plural = plural
|
|
294
|
+
plural = last(plural)
|
|
301
295
|
}
|
|
302
296
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
303
297
|
// so it can get passed as value to CQL functions.
|
|
@@ -341,14 +335,14 @@ class Visitor {
|
|
|
341
335
|
}
|
|
342
336
|
|
|
343
337
|
/**
|
|
344
|
-
* @param {string} name
|
|
345
|
-
* @param {object} operation
|
|
346
|
-
* @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
|
|
347
341
|
*/
|
|
348
|
-
#printOperation(
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
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)
|
|
352
346
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
353
347
|
const returnType = operation.returns
|
|
354
348
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
@@ -357,59 +351,59 @@ class Visitor {
|
|
|
357
351
|
returnType,
|
|
358
352
|
returnType.typeInfo.isArray ? returnType.typeName : returnType.typeInfo.inflection.singular
|
|
359
353
|
)
|
|
360
|
-
file.addOperation(
|
|
354
|
+
file.addOperation(last(fq), params, returns, kind)
|
|
361
355
|
}
|
|
362
356
|
|
|
363
|
-
#printType(
|
|
364
|
-
|
|
365
|
-
const { namespace
|
|
366
|
-
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)
|
|
367
361
|
// skip references to enums.
|
|
368
362
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
369
363
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
370
364
|
if ('enum' in type && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
371
|
-
file.addEnum(
|
|
365
|
+
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
372
366
|
} else {
|
|
373
367
|
// alias
|
|
374
|
-
file.addType(
|
|
368
|
+
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
|
|
375
369
|
}
|
|
376
370
|
// TODO: annotations not handled yet
|
|
377
371
|
}
|
|
378
372
|
|
|
379
|
-
#printAspect(
|
|
380
|
-
|
|
381
|
-
const { namespace
|
|
382
|
-
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)
|
|
383
377
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
384
378
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
385
379
|
// So we separate them into another buffer which is printed before the classes.
|
|
386
|
-
file.addClass(
|
|
387
|
-
file.aspects.add(`// the following represents the CDS aspect '${
|
|
388
|
-
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 })
|
|
389
383
|
}
|
|
390
384
|
|
|
391
|
-
#printEvent(
|
|
392
|
-
|
|
393
|
-
const { namespace
|
|
394
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
395
|
-
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)
|
|
396
390
|
const buffer = file.events.buffer
|
|
397
391
|
buffer.add('// event')
|
|
398
392
|
// only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
|
|
399
|
-
buffer.addIndentedBlock(`export declare class ${
|
|
393
|
+
buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
|
|
400
394
|
const propOpt = this.options.propertiesOptional
|
|
401
395
|
this.options.propertiesOptional = false
|
|
402
396
|
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
403
397
|
this.visitElement(ename, element, file, buffer)
|
|
404
398
|
}
|
|
405
399
|
this.options.propertiesOptional = propOpt
|
|
406
|
-
}
|
|
400
|
+
}, '}')
|
|
407
401
|
}
|
|
408
402
|
|
|
409
|
-
#printService(
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
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)
|
|
413
407
|
// service.name is clean of namespace
|
|
414
408
|
file.services.buffer.add(`export default { name: '${service.name}' }`)
|
|
415
409
|
file.addService(service.name)
|
|
@@ -418,36 +412,36 @@ class Visitor {
|
|
|
418
412
|
/**
|
|
419
413
|
* Visits a single entity from the CSN's definition field.
|
|
420
414
|
* Will call #printEntity or #printAction based on the entity's kind.
|
|
421
|
-
* @param {string}
|
|
415
|
+
* @param {string} fq - name of the entity, fully qualified as is used in the definition field.
|
|
422
416
|
* @param {CSN} entity - CSN data belonging to the entity to perform lookups in.
|
|
423
417
|
*/
|
|
424
|
-
visitEntity(
|
|
418
|
+
visitEntity(fq, entity) {
|
|
425
419
|
switch (entity.kind) {
|
|
426
420
|
case 'entity':
|
|
427
|
-
this.#printEntity(
|
|
421
|
+
this.#printEntity(fq, entity)
|
|
428
422
|
break
|
|
429
423
|
case 'action':
|
|
430
424
|
case 'function':
|
|
431
|
-
this.#printOperation(
|
|
425
|
+
this.#printOperation(fq, entity, entity.kind)
|
|
432
426
|
break
|
|
433
427
|
case 'aspect':
|
|
434
|
-
this.#printAspect(
|
|
428
|
+
this.#printAspect(fq, entity)
|
|
435
429
|
break
|
|
436
430
|
case 'type': {
|
|
437
431
|
// types like inline definitions can be used very similarly to entities.
|
|
438
432
|
// They can be extended, contain inline enums, etc., so we treat them as entities.
|
|
439
433
|
const handler = entity.elements ? this.#printEntity : this.#printType
|
|
440
|
-
handler.call(this,
|
|
434
|
+
handler.call(this, fq, entity)
|
|
441
435
|
break
|
|
442
436
|
}
|
|
443
437
|
case 'event':
|
|
444
|
-
this.#printEvent(
|
|
438
|
+
this.#printEvent(fq, entity)
|
|
445
439
|
break
|
|
446
440
|
case 'service':
|
|
447
|
-
this.#printService(
|
|
441
|
+
this.#printService(fq, entity)
|
|
448
442
|
break
|
|
449
443
|
default:
|
|
450
|
-
|
|
444
|
+
LOG.debug(`Unhandled entity kind '${entity.kind}'.`)
|
|
451
445
|
}
|
|
452
446
|
}
|
|
453
447
|
|
|
@@ -457,7 +451,7 @@ class Visitor {
|
|
|
457
451
|
* refer to types via their alias that hides the aspectification.
|
|
458
452
|
* If we attempt to directly refer to this alias while it has not been fully created,
|
|
459
453
|
* that will result in a TS error.
|
|
460
|
-
* @param {string}
|
|
454
|
+
* @param {string} fq - fully qualified name of the entity
|
|
461
455
|
* @returns {boolean} true, if `entityName` refers to the surrounding class
|
|
462
456
|
* @example
|
|
463
457
|
* ```ts
|
|
@@ -467,14 +461,14 @@ class Visitor {
|
|
|
467
461
|
* }
|
|
468
462
|
* ```
|
|
469
463
|
*/
|
|
470
|
-
isSelfReference(
|
|
471
|
-
return
|
|
464
|
+
isSelfReference(fq) {
|
|
465
|
+
return fq === this.contexts.at(-1)?.entity
|
|
472
466
|
}
|
|
473
467
|
|
|
474
468
|
/**
|
|
475
469
|
* Visits a single element in an entity.
|
|
476
470
|
* @param {string} name - name of the element
|
|
477
|
-
* @param {import('./
|
|
471
|
+
* @param {import('./resolution/resolver').CSN} element - CSN data belonging to the the element.
|
|
478
472
|
* @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
|
|
479
473
|
* @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
480
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",
|