@cap-js/cds-typer 0.22.0 → 0.24.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 +18 -1
- package/cds-plugin.js +12 -7
- 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 +119 -125
- 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 ${clean} extends
|
|
168
|
+
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
|
|
169
|
+
buffer.addIndentedBlock(`return class ${clean} 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,32 @@ 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
|
-
if
|
|
262
|
-
|
|
263
|
-
|
|
253
|
+
// if the user defined their entities in singular form we would also have a false positive here -> skip
|
|
254
|
+
const namespacedSingular = `${ns.asNamespace()}.${singular}`
|
|
255
|
+
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.xtended.definitions) {
|
|
256
|
+
LOG.error(
|
|
257
|
+
`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
258
|
)
|
|
265
259
|
}
|
|
266
|
-
file.addClass(singular,
|
|
267
|
-
file.addClass(plural,
|
|
260
|
+
file.addClass(singular, fq)
|
|
261
|
+
file.addClass(plural, fq)
|
|
268
262
|
|
|
269
263
|
const parent = this.resolver.resolveParent(entity.name)
|
|
270
264
|
const buffer = parent && parent.kind === 'entity'
|
|
@@ -282,13 +276,13 @@ class Visitor {
|
|
|
282
276
|
|
|
283
277
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
284
278
|
const target = isProjection(entity) || isView(entity)
|
|
285
|
-
? this.csn.inferred.definitions[
|
|
279
|
+
? this.csn.inferred.definitions[fq]
|
|
286
280
|
: entity
|
|
287
|
-
|
|
281
|
+
|
|
288
282
|
// 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
283
|
target['@odata.draft.enabled'] = isDraftEnabled(entity)
|
|
290
284
|
|
|
291
|
-
this.#aspectify(
|
|
285
|
+
this.#aspectify(fq, target, buffer, { cleanName: singular })
|
|
292
286
|
|
|
293
287
|
buffer.add(overrideNameProperty(singular, entity.name))
|
|
294
288
|
buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
|
|
@@ -299,7 +293,7 @@ class Visitor {
|
|
|
299
293
|
if (!isType(entity)) {
|
|
300
294
|
if (plural.includes('.')) {
|
|
301
295
|
// Foo.text -> namespace Foo { class text { ... }}
|
|
302
|
-
plural = plural
|
|
296
|
+
plural = last(plural)
|
|
303
297
|
}
|
|
304
298
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
305
299
|
// so it can get passed as value to CQL functions.
|
|
@@ -343,14 +337,14 @@ class Visitor {
|
|
|
343
337
|
}
|
|
344
338
|
|
|
345
339
|
/**
|
|
346
|
-
* @param {string} name
|
|
347
|
-
* @param {object} operation
|
|
348
|
-
* @param {'function' | 'action'} kind
|
|
340
|
+
* @param {string} fq - fully qualified name of the operation
|
|
341
|
+
* @param {object} operation - CSN
|
|
342
|
+
* @param {'function' | 'action'} kind - kind of operation
|
|
349
343
|
*/
|
|
350
|
-
#printOperation(
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
344
|
+
#printOperation(fq, operation, kind) {
|
|
345
|
+
LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
|
|
346
|
+
const { namespace } = this.entityRepository.getByFq(fq)
|
|
347
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
354
348
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
355
349
|
const returnType = operation.returns
|
|
356
350
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
@@ -359,59 +353,59 @@ class Visitor {
|
|
|
359
353
|
returnType,
|
|
360
354
|
returnType.typeInfo.isArray ? returnType.typeName : returnType.typeInfo.inflection.singular
|
|
361
355
|
)
|
|
362
|
-
file.addOperation(
|
|
356
|
+
file.addOperation(last(fq), params, returns, kind)
|
|
363
357
|
}
|
|
364
358
|
|
|
365
|
-
#printType(
|
|
366
|
-
|
|
367
|
-
const { namespace
|
|
368
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
359
|
+
#printType(fq, type) {
|
|
360
|
+
LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
|
|
361
|
+
const { namespace, entityName } = this.entityRepository.getByFq(fq)
|
|
362
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
369
363
|
// skip references to enums.
|
|
370
364
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
371
365
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
372
366
|
if ('enum' in type && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
373
|
-
file.addEnum(
|
|
367
|
+
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
374
368
|
} else {
|
|
375
369
|
// alias
|
|
376
|
-
file.addType(
|
|
370
|
+
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
|
|
377
371
|
}
|
|
378
372
|
// TODO: annotations not handled yet
|
|
379
373
|
}
|
|
380
374
|
|
|
381
|
-
#printAspect(
|
|
382
|
-
|
|
383
|
-
const { namespace
|
|
384
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
375
|
+
#printAspect(fq, aspect) {
|
|
376
|
+
LOG.debug(`Printing aspect ${fq}`)
|
|
377
|
+
const { namespace, entityName, inflection } = this.entityRepository.getByFq(fq)
|
|
378
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
385
379
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
386
380
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
387
381
|
// 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(
|
|
382
|
+
file.addClass(entityName, fq)
|
|
383
|
+
file.aspects.add(`// the following represents the CDS aspect '${entityName}'`)
|
|
384
|
+
this.#aspectify(fq, aspect, file.aspects, { cleanName: inflection.singular })
|
|
391
385
|
}
|
|
392
386
|
|
|
393
|
-
#printEvent(
|
|
394
|
-
|
|
395
|
-
const { namespace
|
|
396
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
397
|
-
file.addEvent(
|
|
387
|
+
#printEvent(fq, event) {
|
|
388
|
+
LOG.debug(`Printing event ${fq}`)
|
|
389
|
+
const { namespace, entityName } = this.entityRepository.getByFq(fq)
|
|
390
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
391
|
+
file.addEvent(entityName, fq)
|
|
398
392
|
const buffer = file.events.buffer
|
|
399
393
|
buffer.add('// event')
|
|
400
394
|
// 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 ${
|
|
395
|
+
buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
|
|
402
396
|
const propOpt = this.options.propertiesOptional
|
|
403
397
|
this.options.propertiesOptional = false
|
|
404
398
|
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
405
399
|
this.visitElement(ename, element, file, buffer)
|
|
406
400
|
}
|
|
407
401
|
this.options.propertiesOptional = propOpt
|
|
408
|
-
}
|
|
402
|
+
}, '}')
|
|
409
403
|
}
|
|
410
404
|
|
|
411
|
-
#printService(
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
const file = this.fileRepository.getNamespaceFile(
|
|
405
|
+
#printService(fq, service) {
|
|
406
|
+
LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
|
|
407
|
+
const { namespace } = this.entityRepository.getByFq(fq)
|
|
408
|
+
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
415
409
|
// service.name is clean of namespace
|
|
416
410
|
file.services.buffer.add(`export default { name: '${service.name}' }`)
|
|
417
411
|
file.addService(service.name)
|
|
@@ -420,36 +414,36 @@ class Visitor {
|
|
|
420
414
|
/**
|
|
421
415
|
* Visits a single entity from the CSN's definition field.
|
|
422
416
|
* Will call #printEntity or #printAction based on the entity's kind.
|
|
423
|
-
* @param {string}
|
|
417
|
+
* @param {string} fq - name of the entity, fully qualified as is used in the definition field.
|
|
424
418
|
* @param {CSN} entity - CSN data belonging to the entity to perform lookups in.
|
|
425
419
|
*/
|
|
426
|
-
visitEntity(
|
|
420
|
+
visitEntity(fq, entity) {
|
|
427
421
|
switch (entity.kind) {
|
|
428
422
|
case 'entity':
|
|
429
|
-
this.#printEntity(
|
|
423
|
+
this.#printEntity(fq, entity)
|
|
430
424
|
break
|
|
431
425
|
case 'action':
|
|
432
426
|
case 'function':
|
|
433
|
-
this.#printOperation(
|
|
427
|
+
this.#printOperation(fq, entity, entity.kind)
|
|
434
428
|
break
|
|
435
429
|
case 'aspect':
|
|
436
|
-
this.#printAspect(
|
|
430
|
+
this.#printAspect(fq, entity)
|
|
437
431
|
break
|
|
438
432
|
case 'type': {
|
|
439
433
|
// types like inline definitions can be used very similarly to entities.
|
|
440
434
|
// They can be extended, contain inline enums, etc., so we treat them as entities.
|
|
441
435
|
const handler = entity.elements ? this.#printEntity : this.#printType
|
|
442
|
-
handler.call(this,
|
|
436
|
+
handler.call(this, fq, entity)
|
|
443
437
|
break
|
|
444
438
|
}
|
|
445
439
|
case 'event':
|
|
446
|
-
this.#printEvent(
|
|
440
|
+
this.#printEvent(fq, entity)
|
|
447
441
|
break
|
|
448
442
|
case 'service':
|
|
449
|
-
this.#printService(
|
|
443
|
+
this.#printService(fq, entity)
|
|
450
444
|
break
|
|
451
445
|
default:
|
|
452
|
-
|
|
446
|
+
LOG.debug(`Unhandled entity kind '${entity.kind}'.`)
|
|
453
447
|
}
|
|
454
448
|
}
|
|
455
449
|
|
|
@@ -459,7 +453,7 @@ class Visitor {
|
|
|
459
453
|
* refer to types via their alias that hides the aspectification.
|
|
460
454
|
* If we attempt to directly refer to this alias while it has not been fully created,
|
|
461
455
|
* that will result in a TS error.
|
|
462
|
-
* @param {string}
|
|
456
|
+
* @param {string} fq - fully qualified name of the entity
|
|
463
457
|
* @returns {boolean} true, if `entityName` refers to the surrounding class
|
|
464
458
|
* @example
|
|
465
459
|
* ```ts
|
|
@@ -469,14 +463,14 @@ class Visitor {
|
|
|
469
463
|
* }
|
|
470
464
|
* ```
|
|
471
465
|
*/
|
|
472
|
-
isSelfReference(
|
|
473
|
-
return
|
|
466
|
+
isSelfReference(fq) {
|
|
467
|
+
return fq === this.contexts.at(-1)?.entity
|
|
474
468
|
}
|
|
475
469
|
|
|
476
470
|
/**
|
|
477
471
|
* Visits a single element in an entity.
|
|
478
472
|
* @param {string} name - name of the element
|
|
479
|
-
* @param {import('./
|
|
473
|
+
* @param {import('./resolution/resolver').CSN} element - CSN data belonging to the the element.
|
|
480
474
|
* @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
|
|
481
475
|
* @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
482
476
|
* @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.24.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",
|