@cap-js/cds-typer 0.10.0 → 0.11.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,13 +4,28 @@ 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.10.1 - TBD
7
+ ## Version 0.12.0 - TBD
8
8
 
9
9
  ### Changed
10
10
 
11
11
  ### Added
12
+ ### Fixed
13
+
14
+ ## Version 0.11.0 - 2023-10-10
15
+
16
+ ### Changed
17
+
18
+ ### Added
19
+ - Autoexposed entities in services are now also generated
20
+ - Each generated class now contains their original fully qualified name in a static `.name` property
21
+ - Inline enums that are defined as literal type of properties are now supported as well (note: this feature is experimental. The location to which enums are generated might change in the future!)
12
22
 
13
23
  ### Fixed
24
+ - Fixed an error when an entity uses `type of` on a property they have inherited from another entity
25
+ - Fixed an error during draftability propagation when defining compositions on types that are declared inline
26
+
27
+ ### Removed
28
+ - `compileFromCSN` is no longer part of the package's API
14
29
 
15
30
  ## Version 0.10.0 - 2023-09-21
16
31
 
package/lib/compile.js CHANGED
@@ -45,13 +45,14 @@ const writeJsConfig = (path, logger) => {
45
45
  */
46
46
  const compileFromFile = async (inputFile, parameters) => {
47
47
  const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
48
- const csn = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
49
- return compileFromCSN(csn, parameters)
48
+ const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
49
+ const inferred = await cds.linked(await cds.load(paths, { docs: true }))
50
+ return compileFromCSN({xtended, inferred}, parameters)
50
51
  }
51
52
 
52
53
  /**
53
54
  * Compiles a CSN object to Typescript types.
54
- * @param csn {CSN}
55
+ * @param {{xtended: CSN, inferred: CSN}} csn
55
56
  * @param parameters {CompileParameters} path to root directory for all generated files, min log level
56
57
  */
57
58
  const compileFromCSN = async (csn, parameters) => {
@@ -69,6 +70,5 @@ const compileFromCSN = async (csn, parameters) => {
69
70
  }
70
71
 
71
72
  module.exports = {
72
- compileFromFile,
73
- compileFromCSN,
73
+ compileFromFile
74
74
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Prints an enum to a buffer. To be precise, it prints
3
+ * a constant object and a type which together form an artificial enum.
4
+ * CDS enums differ from TS enums as they can use bools as value (TS: only number and string)
5
+ * So we have to emulate enums by adding an object (name -> value mappings)
6
+ * and a type containing all disctinct values.
7
+ * We can get away with this as TS doesn't feature nominal typing, so the structure
8
+ * is all we care about.
9
+ *
10
+ * @example
11
+ * ```cds
12
+ * type E: enum of String {
13
+ * a = 'A';
14
+ * b = 'B';
15
+ * }
16
+ * ```
17
+ * becomes
18
+ * ```ts
19
+ * const E = { a: 'A', b: 'B' }
20
+ * type E = 'A' | 'B'
21
+ * ```
22
+ *
23
+ * @param {Buffer} buffer Buffer to write into
24
+ * @param {string} name local name of the enum, i.e. the name under which it should be created in the .ts file
25
+ * @param {[string, string][]} kvs list of key-value pairs
26
+ */
27
+ function printEnum(buffer, name, kvs, options = {}) {
28
+ const opts = {...{export: true}, ...options}
29
+ buffer.add('// enum')
30
+ buffer.add(`${opts.export ? 'export ' : ''}const ${name} = {`)
31
+ buffer.indent()
32
+ const vals = new Set()
33
+ for (const [k, v] of kvs) {
34
+ buffer.add(`${k}: ${JSON.stringify(v)},`)
35
+ vals.add(JSON.stringify(v.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
36
+ }
37
+ buffer.outdent()
38
+ buffer.add('} as const;')
39
+ buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${[...vals].join(' | ')}`)
40
+ buffer.add('')
41
+ }
42
+
43
+ // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
44
+ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? `${value ?? key}` : value
45
+
46
+ /**
47
+ * @param {{enum: {[key: name]: string}, type: string}} enumCsn
48
+ * @param {{unwrapVals: boolean}} options if `unwrapVals` is passed,
49
+ * then the CSN structure `{val:x}` is flattened to just `x`.
50
+ * Retaining `val` is closer to the actual CSN structure and should be used where we want
51
+ * to mimic the runtime as closely as possible (anoymous enum types).
52
+ * Stripping that additional wrapper would be more readable for users.
53
+ * @example
54
+ * ```ts
55
+ * const csn = {enum: {x: {val: 42}, y: {val: -42}}}
56
+ * csnToEnum(csn) // -> [['x', 42], ['y': -42]]
57
+ * csnToEnum(csn, {unwrapVals: false}) // -> [['x', {val:42}], ['y': {val:-42}]]
58
+ * ```
59
+ */
60
+ const csnToEnum = ({enum: enm, type}, options = {}) => {
61
+ options = {...{unwrapVals: true}, ...options}
62
+ return Object.entries(enm).map(([k, v]) => {
63
+ const val = enumVal(k, v.val, type)
64
+ return [k, options.unwrapVals ? val : { val }]
65
+ })
66
+ }
67
+
68
+ /**
69
+ * @param {string} entity
70
+ * @param {string} property
71
+ */
72
+ const propertyToAnonymousEnumName = (entity, property) => `${entity}_${property}`
73
+
74
+ /**
75
+ * A type is considered to be an inline enum, iff it has a `.enum` property
76
+ * _and_ its type is a CDS primitive, i.e. it is not contained in `cds.definitions`.
77
+ * If it is contained there, then it is a standard enum declaration that has its own name.
78
+ *
79
+ * @param {{type: string}} element
80
+ * @param {object} csn
81
+ * @returns boolean
82
+ */
83
+ const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
84
+
85
+ const stringifyEnumImplementation = (name, enm) => `module.exports.${name} = Object.fromEntries(Object.entries(${enm}).map(([k,v]) => [k,v.val]))`
86
+
87
+ /**
88
+ * @param {string} name
89
+ * @param {string} fq
90
+ * @returns {string}
91
+ */
92
+ const stringifyNamedEnum = (name, fq) => stringifyEnumImplementation(name, `cds.model.definitions['${fq}'].enum`)
93
+ /**
94
+ * @param {string} name
95
+ * @param {string} fq
96
+ * @param {string} property
97
+ * @returns {string}
98
+ */
99
+ const stringifyAnonymousEnum = (name, fq, property) => stringifyEnumImplementation(fq, `cds.model.definitions['${name}'].elements.${property}.enum`)
100
+
101
+ module.exports = {
102
+ printEnum,
103
+ csnToEnum,
104
+ propertyToAnonymousEnumName,
105
+ isInlineEnumType,
106
+ stringifyNamedEnum,
107
+ stringifyAnonymousEnum
108
+ }
@@ -4,6 +4,7 @@ const util = require('../util')
4
4
  const { Buffer, SourceFile, Path, Library, baseDefinitions } = require("../file")
5
5
  const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
6
6
  const { StructuredInlineDeclarationResolver } = require("./inline")
7
+ const { isInlineEnumType, propertyToInlineEnumName, propertyToAnonymousEnumName } = require('./enum')
7
8
 
8
9
  /** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
9
10
  /** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
@@ -63,7 +64,7 @@ const Builtins = {
63
64
  }
64
65
 
65
66
  class Resolver {
66
- get csn() { return this.visitor.csn }
67
+ get csn() { return this.visitor.csn.inferred }
67
68
 
68
69
  /** @param {Visitor} visitor */
69
70
  constructor(visitor) {
@@ -106,6 +107,7 @@ class Resolver {
106
107
  * @returns {string} the entity name without leading namespace.
107
108
  */
108
109
  trimNamespace(p) {
110
+ // TODO: we might want to cache this
109
111
  // start on right side, go up while we have an entity at hand
110
112
  // we cant start on left side, as that clashes with undefined entities like "sap"
111
113
  const parts = p.split('.')
@@ -167,7 +169,7 @@ class Resolver {
167
169
  new StructuredInlineDeclarationResolver(this.visitor).printInlineType(undefined, { typeInfo }, into, '')
168
170
  typeName = into.join(' ')
169
171
  singular = typeName
170
- plural = createArrayOf(typeName) //`Array<${typeName}>`
172
+ plural = createArrayOf(typeName)
171
173
  } else {
172
174
  // TODO: make sure the resolution still works. Currently, we only cut off the namespace!
173
175
  singular = util.singular4(typeInfo.csn)
@@ -310,10 +312,13 @@ class Resolver {
310
312
  * read from left to right which does not contain a kind 'context' or 'service'.
311
313
  * That is, if in the above example 'D' is a context and 'E' is a service,
312
314
  * the resulting namespace is 'a.b.c'.
313
- * @param {string[]} pathParts the distinct parts of the namespace, i.e. ['a','b','c','D','E']
315
+ * @param {string[] | string} pathParts the distinct parts of the namespace, i.e. ['a','b','c','D','E'] or a single path interspersed with periods
314
316
  * @returns {string} the namespace's name, i.e. 'a.b.c'.
315
317
  */
316
318
  resolveNamespace(pathParts) {
319
+ if (typeof pathParts === 'string') {
320
+ pathParts = pathParts.split('.')
321
+ }
317
322
  let result
318
323
  while (result === undefined) {
319
324
  const path = pathParts.join('.')
@@ -350,14 +355,23 @@ class Resolver {
350
355
  isArray: false,
351
356
  }
352
357
 
353
- // FIXME: switch case
354
358
  if (element?.type === undefined) {
355
359
  // "fallback" type "empty object". May be overriden via #resolveInlineDeclarationType
356
360
  // later on with an inline declaration
357
361
  result.type = '{}'
358
362
  result.isInlineDeclaration = true
359
363
  } else {
360
- this.resolvePotentialReferenceType(element.type, result, file)
364
+ if (isInlineEnumType(element, this.csn)) {
365
+ // we use the singular as the initial declaration of these enums takes place
366
+ // while defining the singular class. Which therefore uses the singular over the plural name.
367
+ const cleanEntityName = util.singular4(element.parent, true)
368
+ const enumName = propertyToAnonymousEnumName(cleanEntityName, element.name)
369
+ result.type = enumName
370
+ result.plainName = enumName
371
+ result.isInlineDeclaration = true
372
+ } else {
373
+ this.resolvePotentialReferenceType(element.type, result, file)
374
+ }
361
375
  }
362
376
 
363
377
  // objects and arrays
package/lib/csn.js CHANGED
@@ -28,6 +28,7 @@ class DraftUnroller {
28
28
  */
29
29
  #setDraftable(entity, value) {
30
30
  if (typeof entity === 'string') entity = this.#getDefinition(entity)
31
+ if (!entity) return // inline definition -- not found in definitions
31
32
  entity[annotation] = value
32
33
  this.#draftable[entity.name] = value
33
34
  if (value) {
@@ -41,9 +42,13 @@ class DraftUnroller {
41
42
  * @param entity {object | string} - entity to look draftability up for.
42
43
  * @returns {boolean}
43
44
  */
44
- #getDraftable(entity) {
45
- if (typeof entity === 'string') entity = this.#getDefinition(entity)
46
- return this.#draftable[entity.name] ??= this.#propagateInheritance(entity)
45
+ #getDraftable(entityOrName) {
46
+ const entity = (typeof entityOrName === 'string')
47
+ ? this.#getDefinition(entityOrName)
48
+ : entityOrName
49
+ // assert(typeof entity !== 'string')
50
+ const name = entity?.name ?? entityOrName
51
+ return this.#draftable[name] ??= this.#propagateInheritance(entity)
47
52
  }
48
53
 
49
54
  /**
@@ -59,8 +64,8 @@ class DraftUnroller {
59
64
  * @param entity {object} - entity to pull draftability from its parents.
60
65
  */
61
66
  #propagateInheritance(entity) {
62
- const annotations = (entity.includes ?? []).map(parent => this.#getDraftable(parent))
63
- annotations.push(entity[annotation])
67
+ const annotations = (entity?.includes ?? []).map(parent => this.#getDraftable(parent))
68
+ annotations.push(entity?.[annotation])
64
69
  this.#setDraftable(entity, annotations.filter(a => a !== undefined).at(-1) ?? false)
65
70
  }
66
71
 
@@ -127,7 +132,7 @@ class DraftUnroller {
127
132
  * (a) aspects via `A: B`, where `B` is draft enabled.
128
133
  * Note that when an entity extends two other entities of which one has drafts enabled and
129
134
  * one has not, then the one that is later in the list of mixins "wins":
130
- * @example sdasd
135
+ * @example
131
136
  * ```ts
132
137
  * @odata.draft.enabled true
133
138
  * entity T {}
package/lib/file.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs').promises
4
4
  const { readFileSync } = require('fs')
5
+ const { printEnum, stringifyNamedEnum, stringifyAnonymousEnum, propertyToAnonymousEnumName } = require('./components/enum')
5
6
  const path = require('path')
6
7
 
7
8
  const AUTO_GEN_NOTE = "// This is an automatically generated file. Please do not change its contents manually!"
@@ -103,8 +104,10 @@ class SourceFile extends File {
103
104
  this.events = { buffer: new Buffer(), fqs: []}
104
105
  /** @type {Buffer} */
105
106
  this.types = new Buffer()
106
- /** @type {{ buffer: Buffer, fqs: {name: string, fq: string}[]}} */
107
+ /** @type {{ buffer: Buffer, fqs: {name: string, fq: string, property?: string}[]}} */
107
108
  this.enums = { buffer: new Buffer(), fqs: [] }
109
+ /** @type {{ buffer: Buffer }} */
110
+ this.inlineEnums = { buffer: new Buffer() }
108
111
  /** @type {Buffer} */
109
112
  this.classes = new Buffer()
110
113
  /** @type {{ buffer: Buffer, names: string[]}} */
@@ -119,6 +122,8 @@ class SourceFile extends File {
119
122
  this.typeNames = {}
120
123
  /** @type {[string, string, string][]} */
121
124
  this.inflections = []
125
+ /** @type {{ buffer: Buffer, names: string[]}} */
126
+ this.services = { buffer: new Buffer(), names: [] }
122
127
  }
123
128
 
124
129
  /**
@@ -218,31 +223,56 @@ class SourceFile extends File {
218
223
 
219
224
  /**
220
225
  * Adds an enum to this file.
221
- * @param {string} fq fully qualified name of the enum
226
+ * @param {string} fq fully qualified name of the enum (entity name within CSN)
222
227
  * @param {string} name local name of the enum
223
228
  * @param {[string, string][]} kvs list of key-value pairs
229
+ * @param {string} [property] property to which the enum is attached.
230
+ * If given, the enum is considered to be an anonymous inline definition of an enum.
231
+ * If not, it is considered to be regular, named enum.
224
232
  */
225
233
  addEnum(fq, name, kvs) {
226
- // CDS differ from TS enums as they can use bools as value (TS: only number and string)
227
- // So we have to emulate enums by adding an object (name -> value mappings)
228
- // and a type containing all disctinct values.
229
- // We can get away with this as TS doesn't feature nominal typing, so the structure
230
- // is all we care about.
231
- // FIXME: this really should be in visitor, as File should not contain logic of this kind
232
234
  this.enums.fqs.push({ name, fq })
233
- const bu = this.enums.buffer
234
- bu.add('// enum')
235
- bu.add(`export const ${name} = {`)
236
- bu.indent()
237
- const vals = new Set()
238
- for (const [k, v] of kvs) {
239
- bu.add(`${k}: ${v},`)
240
- vals.add(v)
241
- }
242
- bu.outdent()
243
- bu.add('}')
244
- bu.add(`export type ${name} = ${[...vals].join(' | ')}`)
245
- bu.add('')
235
+ printEnum(this.enums.buffer, name, kvs)
236
+ }
237
+
238
+ /**
239
+ * Adds an anonymous enum to this file.
240
+ * @param {string} entityCleanName name of the entity the enum is attached to without namespace
241
+ * @param {string} entityFqName name of the entity the enum is attached to with namespace
242
+ *
243
+ * @param {string} propertyName property to which the enum is attached.
244
+ * @param {[string, string][]} kvs list of key-value pairs
245
+ * If given, the enum is considered to be an anonymous inline definition of an enum.
246
+ * If not, it is considered to be regular, named enum.
247
+ *
248
+ * @example
249
+ * ```js
250
+ * addAnonymousEnum('Books.genre', 'Books', 'genre', [['horror','horror']])
251
+ * ```
252
+ * generates
253
+ * ```js
254
+ * // index.js
255
+ * module.exports.Books.genre = F(cds.model.definitions['Books'].elements.genre.enum)
256
+ * // F(...) is a function that maps a CSN enum to a more convenient style
257
+ * ```
258
+ * and also
259
+ * ```ts
260
+ * // index.ts
261
+ * const Books_genre = { horror: 'horror' }
262
+ * type Books_genre = 'horror'
263
+ * class Book {
264
+ * static genre = Books_genre
265
+ * genre: Books_genre
266
+ * }
267
+ * ```
268
+ */
269
+ addAnonymousEnum(entityCleanName, entityFqName, propertyName, kvs) {
270
+ this.enums.fqs.push({
271
+ name: entityFqName,
272
+ property: propertyName,
273
+ fq: `${entityCleanName}.${propertyName}`
274
+ })
275
+ printEnum(this.inlineEnums.buffer, propertyToAnonymousEnumName(entityCleanName, propertyName), kvs, {export: false})
246
276
  }
247
277
 
248
278
  /**
@@ -298,6 +328,19 @@ class SourceFile extends File {
298
328
  this.types.add(`export type ${clean} = ${rhs};`)
299
329
  }
300
330
 
331
+ /**
332
+ * Adds a service to the file.
333
+ * We consider each service its own distinct namespace and therefore expect
334
+ * at most one service per file.
335
+ * @param {string} fq the fully qualified name of the service
336
+ */
337
+ addService(fq) {
338
+ if (this.services.names.length) {
339
+ throw new Error(`trying to add more than one service to file ${this.path.asDirectory()}. Existing service is ${this.services.names[0]}, trying to add ${fq}`)
340
+ }
341
+ this.services.names.push(fq)
342
+ }
343
+
301
344
  /**
302
345
  * Writes all imports to a buffer, relative to the current file.
303
346
  * Creates a new buffer on each call, as concatenating import strings directly
@@ -327,12 +370,14 @@ class SourceFile extends File {
327
370
  this.getImports().join(),
328
371
  this.preamble.join(),
329
372
  this.types.join(),
330
- this.enums.buffer.join(),
373
+ this.enums.buffer.join(),
374
+ this.inlineEnums.buffer.join(), // needs to be before classes
331
375
  namespaces.join(),
332
376
  this.aspects.join(), // needs to be before classes
333
377
  this.classes.join(),
334
378
  this.events.buffer.join(),
335
379
  this.actions.buffer.join(),
380
+ this.services.buffer.join() // should be at the end
336
381
  ].filter(Boolean).join('\n')
337
382
  }
338
383
 
@@ -362,7 +407,11 @@ class SourceFile extends File {
362
407
  .concat(['// actions'])
363
408
  .concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
364
409
  .concat(['// enums'])
365
- .concat(this.enums.fqs.map(({fq, name}) => `module.exports.${name} = Object.fromEntries(Object.entries(cds.model.definitions['${fq}'].enum).map(([k,v]) => [k,v.val]))`))
410
+ .concat(this.enums.fqs.map(({name, fq, property}) => property
411
+ ? stringifyAnonymousEnum(name, fq, property)
412
+ : stringifyNamedEnum(name, fq)))
413
+ // FIXME: move stringification of service into own module
414
+ .concat(this.services.names.map(name => `module.exports.default = { name: '${name}' }`)) // there should be only one
366
415
  .join('\n') + '\n'
367
416
  }
368
417
  }
package/lib/visitor.js CHANGED
@@ -8,6 +8,7 @@ const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = r
8
8
  const { Resolver } = require('./components/resolver')
9
9
  const { Logger } = require('./logging')
10
10
  const { docify } = require('./components/wrappers')
11
+ const { csnToEnum, propertyToAnonymousEnumName, isInlineEnumType } = require('./components/enum')
11
12
 
12
13
  /** @typedef {import('./file').File} File */
13
14
  /** @typedef {{ entity: String }} Context */
@@ -56,11 +57,11 @@ class Visitor {
56
57
  }
57
58
 
58
59
  /**
59
- * @param csn root CSN
60
+ * @param {{xtended: CSN, inferred: CSN}} csn root CSN
60
61
  * @param {VisitorOptions} options
61
62
  */
62
63
  constructor(csn, options = {}, logger = new Logger()) {
63
- amendCSN(csn)
64
+ amendCSN(csn.xtended)
64
65
  this.options = { ...defaults, ...options }
65
66
  this.logger = logger
66
67
  this.csn = csn
@@ -96,8 +97,29 @@ class Visitor {
96
97
  * Visits all definitions within the CSN definitions.
97
98
  */
98
99
  visitDefinitions() {
99
- for (const [name, entity] of Object.entries(this.csn.definitions)) {
100
- this.visitEntity(name, entity)
100
+ for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
101
+ if (entity._unresolved === true) {
102
+ this.logger.error(`Skipping unresolved entity: ${JSON.stringify(entity)}`)
103
+ } else {
104
+ this.visitEntity(name, entity)
105
+ }
106
+ }
107
+ // FIXME: optimise
108
+ // We are currently working with two flavours of CSN:
109
+ // xtended, as it is as close as possible to an OOP class hierarchy
110
+ // inferred, as it contains information missing in xtended
111
+ // This is less than optimal and has to be revisited at some point!
112
+ const handledKeys = new Set(Object.keys(this.csn.xtended.definitions))
113
+ // we are looking for autoexposed entities in services
114
+ const missing = Object.entries(this.csn.inferred.definitions).filter(([key]) => !key.endsWith('.texts') &&!handledKeys.has(key))
115
+ for (const [name, entity] of missing) {
116
+ // instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
117
+ // The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
118
+ if (entity.projection) {
119
+ this.visitEntity(name, this.csn.xtended.definitions[entity.projection.from.ref[0]])
120
+ } else {
121
+ this.logger.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
122
+ }
101
123
  }
102
124
  }
103
125
 
@@ -112,7 +134,7 @@ class Visitor {
112
134
  * @param {Buffer} buffer the buffer to write the resulting definitions into
113
135
  * @param {string?} cleanName the clean name to use. If not passed, it is derived from the passed name instead.
114
136
  */
115
- _aspectify(name, entity, buffer, cleanName = undefined) {
137
+ #aspectify(name, entity, buffer, cleanName = undefined) {
116
138
  const clean = cleanName ?? this.resolver.trimNamespace(name)
117
139
  const ns = this.resolver.resolveNamespace(name.split('.'))
118
140
  const file = this.getNamespaceFile(ns)
@@ -120,9 +142,7 @@ class Visitor {
120
142
  const identSingular = (name) => name
121
143
  const identAspect = (name) => `_${name}Aspect`
122
144
 
123
- this.contexts.push({
124
- entity: name,
125
- })
145
+ this.contexts.push({ entity: name })
126
146
 
127
147
  // CLASS ASPECT
128
148
  buffer.add(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`)
@@ -130,6 +150,7 @@ class Visitor {
130
150
  buffer.add(`return class ${clean} extends Base {`)
131
151
  buffer.indent()
132
152
 
153
+ const enums = []
133
154
  for (const [ename, element] of Object.entries(entity.elements ?? {})) {
134
155
  this.visitElement(ename, element, file, buffer)
135
156
 
@@ -138,12 +159,22 @@ class Visitor {
138
159
  // lookup in cds.definitions can fail for inline structs.
139
160
  // We don't really have to care for this case, as keys from such structs are _not_ propagated to
140
161
  // the containing entity.
141
- for (const [kname, kelement] of Object.entries(this.csn.definitions[element.target]?.keys ?? {})) {
162
+ for (const [kname, kelement] of Object.entries(this.csn.xtended.definitions[element.target]?.keys ?? {})) {
142
163
  this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
143
164
  }
144
165
  }
145
- }
146
166
 
167
+ // store inline enums for later handling, as they have to go into one common "static elements" wrapper
168
+ if (isInlineEnumType(element, this.csn.xtended)) {
169
+ enums.push(element)
170
+ }
171
+ }
172
+
173
+ buffer.indent()
174
+ for (const e of enums) {
175
+ buffer.add(`static ${e.name} = ${propertyToAnonymousEnumName(clean, e.name)}`)
176
+ file.addAnonymousEnum(clean, name, e.name, csnToEnum(e, {unwrapVals: true}))
177
+ }
147
178
  buffer.add('static actions: {')
148
179
  buffer.indent()
149
180
  for (const [aname, action] of Object.entries(entity.actions ?? {})) {
@@ -157,6 +188,7 @@ class Visitor {
157
188
  )
158
189
  }
159
190
  buffer.outdent()
191
+ buffer.outdent()
160
192
  buffer.add('}') // end of actions
161
193
 
162
194
  buffer.outdent()
@@ -192,10 +224,12 @@ class Visitor {
192
224
  }
193
225
 
194
226
  #staticClassContents(clean, entity) {
195
- return this.#isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
227
+ return this.#isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
196
228
  }
197
229
 
198
230
  #printEntity(name, entity) {
231
+ // static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
232
+ const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
199
233
  const clean = this.resolver.trimNamespace(name)
200
234
  const ns = this.resolver.resolveNamespace(name.split('.'))
201
235
  const file = this.getNamespaceFile(ns)
@@ -211,7 +245,7 @@ class Visitor {
211
245
  `Derived singular and plural forms for '${singular}' are the same. This usually happens when your CDS entities are named following singular flexion. Consider naming your entities in plural or providing '@singular:'/ '@plural:' annotations to have a clear distinction between the two. Plural form will be renamed to '${plural}' to avoid compilation errors within the output.`
212
246
  )
213
247
  }
214
- if (singular in this.csn.definitions) {
248
+ if (singular in this.csn.xtended.definitions) {
215
249
  this.logger.error(
216
250
  `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.`
217
251
  )
@@ -220,10 +254,9 @@ class Visitor {
220
254
  file.addClass(plural, name)
221
255
 
222
256
  const parent = this.resolver.resolveParent(entity.name)
223
- const buffer =
224
- parent && parent.kind === 'entity'
225
- ? file.getSubNamespace(this.resolver.trimNamespace(parent.name))
226
- : file.classes
257
+ const buffer = parent && parent.kind === 'entity'
258
+ ? file.getSubNamespace(this.resolver.trimNamespace(parent.name))
259
+ : file.classes
227
260
 
228
261
  // we can't just use "singular" here, as it may have the subnamespace removed:
229
262
  // "Books.text" is just "text" in "singular". Within the inflected exports we need
@@ -236,7 +269,7 @@ class Visitor {
236
269
  docify(entity.doc).forEach((d) => buffer.add(d))
237
270
  }
238
271
 
239
- this._aspectify(name, entity, file.classes, singular)
272
+ this.#aspectify(name, entity, file.classes, singular)
240
273
 
241
274
  // PLURAL
242
275
  if (plural.includes('.')) {
@@ -246,6 +279,8 @@ class Visitor {
246
279
  // plural can not be a type alias to $singular[] but needs to be a proper class instead,
247
280
  // so it can get passed as value to CQL functions.
248
281
  buffer.add(`export class ${plural} extends Array<${singular}> {${this.#staticClassContents(singular, entity).join('\n')}}`)
282
+ buffer.add(overrideNameProperty(singular, entity.name))
283
+ buffer.add(overrideNameProperty(plural, entity.name))
249
284
  buffer.add('')
250
285
  }
251
286
 
@@ -294,13 +329,7 @@ class Visitor {
294
329
  const ns = this.resolver.resolveNamespace(name.split('.'))
295
330
  const file = this.getNamespaceFile(ns)
296
331
  if ('enum' in type) {
297
- // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
298
- const val = (k,v) => type.type === 'cds.String' ? `"${v ?? k}"` : v
299
- file.addEnum(
300
- name,
301
- clean,
302
- Object.entries(type.enum).map(([k, v]) => [k, val(k, v.val)])
303
- )
332
+ file.addEnum(name, clean, csnToEnum(type))
304
333
  } else {
305
334
  // alias
306
335
  file.addType(name, clean, this.resolver.resolveAndRequire(type, file).typeName)
@@ -318,7 +347,7 @@ class Visitor {
318
347
  // So we separate them into another buffer which is printed before the classes.
319
348
  file.addClass(clean, name)
320
349
  file.aspects.add(`// the following represents the CDS aspect '${clean}'`)
321
- this._aspectify(name, aspect, file.aspects, clean)
350
+ this.#aspectify(name, aspect, file.aspects, clean)
322
351
  }
323
352
 
324
353
  #printEvent(name, event) {
@@ -341,6 +370,15 @@ class Visitor {
341
370
  buffer.add('}')
342
371
  }
343
372
 
373
+ #printService(name, service) {
374
+ this.logger.debug(`Printing service ${name}:\n${JSON.stringify(service, null, 2)}`)
375
+ const ns = this.resolver.resolveNamespace(name)
376
+ const file = this.getNamespaceFile(ns)
377
+ // service.name is clean of namespace
378
+ file.services.buffer.add(`export default { name: '${service.name}' }`)
379
+ file.addService(service.name)
380
+ }
381
+
344
382
  /**
345
383
  * Visits a single entity from the CSN's definition field.
346
384
  * Will call #printEntity or #printAction based on the entity's kind.
@@ -367,6 +405,9 @@ class Visitor {
367
405
  case 'event':
368
406
  this.#printEvent(name, entity)
369
407
  break
408
+ case 'service':
409
+ this.#printService(name, entity)
410
+ break
370
411
  default:
371
412
  this.logger.error(`Unhandled entity kind '${entity.kind}'.`)
372
413
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",