@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/file.js CHANGED
@@ -6,13 +6,22 @@ const { readFileSync } = require('fs')
6
6
  const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
7
7
  const { normalise } = require('./components/identifier')
8
8
  const { empty } = require('./components/typescript')
9
+ const { proxyAccessFunction } = require('./components/javascript')
9
10
  const { createObjectOf } = require('./components/wrappers')
10
11
 
11
12
  const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
12
13
 
13
14
  /** @typedef {import('./typedefs').file.Namespace} Namespace */
15
+ /** @typedef {import('./typedefs').file.FileOptions} FileOptions */
14
16
 
15
17
  class File {
18
+ /**
19
+ * The Path for this library file, which is constructed from its namespace.
20
+ * @type {Path}
21
+ */
22
+ // @ts-expect-error - not initialised, but will be done in subclasses (can't make File abstract in JS)
23
+ path
24
+
16
25
  /**
17
26
  * Creates one string from the buffers representing the type definitions.
18
27
  * @returns {string} complete file contents.
@@ -42,6 +51,9 @@ class Library extends File {
42
51
  return this.contents
43
52
  }
44
53
 
54
+ /**
55
+ * @param {string} file - path to the file
56
+ */
45
57
  constructor(file) {
46
58
  super()
47
59
  this.contents = readFileSync(file, 'utf-8')
@@ -97,12 +109,14 @@ class Library extends File {
97
109
  class SourceFile extends File {
98
110
  /**
99
111
  * @param {string | Path} path - path to the file
112
+ * @param {FileOptions} [options] - options for file output
100
113
  */
101
- constructor(path) {
114
+ constructor(path, options) {
102
115
  super()
116
+ this.options = options ?? { useEntitiesProxy: false }
103
117
  /** @type {Path} */
104
118
  this.path = path instanceof Path ? path : new Path(path.split('.'))
105
- /** @type {object} */
119
+ /** @type {{[key:string]: any}} */
106
120
  this.imports = {}
107
121
  /** @type {Buffer} */
108
122
  this.preamble = new Buffer()
@@ -110,7 +124,7 @@ class SourceFile extends File {
110
124
  this.events = { buffer: new Buffer(), fqs: []}
111
125
  /** @type {Buffer} */
112
126
  this.types = new Buffer()
113
- /** @type {{ buffer: Buffer, data: {kvs: [string[]], name: string, fq: string, property?: string}[]}} */
127
+ /** @type {{ buffer: Buffer, data: {kvs: [string, string][], name: string, fq: string, property?: string}[]}} */
114
128
  this.enums = { buffer: new Buffer(), data: [] }
115
129
  /** @type {{ buffer: Buffer }} */
116
130
  this.inlineEnums = { buffer: new Buffer() }
@@ -130,44 +144,64 @@ class SourceFile extends File {
130
144
  this.inflections = []
131
145
  /** @type {{ buffer: Buffer, names: string[]}} */
132
146
  this.services = { buffer: new Buffer(), names: [] }
147
+ /** @type {Record<string,string[]>} */
148
+ this.entityProxies = {}
133
149
  }
134
150
 
135
151
  /**
136
152
  * Stringifies a lambda expression.
137
- * @param {{name: string, parameters: [string, string][], returns: string, initialiser: string}} param - name, parameters, return type, and initialiser expression
138
- * @returns {string} - the stringified lambda
153
+ * @param {object} options - options
154
+ * @param {string} options.name - name of the lambda
155
+ * @param {import('./typedefs').visitor.ParamInfo[]} [options.parameters] - list of parameters, passed as [name, modifier, type, doc] pairs
156
+ * @param {string} [options.returns] - the return type of the function
157
+ * @param {'action' | 'function'} options.kind - kind of the lambda
158
+ * @param {string} [options.initialiser] - the initialiser expression
159
+ * @param {boolean} [options.isStatic] - whether the lambda is static
160
+ * @param {{positional?: boolean, named?: boolean}} [options.callStyles] - whether to generate positional and/or named call styles
161
+ * @param {string[]?} [options.doc] - documentation for the operation
162
+ * @returns {string} the stringified lambda
139
163
  * @example
140
164
  * ```js
141
165
  * // note: these samples are actually simplified! See below.
142
- * stringifyLambda({parameters: [['p','T']]}) // f: { (p: T): any, ... }
143
- * stringifyLambda({name: 'f', parameters: [['p','T']]}) // f: { (p: T) => any, ... }
144
- * stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // f: { (p: T) => number, ... }
145
- * stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number', initialiser: '_ => 42'}) // f: { (p: T): string = _ => 42, ... }
166
+ * stringifyLambda({parameters: [['p','','T']]}) // f: { (p: T): any, ... }
167
+ * stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}]}) // f: { (p: T) => any, ... }
168
+ * stringifyLambda({name: 'f', parameters: [{name:'p',modifier:'?',type:'T',doc:'/** doc *\/'}], returns: 'number'}) // /** doc *\/f?: { (p: T) => number, ... }
169
+ * stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number', initialiser: '_ => 42'}) // f?: { (p: T): string = _ => 42, ... }
146
170
  * ```
147
171
  *
148
- * The generated string will not be just the signature of the function. Instead, it will be an object offering a callable signature.
172
+ * The generated string will not be just the signature of the function. Instead, it will be an object offering callable signature(s).
149
173
  * On top of that, it will also expose a property `__parameters`, which is an object reflecting the functions parameters.
150
174
  * The reason for this is that the CDS runtime actually treats the function parameters as a named object. This can not be rectified via
151
175
  * type magic, as parameter names do not exist on type level. So we can not use these names to reuse them as object properties.
152
176
  * Instead, we generate this utility object for the runtime to use:
153
177
  * @example
154
178
  * ```js
155
- * stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
179
+ * stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
156
180
  * ```
157
181
  */
158
- static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false, kind}) {
159
- const parameterTypes = parameters.map(([n, t]) => `${normalise(n)}: ${t}`).join(', ')
182
+ static stringifyLambda({name, parameters=[], returns='any', kind, initialiser, isStatic=false, callStyles={positional:true, named:true}, doc}) {
183
+ let docStr = doc?.length ? doc.join('\n')+'\n' : ''
184
+ const parameterTypes = parameters.map(({name, modifier, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}${modifier}: ${type}`).join(', ')
160
185
  const parameterTypeAsObject = parameterTypes.length
161
186
  ? createObjectOf(parameterTypes)
162
187
  : empty
163
- const callableSignature = `(${parameterTypes}): ${returns}`
188
+ const callableSignatures = []
189
+ if (callStyles.positional) {
190
+ const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}: ${type}`).join(', ') // must not include ? modifiers
191
+ callableSignatures.push(`// positional\n${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
192
+ }
193
+ if (callStyles.named) {
194
+ const parameterNames = createObjectOf(parameters.map(({name}) => normalise(name)).join(', '))
195
+ callableSignatures.push(`// named\n${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
196
+ }
197
+ if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
164
198
  let prefix = name ? `${normalise(name)}: `: ''
165
199
  if (prefix && isStatic) {
166
200
  prefix = `static ${prefix}`
167
201
  }
168
202
  const kindDef = kind ? `, kind: '${kind}'` : ''
169
203
  const suffix = initialiser ? ` = ${initialiser}` : ''
170
- const lambda = `{ ${callableSignature}, __parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
204
+ const lambda = `{\n${callableSignatures.join('\n')}, \n// metadata (do not use)\n__parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
171
205
  return prefix + lambda + suffix
172
206
  }
173
207
 
@@ -187,13 +221,16 @@ class SourceFile extends File {
187
221
  /**
188
222
  * Adds a function definition in form of a arrow function to the file.
189
223
  * @param {string} name - name of the function
190
- * @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters - list of parameters, passed as [name, type] pairs
224
+ * @param {import('./typedefs').visitor.ParamInfo[]} parameters - list of parameters, passed as [name, modifier, type] tuple
191
225
  * @param {string} returns - the return type of the function
192
226
  * @param {'function' | 'action'} kind - kind of the node
227
+ * @param {string[]} doc - documentation for the function
228
+ * @param {{positional?: boolean, named?: boolean}} callStyles - how the operation can be called
193
229
  */
194
- addOperation(name, parameters, returns, kind) {
195
- //this.operations.buffer.add("// operation")
196
- this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind})};`)
230
+ addOperation(name, parameters, returns, kind, doc, callStyles) {
231
+ // this.operations.buffer.add(`// ${kind}`)
232
+ if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
233
+ this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})};`)
197
234
  this.operations.names.push(name)
198
235
  }
199
236
 
@@ -223,13 +260,11 @@ class SourceFile extends File {
223
260
  * @param {string} fq - fully qualified name of the enum (entity name within CSN)
224
261
  * @param {string} name - local name of the enum
225
262
  * @param {[string, string][]} kvs - list of key-value pairs
226
- * @param {string?} _property - property to which the enum is attached.
227
- * If given, the enum is considered to be an inline definition of an enum.
228
- * If not, it is considered to be regular, named enum.
263
+ * @param {string[]} doc - the enum docs
229
264
  */
230
- addEnum(fq, name, kvs, _property) {
265
+ addEnum(fq, name, kvs, doc) {
231
266
  this.enums.data.push({ name, fq, kvs })
232
- printEnum(this.enums.buffer, name, kvs)
267
+ printEnum(this.enums.buffer, name, kvs, {}, doc)
233
268
  }
234
269
 
235
270
  /**
@@ -238,6 +273,7 @@ class SourceFile extends File {
238
273
  * @param {string} entityFqName - name of the entity the enum is attached to with namespace
239
274
  * @param {string} propertyName - property to which the enum is attached.
240
275
  * @param {[string, string][]} kvs - list of key-value pairs
276
+ * @param {string[]} doc - the enum docs
241
277
  * If given, the enum is considered to be an inline definition of an enum.
242
278
  * If not, it is considered to be regular, named enum.
243
279
  * @example
@@ -261,14 +297,16 @@ class SourceFile extends File {
261
297
  * }
262
298
  * ```
263
299
  */
264
- addInlineEnum(entityCleanName, entityFqName, propertyName, kvs) {
300
+ addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, doc=[]) {
265
301
  this.enums.data.push({
266
302
  name: `${entityCleanName}.${propertyName}`,
267
303
  property: propertyName,
268
304
  kvs,
269
305
  fq: `${entityCleanName}.${propertyName}`
270
306
  })
271
- printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false})
307
+ const entityProxy = this.entityProxies[entityCleanName] ?? (this.entityProxies[entityCleanName] = [])
308
+ entityProxy.push(propertyName)
309
+ printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
272
310
  }
273
311
 
274
312
  /**
@@ -318,10 +356,14 @@ class SourceFile extends File {
318
356
  * @param {string} fq - fully qualified name of the enum
319
357
  * @param {string} clean - local name of the enum
320
358
  * @param {string} rhs - the right hand side of the assignment
359
+ * @param {boolean} exportValueLevel - whether to export the value level of the type (relevant to enums)
321
360
  */
322
- addType(fq, clean, rhs) {
361
+ addType(fq, clean, rhs, exportValueLevel = false) {
323
362
  this.typeNames[clean] = fq
324
363
  this.types.add(`export type ${clean} = ${rhs};`)
364
+ if (exportValueLevel) {
365
+ this.types.add(`export const ${clean} = ${rhs};`)
366
+ }
325
367
  }
326
368
 
327
369
  /**
@@ -346,11 +388,16 @@ class SourceFile extends File {
346
388
  */
347
389
  getImports() {
348
390
  const buffer = new Buffer()
391
+ if (this.services.names.length) {
392
+ // currently only needed to extend cds.Service and would trigger unused-variable-errors in strict configs
393
+ buffer.add('import cds from \'@sap/cds\'') // TODO should go to visitor#printService, but can't express this as Path
394
+ }
349
395
  for (const imp of Object.values(this.imports)) {
350
396
  if (!imp.isCwd(this.path.asDirectory())) {
351
397
  buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
352
398
  }
353
399
  }
400
+ buffer.add('') // empty line after imports
354
401
  return buffer
355
402
  }
356
403
 
@@ -377,31 +424,92 @@ class SourceFile extends File {
377
424
  namespaces.join() // needs to be after classes for possible declaration merging
378
425
  ].filter(Boolean).join('\n')
379
426
  }
427
+ #getEntityProxyFunctionExport() {
428
+ return `module.exports.createEntityProxy = ${proxyAccessFunction}`
429
+ }
430
+ /**
431
+ * Returns boilerplate code for `index.js` files
432
+ * - `useEntitiesProxy = true` -> import `createEntityProxy` function for entity proxy
433
+ * - `useEntitiesProxy = false` -> retrieve entities via `cds.entities(namespace)`
434
+ * @returns {string[]}
435
+ */
436
+ #getJSExportBoilerplate() {
437
+ const namespace = this.path.asNamespace()
380
438
 
439
+ const boilerplate = [AUTO_GEN_NOTE]
440
+ if (this.options.useEntitiesProxy) {
441
+ if (namespace === '_') {
442
+ boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
443
+ } else {
444
+ boilerplate.push(`const { createEntityProxy } = require('${new Path(['_']).asDirectory({relative: this.path.asDirectory()})}')`)
445
+ }
446
+ } else {
447
+ boilerplate.push(
448
+ 'const cds = require(\'@sap/cds\')',
449
+ `const csn = cds.entities('${namespace}')`
450
+ )
451
+ }
452
+ return boilerplate
453
+ }
454
+ /**
455
+ * Returns RHS for entity `module.exports` assignments
456
+ * - `useEntitiesProxy = true` -> use function calls to create `Proxy` objects
457
+ * - `useEntitiesProxy = false` -> access entity from CSN directly
458
+ * @param {string} singular - singular name of entity
459
+ * @param {string} original - original name of entity
460
+ * @returns {{singularRhs: string, pluralRhs: string}}
461
+ */
462
+ #getEntityExportsRhs(singular, original) {
463
+ if (this.options.useEntitiesProxy) {
464
+ const namespace = this.path.asNamespace()
465
+ // determine the custom properties for the proxy function call
466
+ const customProps = this.entityProxies[singular] ?? []
467
+ let customPropsStr = customProps.length ? `, customProps: ${JSON.stringify(customProps)}` : ''
468
+
469
+ return {
470
+ singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
471
+ pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
472
+ }
473
+ } else {
474
+ return {
475
+ singularRhs: `{ is_singular: true, __proto__: csn.${original} }`,
476
+ pluralRhs: `csn.${original}`
477
+ }
478
+ }
479
+ }
381
480
  toJSExports() {
382
- return [AUTO_GEN_NOTE, 'const cds = require(\'@sap/cds\')', `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
481
+ return this.#getJSExportBoilerplate() // boilerplate
383
482
  .concat(
384
483
  // FIXME: move stringification of service into own module
385
- this.services.names.map(name => `module.exports = { name: '${name}' }`)) // there should be only one
484
+ this.services.names.flatMap(name => {
485
+ const nameSimple = name.split('.').pop()
486
+ return [
487
+ '// service',
488
+ `const ${nameSimple} = { name: '${name}' }`,
489
+ `module.exports = ${nameSimple}`, // there should be only one and must be the first
490
+ `module.exports.${nameSimple} = ${nameSimple}`
491
+ ]
492
+ })
493
+ )
386
494
  .concat(this.inflections
387
495
  // sorting the entries based on the number of dots in their singular.
388
496
  // that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
389
497
  // "module.exports.Books" is defined before "module.exports.Books.text"
390
498
  .sort(([a], [b]) => a.split('.').length - b.split('.').length)
391
499
  .flatMap(([singular, plural, original]) => {
392
- const exports = [`module.exports.${singular} = { is_singular: true, __proto__: csn.${original} }`]
500
+ const { singularRhs, pluralRhs } = this.#getEntityExportsRhs(singular, original)
501
+
502
+ const exports = [`// ${original}`, `module.exports.${singular} = ${singularRhs}`]
393
503
  if (!/Array<.*>/.test(plural) && plural !== original) {
394
504
  // FIXME: this is a hack to support CDS types that will produce "Array<MyType>" as plural, which we do not want as export in the index.js files
395
- exports.push(`module.exports.${plural} = csn.${original}`)
505
+ exports.push(`module.exports.${plural} = ${pluralRhs}`)
396
506
  }
397
507
  // FIXME: we currently produce at most 3 entries.
398
508
  // This could be an issue when the user re-used the original name in a @singular/@plural annotation.
399
509
  // Seems unlikely, but we have to eliminate the original entry if users start running into this.
400
510
  if (singular !== original) {
401
511
  // do not do the is_singular spiel if the original name is used for the plural
402
- const rhs = plural === original
403
- ? `csn.${original}`
404
- : `{ is_singular: true, __proto__: csn.${original} }`
512
+ const rhs = plural === original ? pluralRhs : singularRhs
405
513
  exports.push(`module.exports.${original} = ${rhs}`)
406
514
  }
407
515
  return exports
@@ -438,6 +546,10 @@ class Buffer {
438
546
  * @type {string}
439
547
  */
440
548
  this.currentIndent = ''
549
+ /**
550
+ * @type {boolean}
551
+ */
552
+ this.closed = false
441
553
  }
442
554
 
443
555
  /**
@@ -483,7 +595,7 @@ class Buffer {
483
595
 
484
596
  /**
485
597
  * Adds an element to the buffer with one level of indent.
486
- * @param {string | (() => void)} part - either a string or a function. If it is a string, it is added to the buffer.
598
+ * @param {string | string[] | (() => void)} part - either a string or a function. If it is a string, it is added to the buffer.
487
599
  * If not, it is expected to be a function that manipulates the buffer as a side effect.
488
600
  */
489
601
  addIndented(part) {
@@ -492,14 +604,16 @@ class Buffer {
492
604
  part()
493
605
  } else if (Array.isArray(part)) {
494
606
  part.forEach(p => { this.add(p) })
607
+ } else if (typeof part === 'string') {
608
+ this.add(part)
495
609
  }
496
610
  this.outdent()
497
611
  }
498
612
 
499
613
  /**
500
- * Adds an element to a buffer with one level of indent and and opener and closer surrounding it.
614
+ * Adds an element to a buffer with one level of indent and opener and closer surrounding it.
501
615
  * @param {string} opener - the string to put before the indent
502
- * @param {string} content - the content to indent (see {@link addIndented})
616
+ * @param {string | string[] | (() => void)} content - the content to indent (see {@link addIndented})
503
617
  * @param {string} closer - the string to put after the indent
504
618
  */
505
619
  addIndentedBlock(opener, content, closer) {
@@ -518,7 +632,7 @@ class Path {
518
632
  * @param {string[]} parts - parts of the path. 'a.b.c' -> ['a', 'b', 'c']
519
633
  * @param {string} kind - FIXME: currently unused
520
634
  */
521
- constructor(parts, kind) {
635
+ constructor(parts, kind = '') {
522
636
  this.parts = parts
523
637
  this.kind = kind
524
638
  }
@@ -533,9 +647,9 @@ class Path {
533
647
  /**
534
648
  * Transfoms the Path into a directory path.
535
649
  * @param {object} params - parameters
536
- * @param {string?} params.relative - if defined, the path is constructed relative to this directory
537
- * @param {boolean} params.local - if set to true, './' is prefixed to the directory
538
- * @param {boolean} params.posix - if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
650
+ * @param {string} [params.relative] - if defined, the path is constructed relative to this directory
651
+ * @param {boolean} [params.local] - if set to true, './' is prefixed to the directory
652
+ * @param {boolean} [params.posix] - if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
539
653
  * @returns {string} directory 'a.b.c'.asDirectory() -> 'a/b/c' (or a\b\c when on Windows without passing posix = true)
540
654
  */
541
655
  asDirectory(params = {}) {
@@ -569,7 +683,7 @@ class Path {
569
683
  }
570
684
 
571
685
  /**
572
- * @param {string} relative - directory to which we check relatively
686
+ * @param {string} [relative] - directory to which we check relatively
573
687
  * @returns {boolean} true, iff the Path refers to the current working directory, aka './'
574
688
  */
575
689
  isCwd(relative = undefined) {
@@ -580,8 +694,16 @@ class Path {
580
694
  // TODO: having the repository pattern in place we can separate (some of) the printing logic from the visitor.
581
695
  // Most of it hinges primarily on resolving specific files. We can now pass the repository and the resolver to a printer.
582
696
  class FileRepository {
697
+ /** @type {{[key:string]: SourceFile}} */
583
698
  #files = {}
584
699
 
700
+ /**
701
+ * @param {FileOptions} options - options to control file
702
+ */
703
+ constructor(options) {
704
+ this.options = options
705
+ }
706
+
585
707
  /**
586
708
  * @param {string} name - file name
587
709
  * @param {SourceFile} file - the file
@@ -598,7 +720,7 @@ class FileRepository {
598
720
  */
599
721
  getNamespaceFile(path) {
600
722
  const key = path instanceof Path ? path.asNamespace() : path
601
- return (this.#files[key] ??= new SourceFile(path))
723
+ return (this.#files[key] ??= new SourceFile(path, this.options))
602
724
  }
603
725
 
604
726
  /**
@@ -628,7 +750,7 @@ const writeout = async (root, sources) =>
628
750
  fs.writeFile(path.join(dir, 'index.js'), source.toJSExports()),
629
751
  ])
630
752
 
631
- } catch (err) {
753
+ } catch (/** @type {any} **/err) {
632
754
  // eslint-disable-next-line no-console
633
755
  console.error(`Could not create parent directory ${dir}: ${err}.`)
634
756
  // eslint-disable-next-line no-console
package/lib/logging.js CHANGED
@@ -1,12 +1,16 @@
1
1
  const cds = require('@sap/cds')
2
2
 
3
+ /** @param {string} value - the value */
4
+ // @ts-expect-error - yes, cds.log.levels exists...
3
5
  const _keyFor = value => Object.entries(cds.log.levels).find(([,val]) => val === value)?.[0]
4
6
 
5
7
  // workaround until retroactively setting log level to 0 is possible
8
+ // @ts-expect-error - yes, cds.log.levels exists...
6
9
  cds.log('cds-typer', _keyFor(cds.log.levels.SILENT))
7
10
  module.exports = {
8
11
  _keyFor,
9
- setLevel: level => { cds.log('cds-typer', level) },
12
+ setLevel: (/** @type {string | number} */ level) => { cds.log('cds-typer', level) },
13
+ /** @type {Record<string, string>} */
10
14
  deprecated: {
11
15
  WARNING: 'WARN',
12
16
  CRITICAL: 'ERROR',
@@ -2,6 +2,7 @@ class BuiltinResolver {
2
2
  /**
3
3
  * Builtin types defined by CDS.
4
4
  */
5
+ /** @type {Record<string, string>} */
5
6
  #builtins = {
6
7
  UUID: 'string',
7
8
  String: 'string',
@@ -32,7 +33,7 @@ class BuiltinResolver {
32
33
 
33
34
  /**
34
35
  * @param {object} options - additional resolution options
35
- * @param {boolean} options.IEEE754Compatible - if true, the Decimal, DecimalFloat, Float, and Double types are also allowed to be strings
36
+ * @param {boolean} [options.IEEE754Compatible] - if true, the Decimal, DecimalFloat, Float, and Double types are also allowed to be strings
36
37
  */
37
38
  constructor ({ IEEE754Compatible } = {}) {
38
39
  if (IEEE754Compatible) {
@@ -45,7 +46,7 @@ class BuiltinResolver {
45
46
  }
46
47
 
47
48
  /**
48
- * @param {string | string[]} t - name or parts of the type name split on dots
49
+ * @param {string | string[] | import("@sap/cds").ref} t - name or parts of the type name split on dots
49
50
  * @returns {string | undefined | false} if t refers to a builtin, the name of the corresponding TS type is returned.
50
51
  * If t _looks like_ a builtin (`cds.X`), undefined is returned.
51
52
  * If t is obviously not a builtin, false is returned.
@@ -1,3 +1,5 @@
1
+ const { isType } = require('../csn')
2
+
1
3
  class EntityInfo {
2
4
  /**
3
5
  * @example
@@ -6,7 +8,7 @@ class EntityInfo {
6
8
  * // v
7
9
  * Path(['n1', 'n2'])
8
10
  * ```
9
- * @type {Path}
11
+ * @type {import('../file').Path}
10
12
  */
11
13
  namespace
12
14
 
@@ -44,7 +46,7 @@ class EntityInfo {
44
46
  */
45
47
  propertyAccess
46
48
 
47
- /** @type {{singular: string, plural: string}} */
49
+ /** @type {{singular: string, plural: string} | undefined} */
48
50
  #inflection
49
51
 
50
52
  /** @type {import('./resolver').Resolver} */
@@ -53,10 +55,10 @@ class EntityInfo {
53
55
  /** @type {EntityRepository} */
54
56
  #repository
55
57
 
56
- /** @type {EntityInfo} */
57
- #parent
58
+ /** @type {EntityInfo | null} */
59
+ #parent = null
58
60
 
59
- /** @type {import('../typedefs').resolver.EntityCSN} */
61
+ /** @type {import('../typedefs').resolver.EntityCSN | undefined} */
60
62
  #csn
61
63
 
62
64
  get csn () {
@@ -124,7 +126,7 @@ class EntityInfo {
124
126
  }
125
127
 
126
128
  class EntityRepository {
127
- /** @type {{ [key: string]: EntityInfo }} */
129
+ /** @type {{ [key: string]: EntityInfo | null }} */
128
130
  #cache = {}
129
131
 
130
132
  /** @type {import('./resolver').Resolver} */
@@ -142,6 +144,19 @@ class EntityRepository {
142
144
  return this.#cache[fq]
143
145
  }
144
146
 
147
+ /**
148
+ * Convenience for getByFq when you are 100% sure the entity exists.
149
+ * Serves to eliminate cumbersome null-handling where you know it's not necessary.
150
+ * For example when fq is derived from a reference to another entity.
151
+ * @param {string} fq - fully qualified name of the entity
152
+ * @returns {EntityInfo}
153
+ */
154
+ getByFqOrThrow(fq) {
155
+ const entityInfo = this.getByFq(fq)
156
+ if (entityInfo === null) throw new Error(`Entity with fq "${fq}" is not part of the model`)
157
+ return entityInfo
158
+ }
159
+
145
160
  /**
146
161
  * @param {import('./resolver').Resolver} resolver - the resolver
147
162
  */
@@ -150,6 +165,30 @@ class EntityRepository {
150
165
  }
151
166
  }
152
167
 
168
+ /**
169
+ * Derives an identifier from an entity info.
170
+ * That identifier can be used to refer to a specific entity within an index.ts file.
171
+ * By passing a relative file, the identifier will be preceeded with a scope if needed.
172
+ * @param {object} options - the options
173
+ * @param {EntityInfo} options.info - the entity info
174
+ * @param {function(string): string} [options.wrapper] - a function to wrap the identifier
175
+ * @param {import('../file').Path} [options.relative] - the path to resolve the identifier relative to
176
+ * @returns {string} the identifier
177
+ */
178
+ function asIdentifier ({info, wrapper = undefined, relative = undefined}) {
179
+ const name = isType(info.csn)
180
+ ? info.entityName
181
+ : info.inflection.singular
182
+
183
+ const wrapped = typeof wrapper === 'function'
184
+ ? wrapper(name)
185
+ : name
186
+ return !relative || relative.isCwd(info.namespace.asDirectory())
187
+ ? wrapped
188
+ : `${info.namespace.asIdentifier()}.${wrapped}`
189
+ }
190
+
153
191
  module.exports = {
154
- EntityRepository
192
+ EntityRepository,
193
+ asIdentifier
155
194
  }