@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/CHANGELOG.md +28 -2
- package/README.md +19 -0
- package/lib/cli.js +30 -14
- package/lib/compile.js +3 -3
- package/lib/components/enum.js +19 -10
- package/lib/components/identifier.js +1 -1
- package/lib/components/inline.js +81 -26
- package/lib/components/javascript.js +28 -0
- package/lib/components/property.js +12 -0
- package/lib/components/wrappers.js +27 -4
- package/lib/csn.js +90 -26
- package/lib/file.js +166 -44
- package/lib/logging.js +5 -1
- package/lib/resolution/builtin.js +3 -2
- package/lib/resolution/entity.js +46 -7
- package/lib/resolution/resolver.js +60 -20
- package/lib/typedefs.d.ts +83 -13
- package/lib/util.js +4 -3
- package/lib/visitor.js +199 -79
- package/package.json +10 -6
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 {
|
|
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[]
|
|
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 {
|
|
138
|
-
* @
|
|
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: [
|
|
144
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
145
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
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
|
|
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: [
|
|
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,
|
|
159
|
-
|
|
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
|
|
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 = `{
|
|
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 {
|
|
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(
|
|
196
|
-
this.operations.buffer.add(
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
481
|
+
return this.#getJSExportBoilerplate() // boilerplate
|
|
383
482
|
.concat(
|
|
384
483
|
// FIXME: move stringification of service into own module
|
|
385
|
-
this.services.names.
|
|
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
|
|
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} =
|
|
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
|
|
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
|
|
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.
|
package/lib/resolution/entity.js
CHANGED
|
@@ -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
|
}
|