@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
|
@@ -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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
362
|
+
const getTarget = (el, property) => typeof el[property] === 'string'
|
|
363
|
+
? { type: el[property] }
|
|
364
|
+
: el[property]
|
|
325
365
|
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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 =
|
|
364
|
-
typeInfo.inflection.plural =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
407
|
-
plural: [typeNamespaceIdent
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
527
|
-
const
|
|
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 - @
|
|
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
|
|
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:
|
|
149
|
-
plural:
|
|
151
|
+
singular: Identifier,
|
|
152
|
+
plural: Identifier
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
export type Context = {
|
|
153
|
-
entity:
|
|
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 =
|
|
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
|
|
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
|
}
|