@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.
@@ -11,8 +11,7 @@ const { isEntity, getMaxCardinality, isExternal } = require('../csn')
11
11
  const { getBaseDefinitions } = require('../components/basedefs')
12
12
  const { BuiltinResolver } = require('./builtin')
13
13
  const { LOG } = require('../logging')
14
- const { last } = require('../components/identifier')
15
- const { getPropertyModifiers } = require('../components/property')
14
+ const { last, Identifier } = require('../components/identifier')
16
15
  const { configuration } = require('../config')
17
16
 
18
17
  const baseDefinitions = getBaseDefinitions()
@@ -72,7 +71,7 @@ class Resolver {
72
71
 
73
72
  /**
74
73
  * @param {EntityCSN} type - a CSN type
75
- * @returns {boolean} whether the type has the @mandatory annotation
74
+ * @returns {boolean} whether the type has the `@mandatory` annotation
76
75
  */
77
76
  isMandatory(type) {
78
77
  return type['@mandatory'] === true
@@ -214,9 +213,9 @@ class Resolver {
214
213
  // guard: types don't get inflected
215
214
  if (typeInfo.csn?.kind === 'type') {
216
215
  return {
217
- singular: typeInfo.plainName,
218
- plural: createArrayOf(typeInfo.plainName),
219
- typeName: typeInfo.plainName,
216
+ singular: new Identifier(typeInfo.plainName),
217
+ plural: new Identifier(createArrayOf(typeInfo.plainName)),
218
+ typeName: typeInfo.plainName
220
219
  }
221
220
  }
222
221
 
@@ -238,19 +237,46 @@ class Resolver {
238
237
  // FIXME: in most other places where we have an inline declaration, we actually don't need the typeName.
239
238
  // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
240
239
  // piece of code instead to reduce overhead.
241
- const into = new Buffer()
242
- this.structuredInlineResolver.printInlineType({
243
- fq: '',
244
- type: { typeInfo, typeName: '' },
245
- buffer: into,
246
- statementEnd: '',
247
- modifiers: getPropertyModifiers(typeInfo.csn)
248
- })
249
- typeName = into.join()
240
+
241
+ // If the inline type has a structuredType, print it properly.
242
+ // Otherwise, just use the type string directly (e.g., '{}' for empty inline types)
243
+ if (typeInfo.structuredType) {
244
+ const into = new Buffer()
245
+ // For action/function return types, the inline type itself should not have | null appended
246
+ // Nullability is handled at the action level, not within the inline type
247
+ const typeInfoWithNotNull = { ...typeInfo, isNotNull: true }
248
+ this.structuredInlineResolver.printInlineType({
249
+ fq: '',
250
+ type: { typeInfo: typeInfoWithNotNull, typeName: '' },
251
+ buffer: into,
252
+ statementEnd: '',
253
+ modifiers: [] // No modifiers for inline types in type positions (function params/returns)
254
+ })
255
+ // Join with space instead of newline for inline types so they can be used in type parameters
256
+ typeName = into.join(' ')
257
+ } else {
258
+ // No structured type (e.g., empty inline declaration like `element.type === undefined`)
259
+ // Just use the type string directly
260
+ typeName = typeInfo.type
261
+ }
250
262
  singular = typeName
251
- plural = createArrayOf(typeName)
263
+ // For inline types, plural is the same as singular - the wrapper (Composition.of.many, etc.)
264
+ // handles the array semantics, so we don't need to wrap in Array<> here
265
+ plural = typeName
266
+ // Create Identifier instances for inline types to maintain API consistency
267
+ // We mark them as "already normalized" to prevent the Identifier constructor from
268
+ // parsing dots or attempting normalization on TypeScript type literal strings
269
+ // By passing a fake 'from' object with the same plain value, we ensure:
270
+ // - .normalised returns the same instance (no further normalization)
271
+ // - .isChangedFromNormalisation returns false
272
+ // - Dot-parsing is skipped (scope is explicitly provided as [])
273
+ const createInlineIdentifier = (/** @type {string} */typeString) => new Identifier(typeString, [], null, true)
274
+ return {
275
+ typeName,
276
+ singular: createInlineIdentifier(singular),
277
+ plural: createInlineIdentifier(plural)
278
+ }
252
279
  } else {
253
- // TODO: make sure the resolution still works. Currently, we only cut off the namespace!
254
280
  plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName
255
281
  // remove leading entity name
256
282
  if (plural.includes('.')) plural = last(plural)
@@ -272,7 +298,7 @@ class Resolver {
272
298
  LOG.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
273
299
  }
274
300
 
275
- return { typeName, singular, plural }
301
+ return { typeName, singular: new Identifier(singular), plural: new Identifier(plural) }
276
302
  }
277
303
 
278
304
  /**
@@ -300,18 +326,32 @@ class Resolver {
300
326
 
301
327
  /** @type {string|undefined} */
302
328
  let typeNamespaceIdent = undefined
303
- let typeName = typeInfo.plainName ?? typeInfo.type
329
+ // For inline declarations, we use the plain type string without normalization
330
+ // because it's a TypeScript type literal, not an identifier
331
+ let typeName = typeInfo.isInlineDeclaration
332
+ ? (typeInfo.inflection?.singular.plain ?? typeInfo.type)
333
+ : (typeInfo.inflection?.singular.normalised.plain ?? typeInfo.plainName ?? typeInfo.type)
304
334
 
305
335
  // only applies to builtin types, because the association/ composition _themselves_ are the (builtin) types we are checking, not their generic parameter!
306
336
  if (typeInfo.isBuiltin === true) {
307
- const [toOne, toMany] =
308
- {
309
- Association: [createToOneAssociation, createToManyAssociation],
310
- Composition: [createCompositionOfOne, createCompositionOfMany],
311
- array: [createArrayOf, createArrayOf]
312
- }[element.constructor.name] ?? []
313
-
314
- if (toOne && toMany) {
337
+ // Handle regular arrays (array of Type)
338
+ if (typeInfo.isArray === true && typeInfo.itemsType) {
339
+ // Array items should not be nullable - the array itself handles nullability
340
+ // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
341
+ element.items.notNull = true
342
+ // Recursively resolve the items type
343
+ const itemsTypeResolved = this.resolveAndRequire(element.items, file, options)
344
+ // Use the resolved items type name
345
+ typeName = createArrayOf(itemsTypeResolved.typeName)
346
+ } else {
347
+ const [toOne, toMany] =
348
+ {
349
+ Association: [createToOneAssociation, createToManyAssociation],
350
+ Composition: [createCompositionOfOne, createCompositionOfMany],
351
+ array: [createArrayOf, createArrayOf]
352
+ }[element.constructor.name] ?? []
353
+
354
+ if (toOne && toMany) {
315
355
  /**
316
356
  * Resolve a property from a CSN entity. If it is a reference, leave it as is.
317
357
  * If it is a string, return an object with type set to the string.
@@ -319,31 +359,57 @@ class Resolver {
319
359
  * @param {string} property - the property to check
320
360
  * @returns {import('../typedefs').resolver.EntityCSN | { type: string }}
321
361
  */
322
- const getTarget = (el, property) => typeof el[property] === 'string'
323
- ? { type: el[property] }
324
- : el[property]
362
+ const getTarget = (el, property) => typeof el[property] === 'string'
363
+ ? { type: el[property] }
364
+ : el[property]
325
365
 
326
- /** @type { EntityCSN | { type: string } | undefined } */
327
- const target = element.items
366
+ /** @type { EntityCSN | { type: string } | undefined } */
367
+ const target = element.items
328
368
  ?? getTarget(element, 'target')
329
369
  ?? getTarget(element, 'targetAspect') // Composition of aspects
330
- if (!target) {
331
- throw new Error(`Could not resolve target of ${element}`)
332
- }
333
- /** set `notNull = true` to avoid repeated `| not null` TS construction */
334
- // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
335
- target.notNull = true
336
- // @ts-expect-error - yes, target is a valid parameter
337
- const targetTypeInfo = this.resolveAndRequire(target, file)
338
- if (targetTypeInfo.typeInfo.isDeepRequire === true) {
339
- typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
340
- } else {
341
- let { singular, plural } = targetTypeInfo.typeInfo.inflection
342
-
343
- typeName = cardinality > 1
344
- ? toMany(plural)
345
- : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
346
- file.addImport(baseDefinitions.path)
370
+ if (!target) {
371
+ throw new Error(`Could not resolve target of ${element}`)
372
+ }
373
+ /** set `notNull = true` to avoid repeated `| not null` TS construction */
374
+ // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
375
+ target.notNull = true
376
+ // @ts-expect-error - yes, target is a valid parameter
377
+ const targetTypeInfo = this.resolveAndRequire(target, file)
378
+ if (targetTypeInfo.typeInfo.isDeepRequire === true) {
379
+ //typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
380
+ //FIXME: do we ned this change as opposed to line above?
381
+ // For inline declarations, use plain type without normalization but wrap in Array for many cardinality
382
+ let targetTypeName = targetTypeInfo.typeInfo.isInlineDeclaration
383
+ ? targetTypeInfo.typeInfo.inflection.singular.plain
384
+ : targetTypeInfo.typeInfo.inflection.singular.normalised.plain
385
+ // Inline types need to be wrapped in Array<> for many cardinality since they're struct types
386
+ if (targetTypeInfo.typeInfo.isInlineDeclaration && cardinality > 1) {
387
+ targetTypeName = createArrayOf(targetTypeName)
388
+ }
389
+ typeName = cardinality > 1
390
+ ? toMany(targetTypeName)
391
+ : toOne(targetTypeName)
392
+ } else {
393
+ let { singular, plural } = targetTypeInfo.typeInfo.inflection
394
+ // For inline declarations, use plain type without normalization
395
+ // Note: This code path may be unreachable as inline compositions/associations are
396
+ // extracted to named classes by the visitor before type resolution occurs.
397
+ if (targetTypeInfo.typeInfo.isInlineDeclaration) {
398
+ // For inline struct types, we need to wrap in Array<> for many cardinality
399
+ let inlineType = singular.plain
400
+ if (cardinality > 1) {
401
+ inlineType = createArrayOf(inlineType)
402
+ }
403
+ typeName = cardinality > 1
404
+ ? toMany(inlineType)
405
+ : toOne(this.visitor.isSelfReference(target) ? 'this' : inlineType)
406
+ } else {
407
+ typeName = cardinality > 1
408
+ ? toMany(plural.normalised.scoped)
409
+ : toOne(this.visitor.isSelfReference(target) ? 'this' : singular.normalised.scoped)
410
+ }
411
+ file.addImport(baseDefinitions.path)
412
+ }
347
413
  }
348
414
  }
349
415
  } else {
@@ -355,19 +421,24 @@ class Resolver {
355
421
  const parent = new Path(namespace.split('.')) //t.path.getParent()
356
422
  typeInfo.inflection = this.inflect(typeInfo, namespace)
357
423
 
424
+ // Update typeName to use the normalized identifier
425
+ // This is important for delimited identifiers that get normalized (e.g., "T Y P E" -> "__T_Y_P_E")
426
+ typeName = typeInfo.inflection.singular.normalised.plain
427
+
358
428
  if (!parent.isCwd(file.path.asDirectory())) {
359
429
  file.addImport(parent)
360
430
  // prepend namespace
361
431
  typeNamespaceIdent = parent.asIdentifier()
362
432
  typeName = [typeNamespaceIdent, typeName].join('.')
363
- typeInfo.inflection.singular = [typeNamespaceIdent, typeInfo.inflection.singular].join('.')
364
- typeInfo.inflection.plural = [typeNamespaceIdent, typeInfo.inflection.plural].join('.')
433
+ typeInfo.inflection.singular = new Identifier(typeInfo.inflection.singular.plain, [typeNamespaceIdent])
434
+ typeInfo.inflection.plural = new Identifier(typeInfo.inflection.plural.plain, [typeNamespaceIdent])
365
435
  }
366
436
 
367
- if (element.type.ref?.length > 1) {
437
+ if (typeof element.type !== 'string' && element.type.ref?.length > 1) {
368
438
  const [, ...members] = element.type.ref
369
439
  const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
370
- typeName = deepRequire(typeInfo.inflection.singular, lookup)
440
+ // Use normalised.scoped to include namespace prefix (e.g., _.managed instead of managed)
441
+ typeName = deepRequire(typeInfo.inflection.singular.normalised.scoped, lookup)
371
442
  typeInfo.isDeepRequire = true
372
443
  file.addImport(baseDefinitions.path)
373
444
  }
@@ -383,10 +454,34 @@ class Resolver {
383
454
 
384
455
  if (typeInfo.isInlineDeclaration === true) {
385
456
  typeInfo.inflection = this.inflect(typeInfo)
457
+
458
+ // Handle inline enums that originate from a different entity's namespace
459
+ // (e.g., foreign keys from associations to entities with inline enum keys)
460
+ // In such cases, we need to import the namespace and prepend the identifier
461
+ const targetEntity = element._targetEntity
462
+ if (targetEntity) {
463
+ const namespace = this.resolveNamespace(targetEntity)
464
+ const parent = new Path(namespace.split('.'))
465
+
466
+ if (!parent.isCwd(file.path.asDirectory())) {
467
+ // Add import for the target entity's namespace
468
+ file.addImport(parent)
469
+ const parentNamespaceIdent = parent.asIdentifier()
470
+
471
+ // Update typeName to include the namespace
472
+ typeName = [parentNamespaceIdent, typeName].filter(Boolean).join('.')
473
+ typeInfo.inflection.singular = [parentNamespaceIdent, typeInfo.inflection.singular].filter(Boolean).join('.')
474
+ typeInfo.inflection.plural = [parentNamespaceIdent, typeInfo.inflection.plural].filter(Boolean).join('.')
475
+ }
476
+ } else {
477
+ // Update typeName from the inflection for inline types
478
+ typeName = typeInfo.inflection.typeName
479
+ }
386
480
  }
387
481
 
388
482
  // handle typeof (unless it has already been handled above)
389
- const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
483
+ // @ts-expect-error - TS disliked optional chaining of ?.ref on (ref | string), but is valid here
484
+ const target = element.target ?? element.type?.ref?.join('.') ?? element.type
390
485
  if (target && !typeInfo.isDeepRequire) {
391
486
  const { propertyAccess, scope } = this.visitor.entityRepository.getByFq(target) ?? {}
392
487
  if (scope?.length && typeInfo.inflection) {
@@ -394,23 +489,72 @@ class Resolver {
394
489
  // remove already added namespace, so the scope is added after the namespace
395
490
  // i.e. _common.Book.texts instead of Book._common.texts
396
491
  if (typeNamespaceIdent) {
397
- if (singular.startsWith(typeNamespaceIdent)) {
398
- singular = singular.substring(typeNamespaceIdent.length+1)
492
+ if (singular.plain.startsWith(typeNamespaceIdent)) {
493
+ singular.plain = singular.plain.substring(typeNamespaceIdent.length+1)
399
494
  }
400
- if (plural.startsWith(typeNamespaceIdent)) {
401
- plural = plural.substring(typeNamespaceIdent.length+1)
495
+ if (plural.plain.startsWith(typeNamespaceIdent)) {
496
+ plural.plain = plural.plain.substring(typeNamespaceIdent.length+1)
402
497
  }
403
498
  }
404
499
  // update inflections with proper prefix, e.g. Books.text, Books.texts
405
500
  typeInfo.inflection = {
406
- singular: [typeNamespaceIdent, ...scope, singular].filter(Boolean).join('.'),
407
- plural: [typeNamespaceIdent,...scope, plural].filter(Boolean).join('.')
501
+ singular: new Identifier(singular, /** @type {string[]} */([typeNamespaceIdent, ...scope].filter(Boolean))),
502
+ plural: new Identifier(plural, /** @type {string[]} */([typeNamespaceIdent, ...scope].filter(Boolean)))
408
503
  }
409
504
  } else if (propertyAccess?.length) {
410
505
  const element = target.slice(0, -propertyAccess.join('.').length - 1)
411
506
  const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
507
+ // Get the entity info to determine the proper namespace
508
+ const elementInfo = this.visitor.entityRepository.getByFq(element)
509
+ let elementName
510
+ LOG.debug(`propertyAccess: element=${element}, hasInfo=${!!elementInfo}`)
511
+ if (elementInfo) {
512
+ // Check if the element is from the same file or needs an import
513
+ const elementNamespace = this.resolveNamespace(elementInfo.namespace.parts)
514
+ const elementParent = new Path(elementNamespace.split('.'))
515
+ const elementInflection = this.inflect({
516
+ csn: elementInfo.csn,
517
+ plainName: elementInfo.withoutNamespace
518
+ }, elementNamespace)
519
+
520
+ LOG.debug(`propertyAccess with info: elementNamespace=${elementNamespace}, file=${file.path.asDirectory()}, isCwd=${elementParent.isCwd(file.path.asDirectory())}`)
521
+
522
+ if (!elementParent.isCwd(file.path.asDirectory())) {
523
+ // Different file - add import and namespace prefix
524
+ file.addImport(elementParent)
525
+ elementName = `${elementParent.asIdentifier()}.${elementInflection.singular.normalised.plain}`
526
+ } else {
527
+ // Same file - use normalized name with scope if needed
528
+ elementName = elementInflection.singular.normalised.plain
529
+ }
530
+ } else {
531
+ // Fallback: check if element is in CSN and determine its namespace
532
+ const elementCsn = this.csn.definitions[element]
533
+ LOG.debug(`propertyAccess fallback: elementCsn=${!!elementCsn}`)
534
+ if (elementCsn) {
535
+ // Element exists in CSN, determine its namespace
536
+ const elementFqParts = element.split('.')
537
+ const elementNs = elementFqParts.slice(0, -1).join('.')
538
+ const currentFileNs = file.path.parts.join('.')
539
+
540
+ LOG.debug(`propertyAccess CSN: elementNs='${elementNs}', currentFileNs='${currentFileNs}'`)
541
+
542
+ if (elementNs !== currentFileNs) {
543
+ // Different namespace - need prefix
544
+ const elementParent = new Path(elementNs.split('.').filter(Boolean))
545
+ file.addImport(elementParent)
546
+ elementName = `${elementParent.asIdentifier()}.${new Identifier(util.singular4(element)).normalised.plain}`
547
+ } else {
548
+ // Same namespace
549
+ elementName = new Identifier(util.singular4(element)).normalised.plain
550
+ }
551
+ } else {
552
+ // Element not in CSN, just normalize
553
+ elementName = new Identifier(util.singular4(element)).normalised.plain
554
+ }
555
+ }
412
556
  // singular, as we have to access the property of the entity
413
- typeName = deepRequire(util.singular4(element)) + access
557
+ typeName = deepRequire(elementName) + access
414
558
  typeInfo.isDeepRequire = true
415
559
  }
416
560
  }
@@ -418,8 +562,8 @@ class Resolver {
418
562
  // add fallback inflection. Mainly needed for array-of with builtin types.
419
563
  // (array-of relies on inflection being present, which is not the case in builtin)
420
564
  typeInfo.inflection ??= {
421
- singular: typeName,
422
- plural: typeName
565
+ singular: new Identifier(typeName),
566
+ plural: new Identifier(typeName)
423
567
  }
424
568
 
425
569
  if (element.key === true) {
@@ -427,7 +571,10 @@ class Resolver {
427
571
  }
428
572
 
429
573
  // FIXME: typeName could probably just become part of typeInfo
430
- return { typeName, typeInfo }
574
+ return /** @type {ResolveAndRequireInfo}*/({ // can cast, as we gave an .inflection above
575
+ typeName,
576
+ typeInfo
577
+ })
431
578
  }
432
579
 
433
580
  /**
@@ -479,7 +626,7 @@ class Resolver {
479
626
  // with an already resolved type. In that case, just return the type we have.
480
627
  // type guard check purely to satisfy return statement
481
628
  /**
482
- * @param {any} e - the element to check
629
+ * @param {unknown} e - the element to check
483
630
  * @returns {e is TypeResolveInfo}
484
631
  */
485
632
  const isBuiltin = e => Object.hasOwn(e ?? {}, 'isBuiltin')
@@ -487,6 +634,8 @@ class Resolver {
487
634
 
488
635
  const cardinality = getMaxCardinality(element)
489
636
 
637
+ // FIXME while it may be tempting to add the missing plainName here, we do have "typename ?? ..."
638
+ // expressions which will then fail. We should instead use a more reliable way to determine plainName later on.
490
639
  /** @type {TypeResolveInfo} */
491
640
  const result = {
492
641
  isBuiltin: false, // will be rectified in the corresponding handlers, if needed
@@ -503,9 +652,10 @@ class Resolver {
503
652
  result.isNotNull ||= element.kind === 'param' && this.isMandatory(element)
504
653
 
505
654
 
506
- if (element?.type === undefined) {
655
+ if (element?.type === undefined && !element?.items) {
507
656
  // "fallback" type "empty object". May be overriden via #resolveInlineDeclarationType
508
657
  // later on with an inline declaration
658
+ // NOTE: Arrays have no type property, only items, so we exclude them here
509
659
  result.type = '{}'
510
660
  result.isInlineDeclaration = true
511
661
  } else if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
@@ -523,8 +673,10 @@ class Resolver {
523
673
  result.isInlineDeclaration = true
524
674
  // we use the singular as the initial declaration of these enums takes place
525
675
  // while defining the singular class. Which therefore uses the singular over the plural name.
526
- const cleanEntityName = util.singular4(element.parent, true)
527
- const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
676
+ // Normalize both entity and property names to handle delimited identifiers
677
+ const cleanEntityName = new Identifier(util.singular4(element.parent, true)).normalised.plain
678
+ const cleanPropertyName = new Identifier(element.name).normalised.plain
679
+ const enumName = propertyToInlineEnumName(cleanEntityName, cleanPropertyName)
528
680
  result.type = enumName
529
681
  result.plainName = enumName
530
682
  } else {
@@ -537,7 +689,8 @@ class Resolver {
537
689
  // stringifyEnumType(csnToEnumPairs(element))
538
690
  this.resolveTypeName(element.type, result)
539
691
  }
540
- } else {
692
+ } else if (element?.type !== undefined) {
693
+ // Only resolve the type if it exists (not an array or empty inline declaration)
541
694
  this.resolvePotentialReferenceType(element.type, result, file)
542
695
  }
543
696
 
@@ -547,7 +700,7 @@ class Resolver {
547
700
  // TODO: re-implement this line once {element.notNull} will be provided for array-like elements
548
701
  result.isNotNull = true
549
702
  result.isBuiltin = true
550
- this.resolveType(element.items, file)
703
+ result.itemsType = this.resolveType(element.items, file)
551
704
  //delete element.items
552
705
  } else if (!result.isBuiltin && !isExternal(element) && element?.elements && (options?.forceInlineStructs || !element?.type)) {
553
706
  // explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
@@ -567,14 +720,16 @@ class Resolver {
567
720
  * Resolves an inline declaration of a type.
568
721
  * We can encounter declarations like:
569
722
  *
723
+ * ```
570
724
  * record : array of {
571
725
  * column : String;
572
726
  * data : String;
573
727
  * }
728
+ * ```
574
729
  *
575
730
  * These have to be resolved to a new type.
576
731
  * @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
577
- * @param {TypeResolveInfo} into - @see resolveType()
732
+ * @param {TypeResolveInfo} into - {@link resolveType}
578
733
  * @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
579
734
  * @param {TypeResolveOptions} [options] - resolver options
580
735
  * This is important to correctly detect when a field in the inline declaration is referencing
@@ -591,7 +746,6 @@ class Resolver {
591
746
  * @param {SourceFile} file - only needed as we may call #resolveInlineDeclarationType from here. Will be expelled at some point.
592
747
  */
593
748
  resolvePotentialReferenceType(val, into, file) {
594
- // FIXME: get rid of file parameter! it is only used to pass to #resolveInlineDeclarationType
595
749
  if (val.elements) {
596
750
  this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
597
751
  } else if (val.constructor === Object && 'ref' in val) {
@@ -607,11 +761,12 @@ class Resolver {
607
761
  * Attempts to resolve a string to a type.
608
762
  * String is supposed to refer to either a builtin type
609
763
  * or any type defined in CSN.
610
- * @param {string} t - fully qualified type, like cds.String, or a.b.c.d.Foo
764
+ * @param {Identifier | string} t - fully qualified type, like cds.String, or a.b.c.d.Foo
611
765
  * @param {TypeResolveInfo} [into] - optional dictionary to fill by reference, see resolveType()
612
- * @returns @see resolveType
766
+ * @returns {ReturnType<Resolver['resolveType']>}
613
767
  */
614
768
  resolveTypeName(t, into) {
769
+ if (typeof t !== 'string') t = t.plain
615
770
  const result = into ?? /** @type {TypeResolveInfo} */({})
616
771
  const path = t.split('.')
617
772
  const builtin = this.builtinResolver.resolveBuiltin(path)
package/lib/typedefs.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { Identifier } from './components/identifier'
2
+
1
3
  export module resolver {
2
4
  type ref = {
3
5
  ref: string[],
@@ -31,6 +33,7 @@ export module resolver {
31
33
  '@odata.draft.enabled'?: boolean // custom!
32
34
  _unresolved?: boolean
33
35
  isRefNotNull?: boolean // custom!
36
+ _targetEntity?: string // custom! Name of the target entity when this element is a composition or association
34
37
  }
35
38
 
36
39
  export type OperationCSN = EntityCSN & {
@@ -95,9 +98,9 @@ export module resolver {
95
98
  *
96
99
  * They only exist in the original defined form in the CSN and LinkedCSN but not in the compiled
97
100
  * OData or SQL models (i.e. `cds.compile(..).for.odata()`).
98
- *
101
+ *
99
102
  * Therefore they need to be flattened down like inline structs.
100
- *
103
+ *
101
104
  * ```cds
102
105
  * // model.cds
103
106
  * type Adress {
@@ -109,12 +112,12 @@ export module resolver {
109
112
  * address: Adress
110
113
  * }
111
114
  * ```
112
- *
115
+ *
113
116
  * // service.js
114
117
  * ```js
115
118
  * const {title, address_street, address_zipCode} = await SELECT.from(Persons);
116
119
  * ```
117
- *
120
+ *
118
121
  */
119
122
  forceInlineStructs?: boolean
120
123
  }
@@ -145,12 +148,12 @@ export module util {
145
148
  export module visitor {
146
149
  export type Inflection = {
147
150
  typeName?: string,
148
- singular: string,
149
- plural: string
151
+ singular: Identifier,
152
+ plural: Identifier
150
153
  }
151
154
 
152
155
  export type Context = {
153
- entity: string
156
+ entity: Identifier
154
157
  }
155
158
 
156
159
  export type ParamInfo = {
@@ -213,6 +216,8 @@ export module config {
213
216
  * `legacyBinaryTypes = true` -> Binary and LargeBinary are generated as `string` and a union type respectively
214
217
  */
215
218
  legacyBinaryTypes: boolean
219
+ /** `brandedPrimitiveTypes = true` -> all generated primitive types will be branded */
220
+ brandedPrimitiveTypes: boolean
216
221
  }
217
222
  }
218
223
 
package/lib/util.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('node:fs')
5
5
  const path = require('node:path')
6
+ const { LOG } = require('./logging')
6
7
 
7
8
  // inflection functions are stolen from github/cap/dev/blob/main/etc/inflect.js
8
9
 
@@ -13,7 +14,7 @@ if (process.version.startsWith('v14')) {
13
14
  Object.hasOwn = Object.hasOwn ?? ((obj, attr) => Boolean(obj && obj.hasOwnProperty(attr)))
14
15
  }
15
16
 
16
- const last = /\w+$/
17
+ const last = /[^.]+$/
17
18
 
18
19
  const annotations = /** @type {const} */ ({
19
20
  singular: ['@singular'],
@@ -61,9 +62,10 @@ const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOw
61
62
  * unlocalize("{i18n>Foo}") -> "Foo"
62
63
  * @param {string} name - the entity name (singular or plural).
63
64
  * @returns {string} the name without localisation syntax or untouched.
64
- * @deprecated we have dropped this feature altogether, users specify custom names via @singular/@plural now
65
+ * @deprecated we have dropped this feature altogether, users specify custom names via `@singular`/`@plural` now
65
66
  */
66
67
  const unlocalize = name => {
68
+ LOG.warn('util.unlocalize is deprecated and will be removed in future versions.')
67
69
  const match = name.match(/\{i18n>(.*)\}/)
68
70
  return match ? match[1] : name
69
71
  }