@cap-js/cds-typer 0.34.0 → 0.36.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.
@@ -0,0 +1,95 @@
1
+ import cds from '@sap/cds'
2
+ import { type } from '@sap/cds'
3
+
4
+ export type ElementsOf<T> = {[name in keyof Required<T>]: type }
5
+
6
+ export namespace Association {
7
+ export type to <T> = T;
8
+ export namespace to {
9
+ export type many <T extends readonly any[]> = T;
10
+ }
11
+ }
12
+
13
+ export namespace Composition {
14
+ export type of <T> = T;
15
+ export namespace of {
16
+ export type many <T extends readonly any[]> = T;
17
+ }
18
+ }
19
+
20
+ export class Entity {
21
+ static data<T extends Entity> (this:T, _input:Object) : T {
22
+ return {} as T // mock
23
+ }
24
+ }
25
+
26
+ export type EntitySet<T> = T[] & {
27
+ data (input:object[]) : T[]
28
+ data (input:object) : T
29
+ };
30
+
31
+ export type DraftEntity<T> = T & {
32
+ IsActiveEntity?: boolean | null
33
+ HasActiveEntity?: boolean | null
34
+ HasDraftEntity?: boolean | null
35
+ DraftAdministrativeData_DraftUUID?: string | null
36
+ }
37
+
38
+ export type DraftOf<T> = { new(...args: any[]): DraftEntity<T> }
39
+ export type DraftsOf<T> = typeof Array<DraftEntity<T>>
40
+
41
+ export type DeepRequired<T> = {
42
+ [K in keyof T]: DeepRequired<Unkey<T[K]>>
43
+ } & Exclude<Required<T>, null>;
44
+
45
+ const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
46
+ export type Key<T> = T & {[key]?: true}
47
+
48
+ export type KeysOf<T> = {
49
+ [K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
50
+ }
51
+
52
+ export type Unkey<T> = T extends Key<infer U> ? U : T
53
+
54
+ /**
55
+ * Dates and timestamps are strings during runtime, so cds-typer represents them as such.
56
+ */
57
+ export type CdsDate = `${number}${number}${number}${number}-${number}${number}-${number}${number}`;
58
+ /**
59
+ * @see {@link CdsDate}
60
+ */
61
+ export type CdsDateTime = string;
62
+ /**
63
+ * @see {@link CdsDate}
64
+ */
65
+ export type CdsTime = `${number}${number}:${number}${number}:${number}${number}`;
66
+ /**
67
+ * @see {@link CdsDate}
68
+ */
69
+ export type CdsTimestamp = string;
70
+
71
+ export type CdsMap = { [key: string]: unknown };
72
+
73
+
74
+ export const createEntityProxy = function (fqParts: any, opts = {}) {
75
+ const { target, customProps } = { target: {}, customProps: [], ...opts }
76
+ const fq = fqParts.filter((p: any) => !!p).join('.')
77
+ return new Proxy(target, {
78
+ get: function (target:any, prop:any) {
79
+ if (cds.entities) {
80
+ target.__proto__ = cds.entities(fqParts[0])[fqParts[1]]
81
+ // overwrite/simplify getter after cds.entities is accessible
82
+ this.get = (target, prop) => target[prop]
83
+ return target[prop]
84
+ }
85
+ // we already know the name so we skip the cds.entities proxy access
86
+ if (prop === 'name') return fq
87
+ // custom properties access on 'target' as well as cached _entity property access goes here
88
+ if (Object.hasOwn(target, prop)) return target[prop]
89
+ // inline enums have to be caught here for first time access, as they do not exist on the entity
90
+ if (customProps.includes(prop as never)) return target[prop]
91
+ // last but not least we pass the property access to cds.entities
92
+ throw new Error(`Property ${prop} does not exist on entity '${fq}' or cds.entities is not yet defined. Ensure the CDS runtime is fully booted before accessing properties.`)
93
+ }
94
+ })
95
+ }
@@ -1,88 +1,31 @@
1
1
  const { SourceFile } = require('../file')
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
2
4
 
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}`'
5
+ /** @type {string} */
6
+ let tsBoilerplate
7
+ /** @type {SourceFile} */
8
+ let baseDefinitions
7
9
 
8
10
  /**
9
- * Base definitions used throughout the typing process,
10
- * such as Associations and Compositions.
11
- * @type {SourceFile}
11
+ * @returns {string}
12
12
  */
13
- const baseDefinitions = new SourceFile('_')
14
- // FIXME: this should be a library someday
15
- baseDefinitions.addPreamble(`
16
- import { type } from '@sap/cds'
17
-
18
- export type ElementsOf<T> = {[name in keyof Required<T>]: type }
19
-
20
- export namespace Association {
21
- export type to <T> = T;
22
- export namespace to {
23
- export type many <T extends readonly any[]> = T;
13
+ function getTsBoilerplate () {
14
+ if (!tsBoilerplate) {
15
+ tsBoilerplate = fs.readFileSync(path.join(__filename, '..', '..', 'boilerplate/tsBoilerplate.ts'), 'utf8')
24
16
  }
17
+ return tsBoilerplate
25
18
  }
26
19
 
27
- export namespace Composition {
28
- export type of <T> = T;
29
- export namespace of {
30
- export type many <T extends readonly any[]> = T;
31
- }
32
- }
33
-
34
- export class Entity {
35
- static data<T extends Entity> (this:T, _input:Object) : T {
36
- return {} as T // mock
37
- }
38
- }
39
-
40
- export type EntitySet<T> = T[] & {
41
- data (input:object[]) : T[]
42
- data (input:object) : T
43
- };
44
-
45
- export type DraftEntity<T> = T & {
46
- IsActiveEntity?: boolean | null
47
- HasActiveEntity?: boolean | null
48
- HasDraftEntity?: boolean | null
49
- DraftAdministrativeData_DraftUUID?: string | null
50
- }
51
-
52
- export type DraftOf<T> = { new(...args: any[]): DraftEntity<T> }
53
- export type DraftsOf<T> = typeof Array<DraftEntity<T>>
54
-
55
- export type DeepRequired<T> = {
56
- [K in keyof T]: DeepRequired<Unkey<T[K]>>
57
- } & Exclude<Required<T>, null>;
58
-
59
- const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
60
- export type Key<T> = T & {[key]?: true}
61
-
62
- export type KeysOf<T> = {
63
- [K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
64
- }
65
-
66
- export type Unkey<T> = T extends Key<infer U> ? U : T
67
-
68
20
  /**
69
- * Dates and timestamps are strings during runtime, so cds-typer represents them as such.
21
+ * @returns {SourceFile}
70
22
  */
71
- export type CdsDate = ${dateRegex};
72
- /**
73
- * @see {@link CdsDate}
74
- */
75
- export type CdsDateTime = string;
76
- /**
77
- * @see {@link CdsDate}
78
- */
79
- export type CdsTime = ${timeRegex};
80
- /**
81
- * @see {@link CdsDate}
82
- */
83
- export type CdsTimestamp = string;
84
-
85
- export type CdsMap = { [key: string]: unknown };
86
- `)
23
+ function getBaseDefinitions () {
24
+ if (!baseDefinitions) {
25
+ baseDefinitions = new SourceFile('_')
26
+ baseDefinitions.addPreamble(getTsBoilerplate())
27
+ }
28
+ return baseDefinitions
29
+ }
87
30
 
88
- module.exports = { baseDefinitions }
31
+ module.exports = { getBaseDefinitions }
@@ -124,7 +124,6 @@ const isInlineEnumType = (element, csn) => element.enum
124
124
  * @param {[string, string][]} kvs - a list of key-value pairs. Values that are falsey are replaced by
125
125
  * @param {import('../printers/javascript').Printer} jsp - the printer to use
126
126
  */
127
- // ??= for inline enums. If there is some static property of that name, we don't want to override it (for example: ".actions"
128
127
  const stringifyEnumImplementation = (name, kvs, jsp) => jsp.printExport(
129
128
  name,
130
129
  `{ ${kvs.map(([k,v]) => `${normalise(k)}: ${v}`).join(', ')} }`,
package/lib/file.js CHANGED
@@ -471,7 +471,7 @@ class SourceFile extends File {
471
471
  const boilerplate = [AUTO_GEN_NOTE]
472
472
  if (configuration.useEntitiesProxy) {
473
473
  if (namespace === '_') {
474
- boilerplate.push(jsp.printImport('cds', '@sap/cds'), this.#getEntityProxyFunctionExport())
474
+ boilerplate.push(jsp.printDefaultImport('cds', '@sap/cds'), this.#getEntityProxyFunctionExport())
475
475
  } else {
476
476
  boilerplate.push(
477
477
  jsp.printDeconstructedImport(
@@ -33,6 +33,17 @@ class JavaScriptPrinter {
33
33
  throw Error('not implemented')
34
34
  }
35
35
 
36
+ /**
37
+ * @abstract
38
+ * @param {string} alias - what the import should be known as within the importing file
39
+ * @param {string} from - the package/ location to import from
40
+ * @returns {string}
41
+ */
42
+ // eslint-disable-next-line no-unused-vars
43
+ printDefaultImport (alias, from) {
44
+ throw Error('not implemented')
45
+ }
46
+
36
47
  /**
37
48
  * @abstract
38
49
  * @param {string[]} imports - the deconstructed elements
@@ -83,6 +94,11 @@ class ESMPrinter extends JavaScriptPrinter {
83
94
  return `import * as ${alias} from '${from}'`
84
95
  }
85
96
 
97
+ /** @type {JavaScriptPrinter['printDefaultImport']} */
98
+ printDefaultImport (alias, from) {
99
+ return `import ${alias} from '${from}'`
100
+ }
101
+
86
102
  /** @type {JavaScriptPrinter['printDeconstructedImport']} */
87
103
  printDeconstructedImport (imports, from) {
88
104
  return `import { ${imports.join(', ')} } from '${from}/index.js'`
@@ -115,6 +131,11 @@ class CJSPrinter extends JavaScriptPrinter {
115
131
  return `const ${alias} = require('${from}')`
116
132
  }
117
133
 
134
+ /** @type {JavaScriptPrinter['printDefaultImport']} */
135
+ printDefaultImport (alias, from) {
136
+ return `const ${alias} = require('${from}')`
137
+ }
138
+
118
139
  /** @type {JavaScriptPrinter['printDeconstructedImport']} */
119
140
  printDeconstructedImport (imports, from) {
120
141
  return `const { ${imports.join(', ')} } = require('${from}')`
@@ -8,13 +8,15 @@ const { StructuredInlineDeclarationResolver } = require('../components/inline')
8
8
  const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
9
9
  const { isReferenceType } = require('../components/reference')
10
10
  const { isEntity, getMaxCardinality } = require('../csn')
11
- const { baseDefinitions } = require('../components/basedefs')
11
+ const { getBaseDefinitions } = require('../components/basedefs')
12
12
  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
16
  const { configuration } = require('../config')
17
17
 
18
+ const baseDefinitions = getBaseDefinitions()
19
+
18
20
  /** @typedef {import('../visitor').Visitor} Visitor */
19
21
  /** @typedef {import('../typedefs').resolver.CSN} CSN */
20
22
  /** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
@@ -68,6 +70,14 @@ class Resolver {
68
70
  return type.items ? !type.items.notNull : !type.notNull
69
71
  }
70
72
 
73
+ /**
74
+ * @param {EntityCSN} type - a CSN type
75
+ * @returns {boolean} whether the type has the @mandatory annotation
76
+ */
77
+ isMandatory(type) {
78
+ return type['@mandatory'] === true
79
+ }
80
+
71
81
  /**
72
82
  * Returns all libraries that have been referenced at least once.
73
83
  * @returns {Library[]}
@@ -302,11 +312,24 @@ class Resolver {
302
312
  }[element.constructor.name] ?? []
303
313
 
304
314
  if (toOne && toMany) {
305
- /** @type { EntityCSN | { type: string } } */
306
- // @ts-expect-error - nope, it is not undefined
307
- const target = element.items ?? (typeof element.target === 'string'
308
- ? { type: element.target }
309
- : element.target)
315
+ /**
316
+ * Resolve a property from a CSN entity. If it is a reference, leave it as is.
317
+ * If it is a string, return an object with type set to the string.
318
+ * @param {Record<string, any>} el - the element to check
319
+ * @param {string} property - the property to check
320
+ * @returns {import('../typedefs').resolver.EntityCSN | { type: string }}
321
+ */
322
+ const getTarget = (el, property) => typeof el[property] === 'string'
323
+ ? { type: el[property] }
324
+ : el[property]
325
+
326
+ /** @type { EntityCSN | { type: string } | undefined } */
327
+ const target = element.items
328
+ ?? getTarget(element, 'target')
329
+ ?? getTarget(element, 'targetAspect') // Composition of aspects
330
+ if (!target) {
331
+ throw new Error(`Could not resolve target of ${element}`)
332
+ }
310
333
  /** set `notNull = true` to avoid repeated `| not null` TS construction */
311
334
  // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
312
335
  target.notNull = true
@@ -475,6 +498,11 @@ class Resolver {
475
498
  : element?.key || element?.notNull || cardinality > 1,
476
499
  }
477
500
 
501
+ // parameters with @mandatory are always not null
502
+ // as of today, it is not clear if this also applies to other fields annotated with @mandatory.
503
+ result.isNotNull ||= element.kind === 'param' && this.isMandatory(element)
504
+
505
+
478
506
  if (element?.type === undefined) {
479
507
  // "fallback" type "empty object". May be overriden via #resolveInlineDeclarationType
480
508
  // later on with an inline declaration
@@ -521,9 +549,10 @@ class Resolver {
521
549
  result.isBuiltin = true
522
550
  this.resolveType(element.items, file)
523
551
  //delete element.items
524
- } else if (element?.elements && (options?.forceInlineStructs || !element?.type)) {
552
+ } else if (!result.isBuiltin && element?.elements && (options?.forceInlineStructs || !element?.type)) {
525
553
  // explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
526
554
  // if the resolver option `forceInlineStructs` is `true`, named types in elements will be converted to inline
555
+ // Skipping isBuiltin will skip cds.Map, which has elements
527
556
  this.#resolveInlineDeclarationType(element.elements, result, file, options)
528
557
  }
529
558
 
package/lib/typedefs.d.ts CHANGED
@@ -27,6 +27,7 @@ export module resolver {
27
27
  name: string,
28
28
  '@singular'?: string,
29
29
  '@plural'?: string,
30
+ '@mandatory'?: boolean,
30
31
  '@odata.draft.enabled'?: boolean // custom!
31
32
  _unresolved?: boolean
32
33
  isRefNotNull?: boolean // custom!
package/lib/visitor.js CHANGED
@@ -10,7 +10,7 @@ const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf,
10
10
  const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
11
11
  const { isReferenceType } = require('./components/reference')
12
12
  const { empty } = require('./components/typescript')
13
- const { baseDefinitions } = require('./components/basedefs')
13
+ const { getBaseDefinitions } = require('./components/basedefs')
14
14
  const { EntityRepository, asIdentifier } = require('./resolution/entity')
15
15
  const { last } = require('./components/identifier')
16
16
  const { getPropertyModifiers } = require('./components/property')
@@ -18,6 +18,9 @@ const { configuration } = require('./config')
18
18
  const { createMember } = require('./components/class')
19
19
  const { overrideNameProperty } = require('./printers/javascript')
20
20
 
21
+ const baseDefinitions = getBaseDefinitions()
22
+ const MAX_TRANSITIVE_RESOLUTION_STEPS = 10
23
+
21
24
  /** @typedef {import('./file').File} File */
22
25
  /** @typedef {import('./typedefs').visitor.Context} Context */
23
26
  /** @typedef {import('./typedefs').visitor.Inflection} Inflection */
@@ -274,6 +277,31 @@ class Visitor {
274
277
  }))
275
278
  if (typeof e?.type !== 'string' && e?.type?.ref) {
276
279
  e.resolvedType = /** @type {string} */(lookUpRefType(this.csn, e.type.ref)?.type)
280
+ try {
281
+ /**
282
+ * multi-level resolution does not contain a .ref property:
283
+ * ```cds
284
+ * entity A {
285
+ * x: enum ...
286
+ * }
287
+ * entity B {
288
+ * x: A:x
289
+ * }
290
+ * entity C {
291
+ * x: B:x
292
+ * }
293
+ * ```
294
+ * results in B.x having a ref to [A,x], but C.x only has a string 'A.x' as type.
295
+ * So we have to do yet another round of resolution on this string.
296
+ * We attempt to follow this chain for MAX_TRANSITIVE_RESOLUTION_STEPS tops,
297
+ * but we could finish earlier, when the type is a primitive ("string"), which
298
+ * then jumps to the catch {} to leave e.resolvedType at the last, resolvable type.
299
+ */
300
+ for (let i = 0; i < MAX_TRANSITIVE_RESOLUTION_STEPS; i++) {
301
+ const { csn } = this.resolver.resolveTypeName(/** @type {string} */(e.resolvedType))
302
+ e.resolvedType = csn.type
303
+ }
304
+ } catch { /* ignore */ }
277
305
  }
278
306
  file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
279
307
  }
@@ -403,7 +431,9 @@ class Visitor {
403
431
  .filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
404
432
  .map(([name, type]) => ({
405
433
  name,
406
- modifier: this.resolver.isOptional(type) ? '?' : '',
434
+ modifier: this.resolver.isOptional(type) && !this.resolver.isMandatory(type)
435
+ ? '?'
436
+ : '',
407
437
  type: this.#stringifyFunctionParamType(type, file),
408
438
  doc: docify(type.doc).join('\n'),
409
439
  }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Generates .ts files for a CDS model to receive code completion in VS Code",
5
5
  "main": "index.js",
6
6
  "repository": "github:cap-js/cds-typer",
@@ -48,7 +48,7 @@
48
48
  "@stylistic/eslint-plugin-js": "^4.2.0",
49
49
  "acorn": "^8.10.0",
50
50
  "eslint": "^9",
51
- "eslint-plugin-jsdoc": "^50.2.2",
51
+ "eslint-plugin-jsdoc": "^51.2.1",
52
52
  "typescript": ">=4.6.4"
53
53
  },
54
54
  "cds": {