@cap-js/cds-typer 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/visitor.js CHANGED
@@ -2,29 +2,34 @@
2
2
 
3
3
  const util = require('./util')
4
4
 
5
- const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality } = require('./csn')
5
+ const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum } = require('./csn')
6
6
  // eslint-disable-next-line no-unused-vars
7
- const { SourceFile, FileRepository, Buffer } = require('./file')
7
+ const { SourceFile, FileRepository, Buffer, Path } = require('./file')
8
8
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
9
9
  const { Resolver } = require('./resolution/resolver')
10
10
  const { LOG } = require('./logging')
11
- const { docify } = require('./components/wrappers')
11
+ const { docify, createPromiseOf, createUnionOf } = require('./components/wrappers')
12
12
  const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
13
13
  const { isReferenceType } = require('./components/reference')
14
14
  const { empty } = require('./components/typescript')
15
15
  const { baseDefinitions } = require('./components/basedefs')
16
- const { EntityRepository } = require('./resolution/entity')
16
+ const { EntityRepository, asIdentifier } = require('./resolution/entity')
17
17
  const { last } = require('./components/identifier')
18
+ const { getPropertyModifiers } = require('./components/property')
18
19
 
19
20
  /** @typedef {import('./file').File} File */
20
21
  /** @typedef {import('./typedefs').visitor.Context} Context */
21
22
  /** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
22
23
  /** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
23
24
  /** @typedef {import('./typedefs').visitor.Inflection} Inflection */
25
+ /** @typedef {import('./typedefs').resolver.CSN} CSN */
26
+ /** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
27
+ /** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
24
28
 
25
29
  const defaults = {
26
30
  // FIXME: add defaults for remaining parameters
27
31
  propertiesOptional: true,
32
+ useEntitiesProxy: false,
28
33
  inlineDeclarations: 'flat'
29
34
  }
30
35
 
@@ -36,12 +41,12 @@ class Visitor {
36
41
  * @returns {File[]} a full list of files to be written
37
42
  */
38
43
  getWriteoutFiles() {
39
- return this.fileRepository.getFiles().concat(this.resolver.getUsedLibraries())
44
+ return [...this.fileRepository.getFiles(), ...this.resolver.getUsedLibraries()]
40
45
  }
41
46
 
42
47
  /**
43
48
  * @param {{xtended: CSN, inferred: CSN}} csn - root CSN
44
- * @param {VisitorOptions} options - the options
49
+ * @param {VisitorOptions | {}} options - the options
45
50
  */
46
51
  constructor(csn, options = {}) {
47
52
  amendCSN(csn.xtended)
@@ -59,7 +64,9 @@ class Visitor {
59
64
  this.entityRepository = new EntityRepository(this.resolver)
60
65
 
61
66
  /** @type {FileRepository} */
62
- this.fileRepository = new FileRepository()
67
+ this.fileRepository = new FileRepository(this.options)
68
+ // REVISIT: better way to pass options to base source file ???
69
+ baseDefinitions.options = this.options
63
70
  this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
64
71
  this.inlineDeclarationResolver =
65
72
  this.options.inlineDeclarations === 'structured'
@@ -124,6 +131,35 @@ class Visitor {
124
131
  })
125
132
  }
126
133
 
134
+ /**
135
+ * @param {EntityCSN} entity - the entity to print the actions for
136
+ * @param {Buffer} buffer - the buffer to write the actions into
137
+ * @param {import('./typedefs').resolver.EntityInfo[]} ancestors - the fully qualified names of the ancestors of the entity
138
+ * @param {SourceFile} file - the file the entity is being printed into
139
+ */
140
+ #printStaticActions(entity, buffer, ancestors, file) {
141
+ // TODO: refactor away! All these printing functionalities need to go
142
+ const actions = Object.entries(entity.actions ?? {})
143
+ const inherited = ancestors.length
144
+ ? ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`).join(' & ') + ' & '
145
+ : ''
146
+ if (actions.length) {
147
+ buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
148
+ actions.map(([aname, action]) => SourceFile.stringifyLambda({
149
+ name: aname,
150
+ parameters: this.#stringifyFunctionParams(action.params, file),
151
+ returns: action.returns
152
+ ? this.resolver.resolveAndRequire(action.returns, file).typeName
153
+ : 'any',
154
+ kind: action.kind,
155
+ doc: docify(action.doc)
156
+ })), '}'
157
+ ) // end of actions
158
+ } else {
159
+ buffer.add(`declare static readonly actions: ${inherited}${empty}`)
160
+ }
161
+ }
162
+
127
163
  /**
128
164
  * Transforms an entity or CDS aspect into a JS aspect (aka mixin).
129
165
  * That is, for an element A we get:
@@ -131,42 +167,65 @@ class Visitor {
131
167
  * - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
132
168
  * - the type A to use for external typing and is derived from AXtended.
133
169
  * @param {string} fq - the name of the entity
134
- * @param {CSN} entity - the pointer into the CSN to extract the elements from
170
+ * @param {EntityCSN} entity - the pointer into the CSN to extract the elements from
135
171
  * @param {Buffer} buffer - the buffer to write the resulting definitions into
136
172
  * @param {{cleanName?: string}} options - additional options
137
173
  */
138
174
  #aspectify(fq, entity, buffer, options = {}) {
139
- const info = this.entityRepository.getByFq(fq)
175
+ const info = this.entityRepository.getByFqOrThrow(fq)
140
176
  const clean = options?.cleanName ?? info.withoutNamespace
141
177
  const { namespace } = info
142
178
  const file = this.fileRepository.getNamespaceFile(namespace)
143
- const identSingular = name => name
179
+ const identSingular = (/** @type {string} */name) => name // FIXME: remove
180
+ //const identAspect = name => `_${name}Aspect`
181
+ /** @param {string} name - the name */
144
182
  const identAspect = name => `_${name}Aspect`
145
- const toAspectIdent = (wrapped, [ns, n, fq]) => {
183
+ /**
184
+ * @param {object} options - options
185
+ * @param {Path} [options.ns] - namespace
186
+ * @param {string} options.clean - the clean name of the entity
187
+ * @param {string} options.fq - fully qualified name
188
+ * @returns {string} the local identifier
189
+ */
190
+ // FIXME: replace with resolution/entity::asIdentifier
191
+ const toLocalIdent = ({ns, clean, fq}) => {
146
192
  // types are not inflected, so don't change those to singular
147
- const refersToType = isType(this.csn.inferred.definitions[fq])
148
- const ident = identAspect(refersToType
149
- ? n
150
- : this.resolver.inflect({csn: this.csn.inferred.definitions[fq], plainName: n}).singular
151
- )
193
+ const csn = this.csn.inferred.definitions[fq]
194
+ const ident = isType(csn)
195
+ ? clean
196
+ : this.resolver.inflect({csn, plainName: clean}).singular
152
197
  return !ns || ns.isCwd(file.path.asDirectory())
153
- ? `${ident}(${wrapped})`
154
- : `${ns.asIdentifier()}.${ident}(${wrapped})`
198
+ ? ident
199
+ : `${ns.asIdentifier()}.${ident}`
155
200
  }
156
- const ancestors = (entity.includes ?? [])
157
- .map(parent => {
158
- const { namespace, entityName } = this.entityRepository.getByFq(parent)
159
- file.addImport(namespace)
160
- return [namespace, entityName, parent]
201
+ // remove the ancestry of projections/ views.
202
+ // They explicitly define their properties.
203
+ // But at the same time they also carry their .includes clause, which can
204
+ // clash when the user aliases a selected property with the name of
205
+ // a properties of the entities they project on:
206
+ // entity foo as SELECT bar.baz AS ID FROM bar
207
+ // will produce an error if bar also has a property ID which now clashes with foo.ID
208
+ // WARNING: annotations from entities without properties should actually be propagated this way!
209
+ // So once we start caring about annotations, we have to revisit this part.
210
+ /** @type {import('./typedefs').resolver.EntityInfo[]} */
211
+ const ancestorInfos = ((!isViewOrProjection(entity) ? entity.includes : []) ?? [])
212
+ .map(ancestor => {
213
+ const info = this.entityRepository.getByFq(ancestor)
214
+ if (!info) throw new Error(`could not resolve ancestor ${ancestor} for ${fq}`)
215
+ file.addImport(info.namespace)
216
+ return info
161
217
  })
218
+
219
+ const ancestorsAspects = ancestorInfos
162
220
  .reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
163
- .reduce(toAspectIdent, 'Base')
221
+ .reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
164
222
 
165
223
  this.contexts.push({ entity: fq })
166
224
 
167
225
  // CLASS ASPECT
168
226
  buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
169
- buffer.addIndentedBlock(`return class ${clean} extends ${ancestors} {`, () => {
227
+ buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
228
+ /** @type {import('./typedefs').resolver.EnumCSN[]} */
170
229
  const enums = []
171
230
  for (let [ename, element] of Object.entries(entity.elements ?? {})) {
172
231
  if (element.target && /\.texts?/.test(element.target)) {
@@ -176,14 +235,14 @@ class Visitor {
176
235
  this.visitElement(ename, element, file, buffer)
177
236
 
178
237
  // make foreign keys explicit
179
- if ('target' in element) {
238
+ if (element.target) {
180
239
  // lookup in cds.definitions can fail for inline structs.
181
240
  // We don't really have to care for this case, as keys from such structs are _not_ propagated to
182
241
  // the containing entity.
183
242
  for (const [kname, originalKeyElement] of this.#keys(element.target)) {
184
243
  if (getMaxCardinality(element) === 1 && typeof element.on !== 'object') { // FIXME: kelement?
185
244
  const foreignKey = `${ename}_${kname}`
186
- if (Object.hasOwn(entity.elements, foreignKey)) {
245
+ if (entity.elements && Object.hasOwn(entity.elements, foreignKey)) {
187
246
  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.`)
188
247
  } else {
189
248
  const kelement = Object.assign(Object.create(originalKeyElement), {
@@ -201,42 +260,49 @@ class Visitor {
201
260
  }
202
261
  }
203
262
 
263
+ if ('kind' in entity) {
264
+ buffer.addIndented([`static readonly kind: 'entity' | 'type' | 'aspect' = '${entity.kind}';`])
265
+ }
266
+
204
267
  buffer.addIndented(() => {
205
268
  for (const e of enums) {
269
+ const eDoc = docify(e.doc)
270
+ eDoc.forEach(d => { buffer.add(d) })
206
271
  buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
207
- file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
208
- }
209
- const actions = Object.entries(entity.actions ?? {})
210
- if (actions.length) {
211
- buffer.addIndentedBlock('static readonly actions: {',
212
- actions.map(([aname, action]) => SourceFile.stringifyLambda({
213
- name: aname,
214
- parameters: this.#stringifyFunctionParams(action.params, file),
215
- returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
216
- kind: action.kind
217
- }))
218
- , '}') // end of actions
219
- } else {
220
- buffer.add(`static readonly actions: ${empty}`)
272
+ file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
221
273
  }
274
+ this.#printStaticActions(entity, buffer, ancestorInfos, file)
222
275
  })
223
276
  }, '};') // end of generated class
224
277
  }, '}') // end of aspect
225
278
 
226
279
  // CLASS WITH ADDED ASPECTS
227
280
  file.addImport(baseDefinitions.path)
228
- buffer.add(`export class ${identSingular(clean)} extends ${toAspectIdent(`${baseDefinitions.path.asIdentifier()}.Entity`, [undefined, clean, fq])} {${this.#staticClassContents(clean, entity).join('\n')}}`)
281
+ docify(entity.doc).forEach(d => { buffer.add(d) })
282
+ buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(clean, entity).join('\n')}}`)
229
283
  this.contexts.pop()
230
284
  }
231
285
 
286
+ /**
287
+ * @param {string} clean - the clean name of the entity
288
+ * @param {EntityCSN} entity - the entity to generate the static contents for
289
+ */
232
290
  #staticClassContents(clean, entity) {
233
291
  return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
234
292
  }
235
293
 
294
+ /**
295
+ * @param {string} fq - fully qualified name of the entity
296
+ * @param {EntityCSN} entity - the entity to print
297
+ */
236
298
  #printEntity(fq, entity) {
237
299
  // static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
300
+ /**
301
+ * @param {string} clazz - the class to override the name property for
302
+ * @param {string} content - the content to set the name property to
303
+ */
238
304
  const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
239
- const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFq(fq)
305
+ const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFqOrThrow(fq)
240
306
  const file = this.fileRepository.getNamespaceFile(ns)
241
307
  let { singular, plural } = inflection
242
308
 
@@ -272,7 +338,6 @@ class Visitor {
272
338
  // which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
273
339
  // edge case: @singular annotation present. singular4 will take care of that.
274
340
  file.addInflection(util.singular4(entity, true), plural, clean)
275
- docify(entity.doc).forEach(d => { buffer.add(d) })
276
341
 
277
342
  // in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
278
343
  const target = isProjection(entity) || isView(entity)
@@ -299,6 +364,7 @@ class Visitor {
299
364
  // so it can get passed as value to CQL functions.
300
365
  const additionalProperties = this.#staticClassContents(singular, entity)
301
366
  additionalProperties.push('$count?: number')
367
+ docify(entity.doc).forEach(d => { buffer.add(d) })
302
368
  buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
303
369
  buffer.add(overrideNameProperty(plural, entity.name))
304
370
  }
@@ -309,72 +375,98 @@ class Visitor {
309
375
  * Stringifies function parameters in preparation of passing them to {@link SourceFile.stringifyLambda}.
310
376
  * Resolves all parameters to a pair of parameter name and name of the resolved type.
311
377
  * Also filters out parameters that indicate a binding parameter ({@link https://cap.cloud.sap/docs/releases/jan23#simplified-syntax-for-binding-parameters}).
312
- * @param {[string, object][]} params - parameter list as found in CSN.
313
- * @param {File} file - source file relative to which the parameter types should be resolved.
314
- * @returns {[string, string][]} pair of names and types.
378
+ * @param {{[key:string]: EntityCSN}} params - parameter list as found in CSN.
379
+ * @param {SourceFile} file - source file relative to which the parameter types should be resolved.
380
+ * @returns {import('./typedefs').visitor.ParamInfo[]} tuple of name, modifier, type and doc.
315
381
  */
316
382
  #stringifyFunctionParams(params, file) {
317
- return params
318
- ? Object.entries(params)
319
- // filter params of type '[many] $self', as they are not to be part of the implementation
320
- .filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self'))
321
- .map(([name, type]) => [
322
- name,
323
- this.#stringifyFunctionParamType(type, file)
324
- ])
325
- : []
383
+ return Object.entries(params ?? {})
384
+ // filter params of type '[many] $self', as they are not to be part of the implementation
385
+ .filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
386
+ .map(([name, type]) => ({
387
+ name,
388
+ modifier: this.resolver.isOptional(type) ? '?' : '',
389
+ type: this.#stringifyFunctionParamType(type, file),
390
+ doc: docify(type.doc).join('\n'),
391
+ }))
326
392
  }
327
393
 
394
+ /**
395
+ * @param {EntityCSN | EnumCSN} type - type
396
+ * @param {SourceFile} file - the file to resolve types into
397
+ */
328
398
  #stringifyFunctionParamType(type, file) {
329
399
  // if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
330
400
  // at a named enum type. In that case also resolve that type name
331
- if (type.enum && this.resolver.builtinResolver.resolveBuiltin(type.type)) return stringifyEnumType(csnToEnumPairs(type))
401
+ const isNamedEnumType = isEnum(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)
402
+ if (isNamedEnumType) return stringifyEnumType(csnToEnumPairs(type))
332
403
  const paramType = this.resolver.resolveAndRequire(type, file)
333
404
  return this.inlineDeclarationResolver.getPropertyDatatype(
334
405
  paramType,
335
- paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire ? paramType.typeName : paramType.typeInfo.inflection.singular
406
+ paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire
407
+ ? paramType.typeName
408
+ : paramType.typeInfo.inflection.singular
336
409
  )
337
410
  }
338
411
 
339
412
  /**
340
413
  * @param {string} fq - fully qualified name of the operation
341
- * @param {object} operation - CSN
414
+ * @param {import('./typedefs').resolver.OperationCSN} operation - CSN
342
415
  * @param {'function' | 'action'} kind - kind of operation
343
416
  */
344
417
  #printOperation(fq, operation, kind) {
345
418
  LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
346
- const { namespace } = this.entityRepository.getByFq(fq)
419
+ const { namespace } = this.entityRepository.getByFqOrThrow(fq)
347
420
  const file = this.fileRepository.getNamespaceFile(namespace)
348
421
  const params = this.#stringifyFunctionParams(operation.params, file)
349
422
  const returnType = operation.returns
350
423
  ? this.resolver.resolveAndRequire(operation.returns, file)
351
- : { typeName: 'void', typeInfo: { isArray: false, inflection: { singular: 'void', plural: 'void' } } }
352
- const returns = this.inlineDeclarationResolver.getPropertyDatatype(
424
+ : { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
425
+ let returns = this.inlineDeclarationResolver.getPropertyDatatype(
353
426
  returnType,
354
- returnType.typeInfo.isArray ? returnType.typeName : returnType.typeInfo.inflection.singular
427
+ returnType.typeInfo.isArray
428
+ ? returnType.typeName
429
+ : returnType.typeInfo.inflection.singular
355
430
  )
356
- file.addOperation(last(fq), params, returns, kind)
431
+ if (operation.returns) {
432
+ // operation results may be a Promise
433
+ returns = createUnionOf(createPromiseOf(returns), returns)
434
+ }
435
+ // Actions for ABAP RFC modules have 'parameter categories' (import/export/changing/tables) that cannot be called in a flat order.
436
+ // Prevent positional call style there.
437
+ // TODO find a better way to detect ABAP RFC actions
438
+ const isRFC = Object.values(operation.params ?? {}).some(p => Object.keys(p).some(k => k.startsWith('@RFC')))
439
+ file.addOperation(last(fq), params, returns, kind, docify(operation.doc), {named: true, positional: !isRFC})
357
440
  }
358
441
 
442
+ /**
443
+ * @param {string} fq - fully qualified name of the type
444
+ * @param {EntityCSN} type - CSN
445
+ */
359
446
  #printType(fq, type) {
360
447
  LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
361
- const { namespace, entityName } = this.entityRepository.getByFq(fq)
448
+ const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
362
449
  const file = this.fileRepository.getNamespaceFile(namespace)
363
450
  // skip references to enums.
364
451
  // "Base" enums will always have a builtin type (don't skip those).
365
452
  // A type referencing an enum E will be considered an enum itself and have .type === E (skip).
366
- if ('enum' in type && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
367
- file.addEnum(fq, entityName, csnToEnumPairs(type))
453
+ if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
454
+ file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
368
455
  } else {
456
+ const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
369
457
  // alias
370
- file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
458
+ file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
371
459
  }
372
460
  // TODO: annotations not handled yet
373
461
  }
374
462
 
463
+ /**
464
+ * @param {string} fq - fully qualified name of the aspect
465
+ * @param {EntityCSN} aspect - CSN
466
+ */
375
467
  #printAspect(fq, aspect) {
376
468
  LOG.debug(`Printing aspect ${fq}`)
377
- const { namespace, entityName, inflection } = this.entityRepository.getByFq(fq)
469
+ const { namespace, entityName, inflection } = this.entityRepository.getByFqOrThrow(fq)
378
470
  const file = this.fileRepository.getNamespaceFile(namespace)
379
471
  // aspects are technically classes and can therefore be added to the list of defined classes.
380
472
  // Still, when using them as mixins for a class, they need to already be defined.
@@ -384,9 +476,12 @@ class Visitor {
384
476
  this.#aspectify(fq, aspect, file.aspects, { cleanName: inflection.singular })
385
477
  }
386
478
 
479
+ /**
480
+ * @param {string} fq - fully qualified name of the event
481
+ * @param {EntityCSN} event - CSN
482
+ */
387
483
  #printEvent(fq, event) {
388
- LOG.debug(`Printing event ${fq}`)
389
- const { namespace, entityName } = this.entityRepository.getByFq(fq)
484
+ const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
390
485
  const file = this.fileRepository.getNamespaceFile(namespace)
391
486
  file.addEvent(entityName, fq)
392
487
  const buffer = file.events.buffer
@@ -402,12 +497,27 @@ class Visitor {
402
497
  }, '}')
403
498
  }
404
499
 
500
+ /**
501
+ * @param {string} fq - fully qualified name of the service
502
+ * @param {import('./typedefs').resolver.EntityCSN} service - CSN
503
+ */
405
504
  #printService(fq, service) {
406
505
  LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
407
- const { namespace } = this.entityRepository.getByFq(fq)
506
+ const { namespace } = this.entityRepository.getByFqOrThrow(fq)
408
507
  const file = this.fileRepository.getNamespaceFile(namespace)
409
- // service.name is clean of namespace
410
- file.services.buffer.add(`export default { name: '${service.name}' }`)
508
+ const buffer = file.services.buffer
509
+ const serviceNameSimple = service.name.split('.').pop()
510
+
511
+ docify(service.doc).forEach(d => { buffer.add(d) })
512
+ // file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
513
+ buffer.addIndentedBlock(`export class ${serviceNameSimple} extends cds.Service {`, () => {
514
+ Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
515
+ docify(doc).forEach(d => { buffer.add(d) })
516
+ buffer.add(`declare ${name}: typeof ${name}`)
517
+ })
518
+ }, '}')
519
+ buffer.add(`export default ${serviceNameSimple}`)
520
+ buffer.add('')
411
521
  file.addService(service.name)
412
522
  }
413
523
 
@@ -415,7 +525,7 @@ class Visitor {
415
525
  * Visits a single entity from the CSN's definition field.
416
526
  * Will call #printEntity or #printAction based on the entity's kind.
417
527
  * @param {string} fq - name of the entity, fully qualified as is used in the definition field.
418
- * @param {CSN} entity - CSN data belonging to the entity to perform lookups in.
528
+ * @param {EntityCSN} entity - CSN data belonging to the entity to perform lookups in.
419
529
  */
420
530
  visitEntity(fq, entity) {
421
531
  switch (entity.kind) {
@@ -424,6 +534,7 @@ class Visitor {
424
534
  break
425
535
  case 'action':
426
536
  case 'function':
537
+ // @ts-expect-error - we know entity is actually an OperationCSN
427
538
  this.#printOperation(fq, entity, entity.kind)
428
539
  break
429
540
  case 'aspect':
@@ -470,16 +581,25 @@ class Visitor {
470
581
  /**
471
582
  * Visits a single element in an entity.
472
583
  * @param {string} name - name of the element
473
- * @param {import('./resolution/resolver').CSN} element - CSN data belonging to the the element.
584
+ * @param {EntityCSN} element - CSN data belonging to the the element.
474
585
  * @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
475
- * @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
586
+ * @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
476
587
  * @returns @see InlineDeclarationResolver.visitElement
477
588
  */
478
- visitElement(name, element, file, buffer) {
479
- return this.inlineDeclarationResolver.visitElement(name, element, file, buffer)
589
+ visitElement(name, element, file, buffer = file.classes) {
590
+ return this.inlineDeclarationResolver.visitElement({
591
+ name,
592
+ element,
593
+ file,
594
+ buffer,
595
+ // we explicitly pass the "declare" modifier here to avoid problems with noImplicitOverride and useDefineForClassFields in strict tsconfigs
596
+ // but not inside type defs (e.g. parameter types) where this would be a syntax error
597
+ modifiers: getPropertyModifiers(element)
598
+ })
480
599
  }
481
600
  }
482
601
 
483
602
  module.exports = {
484
603
  Visitor
485
604
  }
605
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.24.0",
3
+ "version": "0.26.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",
@@ -24,7 +24,8 @@
24
24
  "doc:clean": "rm -rf ./doc",
25
25
  "doc:prepare": "npm run doc:clean && mkdir -p doc/types",
26
26
  "doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
27
- "doc:cli": "npm run cli -- --help > ./doc/cli.txt"
27
+ "doc:cli": "npm run cli -- --help > ./doc/cli.txt",
28
+ "jsdoc:check": "tsc --noEmit --project jsconfig.json"
28
29
  },
29
30
  "files": [
30
31
  "lib/",
@@ -39,14 +40,17 @@
39
40
  "bin": {
40
41
  "cds-typer": "./lib/cli.js"
41
42
  },
42
- "dependencies": {
43
- "@sap/cds": ">=7.7"
43
+ "peerDependencies": {
44
+ "@cap-js/cds-types": ">=0.6.4",
45
+ "@sap/cds": ">=8"
44
46
  },
45
47
  "devDependencies": {
46
- "@stylistic/eslint-plugin-js": "^1.6.3",
48
+ "@cap-js/cds-types": "^0",
49
+ "@sap/cds": "^8",
50
+ "@stylistic/eslint-plugin-js": "^2.7.2",
47
51
  "acorn": "^8.10.0",
48
52
  "eslint": "^9",
49
- "eslint-plugin-jsdoc": "^48.2.7",
53
+ "eslint-plugin-jsdoc": "^50.2.2",
50
54
  "globals": "^15.0.0",
51
55
  "jest": "^29",
52
56
  "typescript": ">=4.6.4"