@cap-js/cds-typer 0.38.0 → 0.39.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/lib/cli.js +5 -1
- package/lib/components/class.js +1 -1
- package/lib/components/enum.js +3 -3
- package/lib/components/identifier.js +183 -7
- package/lib/components/inline.js +15 -6
- package/lib/file.js +37 -30
- package/lib/printers/wrappers.js +12 -2
- package/lib/resolution/entity.js +6 -6
- package/lib/resolution/resolver.js +229 -74
- package/lib/typedefs.d.ts +12 -7
- package/lib/util.js +4 -2
- package/lib/visitor.js +135 -62
- package/package.json +6 -1
package/lib/visitor.js
CHANGED
|
@@ -6,13 +6,13 @@ const { SourceFile, FileRepository, Buffer, Path } = require('./file')
|
|
|
6
6
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
7
7
|
const { Resolver } = require('./resolution/resolver')
|
|
8
8
|
const { LOG } = require('./logging')
|
|
9
|
-
const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf, stringIdent, createDraftsOf, createDraftOf, createIntersectionOf } = require('./printers/wrappers')
|
|
9
|
+
const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf, stringIdent, createDraftsOf, createDraftOf, createIntersectionOf, createBrandedType } = require('./printers/wrappers')
|
|
10
10
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
11
11
|
const { isReferenceType } = require('./components/reference')
|
|
12
12
|
const { empty } = require('./components/typescript')
|
|
13
13
|
const { getBaseDefinitions } = require('./components/basedefs')
|
|
14
14
|
const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
15
|
-
const { last } = require('./components/identifier')
|
|
15
|
+
const { last, Identifier } = require('./components/identifier')
|
|
16
16
|
const { getPropertyModifiers } = require('./components/property')
|
|
17
17
|
const { configuration } = require('./config')
|
|
18
18
|
const { createMember } = require('./components/class')
|
|
@@ -103,7 +103,13 @@ class Visitor {
|
|
|
103
103
|
#printStaticActions(entity, clean, buffer, ancestors, file) {
|
|
104
104
|
// TODO: refactor away! All these printing functionalities need to go
|
|
105
105
|
const actions = Object.entries(entity.actions ?? {})
|
|
106
|
-
const inherited = ancestors
|
|
106
|
+
const inherited = ancestors
|
|
107
|
+
// Filter out ancestors whose class name matches the current class to avoid circular references
|
|
108
|
+
.filter(a => {
|
|
109
|
+
const ancestorClassName = isType(a.csn) ? a.entityName : a.inflection.singular?.plain
|
|
110
|
+
return ancestorClassName !== clean
|
|
111
|
+
})
|
|
112
|
+
.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`)
|
|
107
113
|
|
|
108
114
|
const typeBuffer = buffer.createSubBuffer()
|
|
109
115
|
if (actions.length) {
|
|
@@ -143,6 +149,12 @@ class Visitor {
|
|
|
143
149
|
#printStaticKeys(buffer, clean, ancestors, file) {
|
|
144
150
|
const ancestorKeys = ancestors
|
|
145
151
|
.filter(a => Object.entries(a.csn.keys ?? {}).length)
|
|
152
|
+
// Filter out ancestors whose class name matches the current class to avoid circular references
|
|
153
|
+
// This happens when a nested entity has the same @singular as its ancestor
|
|
154
|
+
.filter(a => {
|
|
155
|
+
const ancestorClassName = isType(a.csn) ? a.entityName : a.inflection.singular?.plain
|
|
156
|
+
return ancestorClassName !== clean
|
|
157
|
+
})
|
|
146
158
|
.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.keys`)
|
|
147
159
|
buffer.add(createMember({
|
|
148
160
|
name: 'keys',
|
|
@@ -173,38 +185,21 @@ class Visitor {
|
|
|
173
185
|
* - the function A(B) to mix the aspect into another class B
|
|
174
186
|
* - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
|
|
175
187
|
* - the type A to use for external typing and is derived from AXtended.
|
|
176
|
-
* @param {string} fq - the name of the entity
|
|
188
|
+
* @param {Identifier | string} fq - the name of the entity
|
|
177
189
|
* @param {EntityCSN} entity - the pointer into the CSN to extract the elements from
|
|
178
190
|
* @param {Buffer} buffer - the buffer to write the resulting definitions into
|
|
179
191
|
* @param {{cleanName?: string}} options - additional options
|
|
180
192
|
*/
|
|
181
193
|
#aspectify(fq, entity, buffer, options = {}) {
|
|
194
|
+
if (typeof fq !== 'string') fq = fq.plain
|
|
182
195
|
const info = this.entityRepository.getByFqOrThrow(fq)
|
|
183
|
-
const clean = options?.cleanName ?? info.withoutNamespace
|
|
196
|
+
const clean = new Identifier(options?.cleanName ?? info.withoutNamespace).plain
|
|
184
197
|
const { namespace } = info
|
|
185
198
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
199
|
+
const { isNormalised = false, originalName = null } = options
|
|
186
200
|
const identSingular = (/** @type {string} */name) => name // FIXME: remove
|
|
187
|
-
//const identAspect = name => `_${name}Aspect`
|
|
188
201
|
/** @param {string} name - the name */
|
|
189
202
|
const identAspect = name => `_${name}Aspect`
|
|
190
|
-
/**
|
|
191
|
-
* @param {object} options - options
|
|
192
|
-
* @param {Path} [options.ns] - namespace
|
|
193
|
-
* @param {string} options.clean - the clean name of the entity
|
|
194
|
-
* @param {string} options.fq - fully qualified name
|
|
195
|
-
* @returns {string} the local identifier
|
|
196
|
-
*/
|
|
197
|
-
// FIXME: replace with resolution/entity::asIdentifier
|
|
198
|
-
const toLocalIdent = ({ns, clean, fq}) => {
|
|
199
|
-
// types are not inflected, so don't change those to singular
|
|
200
|
-
const csn = this.csn.definitions[fq]
|
|
201
|
-
const ident = isType(csn)
|
|
202
|
-
? clean
|
|
203
|
-
: this.resolver.inflect({csn, plainName: clean}).singular
|
|
204
|
-
return !ns || ns.isCwd(file.path.asDirectory())
|
|
205
|
-
? ident
|
|
206
|
-
: `${ns.asIdentifier()}.${ident}`
|
|
207
|
-
}
|
|
208
203
|
// remove the ancestry of projections/ views.
|
|
209
204
|
// They explicitly define their properties.
|
|
210
205
|
// But at the same time they also carry their .includes clause, which can
|
|
@@ -225,13 +220,23 @@ class Visitor {
|
|
|
225
220
|
|
|
226
221
|
const ancestorsAspects = ancestorInfos
|
|
227
222
|
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
228
|
-
.reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name =>
|
|
223
|
+
.reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => identAspect(name), relative: file.path})}(${wrapped})`, 'Base')
|
|
224
|
+
|
|
225
|
+
// Check if the aspect function name would collide with an ancestor's aspect function name
|
|
226
|
+
// This can happen in nested namespaces where the entity has the same @singular as its ancestor
|
|
227
|
+
const hasNamingCollision = ancestorInfos.some(ancestor => {
|
|
228
|
+
const ancestorName = isType(ancestor.csn) ? ancestor.entityName : ancestor.inflection.singular?.plain
|
|
229
|
+
return ancestorName === clean
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// If there's a naming collision, use a unique name based on the entity name to avoid shadowing
|
|
233
|
+
const aspectFunctionName = hasNamingCollision ? identAspect(info.entityName) : identAspect(clean)
|
|
229
234
|
|
|
230
235
|
const inheritedElements = !isViewOrProjection(entity) ? info.inheritedElements : null
|
|
231
|
-
this.contexts.push({ entity: fq })
|
|
236
|
+
this.contexts.push({ entity: new Identifier(fq) })
|
|
232
237
|
|
|
233
238
|
// CLASS ASPECT
|
|
234
|
-
buffer.addIndentedBlock(`export function ${
|
|
239
|
+
buffer.addIndentedBlock(`export function ${aspectFunctionName}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
|
|
235
240
|
buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
|
|
236
241
|
/** @type {import('./typedefs').resolver.EnumCSN[]} */
|
|
237
242
|
const enums = []
|
|
@@ -255,7 +260,8 @@ class Visitor {
|
|
|
255
260
|
} else {
|
|
256
261
|
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
257
262
|
isRefNotNull: !!element.notNull || !!element.key,
|
|
258
|
-
key: element.key
|
|
263
|
+
key: element.key,
|
|
264
|
+
_targetEntity: element.target // Track the target entity for inline enum resolution
|
|
259
265
|
})
|
|
260
266
|
this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})
|
|
261
267
|
}
|
|
@@ -271,10 +277,12 @@ class Visitor {
|
|
|
271
277
|
|
|
272
278
|
for (const e of enums) {
|
|
273
279
|
const eDoc = docify(e.doc)
|
|
280
|
+
// Normalize property name to handle delimited identifiers
|
|
281
|
+
const normalizedPropertyName = new Identifier(e.name).normalised.plain
|
|
274
282
|
buffer.add(eDoc)
|
|
275
283
|
buffer.add(createMember({
|
|
276
284
|
name: e.name,
|
|
277
|
-
initialiser: propertyToInlineEnumName(clean,
|
|
285
|
+
initialiser: propertyToInlineEnumName(clean, normalizedPropertyName),
|
|
278
286
|
isStatic: true,
|
|
279
287
|
}))
|
|
280
288
|
if (typeof e?.type !== 'string' && e?.type?.ref) {
|
|
@@ -305,7 +313,7 @@ class Visitor {
|
|
|
305
313
|
}
|
|
306
314
|
} catch { /* ignore */ }
|
|
307
315
|
}
|
|
308
|
-
file.addInlineEnum(clean, fq,
|
|
316
|
+
file.addInlineEnum(clean, fq, normalizedPropertyName, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
if ('kind' in entity) {
|
|
@@ -328,7 +336,17 @@ class Visitor {
|
|
|
328
336
|
// CLASS WITH ADDED ASPECTS
|
|
329
337
|
file.addImport(baseDefinitions.path)
|
|
330
338
|
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
331
|
-
|
|
339
|
+
|
|
340
|
+
// Add documentation if the name was normalized
|
|
341
|
+
if (isNormalised && originalName) {
|
|
342
|
+
buffer.add(docify([
|
|
343
|
+
`This class represents "${originalName}" and can be accessed via:`,
|
|
344
|
+
`- Named import: \`import { ${clean} } from '...'\``,
|
|
345
|
+
`- Aliased import: \`import { "${originalName}" as MyAlias } from '...'\`!`
|
|
346
|
+
]))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
buffer.add(`export class ${identSingular(clean)} extends ${aspectFunctionName}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(fq, clean).join('\n')}}`)
|
|
332
350
|
this.contexts.pop()
|
|
333
351
|
}
|
|
334
352
|
|
|
@@ -350,27 +368,28 @@ class Visitor {
|
|
|
350
368
|
const info = this.entityRepository.getByFqOrThrow(fq)
|
|
351
369
|
const { namespace: ns, entityName: clean, inflection, scope } = info
|
|
352
370
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
353
|
-
let { singular, plural } = inflection
|
|
371
|
+
let { singular, plural } = inflection ?? {}
|
|
354
372
|
|
|
355
373
|
// trimNamespace does not properly detect scoped entities, like A.B where both A and B are
|
|
356
374
|
// entities. So to see if we would run into a naming collision, we forcefully take the last
|
|
357
375
|
// part of the name, so "A.B" and "A.Bs" just become "B" and "Bs" to be compared.
|
|
358
|
-
if (last(plural) === `${last(singular)}_`) {
|
|
376
|
+
if (last(plural?.normalised.plain) === `${last(singular?.normalised.plain ?? '')}_`) {
|
|
359
377
|
LOG.warn(
|
|
360
378
|
`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.`
|
|
361
379
|
)
|
|
362
380
|
}
|
|
363
381
|
|
|
382
|
+
// use non-normalised identifier to facilitate lookup in CSN
|
|
383
|
+
const namespacedSingular = `${ns.asNamespace()}.${singular.plain}`
|
|
364
384
|
// as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
|
|
365
385
|
// if the user defined their entities in singular form we would also have a false positive here -> skip
|
|
366
|
-
const namespacedSingular = `${ns.asNamespace()}.${singular}`
|
|
367
386
|
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.definitions) {
|
|
368
387
|
LOG.error(
|
|
369
|
-
`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.`
|
|
388
|
+
`Derived singular '${singular.plain}' 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.`
|
|
370
389
|
)
|
|
371
390
|
}
|
|
372
|
-
file.addClass(singular, fq)
|
|
373
|
-
file.addClass(plural, fq)
|
|
391
|
+
file.addClass(singular.normalised.plain, fq)
|
|
392
|
+
file.addClass(plural.normalised.plain, fq)
|
|
374
393
|
|
|
375
394
|
const parent = this.resolver.resolveParent(entity.name)
|
|
376
395
|
const buffer = parent && parent.kind === 'entity'
|
|
@@ -380,15 +399,26 @@ class Visitor {
|
|
|
380
399
|
if (scope?.length > 0) {
|
|
381
400
|
/** @param {string} n - name of entity */
|
|
382
401
|
const scoped = n => [...scope, n].join('.')
|
|
383
|
-
file.addInflection(scoped(singular), scoped(plural), scoped(clean))
|
|
402
|
+
file.addInflection({singular: scoped(singular.normalised.plain), plural: scoped(plural.normalised.plain), original: scoped(clean), isNormalised: singular.normalised.isChangedFromNormalisation})
|
|
384
403
|
} else {
|
|
385
|
-
file.addInflection(singular, plural, clean)
|
|
404
|
+
file.addInflection({singular:singular.normalised.plain, plural: plural.normalised.plain, original: clean, isNormalised: singular.normalised.isChangedFromNormalisation})
|
|
386
405
|
}
|
|
387
406
|
|
|
388
|
-
|
|
407
|
+
buffer.add(`// entity '${singular.scoped}'`) // identifiers, especially normalised ones, can get really butchered, so this is helpful for debugging
|
|
389
408
|
|
|
390
|
-
|
|
391
|
-
|
|
409
|
+
this.#aspectify(fq, entity, buffer, {
|
|
410
|
+
cleanName: singular.normalised.plain,
|
|
411
|
+
isNormalised: singular.normalised.isChangedFromNormalisation,
|
|
412
|
+
originalName: singular.plain
|
|
413
|
+
}) // FIXME: pass Identifier verbatim?
|
|
414
|
+
|
|
415
|
+
buffer.add(overrideNameProperty(singular.normalised.plain, entity.name))
|
|
416
|
+
buffer.add(`Object.defineProperty(${singular.normalised.plain}, 'is_singular', { value: true })`)
|
|
417
|
+
|
|
418
|
+
// Add export alias if the name was normalized
|
|
419
|
+
if (singular.normalised.isChangedFromNormalisation) {
|
|
420
|
+
buffer.add(`export { ${singular.normalised.plain} as "${singular.plain}" }`)
|
|
421
|
+
}
|
|
392
422
|
|
|
393
423
|
// PLURAL
|
|
394
424
|
// types do not receive a plural
|
|
@@ -406,17 +436,32 @@ class Visitor {
|
|
|
406
436
|
const { fullyQualifiedName: fq, csn: entity } = info
|
|
407
437
|
let { singular, plural } = info.inflection
|
|
408
438
|
|
|
409
|
-
if (plural.includes('.')) {
|
|
439
|
+
if (plural.plain.includes('.')) {
|
|
410
440
|
// Foo.text -> namespace Foo { class text { ... }}
|
|
411
|
-
plural = last(plural)
|
|
441
|
+
plural.plain = last(plural.plain)
|
|
412
442
|
}
|
|
413
443
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
414
444
|
// so it can get passed as value to CQL functions.
|
|
415
|
-
const additionalProperties = this.#staticClassContents(fq, singular, true)
|
|
445
|
+
const additionalProperties = this.#staticClassContents(fq, singular.plain, true)
|
|
416
446
|
additionalProperties.push('$count?: number')
|
|
447
|
+
|
|
448
|
+
// Add documentation if the name was normalized
|
|
449
|
+
if (plural.normalised.isChangedFromNormalisation) {
|
|
450
|
+
buffer.add(docify([
|
|
451
|
+
`This class represents the plural form of "${singular.plain}". It is not a type alias to "${singular.plain}[]" but a separate class, which can be used as value (e.g., for CQL functions). It can be accessed via:`,
|
|
452
|
+
`- Named import: \`import { ${plural.normalised.plain} } from '...'\``,
|
|
453
|
+
`- Aliased import: \`import { "${plural.plain}" as MyAlias } from '...'\`!`
|
|
454
|
+
]))
|
|
455
|
+
}
|
|
456
|
+
|
|
417
457
|
buffer.add(docify(entity.doc))
|
|
418
|
-
buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
|
|
419
|
-
buffer.add(overrideNameProperty(plural, entity.name))
|
|
458
|
+
buffer.add(`export class ${plural.normalised.plain} extends Array<${singular.normalised.plain}> {${additionalProperties.join('\n')}}`)
|
|
459
|
+
buffer.add(overrideNameProperty(plural.normalised.plain, entity.name))
|
|
460
|
+
|
|
461
|
+
// Add export alias if the name was normalized
|
|
462
|
+
if (plural.normalised.isChangedFromNormalisation) {
|
|
463
|
+
buffer.add(`export { ${plural.normalised.plain} as "${plural.plain}" }`)
|
|
464
|
+
}
|
|
420
465
|
}
|
|
421
466
|
|
|
422
467
|
/**
|
|
@@ -447,15 +492,13 @@ class Visitor {
|
|
|
447
492
|
*/
|
|
448
493
|
#stringifyFunctionParamType(type, file) {
|
|
449
494
|
// if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
|
|
450
|
-
//
|
|
495
|
+
// at a named enum type. In that case also resolve that type name
|
|
451
496
|
const isNamedEnumType = isEnum(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)
|
|
452
497
|
if (isNamedEnumType) return stringifyEnumType(csnToEnumPairs(type))
|
|
453
498
|
const paramType = this.resolver.resolveAndRequire(type, file)
|
|
454
499
|
return this.inlineDeclarationResolver.getPropertyDatatype(
|
|
455
500
|
paramType,
|
|
456
|
-
paramType.
|
|
457
|
-
? paramType.typeName
|
|
458
|
-
: paramType.typeInfo.inflection.singular
|
|
501
|
+
paramType.typeName // Use typeName which includes namespace prefix
|
|
459
502
|
)
|
|
460
503
|
}
|
|
461
504
|
|
|
@@ -471,12 +514,10 @@ class Visitor {
|
|
|
471
514
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
472
515
|
const returnType = operation.returns
|
|
473
516
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
474
|
-
: { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
517
|
+
: { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: new Identifier('void'), plural: new Identifier('void') } } }
|
|
475
518
|
let returns = this.inlineDeclarationResolver.getPropertyDatatype(
|
|
476
519
|
returnType,
|
|
477
|
-
returnType.
|
|
478
|
-
? returnType.typeName
|
|
479
|
-
: returnType.typeInfo.inflection.singular
|
|
520
|
+
returnType.typeName // Use typeName which includes namespace prefix
|
|
480
521
|
)
|
|
481
522
|
if (operation.returns) {
|
|
482
523
|
// operation results may be a Promise
|
|
@@ -486,7 +527,12 @@ class Visitor {
|
|
|
486
527
|
// Prevent positional call style there.
|
|
487
528
|
// TODO find a better way to detect ABAP RFC actions
|
|
488
529
|
const isRFC = Object.values(operation.params ?? {}).some(p => Object.keys(p).some(k => k.startsWith('@RFC')))
|
|
489
|
-
|
|
530
|
+
// Normalize the operation name to handle delimited identifiers
|
|
531
|
+
const originalName = last(fq)
|
|
532
|
+
const identifier = new Identifier(originalName)
|
|
533
|
+
const normalizedName = identifier.normalised.plain
|
|
534
|
+
const originalNameIfDifferent = identifier.normalised.plain !== identifier.plain ? originalName : undefined
|
|
535
|
+
file.addOperation(normalizedName, params, returns, kind, docify(operation.doc), {named: true, positional: !isRFC}, originalNameIfDifferent)
|
|
490
536
|
file.addImport(baseDefinitions.path)
|
|
491
537
|
}
|
|
492
538
|
|
|
@@ -498,15 +544,32 @@ class Visitor {
|
|
|
498
544
|
LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
|
|
499
545
|
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
500
546
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
547
|
+
|
|
548
|
+
// Normalize the entity name to handle reserved words and special characters
|
|
549
|
+
const identifier = new Identifier(entityName)
|
|
550
|
+
const normalizedName = identifier.normalised.plain
|
|
551
|
+
|
|
501
552
|
// skip references to enums.
|
|
502
553
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
503
554
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
504
555
|
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
505
|
-
file.addEnum(fq,
|
|
556
|
+
file.addEnum(fq, normalizedName, csnToEnumPairs(type), docify(type.doc))
|
|
557
|
+
// Add export alias if the normalized name differs from the original
|
|
558
|
+
if (normalizedName !== identifier.plain) {
|
|
559
|
+
file.types.add(`export { ${normalizedName} as "${identifier.plain}" }`)
|
|
560
|
+
}
|
|
506
561
|
} else {
|
|
507
562
|
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.definitions[type?.type])
|
|
563
|
+
const resolved = this.resolver.resolveAndRequire(type, file)
|
|
564
|
+
if (configuration.brandedPrimitiveTypes && resolved.typeInfo.isBuiltin) {
|
|
565
|
+
resolved.typeName = createIntersectionOf(resolved.typeName, createBrandedType(fq))
|
|
566
|
+
}
|
|
508
567
|
// alias
|
|
509
|
-
file.addType(fq,
|
|
568
|
+
file.addType(fq, normalizedName, resolved.typeName, isEnumReference)
|
|
569
|
+
// Add export alias if the normalized name differs from the original
|
|
570
|
+
if (normalizedName !== identifier.plain) {
|
|
571
|
+
file.types.add(`export { ${normalizedName} as "${identifier.plain}" }`)
|
|
572
|
+
}
|
|
510
573
|
}
|
|
511
574
|
// TODO: annotations not handled yet
|
|
512
575
|
}
|
|
@@ -543,6 +606,14 @@ class Visitor {
|
|
|
543
606
|
buffer.add('// event')
|
|
544
607
|
// only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
|
|
545
608
|
buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
|
|
609
|
+
buffer.add(createMember({
|
|
610
|
+
name: 'kind',
|
|
611
|
+
type: stringIdent(event.kind),
|
|
612
|
+
isStatic: true,
|
|
613
|
+
isReadonly: true,
|
|
614
|
+
isDeclare: false,
|
|
615
|
+
isOverride: false
|
|
616
|
+
}))
|
|
546
617
|
const propOpt = configuration.propertiesOptional
|
|
547
618
|
// FIXME: shouldn't need to change config here! Idea: init Visitor with .options fed from config, then manipulate that
|
|
548
619
|
configuration.propertiesOptional = false
|
|
@@ -568,10 +639,12 @@ class Visitor {
|
|
|
568
639
|
// file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
|
|
569
640
|
buffer.addIndentedBlock('export default class {', () => {
|
|
570
641
|
Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
|
|
642
|
+
// Normalize the operation name to match how it's exported in #printOperation
|
|
643
|
+
const normalizedName = new Identifier(name).normalised.plain
|
|
571
644
|
buffer.add(docify(doc))
|
|
572
645
|
buffer.add(createMember({
|
|
573
|
-
name,
|
|
574
|
-
type: `typeof ${
|
|
646
|
+
name: normalizedName,
|
|
647
|
+
type: `typeof ${normalizedName}`,
|
|
575
648
|
isStatic: true,
|
|
576
649
|
isReadonly: true,
|
|
577
650
|
isDeclare: true,
|
|
@@ -636,7 +709,7 @@ class Visitor {
|
|
|
636
709
|
* ```
|
|
637
710
|
*/
|
|
638
711
|
isSelfReference(fq) {
|
|
639
|
-
return fq === this.contexts.at(-1)?.entity
|
|
712
|
+
return fq === this.contexts.at(-1)?.entity.scoped
|
|
640
713
|
}
|
|
641
714
|
|
|
642
715
|
/**
|
|
@@ -647,7 +720,7 @@ class Visitor {
|
|
|
647
720
|
* @param {SourceFile} options.file - the namespace file the surrounding entity is being printed into.
|
|
648
721
|
* @param {Buffer} [options.buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
649
722
|
* @param {TypeResolveOptions} [options.resolverOptions] - custom type resolver options
|
|
650
|
-
* @returns
|
|
723
|
+
* @returns {ReturnType<import('./components/inline').FlatInlineDeclarationResolver['visitElement']>}
|
|
651
724
|
*/
|
|
652
725
|
visitElement({name, element, file, buffer = file.classes, resolverOptions}) {
|
|
653
726
|
return this.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.39.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",
|
|
@@ -155,6 +155,11 @@
|
|
|
155
155
|
"type": "boolean",
|
|
156
156
|
"description": "If set to true, the typescript build task will not be registered/ executed.\nThis value must be set in your project configuration.\nPassing it as parameter to the cds-typer CLI has no effect.",
|
|
157
157
|
"default": true
|
|
158
|
+
},
|
|
159
|
+
"branded_primitive_types": {
|
|
160
|
+
"type": "boolean",
|
|
161
|
+
"description": "If set to true, generated primitive types will be branded to prevent accidental mixing of types with the same underlying primitive.\nE.g., type CustomerID = string & { __brand: 'CustomerID' }",
|
|
162
|
+
"default": false
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
}
|