@cap-js/cds-typer 0.25.0 → 0.27.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 +26 -2
- package/README.md +19 -0
- package/cds-plugin.js +11 -4
- package/lib/cli.js +174 -36
- package/lib/compile.js +10 -16
- package/lib/components/basedefs.js +6 -0
- package/lib/components/enum.js +4 -2
- package/lib/components/inline.js +2 -1
- package/lib/components/javascript.js +28 -0
- package/lib/components/wrappers.js +42 -3
- package/lib/config.js +117 -0
- package/lib/file.js +115 -26
- package/lib/resolution/resolver.js +17 -4
- package/lib/typedefs.d.ts +54 -27
- package/lib/util.js +17 -64
- package/lib/visitor.js +78 -57
- package/package.json +8 -6
package/lib/util.js
CHANGED
|
@@ -19,6 +19,16 @@ const annotations = {
|
|
|
19
19
|
plural: ['@plural'],
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Converts a camelCase string to snake_case.
|
|
24
|
+
* @param {string} camel - The camelCase string.
|
|
25
|
+
* @returns {string} - The snake_case string.
|
|
26
|
+
*/
|
|
27
|
+
const camelToSnake = camel => camel
|
|
28
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2') // Handle camelCase
|
|
29
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') // Handle sequences of uppercase letters
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Tries to retrieve an annotation that specifies the singular name
|
|
24
34
|
* from a CSN. Valid annotations are listed in util.annotations
|
|
@@ -27,6 +37,7 @@ const annotations = {
|
|
|
27
37
|
* @param {EntityCSN} csn - the CSN of an entity to check
|
|
28
38
|
* @returns {string | undefined} the singular annotation or undefined
|
|
29
39
|
*/
|
|
40
|
+
// @ts-expect-error - can not use possible undefined from find as key
|
|
30
41
|
const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.hasOwn(csn, a))]
|
|
31
42
|
|
|
32
43
|
/**
|
|
@@ -37,6 +48,7 @@ const getSingularAnnotation = csn => csn[annotations.singular.find(a => Object.h
|
|
|
37
48
|
* @param {EntityCSN} csn - the CSN of an entity to check
|
|
38
49
|
* @returns {string | undefined} the plural annotation or undefined
|
|
39
50
|
*/
|
|
51
|
+
// @ts-expect-error - can not use possible undefined from find as key
|
|
40
52
|
const getPluralAnnotation = csn => csn[annotations.plural.find(a => Object.hasOwn(csn, a))]
|
|
41
53
|
|
|
42
54
|
/**
|
|
@@ -62,9 +74,9 @@ const unlocalize = name => {
|
|
|
62
74
|
* @param {boolean?} stripped - if true, leading namespace will be stripped
|
|
63
75
|
*/
|
|
64
76
|
const singular4 = (dn, stripped = false) => {
|
|
65
|
-
let n = dn.name
|
|
77
|
+
let n = dn.name ?? dn
|
|
66
78
|
if (stripped) {
|
|
67
|
-
n = n.match(last)[0]
|
|
79
|
+
n = n.match(last)?.[0] ?? ''
|
|
68
80
|
}
|
|
69
81
|
return (
|
|
70
82
|
getSingularAnnotation(dn) ??
|
|
@@ -94,9 +106,9 @@ const singular4 = (dn, stripped = false) => {
|
|
|
94
106
|
* @param {boolean} stripped - if true, leading namespace will be stripped
|
|
95
107
|
*/
|
|
96
108
|
const plural4 = (dn, stripped) => {
|
|
97
|
-
let n = dn.name
|
|
109
|
+
let n = dn.name ?? dn
|
|
98
110
|
if (stripped) {
|
|
99
|
-
n = n.match(last)[0]
|
|
111
|
+
n = n.match(last)?.[0]
|
|
100
112
|
}
|
|
101
113
|
return (
|
|
102
114
|
getPluralAnnotation(dn) ??
|
|
@@ -123,72 +135,13 @@ const deepMerge = (target, source) => {
|
|
|
123
135
|
Object.assign(target, source)
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
/**
|
|
127
|
-
* Parses command line arguments into named and positional parameters.
|
|
128
|
-
* Named parameters are expected to start with a double dash (--).
|
|
129
|
-
* If the next argument `B` after a named parameter `A` is not a named parameter itself,
|
|
130
|
-
* `B` is used as value for `A`.
|
|
131
|
-
* If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
|
|
132
|
-
* Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
|
|
133
|
-
* will cause an error.
|
|
134
|
-
* Named parameters that are either not specified or do not have a value assigned to them may draw a default value
|
|
135
|
-
* from their definition in validFlags.
|
|
136
|
-
* @param {string[]} argv - list of command line arguments
|
|
137
|
-
* @param {{[key: string]: CommandlineFlag}} validFlags - allowed flags. May specify default values.
|
|
138
|
-
* @returns {ParsedFlags}
|
|
139
|
-
*/
|
|
140
|
-
const parseCommandlineArgs = (argv, validFlags) => {
|
|
141
|
-
const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
|
|
142
|
-
const positional = []
|
|
143
|
-
const named = {}
|
|
144
|
-
|
|
145
|
-
let i = 0
|
|
146
|
-
while (i < argv.length) {
|
|
147
|
-
let arg = argv[i]
|
|
148
|
-
if (isFlag(arg)) {
|
|
149
|
-
arg = arg.slice(2)
|
|
150
|
-
if (!(arg in validFlags)) {
|
|
151
|
-
throw new Error(`invalid named flag '${arg}'`)
|
|
152
|
-
} else {
|
|
153
|
-
const next = argv[i + 1]
|
|
154
|
-
if (next && !isFlag(next)) {
|
|
155
|
-
named[arg] = next
|
|
156
|
-
i++
|
|
157
|
-
} else {
|
|
158
|
-
named[arg] = validFlags[arg].default
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const { allowed, allowedHint } = validFlags[arg]
|
|
162
|
-
if (allowed && !allowed.includes(named[arg])) {
|
|
163
|
-
throw new Error(`invalid value '${named[arg]}' for flag ${arg}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
positional.push(arg)
|
|
168
|
-
}
|
|
169
|
-
i++
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const defaults = Object.entries(validFlags)
|
|
173
|
-
.filter(e => !!e[1].default)
|
|
174
|
-
.reduce((dict, [k, v]) => {
|
|
175
|
-
dict[k] = v.default
|
|
176
|
-
return dict
|
|
177
|
-
}, {})
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
named: Object.assign(defaults, named),
|
|
181
|
-
positional,
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
138
|
module.exports = {
|
|
186
139
|
annotations,
|
|
140
|
+
camelToSnake,
|
|
187
141
|
getSingularAnnotation,
|
|
188
142
|
getPluralAnnotation,
|
|
189
143
|
unlocalize,
|
|
190
144
|
singular4,
|
|
191
145
|
plural4,
|
|
192
|
-
parseCommandlineArgs,
|
|
193
146
|
deepMerge
|
|
194
147
|
}
|
package/lib/visitor.js
CHANGED
|
@@ -8,7 +8,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, createKeysOf } = 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')
|
|
@@ -16,22 +16,15 @@ const { baseDefinitions } = require('./components/basedefs')
|
|
|
16
16
|
const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
17
17
|
const { last } = require('./components/identifier')
|
|
18
18
|
const { getPropertyModifiers } = require('./components/property')
|
|
19
|
+
const { configuration } = require('./config')
|
|
19
20
|
|
|
20
21
|
/** @typedef {import('./file').File} File */
|
|
21
22
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
22
|
-
/** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
|
|
23
|
-
/** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
|
|
24
23
|
/** @typedef {import('./typedefs').visitor.Inflection} Inflection */
|
|
25
24
|
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
26
25
|
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
27
26
|
/** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
|
|
28
27
|
|
|
29
|
-
const defaults = {
|
|
30
|
-
// FIXME: add defaults for remaining parameters
|
|
31
|
-
propertiesOptional: true,
|
|
32
|
-
inlineDeclarations: 'flat'
|
|
33
|
-
}
|
|
34
|
-
|
|
35
28
|
class Visitor {
|
|
36
29
|
/**
|
|
37
30
|
* Gathers all files that are supposed to be written to
|
|
@@ -45,12 +38,10 @@ class Visitor {
|
|
|
45
38
|
|
|
46
39
|
/**
|
|
47
40
|
* @param {{xtended: CSN, inferred: CSN}} csn - root CSN
|
|
48
|
-
* @param {VisitorOptions | {}} options - the options
|
|
49
41
|
*/
|
|
50
|
-
constructor(csn
|
|
42
|
+
constructor(csn) {
|
|
51
43
|
amendCSN(csn.xtended)
|
|
52
44
|
propagateForeignKeys(csn.inferred)
|
|
53
|
-
this.options = { ...defaults, ...options }
|
|
54
45
|
this.csn = csn
|
|
55
46
|
|
|
56
47
|
/** @type {Context[]} **/
|
|
@@ -66,7 +57,7 @@ class Visitor {
|
|
|
66
57
|
this.fileRepository = new FileRepository()
|
|
67
58
|
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
68
59
|
this.inlineDeclarationResolver =
|
|
69
|
-
|
|
60
|
+
configuration.inlineDeclarations === 'structured'
|
|
70
61
|
? new StructuredInlineDeclarationResolver(this)
|
|
71
62
|
: new FlatInlineDeclarationResolver(this)
|
|
72
63
|
|
|
@@ -102,7 +93,13 @@ class Visitor {
|
|
|
102
93
|
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
|
|
103
94
|
// this should be revisted once we settle on a single flavour.
|
|
104
95
|
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
|
|
105
|
-
|
|
96
|
+
if (target.kind !== 'type') {
|
|
97
|
+
// skip if the target is a property, like in:
|
|
98
|
+
// books: Association to many Author.books ...
|
|
99
|
+
// as this would result in a type definition that
|
|
100
|
+
// name-clashes with the actual declaration of Author
|
|
101
|
+
this.visitEntity(name, target)
|
|
102
|
+
}
|
|
106
103
|
} else {
|
|
107
104
|
LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
|
|
108
105
|
}
|
|
@@ -148,7 +145,8 @@ class Visitor {
|
|
|
148
145
|
returns: action.returns
|
|
149
146
|
? this.resolver.resolveAndRequire(action.returns, file).typeName
|
|
150
147
|
: 'any',
|
|
151
|
-
kind: action.kind
|
|
148
|
+
kind: action.kind,
|
|
149
|
+
doc: docify(action.doc)
|
|
152
150
|
})), '}'
|
|
153
151
|
) // end of actions
|
|
154
152
|
} else {
|
|
@@ -156,6 +154,14 @@ class Visitor {
|
|
|
156
154
|
}
|
|
157
155
|
}
|
|
158
156
|
|
|
157
|
+
/**
|
|
158
|
+
* @param {Buffer} buffer - the buffer to write the keys into
|
|
159
|
+
* @param {string} clean - the clean name of the entity
|
|
160
|
+
*/
|
|
161
|
+
#printStaticKeys(buffer, clean) {
|
|
162
|
+
buffer.add(`declare static readonly keys: ${createKeysOf(clean)}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
/**
|
|
160
166
|
* Transforms an entity or CDS aspect into a JS aspect (aka mixin).
|
|
161
167
|
* That is, for an element A we get:
|
|
@@ -168,8 +174,7 @@ class Visitor {
|
|
|
168
174
|
* @param {{cleanName?: string}} options - additional options
|
|
169
175
|
*/
|
|
170
176
|
#aspectify(fq, entity, buffer, options = {}) {
|
|
171
|
-
const info = this.entityRepository.
|
|
172
|
-
if (!info) throw new Error(`could not resolve entity ${fq}`)
|
|
177
|
+
const info = this.entityRepository.getByFqOrThrow(fq)
|
|
173
178
|
const clean = options?.cleanName ?? info.withoutNamespace
|
|
174
179
|
const { namespace } = info
|
|
175
180
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
@@ -257,18 +262,27 @@ class Visitor {
|
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
if ('kind' in entity) {
|
|
266
|
+
buffer.addIndented([`static readonly kind: 'entity' | 'type' | 'aspect' = '${entity.kind}';`])
|
|
267
|
+
}
|
|
268
|
+
|
|
260
269
|
buffer.addIndented(() => {
|
|
261
270
|
for (const e of enums) {
|
|
271
|
+
const eDoc = docify(e.doc)
|
|
272
|
+
eDoc.forEach(d => { buffer.add(d) })
|
|
262
273
|
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
263
|
-
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
274
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
|
|
264
275
|
}
|
|
265
276
|
this.#printStaticActions(entity, buffer, ancestorInfos, file)
|
|
277
|
+
this.#printStaticKeys(buffer, clean)
|
|
278
|
+
|
|
266
279
|
})
|
|
267
280
|
}, '};') // end of generated class
|
|
268
281
|
}, '}') // end of aspect
|
|
269
282
|
|
|
270
283
|
// CLASS WITH ADDED ASPECTS
|
|
271
284
|
file.addImport(baseDefinitions.path)
|
|
285
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
272
286
|
buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
273
287
|
this.contexts.pop()
|
|
274
288
|
}
|
|
@@ -292,9 +306,7 @@ class Visitor {
|
|
|
292
306
|
* @param {string} content - the content to set the name property to
|
|
293
307
|
*/
|
|
294
308
|
const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
|
|
295
|
-
const
|
|
296
|
-
if (!info) throw new Error(`could not resolve entity ${fq}`)
|
|
297
|
-
const { namespace: ns, entityName: clean, inflection } = info
|
|
309
|
+
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFqOrThrow(fq)
|
|
298
310
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
299
311
|
let { singular, plural } = inflection
|
|
300
312
|
|
|
@@ -330,7 +342,6 @@ class Visitor {
|
|
|
330
342
|
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
|
|
331
343
|
// edge case: @singular annotation present. singular4 will take care of that.
|
|
332
344
|
file.addInflection(util.singular4(entity, true), plural, clean)
|
|
333
|
-
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
334
345
|
|
|
335
346
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
336
347
|
const target = isProjection(entity) || isView(entity)
|
|
@@ -357,6 +368,7 @@ class Visitor {
|
|
|
357
368
|
// so it can get passed as value to CQL functions.
|
|
358
369
|
const additionalProperties = this.#staticClassContents(singular, entity)
|
|
359
370
|
additionalProperties.push('$count?: number')
|
|
371
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
360
372
|
buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
|
|
361
373
|
buffer.add(overrideNameProperty(plural, entity.name))
|
|
362
374
|
}
|
|
@@ -369,18 +381,18 @@ class Visitor {
|
|
|
369
381
|
* Also filters out parameters that indicate a binding parameter ({@link https://cap.cloud.sap/docs/releases/jan23#simplified-syntax-for-binding-parameters}).
|
|
370
382
|
* @param {{[key:string]: EntityCSN}} params - parameter list as found in CSN.
|
|
371
383
|
* @param {SourceFile} file - source file relative to which the parameter types should be resolved.
|
|
372
|
-
* @returns {[
|
|
384
|
+
* @returns {import('./typedefs').visitor.ParamInfo[]} tuple of name, modifier, type and doc.
|
|
373
385
|
*/
|
|
374
386
|
#stringifyFunctionParams(params, file) {
|
|
375
|
-
return params
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
387
|
+
return Object.entries(params ?? {})
|
|
388
|
+
// filter params of type '[many] $self', as they are not to be part of the implementation
|
|
389
|
+
.filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
|
|
390
|
+
.map(([name, type]) => ({
|
|
391
|
+
name,
|
|
392
|
+
modifier: this.resolver.isOptional(type) ? '?' : '',
|
|
393
|
+
type: this.#stringifyFunctionParamType(type, file),
|
|
394
|
+
doc: docify(type.doc).join('\n'),
|
|
395
|
+
}))
|
|
384
396
|
}
|
|
385
397
|
|
|
386
398
|
/**
|
|
@@ -408,21 +420,27 @@ class Visitor {
|
|
|
408
420
|
*/
|
|
409
421
|
#printOperation(fq, operation, kind) {
|
|
410
422
|
LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
|
|
411
|
-
const
|
|
412
|
-
if (!info) throw new Error(`could not resolve operation ${fq}`)
|
|
413
|
-
const { namespace } = info
|
|
423
|
+
const { namespace } = this.entityRepository.getByFqOrThrow(fq)
|
|
414
424
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
415
425
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
416
426
|
const returnType = operation.returns
|
|
417
427
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
418
428
|
: { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
419
|
-
|
|
429
|
+
let returns = this.inlineDeclarationResolver.getPropertyDatatype(
|
|
420
430
|
returnType,
|
|
421
431
|
returnType.typeInfo.isArray
|
|
422
432
|
? returnType.typeName
|
|
423
433
|
: returnType.typeInfo.inflection.singular
|
|
424
434
|
)
|
|
425
|
-
|
|
435
|
+
if (operation.returns) {
|
|
436
|
+
// operation results may be a Promise
|
|
437
|
+
returns = createUnionOf(createPromiseOf(returns), returns)
|
|
438
|
+
}
|
|
439
|
+
// Actions for ABAP RFC modules have 'parameter categories' (import/export/changing/tables) that cannot be called in a flat order.
|
|
440
|
+
// Prevent positional call style there.
|
|
441
|
+
// TODO find a better way to detect ABAP RFC actions
|
|
442
|
+
const isRFC = Object.values(operation.params ?? {}).some(p => Object.keys(p).some(k => k.startsWith('@RFC')))
|
|
443
|
+
file.addOperation(last(fq), params, returns, kind, docify(operation.doc), {named: true, positional: !isRFC})
|
|
426
444
|
}
|
|
427
445
|
|
|
428
446
|
/**
|
|
@@ -431,15 +449,13 @@ class Visitor {
|
|
|
431
449
|
*/
|
|
432
450
|
#printType(fq, type) {
|
|
433
451
|
LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
|
|
434
|
-
const
|
|
435
|
-
if (!info) throw new Error(`could not resolve type ${fq}`)
|
|
436
|
-
const { namespace, entityName } = info
|
|
452
|
+
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
437
453
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
438
454
|
// skip references to enums.
|
|
439
455
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
440
456
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
441
457
|
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
442
|
-
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
458
|
+
file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
|
|
443
459
|
} else {
|
|
444
460
|
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
|
|
445
461
|
// alias
|
|
@@ -454,9 +470,7 @@ class Visitor {
|
|
|
454
470
|
*/
|
|
455
471
|
#printAspect(fq, aspect) {
|
|
456
472
|
LOG.debug(`Printing aspect ${fq}`)
|
|
457
|
-
const
|
|
458
|
-
if (!info) throw new Error(`could not resolve aspect ${fq}`)
|
|
459
|
-
const { namespace, entityName, inflection } = info
|
|
473
|
+
const { namespace, entityName, inflection } = this.entityRepository.getByFqOrThrow(fq)
|
|
460
474
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
461
475
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
462
476
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
@@ -471,37 +485,44 @@ class Visitor {
|
|
|
471
485
|
* @param {EntityCSN} event - CSN
|
|
472
486
|
*/
|
|
473
487
|
#printEvent(fq, event) {
|
|
474
|
-
|
|
475
|
-
const info = this.entityRepository.getByFq(fq)
|
|
476
|
-
if (!info) throw new Error(`could not resolve event ${fq}`)
|
|
477
|
-
const { namespace, entityName } = info
|
|
488
|
+
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
478
489
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
479
490
|
file.addEvent(entityName, fq)
|
|
480
491
|
const buffer = file.events.buffer
|
|
481
492
|
buffer.add('// event')
|
|
482
493
|
// only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
|
|
483
494
|
buffer.addIndentedBlock(`export declare class ${entityName} {`, () => {
|
|
484
|
-
const propOpt =
|
|
485
|
-
|
|
495
|
+
const propOpt = configuration.propertiesOptional
|
|
496
|
+
// FIXME: shouldn't need to change config here! Idea: init Visitor with .options fed from config, then manipulate that
|
|
497
|
+
configuration.propertiesOptional = false
|
|
486
498
|
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
487
499
|
this.visitElement(ename, element, file, buffer)
|
|
488
500
|
}
|
|
489
|
-
|
|
501
|
+
configuration.propertiesOptional = propOpt
|
|
490
502
|
}, '}')
|
|
491
503
|
}
|
|
492
504
|
|
|
493
505
|
/**
|
|
494
506
|
* @param {string} fq - fully qualified name of the service
|
|
495
|
-
* @param {EntityCSN} service - CSN
|
|
507
|
+
* @param {import('./typedefs').resolver.EntityCSN} service - CSN
|
|
496
508
|
*/
|
|
497
509
|
#printService(fq, service) {
|
|
498
510
|
LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
|
|
499
|
-
const
|
|
500
|
-
if (!info) throw new Error(`could not resolve service ${fq}`)
|
|
501
|
-
const { namespace } = info
|
|
511
|
+
const { namespace } = this.entityRepository.getByFqOrThrow(fq)
|
|
502
512
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
503
|
-
|
|
504
|
-
|
|
513
|
+
const buffer = file.services.buffer
|
|
514
|
+
const serviceNameSimple = service.name.split('.').pop()
|
|
515
|
+
|
|
516
|
+
docify(service.doc).forEach(d => { buffer.add(d) })
|
|
517
|
+
// file.addImport(new Path(['cds'], '')) TODO make sap/cds import work
|
|
518
|
+
buffer.addIndentedBlock(`export class ${serviceNameSimple} extends cds.Service {`, () => {
|
|
519
|
+
Object.entries(service.operations ?? {}).forEach(([name, {doc}]) => {
|
|
520
|
+
docify(doc).forEach(d => { buffer.add(d) })
|
|
521
|
+
buffer.add(`declare ${name}: typeof ${name}`)
|
|
522
|
+
})
|
|
523
|
+
}, '}')
|
|
524
|
+
buffer.add(`export default ${serviceNameSimple}`)
|
|
525
|
+
buffer.add('')
|
|
505
526
|
file.addService(service.name)
|
|
506
527
|
}
|
|
507
528
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|
|
@@ -40,15 +40,17 @@
|
|
|
40
40
|
"bin": {
|
|
41
41
|
"cds-typer": "./lib/cli.js"
|
|
42
42
|
},
|
|
43
|
-
"
|
|
44
|
-
"@
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@cap-js/cds-types": ">=0.6.4",
|
|
45
|
+
"@sap/cds": ">=8"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@
|
|
48
|
-
"@
|
|
48
|
+
"@cap-js/cds-types": "^0",
|
|
49
|
+
"@sap/cds": "^8",
|
|
50
|
+
"@stylistic/eslint-plugin-js": "^2.7.2",
|
|
49
51
|
"acorn": "^8.10.0",
|
|
50
52
|
"eslint": "^9",
|
|
51
|
-
"eslint-plugin-jsdoc": "^
|
|
53
|
+
"eslint-plugin-jsdoc": "^50.2.2",
|
|
52
54
|
"globals": "^15.0.0",
|
|
53
55
|
"jest": "^29",
|
|
54
56
|
"typescript": ">=4.6.4"
|