@cap-js/cds-typer 0.24.0 → 0.26.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 +28 -2
- package/README.md +19 -0
- package/lib/cli.js +30 -14
- package/lib/compile.js +3 -3
- package/lib/components/enum.js +19 -10
- package/lib/components/identifier.js +1 -1
- package/lib/components/inline.js +81 -26
- package/lib/components/javascript.js +28 -0
- package/lib/components/property.js +12 -0
- package/lib/components/wrappers.js +27 -4
- package/lib/csn.js +90 -26
- package/lib/file.js +166 -44
- package/lib/logging.js +5 -1
- package/lib/resolution/builtin.js +3 -2
- package/lib/resolution/entity.js +46 -7
- package/lib/resolution/resolver.js +60 -20
- package/lib/typedefs.d.ts +83 -13
- package/lib/util.js +4 -3
- package/lib/visitor.js +199 -79
- package/package.json +10 -6
package/lib/visitor.js
CHANGED
|
@@ -2,29 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
-
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality } = require('./csn')
|
|
5
|
+
const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum } = require('./csn')
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
|
-
const { SourceFile, FileRepository, Buffer } = require('./file')
|
|
7
|
+
const { SourceFile, FileRepository, Buffer, Path } = require('./file')
|
|
8
8
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
9
9
|
const { Resolver } = require('./resolution/resolver')
|
|
10
10
|
const { LOG } = require('./logging')
|
|
11
|
-
const { docify } = require('./components/wrappers')
|
|
11
|
+
const { docify, createPromiseOf, createUnionOf } = require('./components/wrappers')
|
|
12
12
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
13
13
|
const { isReferenceType } = require('./components/reference')
|
|
14
14
|
const { empty } = require('./components/typescript')
|
|
15
15
|
const { baseDefinitions } = require('./components/basedefs')
|
|
16
|
-
const { EntityRepository } = require('./resolution/entity')
|
|
16
|
+
const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
17
17
|
const { last } = require('./components/identifier')
|
|
18
|
+
const { getPropertyModifiers } = require('./components/property')
|
|
18
19
|
|
|
19
20
|
/** @typedef {import('./file').File} File */
|
|
20
21
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
21
22
|
/** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
|
|
22
23
|
/** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
|
|
23
24
|
/** @typedef {import('./typedefs').visitor.Inflection} Inflection */
|
|
25
|
+
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
26
|
+
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
27
|
+
/** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
|
|
24
28
|
|
|
25
29
|
const defaults = {
|
|
26
30
|
// FIXME: add defaults for remaining parameters
|
|
27
31
|
propertiesOptional: true,
|
|
32
|
+
useEntitiesProxy: false,
|
|
28
33
|
inlineDeclarations: 'flat'
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -36,12 +41,12 @@ class Visitor {
|
|
|
36
41
|
* @returns {File[]} a full list of files to be written
|
|
37
42
|
*/
|
|
38
43
|
getWriteoutFiles() {
|
|
39
|
-
return this.fileRepository.getFiles()
|
|
44
|
+
return [...this.fileRepository.getFiles(), ...this.resolver.getUsedLibraries()]
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
44
|
-
* @param {VisitorOptions} options - the options
|
|
49
|
+
* @param {VisitorOptions | {}} options - the options
|
|
45
50
|
*/
|
|
46
51
|
constructor(csn, options = {}) {
|
|
47
52
|
amendCSN(csn.xtended)
|
|
@@ -59,7 +64,9 @@ class Visitor {
|
|
|
59
64
|
this.entityRepository = new EntityRepository(this.resolver)
|
|
60
65
|
|
|
61
66
|
/** @type {FileRepository} */
|
|
62
|
-
this.fileRepository = new FileRepository()
|
|
67
|
+
this.fileRepository = new FileRepository(this.options)
|
|
68
|
+
// REVISIT: better way to pass options to base source file ???
|
|
69
|
+
baseDefinitions.options = this.options
|
|
63
70
|
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
64
71
|
this.inlineDeclarationResolver =
|
|
65
72
|
this.options.inlineDeclarations === 'structured'
|
|
@@ -124,6 +131,35 @@ class Visitor {
|
|
|
124
131
|
})
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
/**
|
|
135
|
+
* @param {EntityCSN} entity - the entity to print the actions for
|
|
136
|
+
* @param {Buffer} buffer - the buffer to write the actions into
|
|
137
|
+
* @param {import('./typedefs').resolver.EntityInfo[]} ancestors - the fully qualified names of the ancestors of the entity
|
|
138
|
+
* @param {SourceFile} file - the file the entity is being printed into
|
|
139
|
+
*/
|
|
140
|
+
#printStaticActions(entity, buffer, ancestors, file) {
|
|
141
|
+
// TODO: refactor away! All these printing functionalities need to go
|
|
142
|
+
const actions = Object.entries(entity.actions ?? {})
|
|
143
|
+
const inherited = ancestors.length
|
|
144
|
+
? ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`).join(' & ') + ' & '
|
|
145
|
+
: ''
|
|
146
|
+
if (actions.length) {
|
|
147
|
+
buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
|
|
148
|
+
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
149
|
+
name: aname,
|
|
150
|
+
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
151
|
+
returns: action.returns
|
|
152
|
+
? this.resolver.resolveAndRequire(action.returns, file).typeName
|
|
153
|
+
: 'any',
|
|
154
|
+
kind: action.kind,
|
|
155
|
+
doc: docify(action.doc)
|
|
156
|
+
})), '}'
|
|
157
|
+
) // end of actions
|
|
158
|
+
} else {
|
|
159
|
+
buffer.add(`declare static readonly actions: ${inherited}${empty}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
127
163
|
/**
|
|
128
164
|
* Transforms an entity or CDS aspect into a JS aspect (aka mixin).
|
|
129
165
|
* That is, for an element A we get:
|
|
@@ -131,42 +167,65 @@ class Visitor {
|
|
|
131
167
|
* - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
|
|
132
168
|
* - the type A to use for external typing and is derived from AXtended.
|
|
133
169
|
* @param {string} fq - the name of the entity
|
|
134
|
-
* @param {
|
|
170
|
+
* @param {EntityCSN} entity - the pointer into the CSN to extract the elements from
|
|
135
171
|
* @param {Buffer} buffer - the buffer to write the resulting definitions into
|
|
136
172
|
* @param {{cleanName?: string}} options - additional options
|
|
137
173
|
*/
|
|
138
174
|
#aspectify(fq, entity, buffer, options = {}) {
|
|
139
|
-
const info = this.entityRepository.
|
|
175
|
+
const info = this.entityRepository.getByFqOrThrow(fq)
|
|
140
176
|
const clean = options?.cleanName ?? info.withoutNamespace
|
|
141
177
|
const { namespace } = info
|
|
142
178
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
143
|
-
const identSingular = name => name
|
|
179
|
+
const identSingular = (/** @type {string} */name) => name // FIXME: remove
|
|
180
|
+
//const identAspect = name => `_${name}Aspect`
|
|
181
|
+
/** @param {string} name - the name */
|
|
144
182
|
const identAspect = name => `_${name}Aspect`
|
|
145
|
-
|
|
183
|
+
/**
|
|
184
|
+
* @param {object} options - options
|
|
185
|
+
* @param {Path} [options.ns] - namespace
|
|
186
|
+
* @param {string} options.clean - the clean name of the entity
|
|
187
|
+
* @param {string} options.fq - fully qualified name
|
|
188
|
+
* @returns {string} the local identifier
|
|
189
|
+
*/
|
|
190
|
+
// FIXME: replace with resolution/entity::asIdentifier
|
|
191
|
+
const toLocalIdent = ({ns, clean, fq}) => {
|
|
146
192
|
// types are not inflected, so don't change those to singular
|
|
147
|
-
const
|
|
148
|
-
const ident =
|
|
149
|
-
?
|
|
150
|
-
: this.resolver.inflect({csn
|
|
151
|
-
)
|
|
193
|
+
const csn = this.csn.inferred.definitions[fq]
|
|
194
|
+
const ident = isType(csn)
|
|
195
|
+
? clean
|
|
196
|
+
: this.resolver.inflect({csn, plainName: clean}).singular
|
|
152
197
|
return !ns || ns.isCwd(file.path.asDirectory())
|
|
153
|
-
?
|
|
154
|
-
: `${ns.asIdentifier()}.${ident}
|
|
198
|
+
? ident
|
|
199
|
+
: `${ns.asIdentifier()}.${ident}`
|
|
155
200
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
201
|
+
// remove the ancestry of projections/ views.
|
|
202
|
+
// They explicitly define their properties.
|
|
203
|
+
// But at the same time they also carry their .includes clause, which can
|
|
204
|
+
// clash when the user aliases a selected property with the name of
|
|
205
|
+
// a properties of the entities they project on:
|
|
206
|
+
// entity foo as SELECT bar.baz AS ID FROM bar
|
|
207
|
+
// will produce an error if bar also has a property ID which now clashes with foo.ID
|
|
208
|
+
// WARNING: annotations from entities without properties should actually be propagated this way!
|
|
209
|
+
// So once we start caring about annotations, we have to revisit this part.
|
|
210
|
+
/** @type {import('./typedefs').resolver.EntityInfo[]} */
|
|
211
|
+
const ancestorInfos = ((!isViewOrProjection(entity) ? entity.includes : []) ?? [])
|
|
212
|
+
.map(ancestor => {
|
|
213
|
+
const info = this.entityRepository.getByFq(ancestor)
|
|
214
|
+
if (!info) throw new Error(`could not resolve ancestor ${ancestor} for ${fq}`)
|
|
215
|
+
file.addImport(info.namespace)
|
|
216
|
+
return info
|
|
161
217
|
})
|
|
218
|
+
|
|
219
|
+
const ancestorsAspects = ancestorInfos
|
|
162
220
|
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
163
|
-
.reduce(
|
|
221
|
+
.reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
|
|
164
222
|
|
|
165
223
|
this.contexts.push({ entity: fq })
|
|
166
224
|
|
|
167
225
|
// CLASS ASPECT
|
|
168
226
|
buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
|
|
169
|
-
buffer.addIndentedBlock(`return class ${clean} extends ${
|
|
227
|
+
buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
|
|
228
|
+
/** @type {import('./typedefs').resolver.EnumCSN[]} */
|
|
170
229
|
const enums = []
|
|
171
230
|
for (let [ename, element] of Object.entries(entity.elements ?? {})) {
|
|
172
231
|
if (element.target && /\.texts?/.test(element.target)) {
|
|
@@ -176,14 +235,14 @@ class Visitor {
|
|
|
176
235
|
this.visitElement(ename, element, file, buffer)
|
|
177
236
|
|
|
178
237
|
// make foreign keys explicit
|
|
179
|
-
if (
|
|
238
|
+
if (element.target) {
|
|
180
239
|
// lookup in cds.definitions can fail for inline structs.
|
|
181
240
|
// We don't really have to care for this case, as keys from such structs are _not_ propagated to
|
|
182
241
|
// the containing entity.
|
|
183
242
|
for (const [kname, originalKeyElement] of this.#keys(element.target)) {
|
|
184
243
|
if (getMaxCardinality(element) === 1 && typeof element.on !== 'object') { // FIXME: kelement?
|
|
185
244
|
const foreignKey = `${ename}_${kname}`
|
|
186
|
-
if (Object.hasOwn(entity.elements, foreignKey)) {
|
|
245
|
+
if (entity.elements && Object.hasOwn(entity.elements, foreignKey)) {
|
|
187
246
|
LOG.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${fq}. But a property of that name is already defined explicitly. Consider renaming that property.`)
|
|
188
247
|
} else {
|
|
189
248
|
const kelement = Object.assign(Object.create(originalKeyElement), {
|
|
@@ -201,42 +260,49 @@ class Visitor {
|
|
|
201
260
|
}
|
|
202
261
|
}
|
|
203
262
|
|
|
263
|
+
if ('kind' in entity) {
|
|
264
|
+
buffer.addIndented([`static readonly kind: 'entity' | 'type' | 'aspect' = '${entity.kind}';`])
|
|
265
|
+
}
|
|
266
|
+
|
|
204
267
|
buffer.addIndented(() => {
|
|
205
268
|
for (const e of enums) {
|
|
269
|
+
const eDoc = docify(e.doc)
|
|
270
|
+
eDoc.forEach(d => { buffer.add(d) })
|
|
206
271
|
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
207
|
-
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
208
|
-
}
|
|
209
|
-
const actions = Object.entries(entity.actions ?? {})
|
|
210
|
-
if (actions.length) {
|
|
211
|
-
buffer.addIndentedBlock('static readonly actions: {',
|
|
212
|
-
actions.map(([aname, action]) => SourceFile.stringifyLambda({
|
|
213
|
-
name: aname,
|
|
214
|
-
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
215
|
-
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
|
|
216
|
-
kind: action.kind
|
|
217
|
-
}))
|
|
218
|
-
, '}') // end of actions
|
|
219
|
-
} else {
|
|
220
|
-
buffer.add(`static readonly actions: ${empty}`)
|
|
272
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
|
|
221
273
|
}
|
|
274
|
+
this.#printStaticActions(entity, buffer, ancestorInfos, file)
|
|
222
275
|
})
|
|
223
276
|
}, '};') // end of generated class
|
|
224
277
|
}, '}') // end of aspect
|
|
225
278
|
|
|
226
279
|
// CLASS WITH ADDED ASPECTS
|
|
227
280
|
file.addImport(baseDefinitions.path)
|
|
228
|
-
|
|
281
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
282
|
+
buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
229
283
|
this.contexts.pop()
|
|
230
284
|
}
|
|
231
285
|
|
|
286
|
+
/**
|
|
287
|
+
* @param {string} clean - the clean name of the entity
|
|
288
|
+
* @param {EntityCSN} entity - the entity to generate the static contents for
|
|
289
|
+
*/
|
|
232
290
|
#staticClassContents(clean, entity) {
|
|
233
291
|
return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
234
292
|
}
|
|
235
293
|
|
|
294
|
+
/**
|
|
295
|
+
* @param {string} fq - fully qualified name of the entity
|
|
296
|
+
* @param {EntityCSN} entity - the entity to print
|
|
297
|
+
*/
|
|
236
298
|
#printEntity(fq, entity) {
|
|
237
299
|
// static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
|
|
300
|
+
/**
|
|
301
|
+
* @param {string} clazz - the class to override the name property for
|
|
302
|
+
* @param {string} content - the content to set the name property to
|
|
303
|
+
*/
|
|
238
304
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
239
|
-
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.
|
|
305
|
+
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFqOrThrow(fq)
|
|
240
306
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
241
307
|
let { singular, plural } = inflection
|
|
242
308
|
|
|
@@ -272,7 +338,6 @@ class Visitor {
|
|
|
272
338
|
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
|
|
273
339
|
// edge case: @singular annotation present. singular4 will take care of that.
|
|
274
340
|
file.addInflection(util.singular4(entity, true), plural, clean)
|
|
275
|
-
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
276
341
|
|
|
277
342
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
278
343
|
const target = isProjection(entity) || isView(entity)
|
|
@@ -299,6 +364,7 @@ class Visitor {
|
|
|
299
364
|
// so it can get passed as value to CQL functions.
|
|
300
365
|
const additionalProperties = this.#staticClassContents(singular, entity)
|
|
301
366
|
additionalProperties.push('$count?: number')
|
|
367
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
302
368
|
buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
|
|
303
369
|
buffer.add(overrideNameProperty(plural, entity.name))
|
|
304
370
|
}
|
|
@@ -309,72 +375,98 @@ class Visitor {
|
|
|
309
375
|
* Stringifies function parameters in preparation of passing them to {@link SourceFile.stringifyLambda}.
|
|
310
376
|
* Resolves all parameters to a pair of parameter name and name of the resolved type.
|
|
311
377
|
* Also filters out parameters that indicate a binding parameter ({@link https://cap.cloud.sap/docs/releases/jan23#simplified-syntax-for-binding-parameters}).
|
|
312
|
-
* @param {[string
|
|
313
|
-
* @param {
|
|
314
|
-
* @returns {[
|
|
378
|
+
* @param {{[key:string]: EntityCSN}} params - parameter list as found in CSN.
|
|
379
|
+
* @param {SourceFile} file - source file relative to which the parameter types should be resolved.
|
|
380
|
+
* @returns {import('./typedefs').visitor.ParamInfo[]} tuple of name, modifier, type and doc.
|
|
315
381
|
*/
|
|
316
382
|
#stringifyFunctionParams(params, file) {
|
|
317
|
-
return params
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
383
|
+
return Object.entries(params ?? {})
|
|
384
|
+
// filter params of type '[many] $self', as they are not to be part of the implementation
|
|
385
|
+
.filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
|
|
386
|
+
.map(([name, type]) => ({
|
|
387
|
+
name,
|
|
388
|
+
modifier: this.resolver.isOptional(type) ? '?' : '',
|
|
389
|
+
type: this.#stringifyFunctionParamType(type, file),
|
|
390
|
+
doc: docify(type.doc).join('\n'),
|
|
391
|
+
}))
|
|
326
392
|
}
|
|
327
393
|
|
|
394
|
+
/**
|
|
395
|
+
* @param {EntityCSN | EnumCSN} type - type
|
|
396
|
+
* @param {SourceFile} file - the file to resolve types into
|
|
397
|
+
*/
|
|
328
398
|
#stringifyFunctionParamType(type, file) {
|
|
329
399
|
// if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
|
|
330
400
|
// at a named enum type. In that case also resolve that type name
|
|
331
|
-
|
|
401
|
+
const isNamedEnumType = isEnum(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)
|
|
402
|
+
if (isNamedEnumType) return stringifyEnumType(csnToEnumPairs(type))
|
|
332
403
|
const paramType = this.resolver.resolveAndRequire(type, file)
|
|
333
404
|
return this.inlineDeclarationResolver.getPropertyDatatype(
|
|
334
405
|
paramType,
|
|
335
|
-
paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire
|
|
406
|
+
paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire
|
|
407
|
+
? paramType.typeName
|
|
408
|
+
: paramType.typeInfo.inflection.singular
|
|
336
409
|
)
|
|
337
410
|
}
|
|
338
411
|
|
|
339
412
|
/**
|
|
340
413
|
* @param {string} fq - fully qualified name of the operation
|
|
341
|
-
* @param {
|
|
414
|
+
* @param {import('./typedefs').resolver.OperationCSN} operation - CSN
|
|
342
415
|
* @param {'function' | 'action'} kind - kind of operation
|
|
343
416
|
*/
|
|
344
417
|
#printOperation(fq, operation, kind) {
|
|
345
418
|
LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
|
|
346
|
-
const { namespace } = this.entityRepository.
|
|
419
|
+
const { namespace } = this.entityRepository.getByFqOrThrow(fq)
|
|
347
420
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
348
421
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
349
422
|
const returnType = operation.returns
|
|
350
423
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
351
|
-
: { typeName: 'void', typeInfo: { isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
352
|
-
|
|
424
|
+
: { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
425
|
+
let returns = this.inlineDeclarationResolver.getPropertyDatatype(
|
|
353
426
|
returnType,
|
|
354
|
-
returnType.typeInfo.isArray
|
|
427
|
+
returnType.typeInfo.isArray
|
|
428
|
+
? returnType.typeName
|
|
429
|
+
: returnType.typeInfo.inflection.singular
|
|
355
430
|
)
|
|
356
|
-
|
|
431
|
+
if (operation.returns) {
|
|
432
|
+
// operation results may be a Promise
|
|
433
|
+
returns = createUnionOf(createPromiseOf(returns), returns)
|
|
434
|
+
}
|
|
435
|
+
// Actions for ABAP RFC modules have 'parameter categories' (import/export/changing/tables) that cannot be called in a flat order.
|
|
436
|
+
// Prevent positional call style there.
|
|
437
|
+
// TODO find a better way to detect ABAP RFC actions
|
|
438
|
+
const isRFC = Object.values(operation.params ?? {}).some(p => Object.keys(p).some(k => k.startsWith('@RFC')))
|
|
439
|
+
file.addOperation(last(fq), params, returns, kind, docify(operation.doc), {named: true, positional: !isRFC})
|
|
357
440
|
}
|
|
358
441
|
|
|
442
|
+
/**
|
|
443
|
+
* @param {string} fq - fully qualified name of the type
|
|
444
|
+
* @param {EntityCSN} type - CSN
|
|
445
|
+
*/
|
|
359
446
|
#printType(fq, type) {
|
|
360
447
|
LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
|
|
361
|
-
const { namespace, entityName } = this.entityRepository.
|
|
448
|
+
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
362
449
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
363
450
|
// skip references to enums.
|
|
364
451
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
365
452
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
366
|
-
if (
|
|
367
|
-
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
453
|
+
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
454
|
+
file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
|
|
368
455
|
} else {
|
|
456
|
+
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
|
|
369
457
|
// alias
|
|
370
|
-
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
|
|
458
|
+
file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
|
|
371
459
|
}
|
|
372
460
|
// TODO: annotations not handled yet
|
|
373
461
|
}
|
|
374
462
|
|
|
463
|
+
/**
|
|
464
|
+
* @param {string} fq - fully qualified name of the aspect
|
|
465
|
+
* @param {EntityCSN} aspect - CSN
|
|
466
|
+
*/
|
|
375
467
|
#printAspect(fq, aspect) {
|
|
376
468
|
LOG.debug(`Printing aspect ${fq}`)
|
|
377
|
-
const { namespace, entityName, inflection } = this.entityRepository.
|
|
469
|
+
const { namespace, entityName, inflection } = this.entityRepository.getByFqOrThrow(fq)
|
|
378
470
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
379
471
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
380
472
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
@@ -384,9 +476,12 @@ class Visitor {
|
|
|
384
476
|
this.#aspectify(fq, aspect, file.aspects, { cleanName: inflection.singular })
|
|
385
477
|
}
|
|
386
478
|
|
|
479
|
+
/**
|
|
480
|
+
* @param {string} fq - fully qualified name of the event
|
|
481
|
+
* @param {EntityCSN} event - CSN
|
|
482
|
+
*/
|
|
387
483
|
#printEvent(fq, event) {
|
|
388
|
-
|
|
389
|
-
const { namespace, entityName } = this.entityRepository.getByFq(fq)
|
|
484
|
+
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
390
485
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
391
486
|
file.addEvent(entityName, fq)
|
|
392
487
|
const buffer = file.events.buffer
|
|
@@ -402,12 +497,27 @@ class Visitor {
|
|
|
402
497
|
}, '}')
|
|
403
498
|
}
|
|
404
499
|
|
|
500
|
+
/**
|
|
501
|
+
* @param {string} fq - fully qualified name of the service
|
|
502
|
+
* @param {import('./typedefs').resolver.EntityCSN} service - CSN
|
|
503
|
+
*/
|
|
405
504
|
#printService(fq, service) {
|
|
406
505
|
LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
|
|
407
|
-
const { namespace } = this.entityRepository.
|
|
506
|
+
const { namespace } = this.entityRepository.getByFqOrThrow(fq)
|
|
408
507
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
409
|
-
|
|
410
|
-
|
|
508
|
+
const buffer = file.services.buffer
|
|
509
|
+
const serviceNameSimple = service.name.split('.').pop()
|
|
510
|
+
|
|
511
|
+
docify(service.doc).forEach(d => { buffer.add(d) })
|
|
512
|
+
// file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
|
|
513
|
+
buffer.addIndentedBlock(`export class ${serviceNameSimple} extends cds.Service {`, () => {
|
|
514
|
+
Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
|
|
515
|
+
docify(doc).forEach(d => { buffer.add(d) })
|
|
516
|
+
buffer.add(`declare ${name}: typeof ${name}`)
|
|
517
|
+
})
|
|
518
|
+
}, '}')
|
|
519
|
+
buffer.add(`export default ${serviceNameSimple}`)
|
|
520
|
+
buffer.add('')
|
|
411
521
|
file.addService(service.name)
|
|
412
522
|
}
|
|
413
523
|
|
|
@@ -415,7 +525,7 @@ class Visitor {
|
|
|
415
525
|
* Visits a single entity from the CSN's definition field.
|
|
416
526
|
* Will call #printEntity or #printAction based on the entity's kind.
|
|
417
527
|
* @param {string} fq - name of the entity, fully qualified as is used in the definition field.
|
|
418
|
-
* @param {
|
|
528
|
+
* @param {EntityCSN} entity - CSN data belonging to the entity to perform lookups in.
|
|
419
529
|
*/
|
|
420
530
|
visitEntity(fq, entity) {
|
|
421
531
|
switch (entity.kind) {
|
|
@@ -424,6 +534,7 @@ class Visitor {
|
|
|
424
534
|
break
|
|
425
535
|
case 'action':
|
|
426
536
|
case 'function':
|
|
537
|
+
// @ts-expect-error - we know entity is actually an OperationCSN
|
|
427
538
|
this.#printOperation(fq, entity, entity.kind)
|
|
428
539
|
break
|
|
429
540
|
case 'aspect':
|
|
@@ -470,16 +581,25 @@ class Visitor {
|
|
|
470
581
|
/**
|
|
471
582
|
* Visits a single element in an entity.
|
|
472
583
|
* @param {string} name - name of the element
|
|
473
|
-
* @param {
|
|
584
|
+
* @param {EntityCSN} element - CSN data belonging to the the element.
|
|
474
585
|
* @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
|
|
475
|
-
* @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
586
|
+
* @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
476
587
|
* @returns @see InlineDeclarationResolver.visitElement
|
|
477
588
|
*/
|
|
478
|
-
visitElement(name, element, file, buffer) {
|
|
479
|
-
return this.inlineDeclarationResolver.visitElement(
|
|
589
|
+
visitElement(name, element, file, buffer = file.classes) {
|
|
590
|
+
return this.inlineDeclarationResolver.visitElement({
|
|
591
|
+
name,
|
|
592
|
+
element,
|
|
593
|
+
file,
|
|
594
|
+
buffer,
|
|
595
|
+
// we explicitly pass the "declare" modifier here to avoid problems with noImplicitOverride and useDefineForClassFields in strict tsconfigs
|
|
596
|
+
// but not inside type defs (e.g. parameter types) where this would be a syntax error
|
|
597
|
+
modifiers: getPropertyModifiers(element)
|
|
598
|
+
})
|
|
480
599
|
}
|
|
481
600
|
}
|
|
482
601
|
|
|
483
602
|
module.exports = {
|
|
484
603
|
Visitor
|
|
485
604
|
}
|
|
605
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"doc:clean": "rm -rf ./doc",
|
|
25
25
|
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|
|
26
26
|
"doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
|
|
27
|
-
"doc:cli": "npm run cli -- --help > ./doc/cli.txt"
|
|
27
|
+
"doc:cli": "npm run cli -- --help > ./doc/cli.txt",
|
|
28
|
+
"jsdoc:check": "tsc --noEmit --project jsconfig.json"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"lib/",
|
|
@@ -39,14 +40,17 @@
|
|
|
39
40
|
"bin": {
|
|
40
41
|
"cds-typer": "./lib/cli.js"
|
|
41
42
|
},
|
|
42
|
-
"
|
|
43
|
-
"@
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@cap-js/cds-types": ">=0.6.4",
|
|
45
|
+
"@sap/cds": ">=8"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
|
-
"@
|
|
48
|
+
"@cap-js/cds-types": "^0",
|
|
49
|
+
"@sap/cds": "^8",
|
|
50
|
+
"@stylistic/eslint-plugin-js": "^2.7.2",
|
|
47
51
|
"acorn": "^8.10.0",
|
|
48
52
|
"eslint": "^9",
|
|
49
|
-
"eslint-plugin-jsdoc": "^
|
|
53
|
+
"eslint-plugin-jsdoc": "^50.2.2",
|
|
50
54
|
"globals": "^15.0.0",
|
|
51
55
|
"jest": "^29",
|
|
52
56
|
"typescript": ">=4.6.4"
|