@cap-js/cds-typer 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,11 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ### Added
7
+ ### Added
8
+ ### Changed
9
+ ### Deprecated
10
+ ### Removed
11
+ ### Fixed
12
+ ### Security
13
+
14
+ ## [0.32.0] - 2025-01-14
15
+
16
+ ### Added
17
+ - dedicated classes for inline compositions
18
+ - dedicated text-classes for entities with `localized` elements
19
+
8
20
  ### Changed
21
+ - prefixed builtin types like `Promise` and `Record` with `globalThis.`, to allow using names of builtin types for entities without collisions
22
+ - bumped peer-dependency to `@cap-js/cds-types` to `>=0.9`
9
23
  ### Deprecated
10
24
  ### Removed
11
25
  ### Fixed
26
+ - referencing another entity's property of type `cds.String` in an enum will now properly quote the generated values
12
27
  ### Security
13
28
 
14
29
  ## [0.31.0] - 2024-12-16
@@ -67,7 +67,7 @@ function printEnum(buffer, name, kvs, options = {}, doc=[]) {
67
67
  * Converts a CSN type describing an enum into a list of kv-pairs.
68
68
  * Values from CSN are unwrapped from their `.val` structure and
69
69
  * will fall back to the key if no value is provided.
70
- * @param {import('../typedefs').resolver.EnumCSN} enumCsn - the CSN type describing the enum
70
+ * @param {import('../typedefs').resolver.EnumCSN & { resolvedType?: string }} enumCsn - the CSN type describing the enum
71
71
  * @param {{unwrapVals: boolean} | {}} options - if `unwrapVals` is passed,
72
72
  * then the CSN structure `{val:x}` is flattened to just `x`.
73
73
  * Retaining `val` is closer to the actual CSN structure and should be used where we want
@@ -81,10 +81,10 @@ function printEnum(buffer, name, kvs, options = {}, doc=[]) {
81
81
  * csnToEnumPairs(csn, {unwrapVals: false}) // -> [['X', {val:'a'}], ['Y': {val:'b'}], ['Z':'Z']]
82
82
  * ```
83
83
  */
84
- const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
84
+ const csnToEnumPairs = ({enum: enm, type, resolvedType}, options = {}) => {
85
85
  const actualOptions = {...{unwrapVals: true}, ...options}
86
86
  return Object.entries(enm).map(([k, v]) => {
87
- const val = enumVal(k, v.val, type)
87
+ const val = enumVal(k, v.val, resolvedType ?? type) // if type is a ref, prefer the resolvedType to catch references to cds.Strings
88
88
  return [k, (actualOptions.unwrapVals ? val : { val })]
89
89
  })
90
90
  }
@@ -1,3 +1,3 @@
1
1
  module.exports = {
2
- empty: 'Record<never, never>'
2
+ empty: 'globalThis.Record<never, never>'
3
3
  }
package/lib/csn.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { LOG } = require('./logging')
2
+ const { annotations } = require('./util')
2
3
 
3
4
  const DRAFT_ENABLED_ANNO = '@odata.draft.enabled'
4
5
  /** @type {string[]} */
@@ -292,6 +293,30 @@ function propagateForeignKeys(csn) {
292
293
  }
293
294
  }
294
295
 
296
+ /**
297
+ * Clears "correct" singular/plural annotations from inferred model
298
+ * copies the ones from the xtended model.
299
+ *
300
+ * This is done to prevent potential duplicate class names because of annotation propagation.
301
+ * @param {{inferred: CSN, xtended: CSN}} csn - CSN models
302
+ */
303
+ function propagateInflectionAnnotations(csn) {
304
+ const singularAnno = annotations.singular[0]
305
+ const pluralAnno = annotations.plural[0]
306
+ for (const [name, def] of Object.entries(csn.inferred.definitions)) {
307
+ const xtendedDef = csn.xtended.definitions[name]
308
+ // we keep the annotations from definition specific to the inferred model (e.g. inline compositions)
309
+ if (!xtendedDef) continue
310
+
311
+ // clear annotations from inferred definition
312
+ if (Object.hasOwn(def, singularAnno)) delete def[singularAnno]
313
+ if (Object.hasOwn(def, pluralAnno)) delete def[pluralAnno]
314
+ // transfer annotation from xtended if existing
315
+ if (Object.hasOwn(xtendedDef, singularAnno)) def[singularAnno] = xtendedDef[singularAnno]
316
+ if (Object.hasOwn(xtendedDef, pluralAnno)) def[pluralAnno] = xtendedDef[pluralAnno]
317
+ }
318
+ }
319
+
295
320
  /**
296
321
  * @param {EntityCSN} entity - the entity
297
322
  */
@@ -311,6 +336,25 @@ const getProjectionAliases = entity => {
311
336
  return { aliases, all }
312
337
  }
313
338
 
339
+ /**
340
+ * Heuristic way of looking up a reference type.
341
+ * We currently only support up to two segments,
342
+ * the first referring to the entity, a possible second
343
+ * referring to an element of the entity.
344
+ * @param {CSN} csn - CSN
345
+ * @param {string[]} ref - reference
346
+ * @returns {EntityCSN}
347
+ */
348
+ function lookUpRefType (csn, ref) {
349
+ if (ref.length > 2) throw new Error(`Unsupported reference type ${ref.join('.')} with ${ref.length} segments. Please report this error.`)
350
+ /** @type {EntityCSN | undefined} */
351
+ let result = csn.definitions[ref[0]] // entity
352
+ if (ref.length === 1) return result
353
+ result = result?.elements?.[ref[1]] // property
354
+ if (!result) throw new Error(`Failed to look up reference type ${ref.join('.')}`)
355
+ return result
356
+ }
357
+
314
358
  module.exports = {
315
359
  collectDraftEnabledEntities,
316
360
  isView,
@@ -326,5 +370,7 @@ module.exports = {
326
370
  getProjectionAliases,
327
371
  getViewTarget,
328
372
  propagateForeignKeys,
329
- isCsnAny
373
+ propagateInflectionAnnotations,
374
+ isCsnAny,
375
+ lookUpRefType
330
376
  }
package/lib/file.js CHANGED
@@ -257,6 +257,7 @@ class SourceFile extends File {
257
257
  if (!(name in this.namespaces)) {
258
258
  const buffer = new Buffer()
259
259
  buffer.closed = false
260
+ buffer.namespace = name
260
261
  buffer.add(`export namespace ${name} {`)
261
262
  buffer.indent()
262
263
  this.namespaces[name] = buffer
@@ -286,6 +287,8 @@ class SourceFile extends File {
286
287
  * @param {string} entityFqName - name of the entity the enum is attached to with namespace
287
288
  * @param {string} propertyName - property to which the enum is attached.
288
289
  * @param {[string, string][]} kvs - list of key-value pairs
290
+ * @param {Buffer} [buffer] - if buffer is of subnamespace the enum will be added there,
291
+ * otherwise to the inline enums of the file
289
292
  * @param {string[]} doc - the enum docs
290
293
  * If given, the enum is considered to be an inline definition of an enum.
291
294
  * If not, it is considered to be regular, named enum.
@@ -310,16 +313,32 @@ class SourceFile extends File {
310
313
  * }
311
314
  * ```
312
315
  */
313
- addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, doc=[]) {
316
+ addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, buffer, doc=[]) {
317
+ const namespacedEntity = [buffer?.namespace, entityCleanName].filter(Boolean).join('.')
314
318
  this.enums.data.push({
315
- name: `${entityCleanName}.${propertyName}`,
319
+ name: `${namespacedEntity}.${propertyName}`,
316
320
  property: propertyName,
317
321
  kvs,
318
- fq: `${entityCleanName}.${propertyName}`
322
+ fq: `${namespacedEntity}.${propertyName}`
319
323
  })
320
- const entityProxy = this.entityProxies[entityCleanName] ?? (this.entityProxies[entityCleanName] = [])
324
+ const entityProxy = this.entityProxies[namespacedEntity] ?? (this.entityProxies[namespacedEntity] = [])
321
325
  entityProxy.push(propertyName)
322
- printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
326
+
327
+ // REVISIT: find a better way to do this???
328
+ const printEnumToBuffer = (/** @type {Buffer} */buffer) => printEnum(buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
329
+
330
+ if (buffer?.namespace) {
331
+ const tempBuffer = new Buffer()
332
+ // we want to put the enums on class level
333
+ tempBuffer.indent()
334
+ printEnumToBuffer(tempBuffer)
335
+
336
+ // we want to write the enums at the beginning of the namespace
337
+ const [first,...rest] = buffer.parts
338
+ buffer.parts = [first, ...tempBuffer.parts, ...rest]
339
+ } else {
340
+ printEnumToBuffer(this.inlineEnums.buffer)
341
+ }
323
342
  }
324
343
 
325
344
  /**
@@ -490,12 +509,15 @@ class SourceFile extends File {
490
509
 
491
510
  return {
492
511
  singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
493
- pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
512
+ pluralRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: false }})`,
494
513
  }
495
514
  } else {
515
+ // standard entity: csn.Books
516
+ // inline entity: csn['Books.texts']
517
+ const csnAccess = original.includes('.') ? `csn['${original}']` : `csn.${original}`
496
518
  return {
497
- singularRhs: `{ is_singular: true, __proto__: csn.${original} }`,
498
- pluralRhs: `csn.${original}`
519
+ singularRhs: `{ is_singular: true, __proto__: ${csnAccess} }`,
520
+ pluralRhs: csnAccess
499
521
  }
500
522
  }
501
523
  }
@@ -589,6 +611,11 @@ class Buffer {
589
611
  * @type {boolean}
590
612
  */
591
613
  this.closed = false
614
+ /**
615
+ * Required for inline enums of inline compositions or text entities
616
+ * @type {string | undefined}
617
+ */
618
+ this.namespace = undefined
592
619
  }
593
620
 
594
621
  /**
@@ -84,12 +84,14 @@ class ESMPrinter extends JavaScriptPrinter {
84
84
 
85
85
  /** @type {JavaScriptPrinter['printDeconstructedImport']} */
86
86
  printDeconstructedImport (imports, from) {
87
- return `import { ${imports.join(', ')} } from '${from}'`
87
+ return `import { ${imports.join(', ')} } from '${from}/index.js'`
88
88
  }
89
89
 
90
90
  /** @type {JavaScriptPrinter['printExport']} */
91
91
  printExport (name, value) {
92
- return `export const ${name} = ${value}`
92
+ return name.includes('.')
93
+ ? `${name} = ${value}`
94
+ : `export const ${name} = ${value}`
93
95
  }
94
96
 
95
97
  /** @type {JavaScriptPrinter['printDefaultExport']} */
@@ -103,7 +103,7 @@ const createIntersectionOf = (...types) => types.join(' & ')
103
103
  * createPromiseOf('string') // -> 'Promise<string>'
104
104
  * ```
105
105
  */
106
- const createPromiseOf = t => `Promise<${t}>`
106
+ const createPromiseOf = t => `globalThis.Promise<${t}>`
107
107
 
108
108
  /**
109
109
  * Wraps type into a deep require (removes all posibilities of undefined recursively).
@@ -61,6 +61,22 @@ class EntityInfo {
61
61
  /** @type {import('../typedefs').resolver.EntityCSN | undefined} */
62
62
  #csn
63
63
 
64
+ /** @type {Set<string> | undefined} */
65
+ #inheritedElements
66
+
67
+ /** @returns set of inherited elements (e.g. ID of aspect cuid) */
68
+ get inheritedElements() {
69
+ if (this.#inheritedElements) return this.#inheritedElements
70
+ this.#inheritedElements = new Set()
71
+ for (const parentName of this.csn.includes ?? []) {
72
+ const parent = this.#repository.getByFq(parentName)
73
+ for (const element of Object.keys(parent?.csn?.elements ?? {})) {
74
+ this.#inheritedElements.add(element)
75
+ }
76
+ }
77
+ return this.#inheritedElements
78
+ }
79
+
64
80
  /** @returns the **inferred** csn for this entity. */
65
81
  get csn () {
66
82
  return this.#csn ??= this.#resolver.csn.definitions[this.fullyQualifiedName]
@@ -24,7 +24,7 @@ const { configuration } = require('../config')
24
24
  /** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
25
25
 
26
26
  class Resolver {
27
- get csn() { return this.visitor.csn.inferred }
27
+ get csn() { return this.visitor.csn }
28
28
 
29
29
  /** @param {Visitor} visitor - the visitor */
30
30
  constructor(visitor) {
@@ -165,11 +165,13 @@ class Resolver {
165
165
  */
166
166
  const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
167
167
 
168
- const defs = this.visitor.csn.inferred.definitions
168
+ const defs = this.visitor.csn.definitions
169
+
170
+ // check if name is already an entity, then we do not have a property access, but a nested entity
171
+ if (defs[p]?.kind === 'entity') return []
172
+
169
173
  // assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
170
- /** @type {string} */
171
- // @ts-expect-error - nope, we know there is at least one element
172
- let qualifier = parts.shift()
174
+ let qualifier = /** @type {string} */ (parts.shift())
173
175
  // find first entity from left (Entity1)
174
176
  while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
175
177
  qualifier += `.${parts.shift()}`
@@ -240,6 +242,8 @@ class Resolver {
240
242
  } else {
241
243
  // TODO: make sure the resolution still works. Currently, we only cut off the namespace!
242
244
  plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName
245
+ // remove leading entity name
246
+ if (plural.includes('.')) plural = last(plural)
243
247
  singular = util.getSingularAnnotation(typeInfo.csn) ?? util.singular4(typeInfo.csn, true) // util.singular4(typeInfo.csn, true) // can not use `plural` to honor possible @singular annotation
244
248
 
245
249
  // don't slice off namespace if it isn't part of the inflected name.
@@ -311,18 +315,6 @@ class Resolver {
311
315
  } else {
312
316
  let { singular, plural } = targetTypeInfo.typeInfo.inflection
313
317
 
314
- // FIXME: super hack!!
315
- // Inflection currently does not retain the scope of the entity.
316
- // But we can't just fix it in inflection(...), as that would break several other things
317
- // So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
318
- if (target.type) {
319
- const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
320
- const scope = untangled.scope.join('.')
321
- if (scope && !singular.startsWith(scope)) {
322
- singular = `${scope}.${singular}`
323
- }
324
- }
325
-
326
318
  typeName = cardinality > 1
327
319
  ? toMany(plural)
328
320
  : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
@@ -370,8 +362,14 @@ class Resolver {
370
362
  // handle typeof (unless it has already been handled above)
371
363
  const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
372
364
  if (target && !typeInfo.isDeepRequire) {
373
- const { propertyAccess } = this.visitor.entityRepository.getByFq(target) ?? {}
374
- if (propertyAccess?.length) {
365
+ const { propertyAccess, scope } = this.visitor.entityRepository.getByFq(target) ?? {}
366
+ if (scope?.length) {
367
+ // update inflections with proper prefix, e.g. Books.text, Books.texts
368
+ typeInfo.inflection = {
369
+ singular: [...scope, typeInfo.inflection?.singular].join('.'),
370
+ plural: [...scope, typeInfo.inflection?.plural].join('.')
371
+ }
372
+ } else if (propertyAccess?.length) {
375
373
  const element = target.slice(0, -propertyAccess.join('.').length - 1)
376
374
  const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
377
375
  // singular, as we have to access the property of the entity
@@ -452,6 +450,7 @@ class Resolver {
452
450
 
453
451
  const cardinality = getMaxCardinality(element)
454
452
 
453
+ /** @type {TypeResolveInfo} */
455
454
  const result = {
456
455
  isBuiltin: false, // will be rectified in the corresponding handlers, if needed
457
456
  isInlineDeclaration: false,
@@ -569,7 +568,7 @@ class Resolver {
569
568
  * @returns @see resolveType
570
569
  */
571
570
  resolveTypeName(t, into) {
572
- const result = into ?? {}
571
+ const result = into ?? /** @type {TypeResolveInfo} */({})
573
572
  const path = t.split('.')
574
573
  const builtin = this.builtinResolver.resolveBuiltin(path)
575
574
  if (builtin === undefined) {
package/lib/typedefs.d.ts CHANGED
@@ -13,7 +13,7 @@ export module resolver {
13
13
  compositions?: { target: string }[]
14
14
  doc?: string,
15
15
  elements?: { [key: string]: EntityCSN }
16
- key?: string // custom!!
16
+ key?: boolean // custom!!
17
17
  keys?: { [key:string]: any }
18
18
  kind: string,
19
19
  includes?: string[]
@@ -25,6 +25,8 @@ export module resolver {
25
25
  target?: string,
26
26
  type: string | ref,
27
27
  name: string,
28
+ '@singular'?: string,
29
+ '@plural'?: string,
28
30
  '@odata.draft.enabled'?: boolean // custom!
29
31
  _unresolved?: boolean
30
32
  isRefNotNull?: boolean // custom!
@@ -46,7 +48,8 @@ export module resolver {
46
48
 
47
49
 
48
50
  export type EnumCSN = EntityCSN & {
49
- enum: {[key:name]: string}
51
+ enum: {[key:name]: string},
52
+ resolvedType?: string // custom property! When .type points to a ref, the visitor will resolve the ref into this property
50
53
  }
51
54
 
52
55
  export type CSN = {
package/lib/util.js CHANGED
@@ -15,10 +15,10 @@ if (process.version.startsWith('v14')) {
15
15
 
16
16
  const last = /\w+$/
17
17
 
18
- const annotations = {
18
+ const annotations = /** @type {const} */ ({
19
19
  singular: ['@singular'],
20
20
  plural: ['@plural'],
21
- }
21
+ })
22
22
 
23
23
  /**
24
24
  * Converts a camelCase string to snake_case.
package/lib/visitor.js CHANGED
@@ -1,8 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const util = require('./util')
4
-
5
- const { isView, isUnresolved, propagateForeignKeys, collectDraftEnabledEntities, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum, isEntity } = require('./csn')
3
+ const { propagateForeignKeys, propagateInflectionAnnotations, collectDraftEnabledEntities, isDraftEnabled, isType, getMaxCardinality, isViewOrProjection, isEnum, isEntity, lookUpRefType } = require('./csn')
6
4
  // eslint-disable-next-line no-unused-vars
7
5
  const { SourceFile, FileRepository, Buffer, Path } = require('./file')
8
6
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
@@ -44,11 +42,12 @@ class Visitor {
44
42
  * @param {{xtended: CSN, inferred: CSN}} csn - root CSN
45
43
  */
46
44
  constructor(csn) {
47
- propagateForeignKeys(csn.xtended)
48
45
  propagateForeignKeys(csn.inferred)
49
- // has to be executed on the inferred model as autoexposed entities are not included in the xtended csn
46
+ propagateInflectionAnnotations(csn)
50
47
  collectDraftEnabledEntities(csn.inferred)
51
- this.csn = csn
48
+
49
+ // xtendend csn not required after this point -> continue with inferred
50
+ this.csn = csn.inferred
52
51
 
53
52
  /** @type {Context[]} **/
54
53
  this.contexts = []
@@ -74,41 +73,8 @@ class Visitor {
74
73
  * Visits all definitions within the CSN definitions.
75
74
  */
76
75
  visitDefinitions() {
77
- for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
78
- if (isView(entity)) {
79
- this.visitEntity(name, this.csn.inferred.definitions[name])
80
- } else if (isProjection(entity) || !isUnresolved(entity)) {
81
- this.visitEntity(name, entity)
82
- } else {
83
- LOG.warn(`Skipping unresolved entity: ${name}`)
84
- }
85
- }
86
- // FIXME: optimise
87
- // We are currently working with two flavours of CSN:
88
- // xtended, as it is as close as possible to an OOP class hierarchy
89
- // inferred, as it contains information missing in xtended
90
- // This is less than optimal and has to be revisited at some point!
91
- const handledKeys = new Set(Object.keys(this.csn.xtended.definitions))
92
- // we are looking for autoexposed entities in services
93
- const missing = Object.entries(this.csn.inferred.definitions).filter(([key]) => !key.endsWith('.texts') &&!handledKeys.has(key))
94
- for (const [name, entity] of missing) {
95
- // instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
96
- // The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
97
- if (entity.projection) {
98
- const targetName = entity.projection.from.ref[0]
99
- // FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
100
- // this should be revisted once we settle on a single flavour.
101
- const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
102
- if (target.kind !== 'type') {
103
- // skip if the target is a property, like in:
104
- // books: Association to many Author.books ...
105
- // as this would result in a type definition that
106
- // name-clashes with the actual declaration of Author
107
- this.visitEntity(name, target)
108
- }
109
- } else {
110
- LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
111
- }
76
+ for (const [name, entity] of Object.entries(this.csn.definitions)) {
77
+ this.visitEntity(name, entity)
112
78
  }
113
79
  }
114
80
 
@@ -119,15 +85,8 @@ class Visitor {
119
85
  * @returns {[string, object][]} array of key name and key element pairs
120
86
  */
121
87
  #keys(fq) {
122
- // FIXME: this is actually pretty bad, as not only have to propagate keys through
123
- // both flavours of CSN (see constructor), but we are now also collecting them from
124
- // both flavours and deduplicating them.
125
- // xtended contains keys that have been inherited from parents
126
- // inferred contains keys from queried entities (thing `entity Foo as select from Bar`, where Bar has keys)
127
- // So we currently need them both.
128
88
  return Object.entries({
129
- ...this.csn.inferred.definitions[fq]?.keys ?? {},
130
- ...this.csn.xtended.definitions[fq]?.keys ?? {}
89
+ ...this.csn.definitions[fq]?.keys ?? {}
131
90
  })
132
91
  }
133
92
 
@@ -233,7 +192,7 @@ class Visitor {
233
192
  // FIXME: replace with resolution/entity::asIdentifier
234
193
  const toLocalIdent = ({ns, clean, fq}) => {
235
194
  // types are not inflected, so don't change those to singular
236
- const csn = this.csn.inferred.definitions[fq]
195
+ const csn = this.csn.definitions[fq]
237
196
  const ident = isType(csn)
238
197
  ? clean
239
198
  : this.resolver.inflect({csn, plainName: clean}).singular
@@ -263,6 +222,7 @@ class Visitor {
263
222
  .reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
264
223
  .reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
265
224
 
225
+ const inheritedElements = !isViewOrProjection(entity) ? info.inheritedElements : null
266
226
  this.contexts.push({ entity: fq })
267
227
 
268
228
  // CLASS ASPECT
@@ -274,10 +234,7 @@ class Visitor {
274
234
  const resolverOptions = { forceInlineStructs: isEntity(entity) && configuration.inlineDeclarations === 'flat'}
275
235
 
276
236
  for (let [ename, element] of Object.entries(entity.elements ?? [])) {
277
- if (element.target && /\.texts?/.test(element.target)) {
278
- LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
279
- continue
280
- }
237
+ if (inheritedElements?.has(ename)) continue
281
238
  this.visitElement({name: ename, element, file, buffer, resolverOptions})
282
239
 
283
240
  // make foreign keys explicit
@@ -292,7 +249,8 @@ class Visitor {
292
249
  LOG.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${fq}. But a property of that name is already defined explicitly. Consider renaming that property.`)
293
250
  } else {
294
251
  const kelement = Object.assign(Object.create(originalKeyElement), {
295
- isRefNotNull: !!element.notNull || !!element.key
252
+ isRefNotNull: !!element.notNull || !!element.key,
253
+ key: element.key
296
254
  })
297
255
  this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})
298
256
  }
@@ -301,7 +259,7 @@ class Visitor {
301
259
  }
302
260
 
303
261
  // store inline enums for later handling, as they have to go into one common "static elements" wrapper
304
- if (isInlineEnumType(element, this.csn.xtended)) {
262
+ if (isInlineEnumType(element, this.csn)) {
305
263
  enums.push(element)
306
264
  }
307
265
  }
@@ -314,7 +272,10 @@ class Visitor {
314
272
  initialiser: propertyToInlineEnumName(clean, e.name),
315
273
  isStatic: true,
316
274
  }))
317
- file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
275
+ if (typeof e?.type !== 'string' && e?.type?.ref) {
276
+ e.resolvedType = /** @type {string} */(lookUpRefType(this.csn, e.type.ref)?.type)
277
+ }
278
+ file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
318
279
  }
319
280
 
320
281
  if ('kind' in entity) {
@@ -357,7 +318,7 @@ class Visitor {
357
318
  */
358
319
  #printEntity(fq, entity) {
359
320
  const info = this.entityRepository.getByFqOrThrow(fq)
360
- const { namespace: ns, entityName: clean, inflection } = info
321
+ const { namespace: ns, entityName: clean, inflection, scope } = info
361
322
  const file = this.fileRepository.getNamespaceFile(ns)
362
323
  let { singular, plural } = inflection
363
324
 
@@ -373,7 +334,7 @@ class Visitor {
373
334
  // as types are not inflected, their singular will always clash and there is also no plural for them anyway -> skip
374
335
  // if the user defined their entities in singular form we would also have a false positive here -> skip
375
336
  const namespacedSingular = `${ns.asNamespace()}.${singular}`
376
- if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.xtended.definitions) {
337
+ if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.definitions) {
377
338
  LOG.error(
378
339
  `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.`
379
340
  )
@@ -386,20 +347,15 @@ class Visitor {
386
347
  ? file.getSubNamespace(this.resolver.trimNamespace(parent.name))
387
348
  : file.classes
388
349
 
389
- // we can't just use "singular" here, as it may have the subnamespace removed:
390
- // "Books.text" is just "text" in "singular". Within the inflected exports we need
391
- // to have Books.texts = Books.text, so we derive the singular once more without cutting off the ns.
392
- // Directly deriving it from the plural makes sure we retain any parent namespaces of kind "entity",
393
- // which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
394
- // edge case: @singular annotation present. singular4 will take care of that.
395
- file.addInflection(util.singular4(entity, true), plural, clean)
396
-
397
- // in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
398
- const target = isProjection(entity) || isView(entity)
399
- ? this.csn.inferred.definitions[fq]
400
- : entity
350
+ if (scope?.length > 0) {
351
+ /** @param {string} n - name of entity */
352
+ const scoped = n => [...scope, n].join('.')
353
+ file.addInflection(scoped(singular), scoped(plural), scoped(clean))
354
+ } else {
355
+ file.addInflection(singular, plural, clean)
356
+ }
401
357
 
402
- this.#aspectify(fq, target, buffer, { cleanName: singular })
358
+ this.#aspectify(fq, entity, buffer, { cleanName: singular })
403
359
 
404
360
  buffer.add(overrideNameProperty(singular, entity.name))
405
361
  buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
@@ -516,7 +472,7 @@ class Visitor {
516
472
  if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
517
473
  file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
518
474
  } else {
519
- const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
475
+ const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.definitions[type?.type])
520
476
  // alias
521
477
  file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
522
478
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.31.0",
3
+ "version": "0.32.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",
@@ -42,7 +42,7 @@
42
42
  "cds-typer": "./lib/cli.js"
43
43
  },
44
44
  "peerDependencies": {
45
- "@cap-js/cds-types": ">=0.6.4",
45
+ "@cap-js/cds-types": ">=0.9",
46
46
  "@sap/cds": ">=8"
47
47
  },
48
48
  "devDependencies": {