@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/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.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`)
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 => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
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 ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
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, e.name),
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, e.name, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
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
- buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(fq, clean).join('\n')}}`)
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
- this.#aspectify(fq, entity, buffer, { cleanName: singular })
407
+ buffer.add(`// entity '${singular.scoped}'`) // identifiers, especially normalised ones, can get really butchered, so this is helpful for debugging
389
408
 
390
- buffer.add(overrideNameProperty(singular, entity.name))
391
- buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
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
- // at a named enum type. In that case also resolve that type name
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.typeInfo.isArray || paramType.typeInfo.isDeepRequire
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.typeInfo.isArray
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
- file.addOperation(last(fq), params, returns, kind, docify(operation.doc), {named: true, positional: !isRFC})
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, entityName, csnToEnumPairs(type), docify(type.doc))
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, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
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 ${name}`,
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 @see InlineDeclarationResolver.visitElement
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.38.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
  }