@cap-js/cds-typer 0.4.0 → 0.6.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 +31 -1
- package/README.md +6 -167
- package/lib/cli.js +32 -26
- package/lib/compile.js +5 -0
- package/lib/components/inline.js +3 -3
- package/lib/components/resolver.js +27 -11
- package/lib/file.js +41 -12
- package/lib/visitor.js +32 -8
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,37 @@ 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.6.1 - TBD
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Version 0.6.0 - 2023-08-07
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Support for `event` syntax
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Initialise bound actions with stubs to support `"strict":true` in _tsconfig.json_
|
|
23
|
+
- Add leading underscore to appease `noUnusedParameters` in strict tsconfigs
|
|
24
|
+
- No longer inflect `type` definitions when they are referenced within entities or other type definitions
|
|
25
|
+
|
|
26
|
+
## Version 0.5.0 - 2023-07-25
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Facilitate strict property checks. Note: `checkJs: true` must be present in the project's _jsconfig.json_ or _tsconfig.json_ respectively for this feature to become effective
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- Support for `array of` syntax
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Generate `string` type for date-related types in CDS definitions
|
|
36
|
+
- Generate `Buffer | string` type for the CDS type `LargeBinary`
|
|
37
|
+
|
|
8
38
|
|
|
9
39
|
## Version 0.4.0 - 2023-07-06
|
|
10
40
|
### Added
|
package/README.md
CHANGED
|
@@ -1,174 +1,13 @@
|
|
|
1
1
|
# CDS type generator for JavaScript
|
|
2
2
|
|
|
3
|
+
[](https://api.reuse.software/info/github.com/cap-js/cds-typer)
|
|
4
|
+

|
|
5
|
+
|
|
3
6
|
## About this project
|
|
4
7
|
|
|
5
8
|
Generates `.ts` files for a CDS model to receive code completion in VS Code.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
## Requirements and Setup
|
|
9
|
-
This project is [available as `@cap-js/cds-typer`](https://www.npmjs.com/package/@cap-js/cds-typer) as NPM package.
|
|
10
|
-
|
|
11
|
-
### Usage
|
|
12
|
-
The type generator can either be used as a standalone tool, or as part of of the [CDS VSCode-Extension](https://www.npmjs.com/package/@sap/vscode-cds).
|
|
13
|
-
|
|
14
|
-
#### Standalone CLI
|
|
15
|
-
Assuming you have the following CDS project structure:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
/home/
|
|
19
|
-
├── mybookshop/
|
|
20
|
-
│ ├── db/
|
|
21
|
-
│ │ └── schema.cds
|
|
22
|
-
│ ├── srv/
|
|
23
|
-
│ │ └── service.js
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
a typical workflow to generate types for your CDS project could look something like this:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
npx @cap-js/cds-typer /home/mybookshop/db/schema.cds --outputDirectory /home/mybookshop/@types
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
You would then end up with a directory `@types`, which contains your entities and their accompanying types in a directory structure. The directory structure directly reflects the namespaces you have defined your entities in. They have to be imported in any JavaScript-based service handlers you want to have type support in and can replace calls to `cds.entities(...)`:
|
|
33
|
-
|
|
34
|
-
```js
|
|
35
|
-
// srv/service.js
|
|
36
|
-
const { Books } = require('my.bookshop')
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
becomes
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
// srv/service.js
|
|
43
|
-
const { Books } = require('../@types/mybookshop')
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
From that point on you should receive code completion from the type system for `Books`.
|
|
47
|
-
|
|
48
|
-
_Note:_ the above command generates types for the model contained within the mentioned `schema.cds` file. If you have multiple `.cds` files that are included via `using` statements by `schema.cds`, then those files will also be included in the type generation process. If you have `.cds` files that are _not_ in some way included in `schema.cds`, you have to explicitly pass those as positional argument as well, if you want types for them.
|
|
49
|
-
|
|
50
|
-
_cds-typer_ comes with rudimentary CLI support and a few command line options:
|
|
51
|
-
|
|
52
|
-
- `--help`: prints all available parameters.
|
|
53
|
-
- `--outputDirectory`: specifies the root directory where all generated files should be put. Defaults to the CWD.
|
|
54
|
-
- `--jsConfigPath`: specifies the path to the `jsconfig.json` file to generate. Usually your project's root directory. If specified, a config file is created that restricts the usage of types even further:
|
|
55
|
-
|
|
56
|
-
```js
|
|
57
|
-
// generated .ts file
|
|
58
|
-
class Book {
|
|
59
|
-
title: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// some hook in your service
|
|
63
|
-
SELECT(Books, b => {
|
|
64
|
-
b.title // 👍 no problem, property exists
|
|
65
|
-
b.numberOfPages // ❌ property does not exist
|
|
66
|
-
})
|
|
67
|
-
```
|
|
68
|
-
With the generated config in place, the language server will display an error, telling you that `numberOfPages` does not exist in this context. Without the config it would just infer it as `any`.
|
|
69
|
-
|
|
70
|
-
- `--loglevel`: minimum log level that should be printed. Defaults to `NONE`. Available log levels roughly follow [Microsoft's dotnet log levels](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=dotnet-plat-ext-6.0):
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
TRACE
|
|
74
|
-
DEBUG
|
|
75
|
-
INFO
|
|
76
|
-
WARNING
|
|
77
|
-
ERROR
|
|
78
|
-
CRITICAL
|
|
79
|
-
NONE
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The utility expects (at least) one path to a `.cds` file as positional parameter which serves as entry point to the model in question, e.g.:
|
|
83
|
-
|
|
84
|
-
```sh
|
|
85
|
-
npx @cap-js/cds-typer ./path/to/my/model/model.cds --outputDirectory /tmp/
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Note that you can also pass multiple paths or `"*"` as glob pattern (with quotes to circumvent expansion by the shell). This passes the pattern on to the compiler where the [regular resolve strategy](https://cap.cloud.sap/docs/node.js/cds-compile?q=compiler#cds-resolve) is used.
|
|
89
|
-
|
|
90
|
-
#### From VSCode
|
|
91
|
-
Installing the [CDS VSCode Extension](https://www.npmjs.com/package/@sap/vscode-cds) also adds support for generating types for your model from within VSCode. Adding the appropriate facet to your project via `cds add typer` (and installing the added dependencies thereafter) allows you to simply hit save on any `.cds` file that is part of your model to trigger the generation process.
|
|
92
|
-
#### Programmatically
|
|
93
|
-
The main API for using _cds-typer_ within another project is contained in [`compile.js`](https://github.tools.sap/cap/cds-typer/blob/master/lib/compile.js), specifically:
|
|
94
|
-
|
|
95
|
-
- `compileFromFile(…)` to parse a `.cds` file. This involves compiling it to CSN first.
|
|
96
|
-
- `compileFromCSN(…)` to directly compile from CSN object. This is useful when you already have your CSN available as part of a tool chain. ⚠️ **WARNING**: the application of `cdstyper` may be impure, meaning that it _could_ alter the provided CSN. If you use the typer this way, you may want to apply it as last step of your tool chain.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### Features
|
|
100
|
-
#### Plural Types
|
|
101
|
-
While CDS encourages the use of plural form for defined entities, their OOP equivalent classes are usually named in singular. _cds-typer_ automatically transforms entity names to singular and adds the plural form for arrays:
|
|
102
|
-
|
|
103
|
-
```cds
|
|
104
|
-
entity Books : cuid {
|
|
105
|
-
…
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
becomes
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
class Book {
|
|
113
|
-
…
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
class Books extends Array<Book> {}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
If you need to customise the singular or plural form, or if your entities are already in singular form, you can do so using annotations:
|
|
120
|
-
|
|
121
|
-
```cds
|
|
122
|
-
@singular: 'Mouse'
|
|
123
|
-
entity Mice {}
|
|
124
|
-
|
|
125
|
-
@plural: 'SomeListList'
|
|
126
|
-
entity SomeList {}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
results in
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
class Mouse { … }
|
|
133
|
-
class Mice extends Array<Mouse> { … }
|
|
134
|
-
|
|
135
|
-
class SomeList { … }
|
|
136
|
-
class SomeListList extends Array<SomeList> { … }
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Relation to _cds2types_
|
|
140
|
-
This project is inspired by the existing [_cds2types_](https://github.com/mrbandler/cds2types), but differs in a few aspects:
|
|
141
|
-
|
|
142
|
-
#### Reworked Imports
|
|
143
|
-
Instead of one monolithic `.d.ts` file containing all entities in nested namespaces, multiple files are generated where each namespace is represented by a directory structure. This facilicates simpler imports in a more Java-esque style:
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
const types = require('./cds2types/compiled.d.ts')
|
|
147
|
-
|
|
148
|
-
console.log(types.sap.cap.bookshop.Books) // a class
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
becomes
|
|
152
|
-
|
|
153
|
-
```js
|
|
154
|
-
const { Books } = require('./cds-typer/sap/cap/bookshop')
|
|
155
|
-
|
|
156
|
-
console.log(Books) // the same class
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
#### Usable in Javascript Projects
|
|
160
|
-
Generated code is usable from within plain Javascript projects. The code generated by _cds2types_ would represent each cds-entity as an interface, which are not visible to Javascript projects. _cds-typer_ uses classes instead.
|
|
161
|
-
|
|
162
|
-
#### Faster
|
|
163
|
-
_cds2types_ takes a detour to create a Typescript AST first and then print out the formatted source files. _cds-typer_ directly walks the linked CSN and creates strings on the fly. Also, file operations are `async`. These two changes speed up _cds-typer_ by around one to two orders of magnitude compared to _cds2types_.
|
|
164
|
-
|
|
165
|
-
#### Small Footprint
|
|
166
|
-
_cds-typer_ tries to keep its dependency footprint as small as possible. Libraries like `typescript` are only needed as dev dependencies.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
10
|
+
Exhaustive documentation can be found on [CAPire](https://cap.cloud.sap/docs/tools/cds-typer).
|
|
172
11
|
|
|
173
12
|
## Support, Feedback, Contributing
|
|
174
13
|
|
|
@@ -176,8 +15,8 @@ This project is open to feature requests/suggestions, bug reports etc. via [GitH
|
|
|
176
15
|
|
|
177
16
|
## Code of Conduct
|
|
178
17
|
|
|
179
|
-
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
|
|
18
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/cap-js/.github/blob/main/CODE_OF_CONDUCT.md) at all times.
|
|
180
19
|
|
|
181
20
|
## Licensing
|
|
182
21
|
|
|
183
|
-
Copyright 2022-2022 SAP SE or an SAP affiliate company and cds-
|
|
22
|
+
Copyright 2022-2022 SAP SE or an SAP affiliate company and cds-typer contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/cds-dts-generator).
|
package/lib/cli.js
CHANGED
|
@@ -6,80 +6,82 @@ const { compileFromFile } = require('./compile')
|
|
|
6
6
|
const { parseCommandlineArgs } = require('./util')
|
|
7
7
|
const { Levels } = require('./logging')
|
|
8
8
|
const path = require('path')
|
|
9
|
+
const { EOL } = require('node:os')
|
|
9
10
|
|
|
11
|
+
const EOL2 = EOL + EOL
|
|
10
12
|
const toolName = 'cds-typer'
|
|
11
13
|
|
|
12
14
|
const flags = {
|
|
13
|
-
// FIXME: remove asap
|
|
14
|
-
rootDir: {
|
|
15
|
-
desc: '[DEPRICATED] use outputDirectory instead',
|
|
16
|
-
default: './',
|
|
17
|
-
},
|
|
18
15
|
outputDirectory: {
|
|
19
|
-
desc: '
|
|
16
|
+
desc: 'Root directory to write the generated files to.',
|
|
20
17
|
default: './',
|
|
18
|
+
type: 'string'
|
|
21
19
|
},
|
|
22
20
|
help: {
|
|
23
|
-
desc: '
|
|
21
|
+
desc: 'This text.',
|
|
24
22
|
},
|
|
25
23
|
logLevel: {
|
|
26
|
-
desc: `
|
|
24
|
+
desc: `Minimum log level that is printed.`,
|
|
27
25
|
allowed: Object.keys(Levels),
|
|
28
26
|
default: Object.keys(Levels).at(-1),
|
|
29
27
|
},
|
|
30
28
|
jsConfigPath: {
|
|
31
|
-
desc: `Path to where the jsconfig.json should be written
|
|
29
|
+
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.`,
|
|
30
|
+
type: 'string'
|
|
32
31
|
},
|
|
33
32
|
version: {
|
|
34
|
-
desc: '
|
|
33
|
+
desc: 'Prints the version of this tool.'
|
|
35
34
|
},
|
|
36
35
|
inlineDeclarations: {
|
|
37
|
-
desc:
|
|
36
|
+
desc: `Whether to resolve inline type declarations${EOL}flat: (x_a, x_b, ...)${EOL}or structured: (x: {a, b}).`,
|
|
38
37
|
allowed: ['flat', 'structured'],
|
|
39
38
|
default: 'structured'
|
|
40
39
|
},
|
|
41
40
|
propertiesOptional: {
|
|
42
|
-
desc:
|
|
41
|
+
desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
|
|
43
42
|
allowed: ['true', 'false'],
|
|
44
43
|
default: 'true'
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
const hint = () => console.log('Missing or invalid parameter(s). Call with --help for more details.')
|
|
48
|
+
const indent = (s, indentation) => s.split(EOL).map(line => `${indentation}${line}`).join(EOL)
|
|
49
49
|
|
|
50
|
-
const help = () =>
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
'Additionaly, you can use the following parameters:\n' +
|
|
50
|
+
const help = () => `SYNOPSIS${EOL2}` +
|
|
51
|
+
indent(`cds-typer [cds file | "*"]`, ' ') + EOL2 +
|
|
52
|
+
indent(`Generates type information based on a CDS model.${EOL}Call with at least one positional parameter pointing${EOL}to the (root) CDS file you want to compile.`, ' ') + EOL2 +
|
|
53
|
+
`OPTIONS${EOL2}` +
|
|
55
54
|
Object.entries(flags)
|
|
56
55
|
.sort()
|
|
57
56
|
.map(([key, value]) => {
|
|
58
|
-
let s = `--${key}
|
|
57
|
+
let s = indent(`--${key}`, ' ')
|
|
59
58
|
if (value.allowed) {
|
|
60
|
-
s +=
|
|
59
|
+
s += `: <${value.allowed.join(' | ')}>`
|
|
60
|
+
} else if (value.type) {
|
|
61
|
+
s += `: <${value.type}>`
|
|
61
62
|
}
|
|
62
63
|
if (value.default) {
|
|
63
|
-
s +=
|
|
64
|
+
s += EOL
|
|
65
|
+
s += indent(`(default: ${value.default})`, ' ')
|
|
64
66
|
}
|
|
67
|
+
s += `${EOL2}${indent(value.desc, ' ')}`
|
|
65
68
|
return s
|
|
66
69
|
}
|
|
67
|
-
).join(
|
|
68
|
-
)
|
|
70
|
+
).join(EOL2)
|
|
69
71
|
|
|
70
|
-
const version = () =>
|
|
72
|
+
const version = () => require('../package.json').version
|
|
71
73
|
|
|
72
74
|
const main = async (args) => {
|
|
73
75
|
if ('help' in args.named) {
|
|
74
|
-
help()
|
|
76
|
+
console.log(help())
|
|
75
77
|
process.exit(0)
|
|
76
78
|
}
|
|
77
79
|
if ('version' in args.named) {
|
|
78
|
-
version()
|
|
80
|
+
console.log(version())
|
|
79
81
|
process.exit(0)
|
|
80
82
|
}
|
|
81
83
|
if (args.positional.length === 0) {
|
|
82
|
-
hint()
|
|
84
|
+
console.log(hint())
|
|
83
85
|
process.exit(1)
|
|
84
86
|
}
|
|
85
87
|
if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
|
|
@@ -98,3 +100,7 @@ const main = async (args) => {
|
|
|
98
100
|
if (require.main === module) {
|
|
99
101
|
main(parseCommandlineArgs(process.argv.slice(2), flags))
|
|
100
102
|
}
|
|
103
|
+
|
|
104
|
+
function helpToCapire() {
|
|
105
|
+
|
|
106
|
+
}
|
package/lib/compile.js
CHANGED
|
@@ -8,11 +8,16 @@ const { writeout } = require('./file')
|
|
|
8
8
|
const { Logger } = require('./logging')
|
|
9
9
|
const { Visitor } = require('./visitor')
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./visitor').CompileParameters} CompileParameters
|
|
13
|
+
*/
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* Writes the accompanying jsconfig.json file to the specified paths.
|
|
13
17
|
* Tries to merge nicely if an existing file is found.
|
|
14
18
|
* @param path {string} filepath to jsconfig.json.
|
|
15
19
|
* @param logger {import('./logging').Logger} logger
|
|
20
|
+
* @private
|
|
16
21
|
*/
|
|
17
22
|
const writeJsConfig = (path, logger) => {
|
|
18
23
|
let values = {
|
package/lib/components/inline.js
CHANGED
|
@@ -142,7 +142,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
142
142
|
flatten(prefix, type) {
|
|
143
143
|
return type.typeInfo.structuredType
|
|
144
144
|
? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v))
|
|
145
|
-
: [`${prefix}
|
|
145
|
+
: [`${prefix}${this.getPropertyTypeSeparator()} ${type.typeName}`]
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
printInlineType(name, type, buffer) {
|
|
@@ -185,7 +185,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
185
185
|
this.printDepth++
|
|
186
186
|
const lineEnding = this.printDepth > 1 ? ',' : statementEnd
|
|
187
187
|
if (type.typeInfo.structuredType) {
|
|
188
|
-
const prefix = name ? `${name}
|
|
188
|
+
const prefix = name ? `${name}${this.getPropertyTypeSeparator()}`: ''
|
|
189
189
|
buffer.add(`${prefix} {`)
|
|
190
190
|
buffer.indent()
|
|
191
191
|
for (const [n, t] of Object.entries(type.typeInfo.structuredType)) {
|
|
@@ -194,7 +194,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
194
194
|
buffer.outdent()
|
|
195
195
|
buffer.add(`}${lineEnding}`)
|
|
196
196
|
} else {
|
|
197
|
-
buffer.add(`${name}
|
|
197
|
+
buffer.add(`${name}${this.getPropertyTypeSeparator()} ${type.typeName}${lineEnding}`)
|
|
198
198
|
}
|
|
199
199
|
this.printDepth--
|
|
200
200
|
return buffer
|
|
@@ -40,7 +40,7 @@ const Builtins = {
|
|
|
40
40
|
String: 'string',
|
|
41
41
|
Binary: 'string',
|
|
42
42
|
LargeString: 'string',
|
|
43
|
-
LargeBinary: 'string',
|
|
43
|
+
LargeBinary: 'Buffer | string',
|
|
44
44
|
Integer: 'number',
|
|
45
45
|
UInt8: 'number',
|
|
46
46
|
Int16: 'number',
|
|
@@ -52,10 +52,11 @@ const Builtins = {
|
|
|
52
52
|
Float: 'number',
|
|
53
53
|
Double: 'number',
|
|
54
54
|
Boolean: 'boolean',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// note: the date-related types _can_ be Date in some cases, but let's start with string
|
|
56
|
+
Date: 'string', // yyyy-mm-dd
|
|
57
|
+
DateTime: 'string', // yyyy-mm-dd + time + TZ (precision: seconds
|
|
58
|
+
Time: 'string',
|
|
59
|
+
Timestamp: 'string', // yyy-mm-dd + time + TZ (ms precision)
|
|
59
60
|
//
|
|
60
61
|
Composition: 'Array',
|
|
61
62
|
Association: 'Array'
|
|
@@ -115,7 +116,7 @@ class Resolver {
|
|
|
115
116
|
let qualifier = parts.join('.')
|
|
116
117
|
while (
|
|
117
118
|
this.csn.definitions[qualifier] &&
|
|
118
|
-
['entity', 'type', 'aspect'].includes(this.csn.definitions[qualifier].kind)
|
|
119
|
+
['entity', 'type', 'aspect', 'event'].includes(this.csn.definitions[qualifier].kind)
|
|
119
120
|
) {
|
|
120
121
|
parts.pop()
|
|
121
122
|
qualifier = parts.join('.')
|
|
@@ -130,6 +131,7 @@ class Resolver {
|
|
|
130
131
|
* - explicit annotation by the user in the CSN
|
|
131
132
|
* - implicitly derived inflection based on simple grammar rules
|
|
132
133
|
* - collisions between singular and plural name (resolved by appending a '_' suffix)
|
|
134
|
+
* - type definitions, which are not inflected
|
|
133
135
|
* - inline type definitions, which don't really have a linguistic plural,
|
|
134
136
|
* but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
|
|
135
137
|
* @param {import('./resolver').TypeResolveInfo} typeInfo information about the type gathered so far.
|
|
@@ -137,6 +139,16 @@ class Resolver {
|
|
|
137
139
|
* @returns {Inflection}
|
|
138
140
|
*/
|
|
139
141
|
inflect(typeInfo, namespace) {
|
|
142
|
+
// TODO: handle builtins here as well?
|
|
143
|
+
// guard: types don't get inflected
|
|
144
|
+
if (typeInfo.csn?.kind === 'type') {
|
|
145
|
+
return {
|
|
146
|
+
singular: typeInfo.plainName,
|
|
147
|
+
plural: typeInfo.plainName,
|
|
148
|
+
typeName: typeInfo.plainName,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
let typeName
|
|
141
153
|
let singular
|
|
142
154
|
let plural
|
|
@@ -212,13 +224,14 @@ class Resolver {
|
|
|
212
224
|
{
|
|
213
225
|
Association: [createToOneAssociation, createToManyAssociation],
|
|
214
226
|
Composition: [createCompositionOfOne, createCompositionOfMany],
|
|
227
|
+
array: [createArrayOf, createArrayOf]
|
|
215
228
|
}[element.constructor.name] ?? []
|
|
216
229
|
|
|
217
230
|
if (toOne && toMany) {
|
|
218
|
-
const target = typeof element.target === 'string' ? { type: element.target } : element.target
|
|
231
|
+
const target = element.items ?? (typeof element.target === 'string' ? { type: element.target } : element.target)
|
|
219
232
|
const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
|
|
220
233
|
typeName =
|
|
221
|
-
cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(
|
|
234
|
+
cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(target) ? 'this' : singular)
|
|
222
235
|
file.addImport(baseDefinitions.path)
|
|
223
236
|
}
|
|
224
237
|
} else {
|
|
@@ -258,8 +271,11 @@ class Resolver {
|
|
|
258
271
|
typeInfo.inflection = this.inflect(typeInfo)
|
|
259
272
|
}
|
|
260
273
|
|
|
261
|
-
|
|
262
|
-
|
|
274
|
+
// add fallback inflection. Mainly needed for array-of with builtin types.
|
|
275
|
+
// (array-of relies on inflection being present, which is not the case in builtin)
|
|
276
|
+
typeInfo.inflection ??= {
|
|
277
|
+
singular: typeName,
|
|
278
|
+
plural: typeName
|
|
263
279
|
}
|
|
264
280
|
|
|
265
281
|
// FIXME: typeName could probably just become part of typeInfo
|
|
@@ -346,8 +362,8 @@ class Resolver {
|
|
|
346
362
|
|
|
347
363
|
// objects and arrays
|
|
348
364
|
if (element?.items) {
|
|
349
|
-
// FIXME: builtin = true? arrays are kinda builtin
|
|
350
365
|
result.isArray = true
|
|
366
|
+
result.isBuiltin = true
|
|
351
367
|
this.resolveType(element.items, file)
|
|
352
368
|
//delete element.items
|
|
353
369
|
} else if (element?.elements && !element?.type) {
|
package/lib/file.js
CHANGED
|
@@ -99,6 +99,8 @@ class SourceFile extends File {
|
|
|
99
99
|
this.imports = {}
|
|
100
100
|
/** @type {Buffer} */
|
|
101
101
|
this.preamble = new Buffer()
|
|
102
|
+
/** @type {{ buffer: Buffer, fqs: {name: string, fq: string}[]}} */
|
|
103
|
+
this.events = { buffer: new Buffer(), fqs: []}
|
|
102
104
|
/** @type {Buffer} */
|
|
103
105
|
this.types = new Buffer()
|
|
104
106
|
/** @type {{ buffer: Buffer, fqs: {name: string, fq: string}[]}} */
|
|
@@ -119,8 +121,24 @@ class SourceFile extends File {
|
|
|
119
121
|
this.inflections = []
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Stringifies a lambda expression.
|
|
126
|
+
* @param {{name: string, parameters: [string, string][], returns: string, initialiser: string}} - name, parameters, return type, and initialiser expression
|
|
127
|
+
* @returns {string} - the stringified lambda
|
|
128
|
+
* @example
|
|
129
|
+
* ```js
|
|
130
|
+
* stringifyLambda({parameters: [['p','T']]} // (p: T) => any
|
|
131
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']]} // f: (p: T) => any
|
|
132
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'} // f: (p: T) => number
|
|
133
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number', initialiser: '_ => 42'} // f: (p: T) => string = _ => 42
|
|
134
|
+
*
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
static stringifyLambda({name, parameters=[], returns='any', initialiser}) {
|
|
138
|
+
const signature = `(${parameters.map(([n, t]) => `${n}: ${t}`).join(', ')}) => ${returns}`
|
|
139
|
+
const prefix = name ? `${name}: `: ''
|
|
140
|
+
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
141
|
+
return prefix + signature + suffix
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
/**
|
|
@@ -139,27 +157,25 @@ class SourceFile extends File {
|
|
|
139
157
|
/**
|
|
140
158
|
* Adds a function definition in form of a arrow function to the file.
|
|
141
159
|
* @param {string} name name of the function
|
|
142
|
-
* @param {{relative: string | undefined, local: boolean, posix: boolean}}
|
|
160
|
+
* @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
|
|
143
161
|
* @param returns the return type of the function
|
|
144
162
|
*/
|
|
145
|
-
addFunction(name,
|
|
163
|
+
addFunction(name, parameters, returns) {
|
|
146
164
|
// FIXME: use different buffers for buffers and actions, or at least rename buffer to the more general category "functions"?
|
|
147
165
|
this.actions.buffer.add("// function")
|
|
148
|
-
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name,
|
|
166
|
+
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
|
|
149
167
|
this.actions.names.push(name)
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
/**
|
|
153
171
|
* Adds an action definition in form of a arrow function to the file.
|
|
154
172
|
* @param {string} name name of the action
|
|
155
|
-
* @param {{relative: string | undefined, local: boolean, posix: boolean}}
|
|
173
|
+
* @param {{relative: string | undefined, local: boolean, posix: boolean}} parameters list of parameters, passed as [name, type] pairs
|
|
156
174
|
* @param returns the return type of the action
|
|
157
175
|
*/
|
|
158
|
-
addAction(name,
|
|
159
|
-
//const ps = params.map(([n, t]) => `${n}: ${t}`).join(', ')
|
|
176
|
+
addAction(name, parameters, returns) {
|
|
160
177
|
this.actions.buffer.add("// action")
|
|
161
|
-
|
|
162
|
-
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name, params, returns)};`)
|
|
178
|
+
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda({name, parameters, returns})};`)
|
|
163
179
|
this.actions.names.push(name)
|
|
164
180
|
}
|
|
165
181
|
|
|
@@ -196,6 +212,7 @@ class SourceFile extends File {
|
|
|
196
212
|
// and a type containing all disctinct values.
|
|
197
213
|
// We can get away with this as TS doesn't feature nominal typing, so the structure
|
|
198
214
|
// is all we care about.
|
|
215
|
+
// FIXME: this really should be in visitor, as File should not contain logic of this kind
|
|
199
216
|
this.enums.fqs.push({ name, fq })
|
|
200
217
|
const bu = this.enums.buffer
|
|
201
218
|
bu.add('// enum')
|
|
@@ -210,7 +227,6 @@ class SourceFile extends File {
|
|
|
210
227
|
bu.add('}')
|
|
211
228
|
bu.add(`export type ${name} = ${[...vals].join(' | ')}`)
|
|
212
229
|
bu.add('')
|
|
213
|
-
|
|
214
230
|
}
|
|
215
231
|
|
|
216
232
|
/**
|
|
@@ -225,6 +241,16 @@ class SourceFile extends File {
|
|
|
225
241
|
this.classNames[clean] = fq
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Adds an event to this file.
|
|
246
|
+
* are supposed to be present in this file.
|
|
247
|
+
* @param {string} name cleaned name of the event
|
|
248
|
+
* @param {string} fq fully qualified name, including the namespace
|
|
249
|
+
*/
|
|
250
|
+
addEvent(name, fq) {
|
|
251
|
+
this.events.fqs.push({ name, fq })
|
|
252
|
+
}
|
|
253
|
+
|
|
228
254
|
/**
|
|
229
255
|
* Adds an import if it does not exist yet.
|
|
230
256
|
* @param {Path} imp qualifier for the namespace to import.
|
|
@@ -289,6 +315,7 @@ class SourceFile extends File {
|
|
|
289
315
|
namespaces.join(),
|
|
290
316
|
this.aspects.join(), // needs to be before classes
|
|
291
317
|
this.classes.join(),
|
|
318
|
+
this.events.buffer.join(),
|
|
292
319
|
this.actions.buffer.join(),
|
|
293
320
|
].filter(Boolean).join('\n')
|
|
294
321
|
}
|
|
@@ -311,6 +338,8 @@ class SourceFile extends File {
|
|
|
311
338
|
`module.exports.${original} = csn.${original}`
|
|
312
339
|
])))
|
|
313
340
|
) // singular -> plural aliases
|
|
341
|
+
.concat(['// events'])
|
|
342
|
+
.concat(this.events.fqs.map(({fq, name}) => `module.exports.${name} = '${fq}'`))
|
|
314
343
|
.concat(['// actions'])
|
|
315
344
|
.concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
|
|
316
345
|
.concat(['// enums'])
|
|
@@ -473,7 +502,7 @@ export namespace Composition {
|
|
|
473
502
|
}
|
|
474
503
|
|
|
475
504
|
export class Entity {
|
|
476
|
-
static data<T extends Entity> (this:T,
|
|
505
|
+
static data<T extends Entity> (this:T, _input:Object) : T {
|
|
477
506
|
return {} as T // mock
|
|
478
507
|
}
|
|
479
508
|
}
|
package/lib/visitor.js
CHANGED
|
@@ -124,7 +124,7 @@ class Visitor {
|
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
// CLASS ASPECT
|
|
127
|
-
buffer.add(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) =>
|
|
127
|
+
buffer.add(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`)
|
|
128
128
|
buffer.indent()
|
|
129
129
|
buffer.add(`return class ${clean} extends Base {`)
|
|
130
130
|
buffer.indent()
|
|
@@ -132,17 +132,18 @@ class Visitor {
|
|
|
132
132
|
this.visitElement(ename, element, file, buffer)
|
|
133
133
|
}
|
|
134
134
|
for (const [aname, action] of Object.entries(entity.actions ?? {})) {
|
|
135
|
+
const lambdaString =
|
|
135
136
|
buffer.add(
|
|
136
|
-
SourceFile.stringifyLambda(
|
|
137
|
-
aname,
|
|
138
|
-
Object.entries(action.params ?? {}).map(([n, t]) => [
|
|
137
|
+
SourceFile.stringifyLambda({
|
|
138
|
+
name: aname,
|
|
139
|
+
parameters: Object.entries(action.params ?? {}).map(([n, t]) => [
|
|
139
140
|
n,
|
|
140
141
|
this.resolver.resolveAndRequire(t, file).typeName,
|
|
141
142
|
]),
|
|
142
|
-
action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
143
|
-
|
|
143
|
+
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
|
|
144
|
+
initialiser: `undefined as unknown as this['${aname}']`
|
|
145
|
+
})
|
|
144
146
|
)
|
|
145
|
-
//this.visitEntity(aname, action, file, buffer)
|
|
146
147
|
}
|
|
147
148
|
buffer.outdent()
|
|
148
149
|
buffer.add('};')
|
|
@@ -287,6 +288,26 @@ class Visitor {
|
|
|
287
288
|
this._aspectify(name, aspect, file.aspects, clean)
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
#printEvent(name, event) {
|
|
292
|
+
this.logger.debug(`Printing event ${name}`)
|
|
293
|
+
const clean = this.resolver.trimNamespace(name)
|
|
294
|
+
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
295
|
+
const file = this.getNamespaceFile(ns)
|
|
296
|
+
file.addEvent(clean, name)
|
|
297
|
+
const buffer = file.events.buffer
|
|
298
|
+
buffer.add('// event')
|
|
299
|
+
buffer.add(`export class ${clean} {`)
|
|
300
|
+
buffer.indent()
|
|
301
|
+
const propOpt = this.options.propertiesOptional
|
|
302
|
+
this.options.propertiesOptional = false
|
|
303
|
+
for (const [ename, element] of Object.entries(event.elements ?? {})) {
|
|
304
|
+
this.visitElement(ename, element, file, buffer)
|
|
305
|
+
}
|
|
306
|
+
this.options.propertiesOptional = propOpt
|
|
307
|
+
buffer.outdent()
|
|
308
|
+
buffer.add('}')
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
/**
|
|
291
312
|
* Visits a single entity from the CSN's definition field.
|
|
292
313
|
* Will call #printEntity or #printAction based on the entity's kind.
|
|
@@ -310,6 +331,9 @@ class Visitor {
|
|
|
310
331
|
case 'aspect':
|
|
311
332
|
this.#printAspect(name, entity)
|
|
312
333
|
break
|
|
334
|
+
case 'event':
|
|
335
|
+
this.#printEvent(name, entity)
|
|
336
|
+
break
|
|
313
337
|
default:
|
|
314
338
|
this.logger.error(`Unhandled entity kind '${entity.kind}'.`)
|
|
315
339
|
}
|
|
@@ -318,7 +342,7 @@ class Visitor {
|
|
|
318
342
|
/**
|
|
319
343
|
* A self reference is a property that references the class it appears in.
|
|
320
344
|
* They need to be detected on CDS level, as the emitted TS types will try to
|
|
321
|
-
* refer to
|
|
345
|
+
* refer to types via their alias that hides the aspectification.
|
|
322
346
|
* If we attempt to directly refer to this alias while it has not been fully created,
|
|
323
347
|
* that will result in a TS error.
|
|
324
348
|
* @param {String} entityName
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Generates .ts files for a CDS model to receive code completion in VS Code",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"repository": "github:cap-js/cds-typer",
|
|
6
7
|
"homepage": "https://cap.cloud.sap/",
|
|
7
8
|
"keywords": [
|
|
8
9
|
"CAP",
|
|
@@ -17,7 +18,14 @@
|
|
|
17
18
|
"test:all": "jest",
|
|
18
19
|
"test": "npm run test:unit",
|
|
19
20
|
"lint": "eslint",
|
|
20
|
-
"cli": "node lib/cli.js"
|
|
21
|
+
"cli": "node lib/cli.js",
|
|
22
|
+
"doc:clean": "rm -rf ./doc",
|
|
23
|
+
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|
|
24
|
+
"doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
|
|
25
|
+
"doc:html": "npm run doc:typegen && ./node_modules/.bin/typedoc 'doc/types/**/*.d.ts' --entryPointStrategy expand --out doc/html --tsconfig doc/types/tsconfig.json",
|
|
26
|
+
"doc:md": "npm run doc:typegen && ./node_modules/.bin/typedoc --plugin typedoc-plugin-markdown 'doc/types/compile.d.ts' --out doc/md --tsconfig doc/types/tsconfig.json",
|
|
27
|
+
"doc:cli": "npm run cli -- --help > ./doc/cli.txt",
|
|
28
|
+
"doc:full": "npm run doc:prepare && npm run doc:html && npm run doc:cli"
|
|
21
29
|
},
|
|
22
30
|
"files": [
|
|
23
31
|
"lib/",
|
|
@@ -39,6 +47,8 @@
|
|
|
39
47
|
"eslint-config-prettier": "^8.5.0",
|
|
40
48
|
"eslint-plugin-prettier": "^4.0.0",
|
|
41
49
|
"jest": "^29",
|
|
50
|
+
"typedoc": "^0.24.8",
|
|
51
|
+
"typedoc-plugin-markdown": "^3.15.3",
|
|
42
52
|
"typescript": ">=4.6.4"
|
|
43
53
|
},
|
|
44
54
|
"jest": {
|