@cap-js/cds-typer 0.24.0 → 0.25.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.
@@ -12,11 +12,14 @@ const { baseDefinitions } = require('../components/basedefs')
12
12
  const { BuiltinResolver } = require('./builtin')
13
13
  const { LOG } = require('../logging')
14
14
  const { last } = require('../components/identifier')
15
+ const { getPropertyModifiers } = require('../components/property')
15
16
 
16
17
  /** @typedef {import('../visitor').Visitor} Visitor */
17
18
  /** @typedef {import('../typedefs').resolver.CSN} CSN */
19
+ /** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
18
20
  /** @typedef {import('../typedefs').resolver.TypeResolveInfo} TypeResolveInfo */
19
- /** @typedef {import('../typedefs').visitor.Inflection} TypeResolveInfo */
21
+ /** @typedef {import('../typedefs').visitor.Inflection} Inflection */
22
+ /** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
20
23
 
21
24
  class Resolver {
22
25
  get csn() { return this.visitor.csn.inferred }
@@ -86,7 +89,7 @@ class Resolver {
86
89
  return {
87
90
  namespace: new Path(ns.split('.')),
88
91
  scope: nameParts.slice(0, -1),
89
- name: nameParts.at(-1),
92
+ name: nameParts.at(-1) ?? nameAndProperty,
90
93
  property
91
94
  }
92
95
  }
@@ -146,10 +149,16 @@ class Resolver {
146
149
  const parts = p.split('.')
147
150
  if (parts.length <= 1) return []
148
151
 
149
- const isPropertyOf = (property, entity) => entity && property && Object.hasOwn(entity?.elements, property)
152
+ /**
153
+ * @param {string} property
154
+ * @param {import('../typedefs').resolver.EntityCSN} entity
155
+ */
156
+ const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
150
157
 
151
158
  const defs = this.visitor.csn.inferred.definitions
152
159
  // assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
160
+ /** @type {string} */
161
+ // @ts-expect-error - nope, we know there is at least one element
153
162
  let qualifier = parts.shift()
154
163
  // find first entity from left (Entity1)
155
164
  while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
@@ -175,7 +184,7 @@ class Resolver {
175
184
  * - inline type definitions, which don't really have a linguistic plural,
176
185
  * but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
177
186
  * @param {import('./resolver').TypeResolveInfo} typeInfo - information about the type gathered so far.
178
- * @param {string} [namespace] - namespace the type occurs in. If passed, will be shaved off from the name
187
+ * @param {string | import('../file').Path} [namespace] - namespace the type occurs in. If passed, will be shaved off from the name
179
188
  * @returns {Inflection}
180
189
  */
181
190
  inflect(typeInfo, namespace) {
@@ -189,7 +198,11 @@ class Resolver {
189
198
  }
190
199
  }
191
200
 
192
- let typeName
201
+ if (namespace instanceof Path) {
202
+ namespace = namespace.asNamespace()
203
+ }
204
+
205
+ let typeName = ''
193
206
  let singular
194
207
  let plural
195
208
 
@@ -204,7 +217,13 @@ class Resolver {
204
217
  // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
205
218
  // piece of code instead to reduce overhead.
206
219
  const into = new Buffer()
207
- this.structuredInlineResolver.printInlineType(undefined, { typeInfo }, into, '')
220
+ this.structuredInlineResolver.printInlineType({
221
+ fq: '',
222
+ type: { typeInfo, typeName: '' },
223
+ buffer: into,
224
+ statementEnd: '',
225
+ modifiers: getPropertyModifiers(typeInfo.csn)
226
+ })
208
227
  typeName = into.join(' ')
209
228
  singular = typeName
210
229
  plural = createArrayOf(typeName)
@@ -246,9 +265,9 @@ class Resolver {
246
265
  * 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
247
266
  * 2. resolve any singular/ plural issues and association/ composition around it
248
267
  * 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
249
- * @param {CSN} element - the CSN element to resolve the type for.
268
+ * @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
250
269
  * @param {SourceFile} file - source file for context.
251
- * @returns {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} info about the resolved type
270
+ * @returns {ResolveAndRequireInfo} info about the resolved type
252
271
  */
253
272
  resolveAndRequire(element, file) {
254
273
  const typeInfo = this.resolveType(element, file)
@@ -266,9 +285,15 @@ class Resolver {
266
285
  }[element.constructor.name] ?? []
267
286
 
268
287
  if (toOne && toMany) {
269
- const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
288
+ /** @type { EntityCSN | { type: string } } */
289
+ // @ts-expect-error - nope, it is not undefined
290
+ const target = element.items ?? (typeof element.target === 'string'
291
+ ? { type: element.target }
292
+ : element.target)
270
293
  /** set `notNull = true` to avoid repeated `| not null` TS construction */
294
+ // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
271
295
  target.notNull = true
296
+ // @ts-expect-error - yes, target is a valid parameter
272
297
  const targetTypeInfo = this.resolveAndRequire(target, file)
273
298
  if (targetTypeInfo.typeInfo.isDeepRequire === true) {
274
299
  typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
@@ -280,7 +305,7 @@ class Resolver {
280
305
  // But we can't just fix it in inflection(...), as that would break several other things
281
306
  // So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
282
307
  if (target.type) {
283
- const untangled = this.visitor.entityRepository.getByFq(target.type)
308
+ const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
284
309
  const scope = untangled.scope.join('.')
285
310
  if (scope && !singular.startsWith(scope)) {
286
311
  singular = `${scope}.${singular}`
@@ -359,7 +384,7 @@ class Resolver {
359
384
  * Resolves the fully qualified name of an entity to its parent entity.
360
385
  * resolveParent(a.b.c.D) -> CSN {a.b.c}
361
386
  * @param {string} name - fully qualified name of the entity to resolve the parent of.
362
- * @returns {CSN} the resolved parent CSN.
387
+ * @returns {import('../typedefs').resolver.EntityCSN} the resolved parent CSN.
363
388
  */
364
389
  resolveParent(name) {
365
390
  return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
@@ -394,14 +419,20 @@ class Resolver {
394
419
  /**
395
420
  * Resolves an element's type to either a builtin or a user defined type.
396
421
  * Enriched with additional information for improved printout (see return type).
397
- * @param {CSN} element - the CSN element to resolve the type for.
422
+ * @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
398
423
  * @param {SourceFile} file - source file for context.
399
424
  * @returns {TypeResolveInfo} description of the resolved type
400
425
  */
401
426
  resolveType(element, file) {
402
427
  // while resolving inline declarations, it can happen that we land here
403
428
  // with an already resolved type. In that case, just return the type we have.
404
- if (element && Object.hasOwn(element, 'isBuiltin')) return element
429
+ // type guard check purely to satisfy return statement
430
+ /**
431
+ * @param {any} e - the element to check
432
+ * @returns {e is TypeResolveInfo}
433
+ */
434
+ const isBuiltin = e => Object.hasOwn(e ?? {}, 'isBuiltin')
435
+ if (isBuiltin(element)) return element
405
436
 
406
437
  const cardinality = getMaxCardinality(element)
407
438
 
@@ -447,7 +478,7 @@ class Resolver {
447
478
  // this.#resolveInlineDeclarationType(element.enum, result, file)
448
479
  // or
449
480
  // stringifyEnumType(csnToEnumPairs(element))
450
- this.#resolveTypeName(element.type, result)
481
+ this.resolveTypeName(element.type, result)
451
482
  }
452
483
  } else {
453
484
  this.resolvePotentialReferenceType(element.type, result, file)
@@ -482,7 +513,7 @@ class Resolver {
482
513
  * }
483
514
  *
484
515
  * These have to be resolved to a new type.
485
- * @param {any[]} items - the properties of the inline declaration.
516
+ * @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
486
517
  * @param {TypeResolveInfo} into - @see resolveType()
487
518
  * @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
488
519
  * This is important to correctly detect when a field in the inline declaration is referencing
@@ -503,11 +534,11 @@ class Resolver {
503
534
  if (val.elements) {
504
535
  this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
505
536
  } else if (val.constructor === Object && 'ref' in val) {
506
- this.#resolveTypeName(val.ref[0], into)
537
+ this.resolveTypeName(val.ref[0], into)
507
538
  into.isForeignKeyReference = true
508
539
  } else {
509
540
  // val is string
510
- this.#resolveTypeName(val, into)
541
+ this.resolveTypeName(val, into)
511
542
  }
512
543
  }
513
544
 
@@ -516,11 +547,11 @@ class Resolver {
516
547
  * String is supposed to refer to either a builtin type
517
548
  * or any type defined in CSN.
518
549
  * @param {string} t - fully qualified type, like cds.String, or a.b.c.d.Foo
519
- * @param {TypeResolveInfo} into - optional dictionary to fill by reference, see resolveType()
550
+ * @param {TypeResolveInfo} [into] - optional dictionary to fill by reference, see resolveType()
520
551
  * @returns @see resolveType
521
552
  */
522
- #resolveTypeName(t, into) {
523
- const result = into || {}
553
+ resolveTypeName(t, into) {
554
+ const result = into ?? {}
524
555
  const path = t.split('.')
525
556
  const builtin = this.builtinResolver.resolveBuiltin(path)
526
557
  if (builtin === undefined) {
package/lib/typedefs.d.ts CHANGED
@@ -1,10 +1,55 @@
1
1
  export module resolver {
2
+ type ref = {
3
+ ref: string[],
4
+ as?: string
5
+ }
6
+
7
+ export type PropertyModifier = 'override' | 'declare'
8
+
2
9
  export type EntityCSN = {
3
- cardinality?: { max?: '*' | number }
10
+ actions?: OperationCSN[],
11
+ cardinality?: { max?: '*' | string }
12
+ compositions?: { target: string }[]
13
+ doc?: string,
14
+ elements?: { [key: string]: EntityCSN }
15
+ key?: string // custom!!
16
+ keys?: { [key:string]: any }
17
+ kind: string,
18
+ includes?: string[]
19
+ items?: EntityCSN
20
+ notNull?: boolean, // custom!
21
+ on?: string,
22
+ parent?: EntityCSN
23
+ projection?: { from: ref, columns: (ref | '*')[]}
24
+ target?: string,
25
+ type: string | ref,
26
+ name: string,
27
+ '@odata.draft.enabled'?: boolean // custom!
28
+ _unresolved?: boolean
29
+ isRefNotNull?: boolean // custom!
30
+ }
31
+
32
+ export type OperationCSN = EntityCSN & {
33
+ params: {[key:string]: EntityCSN},
34
+ returns?: any,
35
+ kind: 'action' | 'function'
36
+ }
37
+
38
+ export type ProjectionCSN = EntityCSN & {
39
+ projection: any
40
+ }
41
+
42
+ export type ViewCSN = EntityCSN & {
43
+ query?: any
44
+ }
45
+
46
+
47
+ export type EnumCSN = EntityCSN & {
48
+ enum: {[key:name]: string}
4
49
  }
5
50
 
6
51
  export type CSN = {
7
- definitions?: { [key: string]: EntityCSN }
52
+ definitions: { [key: string]: EntityCSN },
8
53
  }
9
54
 
10
55
  /**
@@ -19,19 +64,25 @@ export module resolver {
19
64
  * ```
20
65
  */
21
66
  export type TypeResolveInfo = {
22
- isBuiltin: boolean,
23
- isDeepRequire: boolean,
24
- isNotNull: boolean,
25
- isInlineDeclaration: boolean,
26
- isForeignKeyReference: boolean,
27
- isArray: boolean,
28
- type: string,
67
+ isBuiltin?: boolean,
68
+ isDeepRequire?: boolean,
69
+ isNotNull?: boolean,
70
+ isInlineDeclaration?: boolean,
71
+ isForeignKeyReference?: boolean,
72
+ isArray?: boolean,
73
+ type?: string,
29
74
  path?: Path,
30
- csn?: CSN,
31
- imports: Path[]
32
- inner: TypeResolveInfo
75
+ csn?: EntityCSNCSN,
76
+ imports?: Path[]
77
+ inner?: TypeResolveInfo,
78
+ structuredType?: {[key: string]: {typeName: string, typeInfo: TypeResolveInfo}} // FIXME: same as inner?
79
+ plainName: string,
80
+ typeName?: string // FIXME: same as plainName?
81
+ inflection?: visitor.Inflection
33
82
  }
34
83
 
84
+ export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
85
+
35
86
  // TODO: this will be completely replaced by EntityInfo
36
87
  export type Untangled = {
37
88
  // scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
@@ -68,6 +119,9 @@ export module visitor {
68
119
  outputDirectory: string,
69
120
  logLevel: number,
70
121
  jsConfigPath?: string,
122
+ inlineDeclarations: 'flat' | 'structured',
123
+ propertiesOptional: boolean,
124
+ IEEE754Compatible: boolean,
71
125
  }
72
126
 
73
127
  export type VisitorOptions = {
@@ -81,7 +135,7 @@ export module visitor {
81
135
  }
82
136
 
83
137
  export type Inflection = {
84
- typeName: string,
138
+ typeName?: string,
85
139
  singular: string,
86
140
  plural: string
87
141
  }
package/lib/util.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** @typedef { import('./typedefs').util.Annotations} Annotations */
2
2
  /** @typedef { import('./typedefs').util.CommandLineFlags } CommandlineFlag */
3
3
  /** @typedef { import('./typedefs').util.ParsedFlag } ParsedFlags */
4
+ /** @typedef { import('./typedefs').resolver.EntityCSN } EntityCSN */
4
5
 
5
6
  // inflection functions are stolen from github/cap/dev/blob/main/etc/inflect.js
6
7
 
@@ -23,7 +24,7 @@ const annotations = {
23
24
  * from a CSN. Valid annotations are listed in util.annotations
24
25
  * and their precedence is in order of definition.
25
26
  * If no singular is specified at all, undefined is returned.
26
- * @param {object} csn - the CSN of an entity to check
27
+ * @param {EntityCSN} csn - the CSN of an entity to check
27
28
  * @returns {string | undefined} the singular annotation or undefined
28
29
  */
29
30
  const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
@@ -33,7 +34,7 @@ const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.h
33
34
  * from a CSN. Valid annotations are listed in util.annotations
34
35
  * and their precedence is in order of definition.
35
36
  * If no plural is specified at all, undefined is returned.
36
- * @param {object} csn - the CSN of an entity to check
37
+ * @param {EntityCSN} csn - the CSN of an entity to check
37
38
  * @returns {string | undefined} the plural annotation or undefined
38
39
  */
39
40
  const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
@@ -137,7 +138,7 @@ const deepMerge = (target, source) => {
137
138
  * @returns {ParsedFlags}
138
139
  */
139
140
  const parseCommandlineArgs = (argv, validFlags) => {
140
- const isFlag = arg => arg.startsWith('--')
141
+ const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
141
142
  const positional = []
142
143
  const named = {}
143
144