@cap-js/cds-typer 0.19.0 → 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,14 +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.20.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
8
24
 
9
25
  ## Version 0.19.0 - 2024-03-28
10
26
  ### Added
11
27
  - Support for `cds.Vector`, which will be represented as `string`
12
28
 
13
29
  ## Version 0.18.2 - 2024-03-21
14
- ### Fix
30
+ ### Fixed
15
31
  - Resolving `@sap/cds` will now look in the CWD first to ensure a consistent use the same CDS version across different setups
16
32
  - Types of function parameters starting with `cds.` are not automatically considered builtin anymore and receive a more thorough check against an allow-list
17
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,
@@ -58,10 +61,10 @@ const Builtins = {
58
61
  Double: 'number',
59
62
  Boolean: 'boolean',
60
63
  // note: the date-related types are strings on purpose, which reflects their runtime behaviour
61
- Date: 'string', // yyyy-mm-dd
62
- DateTime: 'string', // yyyy-mm-dd + time + TZ (precision: seconds
63
- Time: 'string',
64
- 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)
65
68
  //
66
69
  Composition: 'Array',
67
70
  Association: 'Array'
@@ -88,7 +91,7 @@ class Resolver {
88
91
  */
89
92
  namespaces: {},
90
93
  /**
91
- * @type {{ [qualifier: string]: string }}
94
+ * @type {{ [qualifier: string]: string[] }}
92
95
  */
93
96
  propertyAccesses: {}
94
97
  }
@@ -103,12 +106,28 @@ class Resolver {
103
106
 
104
107
  /**
105
108
  * @param {string} qualifier
106
- * @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[]?}
107
118
  */
108
119
  #getCachedPropertyAccess (qualifier) {
109
120
  return this.#caches.propertyAccesses[qualifier]
110
121
  }
111
122
 
123
+ /**
124
+ * @param {string} qualifier
125
+ * @param {string[]} propertyAccess
126
+ */
127
+ #cachePropertyAccess (qualifier, propertyAccess) {
128
+ this.#caches.propertyAccesses[qualifier] = propertyAccess
129
+ }
130
+
112
131
  get csn() { return this.visitor.csn.inferred }
113
132
 
114
133
  /** @param {Visitor} visitor */
@@ -118,6 +137,12 @@ class Resolver {
118
137
 
119
138
  /** @type {Library[]} */
120
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)
121
146
  }
122
147
 
123
148
  /**
@@ -128,17 +153,40 @@ class Resolver {
128
153
  return this.libraries.filter(l => l.referenced)
129
154
  }
130
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
+
131
166
  /**
132
167
  * Conveniently combines resolveNamespace and trimNamespace
133
168
  * to end up with both the resolved Path of the namespace,
134
169
  * and the clean name of the class.
135
170
  * @param {string} fq the fully qualified name of an entity.
136
- * @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
137
172
  */
138
173
  untangle(fq) {
139
- const ns = this.resolveNamespace(fq.split('.'))
140
- 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
141
- 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
+ }
142
190
  }
143
191
 
144
192
  /**
@@ -152,7 +200,7 @@ class Resolver {
152
200
  * @returns {string} the entity name without leading namespace.
153
201
  */
154
202
  trimNamespace(p) {
155
- if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
203
+ //if (this.#getCachedNamespace(p)) return this.#getCachedNamespace(p)
156
204
  const parts = p.split('.')
157
205
  if (parts.length <= 1) return p
158
206
 
@@ -172,6 +220,7 @@ class Resolver {
172
220
  * From a fully qualified path, finds the parts that are property accesses.
173
221
  * This are specifically used in CDS' `typeof` syntax, where a property can
174
222
  * refer to another entity's property type.
223
+ * @param {string} p path
175
224
  * @example
176
225
  * ```
177
226
  * namespace namespace;
@@ -198,19 +247,24 @@ class Resolver {
198
247
  const parts = p.split('.')
199
248
  if (parts.length <= 1) return []
200
249
 
201
- // start on right side, go up while we have an entity at hand
202
- // we cant start on left side, as that clashes with undefined entities like "sap"
203
- // sadly we have to use the extended flavour here, as inferred csn contains artificial entities for
204
- // this kind of property access
205
- const defs = this.visitor.csn.xtended.definitions
206
- const properties = []
207
- let qualifier = parts.join('.')
208
- while (!defs[qualifier] && parts.length) {
209
- properties.unshift(parts.pop())
210
- qualifier = parts.join('.')
211
- }
250
+ const isPropertyOf = (property, entity) => entity && property && Object.hasOwn(entity?.elements, property)
212
251
 
213
- 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
214
268
  }
215
269
 
216
270
  /**
@@ -252,7 +306,7 @@ class Resolver {
252
306
  // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
253
307
  // piece of code instead to reduce overhead.
254
308
  const into = new Buffer()
255
- new StructuredInlineDeclarationResolver(this.visitor).printInlineType(undefined, { typeInfo }, into, '')
309
+ this.structuredInlineResolver.printInlineType(undefined, { typeInfo }, into, '')
256
310
  typeName = into.join(' ')
257
311
  singular = typeName
258
312
  plural = createArrayOf(typeName)
@@ -318,10 +372,29 @@ class Resolver {
318
372
  const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
319
373
  /** set `notNull = true` to avoid repeated `| not null` TS construction */
320
374
  target.notNull = true
321
- const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
322
- typeName =
323
- cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
324
- 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
+ }
325
398
  }
326
399
  } else {
327
400
  // TODO: this could go into resolve type
@@ -344,6 +417,7 @@ class Resolver {
344
417
  const [, ...members] = element.type.ref
345
418
  const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
346
419
  typeName = deepRequire(typeInfo.inflection.singular, lookup)
420
+ typeInfo.isDeepRequire = true
347
421
  file.addImport(baseDefinitions.path)
348
422
  }
349
423
  }
@@ -360,11 +434,17 @@ class Resolver {
360
434
  typeInfo.inflection = this.inflect(typeInfo)
361
435
  }
362
436
 
363
- const [,,propertyAccess] = this.untangle(typeName)
364
- if (propertyAccess.length) {
365
- const element = typeName.slice(0, -propertyAccess.join('.').length - 1)
366
- const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)
367
- 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
+ }
368
448
  }
369
449
 
370
450
  // add fallback inflection. Mainly needed for array-of with builtin types.
@@ -410,9 +490,9 @@ class Resolver {
410
490
  * @returns {string} the namespace's name, i.e. 'a.b.c'.
411
491
  */
412
492
  resolveNamespace(pathParts) {
413
- if (typeof pathParts === 'string') {
414
- pathParts = pathParts.split('.')
415
- }
493
+ if (typeof pathParts === 'string') pathParts = pathParts.split('.')
494
+ const fq = pathParts.join('.')
495
+ if (this.#getCachedNamespace(fq)) return this.#getCachedNamespace(fq)
416
496
  let result
417
497
  while (result === undefined) {
418
498
  const path = pathParts.join('.')
@@ -425,6 +505,7 @@ class Resolver {
425
505
  pathParts = pathParts.slice(0, -1)
426
506
  }
427
507
  }
508
+ this.#cacheNamespace(fq, result)
428
509
  return result
429
510
  }
430
511
 
@@ -457,40 +538,38 @@ class Resolver {
457
538
  // later on with an inline declaration
458
539
  result.type = '{}'
459
540
  result.isInlineDeclaration = true
460
- } else {
461
- if (!isReferenceType(element) && isInlineEnumType(element, this.csn)) {
462
- // element.parent is only set if the enum is attached to an entity's property.
463
- // If it is missing then we are dealing with an inline parameter type of an action.
464
- // Edge case: element.parent is set, but no .name property is attached. This happens
465
- // for inline enums inside types:
466
- // ```cds
467
- // type T {
468
- // x : String enum { ... }; // no element.name for x
469
- // }
470
- // ```
471
- // In that case, we currently resolve to the more general type (cds.String, here)
472
- if (element.parent?.name) {
473
- result.isInlineDeclaration = true
474
- // we use the singular as the initial declaration of these enums takes place
475
- // while defining the singular class. Which therefore uses the singular over the plural name.
476
- const cleanEntityName = util.singular4(element.parent, true)
477
- const enumName = propertyToInlineEnumName(cleanEntityName, element.name)
478
- result.type = enumName
479
- result.plainName = enumName
480
- } else {
481
- // FIXME: this is the case where users have arrays of enums as action parameter type.
482
- // Instead of building the proper type (e.g. `'A' | 'B' | ...`, we are instead building
483
- // the encasing type (e.g. `string` here)
484
- // We should instead aim for a proper type, i.e.
485
- // this.#resolveInlineDeclarationType(element.enum, result, file)
486
- // or
487
- // stringifyEnumType(csnToEnumPairs(element))
488
- this.#resolveTypeName(element.type, result)
489
- }
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
490
560
  } else {
491
- this.resolvePotentialReferenceType(element.type, result, file)
492
- }
493
- }
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
+ }
494
573
 
495
574
  // objects and arrays
496
575
  if (element?.items) {
@@ -506,9 +585,7 @@ class Resolver {
506
585
  }
507
586
 
508
587
  if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
509
- this.logger.warning(
510
- `Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`
511
- )
588
+ this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
512
589
  }
513
590
  return result
514
591
  }
@@ -585,7 +662,7 @@ class Resolver {
585
662
  result.plainName = 'this'
586
663
  } else {
587
664
  // type offered by some library
588
- const lib = this.libraries.find((lib) => lib.offers(t))
665
+ const lib = this.libraries.find(lib => lib.offers(t))
589
666
  if (lib) {
590
667
  // only use the last name of the (fully qualified) type name in this case.
591
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,