@cap-js/cds-typer 0.25.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 +15 -2
- package/README.md +19 -0
- package/lib/cli.js +6 -0
- package/lib/components/enum.js +4 -2
- package/lib/components/javascript.js +28 -0
- package/lib/components/wrappers.js +26 -3
- package/lib/file.js +126 -28
- package/lib/resolution/resolver.js +9 -0
- package/lib/typedefs.d.ts +16 -0
- package/lib/visitor.js +58 -42
- package/package.json +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,20 @@ 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.
|
|
7
|
+
## Version 0.27.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.26.0 - 2024-09-11
|
|
10
|
+
### Added
|
|
11
|
+
- Added a CLI option `--useEntitiesProxy`. When set to `true`, all entities are wrapped into `Proxy` objects during runtime, allowing top level imports of entity types.
|
|
12
|
+
- Added a static `.kind` property for entities and types, which contains `'entity'` or `'type'` respectively
|
|
13
|
+
- Apps need to provide `@sap/cds` version `8.2` or higher.
|
|
14
|
+
- Apps need to provide `@cap-js/cds-types` version `0.6.4` or higher.
|
|
15
|
+
- Typed methods are now generated for calls of unbound actions. Named and positional call styles are supported, e.g. `service.action({one, two})` and `service.action(one, two)`.
|
|
16
|
+
- Action parameters can be optional in the named call style (`service.action({one:1, ...})`).
|
|
17
|
+
- Actions for ABAP RFC modules cannot be called with positional parameters, but only with named ones. They have 'parameter categories' (import/export/changing/tables) that cannot be called in a flat order.
|
|
18
|
+
- Services now have their own export (named like the service itself). The current default export is not usable in some scenarios from CommonJS modules.
|
|
19
|
+
- Enums and operation parameters can have doc comments
|
|
20
|
+
|
|
8
21
|
|
|
9
22
|
## Version 0.25.0 - 2024-08-13
|
|
10
23
|
### Added
|
|
@@ -85,7 +98,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
85
98
|
- Importing an enum into a service will now generate an alias to the original enum, instead of incorrectly duplicating the definition
|
|
86
99
|
- Returning entities from actions/ functions and using them as parameters will now properly use the singular inflection instead of returning an array thereof
|
|
87
100
|
- Aspects are now consistently named and called in their singular form
|
|
88
|
-
- Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
|
|
101
|
+
- Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
|
|
89
102
|
|
|
90
103
|
## Version 0.19.0 - 2024-03-28
|
|
91
104
|
### Added
|
package/README.md
CHANGED
|
@@ -9,6 +9,25 @@ Generates `.ts` files for a CDS model to receive code completion in VS Code.
|
|
|
9
9
|
|
|
10
10
|
Exhaustive documentation can be found on [CAPire](https://cap.cloud.sap/docs/tools/cds-typer).
|
|
11
11
|
|
|
12
|
+
## Known Restrictions
|
|
13
|
+
|
|
14
|
+
Certain language features of CDS can not be represented in TypeScript.
|
|
15
|
+
Trying to generate types for models using these features will therefore result in incorrect or broken TypeScript code.
|
|
16
|
+
|
|
17
|
+
### Changing Types
|
|
18
|
+
|
|
19
|
+
While the following is valid CDS, there is no TypeScript equivalent that would allow the type of an inherited property to change (TS2416).
|
|
20
|
+
|
|
21
|
+
```cds
|
|
22
|
+
entity A {
|
|
23
|
+
foo: Integer
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
entity B: A {
|
|
27
|
+
foo: String
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
12
31
|
## Support, Feedback, Contributing
|
|
13
32
|
|
|
14
33
|
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/cds-typer/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
package/lib/cli.js
CHANGED
|
@@ -35,6 +35,11 @@ const flags = {
|
|
|
35
35
|
desc: `Path to where the jsconfig.json should be written.${EOL}If specified, ${toolName} will create a jsconfig.json file and${EOL}set it up to restrict property usage in types entities to${EOL}existing properties only.`,
|
|
36
36
|
type: 'string'
|
|
37
37
|
},
|
|
38
|
+
useEntitiesProxy: {
|
|
39
|
+
desc: `If set to true the 'cds.entities' exports in the generated 'index.js'${EOL}files will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.${EOL}${EOL}WARNING: entity properties can still only be accessed after${EOL}'cds.entities' has been loaded`,
|
|
40
|
+
allowed: ['true', 'false'],
|
|
41
|
+
default: 'false'
|
|
42
|
+
},
|
|
38
43
|
version: {
|
|
39
44
|
desc: 'Prints the version of this tool.'
|
|
40
45
|
},
|
|
@@ -117,6 +122,7 @@ const main = async (/** @type {any} */ args) => {
|
|
|
117
122
|
// temporary fix until rootDir is faded out
|
|
118
123
|
outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
|
|
119
124
|
logLevel: args.named.logLevel,
|
|
125
|
+
useEntitiesProxy: args.named.useEntitiesProxy === 'true',
|
|
120
126
|
jsConfigPath: args.named.jsConfigPath,
|
|
121
127
|
inlineDeclarations: args.named.inlineDeclarations,
|
|
122
128
|
propertiesOptional: args.named.propertiesOptional === 'true',
|
package/lib/components/enum.js
CHANGED
|
@@ -22,7 +22,7 @@ const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
|
|
|
22
22
|
/**
|
|
23
23
|
* @param {string} key - the key of the enum
|
|
24
24
|
* @param {any} value - the value of the enum
|
|
25
|
-
* @param {string | import('
|
|
25
|
+
* @param {string | import('../typedefs').resolver.ref} enumType - the type of the enum
|
|
26
26
|
*/
|
|
27
27
|
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
|
|
28
28
|
|
|
@@ -50,10 +50,12 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
|
|
|
50
50
|
* @param {string} name - local name of the enum, i.e. the name under which it should be created in the .ts file
|
|
51
51
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
52
52
|
* @param {object} options - options for printing the enum
|
|
53
|
+
* @param {string[]} doc - the enum docs
|
|
53
54
|
*/
|
|
54
|
-
function printEnum(buffer, name, kvs, options = {}) {
|
|
55
|
+
function printEnum(buffer, name, kvs, options = {}, doc=[]) {
|
|
55
56
|
const opts = {...{export: true}, ...options}
|
|
56
57
|
buffer.add('// enum')
|
|
58
|
+
if (opts.export) doc.forEach(d => { buffer.add(d) })
|
|
57
59
|
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
58
60
|
kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
|
|
59
61
|
, '} as const;')
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
// this function will be stringified as part of the generated types and is not supposed to be used internally
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
const proxyAccessFunction = function (fqParts, opts = {}) {
|
|
5
|
+
const { target, customProps } = { target: {}, customProps: [], ...opts }
|
|
6
|
+
const fq = fqParts.join('.')
|
|
7
|
+
return new Proxy(target, {
|
|
8
|
+
get: function (target, prop) {
|
|
9
|
+
if (cds.entities) {
|
|
10
|
+
target.__proto__ = cds.entities[fq]
|
|
11
|
+
// overwrite/simplify getter after cds.entities is accessible
|
|
12
|
+
this.get = (target, prop) => target[prop]
|
|
13
|
+
return target[prop]
|
|
14
|
+
}
|
|
15
|
+
// we already know the name so we skip the cds.entities proxy access
|
|
16
|
+
if (prop === 'name') return fq
|
|
17
|
+
// custom properties access on 'target' as well as cached _entity property access goes here
|
|
18
|
+
if (Object.hasOwn(target, prop)) return target[prop]
|
|
19
|
+
// inline enums have to be caught here for first time access, as they do not exist on the entity
|
|
20
|
+
if (customProps.includes(prop)) return target[prop]
|
|
21
|
+
// last but not least we pass the property access to cds.entities
|
|
22
|
+
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.`)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}.toString()
|
|
26
|
+
/* eslint-enable no-undef */
|
|
27
|
+
|
|
28
|
+
module.exports = { proxyAccessFunction }
|
|
@@ -45,6 +45,24 @@ const createArrayOf = t => `Array<${t}>`
|
|
|
45
45
|
*/
|
|
46
46
|
const createObjectOf = t => `{${t}}`
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Wraps types into a union type string
|
|
50
|
+
* @param {string[]} types - an array of types
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
const createUnionOf = (...types) => types.join(' | ')
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Wraps type into a promise
|
|
57
|
+
* @param {string} t - the type to wrap.
|
|
58
|
+
* @returns {string}
|
|
59
|
+
* @example
|
|
60
|
+
* ```js
|
|
61
|
+
* createPromiseOf('string') // -> 'Promise<string>'
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
const createPromiseOf = t => `Promise<${t}>`
|
|
65
|
+
|
|
48
66
|
/**
|
|
49
67
|
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
|
50
68
|
* @param {string} t - the singular type name.
|
|
@@ -59,13 +77,18 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
|
|
|
59
77
|
* @returns {string[]} an array of lines wrapped in doc format. The result is not
|
|
60
78
|
* concatenated to be properly indented by `buffer.add(...)`.
|
|
61
79
|
*/
|
|
62
|
-
const docify = doc =>
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
const docify = doc => {
|
|
81
|
+
if (!doc) return []
|
|
82
|
+
const lines = doc.split(/\r?\n/).map(l => l.trim().replaceAll('*/', '*\\/')) // mask any */ with *\/
|
|
83
|
+
if (lines.length === 1) return [`/** ${lines[0]} */`] // one-line doc
|
|
84
|
+
return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
|
|
85
|
+
}
|
|
65
86
|
|
|
66
87
|
module.exports = {
|
|
67
88
|
createArrayOf,
|
|
68
89
|
createObjectOf,
|
|
90
|
+
createPromiseOf,
|
|
91
|
+
createUnionOf,
|
|
69
92
|
createToOneAssociation,
|
|
70
93
|
createToManyAssociation,
|
|
71
94
|
createCompositionOfOne,
|
package/lib/file.js
CHANGED
|
@@ -6,11 +6,13 @@ const { readFileSync } = require('fs')
|
|
|
6
6
|
const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
|
|
7
7
|
const { normalise } = require('./components/identifier')
|
|
8
8
|
const { empty } = require('./components/typescript')
|
|
9
|
+
const { proxyAccessFunction } = require('./components/javascript')
|
|
9
10
|
const { createObjectOf } = require('./components/wrappers')
|
|
10
11
|
|
|
11
12
|
const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
|
|
12
13
|
|
|
13
14
|
/** @typedef {import('./typedefs').file.Namespace} Namespace */
|
|
15
|
+
/** @typedef {import('./typedefs').file.FileOptions} FileOptions */
|
|
14
16
|
|
|
15
17
|
class File {
|
|
16
18
|
/**
|
|
@@ -107,9 +109,11 @@ class Library extends File {
|
|
|
107
109
|
class SourceFile extends File {
|
|
108
110
|
/**
|
|
109
111
|
* @param {string | Path} path - path to the file
|
|
112
|
+
* @param {FileOptions} [options] - options for file output
|
|
110
113
|
*/
|
|
111
|
-
constructor(path) {
|
|
114
|
+
constructor(path, options) {
|
|
112
115
|
super()
|
|
116
|
+
this.options = options ?? { useEntitiesProxy: false }
|
|
113
117
|
/** @type {Path} */
|
|
114
118
|
this.path = path instanceof Path ? path : new Path(path.split('.'))
|
|
115
119
|
/** @type {{[key:string]: any}} */
|
|
@@ -140,50 +144,64 @@ class SourceFile extends File {
|
|
|
140
144
|
this.inflections = []
|
|
141
145
|
/** @type {{ buffer: Buffer, names: string[]}} */
|
|
142
146
|
this.services = { buffer: new Buffer(), names: [] }
|
|
147
|
+
/** @type {Record<string,string[]>} */
|
|
148
|
+
this.entityProxies = {}
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
/**
|
|
146
152
|
* Stringifies a lambda expression.
|
|
147
153
|
* @param {object} options - options
|
|
148
154
|
* @param {string} options.name - name of the lambda
|
|
149
|
-
* @param {[
|
|
155
|
+
* @param {import('./typedefs').visitor.ParamInfo[]} [options.parameters] - list of parameters, passed as [name, modifier, type, doc] pairs
|
|
150
156
|
* @param {string} [options.returns] - the return type of the function
|
|
151
157
|
* @param {'action' | 'function'} options.kind - kind of the lambda
|
|
152
158
|
* @param {string} [options.initialiser] - the initialiser expression
|
|
153
159
|
* @param {boolean} [options.isStatic] - whether the lambda is static
|
|
160
|
+
* @param {{positional?: boolean, named?: boolean}} [options.callStyles] - whether to generate positional and/or named call styles
|
|
161
|
+
* @param {string[]?} [options.doc] - documentation for the operation
|
|
154
162
|
* @returns {string} the stringified lambda
|
|
155
163
|
* @example
|
|
156
164
|
* ```js
|
|
157
165
|
* // note: these samples are actually simplified! See below.
|
|
158
|
-
* stringifyLambda({parameters: [['p','T']]}) // f: { (p: T): any, ... }
|
|
159
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
160
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
161
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
166
|
+
* stringifyLambda({parameters: [['p','','T']]}) // f: { (p: T): any, ... }
|
|
167
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}]}) // f: { (p: T) => any, ... }
|
|
168
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',modifier:'?',type:'T',doc:'/** doc *\/'}], returns: 'number'}) // /** doc *\/f?: { (p: T) => number, ... }
|
|
169
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number', initialiser: '_ => 42'}) // f?: { (p: T): string = _ => 42, ... }
|
|
162
170
|
* ```
|
|
163
171
|
*
|
|
164
|
-
* The generated string will not be just the signature of the function. Instead, it will be an object offering
|
|
172
|
+
* The generated string will not be just the signature of the function. Instead, it will be an object offering callable signature(s).
|
|
165
173
|
* On top of that, it will also expose a property `__parameters`, which is an object reflecting the functions parameters.
|
|
166
174
|
* The reason for this is that the CDS runtime actually treats the function parameters as a named object. This can not be rectified via
|
|
167
175
|
* type magic, as parameter names do not exist on type level. So we can not use these names to reuse them as object properties.
|
|
168
176
|
* Instead, we generate this utility object for the runtime to use:
|
|
169
177
|
* @example
|
|
170
178
|
* ```js
|
|
171
|
-
* stringifyLambda({name: 'f', parameters: [
|
|
179
|
+
* stringifyLambda({name: 'f', parameters: [{name:'p',type:'T'}], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
|
|
172
180
|
* ```
|
|
173
181
|
*/
|
|
174
|
-
static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false,
|
|
175
|
-
|
|
182
|
+
static stringifyLambda({name, parameters=[], returns='any', kind, initialiser, isStatic=false, callStyles={positional:true, named:true}, doc}) {
|
|
183
|
+
let docStr = doc?.length ? doc.join('\n')+'\n' : ''
|
|
184
|
+
const parameterTypes = parameters.map(({name, modifier, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}${modifier}: ${type}`).join(', ')
|
|
176
185
|
const parameterTypeAsObject = parameterTypes.length
|
|
177
186
|
? createObjectOf(parameterTypes)
|
|
178
187
|
: empty
|
|
179
|
-
const
|
|
188
|
+
const callableSignatures = []
|
|
189
|
+
if (callStyles.positional) {
|
|
190
|
+
const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}: ${type}`).join(', ') // must not include ? modifiers
|
|
191
|
+
callableSignatures.push(`// positional\n${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
|
|
192
|
+
}
|
|
193
|
+
if (callStyles.named) {
|
|
194
|
+
const parameterNames = createObjectOf(parameters.map(({name}) => normalise(name)).join(', '))
|
|
195
|
+
callableSignatures.push(`// named\n${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
|
|
196
|
+
}
|
|
197
|
+
if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
|
|
180
198
|
let prefix = name ? `${normalise(name)}: `: ''
|
|
181
199
|
if (prefix && isStatic) {
|
|
182
200
|
prefix = `static ${prefix}`
|
|
183
201
|
}
|
|
184
202
|
const kindDef = kind ? `, kind: '${kind}'` : ''
|
|
185
203
|
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
186
|
-
const lambda = `{
|
|
204
|
+
const lambda = `{\n${callableSignatures.join('\n')}, \n// metadata (do not use)\n__parameters: ${parameterTypeAsObject}, __returns: ${returns}${kindDef}}`
|
|
187
205
|
return prefix + lambda + suffix
|
|
188
206
|
}
|
|
189
207
|
|
|
@@ -203,13 +221,16 @@ class SourceFile extends File {
|
|
|
203
221
|
/**
|
|
204
222
|
* Adds a function definition in form of a arrow function to the file.
|
|
205
223
|
* @param {string} name - name of the function
|
|
206
|
-
* @param {[
|
|
224
|
+
* @param {import('./typedefs').visitor.ParamInfo[]} parameters - list of parameters, passed as [name, modifier, type] tuple
|
|
207
225
|
* @param {string} returns - the return type of the function
|
|
208
226
|
* @param {'function' | 'action'} kind - kind of the node
|
|
227
|
+
* @param {string[]} doc - documentation for the function
|
|
228
|
+
* @param {{positional?: boolean, named?: boolean}} callStyles - how the operation can be called
|
|
209
229
|
*/
|
|
210
|
-
addOperation(name, parameters, returns, kind) {
|
|
211
|
-
//this.operations.buffer.add(
|
|
212
|
-
this.operations.buffer.add(
|
|
230
|
+
addOperation(name, parameters, returns, kind, doc, callStyles) {
|
|
231
|
+
// this.operations.buffer.add(`// ${kind}`)
|
|
232
|
+
if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
|
|
233
|
+
this.operations.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})};`)
|
|
213
234
|
this.operations.names.push(name)
|
|
214
235
|
}
|
|
215
236
|
|
|
@@ -239,10 +260,11 @@ class SourceFile extends File {
|
|
|
239
260
|
* @param {string} fq - fully qualified name of the enum (entity name within CSN)
|
|
240
261
|
* @param {string} name - local name of the enum
|
|
241
262
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
263
|
+
* @param {string[]} doc - the enum docs
|
|
242
264
|
*/
|
|
243
|
-
addEnum(fq, name, kvs) {
|
|
265
|
+
addEnum(fq, name, kvs, doc) {
|
|
244
266
|
this.enums.data.push({ name, fq, kvs })
|
|
245
|
-
printEnum(this.enums.buffer, name, kvs)
|
|
267
|
+
printEnum(this.enums.buffer, name, kvs, {}, doc)
|
|
246
268
|
}
|
|
247
269
|
|
|
248
270
|
/**
|
|
@@ -251,6 +273,7 @@ class SourceFile extends File {
|
|
|
251
273
|
* @param {string} entityFqName - name of the entity the enum is attached to with namespace
|
|
252
274
|
* @param {string} propertyName - property to which the enum is attached.
|
|
253
275
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
276
|
+
* @param {string[]} doc - the enum docs
|
|
254
277
|
* If given, the enum is considered to be an inline definition of an enum.
|
|
255
278
|
* If not, it is considered to be regular, named enum.
|
|
256
279
|
* @example
|
|
@@ -274,14 +297,16 @@ class SourceFile extends File {
|
|
|
274
297
|
* }
|
|
275
298
|
* ```
|
|
276
299
|
*/
|
|
277
|
-
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs) {
|
|
300
|
+
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, doc=[]) {
|
|
278
301
|
this.enums.data.push({
|
|
279
302
|
name: `${entityCleanName}.${propertyName}`,
|
|
280
303
|
property: propertyName,
|
|
281
304
|
kvs,
|
|
282
305
|
fq: `${entityCleanName}.${propertyName}`
|
|
283
306
|
})
|
|
284
|
-
|
|
307
|
+
const entityProxy = this.entityProxies[entityCleanName] ?? (this.entityProxies[entityCleanName] = [])
|
|
308
|
+
entityProxy.push(propertyName)
|
|
309
|
+
printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
|
|
285
310
|
}
|
|
286
311
|
|
|
287
312
|
/**
|
|
@@ -363,11 +388,16 @@ class SourceFile extends File {
|
|
|
363
388
|
*/
|
|
364
389
|
getImports() {
|
|
365
390
|
const buffer = new Buffer()
|
|
391
|
+
if (this.services.names.length) {
|
|
392
|
+
// currently only needed to extend cds.Service and would trigger unused-variable-errors in strict configs
|
|
393
|
+
buffer.add('import cds from \'@sap/cds\'') // TODO should go to visitor#printService, but can't express this as Path
|
|
394
|
+
}
|
|
366
395
|
for (const imp of Object.values(this.imports)) {
|
|
367
396
|
if (!imp.isCwd(this.path.asDirectory())) {
|
|
368
397
|
buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
|
|
369
398
|
}
|
|
370
399
|
}
|
|
400
|
+
buffer.add('') // empty line after imports
|
|
371
401
|
return buffer
|
|
372
402
|
}
|
|
373
403
|
|
|
@@ -394,31 +424,92 @@ class SourceFile extends File {
|
|
|
394
424
|
namespaces.join() // needs to be after classes for possible declaration merging
|
|
395
425
|
].filter(Boolean).join('\n')
|
|
396
426
|
}
|
|
427
|
+
#getEntityProxyFunctionExport() {
|
|
428
|
+
return `module.exports.createEntityProxy = ${proxyAccessFunction}`
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Returns boilerplate code for `index.js` files
|
|
432
|
+
* - `useEntitiesProxy = true` -> import `createEntityProxy` function for entity proxy
|
|
433
|
+
* - `useEntitiesProxy = false` -> retrieve entities via `cds.entities(namespace)`
|
|
434
|
+
* @returns {string[]}
|
|
435
|
+
*/
|
|
436
|
+
#getJSExportBoilerplate() {
|
|
437
|
+
const namespace = this.path.asNamespace()
|
|
397
438
|
|
|
439
|
+
const boilerplate = [AUTO_GEN_NOTE]
|
|
440
|
+
if (this.options.useEntitiesProxy) {
|
|
441
|
+
if (namespace === '_') {
|
|
442
|
+
boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
|
|
443
|
+
} else {
|
|
444
|
+
boilerplate.push(`const { createEntityProxy } = require('${new Path(['_']).asDirectory({relative: this.path.asDirectory()})}')`)
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
boilerplate.push(
|
|
448
|
+
'const cds = require(\'@sap/cds\')',
|
|
449
|
+
`const csn = cds.entities('${namespace}')`
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
return boilerplate
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Returns RHS for entity `module.exports` assignments
|
|
456
|
+
* - `useEntitiesProxy = true` -> use function calls to create `Proxy` objects
|
|
457
|
+
* - `useEntitiesProxy = false` -> access entity from CSN directly
|
|
458
|
+
* @param {string} singular - singular name of entity
|
|
459
|
+
* @param {string} original - original name of entity
|
|
460
|
+
* @returns {{singularRhs: string, pluralRhs: string}}
|
|
461
|
+
*/
|
|
462
|
+
#getEntityExportsRhs(singular, original) {
|
|
463
|
+
if (this.options.useEntitiesProxy) {
|
|
464
|
+
const namespace = this.path.asNamespace()
|
|
465
|
+
// determine the custom properties for the proxy function call
|
|
466
|
+
const customProps = this.entityProxies[singular] ?? []
|
|
467
|
+
let customPropsStr = customProps.length ? `, customProps: ${JSON.stringify(customProps)}` : ''
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
|
|
471
|
+
pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
return {
|
|
475
|
+
singularRhs: `{ is_singular: true, __proto__: csn.${original} }`,
|
|
476
|
+
pluralRhs: `csn.${original}`
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
398
480
|
toJSExports() {
|
|
399
|
-
return
|
|
481
|
+
return this.#getJSExportBoilerplate() // boilerplate
|
|
400
482
|
.concat(
|
|
401
483
|
// FIXME: move stringification of service into own module
|
|
402
|
-
this.services.names.
|
|
484
|
+
this.services.names.flatMap(name => {
|
|
485
|
+
const nameSimple = name.split('.').pop()
|
|
486
|
+
return [
|
|
487
|
+
'// service',
|
|
488
|
+
`const ${nameSimple} = { name: '${name}' }`,
|
|
489
|
+
`module.exports = ${nameSimple}`, // there should be only one and must be the first
|
|
490
|
+
`module.exports.${nameSimple} = ${nameSimple}`
|
|
491
|
+
]
|
|
492
|
+
})
|
|
493
|
+
)
|
|
403
494
|
.concat(this.inflections
|
|
404
495
|
// sorting the entries based on the number of dots in their singular.
|
|
405
496
|
// that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
|
|
406
497
|
// "module.exports.Books" is defined before "module.exports.Books.text"
|
|
407
498
|
.sort(([a], [b]) => a.split('.').length - b.split('.').length)
|
|
408
499
|
.flatMap(([singular, plural, original]) => {
|
|
409
|
-
const
|
|
500
|
+
const { singularRhs, pluralRhs } = this.#getEntityExportsRhs(singular, original)
|
|
501
|
+
|
|
502
|
+
const exports = [`// ${original}`, `module.exports.${singular} = ${singularRhs}`]
|
|
410
503
|
if (!/Array<.*>/.test(plural) && plural !== original) {
|
|
411
504
|
// FIXME: this is a hack to support CDS types that will produce "Array<MyType>" as plural, which we do not want as export in the index.js files
|
|
412
|
-
exports.push(`module.exports.${plural} =
|
|
505
|
+
exports.push(`module.exports.${plural} = ${pluralRhs}`)
|
|
413
506
|
}
|
|
414
507
|
// FIXME: we currently produce at most 3 entries.
|
|
415
508
|
// This could be an issue when the user re-used the original name in a @singular/@plural annotation.
|
|
416
509
|
// Seems unlikely, but we have to eliminate the original entry if users start running into this.
|
|
417
510
|
if (singular !== original) {
|
|
418
511
|
// do not do the is_singular spiel if the original name is used for the plural
|
|
419
|
-
const rhs = plural === original
|
|
420
|
-
? `csn.${original}`
|
|
421
|
-
: `{ is_singular: true, __proto__: csn.${original} }`
|
|
512
|
+
const rhs = plural === original ? pluralRhs : singularRhs
|
|
422
513
|
exports.push(`module.exports.${original} = ${rhs}`)
|
|
423
514
|
}
|
|
424
515
|
return exports
|
|
@@ -606,6 +697,13 @@ class FileRepository {
|
|
|
606
697
|
/** @type {{[key:string]: SourceFile}} */
|
|
607
698
|
#files = {}
|
|
608
699
|
|
|
700
|
+
/**
|
|
701
|
+
* @param {FileOptions} options - options to control file
|
|
702
|
+
*/
|
|
703
|
+
constructor(options) {
|
|
704
|
+
this.options = options
|
|
705
|
+
}
|
|
706
|
+
|
|
609
707
|
/**
|
|
610
708
|
* @param {string} name - file name
|
|
611
709
|
* @param {SourceFile} file - the file
|
|
@@ -622,7 +720,7 @@ class FileRepository {
|
|
|
622
720
|
*/
|
|
623
721
|
getNamespaceFile(path) {
|
|
624
722
|
const key = path instanceof Path ? path.asNamespace() : path
|
|
625
|
-
return (this.#files[key] ??= new SourceFile(path))
|
|
723
|
+
return (this.#files[key] ??= new SourceFile(path, this.options))
|
|
626
724
|
}
|
|
627
725
|
|
|
628
726
|
/**
|
|
@@ -58,6 +58,15 @@ class Resolver {
|
|
|
58
58
|
return this.existsInCsn(fq) || Boolean(this.builtinResolver.resolveBuiltin(fq))
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {EntityCSN} type - a CSN type
|
|
63
|
+
* @returns {boolean} whether the type is configured to be optional
|
|
64
|
+
*/
|
|
65
|
+
isOptional(type) {
|
|
66
|
+
// TODO temporary solution to determine optional parameters. Align w/ compiler/importer.
|
|
67
|
+
return Object.keys(type).some(k => k.startsWith('@Core.OptionalParameter'))
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Returns all libraries that have been referenced at least once.
|
|
63
72
|
* @returns {Library[]}
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export module resolver {
|
|
|
8
8
|
|
|
9
9
|
export type EntityCSN = {
|
|
10
10
|
actions?: OperationCSN[],
|
|
11
|
+
operations?: OperationCSN[],
|
|
11
12
|
cardinality?: { max?: '*' | string }
|
|
12
13
|
compositions?: { target: string }[]
|
|
13
14
|
doc?: string,
|
|
@@ -118,6 +119,7 @@ export module visitor {
|
|
|
118
119
|
export type CompileParameters = {
|
|
119
120
|
outputDirectory: string,
|
|
120
121
|
logLevel: number,
|
|
122
|
+
useEntitiesProxy: boolean,
|
|
121
123
|
jsConfigPath?: string,
|
|
122
124
|
inlineDeclarations: 'flat' | 'structured',
|
|
123
125
|
propertiesOptional: boolean,
|
|
@@ -132,6 +134,10 @@ export module visitor {
|
|
|
132
134
|
* `inlineDeclarations = 'flat'` -> @see {@link inline.FlatInlineDeclarationResolver}
|
|
133
135
|
*/
|
|
134
136
|
inlineDeclarations: 'flat' | 'structured',
|
|
137
|
+
/**
|
|
138
|
+
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
|
139
|
+
*/
|
|
140
|
+
useEntitiesProxy: boolean
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
export type Inflection = {
|
|
@@ -143,8 +149,18 @@ export module visitor {
|
|
|
143
149
|
export type Context = {
|
|
144
150
|
entity: string
|
|
145
151
|
}
|
|
152
|
+
|
|
153
|
+
export type ParamInfo = {
|
|
154
|
+
name: string,
|
|
155
|
+
modifier: '' | '?',
|
|
156
|
+
type: string,
|
|
157
|
+
doc?: string
|
|
158
|
+
}
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
export module file {
|
|
149
162
|
export type Namespace = Object<string, Buffer>
|
|
163
|
+
export type FileOptions = {
|
|
164
|
+
useEntitiesProxy: boolean
|
|
165
|
+
}
|
|
150
166
|
}
|
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 } = 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')
|
|
@@ -29,6 +29,7 @@ const { getPropertyModifiers } = require('./components/property')
|
|
|
29
29
|
const defaults = {
|
|
30
30
|
// FIXME: add defaults for remaining parameters
|
|
31
31
|
propertiesOptional: true,
|
|
32
|
+
useEntitiesProxy: false,
|
|
32
33
|
inlineDeclarations: 'flat'
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -63,7 +64,9 @@ class Visitor {
|
|
|
63
64
|
this.entityRepository = new EntityRepository(this.resolver)
|
|
64
65
|
|
|
65
66
|
/** @type {FileRepository} */
|
|
66
|
-
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
|
|
67
70
|
this.fileRepository.add(baseDefinitions.path.asNamespace(), baseDefinitions)
|
|
68
71
|
this.inlineDeclarationResolver =
|
|
69
72
|
this.options.inlineDeclarations === 'structured'
|
|
@@ -148,7 +151,8 @@ class Visitor {
|
|
|
148
151
|
returns: action.returns
|
|
149
152
|
? this.resolver.resolveAndRequire(action.returns, file).typeName
|
|
150
153
|
: 'any',
|
|
151
|
-
kind: action.kind
|
|
154
|
+
kind: action.kind,
|
|
155
|
+
doc: docify(action.doc)
|
|
152
156
|
})), '}'
|
|
153
157
|
) // end of actions
|
|
154
158
|
} else {
|
|
@@ -168,8 +172,7 @@ class Visitor {
|
|
|
168
172
|
* @param {{cleanName?: string}} options - additional options
|
|
169
173
|
*/
|
|
170
174
|
#aspectify(fq, entity, buffer, options = {}) {
|
|
171
|
-
const info = this.entityRepository.
|
|
172
|
-
if (!info) throw new Error(`could not resolve entity ${fq}`)
|
|
175
|
+
const info = this.entityRepository.getByFqOrThrow(fq)
|
|
173
176
|
const clean = options?.cleanName ?? info.withoutNamespace
|
|
174
177
|
const { namespace } = info
|
|
175
178
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
@@ -257,10 +260,16 @@ class Visitor {
|
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
if ('kind' in entity) {
|
|
264
|
+
buffer.addIndented([`static readonly kind: 'entity' | 'type' | 'aspect' = '${entity.kind}';`])
|
|
265
|
+
}
|
|
266
|
+
|
|
260
267
|
buffer.addIndented(() => {
|
|
261
268
|
for (const e of enums) {
|
|
269
|
+
const eDoc = docify(e.doc)
|
|
270
|
+
eDoc.forEach(d => { buffer.add(d) })
|
|
262
271
|
buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
|
|
263
|
-
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
|
|
272
|
+
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
|
|
264
273
|
}
|
|
265
274
|
this.#printStaticActions(entity, buffer, ancestorInfos, file)
|
|
266
275
|
})
|
|
@@ -269,6 +278,7 @@ class Visitor {
|
|
|
269
278
|
|
|
270
279
|
// CLASS WITH ADDED ASPECTS
|
|
271
280
|
file.addImport(baseDefinitions.path)
|
|
281
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
272
282
|
buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
273
283
|
this.contexts.pop()
|
|
274
284
|
}
|
|
@@ -292,9 +302,7 @@ class Visitor {
|
|
|
292
302
|
* @param {string} content - the content to set the name property to
|
|
293
303
|
*/
|
|
294
304
|
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
|
|
305
|
+
const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFqOrThrow(fq)
|
|
298
306
|
const file = this.fileRepository.getNamespaceFile(ns)
|
|
299
307
|
let { singular, plural } = inflection
|
|
300
308
|
|
|
@@ -330,7 +338,6 @@ class Visitor {
|
|
|
330
338
|
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
|
|
331
339
|
// edge case: @singular annotation present. singular4 will take care of that.
|
|
332
340
|
file.addInflection(util.singular4(entity, true), plural, clean)
|
|
333
|
-
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
334
341
|
|
|
335
342
|
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
|
|
336
343
|
const target = isProjection(entity) || isView(entity)
|
|
@@ -357,6 +364,7 @@ class Visitor {
|
|
|
357
364
|
// so it can get passed as value to CQL functions.
|
|
358
365
|
const additionalProperties = this.#staticClassContents(singular, entity)
|
|
359
366
|
additionalProperties.push('$count?: number')
|
|
367
|
+
docify(entity.doc).forEach(d => { buffer.add(d) })
|
|
360
368
|
buffer.add(`export class ${plural} extends Array<${singular}> {${additionalProperties.join('\n')}}`)
|
|
361
369
|
buffer.add(overrideNameProperty(plural, entity.name))
|
|
362
370
|
}
|
|
@@ -369,18 +377,18 @@ class Visitor {
|
|
|
369
377
|
* Also filters out parameters that indicate a binding parameter ({@link https://cap.cloud.sap/docs/releases/jan23#simplified-syntax-for-binding-parameters}).
|
|
370
378
|
* @param {{[key:string]: EntityCSN}} params - parameter list as found in CSN.
|
|
371
379
|
* @param {SourceFile} file - source file relative to which the parameter types should be resolved.
|
|
372
|
-
* @returns {[
|
|
380
|
+
* @returns {import('./typedefs').visitor.ParamInfo[]} tuple of name, modifier, type and doc.
|
|
373
381
|
*/
|
|
374
382
|
#stringifyFunctionParams(params, file) {
|
|
375
|
-
return params
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
}))
|
|
384
392
|
}
|
|
385
393
|
|
|
386
394
|
/**
|
|
@@ -408,21 +416,27 @@ class Visitor {
|
|
|
408
416
|
*/
|
|
409
417
|
#printOperation(fq, operation, kind) {
|
|
410
418
|
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
|
|
419
|
+
const { namespace } = this.entityRepository.getByFqOrThrow(fq)
|
|
414
420
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
415
421
|
const params = this.#stringifyFunctionParams(operation.params, file)
|
|
416
422
|
const returnType = operation.returns
|
|
417
423
|
? this.resolver.resolveAndRequire(operation.returns, file)
|
|
418
424
|
: { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
|
|
419
|
-
|
|
425
|
+
let returns = this.inlineDeclarationResolver.getPropertyDatatype(
|
|
420
426
|
returnType,
|
|
421
427
|
returnType.typeInfo.isArray
|
|
422
428
|
? returnType.typeName
|
|
423
429
|
: returnType.typeInfo.inflection.singular
|
|
424
430
|
)
|
|
425
|
-
|
|
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})
|
|
426
440
|
}
|
|
427
441
|
|
|
428
442
|
/**
|
|
@@ -431,15 +445,13 @@ class Visitor {
|
|
|
431
445
|
*/
|
|
432
446
|
#printType(fq, type) {
|
|
433
447
|
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
|
|
448
|
+
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
437
449
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
438
450
|
// skip references to enums.
|
|
439
451
|
// "Base" enums will always have a builtin type (don't skip those).
|
|
440
452
|
// A type referencing an enum E will be considered an enum itself and have .type === E (skip).
|
|
441
453
|
if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
|
|
442
|
-
file.addEnum(fq, entityName, csnToEnumPairs(type))
|
|
454
|
+
file.addEnum(fq, entityName, csnToEnumPairs(type), docify(type.doc))
|
|
443
455
|
} else {
|
|
444
456
|
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
|
|
445
457
|
// alias
|
|
@@ -454,9 +466,7 @@ class Visitor {
|
|
|
454
466
|
*/
|
|
455
467
|
#printAspect(fq, aspect) {
|
|
456
468
|
LOG.debug(`Printing aspect ${fq}`)
|
|
457
|
-
const
|
|
458
|
-
if (!info) throw new Error(`could not resolve aspect ${fq}`)
|
|
459
|
-
const { namespace, entityName, inflection } = info
|
|
469
|
+
const { namespace, entityName, inflection } = this.entityRepository.getByFqOrThrow(fq)
|
|
460
470
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
461
471
|
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
462
472
|
// Still, when using them as mixins for a class, they need to already be defined.
|
|
@@ -471,10 +481,7 @@ class Visitor {
|
|
|
471
481
|
* @param {EntityCSN} event - CSN
|
|
472
482
|
*/
|
|
473
483
|
#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
|
|
484
|
+
const { namespace, entityName } = this.entityRepository.getByFqOrThrow(fq)
|
|
478
485
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
479
486
|
file.addEvent(entityName, fq)
|
|
480
487
|
const buffer = file.events.buffer
|
|
@@ -492,16 +499,25 @@ class Visitor {
|
|
|
492
499
|
|
|
493
500
|
/**
|
|
494
501
|
* @param {string} fq - fully qualified name of the service
|
|
495
|
-
* @param {EntityCSN} service - CSN
|
|
502
|
+
* @param {import('./typedefs').resolver.EntityCSN} service - CSN
|
|
496
503
|
*/
|
|
497
504
|
#printService(fq, service) {
|
|
498
505
|
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
|
|
506
|
+
const { namespace } = this.entityRepository.getByFqOrThrow(fq)
|
|
502
507
|
const file = this.fileRepository.getNamespaceFile(namespace)
|
|
503
|
-
|
|
504
|
-
|
|
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('')
|
|
505
521
|
file.addService(service.name)
|
|
506
522
|
}
|
|
507
523
|
|
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",
|
|
@@ -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"
|