@cap-js/cds-typer 0.15.0 → 0.17.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,7 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
6
6
 
7
- ## Version 0.16.0 - TBD
7
+ ## Version 0.18.0 - TBD
8
+
9
+ ## Version 0.17.0 - 2024-03-05
10
+ ### Fixed
11
+ - Fixed a bug where refering to an externally defined enum via the `typeof` syntax would crash the type generation
12
+
13
+ ## Version 0.16.0 - 2024-02-01
14
+ ### Changed
15
+ - Changed default log level from `NONE` to `ERROR`. See the doc to manually pass in another log level for cds-typer runs
16
+ - Name collisions between automatically generated foreign key fields (`.…_ID`, `.…_code`, etc.) with explicitly named fields will now raise an error
17
+ - Generate CDS types that are actually structured types as if they were entities. This allows the correct representation of mixing aspects and types in CDS inheritance, and also fixes issues with inline enums in such types
18
+
19
+ ### Fixed
20
+ - Externally defined enums can now be used as parameter types in actions
8
21
 
9
22
  ## Version 0.15.0 - 2023-12-21
10
23
  ### Added
package/lib/cli.js CHANGED
@@ -23,7 +23,7 @@ const flags = {
23
23
  logLevel: {
24
24
  desc: `Minimum log level that is printed.`,
25
25
  allowed: Object.keys(Levels),
26
- default: Object.keys(Levels).at(-1),
26
+ default: Levels.ERROR,
27
27
  },
28
28
  jsConfigPath: {
29
29
  desc: `Path to where the jsconfig.json should be written.${EOL}If specified, ${toolName} will create a jsconfig.json file and${EOL}set it up to restrict property usage in types entities to${EOL}existing properties only.`,
@@ -90,7 +90,7 @@ const main = async (args) => {
90
90
  compileFromFile(args.positional, {
91
91
  // temporary fix until rootDir is faded out
92
92
  outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
93
- logLevel: Levels[args.named.logLevel],
93
+ logLevel: Levels[args.named.logLevel] ?? args.named.logLevel,
94
94
  jsConfigPath: args.named.jsConfigPath,
95
95
  inlineDeclarations: args.named.inlineDeclarations,
96
96
  propertiesOptional: args.named.propertiesOptional === 'true'
@@ -29,13 +29,9 @@ const { normalise } = require('./identifier')
29
29
  function printEnum(buffer, name, kvs, options = {}) {
30
30
  const opts = {...{export: true}, ...options}
31
31
  buffer.add('// enum')
32
- buffer.add(`${opts.export ? 'export ' : ''}const ${name} = {`)
33
- buffer.indent()
34
- for (const [k, v] of kvs) {
35
- buffer.add(`${normalise(k)}: ${v},`)
36
- }
37
- buffer.outdent()
38
- buffer.add('} as const;')
32
+ buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
33
+ kvs.forEach(([k, v]) => buffer.add(`${normalise(k)}: ${v},`))
34
+ , '} as const;')
39
35
  buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
40
36
  buffer.add('')
41
37
  }
@@ -59,7 +55,7 @@ const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
59
55
  const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
60
56
 
61
57
  // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
62
- const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
58
+ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
63
59
 
64
60
  /**
65
61
  * Converts a CSN type describing an enum into a list of kv-pairs.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Check if an element references another type.
3
+ * This happens for foreign key relationships
4
+ * and for the typeof syntax.
5
+ *
6
+ * ```cds
7
+ * entity E {
8
+ * x: Integer
9
+ * }
10
+ *
11
+ * entity F {
12
+ * y: E.x // <- ref
13
+ * }
14
+ * ```
15
+ *
16
+ * @param {{type: any}} element
17
+ * @returns boolean
18
+ */
19
+ const isReferenceType = (element) => element.type && Object.hasOwn(element.type, 'ref')
20
+
21
+ module.exports = {
22
+ isReferenceType
23
+ }
@@ -6,6 +6,7 @@ const { Buffer, SourceFile, Path, Library, baseDefinitions } = require('../file'
6
6
  const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
7
7
  const { StructuredInlineDeclarationResolver } = require('./inline')
8
8
  const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
9
+ const { isReferenceType } = require('./reference')
9
10
 
10
11
  /** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
11
12
  /** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
@@ -55,7 +56,7 @@ const Builtins = {
55
56
  Float: 'number',
56
57
  Double: 'number',
57
58
  Boolean: 'boolean',
58
- // note: the date-related types _can_ be Date in some cases, but let's start with string
59
+ // note: the date-related types are strings on purpose, which reflects their runtime behaviour
59
60
  Date: 'string', // yyyy-mm-dd
60
61
  DateTime: 'string', // yyyy-mm-dd + time + TZ (precision: seconds
61
62
  Time: 'string',
@@ -370,10 +371,18 @@ class Resolver {
370
371
  result.type = '{}'
371
372
  result.isInlineDeclaration = true
372
373
  } else {
373
- if (isInlineEnumType(element, this.csn)) {
374
+ if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
374
375
  // element.parent is only set if the enum is attached to an entity's property.
375
376
  // If it is missing then we are dealing with an inline parameter type of an action.
376
- if (element.parent) {
377
+ // Edge case: element.parent is set, but no .name property is attached. This happens
378
+ // for inline enums inside types:
379
+ // ```cds
380
+ // type T {
381
+ // x : String enum { ... }; // no element.name for x
382
+ // }
383
+ // ```
384
+ // In that case, we currently resolve to the more general type (cds.String, here)
385
+ if (element.parent?.name) {
377
386
  result.isInlineDeclaration = true
378
387
  // we use the singular as the initial declaration of these enums takes place
379
388
  // while defining the singular class. Which therefore uses the singular over the plural name.
package/lib/csn.js CHANGED
@@ -234,10 +234,12 @@ function amendCSN(csn) {
234
234
  */
235
235
  const isView = entity => entity.query && !entity.projection
236
236
 
237
+ const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
238
+
237
239
  /**
238
240
  * @see isView
239
241
  * Unresolved entities have to be looked up from inferred csn.
240
242
  */
241
243
  const isUnresolved = entity => entity._unresolved === true
242
244
 
243
- module.exports = { amendCSN, isView, isUnresolved, propagateForeignKeys }
245
+ module.exports = { amendCSN, isView, isDraftEnabled, isUnresolved, propagateForeignKeys }
package/lib/file.js CHANGED
@@ -93,10 +93,13 @@ class Library extends File {
93
93
  * Source file containing several buffers.
94
94
  */
95
95
  class SourceFile extends File {
96
+ /**
97
+ * @param {string | Path} path
98
+ */
96
99
  constructor(path) {
97
100
  super()
98
101
  /** @type {Path} */
99
- this.path = new Path(path.split('.'))
102
+ this.path = path instanceof Path ? path : new Path(path.split('.'))
100
103
  /** @type {Object} */
101
104
  this.imports = {}
102
105
  /** @type {Buffer} */
@@ -480,6 +483,33 @@ class Buffer {
480
483
  add(part) {
481
484
  this.parts.push(this.currentIndent + part)
482
485
  }
486
+
487
+ /**
488
+ * Adds an element to the buffer with one level of indent.
489
+ * @param {string | (() => void)} part either a string or a function. If it is a string, it is added to the buffer.
490
+ * If not, it is expected to be a function that manipulates the buffer as a side effect.
491
+ */
492
+ addIndented(part) {
493
+ this.indent()
494
+ if (typeof part === 'function') {
495
+ part()
496
+ } else if (Array.isArray(part)) {
497
+ part.forEach(p => this.add(p))
498
+ }
499
+ this.outdent()
500
+ }
501
+
502
+ /**
503
+ * Adds an element to a buffer with one level of indent and and opener and closer surrounding it.
504
+ * @param {string} opener the string to put before the indent
505
+ * @param {string} content the content to indent (see {@link addIndented})
506
+ * @param {string} closer the string to put after the indent
507
+ */
508
+ addIndentedBlock(opener, content, closer) {
509
+ this.add(opener)
510
+ this.addIndented(content)
511
+ this.add(closer)
512
+ }
483
513
  }
484
514
 
485
515
  /**
@@ -548,6 +578,38 @@ class Path {
548
578
  }
549
579
  }
550
580
 
581
+ // TODO: having the repository pattern in place we can separate (some of) the printing logic from the visitor.
582
+ // Most of it hinges primarily on resolving specific files. We can now pass the repository and the resolver to a printer.
583
+ class FileRepository {
584
+ #files = {}
585
+
586
+ /**
587
+ * @param {string} name
588
+ * @param {SourceFile} file
589
+ */
590
+ add(name, file) {
591
+ this.#files[name] = file
592
+ }
593
+
594
+ /**
595
+ * Determines the file corresponding to the namespace.
596
+ * If no such file exists yet, it is created first.
597
+ * @param {string | Path} path the name of the namespace (foo.bar.baz)
598
+ * @returns {SourceFile} the file corresponding to that namespace name
599
+ */
600
+ getNamespaceFile(path) {
601
+ const key = path instanceof Path ? path.asNamespace() : path
602
+ return (this.#files[key] ??= new SourceFile(path))
603
+ }
604
+
605
+ /**
606
+ * @returns {SourceFile[]}
607
+ */
608
+ getFiles() {
609
+ return Object.values(this.#files)
610
+ }
611
+ }
612
+
551
613
  /**
552
614
  * Base definitions used throughout the typing process,
553
615
  * such as Associations and Compositions.
@@ -583,7 +645,7 @@ export type EntitySet<T> = T[] & {
583
645
 
584
646
  export type DeepRequired<T> = {
585
647
  [K in keyof T]: DeepRequired<T[K]>
586
- } & Required<T>;
648
+ } & Exclude<Required<T>, null>;
587
649
  `)
588
650
 
589
651
  /**
@@ -618,6 +680,7 @@ module.exports = {
618
680
  Library,
619
681
  Buffer,
620
682
  File,
683
+ FileRepository,
621
684
  SourceFile,
622
685
  Path,
623
686
  writeout,
package/lib/visitor.js CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  const util = require('./util')
4
4
 
5
- const { amendCSN, isView, isUnresolved, propagateForeignKeys } = require('./csn')
5
+ const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled } = require('./csn')
6
6
  // eslint-disable-next-line no-unused-vars
7
- const { SourceFile, baseDefinitions, Buffer } = require('./file')
7
+ const { SourceFile, FileRepository, baseDefinitions, Buffer } = require('./file')
8
8
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
9
9
  const { Resolver } = require('./components/resolver')
10
10
  const { Logger } = require('./logging')
11
11
  const { docify } = require('./components/wrappers')
12
12
  const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
13
+ const { isReferenceType } = require('./components/reference')
13
14
 
14
15
  /** @typedef {import('./file').File} File */
15
16
  /** @typedef {{ entity: String }} Context */
@@ -54,7 +55,7 @@ class Visitor {
54
55
  * @returns {File[]} a full list of files to be written
55
56
  */
56
57
  getWriteoutFiles() {
57
- return Object.values(this.files).concat(this.resolver.getUsedLibraries())
58
+ return this.fileRepository.getFiles().concat(this.resolver.getUsedLibraries())
58
59
  }
59
60
 
60
61
  /**
@@ -74,9 +75,9 @@ class Visitor {
74
75
  /** @type {Resolver} */
75
76
  this.resolver = new Resolver(this)
76
77
 
77
- /** @type {Object<string, File>} */
78
- this.files = {}
79
- this.files[baseDefinitions.path.asIdentifier()] = baseDefinitions
78
+ /** @type {FileRepository} */
79
+ this.fileRepository = new FileRepository()
80
+ this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
80
81
  this.inlineDeclarationResolver =
81
82
  this.options.inlineDeclarations === 'structured'
82
83
  ? new StructuredInlineDeclarationResolver(this)
@@ -85,16 +86,6 @@ class Visitor {
85
86
  this.visitDefinitions()
86
87
  }
87
88
 
88
- /**
89
- * Determines the file corresponding to the namespace.
90
- * If no such file exists yet, it is created first.
91
- * @param {string} path the name of the namespace (foo.bar.baz)
92
- * @returns {SourceFile} the file corresponding to that namespace name
93
- */
94
- getNamespaceFile(path) {
95
- return (this.files[path] ??= new SourceFile(path))
96
- }
97
-
98
89
  /**
99
90
  * Visits all definitions within the CSN definitions.
100
91
  */
@@ -163,7 +154,7 @@ class Visitor {
163
154
  #aspectify(name, entity, buffer, cleanName = undefined) {
164
155
  const clean = cleanName ?? this.resolver.trimNamespace(name)
165
156
  const ns = this.resolver.resolveNamespace(name.split('.'))
166
- const file = this.getNamespaceFile(ns)
157
+ const file = this.fileRepository.getNamespaceFile(ns)
167
158
 
168
159
  const identSingular = (name) => name
169
160
  const identAspect = (name) => `_${name}Aspect`
@@ -171,61 +162,54 @@ class Visitor {
171
162
  this.contexts.push({ entity: name })
172
163
 
173
164
  // CLASS ASPECT
174
- buffer.add(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`)
175
- buffer.indent()
176
- buffer.add(`return class ${clean} extends Base {`)
177
- buffer.indent()
178
-
179
- const enums = []
180
- const exclusions = new Set(entity.projection?.excluding ?? [])
181
- const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
182
- for (const [ename, element] of elements) {
183
- this.visitElement(ename, element, file, buffer)
184
-
185
- // make foreign keys explicit
186
- if ('target' in element) {
187
- // lookup in cds.definitions can fail for inline structs.
188
- // We don't really have to care for this case, as keys from such structs are _not_ propagated to
189
- // the containing entity.
190
- for (const [kname, kelement] of this.#keys(element.target)) {
191
- if (this.resolver.getMaxCardinality(element) === 1) {
192
- kelement.isRefNotNull = !!element.notNull || !!element.key
193
- this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
165
+ buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, function () {
166
+ buffer.addIndentedBlock(`return class ${clean} extends Base {`, function () {
167
+ const enums = []
168
+ const exclusions = new Set(entity.projection?.excluding ?? [])
169
+ const elements = Object.entries(entity.elements ?? {}).filter(([ename]) => !exclusions.has(ename))
170
+ for (const [ename, element] of elements) {
171
+ this.visitElement(ename, element, file, buffer)
172
+
173
+ // make foreign keys explicit
174
+ if ('target' in element) {
175
+ // lookup in cds.definitions can fail for inline structs.
176
+ // We don't really have to care for this case, as keys from such structs are _not_ propagated to
177
+ // the containing entity.
178
+ for (const [kname, kelement] of this.#keys(element.target)) {
179
+ if (this.resolver.getMaxCardinality(element) === 1) { // FIXME: kelement?
180
+ const foreignKey = `${ename}_${kname}`
181
+ if (Object.hasOwn(entity.elements, foreignKey)) {
182
+ this.logger.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${name}. But a property of that name is already defined explicitly. Consider renaming that property.`)
183
+ } else {
184
+ kelement.isRefNotNull = !!element.notNull || !!element.key
185
+ this.visitElement(foreignKey, kelement, file, buffer)
186
+ }
187
+ }
188
+ }
194
189
  }
195
- }
196
- }
197
-
198
- // store inline enums for later handling, as they have to go into one common "static elements" wrapper
199
- if (isInlineEnumType(element, this.csn.xtended)) {
200
- enums.push(element)
201
- }
202
- }
203
190
 
204
- buffer.indent()
205
- for (const e of enums) {
206
- buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
207
- file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
208
- }
209
- buffer.add('static actions: {')
210
- buffer.indent()
211
- for (const [aname, action] of Object.entries(entity.actions ?? {})) {
212
- buffer.add(
213
- SourceFile.stringifyLambda({
214
- name: aname,
215
- parameters: this.#stringifyFunctionParams(action.params, file),
216
- returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
217
- //initialiser: `undefined as unknown as typeof ${clean}.${aname}`,
218
- })
219
- )
220
- }
221
- buffer.outdent()
222
- buffer.outdent()
223
- buffer.add('}') // end of actions
191
+ // store inline enums for later handling, as they have to go into one common "static elements" wrapper
192
+ if (isInlineEnumType(element, this.csn.xtended)) {
193
+ enums.push(element)
194
+ }
195
+ }
224
196
 
225
- buffer.outdent()
226
- buffer.add('};') // end of generated class
227
- buffer.outdent()
228
- buffer.add('}') // end of aspect
197
+ buffer.addIndented(function() {
198
+ for (const e of enums) {
199
+ buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
200
+ file.addInlineEnum(clean, name, e.name, csnToEnumPairs(e, {unwrapVals: true}))
201
+ }
202
+ const actions = Object.entries(entity.actions ?? {})
203
+ buffer.addIndentedBlock('static actions: {',
204
+ actions.map(([aname, action]) => SourceFile.stringifyLambda({
205
+ name: aname,
206
+ parameters: this.#stringifyFunctionParams(action.params, file),
207
+ returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
208
+ }))
209
+ , '}') // end of actions
210
+ }.bind(this))
211
+ }.bind(this), '};') // end of generated class
212
+ }.bind(this), '}') // end of aspect
229
213
 
230
214
  // CLASS WITH ADDED ASPECTS
231
215
  file.addImport(baseDefinitions.path)
@@ -250,12 +234,8 @@ class Visitor {
250
234
  this.contexts.pop()
251
235
  }
252
236
 
253
- #isDraftEnabled(entity) {
254
- return entity['@odata.draft.enabled'] === true
255
- }
256
-
257
237
  #staticClassContents(clean, entity) {
258
- return this.#isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
238
+ return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
259
239
  }
260
240
 
261
241
  #printEntity(name, entity) {
@@ -263,7 +243,7 @@ class Visitor {
263
243
  const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
264
244
  const clean = this.resolver.trimNamespace(name)
265
245
  const ns = this.resolver.resolveNamespace(name.split('.'))
266
- const file = this.getNamespaceFile(ns)
246
+ const file = this.fileRepository.getNamespaceFile(ns)
267
247
  // entities are expected to be in plural anyway, so we would favour the regular name.
268
248
  // If the user decides to pass a @plural annotation, that gets precedence over the regular name.
269
249
  let plural = this.resolver.trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name)
@@ -280,7 +260,7 @@ class Visitor {
280
260
  }
281
261
  if (singular in this.csn.xtended.definitions) {
282
262
  this.logger.error(
283
- `Derived singular '${singular}' for your entity '${name}', already exists. The resulting types will be erronous. Please consider using '@singular:'/ '@plural:' annotations in your model to resolve this collision.`
263
+ `Derived singular '${singular}' for your entity '${name}', 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.`
284
264
  )
285
265
  }
286
266
  file.addClass(singular, name)
@@ -338,7 +318,9 @@ class Visitor {
338
318
  }
339
319
 
340
320
  #stringifyFunctionParamType(type, file) {
341
- return type.enum
321
+ // if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
322
+ // at a named enum type. In that case also resolve that type name
323
+ return type.enum && type.type.startsWith('cds.')
342
324
  ? stringifyEnumType(csnToEnumPairs(type))
343
325
  : this.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file))
344
326
  }
@@ -347,7 +329,7 @@ class Visitor {
347
329
  // FIXME: mostly duplicate of printAction -> reuse
348
330
  this.logger.debug(`Printing function ${name}:\n${JSON.stringify(func, null, 2)}`)
349
331
  const ns = this.resolver.resolveNamespace(name.split('.'))
350
- const file = this.getNamespaceFile(ns)
332
+ const file = this.fileRepository.getNamespaceFile(ns)
351
333
  const params = this.#stringifyFunctionParams(func.params, file)
352
334
  const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
353
335
  this.resolver.resolveAndRequire(func.returns, file)
@@ -358,7 +340,7 @@ class Visitor {
358
340
  #printAction(name, action) {
359
341
  this.logger.debug(`Printing action ${name}:\n${JSON.stringify(action, null, 2)}`)
360
342
  const ns = this.resolver.resolveNamespace(name.split('.'))
361
- const file = this.getNamespaceFile(ns)
343
+ const file = this.fileRepository.getNamespaceFile(ns)
362
344
  const params = this.#stringifyFunctionParams(action.params, file)
363
345
  const returns = this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(
364
346
  this.resolver.resolveAndRequire(action.returns, file)
@@ -368,10 +350,9 @@ class Visitor {
368
350
 
369
351
  #printType(name, type) {
370
352
  this.logger.debug(`Printing type ${name}:\n${JSON.stringify(type, null, 2)}`)
371
- const clean = this.resolver.trimNamespace(name)
372
- const ns = this.resolver.resolveNamespace(name.split('.'))
373
- const file = this.getNamespaceFile(ns)
374
- if ('enum' in type) {
353
+ const [ns, clean] = this.resolver.untangle(name)
354
+ const file = this.fileRepository.getNamespaceFile(ns)
355
+ if ('enum' in type && !isReferenceType(type)) { // skip references to enums
375
356
  file.addEnum(name, clean, csnToEnumPairs(type))
376
357
  } else {
377
358
  // alias
@@ -382,9 +363,8 @@ class Visitor {
382
363
 
383
364
  #printAspect(name, aspect) {
384
365
  this.logger.debug(`Printing aspect ${name}`)
385
- const clean = this.resolver.trimNamespace(name)
386
- const ns = this.resolver.resolveNamespace(name.split('.'))
387
- const file = this.getNamespaceFile(ns)
366
+ const [ns, clean] = this.resolver.untangle(name)
367
+ const file = this.fileRepository.getNamespaceFile(ns)
388
368
  // aspects are technically classes and can therefore be added to the list of defined classes.
389
369
  // Still, when using them as mixins for a class, they need to already be defined.
390
370
  // So we separate them into another buffer which is printed before the classes.
@@ -395,28 +375,25 @@ class Visitor {
395
375
 
396
376
  #printEvent(name, event) {
397
377
  this.logger.debug(`Printing event ${name}`)
398
- const clean = this.resolver.trimNamespace(name)
399
- const ns = this.resolver.resolveNamespace(name.split('.'))
400
- const file = this.getNamespaceFile(ns)
378
+ const [ns, clean] = this.resolver.untangle(name)
379
+ const file = this.fileRepository.getNamespaceFile(ns)
401
380
  file.addEvent(clean, name)
402
381
  const buffer = file.events.buffer
403
382
  buffer.add('// event')
404
- buffer.add(`export class ${clean} {`)
405
- buffer.indent()
406
- const propOpt = this.options.propertiesOptional
407
- this.options.propertiesOptional = false
408
- for (const [ename, element] of Object.entries(event.elements ?? {})) {
409
- this.visitElement(ename, element, file, buffer)
410
- }
411
- this.options.propertiesOptional = propOpt
412
- buffer.outdent()
413
- buffer.add('}')
383
+ buffer.addIndentedBlock(`export class ${clean} {`, function() {
384
+ const propOpt = this.options.propertiesOptional
385
+ this.options.propertiesOptional = false
386
+ for (const [ename, element] of Object.entries(event.elements ?? {})) {
387
+ this.visitElement(ename, element, file, buffer)
388
+ }
389
+ this.options.propertiesOptional = propOpt
390
+ }.bind(this), '}')
414
391
  }
415
392
 
416
393
  #printService(name, service) {
417
394
  this.logger.debug(`Printing service ${name}:\n${JSON.stringify(service, null, 2)}`)
418
395
  const ns = this.resolver.resolveNamespace(name)
419
- const file = this.getNamespaceFile(ns)
396
+ const file = this.fileRepository.getNamespaceFile(ns)
420
397
  // service.name is clean of namespace
421
398
  file.services.buffer.add(`export default { name: '${service.name}' }`)
422
399
  file.addService(service.name)
@@ -439,12 +416,16 @@ class Visitor {
439
416
  case 'function':
440
417
  this.#printAction(name, entity)
441
418
  break
442
- case 'type':
443
- this.#printType(name, entity)
444
- break
445
419
  case 'aspect':
446
420
  this.#printAspect(name, entity)
447
421
  break
422
+ case 'type': {
423
+ // types like inline definitions can be used very similarly to entities.
424
+ // They can be extended, contain inline enums, etc., so we treat them as entities.
425
+ const handler = entity.elements ? this.#printEntity : this.#printType
426
+ handler.bind(this)(name, entity)
427
+ break
428
+ }
448
429
  case 'event':
449
430
  this.#printEvent(name, entity)
450
431
  break
@@ -452,7 +433,7 @@ class Visitor {
452
433
  this.#printService(name, entity)
453
434
  break
454
435
  default:
455
- this.logger.error(`Unhandled entity kind '${entity.kind}'.`)
436
+ this.logger.debug(`Unhandled entity kind '${entity.kind}'.`)
456
437
  }
457
438
  }
458
439
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",