@cap-js/cds-typer 0.35.0 → 0.37.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.
@@ -72,7 +72,7 @@ export type CdsMap = { [key: string]: unknown };
72
72
 
73
73
 
74
74
  export const createEntityProxy = function (fqParts: any, opts = {}) {
75
- const { target, customProps } = { target: {}, customProps: [], ...opts }
75
+ const { target, customProps } = { target: {}, customProps: [] as any[], ...opts }
76
76
  const fq = fqParts.filter((p: any) => !!p).join('.')
77
77
  return new Proxy(target, {
78
78
  get: function (target:any, prop:any) {
@@ -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(', ')} }`,
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * Determines the proper modifiers for a property.
3
- * For most properties, this will be "declare". But for some properties,
4
- * like fields of a type, we don't want any modifiers.
3
+ *
4
+ * All properties must be modified by `declare`.
5
+ * The only exception to this is in case of an action return type being defined
6
+ * as an object. Here `declare` would cause an error.
7
+ * This case can be identified by checking the parent of the property:
8
+ * If the parent is a type and has no name, then it is an action return type.
5
9
  * @param {import('../typedefs').resolver.EntityCSN} element - The element to determine the modifiers for.
6
10
  * @returns {import('../typedefs').resolver.PropertyModifier[]} The modifiers for the property.
7
11
  */
8
- const getPropertyModifiers = element => element?.parent?.kind !== 'type' ? ['declare'] : []
12
+ const getPropertyModifiers = element => element?.parent?.kind === 'type' && element?.parent?.name === undefined ? [] : ['declare']
9
13
 
10
14
  module.exports = {
11
15
  getPropertyModifiers
package/lib/csn.js CHANGED
@@ -65,6 +65,11 @@ const isEntity = entity => entity?.kind === 'entity'
65
65
  */
66
66
  const isEnum = entity => Boolean(entity && Object.hasOwn(entity, 'enum'))
67
67
 
68
+ /**
69
+ * @param {EntityCSN & { '@cds.external'?: boolean } | undefined} entity - the entity
70
+ */
71
+ const isExternal = entity => entity?.['@cds.external'] === true
72
+
68
73
  /**
69
74
  * Attempts to retrieve the max cardinality of a CSN for an entity.
70
75
  * @param {EntityCSN} element - csn of entity to retrieve cardinality for
@@ -365,6 +370,7 @@ module.exports = {
365
370
  isEnum,
366
371
  isUnresolved,
367
372
  isType,
373
+ isExternal,
368
374
  getMaxCardinality,
369
375
  getProjectionTarget,
370
376
  getProjectionAliases,
package/lib/file.js CHANGED
@@ -162,6 +162,7 @@ class SourceFile extends File {
162
162
  * @param {string} options.name - name of the lambda
163
163
  * @param {import('./typedefs').visitor.ParamInfo[]} [options.parameters] - list of parameters, passed as [name, modifier, type, doc] pairs
164
164
  * @param {string} [options.returns] - the return type of the function
165
+ * @param {string} [options.self] - what is set as "__self", which is used for bound actions
165
166
  * @param {'action' | 'function'} options.kind - kind of the lambda
166
167
  * @param {string} [options.initialiser] - the initialiser expression
167
168
  * @param {boolean} [options.isStatic] - whether the lambda is static
@@ -187,7 +188,7 @@ class SourceFile extends File {
187
188
  * stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
188
189
  * ```
189
190
  */
190
- static stringifyLambda({name, parameters=[], returns='any', kind, initialiser, isStatic=false, callStyles={positional:true, named:true}, doc}) {
191
+ static stringifyLambda({name, parameters=[], returns='any', kind, initialiser, self='null', isStatic=false, callStyles={positional:true, named:true}, doc}) {
191
192
  let docStr = doc?.length ? doc.join('\n')+'\n' : ''
192
193
  const parameterTypes = parameters.map(({name, modifier, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}${modifier}: ${type}`).join(', ')
193
194
  const parameterTypeAsObject = parameterTypes.length
@@ -212,7 +213,7 @@ class SourceFile extends File {
212
213
 
213
214
  return [
214
215
  `${prefix} {`,
215
- [...callableSignatures, '// metadata (do not use)', `__parameters: ${parameterTypeAsObject}, __returns: ${returns}`, ...kindDef],
216
+ [...callableSignatures, '// metadata (do not use)', `__parameters: ${parameterTypeAsObject}, __returns: ${returns}, __self: ${self}`, ...kindDef],
216
217
  `}${suffix}`,
217
218
  ]
218
219
  }
@@ -7,7 +7,7 @@ const { deepRequire, createToManyAssociation, createToOneAssociation, createArra
7
7
  const { StructuredInlineDeclarationResolver } = require('../components/inline')
8
8
  const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
9
9
  const { isReferenceType } = require('../components/reference')
10
- const { isEntity, getMaxCardinality } = require('../csn')
10
+ const { isEntity, getMaxCardinality, isExternal } = require('../csn')
11
11
  const { getBaseDefinitions } = require('../components/basedefs')
12
12
  const { BuiltinResolver } = require('./builtin')
13
13
  const { LOG } = require('../logging')
@@ -312,11 +312,24 @@ class Resolver {
312
312
  }[element.constructor.name] ?? []
313
313
 
314
314
  if (toOne && toMany) {
315
- /** @type { EntityCSN | { type: string } } */
316
- // @ts-expect-error - nope, it is not undefined
317
- const target = element.items ?? (typeof element.target === 'string'
318
- ? { type: element.target }
319
- : 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
+ }
320
333
  /** set `notNull = true` to avoid repeated `| not null` TS construction */
321
334
  // @ts-expect-error - yes, we know that notNull is not part of the type in some cases
322
335
  target.notNull = true
@@ -536,10 +549,11 @@ class Resolver {
536
549
  result.isBuiltin = true
537
550
  this.resolveType(element.items, file)
538
551
  //delete element.items
539
- } else if (!result.isBuiltin && element?.elements && (options?.forceInlineStructs || !element?.type)) {
552
+ } else if (!result.isBuiltin && !isExternal(element) && element?.elements && (options?.forceInlineStructs || !element?.type)) {
540
553
  // explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
541
554
  // if the resolver option `forceInlineStructs` is `true`, named types in elements will be converted to inline
542
555
  // Skipping isBuiltin will skip cds.Map, which has elements
556
+ // Skipping isExternal will skip @cds.external entities, which have elements as well
543
557
  this.#resolveInlineDeclarationType(element.elements, result, file, options)
544
558
  }
545
559
 
package/lib/visitor.js CHANGED
@@ -19,6 +19,7 @@ const { createMember } = require('./components/class')
19
19
  const { overrideNameProperty } = require('./printers/javascript')
20
20
 
21
21
  const baseDefinitions = getBaseDefinitions()
22
+ const MAX_TRANSITIVE_RESOLUTION_STEPS = 10
22
23
 
23
24
  /** @typedef {import('./file').File} File */
24
25
  /** @typedef {import('./typedefs').visitor.Context} Context */
@@ -94,11 +95,12 @@ class Visitor {
94
95
 
95
96
  /**
96
97
  * @param {EntityCSN} entity - the entity to print the actions for
98
+ * @param {string} clean - the clean entity name
97
99
  * @param {Buffer} buffer - the buffer to write the actions into
98
100
  * @param {import('./typedefs').resolver.EntityInfo[]} ancestors - the fully qualified names of the ancestors of the entity
99
101
  * @param {SourceFile} file - the file the entity is being printed into
100
102
  */
101
- #printStaticActions(entity, buffer, ancestors, file) {
103
+ #printStaticActions(entity, clean, buffer, ancestors, file) {
102
104
  // TODO: refactor away! All these printing functionalities need to go
103
105
  const actions = Object.entries(entity.actions ?? {})
104
106
  const inherited = ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`)
@@ -109,6 +111,7 @@ class Visitor {
109
111
  () => {
110
112
  for (const [aname, action] of actions) {
111
113
  const [opener, content, closer] = SourceFile.stringifyLambda({
114
+ self: clean,
112
115
  name: aname,
113
116
  parameters: this.#stringifyFunctionParams(action.params, file),
114
117
  returns: action.returns
@@ -276,6 +279,31 @@ class Visitor {
276
279
  }))
277
280
  if (typeof e?.type !== 'string' && e?.type?.ref) {
278
281
  e.resolvedType = /** @type {string} */(lookUpRefType(this.csn, e.type.ref)?.type)
282
+ try {
283
+ /**
284
+ * multi-level resolution does not contain a .ref property:
285
+ * ```cds
286
+ * entity A {
287
+ * x: enum ...
288
+ * }
289
+ * entity B {
290
+ * x: A:x
291
+ * }
292
+ * entity C {
293
+ * x: B:x
294
+ * }
295
+ * ```
296
+ * results in B.x having a ref to [A,x], but C.x only has a string 'A.x' as type.
297
+ * So we have to do yet another round of resolution on this string.
298
+ * We attempt to follow this chain for MAX_TRANSITIVE_RESOLUTION_STEPS tops,
299
+ * but we could finish earlier, when the type is a primitive ("string"), which
300
+ * then jumps to the catch {} to leave e.resolvedType at the last, resolvable type.
301
+ */
302
+ for (let i = 0; i < MAX_TRANSITIVE_RESOLUTION_STEPS; i++) {
303
+ const { csn } = this.resolver.resolveTypeName(/** @type {string} */(e.resolvedType))
304
+ e.resolvedType = csn.type
305
+ }
306
+ } catch { /* ignore */ }
279
307
  }
280
308
  file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
281
309
  }
@@ -293,7 +321,7 @@ class Visitor {
293
321
  }
294
322
  this.#printStaticKeys(buffer, clean, ancestorInfos, file)
295
323
  this.#printStaticElements(buffer, clean)
296
- this.#printStaticActions(entity, buffer, ancestorInfos, file)
324
+ this.#printStaticActions(entity, clean, buffer, ancestorInfos, file)
297
325
  }, '};') // end of generated class
298
326
  }, '}') // end of aspect
299
327
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.35.0",
3
+ "version": "0.37.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": {