@cap-js/cds-typer 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/cds-plugin.js +8 -3
- package/lib/cli.js +24 -14
- package/lib/compile.js +3 -3
- package/lib/components/enum.js +16 -9
- package/lib/components/identifier.js +1 -1
- package/lib/components/inline.js +81 -26
- package/lib/components/property.js +12 -0
- package/lib/components/wrappers.js +1 -1
- package/lib/csn.js +90 -26
- package/lib/file.js +43 -19
- package/lib/logging.js +5 -1
- package/lib/resolution/builtin.js +3 -2
- package/lib/resolution/entity.js +46 -7
- package/lib/resolution/resolver.js +51 -20
- package/lib/typedefs.d.ts +67 -13
- package/lib/util.js +4 -3
- package/lib/visitor.js +167 -61
- package/package.json +4 -2
package/lib/visitor.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality } = require('./csn')
|
|
5
|
+
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
|
-
const { SourceFile, FileRepository, Buffer } = require('./file')
|
|
7
|
+
const { SourceFile, FileRepository, Buffer, Path } = require('./file')
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
9
|
const { Resolver } = require('./resolution/resolver')
|
|
10
10
|
const { LOG } = require('./logging')
|
|
@@ -13,14 +13,18 @@ const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnu
|
|
|
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')
|
|
16
|
+
const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
17
17
|
const { last } = require('./components/identifier')
|
|
18
|
+
const { getPropertyModifiers } = require('./components/property')
|
|
18
19
|
|
|
19
20
|
/** @typedef {import('./file').File} File */
|
|
20
21
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
21
22
|
/** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
|
|
22
23
|
/** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
|
|
23
24
|
/** @typedef {import('./typedefs').visitor.Inflection} Inflection */
|
|
25
|
+
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
26
|
+
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
27
|
+
/** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
|
|
24
28
|
|
|
25
29
|
const defaults = {
|
|
26
30
|
// FIXME: add defaults for remaining parameters
|
|
@@ -36,12 +40,12 @@ class Visitor {
|
|
|
36
40
|
* @returns {File[]} a full list of files to be written
|
|
37
41
|
*/
|
|
38
42
|
getWriteoutFiles() {
|
|
39
|
-
return this.fileRepository.getFiles()
|
|
43
|
+
return [...this.fileRepository.getFiles(), ...this.resolver.getUsedLibraries()]
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
44
|
-
* @param {VisitorOptions} options - the options
|
|
48
|
+
* @param {VisitorOptions | {}} options - the options
|
|
45
49
|
*/
|
|
46
50
|
constructor(csn, options = {}) {
|
|
47
51
|
amendCSN(csn.xtended)
|
|
@@ -124,6 +128,34 @@ class Visitor {
|
|
|
124
128
|
})
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @param {EntityCSN} entity - the entity to print the actions for
|
|
133
|
+
* @param {Buffer} buffer - the buffer to write the actions into
|
|
134
|
+
* @param {import('./typedefs').resolver.EntityInfo[]} ancestors - the fully qualified names of the ancestors of the entity
|
|
135
|
+
* @param {SourceFile} file - the file the entity is being printed into
|
|
136
|
+
*/
|
|
137
|
+
#printStaticActions(entity, buffer, ancestors, file) {
|
|
138
|
+
// TODO: refactor away! All these printing functionalities need to go
|
|
139
|
+
const actions = Object.entries(entity.actions ?? {})
|
|
140
|
+
const inherited = ancestors.length
|
|
141
|
+
? ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`).join(' & ') + ' & '
|
|
142
|
+
: ''
|
|
143
|
+
if (actions.length) {
|
|
144
|
+
buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
|
|
145
|
+
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
146
|
+
name: aname,
|
|
147
|
+
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
148
|
+
returns: action.returns
|
|
149
|
+
? this.resolver.resolveAndRequire(action.returns, file).typeName
|
|
150
|
+
: 'any',
|
|
151
|
+
kind: action.kind
|
|
152
|
+
})), '}'
|
|
153
|
+
) // end of actions
|
|
154
|
+
} else {
|
|
155
|
+
buffer.add(`declare static readonly actions: ${inherited}${empty}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
/**
|
|
128
160
|
* Transforms an entity or CDS aspect into a JS aspect (aka mixin).
|
|
129
161
|
* That is, for an element A we get:
|
|
@@ -131,42 +163,66 @@ class Visitor {
|
|
|
131
163
|
* - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
|
|
132
164
|
* - the type A to use for external typing and is derived from AXtended.
|
|
133
165
|
* @param {string} fq - the name of the entity
|
|
134
|
-
* @param {
|
|
166
|
+
* @param {EntityCSN} entity - the pointer into the CSN to extract the elements from
|
|
135
167
|
* @param {Buffer} buffer - the buffer to write the resulting definitions into
|
|
136
168
|
* @param {{cleanName?: string}} options - additional options
|
|
137
169
|
*/
|
|
138
170
|
#aspectify(fq, entity, buffer, options = {}) {
|
|
139
171
|
const info = this.entityRepository.getByFq(fq)
|
|
172
|
+
if (!info) throw new Error(`could not resolve entity ${fq}`)
|
|
140
173
|
const clean = options?.cleanName ?? info.withoutNamespace
|
|
141
174
|
const { namespace } = info
|
|
142
175
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
143
|
-
const identSingular = name => name
|
|
176
|
+
const identSingular = (/** @type {string} */name) => name // FIXME: remove
|
|
177
|
+
//const identAspect = name => `_${name}Aspect`
|
|
178
|
+
/** @param {string} name - the name */
|
|
144
179
|
const identAspect = name => `_${name}Aspect`
|
|
145
|
-
|
|
180
|
+
/**
|
|
181
|
+
* @param {object} options - options
|
|
182
|
+
* @param {Path} [options.ns] - namespace
|
|
183
|
+
* @param {string} options.clean - the clean name of the entity
|
|
184
|
+
* @param {string} options.fq - fully qualified name
|
|
185
|
+
* @returns {string} the local identifier
|
|
186
|
+
*/
|
|
187
|
+
// FIXME: replace with resolution/entity::asIdentifier
|
|
188
|
+
const toLocalIdent = ({ns, clean, fq}) => {
|
|
146
189
|
// types are not inflected, so don't change those to singular
|
|
147
|
-
const
|
|
148
|
-
const ident =
|
|
149
|
-
?
|
|
150
|
-
: this.resolver.inflect({csn
|
|
151
|
-
)
|
|
190
|
+
const csn = this.csn.inferred.definitions[fq]
|
|
191
|
+
const ident = isType(csn)
|
|
192
|
+
? clean
|
|
193
|
+
: this.resolver.inflect({csn, plainName: clean}).singular
|
|
152
194
|
return !ns || ns.isCwd(file.path.asDirectory())
|
|
153
|
-
?
|
|
154
|
-
: `${ns.asIdentifier()}.${ident}
|
|
195
|
+
? ident
|
|
196
|
+
: `${ns.asIdentifier()}.${ident}`
|
|
155
197
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
198
|
+
// remove the ancestry of projections/ views.
|
|
199
|
+
// They explicitly define their properties.
|
|
200
|
+
// But at the same time they also carry their .includes clause, which can
|
|
201
|
+
// clash when the user aliases a selected property with the name of
|
|
202
|
+
// a properties of the entities they project on:
|
|
203
|
+
// entity foo as SELECT bar.baz AS ID FROM bar
|
|
204
|
+
// will produce an error if bar also has a property ID which now clashes with foo.ID
|
|
205
|
+
// WARNING: annotations from entities without properties should actually be propagated this way!
|
|
206
|
+
// So once we start caring about annotations, we have to revisit this part.
|
|
207
|
+
/** @type {import('./typedefs').resolver.EntityInfo[]} */
|
|
208
|
+
const ancestorInfos = ((!isViewOrProjection(entity) ? entity.includes : []) ?? [])
|
|
209
|
+
.map(ancestor => {
|
|
210
|
+
const info = this.entityRepository.getByFq(ancestor)
|
|
211
|
+
if (!info) throw new Error(`could not resolve ancestor ${ancestor} for ${fq}`)
|
|
212
|
+
file.addImport(info.namespace)
|
|
213
|
+
return info
|
|
161
214
|
})
|
|
215
|
+
|
|
216
|
+
const ancestorsAspects = ancestorInfos
|
|
162
217
|
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
163
|
-
.reduce(
|
|
218
|
+
.reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
|
|
164
219
|
|
|
165
220
|
this.contexts.push({ entity: fq })
|
|
166
221
|
|
|
167
222
|
// CLASS ASPECT
|
|
168
223
|
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
|
|
169
|
-
buffer.addIndentedBlock(`return class extends ${
|
|
224
|
+
buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
|
|
225
|
+
/** @type {import('./typedefs').resolver.EnumCSN[]} */
|
|
170
226
|
const enums = []
|
|
171
227
|
for (let [ename, element] of Object.entries(entity.elements ?? {})) {
|
|
172
228
|
if (element.target && /\.texts?/.test(element.target)) {
|
|
@@ -176,14 +232,14 @@ class Visitor {
|
|
|
176
232
|
this.visitElement(ename, element, file, buffer)
|
|
177
233
|
|
|
178
234
|
// make foreign keys explicit
|
|
179
|
-
if (
|
|
235
|
+
if (element.target) {
|
|
180
236
|
// lookup in cds.definitions can fail for inline structs.
|
|
181
237
|
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
182
238
|
// the containing entity.
|
|
183
239
|
for (const [kname, originalKeyElement] of this.#keys(element.target)) {
|
|
184
240
|
if (getMaxCardinality(element) === 1 && typeof element.on !== 'object') { // FIXME: kelement?
|
|
185
241
|
const foreignKey = `${ename}_${kname}`
|
|
186
|
-
if (Object.hasOwn(entity.elements, foreignKey)) {
|
|
242
|
+
if (entity.elements && Object.hasOwn(entity.elements, foreignKey)) {
|
|
187
243
|
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.`)
|
|
188
244
|
} else {
|
|
189
245
|
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
@@ -206,37 +262,39 @@ class Visitor {
|
|
|
206
262
|
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
207
263
|
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
208
264
|
}
|
|
209
|
-
|
|
210
|
-
if (actions.length) {
|
|
211
|
-
buffer.addIndentedBlock('static readonly actions: {',
|
|
212
|
-
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
213
|
-
name: aname,
|
|
214
|
-
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
215
|
-
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
|
|
216
|
-
kind: action.kind
|
|
217
|
-
}))
|
|
218
|
-
, '}') // end of actions
|
|
219
|
-
} else {
|
|
220
|
-
buffer.add(`static readonly actions: ${empty}`)
|
|
221
|
-
}
|
|
265
|
+
this.#printStaticActions(entity, buffer, ancestorInfos, file)
|
|
222
266
|
})
|
|
223
267
|
}, '};') // end of generated class
|
|
224
268
|
}, '}') // end of aspect
|
|
225
269
|
|
|
226
270
|
// CLASS WITH ADDED ASPECTS
|
|
227
271
|
file.addImport(baseDefinitions.path)
|
|
228
|
-
buffer.add(`export class ${identSingular(clean)} extends ${
|
|
272
|
+
buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
229
273
|
this.contexts.pop()
|
|
230
274
|
}
|
|
231
275
|
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} clean - the clean name of the entity
|
|
278
|
+
* @param {EntityCSN} entity - the entity to generate the static contents for
|
|
279
|
+
*/
|
|
232
280
|
#staticClassContents(clean, entity) {
|
|
233
281
|
return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
234
282
|
}
|
|
235
283
|
|
|
284
|
+
/**
|
|
285
|
+
* @param {string} fq - fully qualified name of the entity
|
|
286
|
+
* @param {EntityCSN} entity - the entity to print
|
|
287
|
+
*/
|
|
236
288
|
#printEntity(fq, entity) {
|
|
237
289
|
// static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
|
|
290
|
+
/**
|
|
291
|
+
* @param {string} clazz - the class to override the name property for
|
|
292
|
+
* @param {string} content - the content to set the name property to
|
|
293
|
+
*/
|
|
238
294
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
239
|
-
const
|
|
295
|
+
const info = this.entityRepository.getByFq(fq)
|
|
296
|
+
if (!info) throw new Error(`could not resolve entity ${fq}`)
|
|
297
|
+
const { namespace: ns, entityName: clean, inflection } = info
|
|
240
298
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
241
299
|
let { singular, plural } = inflection
|
|
242
300
|
|
|
@@ -250,7 +308,9 @@ class Visitor {
|
|
|
250
308
|
}
|
|
251
309
|
|
|
252
310
|
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
|
|
253
|
-
if
|
|
311
|
+
// if the user defined their entities in singular form we would also have a false positive here -> skip
|
|
312
|
+
const namespacedSingular = `${ns.asNamespace()}.${singular}`
|
|
313
|
+
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.xtended.definitions) {
|
|
254
314
|
LOG.error(
|
|
255
315
|
`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.`
|
|
256
316
|
)
|
|
@@ -307,15 +367,15 @@ class Visitor {
|
|
|
307
367
|
* Stringifies function parameters in preparation of passing them to {@link SourceFile.stringifyLambda}.
|
|
308
368
|
* Resolves all parameters to a pair of parameter name and name of the resolved type.
|
|
309
369
|
* Also filters out parameters that indicate a binding parameter ({@link https://cap.cloud.sap/docs/releases/jan23#simplified-syntax-for-binding-parameters}).
|
|
310
|
-
* @param {[string
|
|
311
|
-
* @param {
|
|
370
|
+
* @param {{[key:string]: EntityCSN}} params - parameter list as found in CSN.
|
|
371
|
+
* @param {SourceFile} file - source file relative to which the parameter types should be resolved.
|
|
312
372
|
* @returns {[string, string][]} pair of names and types.
|
|
313
373
|
*/
|
|
314
374
|
#stringifyFunctionParams(params, file) {
|
|
315
375
|
return params
|
|
316
376
|
? Object.entries(params)
|
|
317
377
|
// filter params of type '[many] $self', as they are not to be part of the implementation
|
|
318
|
-
.filter(([, type]) => type?.type !== '$self' &&
|
|
378
|
+
.filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
|
|
319
379
|
.map(([name, type]) => [
|
|
320
380
|
name,
|
|
321
381
|
this.#stringifyFunctionParamType(type, file)
|
|
@@ -323,68 +383,98 @@ class Visitor {
|
|
|
323
383
|
: []
|
|
324
384
|
}
|
|
325
385
|
|
|
386
|
+
/**
|
|
387
|
+
* @param {EntityCSN | EnumCSN} type - type
|
|
388
|
+
* @param {SourceFile} file - the file to resolve types into
|
|
389
|
+
*/
|
|
326
390
|
#stringifyFunctionParamType(type, file) {
|
|
327
391
|
// if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
|
|
328
392
|
// at a named enum type. In that case also resolve that type name
|
|
329
|
-
|
|
393
|
+
const isNamedEnumType = isEnum(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)
|
|
394
|
+
if (isNamedEnumType) return stringifyEnumType(csnToEnumPairs(type))
|
|
330
395
|
const paramType = this.resolver.resolveAndRequire(type, file)
|
|
331
396
|
return this.inlineDeclarationResolver.getPropertyDatatype(
|
|
332
397
|
paramType,
|
|
333
|
-
paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire
|
|
398
|
+
paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire
|
|
399
|
+
? paramType.typeName
|
|
400
|
+
: paramType.typeInfo.inflection.singular
|
|
334
401
|
)
|
|
335
402
|
}
|
|
336
403
|
|
|
337
404
|
/**
|
|
338
405
|
* @param {string} fq - fully qualified name of the operation
|
|
339
|
-
* @param {
|
|
406
|
+
* @param {import('./typedefs').resolver.OperationCSN} operation - CSN
|
|
340
407
|
* @param {'function' | 'action'} kind - kind of operation
|
|
341
408
|
*/
|
|
342
409
|
#printOperation(fq, operation, kind) {
|
|
343
410
|
LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
|
|
344
|
-
const
|
|
411
|
+
const info = this.entityRepository.getByFq(fq)
|
|
412
|
+
if (!info) throw new Error(`could not resolve operation ${fq}`)
|
|
413
|
+
const { namespace } = info
|
|
345
414
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
346
415
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
347
416
|
const returnType = operation.returns
|
|
348
417
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
349
|
-
: { typeName: 'void', typeInfo: { isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
418
|
+
: { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
350
419
|
const returns = this.inlineDeclarationResolver.getPropertyDatatype(
|
|
351
420
|
returnType,
|
|
352
|
-
returnType.typeInfo.isArray
|
|
421
|
+
returnType.typeInfo.isArray
|
|
422
|
+
? returnType.typeName
|
|
423
|
+
: returnType.typeInfo.inflection.singular
|
|
353
424
|
)
|
|
354
425
|
file.addOperation(last(fq), params, returns, kind)
|
|
355
426
|
}
|
|
356
427
|
|
|
428
|
+
/**
|
|
429
|
+
* @param {string} fq - fully qualified name of the type
|
|
430
|
+
* @param {EntityCSN} type - CSN
|
|
431
|
+
*/
|
|
357
432
|
#printType(fq, type) {
|
|
358
433
|
LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
|
|
359
|
-
const
|
|
434
|
+
const info = this.entityRepository.getByFq(fq)
|
|
435
|
+
if (!info) throw new Error(`could not resolve type ${fq}`)
|
|
436
|
+
const { namespace, entityName } = info
|
|
360
437
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
361
438
|
// skip references to enums.
|
|
362
439
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
363
440
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
364
|
-
if (
|
|
441
|
+
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
365
442
|
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
366
443
|
} else {
|
|
444
|
+
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
|
|
367
445
|
// alias
|
|
368
|
-
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
|
|
446
|
+
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
|
|
369
447
|
}
|
|
370
448
|
// TODO: annotations not handled yet
|
|
371
449
|
}
|
|
372
450
|
|
|
451
|
+
/**
|
|
452
|
+
* @param {string} fq - fully qualified name of the aspect
|
|
453
|
+
* @param {EntityCSN} aspect - CSN
|
|
454
|
+
*/
|
|
373
455
|
#printAspect(fq, aspect) {
|
|
374
456
|
LOG.debug(`Printing aspect ${fq}`)
|
|
375
|
-
const
|
|
457
|
+
const info = this.entityRepository.getByFq(fq)
|
|
458
|
+
if (!info) throw new Error(`could not resolve aspect ${fq}`)
|
|
459
|
+
const { namespace, entityName, inflection } = info
|
|
376
460
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
377
461
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
378
462
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
379
463
|
// So we separate them into another buffer which is printed before the classes.
|
|
380
464
|
file.addClass(entityName, fq)
|
|
381
465
|
file.aspects.add(`// the following represents the CDS aspect '${entityName}'`)
|
|
382
|
-
this.#aspectify(fq, aspect, file.aspects, { cleanName:
|
|
466
|
+
this.#aspectify(fq, aspect, file.aspects, { cleanName: inflection.singular })
|
|
383
467
|
}
|
|
384
468
|
|
|
469
|
+
/**
|
|
470
|
+
* @param {string} fq - fully qualified name of the event
|
|
471
|
+
* @param {EntityCSN} event - CSN
|
|
472
|
+
*/
|
|
385
473
|
#printEvent(fq, event) {
|
|
386
474
|
LOG.debug(`Printing event ${fq}`)
|
|
387
|
-
const
|
|
475
|
+
const info = this.entityRepository.getByFq(fq)
|
|
476
|
+
if (!info) throw new Error(`could not resolve event ${fq}`)
|
|
477
|
+
const { namespace, entityName } = info
|
|
388
478
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
389
479
|
file.addEvent(entityName, fq)
|
|
390
480
|
const buffer = file.events.buffer
|
|
@@ -400,9 +490,15 @@ class Visitor {
|
|
|
400
490
|
}, '}')
|
|
401
491
|
}
|
|
402
492
|
|
|
493
|
+
/**
|
|
494
|
+
* @param {string} fq - fully qualified name of the service
|
|
495
|
+
* @param {EntityCSN} service - CSN
|
|
496
|
+
*/
|
|
403
497
|
#printService(fq, service) {
|
|
404
498
|
LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
|
|
405
|
-
const
|
|
499
|
+
const info = this.entityRepository.getByFq(fq)
|
|
500
|
+
if (!info) throw new Error(`could not resolve service ${fq}`)
|
|
501
|
+
const { namespace } = info
|
|
406
502
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
407
503
|
// service.name is clean of namespace
|
|
408
504
|
file.services.buffer.add(`export default { name: '${service.name}' }`)
|
|
@@ -413,7 +509,7 @@ class Visitor {
|
|
|
413
509
|
* Visits a single entity from the CSN's definition field.
|
|
414
510
|
* Will call #printEntity or #printAction based on the entity's kind.
|
|
415
511
|
* @param {string} fq - name of the entity, fully qualified as is used in the definition field.
|
|
416
|
-
* @param {
|
|
512
|
+
* @param {EntityCSN} entity - CSN data belonging to the entity to perform lookups in.
|
|
417
513
|
*/
|
|
418
514
|
visitEntity(fq, entity) {
|
|
419
515
|
switch (entity.kind) {
|
|
@@ -422,6 +518,7 @@ class Visitor {
|
|
|
422
518
|
break
|
|
423
519
|
case 'action':
|
|
424
520
|
case 'function':
|
|
521
|
+
// @ts-expect-error - we know entity is actually an OperationCSN
|
|
425
522
|
this.#printOperation(fq, entity, entity.kind)
|
|
426
523
|
break
|
|
427
524
|
case 'aspect':
|
|
@@ -468,16 +565,25 @@ class Visitor {
|
|
|
468
565
|
/**
|
|
469
566
|
* Visits a single element in an entity.
|
|
470
567
|
* @param {string} name - name of the element
|
|
471
|
-
* @param {
|
|
568
|
+
* @param {EntityCSN} element - CSN data belonging to the the element.
|
|
472
569
|
* @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
|
|
473
|
-
* @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
570
|
+
* @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
474
571
|
* @returns @see InlineDeclarationResolver.visitElement
|
|
475
572
|
*/
|
|
476
|
-
visitElement(name, element, file, buffer) {
|
|
477
|
-
return this.inlineDeclarationResolver.visitElement(
|
|
573
|
+
visitElement(name, element, file, buffer = file.classes) {
|
|
574
|
+
return this.inlineDeclarationResolver.visitElement({
|
|
575
|
+
name,
|
|
576
|
+
element,
|
|
577
|
+
file,
|
|
578
|
+
buffer,
|
|
579
|
+
// we explicitly pass the "declare" modifier here to avoid problems with noImplicitOverride and useDefineForClassFields in strict tsconfigs
|
|
580
|
+
// but not inside type defs (e.g. parameter types) where this would be a syntax error
|
|
581
|
+
modifiers: getPropertyModifiers(element)
|
|
582
|
+
})
|
|
478
583
|
}
|
|
479
584
|
}
|
|
480
585
|
|
|
481
586
|
module.exports = {
|
|
482
587
|
Visitor
|
|
483
588
|
}
|
|
589
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"doc:clean": "rm -rf ./doc",
|
|
25
25
|
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|
|
26
26
|
"doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
|
|
27
|
-
"doc:cli": "npm run cli -- --help > ./doc/cli.txt"
|
|
27
|
+
"doc:cli": "npm run cli -- --help > ./doc/cli.txt",
|
|
28
|
+
"jsdoc:check": "tsc --noEmit --project jsconfig.json"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"lib/",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@stylistic/eslint-plugin-js": "^1.6.3",
|
|
48
|
+
"@cap-js/cds-types": ">=0.6",
|
|
47
49
|
"acorn": "^8.10.0",
|
|
48
50
|
"eslint": "^9",
|
|
49
51
|
"eslint-plugin-jsdoc": "^48.2.7",
|