@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/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?} property - property to which the enum is attached.
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, property) {
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
- exports.push(`module.exports.${original} = { is_singular: true, __proto__: csn.${original} }`)
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
- /** @enum {number} */
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
- Levels,
73
- Logger,
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
+ }