@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.
@@ -3,122 +3,25 @@
3
3
  const util = require('../util')
4
4
  // eslint-disable-next-line no-unused-vars
5
5
  const { Buffer, SourceFile, Path, Library } = require('../file')
6
- const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
7
- const { StructuredInlineDeclarationResolver } = require('./inline')
8
- const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
9
- const { isReferenceType } = require('./reference')
10
- const { isEntity } = require('../csn')
11
- const { baseDefinitions } = require('./basedefs')
6
+ const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('../components/wrappers')
7
+ const { StructuredInlineDeclarationResolver } = require('../components/inline')
8
+ const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
9
+ const { isReferenceType } = require('../components/reference')
10
+ const { isEntity, getMaxCardinality } = require('../csn')
11
+ const { baseDefinitions } = require('../components/basedefs')
12
+ const { BuiltinResolver } = require('./builtin')
13
+ const { LOG } = require('../logging')
14
+ const { last } = require('../components/identifier')
12
15
 
13
16
  /** @typedef {import('../visitor').Visitor} Visitor */
14
17
  /** @typedef {import('../typedefs').resolver.CSN} CSN */
15
18
  /** @typedef {import('../typedefs').resolver.TypeResolveInfo} TypeResolveInfo */
16
-
17
- class BuiltinResolver {
18
- /**
19
- * Builtin types defined by CDS.
20
- */
21
- #builtins = {
22
- UUID: 'string',
23
- String: 'string',
24
- Binary: 'string',
25
- LargeString: 'string',
26
- LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
27
- Vector: 'string',
28
- Integer: 'number',
29
- UInt8: 'number',
30
- Int16: 'number',
31
- Int32: 'number',
32
- Int64: 'number',
33
- Integer64: 'number',
34
- Decimal: 'number',
35
- DecimalFloat: 'number',
36
- Float: 'number',
37
- Double: 'number',
38
- Boolean: 'boolean',
39
- // note: the date-related types are strings on purpose, which reflects their runtime behaviour
40
- Date: '__.CdsDate', // yyyy-mm-dd
41
- DateTime: '__.CdsDateTime', // yyyy-mm-dd + time + TZ (precision: seconds)
42
- Time: '__.CdsTime', // hh:mm:ss
43
- Timestamp: '__.CdsTimestamp', // yyy-mm-dd + time + TZ (ms precision)
44
- //
45
- Composition: 'Array',
46
- Association: 'Array'
47
- }
48
-
49
- constructor ({ IEEE754Compatible } = {}) {
50
- if (IEEE754Compatible) {
51
- this.#builtins.Decimal = '(number | string)'
52
- this.#builtins.DecimalFloat = '(number | string)'
53
- this.#builtins.Float = '(number | string)'
54
- this.#builtins.Double = '(number | string)'
55
- }
56
- this.#builtins = Object.freeze(this.#builtins)
57
- }
58
-
59
- /**
60
- * @param {string | string[]} t - name or parts of the type name split on dots
61
- * @returns {string | undefined | false} if t refers to a builtin, the name of the corresponding TS type is returned.
62
- * If t _looks like_ a builtin (`cds.X`), undefined is returned.
63
- * If t is obviously not a builtin, false is returned.
64
- */
65
- resolveBuiltin (t) {
66
- if (!Array.isArray(t) && typeof t !== 'string') return false
67
- const path = Array.isArray(t) ? t : t.split('.')
68
- return path.length === 2 && path[0] === 'cds'
69
- ? this.#builtins[path[1]]
70
- : false
71
- }
72
- }
19
+ /** @typedef {import('../typedefs').visitor.Inflection} TypeResolveInfo */
73
20
 
74
21
  class Resolver {
75
-
76
- #caches = {
77
- /**
78
- * @type {{ [qualifier: string]: string }}
79
- */
80
- namespaces: {},
81
- /**
82
- * @type {{ [qualifier: string]: string[] }}
83
- */
84
- propertyAccesses: {}
85
- }
86
-
87
- /**
88
- * @param {string} qualifier
89
- * @returns {string?}
90
- */
91
- #getCachedNamespace (qualifier) {
92
- return this.#caches.namespaces[qualifier]
93
- }
94
-
95
- /**
96
- * @param {string} qualifier
97
- * @param {string} namespace
98
- */
99
- #cacheNamespace (qualifier, namespace) {
100
- this.#caches.namespaces[qualifier] = namespace
101
- }
102
-
103
- /**
104
- * @param {string} qualifier
105
- * @returns {string[]?}
106
- */
107
- #getCachedPropertyAccess (qualifier) {
108
- return this.#caches.propertyAccesses[qualifier]
109
- }
110
-
111
- /**
112
- * @param {string} qualifier
113
- * @param {string[]} propertyAccess
114
- */
115
- #cachePropertyAccess (qualifier, propertyAccess) {
116
- this.#caches.propertyAccesses[qualifier] = propertyAccess
117
- }
118
-
119
22
  get csn() { return this.visitor.csn.inferred }
120
-
121
- /** @param {Visitor} visitor */
23
+
24
+ /** @param {Visitor} visitor - the visitor */
122
25
  constructor(visitor) {
123
26
  /** @type {Visitor} */
124
27
  this.visitor = visitor
@@ -134,8 +37,24 @@ class Resolver {
134
37
  * needed for inline declarations
135
38
  */
136
39
  this.structuredInlineResolver = new StructuredInlineDeclarationResolver(this.visitor)
137
- }
138
-
40
+ }
41
+
42
+ /**
43
+ * @param {string} fq - fully qualified name of the entity
44
+ * @returns {boolean} true, iff the entity exists in the CSN (excluding builtins, see {@link isPartOfModel})
45
+ */
46
+ existsInCsn(fq) {
47
+ return Boolean(this.csn.definitions[fq])
48
+ }
49
+
50
+ /**
51
+ * @param {string} fq - fully qualified name of the entity or builtin
52
+ * @returns {boolean} true, iff the entity exists in the CSN or is identified as a builtin
53
+ */
54
+ isPartOfModel(fq) {
55
+ return this.existsInCsn(fq) || Boolean(this.builtinResolver.resolveBuiltin(fq))
56
+ }
57
+
139
58
  /**
140
59
  * Returns all libraries that have been referenced at least once.
141
60
  * @returns {Library[]}
@@ -144,27 +63,19 @@ class Resolver {
144
63
  return this.libraries.filter(l => l.referenced)
145
64
  }
146
65
 
147
- /**
148
- * TODO: this should probably be a class where we can also cache the properties
149
- * and only retrieve them on demand
150
- * @typedef {object} Untangled
151
- * @property {string[]} scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
152
- * @property {string} name name of the leaf entity `a.b.C.D.E.f.g` -> `E`
153
- * @property {string[]} property the property access path `a.b.C.D.E.f.g` -> `[f,g]`
154
- * @property {Path} namespace the cds namespace of the entity `a.b.C.D.E.f.g` -> `a.b`
155
- */
156
-
157
66
  /**
158
67
  * Conveniently combines resolveNamespace and trimNamespace
159
68
  * to end up with both the resolved Path of the namespace,
160
69
  * and the clean name of the class.
161
70
  * @param {string} fq - the fully qualified name of an entity.
162
- * @returns {Untangled} untangled qualifier
71
+ * @returns {import('../typedefs').resolver.Untangled} untangled qualifier
163
72
  */
164
73
  untangle(fq) {
165
74
  const builtin = this.builtinResolver.resolveBuiltin(fq)
166
75
  if (builtin) return { namespace: new Path([]), name: builtin, property: [], scope: [] }
167
76
 
77
+ // FIXME: if fq points to a service definition, ns will be the same as nameAndProperty
78
+ // this currently isn't a problem as we only use the its name, but should be addressed at some point
168
79
  const ns = this.resolveNamespace(fq)
169
80
  const nameAndProperty = this.trimNamespace(fq)
170
81
  const property = this.findPropertyAccess(fq)
@@ -172,7 +83,7 @@ class Resolver {
172
83
  ? nameAndProperty.slice(0, -(property.join('').length + property.length)) // +1 for each dot
173
84
  : nameAndProperty
174
85
  ).split('.')//.at(-1) // nested entities would return Foo.Bar, so we only take the last part to get the actual entity name
175
- return {
86
+ return {
176
87
  namespace: new Path(ns.split('.')),
177
88
  scope: nameParts.slice(0, -1),
178
89
  name: nameParts.at(-1),
@@ -191,7 +102,6 @@ class Resolver {
191
102
  * @returns {string} the entity name without leading namespace.
192
103
  */
193
104
  trimNamespace(p) {
194
- //if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
195
105
  const parts = p.split('.')
196
106
  if (parts.length <= 1) return p
197
107
 
@@ -218,13 +128,13 @@ class Resolver {
218
128
  * entity Entity {
219
129
  * x: Composition of { y: Composition of z: { a: Integer }}
220
130
  * }
221
- *
131
+ *
222
132
  * // somewhere else
223
133
  * entity Foo {
224
134
  * x: namespace.Entity.x.y.z;
225
135
  * }
226
136
  * ```
227
- * @example
137
+ * @example
228
138
  * ```js
229
139
  * findPropertyAccess('namespace') // []
230
140
  * findPropertyAccess('namespace.Entity') // []
@@ -233,7 +143,6 @@ class Resolver {
233
143
  * ```
234
144
  */
235
145
  findPropertyAccess(p) {
236
- if (this.#getCachedPropertyAccess(p)) return this.#getCachedPropertyAccess(p)
237
146
  const parts = p.split('.')
238
147
  if (parts.length <= 1) return []
239
148
 
@@ -247,13 +156,12 @@ class Resolver {
247
156
  qualifier += `.${parts.shift()}`
248
157
  }
249
158
  // skip forward to the last entity from left (Entity3), assuming that there is no name conflict between entities and properties
250
- // i.e.: if there is a property "Entity2" in the entity Entity1, this will instead [Entity2, Entity3, property1, property2] as property access
159
+ // i.e.: if there is a property "Entity2" in the entity Entity1, this will instead [Entity2, Entity3, property1, property2] as property access
251
160
  while (!isPropertyOf(parts[0], defs[qualifier]) && isEntity(defs[qualifier + `.${parts[0]}`])) {
252
161
  qualifier += `.${parts.shift()}`
253
162
  }
254
163
  // assuming Entity3 _does_ own a property "property1", return [property1, property2]
255
164
  const propertyAccess = isPropertyOf(parts[0], defs[qualifier]) ? parts : []
256
- this.#cachePropertyAccess(p, propertyAccess)
257
165
  return propertyAccess
258
166
  }
259
167
 
@@ -274,7 +182,7 @@ class Resolver {
274
182
  // TODO: handle builtins here as well?
275
183
  // guard: types don't get inflected
276
184
  if (typeInfo.csn?.kind === 'type') {
277
- return {
185
+ return {
278
186
  singular: typeInfo.plainName,
279
187
  plural: createArrayOf(typeInfo.plainName),
280
188
  typeName: typeInfo.plainName,
@@ -318,7 +226,7 @@ class Resolver {
318
226
  }
319
227
  }
320
228
  if (!singular || !plural) {
321
- this.visitor.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
229
+ LOG.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
322
230
  }
323
231
 
324
232
  return { typeName, singular, plural }
@@ -344,7 +252,7 @@ class Resolver {
344
252
  */
345
253
  resolveAndRequire(element, file) {
346
254
  const typeInfo = this.resolveType(element, file)
347
- const cardinality = this.getMaxCardinality(element)
255
+ const cardinality = getMaxCardinality(element)
348
256
 
349
257
  let typeName = typeInfo.plainName ?? typeInfo.type
350
258
 
@@ -372,14 +280,14 @@ class Resolver {
372
280
  // But we can't just fix it in inflection(...), as that would break several other things
373
281
  // So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
374
282
  if (target.type) {
375
- const untangled = this.untangle(target.type)
283
+ const untangled = this.visitor.entityRepository.getByFq(target.type)
376
284
  const scope = untangled.scope.join('.')
377
285
  if (scope && !singular.startsWith(scope)) {
378
286
  singular = `${scope}.${singular}`
379
287
  }
380
288
  }
381
289
 
382
- typeName = cardinality > 1
290
+ typeName = cardinality > 1
383
291
  ? toMany(plural)
384
292
  : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
385
293
  file.addImport(baseDefinitions.path)
@@ -426,13 +334,13 @@ class Resolver {
426
334
  // handle typeof (unless it has already been handled above)
427
335
  const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
428
336
  if (target && !typeInfo.isDeepRequire) {
429
- const { property: propertyAccess } = this.untangle(target)
430
- if (propertyAccess.length) {
337
+ const { propertyAccess } = this.visitor.entityRepository.getByFq(target) ?? {}
338
+ if (propertyAccess?.length) {
431
339
  const element = target.slice(0, -propertyAccess.join('.').length - 1)
432
340
  const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
433
341
  // singular, as we have to access the property of the entity
434
342
  typeName = deepRequire(util.singular4(element)) + access
435
- typeInfo.isDeepRequire = true
343
+ typeInfo.isDeepRequire = true
436
344
  }
437
345
  }
438
346
 
@@ -447,18 +355,6 @@ class Resolver {
447
355
  return { typeName, typeInfo }
448
356
  }
449
357
 
450
- /**
451
- * Attempts to retrieve the max cardinality of a CSN for an entity.
452
- * @param {EntityCSN} element - csn of entity to retrieve cardinality for
453
- * @returns {number} max cardinality of the element.
454
- * If no cardinality is attached to the element, cardinality is 1.
455
- * If it is set to '*', result is Infinity.
456
- */
457
- getMaxCardinality(element) {
458
- const cardinality = element?.cardinality?.max ?? 1
459
- return cardinality === '*' ? Infinity : parseInt(cardinality)
460
- }
461
-
462
358
  /**
463
359
  * Resolves the fully qualified name of an entity to its parent entity.
464
360
  * resolveParent(a.b.c.D) -> CSN {a.b.c}
@@ -480,8 +376,6 @@ class Resolver {
480
376
  */
481
377
  resolveNamespace(pathParts) {
482
378
  if (typeof pathParts === 'string') pathParts = pathParts.split('.')
483
- const fq = pathParts.join('.')
484
- if (this.#getCachedNamespace(fq)) return this.#getCachedNamespace(fq)
485
379
  let result
486
380
  while (result === undefined) {
487
381
  const path = pathParts.join('.')
@@ -494,7 +388,6 @@ class Resolver {
494
388
  pathParts = pathParts.slice(0, -1)
495
389
  }
496
390
  }
497
- this.#cacheNamespace(fq, result)
498
391
  return result
499
392
  }
500
393
 
@@ -510,15 +403,15 @@ class Resolver {
510
403
  // with an already resolved type. In that case, just return the type we have.
511
404
  if (element && Object.hasOwn(element, 'isBuiltin')) return element
512
405
 
513
- const cardinality = this.getMaxCardinality(element)
406
+ const cardinality = getMaxCardinality(element)
514
407
 
515
408
  const result = {
516
409
  isBuiltin: false, // will be rectified in the corresponding handlers, if needed
517
410
  isInlineDeclaration: false,
518
411
  isForeignKeyReference: false,
519
412
  isArray: false,
520
- isNotNull: element?.isRefNotNull !== undefined
521
- ? element?.isRefNotNull
413
+ isNotNull: element?.isRefNotNull !== undefined
414
+ ? element?.isRefNotNull
522
415
  : element?.key || element?.notNull || cardinality > 1,
523
416
  }
524
417
 
@@ -550,7 +443,7 @@ class Resolver {
550
443
  // FIXME: this is the case where users have arrays of enums as action parameter type.
551
444
  // Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
552
445
  // the encasing type (e.g. `string` here)
553
- // We should instead aim for a proper type, i.e.
446
+ // We should instead aim for a proper type, i.e.
554
447
  // this.#resolveInlineDeclarationType(element.enum, result, file)
555
448
  // or
556
449
  // stringifyEnumType(csnToEnumPairs(element))
@@ -558,7 +451,7 @@ class Resolver {
558
451
  }
559
452
  } else {
560
453
  this.resolvePotentialReferenceType(element.type, result, file)
561
- }
454
+ }
562
455
 
563
456
  // objects and arrays
564
457
  if (element?.items) {
@@ -574,7 +467,7 @@ class Resolver {
574
467
  }
575
468
 
576
469
  if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
577
- this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
470
+ LOG.warn(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
578
471
  }
579
472
  return result
580
473
  }
@@ -601,7 +494,7 @@ class Resolver {
601
494
 
602
495
  /**
603
496
  * Attempts to resolve a type that could reference another type.
604
- * @param {?} val
497
+ * @param {?} val - the value
605
498
  * @param {TypeResolveInfo} into - see resolveType()
606
499
  * @param {SourceFile} file - only needed as we may call #resolveInlineDeclarationType from here. Will be expelled at some point.
607
500
  */
@@ -669,7 +562,7 @@ class Resolver {
669
562
  // class Book { title: _cds_hana.cds.hana.VARCHAR } // <- how it would be without discarding the namespace
670
563
  // class Book { title: _cds_hana.VARCHAR } // <- how we want it to look
671
564
  // ```
672
- const plain = t.split('.').at(-1)
565
+ const plain = last(t)
673
566
  lib.referenced = true
674
567
  result.type = plain
675
568
  result.isBuiltin = false
@@ -685,6 +578,4 @@ class Resolver {
685
578
  }
686
579
  }
687
580
 
688
- module.exports = {
689
- Resolver
690
- }
581
+ module.exports = { Resolver }
package/lib/typedefs.d.ts CHANGED
@@ -31,6 +31,18 @@ export module resolver {
31
31
  imports: Path[]
32
32
  inner: TypeResolveInfo
33
33
  }
34
+
35
+ // TODO: this will be completely replaced by EntityInfo
36
+ export type Untangled = {
37
+ // scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
38
+ scope: string[],
39
+ // name name of the leaf entity `a.b.C.D.E.f.g` -> `E`
40
+ name: string,
41
+ // property the property access path `a.b.C.D.E.f.g` -> `[f,g]`
42
+ property: string[],
43
+ // namespace the cds namespace of the entity `a.b.C.D.E.f.g` -> `a.b`
44
+ namespace: Path
45
+ }
34
46
  }
35
47
 
36
48
  export module util {
@@ -59,7 +71,12 @@ export module visitor {
59
71
  }
60
72
 
61
73
  export type VisitorOptions = {
74
+ /** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
62
75
  propertiesOptional: boolean,
76
+ /**
77
+ * `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
78
+ * `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
79
+ */
63
80
  inlineDeclarations: 'flat' | 'structured',
64
81
  }
65
82
 
@@ -68,6 +85,10 @@ export module visitor {
68
85
  singular: string,
69
86
  plural: string
70
87
  }
88
+
89
+ export type Context = {
90
+ entity: string
91
+ }
71
92
  }
72
93
 
73
94
  export module file {
package/lib/util.js CHANGED
@@ -88,7 +88,7 @@ const singular4 = (dn, stripped = false) => {
88
88
 
89
89
  /**
90
90
  * Attempts to derive the plural form of an English noun.
91
- * If '@plural' is passed as annotation, that is preferred.
91
+ * If '@plural' is passed as annotation, that is preferred.
92
92
  * @param {Annotations} dn - annotations
93
93
  * @param {boolean} stripped - if true, leading namespace will be stripped
94
94
  */
@@ -110,7 +110,7 @@ const plural4 = (dn, stripped) => {
110
110
  }
111
111
 
112
112
  /**
113
- * Performs a deep merge of the passed objects into the first object.
113
+ * Performs a deep merge of the passed objects into the first object.
114
114
  * See Object.assign(target, source).
115
115
  * @param {object} target - object to assign into.
116
116
  * @param {object} source - object to assign from.
@@ -124,13 +124,13 @@ const deepMerge = (target, source) => {
124
124
 
125
125
  /**
126
126
  * Parses command line arguments into named and positional parameters.
127
- * Named parameters are expected to start with a double dash (--).
127
+ * Named parameters are expected to start with a double dash (--).
128
128
  * If the next argument `B` after a named parameter `A` is not a named parameter itself,
129
129
  * `B` is used as value for `A`.
130
130
  * If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
131
131
  * Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
132
132
  * will cause an error.
133
- * Named parameters that are either not specified or do not have a value assigned to them may draw a default value
133
+ * Named parameters that are either not specified or do not have a value assigned to them may draw a default value
134
134
  * from their definition in validFlags.
135
135
  * @param {string[]} argv - list of command line arguments
136
136
  * @param {{[key: string]: CommandlineFlag}} validFlags - allowed flags. May specify default values.
@@ -157,9 +157,9 @@ const parseCommandlineArgs = (argv, validFlags) => {
157
157
  named[arg] = validFlags[arg].default
158
158
  }
159
159
 
160
- const allowed = validFlags[arg].allowed
160
+ const { allowed, allowedHint } = validFlags[arg]
161
161
  if (allowed && !allowed.includes(named[arg])) {
162
- throw new Error(`invalid value '${named[arg]}' for flag ${arg}. Must be one of ${allowed.join(', ')}`)
162
+ throw new Error(`invalid value '${named[arg]}' for flag ${arg}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
163
163
  }
164
164
  }
165
165
  } else {