@cap-js/cds-typer 0.18.2 → 0.20.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/CHANGELOG.md CHANGED
@@ -4,10 +4,30 @@ All notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
6
6
 
7
- ## Version 0.19.0 - TBD
7
+ ## Version 0.21.0 - TBD
8
+
9
+ ## Version 0.20.0 - 2024-04-23
10
+ ### Added
11
+ - Types for actions and functions now expose a `.kind` property which holds the string `'function'` or `'action'` respectively
12
+ - Added the CdsDate, CdsDateTime, CdsTime, CdsTimestamp types, which are each represented as a `string`.
13
+ - Plural types can now also contain an optional numeric `$count` property
14
+
15
+ ### Changed
16
+ - Empty `.actions` properties and operations without parameters are now typed as `Record<never, never>` to make it clear they contain nothing and also to satisfy overzealous linters
17
+
18
+ ### Fixed
19
+ - Composition of aspects now properly resolve implicit `typeof` references in their properties
20
+ - Importing an enum into a service will now generate an alias to the original enum, instead of incorrectly duplicating the definition
21
+ - Returning entities from actions/ functions and using them as parameters will now properly use the singular inflection instead of returning an array thereof
22
+ - Aspects are now consistently named and called in their singular form
23
+ - Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
24
+
25
+ ## Version 0.19.0 - 2024-03-28
26
+ ### Added
27
+ - Support for `cds.Vector`, which will be represented as `string`
8
28
 
9
29
  ## Version 0.18.2 - 2024-03-21
10
- ### Fix
30
+ ### Fixed
11
31
  - Resolving `@sap/cds` will now look in the CWD first to ensure a consistent use the same CDS version across different setups
12
32
  - Types of function parameters starting with `cds.` are not automatically considered builtin anymore and receive a more thorough check against an allow-list
13
33
 
package/lib/cli.js CHANGED
@@ -21,7 +21,7 @@ const flags = {
21
21
  desc: 'This text.',
22
22
  },
23
23
  logLevel: {
24
- desc: `Minimum log level that is printed.`,
24
+ desc: 'Minimum log level that is printed.',
25
25
  allowed: Object.keys(Levels),
26
26
  default: Levels.ERROR,
27
27
  },
@@ -48,7 +48,7 @@ const hint = () => 'Missing or invalid parameter(s). Call with --help for more d
48
48
  const indent = (s, indentation) => s.split(EOL).map(line => `${indentation}${line}`).join(EOL)
49
49
 
50
50
  const help = () => `SYNOPSIS${EOL2}` +
51
- indent(`cds-typer [cds file | "*"]`, ' ') + EOL2 +
51
+ indent('cds-typer [cds file | "*"]', ' ') + EOL2 +
52
52
  indent(`Generates type information based on a CDS model.${EOL}Call with at least one positional parameter pointing${EOL}to the (root) CDS file you want to compile.`, ' ') + EOL2 +
53
53
  `OPTIONS${EOL2}` +
54
54
  Object.entries(flags)
@@ -67,11 +67,11 @@ const help = () => `SYNOPSIS${EOL2}` +
67
67
  s += `${EOL2}${indent(value.desc, ' ')}`
68
68
  return s
69
69
  }
70
- ).join(EOL2)
70
+ ).join(EOL2)
71
71
 
72
72
  const version = () => require('../package.json').version
73
73
 
74
- const main = async (args) => {
74
+ const main = async args => {
75
75
  if ('help' in args.named) {
76
76
  console.log(help())
77
77
  process.exit(0)
package/lib/compile.js CHANGED
@@ -38,18 +38,6 @@ const writeJsConfig = (path, logger) => {
38
38
  fs.writeFileSync(path, JSON.stringify(values, null, 2))
39
39
  }
40
40
 
41
- /**
42
- * Compiles a .cds file to Typescript types.
43
- * @param inputFile {string} path to input .cds file
44
- * @param parameters {CompileParameters} path to root directory for all generated files, min log level, etc.
45
- */
46
- const compileFromFile = async (inputFile, parameters) => {
47
- const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
48
- const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
49
- const inferred = await cds.linked(await cds.load(paths, { docs: true }))
50
- return compileFromCSN({xtended, inferred}, parameters)
51
- }
52
-
53
41
  /**
54
42
  * Compiles a CSN object to Typescript types.
55
43
  * @param {{xtended: CSN, inferred: CSN}} csn
@@ -69,6 +57,18 @@ const compileFromCSN = async (csn, parameters) => {
69
57
  )
70
58
  }
71
59
 
60
+ /**
61
+ * Compiles a .cds file to Typescript types.
62
+ * @param inputFile {string} path to input .cds file
63
+ * @param parameters {CompileParameters} path to root directory for all generated files, min log level, etc.
64
+ */
65
+ const compileFromFile = async (inputFile, parameters) => {
66
+ const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
67
+ const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
68
+ const inferred = await cds.linked(await cds.load(paths, { docs: true }))
69
+ return compileFromCSN({xtended, inferred}, parameters)
70
+ }
71
+
72
72
  module.exports = {
73
73
  compileFromFile
74
74
  }
@@ -0,0 +1,64 @@
1
+ const { SourceFile } = require('../file')
2
+
3
+ // eslint-disable-next-line no-template-curly-in-string
4
+ const dateRegex = '`${number}${number}${number}${number}-${number}${number}-${number}${number}`'
5
+ // eslint-disable-next-line no-template-curly-in-string
6
+ const timeRegex = '`${number}${number}:${number}${number}:${number}${number}`'
7
+
8
+ /**
9
+ * Base definitions used throughout the typing process,
10
+ * such as Associations and Compositions.
11
+ * @type {SourceFile}
12
+ */
13
+ const baseDefinitions = new SourceFile('_')
14
+ // FIXME: this should be a library someday
15
+ baseDefinitions.addPreamble(`
16
+ export namespace Association {
17
+ export type to <T> = T;
18
+ export namespace to {
19
+ export type many <T extends readonly any[]> = T;
20
+ }
21
+ }
22
+
23
+ export namespace Composition {
24
+ export type of <T> = T;
25
+ export namespace of {
26
+ export type many <T extends readonly any[]> = T;
27
+ }
28
+ }
29
+
30
+ export class Entity {
31
+ static data<T extends Entity> (this:T, _input:Object) : T {
32
+ return {} as T // mock
33
+ }
34
+ }
35
+
36
+ export type EntitySet<T> = T[] & {
37
+ data (input:object[]) : T[]
38
+ data (input:object) : T
39
+ };
40
+
41
+ export type DeepRequired<T> = {
42
+ [K in keyof T]: DeepRequired<T[K]>
43
+ } & Exclude<Required<T>, null>;
44
+
45
+
46
+ /**
47
+ * Dates and timestamps are strings during runtime, so cds-typer represents them as such.
48
+ */
49
+ export type CdsDate = ${dateRegex};
50
+ /**
51
+ * @see {@link CdsDate}
52
+ */
53
+ export type CdsDateTime = string;
54
+ /**
55
+ * @see {@link CdsDate}
56
+ */
57
+ export type CdsTime = ${timeRegex};
58
+ /**
59
+ * @see {@link CdsDate}
60
+ */
61
+ export type CdsTimestamp = string;
62
+ `)
63
+
64
+ module.exports = { baseDefinitions }
@@ -1,5 +1,26 @@
1
1
  const { normalise } = require('./identifier')
2
2
 
3
+ /**
4
+ * Extracts all unique values from a list of enum key-value pairs.
5
+ * If the value is an object, then the `.val` property is used.
6
+ * @param {[string, any | {val: any}][]} kvs
7
+ */
8
+ const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
9
+
10
+ /**
11
+ * Stringifies a list of enum key-value pairs into the righthand side of a TS type.
12
+ * @param {[string, string][]} kvs list of key-value pairs
13
+ * @returns {string} a stringified type
14
+ * @example
15
+ * ```js
16
+ * ['A', 'B', 'A'] // -> '"A" | "B"'
17
+ * ```
18
+ */
19
+ const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
20
+
21
+ // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
22
+ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
23
+
3
24
  /**
4
25
  * Prints an enum to a buffer. To be precise, it prints
5
26
  * a constant object and a type which together form an artificial enum.
@@ -30,33 +51,12 @@ function printEnum(buffer, name, kvs, options = {}) {
30
51
  const opts = {...{export: true}, ...options}
31
52
  buffer.add('// enum')
32
53
  buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
33
- kvs.forEach(([k, v]) => buffer.add(`${normalise(k)}: ${v},`))
54
+ kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
34
55
  , '} as const;')
35
56
  buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
36
57
  buffer.add('')
37
58
  }
38
59
 
39
- /**
40
- * Stringifies a list of enum key-value pairs into the righthand side of a TS type.
41
- * @param {[string, string][]} kvs list of key-value pairs
42
- * @returns {string} a stringified type
43
- * @example
44
- * ```js
45
- * ['A', 'B', 'A'] // -> '"A" | "B"'
46
- * ```
47
- */
48
- const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
49
-
50
- /**
51
- * Extracts all unique values from a list of enum key-value pairs.
52
- * If the value is an object, then the `.val` property is used.
53
- * @param {[string, any | {val: any}][]} kvs
54
- */
55
- const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
56
-
57
- // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key
58
- const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
59
-
60
60
  /**
61
61
  * Converts a CSN type describing an enum into a list of kv-pairs.
62
62
  * Values from CSN are unwrapped from their `.val` structure and
@@ -16,7 +16,7 @@
16
16
  * @param {{type: any}} element
17
17
  * @returns boolean
18
18
  */
19
- const isReferenceType = (element) => element.type && Object.hasOwn(element.type, 'ref')
19
+ const isReferenceType = element => element.type && Object.hasOwn(element.type, 'ref')
20
20
 
21
21
  module.exports = {
22
22
  isReferenceType
@@ -2,11 +2,13 @@
2
2
 
3
3
  const util = require('../util')
4
4
  // eslint-disable-next-line no-unused-vars
5
- const { Buffer, SourceFile, Path, Library, baseDefinitions } = require('../file')
5
+ const { Buffer, SourceFile, Path, Library } = require('../file')
6
6
  const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
7
7
  const { StructuredInlineDeclarationResolver } = require('./inline')
8
8
  const { isInlineEnumType, propertyToInlineEnumName } = require('./enum')
9
9
  const { isReferenceType } = require('./reference')
10
+ const { isEntity } = require('../csn')
11
+ const { baseDefinitions } = require('./basedefs')
10
12
 
11
13
  /** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
12
14
  /** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
@@ -24,6 +26,7 @@ const { isReferenceType } = require('./reference')
24
26
  * ```
25
27
  * @typedef {{
26
28
  * isBuiltin: boolean,
29
+ * isDeepRequire: boolean,
27
30
  * isNotNull: boolean,
28
31
  * isInlineDeclaration: boolean,
29
32
  * isForeignKeyReference: boolean,
@@ -45,6 +48,7 @@ const Builtins = {
45
48
  Binary: 'string',
46
49
  LargeString: 'string',
47
50
  LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
51
+ Vector: 'string',
48
52
  Integer: 'number',
49
53
  UInt8: 'number',
50
54
  Int16: 'number',
@@ -57,10 +61,10 @@ const Builtins = {
57
61
  Double: 'number',
58
62
  Boolean: 'boolean',
59
63
  // note: the date-related types are strings on purpose, which reflects their runtime behaviour
60
- Date: 'string', // yyyy-mm-dd
61
- DateTime: 'string', // yyyy-mm-dd + time + TZ (precision: seconds
62
- Time: 'string',
63
- Timestamp: 'string', // yyy-mm-dd + time + TZ (ms precision)
64
+ Date: '__.CdsDate', // yyyy-mm-dd
65
+ DateTime: '__.CdsDateTime', // yyyy-mm-dd + time + TZ (precision: seconds)
66
+ Time: '__.CdsTime', // hh:mm:ss
67
+ Timestamp: '__.CdsTimestamp', // yyy-mm-dd + time + TZ (ms precision)
64
68
  //
65
69
  Composition: 'Array',
66
70
  Association: 'Array'
@@ -87,7 +91,7 @@ class Resolver {
87
91
  */
88
92
  namespaces: {},
89
93
  /**
90
- * @type {{ [qualifier: string]: string }}
94
+ * @type {{ [qualifier: string]: string[] }}
91
95
  */
92
96
  propertyAccesses: {}
93
97
  }
@@ -102,12 +106,28 @@ class Resolver {
102
106
 
103
107
  /**
104
108
  * @param {string} qualifier
105
- * @returns {string?}
109
+ * @param {string} namespace
110
+ */
111
+ #cacheNamespace (qualifier, namespace) {
112
+ this.#caches.namespaces[qualifier] = namespace
113
+ }
114
+
115
+ /**
116
+ * @param {string} qualifier
117
+ * @returns {string[]?}
106
118
  */
107
119
  #getCachedPropertyAccess (qualifier) {
108
120
  return this.#caches.propertyAccesses[qualifier]
109
121
  }
110
122
 
123
+ /**
124
+ * @param {string} qualifier
125
+ * @param {string[]} propertyAccess
126
+ */
127
+ #cachePropertyAccess (qualifier, propertyAccess) {
128
+ this.#caches.propertyAccesses[qualifier] = propertyAccess
129
+ }
130
+
111
131
  get csn() { return this.visitor.csn.inferred }
112
132
 
113
133
  /** @param {Visitor} visitor */
@@ -117,6 +137,12 @@ class Resolver {
117
137
 
118
138
  /** @type {Library[]} */
119
139
  this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
140
+
141
+ /**
142
+ * @type {StructuredInlineDeclarationResolver}
143
+ * needed for inline declarations
144
+ */
145
+ this.structuredInlineResolver = new StructuredInlineDeclarationResolver(this.visitor)
120
146
  }
121
147
 
122
148
  /**
@@ -127,17 +153,40 @@ class Resolver {
127
153
  return this.libraries.filter(l => l.referenced)
128
154
  }
129
155
 
156
+ /**
157
+ * TODO: this should probably be a class where we can also cache the properties
158
+ * and only retrieve them on demand
159
+ * @typedef {Object} Untangled
160
+ * @property {string[]} scope in case the entity is wrapped in another entity `a.b.C.D.E.f.g` -> `[C,D]`
161
+ * @property {string} name name of the leaf entity `a.b.C.D.E.f.g` -> `E`
162
+ * @property {string[]} property the property access path `a.b.C.D.E.f.g` -> `[f,g]`
163
+ * @property {Path} namespace the cds namespace of the entity `a.b.C.D.E.f.g` -> `a.b`
164
+ */
165
+
130
166
  /**
131
167
  * Conveniently combines resolveNamespace and trimNamespace
132
168
  * to end up with both the resolved Path of the namespace,
133
169
  * and the clean name of the class.
134
170
  * @param {string} fq the fully qualified name of an entity.
135
- * @returns {[Path, string, string[]]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity, [2] holding chained property accesses
171
+ * @returns {Untangled} untangled qualifier
136
172
  */
137
173
  untangle(fq) {
138
- const ns = this.resolveNamespace(fq.split('.'))
139
- const name = this.trimNamespace(fq).split('.').at(-1) // nested entities would return Foo.Bar, so we only take the last part to get the actual entity name
140
- return [new Path(ns.split('.')), name, this.findPropertyAccess(name)]
174
+ const builtin = resolveBuiltin(fq)
175
+ if (builtin) return { namespace: new Path([]), name: builtin, property: [], scope: [] }
176
+
177
+ const ns = this.resolveNamespace(fq)
178
+ const nameAndProperty = this.trimNamespace(fq)
179
+ const property = this.findPropertyAccess(fq)
180
+ const nameParts = (property.length
181
+ ? nameAndProperty.slice(0, -(property.join('').length + property.length)) // +1 for each dot
182
+ : nameAndProperty
183
+ ).split('.')//.at(-1) // nested entities would return Foo.Bar, so we only take the last part to get the actual entity name
184
+ return {
185
+ namespace: new Path(ns.split('.')),
186
+ scope: nameParts.slice(0, -1),
187
+ name: nameParts.at(-1),
188
+ property
189
+ }
141
190
  }
142
191
 
143
192
  /**
@@ -151,7 +200,7 @@ class Resolver {
151
200
  * @returns {string} the entity name without leading namespace.
152
201
  */
153
202
  trimNamespace(p) {
154
- if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
203
+ //if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
155
204
  const parts = p.split('.')
156
205
  if (parts.length <= 1) return p
157
206
 
@@ -171,6 +220,7 @@ class Resolver {
171
220
  * From a fully qualified path, finds the parts that are property accesses.
172
221
  * This are specifically used in CDS' `typeof` syntax, where a property can
173
222
  * refer to another entity's property type.
223
+ * @param {string} p path
174
224
  * @example
175
225
  * ```
176
226
  * namespace namespace;
@@ -197,19 +247,24 @@ class Resolver {
197
247
  const parts = p.split('.')
198
248
  if (parts.length <= 1) return []
199
249
 
200
- // start on right side, go up while we have an entity at hand
201
- // we cant start on left side, as that clashes with undefined entities like "sap"
202
- // sadly we have to use the extended flavour here, as inferred csn contains artificial entities for
203
- // this kind of property access
204
- const defs = this.visitor.csn.xtended.definitions
205
- const properties = []
206
- let qualifier = parts.join('.')
207
- while (!defs[qualifier] && parts.length) {
208
- properties.unshift(parts.pop())
209
- qualifier = parts.join('.')
210
- }
250
+ const isPropertyOf = (property, entity) => entity && property && Object.hasOwn(entity?.elements, property)
211
251
 
212
- return properties
252
+ const defs = this.visitor.csn.inferred.definitions
253
+ // assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
254
+ let qualifier = parts.shift()
255
+ // find first entity from left (Entity1)
256
+ while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {
257
+ qualifier += `.${parts.shift()}`
258
+ }
259
+ // skip forward to the last entity from left (Entity3), assuming that there is no name conflict between entities and properties
260
+ // i.e.: if there is a property "Entity2" in the entity Entity1, this will instead [Entity2, Entity3, property1, property2] as property access
261
+ while (!isPropertyOf(parts[0], defs[qualifier]) && isEntity(defs[qualifier + `.${parts[0]}`])) {
262
+ qualifier += `.${parts.shift()}`
263
+ }
264
+ // assuming Entity3 _does_ own a property "property1", return [property1, property2]
265
+ const propertyAccess = isPropertyOf(parts[0], defs[qualifier]) ? parts : []
266
+ this.#cachePropertyAccess(p, propertyAccess)
267
+ return propertyAccess
213
268
  }
214
269
 
215
270
  /**
@@ -251,7 +306,7 @@ class Resolver {
251
306
  // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
252
307
  // piece of code instead to reduce overhead.
253
308
  const into = new Buffer()
254
- new StructuredInlineDeclarationResolver(this.visitor).printInlineType(undefined, { typeInfo }, into, '')
309
+ this.structuredInlineResolver.printInlineType(undefined, { typeInfo }, into, '')
255
310
  typeName = into.join(' ')
256
311
  singular = typeName
257
312
  plural = createArrayOf(typeName)
@@ -317,10 +372,29 @@ class Resolver {
317
372
  const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
318
373
  /** set `notNull = true` to avoid repeated `| not null` TS construction */
319
374
  target.notNull = true
320
- const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
321
- typeName =
322
- cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
323
- file.addImport(baseDefinitions.path)
375
+ const targetTypeInfo = this.resolveAndRequire(target, file)
376
+ if (targetTypeInfo.typeInfo.isDeepRequire === true) {
377
+ typeName = cardinality > 1 ? toMany(targetTypeInfo.typeName) : toOne(targetTypeInfo.typeName)
378
+ } else {
379
+ let { singular, plural } = targetTypeInfo.typeInfo.inflection
380
+
381
+ // FIXME: super hack!!
382
+ // Inflection currently does not retain the scope of the entity.
383
+ // But we can't just fix it in inflection(...), as that would break several other things
384
+ // So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
385
+ if (target.type) {
386
+ const untangled = this.untangle(target.type)
387
+ const scope = untangled.scope.join('.')
388
+ if (scope && !singular.startsWith(scope)) {
389
+ singular = `${scope}.${singular}`
390
+ }
391
+ }
392
+
393
+ typeName = cardinality > 1
394
+ ? toMany(plural)
395
+ : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
396
+ file.addImport(baseDefinitions.path)
397
+ }
324
398
  }
325
399
  } else {
326
400
  // TODO: this could go into resolve type
@@ -343,6 +417,7 @@ class Resolver {
343
417
  const [, ...members] = element.type.ref
344
418
  const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
345
419
  typeName = deepRequire(typeInfo.inflection.singular, lookup)
420
+ typeInfo.isDeepRequire = true
346
421
  file.addImport(baseDefinitions.path)
347
422
  }
348
423
  }
@@ -359,11 +434,17 @@ class Resolver {
359
434
  typeInfo.inflection = this.inflect(typeInfo)
360
435
  }
361
436
 
362
- const [,,propertyAccess] = this.untangle(typeName)
363
- if (propertyAccess.length) {
364
- const element = typeName.slice(0, -propertyAccess.join('.').length - 1)
365
- const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
366
- typeName = deepRequire(element) + access
437
+ // handle typeof (unless it has already been handled above)
438
+ const target = element.target?.name ?? element.type?.ref?.join('.') ?? element.type
439
+ if (target && !typeInfo.isDeepRequire) {
440
+ const { property: propertyAccess } = this.untangle(target)
441
+ if (propertyAccess.length) {
442
+ const element = target.slice(0, -propertyAccess.join('.').length - 1)
443
+ const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
444
+ // singular, as we have to access the property of the entity
445
+ typeName = deepRequire(util.singular4(element)) + access
446
+ typeInfo.isDeepRequire = true
447
+ }
367
448
  }
368
449
 
369
450
  // add fallback inflection. Mainly needed for array-of with builtin types.
@@ -409,9 +490,9 @@ class Resolver {
409
490
  * @returns {string} the namespace's name, i.e. 'a.b.c'.
410
491
  */
411
492
  resolveNamespace(pathParts) {
412
- if (typeof pathParts === 'string') {
413
- pathParts = pathParts.split('.')
414
- }
493
+ if (typeof pathParts === 'string') pathParts = pathParts.split('.')
494
+ const fq = pathParts.join('.')
495
+ if (this.#getCachedNamespace(fq)) return this.#getCachedNamespace(fq)
415
496
  let result
416
497
  while (result === undefined) {
417
498
  const path = pathParts.join('.')
@@ -424,6 +505,7 @@ class Resolver {
424
505
  pathParts = pathParts.slice(0, -1)
425
506
  }
426
507
  }
508
+ this.#cacheNamespace(fq, result)
427
509
  return result
428
510
  }
429
511
 
@@ -456,40 +538,38 @@ class Resolver {
456
538
  // later on with an inline declaration
457
539
  result.type = '{}'
458
540
  result.isInlineDeclaration = true
459
- } else {
460
- if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
461
- // element.parent is only set if the enum is attached to an entity's property.
462
- // If it is missing then we are dealing with an inline parameter type of an action.
463
- // Edge case: element.parent is set, but no .name property is attached. This happens
464
- // for inline enums inside types:
465
- // ```cds
466
- // type T {
467
- // x : String enum { ... }; // no element.name for x
468
- // }
469
- // ```
470
- // In that case, we currently resolve to the more general type (cds.String, here)
471
- if (element.parent?.name) {
472
- result.isInlineDeclaration = true
473
- // we use the singular as the initial declaration of these enums takes place
474
- // while defining the singular class. Which therefore uses the singular over the plural name.
475
- const cleanEntityName = util.singular4(element.parent, true)
476
- const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
477
- result.type = enumName
478
- result.plainName = enumName
479
- } else {
480
- // FIXME: this is the case where users have arrays of enums as action parameter type.
481
- // Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
482
- // the encasing type (e.g. `string` here)
483
- // We should instead aim for a proper type, i.e.
484
- // this.#resolveInlineDeclarationType(element.enum, result, file)
485
- // or
486
- // stringifyEnumType(csnToEnumPairs(element))
487
- this.#resolveTypeName(element.type, result)
488
- }
541
+ } else if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
542
+ // element.parent is only set if the enum is attached to an entity's property.
543
+ // If it is missing then we are dealing with an inline parameter type of an action.
544
+ // Edge case: element.parent is set, but no .name property is attached. This happens
545
+ // for inline enums inside types:
546
+ // ```cds
547
+ // type T {
548
+ // x : String enum { ... }; // no element.name for x
549
+ // }
550
+ // ```
551
+ // In that case, we currently resolve to the more general type (cds.String, here)
552
+ if (element.parent?.name) {
553
+ result.isInlineDeclaration = true
554
+ // we use the singular as the initial declaration of these enums takes place
555
+ // while defining the singular class. Which therefore uses the singular over the plural name.
556
+ const cleanEntityName = util.singular4(element.parent, true)
557
+ const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
558
+ result.type = enumName
559
+ result.plainName = enumName
489
560
  } else {
490
- this.resolvePotentialReferenceType(element.type, result, file)
491
- }
492
- }
561
+ // FIXME: this is the case where users have arrays of enums as action parameter type.
562
+ // Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
563
+ // the encasing type (e.g. `string` here)
564
+ // We should instead aim for a proper type, i.e.
565
+ // this.#resolveInlineDeclarationType(element.enum, result, file)
566
+ // or
567
+ // stringifyEnumType(csnToEnumPairs(element))
568
+ this.#resolveTypeName(element.type, result)
569
+ }
570
+ } else {
571
+ this.resolvePotentialReferenceType(element.type, result, file)
572
+ }
493
573
 
494
574
  // objects and arrays
495
575
  if (element?.items) {
@@ -505,9 +585,7 @@ class Resolver {
505
585
  }
506
586
 
507
587
  if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
508
- this.logger.warning(
509
- `Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`
510
- )
588
+ this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
511
589
  }
512
590
  return result
513
591
  }
@@ -584,7 +662,7 @@ class Resolver {
584
662
  result.plainName = 'this'
585
663
  } else {
586
664
  // type offered by some library
587
- const lib = this.libraries.find((lib) => lib.offers(t))
665
+ const lib = this.libraries.find(lib => lib.offers(t))
588
666
  if (lib) {
589
667
  // only use the last name of the (fully qualified) type name in this case.
590
668
  // We can not use trimNamespace, as that actually does a semantic lookup within the CSN.
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ empty: 'Record<never, never>'
3
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const base = require('../file').baseDefinitions.path.asIdentifier()
3
+ // this was derived from baseDefinitions before, but caused a circular dependency
4
+ const base = '__'
4
5
 
5
6
  /**
6
7
  * Wraps type into association to scalar.
@@ -37,6 +38,13 @@ const createCompositionOfMany = t => `${base}.Composition.of.many<${t}>`
37
38
  */
38
39
  const createArrayOf = t => `Array<${t}>`
39
40
 
41
+ /**
42
+ * Wraps type into object braces
43
+ * @param {string} t the properties, stringified and comma separated.
44
+ * @returns {string}
45
+ */
46
+ const createObjectOf = t => `{${t}}`
47
+
40
48
  /**
41
49
  * Wraps type into a deep require (removes all posibilities of undefined recursively).
42
50
  * @param {string} t the singular type name.
@@ -52,11 +60,12 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
52
60
  * concatenated to be properly indented by `buffer.add(...)`.
53
61
  */
54
62
  const docify = doc => doc
55
- ? ['/**'].concat(doc.split('\n').map((line) => `* ${line}`)).concat(['*/'])
63
+ ? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
56
64
  : []
57
65
 
58
66
  module.exports = {
59
67
  createArrayOf,
68
+ createObjectOf,
60
69
  createToOneAssociation,
61
70
  createToManyAssociation,
62
71
  createCompositionOfOne,