@cap-js/cds-typer 0.31.0 → 0.32.1

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,39 @@ 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.1] - 2025-01-20
15
+
16
+ ### Added
8
17
  ### Changed
9
18
  ### Deprecated
10
19
  ### Removed
11
20
  ### Fixed
21
+ - default value for `inline_declarations` in help command
22
+ - entity scope and namespace are now added in the correct order to inflected type names
23
+ ### Security
24
+
25
+ ## [0.32.0] - 2025-01-14
26
+
27
+ ### Added
28
+ - dedicated classes for inline compositions
29
+ - dedicated text-classes for entities with `localized` elements
30
+
31
+ ### Changed
32
+ - prefixed builtin types like `Promise` and `Record` with `globalThis.`, to allow using names of builtin types for entities without collisions
33
+ - default export class representing the service itself is now exported without name
34
+ - bumped peer-dependency to `@cap-js/cds-types` to `>=0.9`
35
+
36
+ ### Deprecated
37
+ ### Removed
38
+ ### Fixed
39
+ - referencing another entity's property of type `cds.String` in an enum will now properly quote the generated values
12
40
  ### Security
13
41
 
14
42
  ## [0.31.0] - 2024-12-16
package/lib/cli.js CHANGED
@@ -182,7 +182,7 @@ const flags = enrichFlagSchema({
182
182
  inlineDeclarations: {
183
183
  desc: `Whether to resolve inline type declarations${EOL}flat: (x_a, x_b, ...)${EOL}or structured: (x: {a, b}).`,
184
184
  allowed: ['flat', 'structured'],
185
- default: 'structured'
185
+ default: 'flat'
186
186
  },
187
187
  propertiesOptional: parameterTypes.boolean({
188
188
  desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
@@ -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
  /**
@@ -401,10 +420,6 @@ class SourceFile extends File {
401
420
  */
402
421
  getImports() {
403
422
  const buffer = new Buffer()
404
- if (this.services.names.length) {
405
- // currently only needed to extend cds.Service and would trigger unused-variable-errors in strict configs
406
- buffer.add('import cds from \'@sap/cds\'') // TODO should go to visitor#printService, but can't express this as Path
407
- }
408
423
  const file = configuration.targetModuleType === 'esm'
409
424
  ? '/index.js'
410
425
  : ''
@@ -490,12 +505,15 @@ class SourceFile extends File {
490
505
 
491
506
  return {
492
507
  singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
493
- pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
508
+ pluralRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: false }})`,
494
509
  }
495
510
  } else {
511
+ // standard entity: csn.Books
512
+ // inline entity: csn['Books.texts']
513
+ const csnAccess = original.includes('.') ? `csn['${original}']` : `csn.${original}`
496
514
  return {
497
- singularRhs: `{ is_singular: true, __proto__: csn.${original} }`,
498
- pluralRhs: `csn.${original}`
515
+ singularRhs: `{ is_singular: true, __proto__: ${csnAccess} }`,
516
+ pluralRhs: csnAccess
499
517
  }
500
518
  }
501
519
  }
@@ -589,6 +607,11 @@ class Buffer {
589
607
  * @type {boolean}
590
608
  */
591
609
  this.closed = false
610
+ /**
611
+ * Required for inline enums of inline compositions or text entities
612
+ * @type {string | undefined}
613
+ */
614
+ this.namespace = undefined
592
615
  }
593
616
 
594
617
  /**
@@ -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.
@@ -284,6 +288,8 @@ class Resolver {
284
288
  const typeInfo = this.resolveType(element, file, options)
285
289
  const cardinality = getMaxCardinality(element)
286
290
 
291
+ /** @type {string|undefined} */
292
+ let typeNamespaceIdent = undefined
287
293
  let typeName = typeInfo.plainName ?? typeInfo.type
288
294
 
289
295
  // only applies to builtin types, because the association/ composition _themselves_ are the (builtin) types we are checking, not their generic parameter!
@@ -311,18 +317,6 @@ class Resolver {
311
317
  } else {
312
318
  let { singular, plural } = targetTypeInfo.typeInfo.inflection
313
319
 
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
320
  typeName = cardinality > 1
327
321
  ? toMany(plural)
328
322
  : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
@@ -341,9 +335,10 @@ class Resolver {
341
335
  if (!parent.isCwd(file.path.asDirectory())) {
342
336
  file.addImport(parent)
343
337
  // prepend namespace
344
- typeName = `${parent.asIdentifier()}.${typeName}`
345
- typeInfo.inflection.singular = `${parent.asIdentifier()}.${typeInfo.inflection.singular}`
346
- typeInfo.inflection.plural = `${parent.asIdentifier()}.${typeInfo.inflection.plural}`
338
+ typeNamespaceIdent = parent.asIdentifier()
339
+ typeName = [typeNamespaceIdent, typeName].join('.')
340
+ typeInfo.inflection.singular = [typeNamespaceIdent, typeInfo.inflection.singular].join('.')
341
+ typeInfo.inflection.plural = [typeNamespaceIdent, typeInfo.inflection.plural].join('.')
347
342
  }
348
343
 
349
344
  if (element.type.ref?.length > 1) {
@@ -370,8 +365,25 @@ class Resolver {
370
365
  // handle typeof (unless it has already been handled above)
371
366
  const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
372
367
  if (target && !typeInfo.isDeepRequire) {
373
- const { propertyAccess } = this.visitor.entityRepository.getByFq(target) ?? {}
374
- if (propertyAccess?.length) {
368
+ const { propertyAccess, scope } = this.visitor.entityRepository.getByFq(target) ?? {}
369
+ if (scope?.length && typeInfo.inflection) {
370
+ let { singular, plural } = typeInfo.inflection
371
+ // remove already added namespace, so the scope is added after the namespace
372
+ // i.e. _common.Book.texts instead of Book._common.texts
373
+ if (typeNamespaceIdent) {
374
+ if (singular.startsWith(typeNamespaceIdent)) {
375
+ singular = singular.substring(typeNamespaceIdent.length+1)
376
+ }
377
+ if (plural.startsWith(typeNamespaceIdent)) {
378
+ plural = plural.substring(typeNamespaceIdent.length+1)
379
+ }
380
+ }
381
+ // update inflections with proper prefix, e.g. Books.text, Books.texts
382
+ typeInfo.inflection = {
383
+ singular: [typeNamespaceIdent, ...scope, singular].filter(Boolean).join('.'),
384
+ plural: [typeNamespaceIdent,...scope, plural].filter(Boolean).join('.')
385
+ }
386
+ } else if (propertyAccess?.length) {
375
387
  const element = target.slice(0, -propertyAccess.join('.').length - 1)
376
388
  const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
377
389
  // singular, as we have to access the property of the entity
@@ -452,6 +464,7 @@ class Resolver {
452
464
 
453
465
  const cardinality = getMaxCardinality(element)
454
466
 
467
+ /** @type {TypeResolveInfo} */
455
468
  const result = {
456
469
  isBuiltin: false, // will be rectified in the corresponding handlers, if needed
457
470
  isInlineDeclaration: false,
@@ -569,7 +582,7 @@ class Resolver {
569
582
  * @returns @see resolveType
570
583
  */
571
584
  resolveTypeName(t, into) {
572
- const result = into ?? {}
585
+ const result = into ?? /** @type {TypeResolveInfo} */({})
573
586
  const path = t.split('.')
574
587
  const builtin = this.builtinResolver.resolveBuiltin(path)
575
588
  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
  }
@@ -579,13 +535,18 @@ class Visitor {
579
535
 
580
536
  docify(service.doc).forEach(d => { buffer.add(d) })
581
537
  // file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
582
- buffer.addIndentedBlock(`export class ${serviceNameSimple} extends cds.Service {`, () => {
538
+ buffer.addIndentedBlock('export default class {', () => {
583
539
  Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
584
540
  buffer.add(docify(doc))
585
- buffer.add(`declare ${name}: typeof ${name}`)
541
+ buffer.add(createMember({
542
+ name,
543
+ type: `typeof ${name}`,
544
+ isStatic: true,
545
+ isReadonly: true,
546
+ isDeclare: true,
547
+ }))
586
548
  })
587
549
  }, '}')
588
- buffer.add(`export default ${serviceNameSimple}`)
589
550
  buffer.blankLine()
590
551
  file.addService(service.name)
591
552
  }
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.1",
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": {