@cap-js/cds-typer 0.26.0 → 0.28.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.
@@ -61,6 +61,7 @@ class EntityInfo {
61
61
  /** @type {import('../typedefs').resolver.EntityCSN | undefined} */
62
62
  #csn
63
63
 
64
+ /** @returns the **inferred** csn for this entity. */
64
65
  get csn () {
65
66
  return this.#csn ??= this.#resolver.csn.definitions[this.fullyQualifiedName]
66
67
  }
@@ -3,7 +3,7 @@
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('../components/wrappers')
6
+ const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne, createKey } = require('../components/wrappers')
7
7
  const { StructuredInlineDeclarationResolver } = require('../components/inline')
8
8
  const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
9
9
  const { isReferenceType } = require('../components/reference')
@@ -13,11 +13,13 @@ const { BuiltinResolver } = require('./builtin')
13
13
  const { LOG } = require('../logging')
14
14
  const { last } = require('../components/identifier')
15
15
  const { getPropertyModifiers } = require('../components/property')
16
+ const { configuration } = require('../config')
16
17
 
17
18
  /** @typedef {import('../visitor').Visitor} Visitor */
18
19
  /** @typedef {import('../typedefs').resolver.CSN} CSN */
19
20
  /** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
20
21
  /** @typedef {import('../typedefs').resolver.TypeResolveInfo} TypeResolveInfo */
22
+ /** @typedef {import('../typedefs').resolver.TypeResolveOptions} TypeResolveOptions */
21
23
  /** @typedef {import('../typedefs').visitor.Inflection} Inflection */
22
24
  /** @typedef {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection }}} ResolveAndRequireInfo */
23
25
 
@@ -30,7 +32,7 @@ class Resolver {
30
32
  this.visitor = visitor
31
33
 
32
34
  /** @type {BuiltinResolver} */
33
- this.builtinResolver = new BuiltinResolver(visitor.options)
35
+ this.builtinResolver = new BuiltinResolver({ IEEE754Compatible: configuration.IEEE754Compatible })
34
36
 
35
37
  /** @type {Library[]} */
36
38
  this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
@@ -63,8 +65,7 @@ class Resolver {
63
65
  * @returns {boolean} whether the type is configured to be optional
64
66
  */
65
67
  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
+ return type.items ? !type.items.notNull : !type.notNull
68
69
  }
69
70
 
70
71
  /**
@@ -159,8 +160,8 @@ class Resolver {
159
160
  if (parts.length <= 1) return []
160
161
 
161
162
  /**
162
- * @param {string} property
163
- * @param {import('../typedefs').resolver.EntityCSN} entity
163
+ * @param {string} property - the property to check
164
+ * @param {import('../typedefs').resolver.EntityCSN} entity - the entity to check the property against
164
165
  */
165
166
  const isPropertyOf = (property, entity) => property && Object.hasOwn(entity?.elements ?? {}, property)
166
167
 
@@ -233,7 +234,7 @@ class Resolver {
233
234
  statementEnd: '',
234
235
  modifiers: getPropertyModifiers(typeInfo.csn)
235
236
  })
236
- typeName = into.join(' ')
237
+ typeName = into.join()
237
238
  singular = typeName
238
239
  plural = createArrayOf(typeName)
239
240
  } else {
@@ -276,10 +277,11 @@ class Resolver {
276
277
  * 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
277
278
  * @param {import('../visitor').EntityCSN} element - the CSN element to resolve the type for.
278
279
  * @param {SourceFile} file - source file for context.
280
+ * @param {TypeResolveOptions} [options] - resolver options
279
281
  * @returns {ResolveAndRequireInfo} info about the resolved type
280
282
  */
281
- resolveAndRequire(element, file) {
282
- const typeInfo = this.resolveType(element, file)
283
+ resolveAndRequire(element, file, options) {
284
+ const typeInfo = this.resolveType(element, file, options)
283
285
  const cardinality = getMaxCardinality(element)
284
286
 
285
287
  let typeName = typeInfo.plainName ?? typeInfo.type
@@ -385,6 +387,10 @@ class Resolver {
385
387
  plural: typeName
386
388
  }
387
389
 
390
+ if (element.key === true) {
391
+ typeName = createKey(typeName)
392
+ }
393
+
388
394
  // FIXME: typeName could probably just become part of typeInfo
389
395
  return { typeName, typeInfo }
390
396
  }
@@ -430,9 +436,10 @@ class Resolver {
430
436
  * Enriched with additional information for improved printout (see return type).
431
437
  * @param {import('../typedefs').resolver.EntityCSN | TypeResolveInfo} element - the CSN element to resolve the type for.
432
438
  * @param {SourceFile} file - source file for context.
439
+ * @param {TypeResolveOptions} [options] - resolver options
433
440
  * @returns {TypeResolveInfo} description of the resolved type
434
441
  */
435
- resolveType(element, file) {
442
+ resolveType(element, file, options) {
436
443
  // while resolving inline declarations, it can happen that we land here
437
444
  // with an already resolved type. In that case, just return the type we have.
438
445
  // type guard check purely to satisfy return statement
@@ -501,9 +508,10 @@ class Resolver {
501
508
  result.isBuiltin = true
502
509
  this.resolveType(element.items, file)
503
510
  //delete element.items
504
- } else if (element?.elements && !element?.type) {
511
+ } else if (element?.elements && (options?.forceInlineStructs || !element?.type)) {
505
512
  // explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
506
- this.#resolveInlineDeclarationType(element.elements, result, file)
513
+ // if the resolver option `forceInlineStructs` is `true`, named types in elements will be converted to inline
514
+ this.#resolveInlineDeclarationType(element.elements, result, file, options)
507
515
  }
508
516
 
509
517
  if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
@@ -525,11 +533,12 @@ class Resolver {
525
533
  * @param {{ [key: string]: EntityCSN }} items - the properties of the inline declaration.
526
534
  * @param {TypeResolveInfo} into - @see resolveType()
527
535
  * @param {SourceFile} relativeTo - the sourcefile in which we have found the reference to the type.
536
+ * @param {TypeResolveOptions} [options] - resolver options
528
537
  * This is important to correctly detect when a field in the inline declaration is referencing
529
538
  * types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
530
539
  */
531
- #resolveInlineDeclarationType(items, into, relativeTo) {
532
- return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
540
+ #resolveInlineDeclarationType(items, into, relativeTo, options) {
541
+ return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo, options)
533
542
  }
534
543
 
535
544
  /**
package/lib/typedefs.d.ts CHANGED
@@ -82,6 +82,39 @@ export module resolver {
82
82
  inflection?: visitor.Inflection
83
83
  }
84
84
 
85
+ /**
86
+ * Custom options to be used during type resolvement
87
+ */
88
+ export type TypeResolveOptions = {
89
+ /**
90
+ * Entity elements that have a custom type are not available when entity is accessed using CQL.
91
+ *
92
+ * They only exist in the original defined form in the CSN and LinkedCSN but not in the compiled
93
+ * OData or SQL models (i.e. `cds.compile(..).for.odata()`).
94
+ *
95
+ * Therefore they need to be flattened down like inline structs.
96
+ *
97
+ * ```cds
98
+ * // model.cds
99
+ * type Adress {
100
+ * street: String;
101
+ * zipCode: String;
102
+ * }
103
+ * entity Persons {
104
+ * title: String
105
+ * address: Adress
106
+ * }
107
+ * ```
108
+ *
109
+ * // service.js
110
+ * ```js
111
+ * const {title, address_street, address_zipCode} = await SELECT.from(Persons);
112
+ * ```
113
+ *
114
+ */
115
+ forceInlineStructs?: boolean
116
+ }
117
+
85
118
  export type EntityInfo = Exclude<ReturnType<import('../lib/resolution/entity').EntityRepository['getByFq']>, null>
86
119
 
87
120
  // TODO: this will be completely replaced by EntityInfo
@@ -99,47 +132,13 @@ export module resolver {
99
132
 
100
133
  export module util {
101
134
  export type Annotations = {
102
- name?: string,
135
+ name: string,
103
136
  '@singular'?: string,
104
137
  '@plural'?: string
105
138
  }
106
-
107
- export type CommandLineFlags = {
108
- desc: string,
109
- default?: any
110
- }
111
-
112
- export type ParsedFlag = {
113
- positional: string[],
114
- named: { [key: string]: any }
115
- }
116
139
  }
117
140
 
118
141
  export module visitor {
119
- export type CompileParameters = {
120
- outputDirectory: string,
121
- logLevel: number,
122
- useEntitiesProxy: boolean,
123
- jsConfigPath?: string,
124
- inlineDeclarations: 'flat' | 'structured',
125
- propertiesOptional: boolean,
126
- IEEE754Compatible: boolean,
127
- }
128
-
129
- export type VisitorOptions = {
130
- /** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
131
- propertiesOptional: boolean,
132
- /**
133
- * `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
134
- * `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
135
- */
136
- inlineDeclarations: 'flat' | 'structured',
137
- /**
138
- * `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
139
- */
140
- useEntitiesProxy: boolean
141
- }
142
-
143
142
  export type Inflection = {
144
143
  typeName?: string,
145
144
  singular: string,
@@ -158,9 +157,54 @@ export module visitor {
158
157
  }
159
158
  }
160
159
 
160
+ export module config {
161
+ export module cli {
162
+ export type CLIFlags = 'version' | 'help'
163
+ export type ParameterSchema = {
164
+ [key: string]: {
165
+ desc: string,
166
+ allowed?: string[],
167
+ allowedHint?: string,
168
+ type?: 'string' | 'boolean' | 'number',
169
+ default?: string,
170
+ defaultHint?: string,
171
+ postprocess?: (value: string) => any,
172
+ camel?: string,
173
+ snake?: string
174
+ }
175
+ }
176
+
177
+ export type ParsedParameters = {
178
+ positional: string[],
179
+ named: { [key: keyof RuntimeParameters]: {
180
+ value: any,
181
+ isDefault: boolean,
182
+ } }
183
+ }
184
+ }
185
+
186
+ export type Configuration = {
187
+ outputDirectory: string,
188
+ logLevel: number,
189
+ /**
190
+ * `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
191
+ */
192
+ useEntitiesProxy: boolean,
193
+ jsConfigPath?: string,
194
+ /**
195
+ * `inlineDeclarations = 'structured'` -> @see {@link inline.StructuredInlineDeclarationResolver}
196
+ * `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
197
+ */
198
+ inlineDeclarations: 'flat' | 'structured',
199
+ /** `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable) */
200
+ propertiesOptional: boolean,
201
+ /**
202
+ * `IEEE754Compatible = true` -> any cds.Decimal will become `number | string`
203
+ */
204
+ IEEE754Compatible: boolean
205
+ }
206
+ }
207
+
161
208
  export module file {
162
209
  export type Namespace = Object<string, Buffer>
163
- export type FileOptions = {
164
- useEntitiesProxy: boolean
165
- }
166
210
  }
package/lib/util.js CHANGED
@@ -19,6 +19,16 @@ const annotations = {
19
19
  plural: ['@plural'],
20
20
  }
21
21
 
22
+ /**
23
+ * Converts a camelCase string to snake_case.
24
+ * @param {string} camel - The camelCase string.
25
+ * @returns {string} - The snake_case string.
26
+ */
27
+ const camelToSnake = camel => camel
28
+ .replace(/([a-z])([A-Z])/g, '$1_$2') // Handle camelCase
29
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') // Handle sequences of uppercase letters
30
+ .toLowerCase()
31
+
22
32
  /**
23
33
  * Tries to retrieve an annotation that specifies the singular name
24
34
  * from a CSN. Valid annotations are listed in util.annotations
@@ -27,6 +37,7 @@ const annotations = {
27
37
  * @param {EntityCSN} csn - the CSN of an entity to check
28
38
  * @returns {string | undefined} the singular annotation or undefined
29
39
  */
40
+ // @ts-expect-error - can not use possible undefined from find as key
30
41
  const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
31
42
 
32
43
  /**
@@ -37,6 +48,7 @@ const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.h
37
48
  * @param {EntityCSN} csn - the CSN of an entity to check
38
49
  * @returns {string | undefined} the plural annotation or undefined
39
50
  */
51
+ // @ts-expect-error - can not use possible undefined from find as key
40
52
  const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
41
53
 
42
54
  /**
@@ -62,9 +74,9 @@ const unlocalize = name => {
62
74
  * @param {boolean?} stripped - if true, leading namespace will be stripped
63
75
  */
64
76
  const singular4 = (dn, stripped = false) => {
65
- let n = dn.name || dn
77
+ let n = dn.name ?? dn
66
78
  if (stripped) {
67
- n = n.match(last)[0]
79
+ n = n.match(last)?.[0] ?? ''
68
80
  }
69
81
  return (
70
82
  getSingularAnnotation(dn) ??
@@ -94,9 +106,9 @@ const singular4 = (dn, stripped = false) => {
94
106
  * @param {boolean} stripped - if true, leading namespace will be stripped
95
107
  */
96
108
  const plural4 = (dn, stripped) => {
97
- let n = dn.name || dn
109
+ let n = dn.name ?? dn
98
110
  if (stripped) {
99
- n = n.match(last)[0]
111
+ n = n.match(last)?.[0]
100
112
  }
101
113
  return (
102
114
  getPluralAnnotation(dn) ??
@@ -123,72 +135,13 @@ const deepMerge = (target, source) => {
123
135
  Object.assign(target, source)
124
136
  }
125
137
 
126
- /**
127
- * Parses command line arguments into named and positional parameters.
128
- * Named parameters are expected to start with a double dash (--).
129
- * If the next argument `B` after a named parameter `A` is not a named parameter itself,
130
- * `B` is used as value for `A`.
131
- * If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
132
- * Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
133
- * will cause an error.
134
- * Named parameters that are either not specified or do not have a value assigned to them may draw a default value
135
- * from their definition in validFlags.
136
- * @param {string[]} argv - list of command line arguments
137
- * @param {{[key: string]: CommandlineFlag}} validFlags - allowed flags. May specify default values.
138
- * @returns {ParsedFlags}
139
- */
140
- const parseCommandlineArgs = (argv, validFlags) => {
141
- const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
142
- const positional = []
143
- const named = {}
144
-
145
- let i = 0
146
- while (i < argv.length) {
147
- let arg = argv[i]
148
- if (isFlag(arg)) {
149
- arg = arg.slice(2)
150
- if (!(arg in validFlags)) {
151
- throw new Error(`invalid named flag '${arg}'`)
152
- } else {
153
- const next = argv[i + 1]
154
- if (next && !isFlag(next)) {
155
- named[arg] = next
156
- i++
157
- } else {
158
- named[arg] = validFlags[arg].default
159
- }
160
-
161
- const { allowed, allowedHint } = validFlags[arg]
162
- if (allowed && !allowed.includes(named[arg])) {
163
- throw new Error(`invalid value '${named[arg]}' for flag ${arg}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
164
- }
165
- }
166
- } else {
167
- positional.push(arg)
168
- }
169
- i++
170
- }
171
-
172
- const defaults = Object.entries(validFlags)
173
- .filter(e => !!e[1].default)
174
- .reduce((dict, [k, v]) => {
175
- dict[k] = v.default
176
- return dict
177
- }, {})
178
-
179
- return {
180
- named: Object.assign(defaults, named),
181
- positional,
182
- }
183
- }
184
-
185
138
  module.exports = {
186
139
  annotations,
140
+ camelToSnake,
187
141
  getSingularAnnotation,
188
142
  getPluralAnnotation,
189
143
  unlocalize,
190
144
  singular4,
191
145
  plural4,
192
- parseCommandlineArgs,
193
146
  deepMerge
194
147
  }