@cap-js/cds-typer 0.8.0 → 0.10.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 +20 -1
- package/lib/csn.js +267 -0
- package/lib/file.js +24 -8
- package/lib/util.js +1 -62
- package/lib/visitor.js +25 -8
- package/package.json +2 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ 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.10.1 - TBD
|
|
8
8
|
|
|
9
9
|
### Changed
|
|
10
10
|
|
|
@@ -12,6 +12,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
12
12
|
|
|
13
13
|
### Fixed
|
|
14
14
|
|
|
15
|
+
## Version 0.10.0 - 2023-09-21
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Actions and functions are now attached to a static `.actions` property of each generated class. This reflects the runtime behaviour better than the former way of generating instance methods
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
## Version 0.9.0 - 2023-09-08
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Support for drafts via `@odata.draft.enabled` annotation
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Foreign keys are now propagated more than one level (think: `x_ID_ID_ID`)
|
|
33
|
+
|
|
15
34
|
|
|
16
35
|
## Version 0.8.0 - 2023-09-05
|
|
17
36
|
|
package/lib/csn.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
const annotation = '@odata.draft.enabled'
|
|
2
|
+
|
|
3
|
+
class DraftUnroller {
|
|
4
|
+
/** @type {Set<string>} */
|
|
5
|
+
#positives = new Set()
|
|
6
|
+
/** @type {{[key: string]: boolean}} */
|
|
7
|
+
#draftable = {}
|
|
8
|
+
/** @type {{[key: string]: string}} */
|
|
9
|
+
#projections
|
|
10
|
+
/** @type {object[]} */
|
|
11
|
+
#entities
|
|
12
|
+
#csn
|
|
13
|
+
set csn(c) {
|
|
14
|
+
this.#csn = c
|
|
15
|
+
this.#entities = Object.values(c.definitions)
|
|
16
|
+
this.#projections = this.#entities.reduce((pjs, entity) => {
|
|
17
|
+
if (entity.projection) {
|
|
18
|
+
pjs[entity.name] = entity.projection.from.ref[0]
|
|
19
|
+
}
|
|
20
|
+
return pjs
|
|
21
|
+
}, {})
|
|
22
|
+
}
|
|
23
|
+
get csn() { return this.#csn }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param entity {object | string} - entity to set draftable annotation for.
|
|
27
|
+
* @param value {boolean} - whether the entity is draftable.
|
|
28
|
+
*/
|
|
29
|
+
#setDraftable(entity, value) {
|
|
30
|
+
if (typeof entity === 'string') entity = this.#getDefinition(entity)
|
|
31
|
+
entity[annotation] = value
|
|
32
|
+
this.#draftable[entity.name] = value
|
|
33
|
+
if (value) {
|
|
34
|
+
this.#positives.add(entity.name)
|
|
35
|
+
} else {
|
|
36
|
+
this.#positives.delete(entity.name)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param entity {object | string} - entity to look draftability up for.
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
#getDraftable(entity) {
|
|
45
|
+
if (typeof entity === 'string') entity = this.#getDefinition(entity)
|
|
46
|
+
return this.#draftable[entity.name] ??= this.#propagateInheritance(entity)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param name {string} - name of the entity.
|
|
51
|
+
*/
|
|
52
|
+
#getDefinition(name) { return this.csn.definitions[name] }
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Propagate draft annotations through inheritance (includes).
|
|
56
|
+
* The latest annotation through the inheritance chain "wins".
|
|
57
|
+
* Annotations on the entity itself are always queued last, so they will always be decisive over ancestors.
|
|
58
|
+
*
|
|
59
|
+
* @param entity {object} - entity to pull draftability from its parents.
|
|
60
|
+
*/
|
|
61
|
+
#propagateInheritance(entity) {
|
|
62
|
+
const annotations = (entity.includes ?? []).map(parent => this.#getDraftable(parent))
|
|
63
|
+
annotations.push(entity[annotation])
|
|
64
|
+
this.#setDraftable(entity, annotations.filter(a => a !== undefined).at(-1) ?? false)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Propagate draft-enablement through projections.
|
|
69
|
+
*/
|
|
70
|
+
#propagateProjections() {
|
|
71
|
+
const propagate = (from, to) => {
|
|
72
|
+
do {
|
|
73
|
+
this.#setDraftable(to, this.#getDraftable(to) || this.#getDraftable(from))
|
|
74
|
+
from = to
|
|
75
|
+
to = this.#projections[to]
|
|
76
|
+
} while (to)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let [projection, target] of Object.entries(this.#projections)) {
|
|
80
|
+
propagate(projection, target)
|
|
81
|
+
propagate(target, projection)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* If an entity E is draftable and contains any composition of entities,
|
|
87
|
+
* then those entities also become draftable. Recursively.
|
|
88
|
+
*
|
|
89
|
+
* @param entity {object} - entity to propagate all compositions from.
|
|
90
|
+
*/
|
|
91
|
+
#propagateCompositions(entity) {
|
|
92
|
+
if (!this.#getDraftable(entity)) return
|
|
93
|
+
|
|
94
|
+
for (const comp of Object.values(entity.compositions ?? {})) {
|
|
95
|
+
const target = this.#getDefinition(comp.target)
|
|
96
|
+
const current = this.#getDraftable(target)
|
|
97
|
+
if (!current) {
|
|
98
|
+
this.#setDraftable(target, true)
|
|
99
|
+
this.#propagateCompositions(target)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
unroll(csn) {
|
|
105
|
+
this.csn = csn
|
|
106
|
+
|
|
107
|
+
// inheritance
|
|
108
|
+
for (const entity of this.#entities) {
|
|
109
|
+
this.#propagateInheritance(entity)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// transitivity through compositions
|
|
113
|
+
// we have to do this in a second pass, as we only now know which entities are draft-enables themselves
|
|
114
|
+
for (const entity of this.#entities) {
|
|
115
|
+
this.#propagateCompositions(entity)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.#propagateProjections()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// note to self: following doc uses @ homoglyph instead of @, as the latter apparently has special semantics in code listings
|
|
123
|
+
/**
|
|
124
|
+
* We are unrolling the @odata.draft.enabled annotations into related entities manually.
|
|
125
|
+
* This includes three scenarios:
|
|
126
|
+
*
|
|
127
|
+
* (a) aspects via `A: B`, where `B` is draft enabled.
|
|
128
|
+
* Note that when an entity extends two other entities of which one has drafts enabled and
|
|
129
|
+
* one has not, then the one that is later in the list of mixins "wins":
|
|
130
|
+
* @example sdasd
|
|
131
|
+
* ```ts
|
|
132
|
+
* @odata.draft.enabled true
|
|
133
|
+
* entity T {}
|
|
134
|
+
* @odata.draft.enabled false
|
|
135
|
+
* entity F {}
|
|
136
|
+
* entity A: T,F {} // draft not enabled
|
|
137
|
+
* entity B: F,T {} // draft enabled
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* (b) Draft enabled projections make the entity we project on draft enabled.
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* @odata.draft.enabled: true
|
|
144
|
+
* entity A as projection on B {}
|
|
145
|
+
* entity B {} // draft enabled
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* (c) Entities that are draft enabled propagate this property down through compositions:
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* @odata.draft.enabled: true
|
|
152
|
+
* entity A {
|
|
153
|
+
* b: Composition of B
|
|
154
|
+
* }
|
|
155
|
+
* entity B {} // draft enabled
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
function unrollDraftability(csn) {
|
|
159
|
+
new DraftUnroller().unroll(csn)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Propagates keys elements through the CSN. This includes
|
|
164
|
+
*
|
|
165
|
+
* (a) keys that are explicitly declared as key in an entity
|
|
166
|
+
* (b) keys from aspects the entity extends
|
|
167
|
+
*
|
|
168
|
+
* This explicit propagation is required to add foreign key relations
|
|
169
|
+
* to referring entities.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```cds
|
|
173
|
+
* entity A: cuid { key name: String; }
|
|
174
|
+
* entity B { ref: Association to one A }
|
|
175
|
+
* ```
|
|
176
|
+
* must yield
|
|
177
|
+
* ```ts
|
|
178
|
+
* class A {
|
|
179
|
+
* ID: UUID // inherited from cuid
|
|
180
|
+
* name: String;
|
|
181
|
+
* }
|
|
182
|
+
* class B {
|
|
183
|
+
* ref: Association.to<A>
|
|
184
|
+
* ref_ID: UUID
|
|
185
|
+
* ref_name: String;
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
* @returns {{[key: string]: object}}
|
|
189
|
+
*/
|
|
190
|
+
function propagateForeignKeys(csn) {
|
|
191
|
+
for (const element of Object.values(csn.definitions)) {
|
|
192
|
+
Object.defineProperty(element, 'keys', {
|
|
193
|
+
get: function () {
|
|
194
|
+
// cached access to all immediately defined _and_ inherited keys.
|
|
195
|
+
// They need to be explicitly accessible in subclasses to generate
|
|
196
|
+
// foreign key fields from Associations/ Compositions.
|
|
197
|
+
if (!Object.hasOwn(this, '__keys')) {
|
|
198
|
+
const ownKeys = Object.entries(this.elements ?? {}).filter(([,el]) => el.key === true)
|
|
199
|
+
const inheritedKeys = this.includes?.flatMap(parent => Object.entries(csn.definitions[parent].keys)) ?? []
|
|
200
|
+
// not sure why, but .associations contains both Associations, as well as Compositions in CSN.
|
|
201
|
+
// (.compositions contains only Compositions, if any)
|
|
202
|
+
const remoteKeys = Object.entries(this.associations ?? {})
|
|
203
|
+
.filter(([,{key}]) => key) // only follow associations that are keys, that way we avoid cycles
|
|
204
|
+
.flatMap(([kname, key]) => Object.entries(csn.definitions[key.target].keys)
|
|
205
|
+
.map(([ckname, ckey]) => [`${kname}_${ckname}`, ckey]))
|
|
206
|
+
|
|
207
|
+
this.__keys = Object.fromEntries(ownKeys
|
|
208
|
+
.concat(inheritedKeys)
|
|
209
|
+
.concat(remoteKeys)
|
|
210
|
+
.filter(([,ckey]) => !ckey.target) // discard keys that are Associations. Those are already part of .elements
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
return this.__keys
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Entities inherit their ancestors annotations:
|
|
221
|
+
* https://cap.cloud.sap/docs/cds/cdl#annotation-propagation
|
|
222
|
+
* This is a problem if we annotate @singular/ @plural to an entity A,
|
|
223
|
+
* as we don't want all descendents B, C, ... to share the ancestor's
|
|
224
|
+
* annotated inflexion
|
|
225
|
+
* -> remove all such annotations that appear in a parent as well.
|
|
226
|
+
* BUT: we can't just delete the attributes. Imagine three classes
|
|
227
|
+
* A <- B <- C
|
|
228
|
+
* where A contains a @singular annotation.
|
|
229
|
+
* If we erase the annotation from B, C will still contain it and
|
|
230
|
+
* can not detect that its own annotation was inherited without
|
|
231
|
+
* travelling up the entire inheritance chain up to A.
|
|
232
|
+
* So instead, we monkey patch and maintain a dictionary "erased"
|
|
233
|
+
* when removing an annotation which we also check.
|
|
234
|
+
* @deprecated since we use the xtended flavour for CSN, we don't need to fix this anymore
|
|
235
|
+
*/
|
|
236
|
+
function propagateInflectionAnnotations(csn) {
|
|
237
|
+
const erase = (entity, parent, attr) => {
|
|
238
|
+
if (attr in entity) {
|
|
239
|
+
const ea = entity[attr]
|
|
240
|
+
if (parent[attr] === ea || (parent.erased && parent.erased[attr] === ea)) {
|
|
241
|
+
entity.erased ??= {}
|
|
242
|
+
entity.erased[attr] = ea
|
|
243
|
+
delete entity[attr]
|
|
244
|
+
//this.logger.info(`Removing inherited attribute ${attr} from ${entity.name}.`)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const entity of Object.values(csn.definitions)) {
|
|
250
|
+
let i = 0
|
|
251
|
+
while (
|
|
252
|
+
(getSingularAnnotation(entity) || getPluralAnnotation(entity)) &&
|
|
253
|
+
i < (entity.includes ?? []).length
|
|
254
|
+
) {
|
|
255
|
+
const parent = csn.definitions[entity.includes[i]]
|
|
256
|
+
Object.values(annotations).flat().forEach(an => erase(entity, parent, an))
|
|
257
|
+
i++
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function amendCSN(csn) {
|
|
263
|
+
unrollDraftability(csn)
|
|
264
|
+
propagateForeignKeys(csn)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = { amendCSN }
|
package/lib/file.js
CHANGED
|
@@ -127,18 +127,34 @@ class SourceFile extends File {
|
|
|
127
127
|
* @returns {string} - the stringified lambda
|
|
128
128
|
* @example
|
|
129
129
|
* ```js
|
|
130
|
-
*
|
|
131
|
-
* stringifyLambda({
|
|
132
|
-
* stringifyLambda({name: 'f', parameters: [['p','T']]
|
|
133
|
-
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'
|
|
130
|
+
* // note: these samples are actually simplified! See below.
|
|
131
|
+
* stringifyLambda({parameters: [['p','T']]}) // f: { (p: T): any, ... }
|
|
132
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']]}) // f: { (p: T) => any, ... }
|
|
133
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // f: { (p: T) => number, ... }
|
|
134
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number', initialiser: '_ => 42'}) // f: { (p: T): string = _ => 42, ... }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* The generated string will not be just the signature of the function. Instead, it will be an object offering a callable signature.
|
|
138
|
+
* On top of that, it will also expose a property `__parameters`, which is an object reflecting the functions parameters.
|
|
139
|
+
* The reason for this is that the CDS runtime actually treats the function parameters as a named object. This can not be rectified via
|
|
140
|
+
* type magic, as parameter names do not exist on type level. So we can not use these names to reuse them as object properties.
|
|
141
|
+
* Instead, we generate this utility object for the runtime to use:
|
|
134
142
|
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```js
|
|
145
|
+
* stringifyLambda({name: 'f', parameters: [['p','T']], returns: 'number'}) // { (p: T): number, __parameters: { p: T } }
|
|
135
146
|
* ```
|
|
136
147
|
*/
|
|
137
|
-
static stringifyLambda({name, parameters=[], returns='any', initialiser}) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
148
|
+
static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false}) {
|
|
149
|
+
const parameterTypes = parameters.map(([n, t]) => `${n}: ${t}`).join(', ')
|
|
150
|
+
const callableSignature = `(${parameterTypes}): ${returns}`
|
|
151
|
+
let prefix = name ? `${name}: `: ''
|
|
152
|
+
if (prefix && isStatic) {
|
|
153
|
+
prefix = `static ${prefix}`
|
|
154
|
+
}
|
|
140
155
|
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
141
|
-
|
|
156
|
+
const lambda = `{ ${callableSignature}, __parameters: {${parameterTypes}}, __returns: ${returns} }`
|
|
157
|
+
return prefix + lambda + suffix
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
/**
|
package/lib/util.js
CHANGED
|
@@ -184,66 +184,6 @@ const parseCommandlineArgs = (argv, validFlags) => {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
/**
|
|
188
|
-
* Entities inherit their ancestors annotations:
|
|
189
|
-
* https://cap.cloud.sap/docs/cds/cdl#annotation-propagation
|
|
190
|
-
* This is a problem if we annotate @singular/ @plural to an entity A,
|
|
191
|
-
* as we don't want all descendents B, C, ... to share the ancestor's
|
|
192
|
-
* annotated inflexion
|
|
193
|
-
* -> remove all such annotations that appear in a parent as well.
|
|
194
|
-
* BUT: we can't just delete the attributes. Imagine three classes
|
|
195
|
-
* A <- B <- C
|
|
196
|
-
* where A contains a @singular annotation.
|
|
197
|
-
* If we erase the annotation from B, C will still contain it and
|
|
198
|
-
* can not detect that its own annotation was inherited without
|
|
199
|
-
* travelling up the entire inheritance chain up to A.
|
|
200
|
-
* So instead, we monkey patch and maintain a dictionary "erased"
|
|
201
|
-
* when removing an annotation which we also check.
|
|
202
|
-
*/
|
|
203
|
-
function fixCSN(csn) {
|
|
204
|
-
for (const element of Object.values(csn.definitions)) {
|
|
205
|
-
Object.defineProperty(element, 'keys', {
|
|
206
|
-
get: function () {
|
|
207
|
-
// cached access to all immediately defined _and_ inherited keys.
|
|
208
|
-
// They need to be explicitly accessible in subclasses to generate
|
|
209
|
-
// foreign key fields from Associations/ Compositions.
|
|
210
|
-
if (!Object.hasOwn(this, '__keys')) {
|
|
211
|
-
const ownKeys = Object.fromEntries(Object.entries(this.elements ?? {}).filter(([,el]) => el.key === true))
|
|
212
|
-
const inheritedKeys = this.includes?.flatMap(parent => csn.definitions[parent].keys) ?? []
|
|
213
|
-
this.__keys = inheritedKeys.reduce((ks, ps) => ({...ps, ...ks}), ownKeys)
|
|
214
|
-
}
|
|
215
|
-
return this.__keys
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// FIXME: delete after merge with draft-enablement PR
|
|
221
|
-
return
|
|
222
|
-
const erase = (entity, parent, attr) => {
|
|
223
|
-
if (attr in entity) {
|
|
224
|
-
const ea = entity[attr]
|
|
225
|
-
if (parent[attr] === ea || (parent.erased && parent.erased[attr] === ea)) {
|
|
226
|
-
entity.erased ??= {}
|
|
227
|
-
entity.erased[attr] = ea
|
|
228
|
-
delete entity[attr]
|
|
229
|
-
//this.logger.info(`Removing inherited attribute ${attr} from ${entity.name}.`)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
for (const entity of Object.values(csn.definitions)) {
|
|
235
|
-
let i = 0
|
|
236
|
-
while (
|
|
237
|
-
(getSingularAnnotation(entity) || getPluralAnnotation(entity)) &&
|
|
238
|
-
i < (entity.includes ?? []).length
|
|
239
|
-
) {
|
|
240
|
-
const parent = csn.definitions[entity.includes[i]]
|
|
241
|
-
Object.values(annotations).flat().forEach(an => erase(entity, parent, an))
|
|
242
|
-
i++
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
187
|
module.exports = {
|
|
248
188
|
annotations,
|
|
249
189
|
getSingularAnnotation,
|
|
@@ -252,6 +192,5 @@ module.exports = {
|
|
|
252
192
|
singular4,
|
|
253
193
|
plural4,
|
|
254
194
|
parseCommandlineArgs,
|
|
255
|
-
deepMerge
|
|
256
|
-
fixCSN
|
|
195
|
+
deepMerge
|
|
257
196
|
}
|
package/lib/visitor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('./util')
|
|
4
4
|
|
|
5
|
+
const { amendCSN } = require('./csn')
|
|
5
6
|
const { SourceFile, baseDefinitions, Buffer } = require('./file')
|
|
6
7
|
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
7
8
|
const { Resolver } = require('./components/resolver')
|
|
@@ -59,7 +60,7 @@ class Visitor {
|
|
|
59
60
|
* @param {VisitorOptions} options
|
|
60
61
|
*/
|
|
61
62
|
constructor(csn, options = {}, logger = new Logger()) {
|
|
62
|
-
|
|
63
|
+
amendCSN(csn)
|
|
63
64
|
this.options = { ...defaults, ...options }
|
|
64
65
|
this.logger = logger
|
|
65
66
|
this.csn = csn
|
|
@@ -128,8 +129,10 @@ class Visitor {
|
|
|
128
129
|
buffer.indent()
|
|
129
130
|
buffer.add(`return class ${clean} extends Base {`)
|
|
130
131
|
buffer.indent()
|
|
132
|
+
|
|
131
133
|
for (const [ename, element] of Object.entries(entity.elements ?? {})) {
|
|
132
134
|
this.visitElement(ename, element, file, buffer)
|
|
135
|
+
|
|
133
136
|
// make foreign keys explicit
|
|
134
137
|
if ('target' in element) {
|
|
135
138
|
// lookup in cds.definitions can fail for inline structs.
|
|
@@ -139,21 +142,27 @@ class Visitor {
|
|
|
139
142
|
this.visitElement(`${ename}_${kname}`, kelement, file, buffer)
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
|
-
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
buffer.add('static actions: {')
|
|
148
|
+
buffer.indent()
|
|
143
149
|
for (const [aname, action] of Object.entries(entity.actions ?? {})) {
|
|
144
150
|
buffer.add(
|
|
145
151
|
SourceFile.stringifyLambda({
|
|
146
152
|
name: aname,
|
|
147
153
|
parameters: this.#stringifyFunctionParams(action.params, file),
|
|
148
|
-
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
149
|
-
initialiser: `undefined as unknown as
|
|
154
|
+
returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
155
|
+
//initialiser: `undefined as unknown as typeof ${clean}.${aname}`,
|
|
150
156
|
})
|
|
151
157
|
)
|
|
152
158
|
}
|
|
153
159
|
buffer.outdent()
|
|
154
|
-
buffer.add('}
|
|
160
|
+
buffer.add('}') // end of actions
|
|
161
|
+
|
|
162
|
+
buffer.outdent()
|
|
163
|
+
buffer.add('};') // end of generated class
|
|
155
164
|
buffer.outdent()
|
|
156
|
-
buffer.add('}')
|
|
165
|
+
buffer.add('}') // end of aspect
|
|
157
166
|
|
|
158
167
|
// CLASS WITH ADDED ASPECTS
|
|
159
168
|
file.addImport(baseDefinitions.path)
|
|
@@ -173,11 +182,19 @@ class Visitor {
|
|
|
173
182
|
`${baseDefinitions.path.asIdentifier()}.Entity`
|
|
174
183
|
)
|
|
175
184
|
|
|
176
|
-
buffer.add(`export class ${identSingular(clean)} extends ${rhs} {}`)
|
|
185
|
+
buffer.add(`export class ${identSingular(clean)} extends ${rhs} {${this.#staticClassContents(clean, entity).join('\n')}}`)
|
|
177
186
|
//buffer.add(`export type ${clean} = InstanceType<typeof ${identSingular(clean)}>`)
|
|
178
187
|
this.contexts.pop()
|
|
179
188
|
}
|
|
180
189
|
|
|
190
|
+
#isDraftEnabled(entity) {
|
|
191
|
+
return entity['@odata.draft.enabled'] === true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#staticClassContents(clean, entity) {
|
|
195
|
+
return this.#isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
|
|
196
|
+
}
|
|
197
|
+
|
|
181
198
|
#printEntity(name, entity) {
|
|
182
199
|
const clean = this.resolver.trimNamespace(name)
|
|
183
200
|
const ns = this.resolver.resolveNamespace(name.split('.'))
|
|
@@ -228,7 +245,7 @@ class Visitor {
|
|
|
228
245
|
}
|
|
229
246
|
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
230
247
|
// so it can get passed as value to CQL functions.
|
|
231
|
-
buffer.add(`export class ${plural} extends Array<${singular}> {}`)
|
|
248
|
+
buffer.add(`export class ${plural} extends Array<${singular}> {${this.#staticClassContents(singular, entity).join('\n')}}`)
|
|
232
249
|
buffer.add('')
|
|
233
250
|
}
|
|
234
251
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -22,10 +22,7 @@
|
|
|
22
22
|
"doc:clean": "rm -rf ./doc",
|
|
23
23
|
"doc:prepare": "npm run doc:clean && mkdir -p doc/types",
|
|
24
24
|
"doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
|
|
25
|
-
"doc:
|
|
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"
|
|
25
|
+
"doc:cli": "npm run cli -- --help > ./doc/cli.txt"
|
|
29
26
|
},
|
|
30
27
|
"files": [
|
|
31
28
|
"lib/",
|
|
@@ -48,8 +45,6 @@
|
|
|
48
45
|
"eslint-config-prettier": "^8.5.0",
|
|
49
46
|
"eslint-plugin-prettier": "^4.0.0",
|
|
50
47
|
"jest": "^29",
|
|
51
|
-
"typedoc": "^0.24.8",
|
|
52
|
-
"typedoc-plugin-markdown": "^3.15.3",
|
|
53
48
|
"typescript": ">=4.6.4"
|
|
54
49
|
},
|
|
55
50
|
"jest": {
|