@cap-js/cds-typer 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/cds-plugin.js +11 -4
- package/lib/cli.js +172 -40
- package/lib/compile.js +10 -16
- package/lib/components/basedefs.js +20 -0
- package/lib/components/class.js +35 -0
- package/lib/components/enum.js +2 -2
- package/lib/components/inline.js +15 -9
- package/lib/components/javascript.js +2 -2
- package/lib/components/wrappers.js +49 -1
- package/lib/config.js +118 -0
- package/lib/csn.js +110 -147
- package/lib/file.js +37 -24
- package/lib/resolution/entity.js +1 -0
- package/lib/resolution/resolver.js +23 -14
- package/lib/typedefs.d.ts +82 -38
- package/lib/util.js +17 -64
- package/lib/visitor.js +119 -66
- package/library/cds.hana.ts +2 -2
- package/package.json +70 -2
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
// this was derived from baseDefinitions before, but caused a circular dependency
|
|
4
4
|
const base = '__'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Wraps type into the Key type.
|
|
8
|
+
* @param {string} t - the type name.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
const createKey = t => `${base}.Key<${t}>`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps type into KeysOf type.
|
|
15
|
+
* @param {string} t - the type name.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
const createKeysOf = t => `${base}.KeysOf<${t}>`
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Wraps type into DraftOf type.
|
|
22
|
+
* @param {string} t - the type name.
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
const createDraftOf = t => `${base}.DraftOf<${t}>`
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wraps type into DraftsOf type.
|
|
29
|
+
* @param {string} t - the type name.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
const createDraftsOf = t => `${base}.DraftsOf<${t}>`
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wraps type into ElementsOf type.
|
|
36
|
+
* @param {string} t - the type name.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
const createElementsOf = t => `${base}.ElementsOf<${t}>`
|
|
40
|
+
|
|
6
41
|
/**
|
|
7
42
|
* Wraps type into association to scalar.
|
|
8
43
|
* @param {string} t - the singular type name.
|
|
@@ -84,8 +119,20 @@ const docify = doc => {
|
|
|
84
119
|
return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
|
|
85
120
|
}
|
|
86
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Wraps a string in single quotes. No escaping is done, so use with caution.
|
|
124
|
+
* @param {string} s - the string to wrap.
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
const stringIdent = s => `'${s}'`
|
|
128
|
+
|
|
87
129
|
module.exports = {
|
|
88
130
|
createArrayOf,
|
|
131
|
+
createDraftOf,
|
|
132
|
+
createDraftsOf,
|
|
133
|
+
createKey,
|
|
134
|
+
createKeysOf,
|
|
135
|
+
createElementsOf,
|
|
89
136
|
createObjectOf,
|
|
90
137
|
createPromiseOf,
|
|
91
138
|
createUnionOf,
|
|
@@ -94,5 +141,6 @@ module.exports = {
|
|
|
94
141
|
createCompositionOfOne,
|
|
95
142
|
createCompositionOfMany,
|
|
96
143
|
deepRequire,
|
|
97
|
-
docify
|
|
144
|
+
docify,
|
|
145
|
+
stringIdent
|
|
98
146
|
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const { camelToSnake } = require('./util')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Makes properties of an object accessible in both camelCase and snake_case.
|
|
6
|
+
* Snake_case gets precedence over camelCase.
|
|
7
|
+
* @template T
|
|
8
|
+
* @param {T} target - The object to proxy.
|
|
9
|
+
* @returns {T} - The proxied object.
|
|
10
|
+
*/
|
|
11
|
+
const camelSnakeHybrid = target => {
|
|
12
|
+
// @ts-expect-error - expecting target to be of type {}, which is not T (same for following)
|
|
13
|
+
const proxy = new Proxy(target, {
|
|
14
|
+
get(target, prop) {
|
|
15
|
+
// @ts-expect-error
|
|
16
|
+
return target[camelToSnake(prop)] ?? target[prop]
|
|
17
|
+
},
|
|
18
|
+
set(target, p, v) {
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
target[camelToSnake(p)] = v
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
// need to make sure all properties are initially available in snake_case
|
|
25
|
+
// @ts-expect-error
|
|
26
|
+
for (const [k,v] of Object.entries(target)) {
|
|
27
|
+
// @ts-expect-error
|
|
28
|
+
proxy[k] = v
|
|
29
|
+
}
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
return proxy
|
|
32
|
+
}
|
|
33
|
+
class Config {
|
|
34
|
+
static #defaults = {
|
|
35
|
+
propertiesOptional: true,
|
|
36
|
+
useEntitiesProxy: false,
|
|
37
|
+
inlineDeclarations: 'flat',
|
|
38
|
+
outputDirectory: '.'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
values = undefined
|
|
42
|
+
proxy = undefined
|
|
43
|
+
|
|
44
|
+
init () {
|
|
45
|
+
this.values = {...Config.#defaults, ...(cds.env.typer ?? {})}
|
|
46
|
+
this.proxy = camelSnakeHybrid(this.values)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
// proxy around config still allows arbitrary property access:
|
|
51
|
+
// require('config').configuration.logLevel = 'warn' will work
|
|
52
|
+
// eslint-disable-next-line no-constructor-return
|
|
53
|
+
return new Proxy(this, {
|
|
54
|
+
get(target, prop) {
|
|
55
|
+
// lazy loading of cds.env
|
|
56
|
+
// if we don't do this, configuration will load cds.env whenever it is
|
|
57
|
+
// first imported anywhere (even by proxy from, say, cli.js).
|
|
58
|
+
// So we don't get to modify cds.env before that, which is important
|
|
59
|
+
// in cds-build.js.
|
|
60
|
+
// FIXME: revisit. This is horrible.
|
|
61
|
+
if (target.values === undefined) target.init()
|
|
62
|
+
return target[prop] ?? target.proxy[prop]
|
|
63
|
+
},
|
|
64
|
+
set(target, p, v) {
|
|
65
|
+
if (target.values === undefined) target.init()
|
|
66
|
+
|
|
67
|
+
// this.value, this.proxy etc should not be forwarded to the wrapped values
|
|
68
|
+
if (target[p]) {
|
|
69
|
+
target[p] = v
|
|
70
|
+
} else {
|
|
71
|
+
target.proxy[p] = v
|
|
72
|
+
}
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} key - The key to set.
|
|
80
|
+
* @param {any} value - The value to set
|
|
81
|
+
*/
|
|
82
|
+
setOne (key, value) {
|
|
83
|
+
this.proxy[key] = value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {object} props - The properties to set.
|
|
88
|
+
*/
|
|
89
|
+
setMany (props) {
|
|
90
|
+
for (const [k,v] of Object.entries(props)) {
|
|
91
|
+
this.proxy[k] = v
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resets the config value and sets all its values from another passed
|
|
97
|
+
* config object. This allows to keep the reference to the same object.
|
|
98
|
+
* @param {Config} config - Another config object to set all config entries from.
|
|
99
|
+
*/
|
|
100
|
+
setFrom (config) {
|
|
101
|
+
this.values = camelSnakeHybrid({})
|
|
102
|
+
this.setMany(config.values)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
clone () {
|
|
106
|
+
const res = new Config()
|
|
107
|
+
res.init()
|
|
108
|
+
res.setMany(this.values)
|
|
109
|
+
return res
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
camelSnakeHybrid,
|
|
115
|
+
/** @type {import('./typedefs').config.Configuration} */
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
configuration: new Config()
|
|
118
|
+
}
|
package/lib/csn.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { LOG } = require('./logging')
|
|
2
|
+
|
|
3
|
+
const DRAFT_ENABLED_ANNO = '@odata.draft.enabled'
|
|
4
|
+
/** @type {string[]} */
|
|
5
|
+
const draftEnabledEntities = []
|
|
2
6
|
|
|
3
7
|
/** @typedef {import('./typedefs').resolver.CSN} CSN */
|
|
4
8
|
/** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
|
|
@@ -40,9 +44,9 @@ const isUnresolved = entity => entity._unresolved === true
|
|
|
40
44
|
const isCsnAny = entity => entity?.constructor?.name === 'any'
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
|
-
* @param {
|
|
47
|
+
* @param {string} fq - the fqn of an entity
|
|
44
48
|
*/
|
|
45
|
-
const isDraftEnabled =
|
|
49
|
+
const isDraftEnabled = fq => draftEnabledEntities.includes(fq)
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* @param {EntityCSN} entity - the entity
|
|
@@ -87,183 +91,151 @@ const getProjectionTarget = entity => isProjection(entity)
|
|
|
87
91
|
? entity.projection?.from?.ref?.[0]
|
|
88
92
|
: undefined
|
|
89
93
|
|
|
90
|
-
class
|
|
91
|
-
/** @type {Set<string>} */
|
|
92
|
-
#positives = new Set()
|
|
93
|
-
/** @type {{[key: string]: boolean}} */
|
|
94
|
-
#draftable = {}
|
|
95
|
-
/** @type {{[key: string]: string}} */
|
|
96
|
-
#projections = {}
|
|
94
|
+
class DraftEnabledEntityCollector {
|
|
97
95
|
/** @type {EntityCSN[]} */
|
|
98
|
-
#
|
|
96
|
+
#draftRoots = []
|
|
97
|
+
/** @type {string[]} */
|
|
98
|
+
#serviceNames = []
|
|
99
99
|
/** @type {CSN | undefined} */
|
|
100
100
|
#csn
|
|
101
|
-
|
|
102
|
-
this.#csn = c
|
|
103
|
-
if (c === undefined) return
|
|
104
|
-
this.#entities = Object.values(c.definitions)
|
|
105
|
-
this.#projections = this.#entities.reduce((pjs, entity) => {
|
|
106
|
-
if (isProjection(entity)) {
|
|
107
|
-
// @ts-ignore - we know that entity is a projection here
|
|
108
|
-
pjs[entity.name] = getProjectionTarget(entity)
|
|
109
|
-
}
|
|
110
|
-
return pjs
|
|
111
|
-
}, {})
|
|
112
|
-
}
|
|
113
|
-
get csn() { return this.#csn }
|
|
101
|
+
#compileError = false
|
|
114
102
|
|
|
115
103
|
/**
|
|
116
|
-
* @
|
|
117
|
-
* @param {boolean} value - whether the entity is draftable.
|
|
104
|
+
* @returns {string[]}
|
|
118
105
|
*/
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
? this.#getDefinition(entityOrFq)
|
|
122
|
-
: entityOrFq
|
|
123
|
-
if (!entity) return // inline definition -- not found in definitions
|
|
124
|
-
entity[annotation] = value
|
|
125
|
-
this.#draftable[entity.name] = value
|
|
126
|
-
if (value) {
|
|
127
|
-
this.#positives.add(entity.name)
|
|
128
|
-
} else {
|
|
129
|
-
this.#positives.delete(entity.name)
|
|
130
|
-
}
|
|
106
|
+
#getServiceNames() {
|
|
107
|
+
return Object.values(this.#csn?.definitions ?? {}).filter(d => d.kind === 'service').map(d => d.name)
|
|
131
108
|
}
|
|
132
109
|
|
|
133
110
|
/**
|
|
134
|
-
* @
|
|
135
|
-
* @returns {boolean}
|
|
111
|
+
* @returns {EntityCSN[]}
|
|
136
112
|
*/
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// assert(typeof entity !== 'string')
|
|
142
|
-
const name = entity?.name ?? entityOrFq
|
|
143
|
-
// @ts-expect-error - .name not being present means entityOrFq is a string, so name is always a string and therefore a valid index
|
|
144
|
-
return this.#draftable[name] ??= this.#propagateInheritance(entity)
|
|
113
|
+
#collectDraftRoots() {
|
|
114
|
+
return Object.values(this.#csn?.definitions ?? {}).filter(
|
|
115
|
+
d => isEntity(d) && this.#isDraftEnabled(d) && this.#isPartOfAnyService(d.name)
|
|
116
|
+
)
|
|
145
117
|
}
|
|
146
118
|
|
|
147
119
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @
|
|
150
|
-
* @returns {EntityCSN}
|
|
151
|
-
*/
|
|
152
|
-
// @ts-expect-error - poor man's #getDefinitionOrThrow. We are always sure name is a valid key
|
|
153
|
-
#getDefinition(name) { return this.csn?.definitions[name] }
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Propagate draft annotations through inheritance (includes).
|
|
157
|
-
* The latest annotation through the inheritance chain "wins".
|
|
158
|
-
* Annotations on the entity itself are always queued last, so they will always be decisive over ancestors.
|
|
159
|
-
* @param {EntityCSN | undefined} entity - entity to pull draftability from its parents.
|
|
120
|
+
* @param {string} entityName - entity to check
|
|
121
|
+
* @returns {boolean} `true` if entity is part an service
|
|
160
122
|
*/
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
/** @type {(boolean | undefined)[]} */
|
|
164
|
-
const annotations = (entity.includes ?? []).map(parent => this.#getDraftable(parent))
|
|
165
|
-
annotations.push(entity[annotation])
|
|
166
|
-
this.#setDraftable(entity, annotations.filter(a => a !== undefined).at(-1) ?? false)
|
|
123
|
+
#isPartOfAnyService(entityName) {
|
|
124
|
+
return this.#serviceNames.some(s => entityName.startsWith(s))
|
|
167
125
|
}
|
|
168
126
|
|
|
169
127
|
/**
|
|
170
|
-
*
|
|
128
|
+
* Collect all entities that are transitively reachable via compositions from `entity` into `draftNodes`.
|
|
129
|
+
* Check that no entity other than the root node has `@odata.draft.enabled`
|
|
130
|
+
* @param {EntityCSN} entity -
|
|
131
|
+
* @param {string} entityName -
|
|
132
|
+
* @param {EntityCSN} rootEntity - root entity where composition traversal started.
|
|
133
|
+
* @param {Record<string,EntityCSN>} draftEntities - Dictionary of entitys
|
|
171
134
|
*/
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
135
|
+
#collectDraftEntitiesInto(entity, entityName, rootEntity, draftEntities) {
|
|
136
|
+
draftEntities[entityName] = entity
|
|
137
|
+
|
|
138
|
+
for (const elem of Object.values(entity.elements ?? {})) {
|
|
139
|
+
if (!elem.target || elem.type !== 'cds.Composition') continue
|
|
140
|
+
|
|
141
|
+
const draftEntity = this.#csn?.definitions[elem.target]
|
|
142
|
+
const draftEntityName = elem.target
|
|
143
|
+
|
|
144
|
+
if (!draftEntity) {
|
|
145
|
+
throw new Error(`Expecting target to be resolved: ${JSON.stringify(elem, null, 2)}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!this.#isPartOfAnyService(draftEntityName)) {
|
|
149
|
+
LOG.warn(`Ignoring draft entity for composition target ${draftEntityName} because it is not part of a service`)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (draftEntity !== rootEntity && this.#isDraftEnabled(draftEntity)) {
|
|
154
|
+
this.#compileError = true
|
|
155
|
+
LOG.error(`Composition in draft-enabled entity can't lead to another entity with "@odata.draft.enabled" (in entity: "${entityName}"/element: ${elem.name})!`)
|
|
156
|
+
delete draftEntities[draftEntityName]
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
184
159
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
160
|
+
if (!this.#isDraftEnabled(draftEntity) && !draftEntities[draftEntityName]) {
|
|
161
|
+
this.#collectDraftEntitiesInto(draftEntity, draftEntityName, rootEntity, draftEntities)
|
|
162
|
+
}
|
|
188
163
|
}
|
|
189
164
|
}
|
|
190
165
|
|
|
191
166
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* @param {EntityCSN} entity - entity to propagate all compositions from.
|
|
167
|
+
* @param {EntityCSN} entity - entity to check
|
|
168
|
+
* @returns {boolean}
|
|
195
169
|
*/
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for (const comp of Object.values(entity.compositions ?? {})) {
|
|
200
|
-
const target = this.#getDefinition(comp.target)
|
|
201
|
-
const current = this.#getDraftable(target)
|
|
202
|
-
if (!current) {
|
|
203
|
-
this.#setDraftable(target, true)
|
|
204
|
-
this.#propagateCompositions(target)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
170
|
+
#isDraftEnabled(entity) {
|
|
171
|
+
return entity[DRAFT_ENABLED_ANNO] === true
|
|
207
172
|
}
|
|
208
173
|
|
|
209
174
|
/** @param {CSN} csn - the full csn */
|
|
210
|
-
|
|
211
|
-
|
|
175
|
+
run(csn) {
|
|
176
|
+
if (!csn) return
|
|
212
177
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
178
|
+
this.#csn = csn
|
|
179
|
+
this.#serviceNames = this.#getServiceNames()
|
|
180
|
+
this.#draftRoots = this.#collectDraftRoots()
|
|
217
181
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.#
|
|
222
|
-
}
|
|
182
|
+
for (const draftRoot of this.#draftRoots) {
|
|
183
|
+
/** @type {Record<string,EntityCSN>} */
|
|
184
|
+
const draftEntities = {}
|
|
185
|
+
this.#collectDraftEntitiesInto(draftRoot, draftRoot.name, draftRoot, draftEntities)
|
|
223
186
|
|
|
224
|
-
|
|
187
|
+
for (const draftNode of Object.values(draftEntities)) {
|
|
188
|
+
draftEnabledEntities.push(draftNode.name)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* If an unreconcilable draft model error occurred, the whole type generation
|
|
193
|
+
* will be cancelled. This aligns with the behavior of commands like e.g.
|
|
194
|
+
* - cds compile srv -4 odata
|
|
195
|
+
* - cds compile srv -4 sql
|
|
196
|
+
* - cds watch
|
|
197
|
+
*/
|
|
198
|
+
if (this.#compileError) throw new Error('Compilation of model failed')
|
|
225
199
|
}
|
|
226
200
|
}
|
|
227
201
|
|
|
228
202
|
// note to self: following doc uses @ homoglyph instead of @, as the latter apparently has special semantics in code listings
|
|
229
203
|
/**
|
|
230
|
-
* We
|
|
231
|
-
*
|
|
204
|
+
* We collect all entities that are draft enabled.
|
|
205
|
+
* (@see `@sap/cds-compiler/lib/transform/draft/db.js#generateDraft`)
|
|
232
206
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
207
|
+
* This includes thwo scenarios:
|
|
208
|
+
* - (a) Entities that are part of a service and have the annotation @odata.draft.enabled
|
|
209
|
+
* - (b) Entities that are draft enabled propagate this property down through compositions.
|
|
210
|
+
* NOTE: The compositions themselves must not be draft enabled, otherwise no draft entity will be generated for them
|
|
236
211
|
* @param {any} csn - the entity
|
|
237
212
|
* @example
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* entity B: F,T {} // draft enabled
|
|
245
|
-
* ```
|
|
213
|
+
* (a)
|
|
214
|
+
* ```cds
|
|
215
|
+
* // service.cds
|
|
216
|
+
* service MyService {
|
|
217
|
+
* @odata.draft.enabled true
|
|
218
|
+
* entity A {}
|
|
246
219
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* @odata.draft.enabled: true
|
|
251
|
-
* entity A as projection on B {}
|
|
252
|
-
* entity B {} // draft enabled
|
|
220
|
+
* @odata.draft.enabled true
|
|
221
|
+
* entity B {}
|
|
222
|
+
* }
|
|
253
223
|
* ```
|
|
254
|
-
*
|
|
255
|
-
* (
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
224
|
+
* @example
|
|
225
|
+
* (b)
|
|
226
|
+
* ```cds
|
|
227
|
+
* // service.cds
|
|
228
|
+
* service MyService {
|
|
229
|
+
* @odata.draft.enabled: true
|
|
230
|
+
* entity A {
|
|
231
|
+
* b: Composition of B
|
|
232
|
+
* }
|
|
233
|
+
* entity B {} // draft enabled
|
|
261
234
|
* }
|
|
262
|
-
* entity B {} // draft enabled
|
|
263
235
|
* ```
|
|
264
236
|
*/
|
|
265
|
-
function
|
|
266
|
-
new
|
|
237
|
+
function collectDraftEnabledEntities(csn) {
|
|
238
|
+
new DraftEnabledEntityCollector().run(csn)
|
|
267
239
|
}
|
|
268
240
|
|
|
269
241
|
/**
|
|
@@ -320,15 +292,6 @@ function propagateForeignKeys(csn) {
|
|
|
320
292
|
}
|
|
321
293
|
}
|
|
322
294
|
|
|
323
|
-
/**
|
|
324
|
-
*
|
|
325
|
-
* @param {any} csn - complete csn
|
|
326
|
-
*/
|
|
327
|
-
function amendCSN(csn) {
|
|
328
|
-
unrollDraftability(csn)
|
|
329
|
-
propagateForeignKeys(csn)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
295
|
/**
|
|
333
296
|
* @param {EntityCSN} entity - the entity
|
|
334
297
|
*/
|
|
@@ -349,7 +312,7 @@ const getProjectionAliases = entity => {
|
|
|
349
312
|
}
|
|
350
313
|
|
|
351
314
|
module.exports = {
|
|
352
|
-
|
|
315
|
+
collectDraftEnabledEntities,
|
|
353
316
|
isView,
|
|
354
317
|
isProjection,
|
|
355
318
|
isViewOrProjection,
|
package/lib/file.js
CHANGED
|
@@ -8,11 +8,11 @@ const { normalise } = require('./components/identifier')
|
|
|
8
8
|
const { empty } = require('./components/typescript')
|
|
9
9
|
const { proxyAccessFunction } = require('./components/javascript')
|
|
10
10
|
const { createObjectOf } = require('./components/wrappers')
|
|
11
|
+
const { configuration } = require('./config')
|
|
11
12
|
|
|
12
13
|
const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
|
|
13
14
|
|
|
14
15
|
/** @typedef {import('./typedefs').file.Namespace} Namespace */
|
|
15
|
-
/** @typedef {import('./typedefs').file.FileOptions} FileOptions */
|
|
16
16
|
|
|
17
17
|
class File {
|
|
18
18
|
/**
|
|
@@ -109,11 +109,9 @@ class Library extends File {
|
|
|
109
109
|
class SourceFile extends File {
|
|
110
110
|
/**
|
|
111
111
|
* @param {string | Path} path - path to the file
|
|
112
|
-
* @param {FileOptions} [options] - options for file output
|
|
113
112
|
*/
|
|
114
|
-
constructor(path
|
|
113
|
+
constructor(path) {
|
|
115
114
|
super()
|
|
116
|
-
this.options = options ?? { useEntitiesProxy: false }
|
|
117
115
|
/** @type {Path} */
|
|
118
116
|
this.path = path instanceof Path ? path : new Path(path.split('.'))
|
|
119
117
|
/** @type {{[key:string]: any}} */
|
|
@@ -159,7 +157,7 @@ class SourceFile extends File {
|
|
|
159
157
|
* @param {boolean} [options.isStatic] - whether the lambda is static
|
|
160
158
|
* @param {{positional?: boolean, named?: boolean}} [options.callStyles] - whether to generate positional and/or named call styles
|
|
161
159
|
* @param {string[]?} [options.doc] - documentation for the operation
|
|
162
|
-
* @returns {string} the stringified lambda
|
|
160
|
+
* @returns {[string,string[],string]} the stringified lambda parts
|
|
163
161
|
* @example
|
|
164
162
|
* ```js
|
|
165
163
|
* // note: these samples are actually simplified! See below.
|
|
@@ -188,21 +186,25 @@ class SourceFile extends File {
|
|
|
188
186
|
const callableSignatures = []
|
|
189
187
|
if (callStyles.positional) {
|
|
190
188
|
const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${normalise(name)}: ${type}`).join(', ') // must not include ? modifiers
|
|
191
|
-
callableSignatures.push(
|
|
189
|
+
callableSignatures.push('// positional',`${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
|
|
192
190
|
}
|
|
193
191
|
if (callStyles.named) {
|
|
194
192
|
const parameterNames = createObjectOf(parameters.map(({name}) => normalise(name)).join(', '))
|
|
195
|
-
callableSignatures.push(
|
|
193
|
+
callableSignatures.push('// named',`${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
|
|
196
194
|
}
|
|
197
195
|
if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
|
|
198
196
|
let prefix = name ? `${normalise(name)}: `: ''
|
|
199
197
|
if (prefix && isStatic) {
|
|
200
198
|
prefix = `static ${prefix}`
|
|
201
199
|
}
|
|
202
|
-
const kindDef = kind ?
|
|
200
|
+
const kindDef = kind ? [`kind: '${kind}'`] : []
|
|
203
201
|
const suffix = initialiser ? ` = ${initialiser}` : ''
|
|
204
|
-
|
|
205
|
-
return
|
|
202
|
+
|
|
203
|
+
return [
|
|
204
|
+
`${prefix} {`,
|
|
205
|
+
[...callableSignatures, '// metadata (do not use)', `__parameters: ${parameterTypeAsObject}, __returns: ${returns}`, ...kindDef],
|
|
206
|
+
`}${suffix}`,
|
|
207
|
+
]
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
/**
|
|
@@ -230,7 +232,8 @@ class SourceFile extends File {
|
|
|
230
232
|
addOperation(name, parameters, returns, kind, doc, callStyles) {
|
|
231
233
|
// this.operations.buffer.add(`// ${kind}`)
|
|
232
234
|
if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
|
|
233
|
-
|
|
235
|
+
const [opener, content, closer] = SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})
|
|
236
|
+
this.operations.buffer.addIndentedBlock(`export declare const ${opener}`, content, closer)
|
|
234
237
|
this.operations.names.push(name)
|
|
235
238
|
}
|
|
236
239
|
|
|
@@ -397,7 +400,7 @@ class SourceFile extends File {
|
|
|
397
400
|
buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
|
|
398
401
|
}
|
|
399
402
|
}
|
|
400
|
-
buffer.
|
|
403
|
+
buffer.blankLine()
|
|
401
404
|
return buffer
|
|
402
405
|
}
|
|
403
406
|
|
|
@@ -437,7 +440,7 @@ class SourceFile extends File {
|
|
|
437
440
|
const namespace = this.path.asNamespace()
|
|
438
441
|
|
|
439
442
|
const boilerplate = [AUTO_GEN_NOTE]
|
|
440
|
-
if (
|
|
443
|
+
if (configuration.useEntitiesProxy) {
|
|
441
444
|
if (namespace === '_') {
|
|
442
445
|
boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
|
|
443
446
|
} else {
|
|
@@ -460,7 +463,7 @@ class SourceFile extends File {
|
|
|
460
463
|
* @returns {{singularRhs: string, pluralRhs: string}}
|
|
461
464
|
*/
|
|
462
465
|
#getEntityExportsRhs(singular, original) {
|
|
463
|
-
if (
|
|
466
|
+
if (configuration.useEntitiesProxy) {
|
|
464
467
|
const namespace = this.path.asNamespace()
|
|
465
468
|
// determine the custom properties for the proxy function call
|
|
466
469
|
const customProps = this.entityProxies[singular] ?? []
|
|
@@ -587,10 +590,27 @@ class Buffer {
|
|
|
587
590
|
|
|
588
591
|
/**
|
|
589
592
|
* Adds an element to the buffer with the current indentation level.
|
|
590
|
-
* @param {string} part - what to attach to the buffer
|
|
593
|
+
* @param {string | (() => string) | ((() => string) | string)[]} part - what to attach to the buffer
|
|
591
594
|
*/
|
|
592
595
|
add(part) {
|
|
593
|
-
|
|
596
|
+
if (typeof part === 'string') {
|
|
597
|
+
this.parts.push(this.currentIndent + part)
|
|
598
|
+
} else if (Array.isArray(part)) {
|
|
599
|
+
for (const p of part) {
|
|
600
|
+
this.add(p) // recurse to have proper indentation
|
|
601
|
+
}
|
|
602
|
+
} else if (typeof part === 'function') {
|
|
603
|
+
this.parts.push(part())
|
|
604
|
+
} else {
|
|
605
|
+
throw new Error(`trying to add something of type ${typeof part} to a Buffer`)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Adds a blank line to the buffer.
|
|
611
|
+
*/
|
|
612
|
+
blankLine() {
|
|
613
|
+
this.add('')
|
|
594
614
|
}
|
|
595
615
|
|
|
596
616
|
/**
|
|
@@ -697,13 +717,6 @@ class FileRepository {
|
|
|
697
717
|
/** @type {{[key:string]: SourceFile}} */
|
|
698
718
|
#files = {}
|
|
699
719
|
|
|
700
|
-
/**
|
|
701
|
-
* @param {FileOptions} options - options to control file
|
|
702
|
-
*/
|
|
703
|
-
constructor(options) {
|
|
704
|
-
this.options = options
|
|
705
|
-
}
|
|
706
|
-
|
|
707
720
|
/**
|
|
708
721
|
* @param {string} name - file name
|
|
709
722
|
* @param {SourceFile} file - the file
|
|
@@ -720,7 +733,7 @@ class FileRepository {
|
|
|
720
733
|
*/
|
|
721
734
|
getNamespaceFile(path) {
|
|
722
735
|
const key = path instanceof Path ? path.asNamespace() : path
|
|
723
|
-
return (this.#files[key] ??= new SourceFile(path
|
|
736
|
+
return (this.#files[key] ??= new SourceFile(path))
|
|
724
737
|
}
|
|
725
738
|
|
|
726
739
|
/**
|