@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.
@@ -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 }
@@ -55,6 +58,15 @@ class Resolver {
55
58
  return this.existsInCsn(fq) || Boolean(this.builtinResolver.resolveBuiltin(fq))
56
59
  }
57
60
 
61
+ /**
62
+ * @param {EntityCSN} type - a CSN type
63
+ * @returns {boolean} whether the type is configured to be optional
64
+ */
65
+ isOptional(type) {
66
+ // TODO temporary solution to determine optional parameters. Align w/ compiler/importer.
67
+ return Object.keys(type).some(k => k.startsWith('@Core.OptionalParameter'))
68
+ }
69
+
58
70
  /**
59
71
  * Returns all libraries that have been referenced at least once.
60
72
  * @returns {Library[]}
@@ -86,7 +98,7 @@ class Resolver {
86
98
  return {
87
99
  namespace: new Path(ns.split('.')),
88
100
  scope: nameParts.slice(0, -1),
89
- name: nameParts.at(-1),
101
+ name: nameParts.at(-1) ?? nameAndProperty,
90
102
  property
91
103
  }
92
104
  }
@@ -146,10 +158,16 @@ class Resolver {
146
158
  const parts = p.split('.')
147
159
  if (parts.length <= 1) return []
148
160
 
149
- const isPropertyOf = (property, entity) => entity && property && Object.hasOwn(entity?.elements, property)
161
+ /**
162
+ * @param {string} property
163
+ * @param {import('../typedefs').resolver.EntityCSN} entity
164
+ */
165
+ const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
150
166
 
151
167
  const defs = this.visitor.csn.inferred.definitions
152
168
  // assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
169
+ /** @type {string} */
170
+ // @ts-expect-error - nope, we know there is at least one element
153
171
  let qualifier = parts.shift()
154
172
  // find first entity from left (Entity1)
155
173
  while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
@@ -175,7 +193,7 @@ class Resolver {
175
193
  * - inline type definitions, which don't really have a linguistic plural,
176
194
  * but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
177
195
  * @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
196
+ * @param {string | import('../file').Path} [namespace] - namespace the type occurs in. If passed, will be shaved off from the name
179
197
  * @returns {Inflection}
180
198
  */
181
199
  inflect(typeInfo, namespace) {
@@ -189,7 +207,11 @@ class Resolver {
189
207
  }
190
208
  }
191
209
 
192
- let typeName
210
+ if (namespace instanceof Path) {
211
+ namespace = namespace.asNamespace()
212
+ }
213
+
214
+ let typeName = ''
193
215
  let singular
194
216
  let plural
195
217
 
@@ -204,7 +226,13 @@ class Resolver {
204
226
  // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
205
227
  // piece of code instead to reduce overhead.
206
228
  const into = new Buffer()
207
- this.structuredInlineResolver.printInlineType(undefined, { typeInfo }, into, '')
229
+ this.structuredInlineResolver.printInlineType({
230
+ fq: '',
231
+ type: { typeInfo, typeName: '' },
232
+ buffer: into,
233
+ statementEnd: '',
234
+ modifiers: getPropertyModifiers(typeInfo.csn)
235
+ })
208
236
  typeName = into.join(' ')
209
237
  singular = typeName
210
238
  plural = createArrayOf(typeName)
@@ -246,9 +274,9 @@ class Resolver {
246
274
  * 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
247
275
  * 2. resolve any singular/ plural issues and association/ composition around it
248
276
  * 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.
277
+ * @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
250
278
  * @param {SourceFile} file - source file for context.
251
- * @returns {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} info about the resolved type
279
+ * @returns {ResolveAndRequireInfo} info about the resolved type
252
280
  */
253
281
  resolveAndRequire(element, file) {
254
282
  const typeInfo = this.resolveType(element, file)
@@ -266,9 +294,15 @@ class Resolver {
266
294
  }[element.constructor.name] ?? []
267
295
 
268
296
  if (toOne && toMany) {
269
- const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
297
+ /** @type { EntityCSN | { type: string } } */
298
+ // @ts-expect-error - nope, it is not undefined
299
+ const target = element.items ?? (typeof element.target === 'string'
300
+ ? { type: element.target }
301
+ : element.target)
270
302
  /** set `notNull = true` to avoid repeated `| not null` TS construction */
303
+ // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
271
304
  target.notNull = true
305
+ // @ts-expect-error - yes, target is a valid parameter
272
306
  const targetTypeInfo = this.resolveAndRequire(target, file)
273
307
  if (targetTypeInfo.typeInfo.isDeepRequire === true) {
274
308
  typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
@@ -280,7 +314,7 @@ class Resolver {
280
314
  // But we can't just fix it in inflection(...), as that would break several other things
281
315
  // So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
282
316
  if (target.type) {
283
- const untangled = this.visitor.entityRepository.getByFq(target.type)
317
+ const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
284
318
  const scope = untangled.scope.join('.')
285
319
  if (scope && !singular.startsWith(scope)) {
286
320
  singular = `${scope}.${singular}`
@@ -359,7 +393,7 @@ class Resolver {
359
393
  * Resolves the fully qualified name of an entity to its parent entity.
360
394
  * resolveParent(a.b.c.D) -> CSN {a.b.c}
361
395
  * @param {string} name - fully qualified name of the entity to resolve the parent of.
362
- * @returns {CSN} the resolved parent CSN.
396
+ * @returns {import('../typedefs').resolver.EntityCSN} the resolved parent CSN.
363
397
  */
364
398
  resolveParent(name) {
365
399
  return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
@@ -394,14 +428,20 @@ class Resolver {
394
428
  /**
395
429
  * Resolves an element's type to either a builtin or a user defined type.
396
430
  * Enriched with additional information for improved printout (see return type).
397
- * @param {CSN} element - the CSN element to resolve the type for.
431
+ * @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
398
432
  * @param {SourceFile} file - source file for context.
399
433
  * @returns {TypeResolveInfo} description of the resolved type
400
434
  */
401
435
  resolveType(element, file) {
402
436
  // while resolving inline declarations, it can happen that we land here
403
437
  // with an already resolved type. In that case, just return the type we have.
404
- if (element && Object.hasOwn(element, 'isBuiltin')) return element
438
+ // type guard check purely to satisfy return statement
439
+ /**
440
+ * @param {any} e - the element to check
441
+ * @returns {e is TypeResolveInfo}
442
+ */
443
+ const isBuiltin = e => Object.hasOwn(e ?? {}, 'isBuiltin')
444
+ if (isBuiltin(element)) return element
405
445
 
406
446
  const cardinality = getMaxCardinality(element)
407
447
 
@@ -447,7 +487,7 @@ class Resolver {
447
487
  // this.#resolveInlineDeclarationType(element.enum, result, file)
448
488
  // or
449
489
  // stringifyEnumType(csnToEnumPairs(element))
450
- this.#resolveTypeName(element.type, result)
490
+ this.resolveTypeName(element.type, result)
451
491
  }
452
492
  } else {
453
493
  this.resolvePotentialReferenceType(element.type, result, file)
@@ -482,7 +522,7 @@ class Resolver {
482
522
  * }
483
523
  *
484
524
  * These have to be resolved to a new type.
485
- * @param {any[]} items - the properties of the inline declaration.
525
+ * @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
486
526
  * @param {TypeResolveInfo} into - @see resolveType()
487
527
  * @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
488
528
  * This is important to correctly detect when a field in the inline declaration is referencing
@@ -503,11 +543,11 @@ class Resolver {
503
543
  if (val.elements) {
504
544
  this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
505
545
  } else if (val.constructor === Object && 'ref' in val) {
506
- this.#resolveTypeName(val.ref[0], into)
546
+ this.resolveTypeName(val.ref[0], into)
507
547
  into.isForeignKeyReference = true
508
548
  } else {
509
549
  // val is string
510
- this.#resolveTypeName(val, into)
550
+ this.resolveTypeName(val, into)
511
551
  }
512
552
  }
513
553
 
@@ -516,11 +556,11 @@ class Resolver {
516
556
  * String is supposed to refer to either a builtin type
517
557
  * or any type defined in CSN.
518
558
  * @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()
559
+ * @param {TypeResolveInfo} [into] - optional dictionary to fill by reference, see resolveType()
520
560
  * @returns @see resolveType
521
561
  */
522
- #resolveTypeName(t, into) {
523
- const result = into || {}
562
+ resolveTypeName(t, into) {
563
+ const result = into ?? {}
524
564
  const path = t.split('.')
525
565
  const builtin = this.builtinResolver.resolveBuiltin(path)
526
566
  if (builtin === undefined) {
package/lib/typedefs.d.ts CHANGED
@@ -1,10 +1,56 @@
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
+ operations?: OperationCSN[],
12
+ cardinality?: { max?: '*' | string }
13
+ compositions?: { target: string }[]
14
+ doc?: string,
15
+ elements?: { [key: string]: EntityCSN }
16
+ key?: string // custom!!
17
+ keys?: { [key:string]: any }
18
+ kind: string,
19
+ includes?: string[]
20
+ items?: EntityCSN
21
+ notNull?: boolean, // custom!
22
+ on?: string,
23
+ parent?: EntityCSN
24
+ projection?: { from: ref, columns: (ref | '*')[]}
25
+ target?: string,
26
+ type: string | ref,
27
+ name: string,
28
+ '@odata.draft.enabled'?: boolean // custom!
29
+ _unresolved?: boolean
30
+ isRefNotNull?: boolean // custom!
31
+ }
32
+
33
+ export type OperationCSN = EntityCSN & {
34
+ params: {[key:string]: EntityCSN},
35
+ returns?: any,
36
+ kind: 'action' | 'function'
37
+ }
38
+
39
+ export type ProjectionCSN = EntityCSN & {
40
+ projection: any
41
+ }
42
+
43
+ export type ViewCSN = EntityCSN & {
44
+ query?: any
45
+ }
46
+
47
+
48
+ export type EnumCSN = EntityCSN & {
49
+ enum: {[key:name]: string}
4
50
  }
5
51
 
6
52
  export type CSN = {
7
- definitions?: { [key: string]: EntityCSN }
53
+ definitions: { [key: string]: EntityCSN },
8
54
  }
9
55
 
10
56
  /**
@@ -19,19 +65,25 @@ export module resolver {
19
65
  * ```
20
66
  */
21
67
  export type TypeResolveInfo = {
22
- isBuiltin: boolean,
23
- isDeepRequire: boolean,
24
- isNotNull: boolean,
25
- isInlineDeclaration: boolean,
26
- isForeignKeyReference: boolean,
27
- isArray: boolean,
28
- type: string,
68
+ isBuiltin?: boolean,
69
+ isDeepRequire?: boolean,
70
+ isNotNull?: boolean,
71
+ isInlineDeclaration?: boolean,
72
+ isForeignKeyReference?: boolean,
73
+ isArray?: boolean,
74
+ type?: string,
29
75
  path?: Path,
30
- csn?: CSN,
31
- imports: Path[]
32
- inner: TypeResolveInfo
76
+ csn?: EntityCSNCSN,
77
+ imports?: Path[]
78
+ inner?: TypeResolveInfo,
79
+ structuredType?: {[key: string]: {typeName: string, typeInfo: TypeResolveInfo}} // FIXME: same as inner?
80
+ plainName: string,
81
+ typeName?: string // FIXME: same as plainName?
82
+ inflection?: visitor.Inflection
33
83
  }
34
84
 
85
+ export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
86
+
35
87
  // TODO: this will be completely replaced by EntityInfo
36
88
  export type Untangled = {
37
89
  // scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
@@ -67,7 +119,11 @@ export module visitor {
67
119
  export type CompileParameters = {
68
120
  outputDirectory: string,
69
121
  logLevel: number,
122
+ useEntitiesProxy: boolean,
70
123
  jsConfigPath?: string,
124
+ inlineDeclarations: 'flat' | 'structured',
125
+ propertiesOptional: boolean,
126
+ IEEE754Compatible: boolean,
71
127
  }
72
128
 
73
129
  export type VisitorOptions = {
@@ -78,10 +134,14 @@ export module visitor {
78
134
  * `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
79
135
  */
80
136
  inlineDeclarations: 'flat' | 'structured',
137
+ /**
138
+ * `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
139
+ */
140
+ useEntitiesProxy: boolean
81
141
  }
82
142
 
83
143
  export type Inflection = {
84
- typeName: string,
144
+ typeName?: string,
85
145
  singular: string,
86
146
  plural: string
87
147
  }
@@ -89,8 +149,18 @@ export module visitor {
89
149
  export type Context = {
90
150
  entity: string
91
151
  }
152
+
153
+ export type ParamInfo = {
154
+ name: string,
155
+ modifier: '' | '?',
156
+ type: string,
157
+ doc?: string
158
+ }
92
159
  }
93
160
 
94
161
  export module file {
95
162
  export type Namespace = Object<string, Buffer>
163
+ export type FileOptions = {
164
+ useEntitiesProxy: boolean
165
+ }
96
166
  }
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