@cap-js/cds-typer 0.22.0 → 0.23.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 +9 -1
- package/cds-plugin.js +4 -4
- package/lib/cli.js +19 -8
- package/lib/compile.js +8 -10
- package/lib/components/enum.js +12 -12
- package/lib/components/identifier.js +9 -1
- package/lib/components/inline.js +15 -13
- package/lib/components/reference.js +1 -1
- package/lib/components/wrappers.js +8 -8
- package/lib/csn.js +38 -25
- package/lib/file.js +34 -30
- package/lib/logging.js +12 -70
- package/lib/resolution/builtin.js +64 -0
- package/lib/resolution/entity.js +155 -0
- package/lib/{components → resolution}/resolver.js +54 -163
- package/lib/typedefs.d.ts +21 -0
- package/lib/util.js +6 -6
- package/lib/visitor.js +116 -124
- package/package.json +2 -1
package/lib/file.js
CHANGED
|
@@ -10,7 +10,7 @@ const { createObjectOf } = require('./components/wrappers')
|
|
|
10
10
|
|
|
11
11
|
const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
|
|
12
12
|
|
|
13
|
-
/** @typedef {import('./typedefs').file.Namespace} Namespace */
|
|
13
|
+
/** @typedef {import('./typedefs').file.Namespace} Namespace */
|
|
14
14
|
|
|
15
15
|
class File {
|
|
16
16
|
/**
|
|
@@ -20,7 +20,7 @@ class File {
|
|
|
20
20
|
toTypeDefs() { return '' }
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Concats the classnames to an export dictionary
|
|
23
|
+
* Concats the classnames to an export dictionary
|
|
24
24
|
* to create the accompanying JS file for the typings.
|
|
25
25
|
* @returns {string} a string containing the module.exports for the JS file.
|
|
26
26
|
*/
|
|
@@ -32,7 +32,7 @@ class File {
|
|
|
32
32
|
* For example, the cap.hana types are included in a separate predefined library file
|
|
33
33
|
* which is only included if the model that is being compiled references any of these types.
|
|
34
34
|
* A library is uniquely identified by the namespace it represents. That namespace is directly
|
|
35
|
-
* derived from the file's name. i.e. path/to/file/cap.hana.ts will be considered to hold
|
|
35
|
+
* derived from the file's name. i.e. path/to/file/cap.hana.ts will be considered to hold
|
|
36
36
|
* definitions describing the namespace "cap.hana".
|
|
37
37
|
* These files are supposed to contain fully usable types, not CSN or a CDS file, as they are just
|
|
38
38
|
* being copied verbatim when they are being used.
|
|
@@ -57,19 +57,19 @@ class Library extends File {
|
|
|
57
57
|
* @type {string[]}
|
|
58
58
|
*/
|
|
59
59
|
this.entities = Array.from(this.contents.matchAll(/export class (\w+)/g), ([,m]) => m)
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
/**
|
|
62
62
|
* Namespace (a.b.c.d)
|
|
63
63
|
* @type {string}
|
|
64
64
|
*/
|
|
65
65
|
this.namespace = path.basename(file, '.ts')
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
/**
|
|
68
68
|
* Whether this library was referenced at least once
|
|
69
69
|
* @type {boolean}
|
|
70
70
|
*/
|
|
71
71
|
this.referenced = false
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
/**
|
|
74
74
|
* The Path for this library file, which is constructed from its namespace.
|
|
75
75
|
* @type {Path}
|
|
@@ -80,7 +80,7 @@ class Library extends File {
|
|
|
80
80
|
/**
|
|
81
81
|
* Whether this library offers an entity of a given type (fully qualified).
|
|
82
82
|
* @param {string} entity - the entity's name, e.g. cap.hana.TINYINT
|
|
83
|
-
* @returns {boolean} true, iff the namespace inferred from the passed string matches that of this library
|
|
83
|
+
* @returns {boolean} true, iff the namespace inferred from the passed string matches that of this library
|
|
84
84
|
* and this library contains a class of that name. i.e.:
|
|
85
85
|
* ```js
|
|
86
86
|
* new Library('cap.hana.ts').offers('cap.hana.TINYINT') // -> true`
|
|
@@ -96,7 +96,7 @@ class Library extends File {
|
|
|
96
96
|
*/
|
|
97
97
|
class SourceFile extends File {
|
|
98
98
|
/**
|
|
99
|
-
* @param {string | Path} path
|
|
99
|
+
* @param {string | Path} path - path to the file
|
|
100
100
|
*/
|
|
101
101
|
constructor(path) {
|
|
102
102
|
super()
|
|
@@ -107,7 +107,7 @@ class SourceFile extends File {
|
|
|
107
107
|
/** @type {Buffer} */
|
|
108
108
|
this.preamble = new Buffer()
|
|
109
109
|
/** @type {{ buffer: Buffer, fqs: {name: string, fq: string}[]}} */
|
|
110
|
-
this.events = { buffer: new Buffer(), fqs: []}
|
|
110
|
+
this.events = { buffer: new Buffer(), fqs: []}
|
|
111
111
|
/** @type {Buffer} */
|
|
112
112
|
this.types = new Buffer()
|
|
113
113
|
/** @type {{ buffer: Buffer, data: {kvs: [string[]], name: string, fq: string, property?: string}[]}} */
|
|
@@ -144,7 +144,7 @@ class SourceFile extends File {
|
|
|
144
144
|
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // f: { (p: T) => number, ... }
|
|
145
145
|
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number', initialiser: '_ => 42'}) // f: { (p: T): string = _ => 42, ... }
|
|
146
146
|
* ```
|
|
147
|
-
*
|
|
147
|
+
*
|
|
148
148
|
* The generated string will not be just the signature of the function. Instead, it will be an object offering a callable signature.
|
|
149
149
|
* On top of that, it will also expose a property `__parameters`, which is an object reflecting the functions parameters.
|
|
150
150
|
* The reason for this is that the CDS runtime actually treats the function parameters as a named object. This can not be rectified via
|
|
@@ -177,7 +177,7 @@ class SourceFile extends File {
|
|
|
177
177
|
* aliases in the index.js file.
|
|
178
178
|
* @param {string} singular - singular type without namespace.
|
|
179
179
|
* @param {string} plural - plural type without namespace
|
|
180
|
-
* @param {string} original - original entity name without namespace.
|
|
180
|
+
* @param {string} original - original entity name without namespace.
|
|
181
181
|
* In many cases this will be the same as plural.
|
|
182
182
|
*/
|
|
183
183
|
addInflection(singular, plural, original) {
|
|
@@ -189,7 +189,7 @@ class SourceFile extends File {
|
|
|
189
189
|
* @param {string} name - name of the function
|
|
190
190
|
* @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters - list of parameters, passed as [name, type] pairs
|
|
191
191
|
* @param {string} returns - the return type of the function
|
|
192
|
-
* @param {'function' | 'action'} kind
|
|
192
|
+
* @param {'function' | 'action'} kind - kind of the node
|
|
193
193
|
*/
|
|
194
194
|
addOperation(name, parameters, returns, kind) {
|
|
195
195
|
//this.operations.buffer.add("// operation")
|
|
@@ -223,11 +223,11 @@ class SourceFile extends File {
|
|
|
223
223
|
* @param {string} fq - fully qualified name of the enum (entity name within CSN)
|
|
224
224
|
* @param {string} name - local name of the enum
|
|
225
225
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
226
|
-
* @param {string?}
|
|
226
|
+
* @param {string?} _property - property to which the enum is attached.
|
|
227
227
|
* If given, the enum is considered to be an inline definition of an enum.
|
|
228
228
|
* If not, it is considered to be regular, named enum.
|
|
229
229
|
*/
|
|
230
|
-
addEnum(fq, name, kvs,
|
|
230
|
+
addEnum(fq, name, kvs, _property) {
|
|
231
231
|
this.enums.data.push({ name, fq, kvs })
|
|
232
232
|
printEnum(this.enums.buffer, name, kvs)
|
|
233
233
|
}
|
|
@@ -236,7 +236,7 @@ class SourceFile extends File {
|
|
|
236
236
|
* Adds an inline enum to this file.
|
|
237
237
|
* @param {string} entityCleanName - name of the entity the enum is attached to without namespace
|
|
238
238
|
* @param {string} entityFqName - name of the entity the enum is attached to with namespace
|
|
239
|
-
* @param {string} propertyName - property to which the enum is attached.
|
|
239
|
+
* @param {string} propertyName - property to which the enum is attached.
|
|
240
240
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
241
241
|
* If given, the enum is considered to be an inline definition of an enum.
|
|
242
242
|
* If not, it is considered to be regular, named enum.
|
|
@@ -262,7 +262,7 @@ class SourceFile extends File {
|
|
|
262
262
|
* ```
|
|
263
263
|
*/
|
|
264
264
|
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs) {
|
|
265
|
-
this.enums.data.push({
|
|
265
|
+
this.enums.data.push({
|
|
266
266
|
name: `${entityCleanName}.${propertyName}`,
|
|
267
267
|
property: propertyName,
|
|
268
268
|
kvs,
|
|
@@ -272,11 +272,11 @@ class SourceFile extends File {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
|
-
* Adds a class to this file.
|
|
275
|
+
* Adds a class to this file.
|
|
276
276
|
* This differs from writing to the classes buffer,
|
|
277
|
-
* as it is just a cache to collect all classes that
|
|
278
|
-
* are supposed to be present in this file.
|
|
279
|
-
* @param {string} clean - cleaned name of the class
|
|
277
|
+
* as it is just a cache to collect all classes that
|
|
278
|
+
* are supposed to be present in this file.
|
|
279
|
+
* @param {string} clean - cleaned name of the class
|
|
280
280
|
* @param {string} fq - fully qualified name, including the namespace
|
|
281
281
|
*/
|
|
282
282
|
addClass(clean, fq) {
|
|
@@ -285,7 +285,7 @@ class SourceFile extends File {
|
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
287
|
* Adds an event to this file.
|
|
288
|
-
* are supposed to be present in this file.
|
|
288
|
+
* are supposed to be present in this file.
|
|
289
289
|
* @param {string} name - cleaned name of the event
|
|
290
290
|
* @param {string} fq - fully qualified name, including the namespace
|
|
291
291
|
*/
|
|
@@ -368,7 +368,7 @@ class SourceFile extends File {
|
|
|
368
368
|
this.preamble.join(),
|
|
369
369
|
this.services.buffer.join(), // must be the very first
|
|
370
370
|
this.types.join(),
|
|
371
|
-
this.enums.buffer.join(),
|
|
371
|
+
this.enums.buffer.join(),
|
|
372
372
|
this.inlineEnums.buffer.join(), // needs to be before classes
|
|
373
373
|
this.aspects.join(), // needs to be before classes
|
|
374
374
|
this.classes.join(),
|
|
@@ -398,7 +398,11 @@ class SourceFile extends File {
|
|
|
398
398
|
// This could be an issue when the user re-used the original name in a @singular/@plural annotation.
|
|
399
399
|
// Seems unlikely, but we have to eliminate the original entry if users start running into this.
|
|
400
400
|
if (singular !== original) {
|
|
401
|
-
|
|
401
|
+
// 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} }`
|
|
405
|
+
exports.push(`module.exports.${original} = ${rhs}`)
|
|
402
406
|
}
|
|
403
407
|
return exports
|
|
404
408
|
})
|
|
@@ -419,7 +423,7 @@ class SourceFile extends File {
|
|
|
419
423
|
class Buffer {
|
|
420
424
|
|
|
421
425
|
/**
|
|
422
|
-
* @param {string} indentation
|
|
426
|
+
* @param {string} indentation - indentation to use (two spaces by default)
|
|
423
427
|
*/
|
|
424
428
|
constructor(indentation = ' ') {
|
|
425
429
|
/**
|
|
@@ -471,7 +475,7 @@ class Buffer {
|
|
|
471
475
|
|
|
472
476
|
/**
|
|
473
477
|
* Adds an element to the buffer with the current indentation level.
|
|
474
|
-
* @param {string} part
|
|
478
|
+
* @param {string} part - what to attach to the buffer
|
|
475
479
|
*/
|
|
476
480
|
add(part) {
|
|
477
481
|
this.parts.push(this.currentIndent + part)
|
|
@@ -479,7 +483,7 @@ class Buffer {
|
|
|
479
483
|
|
|
480
484
|
/**
|
|
481
485
|
* Adds an element to the buffer with one level of indent.
|
|
482
|
-
* @param {string | (() => void)} part - either a string or a function. If it is a string, it is added to the buffer.
|
|
486
|
+
* @param {string | (() => void)} part - either a string or a function. If it is a string, it is added to the buffer.
|
|
483
487
|
* If not, it is expected to be a function that manipulates the buffer as a side effect.
|
|
484
488
|
*/
|
|
485
489
|
addIndented(part) {
|
|
@@ -528,7 +532,7 @@ class Path {
|
|
|
528
532
|
|
|
529
533
|
/**
|
|
530
534
|
* Transfoms the Path into a directory path.
|
|
531
|
-
* @param {object} params
|
|
535
|
+
* @param {object} params - parameters
|
|
532
536
|
* @param {string?} params.relative - if defined, the path is constructed relative to this directory
|
|
533
537
|
* @param {boolean} params.local - if set to true, './' is prefixed to the directory
|
|
534
538
|
* @param {boolean} params.posix - if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
|
|
@@ -565,7 +569,7 @@ class Path {
|
|
|
565
569
|
}
|
|
566
570
|
|
|
567
571
|
/**
|
|
568
|
-
* @param {string} relative
|
|
572
|
+
* @param {string} relative - directory to which we check relatively
|
|
569
573
|
* @returns {boolean} true, iff the Path refers to the current working directory, aka './'
|
|
570
574
|
*/
|
|
571
575
|
isCwd(relative = undefined) {
|
|
@@ -579,8 +583,8 @@ class FileRepository {
|
|
|
579
583
|
#files = {}
|
|
580
584
|
|
|
581
585
|
/**
|
|
582
|
-
* @param {string} name
|
|
583
|
-
* @param {SourceFile} file
|
|
586
|
+
* @param {string} name - file name
|
|
587
|
+
* @param {SourceFile} file - the file
|
|
584
588
|
*/
|
|
585
589
|
add(name, file) {
|
|
586
590
|
this.#files[name] = file
|
package/lib/logging.js
CHANGED
|
@@ -1,74 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
const Levels = {
|
|
3
|
-
TRACE: 1,
|
|
4
|
-
DEBUG: 2,
|
|
5
|
-
INFO: 3,
|
|
6
|
-
WARNING: 4,
|
|
7
|
-
ERROR: 8,
|
|
8
|
-
CRITICAL: 16,
|
|
9
|
-
NONE: 32,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class Logger {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.mask = 0
|
|
15
|
-
const lvls = Object.keys(Levels)
|
|
16
|
-
for (let i = 0; i < lvls.length - 1; i++) {
|
|
17
|
-
// -1 to ignore NONE
|
|
18
|
-
const level = lvls[i]
|
|
19
|
-
this[level.toLowerCase()] = function (message) { this._log(level, message) }.bind(this)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// only temporarily to disable those warnings...
|
|
24
|
-
//warning(s) {}; error(s) {}; info(s) {}; debug(s) {};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Add all log levels starting at level.
|
|
28
|
-
* @param {number} baseLevel - level to start from.
|
|
29
|
-
*/
|
|
30
|
-
addFrom(baseLevel) {
|
|
31
|
-
const vals = Object.values(Levels)
|
|
32
|
-
const highest = vals[vals.length - 1]
|
|
33
|
-
for (let l = Math.log2(baseLevel); Math.pow(2, l) <= highest; l++) {
|
|
34
|
-
this.add(Math.pow(2, l))
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
const cds = require('@sap/cds')
|
|
37
2
|
|
|
38
|
-
|
|
39
|
-
* Adds a log level to react to.
|
|
40
|
-
* @param {number} level - the level to react to.
|
|
41
|
-
*/
|
|
42
|
-
add(level) {
|
|
43
|
-
this.mask = this.mask | level
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Ignores a log level.
|
|
48
|
-
* @param {number} level - the level to ignore.
|
|
49
|
-
*/
|
|
50
|
-
ignore(level) {
|
|
51
|
-
this.mask = this.mask ^ level
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Attempts to log a message.
|
|
56
|
-
* Only iff levelName is a valid log level
|
|
57
|
-
* and the corresponding number if part of mask,
|
|
58
|
-
* the message gets logged.
|
|
59
|
-
* @param {Levels} levelName - name of the log level.
|
|
60
|
-
* @param {string} message - message to log.
|
|
61
|
-
*/
|
|
62
|
-
_log(levelName, message) {
|
|
63
|
-
const level = Levels[levelName]
|
|
64
|
-
if (level && (this.mask & level) === level) {
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.log(`[${levelName}]`, message)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
3
|
+
const _keyFor = value => Object.entries(cds.log.levels).find(([,val]) => val === value)?.[0]
|
|
70
4
|
|
|
5
|
+
// workaround until retroactively setting log level to 0 is possible
|
|
6
|
+
cds.log('cds-typer', _keyFor(cds.log.levels.SILENT))
|
|
71
7
|
module.exports = {
|
|
72
|
-
|
|
73
|
-
|
|
8
|
+
_keyFor,
|
|
9
|
+
setLevel: level => { cds.log('cds-typer', level) },
|
|
10
|
+
deprecated: {
|
|
11
|
+
WARNING: 'WARN',
|
|
12
|
+
CRITICAL: 'ERROR',
|
|
13
|
+
NONE: 'SILENT'
|
|
14
|
+
},
|
|
15
|
+
get LOG () { return cds.log('cds-typer') }
|
|
74
16
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class BuiltinResolver {
|
|
2
|
+
/**
|
|
3
|
+
* Builtin types defined by CDS.
|
|
4
|
+
*/
|
|
5
|
+
#builtins = {
|
|
6
|
+
UUID: 'string',
|
|
7
|
+
String: 'string',
|
|
8
|
+
Binary: 'string',
|
|
9
|
+
LargeString: 'string',
|
|
10
|
+
LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
|
|
11
|
+
Vector: 'string',
|
|
12
|
+
Integer: 'number',
|
|
13
|
+
UInt8: 'number',
|
|
14
|
+
Int16: 'number',
|
|
15
|
+
Int32: 'number',
|
|
16
|
+
Int64: 'number',
|
|
17
|
+
Integer64: 'number',
|
|
18
|
+
Decimal: 'number',
|
|
19
|
+
DecimalFloat: 'number',
|
|
20
|
+
Float: 'number',
|
|
21
|
+
Double: 'number',
|
|
22
|
+
Boolean: 'boolean',
|
|
23
|
+
// note: the date-related types are strings on purpose, which reflects their runtime behaviour
|
|
24
|
+
Date: '__.CdsDate', // yyyy-mm-dd
|
|
25
|
+
DateTime: '__.CdsDateTime', // yyyy-mm-dd + time + TZ (precision: seconds)
|
|
26
|
+
Time: '__.CdsTime', // hh:mm:ss
|
|
27
|
+
Timestamp: '__.CdsTimestamp', // yyy-mm-dd + time + TZ (ms precision)
|
|
28
|
+
//
|
|
29
|
+
Composition: 'Array',
|
|
30
|
+
Association: 'Array'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @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
|
+
*/
|
|
37
|
+
constructor ({ IEEE754Compatible } = {}) {
|
|
38
|
+
if (IEEE754Compatible) {
|
|
39
|
+
this.#builtins.Decimal = '(number | string)'
|
|
40
|
+
this.#builtins.DecimalFloat = '(number | string)'
|
|
41
|
+
this.#builtins.Float = '(number | string)'
|
|
42
|
+
this.#builtins.Double = '(number | string)'
|
|
43
|
+
}
|
|
44
|
+
this.#builtins = Object.freeze(this.#builtins)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string | string[]} t - name or parts of the type name split on dots
|
|
49
|
+
* @returns {string | undefined | false} if t refers to a builtin, the name of the corresponding TS type is returned.
|
|
50
|
+
* If t _looks like_ a builtin (`cds.X`), undefined is returned.
|
|
51
|
+
* If t is obviously not a builtin, false is returned.
|
|
52
|
+
*/
|
|
53
|
+
resolveBuiltin (t) {
|
|
54
|
+
if (!Array.isArray(t) && typeof t !== 'string') return false
|
|
55
|
+
const path = Array.isArray(t) ? t : t.split('.')
|
|
56
|
+
return path.length === 2 && path[0] === 'cds'
|
|
57
|
+
? this.#builtins[path[1]]
|
|
58
|
+
: false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
BuiltinResolver
|
|
64
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
class EntityInfo {
|
|
2
|
+
/**
|
|
3
|
+
* @example
|
|
4
|
+
* ```ts
|
|
5
|
+
* 'n1.n2.A.B.p.q'
|
|
6
|
+
* // v
|
|
7
|
+
* Path(['n1', 'n2'])
|
|
8
|
+
* ```
|
|
9
|
+
* @type {Path}
|
|
10
|
+
*/
|
|
11
|
+
namespace
|
|
12
|
+
|
|
13
|
+
// FIXME: check if scope can actually be more than one entity deep
|
|
14
|
+
/**
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* 'n1.n2.A.B.p.q'
|
|
18
|
+
* // v
|
|
19
|
+
* ['A']
|
|
20
|
+
* ```
|
|
21
|
+
* @type {string[]}
|
|
22
|
+
*/
|
|
23
|
+
scope
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* 'n1.n2.A.B.p.q'
|
|
29
|
+
* // v
|
|
30
|
+
* 'B'
|
|
31
|
+
* ```
|
|
32
|
+
* @type {string}
|
|
33
|
+
*/
|
|
34
|
+
entityName
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* 'n1.n2.A.B.p.q'
|
|
40
|
+
* // v
|
|
41
|
+
* ['p', 'q']
|
|
42
|
+
* ```
|
|
43
|
+
* @type {string[]}
|
|
44
|
+
*/
|
|
45
|
+
propertyAccess
|
|
46
|
+
|
|
47
|
+
/** @type {{singular: string, plural: string}} */
|
|
48
|
+
#inflection
|
|
49
|
+
|
|
50
|
+
/** @type {import('./resolver').Resolver} */
|
|
51
|
+
#resolver
|
|
52
|
+
|
|
53
|
+
/** @type {EntityRepository} */
|
|
54
|
+
#repository
|
|
55
|
+
|
|
56
|
+
/** @type {EntityInfo} */
|
|
57
|
+
#parent
|
|
58
|
+
|
|
59
|
+
/** @type {import('../typedefs').resolver.EntityCSN} */
|
|
60
|
+
#csn
|
|
61
|
+
|
|
62
|
+
get csn () {
|
|
63
|
+
return this.#csn ??= this.#resolver.csn.definitions[this.fullyQualifiedName]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* 'n1.n2.A.B.p.q'
|
|
70
|
+
* // v
|
|
71
|
+
* { singular: B, plural: Bs }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
get inflection () {
|
|
75
|
+
if (!this.#inflection) {
|
|
76
|
+
const dummyTypeInfo = {
|
|
77
|
+
plainName: this.entityName,
|
|
78
|
+
csn: this.csn,
|
|
79
|
+
isInlineDeclaration: false
|
|
80
|
+
}
|
|
81
|
+
const { singular, plural } = this.#resolver.inflect(dummyTypeInfo, this.namespace)
|
|
82
|
+
this.#inflection = { singular, plural }
|
|
83
|
+
}
|
|
84
|
+
return this.#inflection
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* 'n1.n2.A.B.p.q'
|
|
91
|
+
* // v
|
|
92
|
+
* 'A.B.p.q'
|
|
93
|
+
* ```
|
|
94
|
+
* @type {string}
|
|
95
|
+
*/
|
|
96
|
+
get withoutNamespace () {
|
|
97
|
+
return [this.scope, this.entityName, this.propertyAccess].flat().join('.')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @returns {EntityInfo | null}
|
|
102
|
+
*/
|
|
103
|
+
get parent () {
|
|
104
|
+
if (this.#parent !== undefined) return this.#parent
|
|
105
|
+
const parentFq = [this.namespace, this.scope].flat().join('.')
|
|
106
|
+
return this.#parent = this.#repository.getByFq(parentFq)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} fullyQualifiedName - the fully qualified name of the entity
|
|
111
|
+
* @param {EntityRepository} repository - the repository this info is stored in
|
|
112
|
+
* @param {import('./resolver').Resolver} resolver - the resolver
|
|
113
|
+
*/
|
|
114
|
+
constructor (fullyQualifiedName, repository, resolver) {
|
|
115
|
+
const untangled = resolver.untangle(fullyQualifiedName)
|
|
116
|
+
this.#repository = repository
|
|
117
|
+
this.#resolver = resolver
|
|
118
|
+
this.fullyQualifiedName = fullyQualifiedName
|
|
119
|
+
this.namespace = untangled.namespace
|
|
120
|
+
this.scope = untangled.scope
|
|
121
|
+
this.entityName = untangled.name
|
|
122
|
+
this.propertyAccess = untangled.property
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class EntityRepository {
|
|
127
|
+
/** @type {{ [key: string]: EntityInfo }} */
|
|
128
|
+
#cache = {}
|
|
129
|
+
|
|
130
|
+
/** @type {import('./resolver').Resolver} */
|
|
131
|
+
#resolver
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} fq - fully qualified name of the entity
|
|
135
|
+
* @returns {EntityInfo | null}
|
|
136
|
+
*/
|
|
137
|
+
getByFq (fq) {
|
|
138
|
+
if (this.#cache[fq] !== undefined) return this.#cache[fq]
|
|
139
|
+
this.#cache[fq] = this.#resolver.isPartOfModel(fq)
|
|
140
|
+
? new EntityInfo(fq, this, this.#resolver)
|
|
141
|
+
: null
|
|
142
|
+
return this.#cache[fq]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {import('./resolver').Resolver} resolver - the resolver
|
|
147
|
+
*/
|
|
148
|
+
constructor (resolver) {
|
|
149
|
+
this.#resolver = resolver
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
EntityRepository
|
|
155
|
+
}
|