@cap-js/db-service 1.0.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 +9 -0
- package/LICENSE +201 -0
- package/README.md +5 -0
- package/index.js +4 -0
- package/lib/InsertResults.js +95 -0
- package/lib/SQLService.js +288 -0
- package/lib/common/DatabaseService.js +144 -0
- package/lib/cql-functions.js +148 -0
- package/lib/cqn2sql.js +605 -0
- package/lib/cqn4sql.js +1846 -0
- package/lib/deep-queries.js +244 -0
- package/lib/fill-in-keys.js +65 -0
- package/lib/infer/index.js +922 -0
- package/lib/infer/join-tree.js +188 -0
- package/lib/infer/pseudos.js +23 -0
- package/package.json +30 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const { compareJson } = require('@sap/cds/libx/_runtime/cds-services/services/utils/compareJson')
|
|
3
|
+
const { _target_name4 } = require('./SQLService')
|
|
4
|
+
|
|
5
|
+
const handledDeep = Symbol('handledDeep')
|
|
6
|
+
|
|
7
|
+
async function onDeep(req, next) {
|
|
8
|
+
const { query } = req
|
|
9
|
+
// REVISIT: req.target does not match the query.INSERT target for path insert
|
|
10
|
+
// const target = query.sources[Object.keys(query.sources)[0]]
|
|
11
|
+
if (!this.model?.definitions[_target_name4(req.query)]) {
|
|
12
|
+
return next()
|
|
13
|
+
}
|
|
14
|
+
const { target } = this.infer(query)
|
|
15
|
+
if (!hasDeep(query, target)) return next()
|
|
16
|
+
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
|
|
17
|
+
|
|
18
|
+
if (query.UPDATE && !beforeData.length) {
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const queries = getDeepQueries(query, beforeData, target)
|
|
23
|
+
const res = await Promise.all(
|
|
24
|
+
queries.map(query => {
|
|
25
|
+
if (query.INSERT) return this.onINSERT({ query })
|
|
26
|
+
if (query.UPDATE) return this.onUPDATE({ query })
|
|
27
|
+
if (query.DELETE) return this.onSIMPLE({ query })
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
return res[0] ?? 0 // TODO what todo with multiple result responses?
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasDeep = (query, target) => {
|
|
34
|
+
if (handledDeep in query) return
|
|
35
|
+
if (query.DELETE) {
|
|
36
|
+
for (let c in target?.compositions) return true
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
const data =
|
|
40
|
+
query.INSERT?.entries || (query.UPDATE?.data && [query.UPDATE.data]) || (query.UPDATE?.with && [query.UPDATE.with])
|
|
41
|
+
if (data)
|
|
42
|
+
for (const c in target.compositions) {
|
|
43
|
+
for (const row of data) if (row[c] !== undefined) return true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// unofficial config!
|
|
48
|
+
const DEEP_DELETE_MAX_RECURSION_DEPTH =
|
|
49
|
+
(cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4 // we use 4 here as our test data has a max depth of 3
|
|
50
|
+
|
|
51
|
+
// IMPORTANT: Skip only if @cds.persistence.skip is `true` → e.g. this skips skipping targets marked with @cds.persistence.skip: 'if-unused'
|
|
52
|
+
const _hasPersistenceSkip = target => target?.['@cds.persistence.skip'] === true
|
|
53
|
+
|
|
54
|
+
const getColumnsFromDataOrKeys = (data, target) => {
|
|
55
|
+
if (Array.isArray(data)) {
|
|
56
|
+
// loop and get all columns from current level
|
|
57
|
+
const columns = new Set()
|
|
58
|
+
data.forEach(row =>
|
|
59
|
+
Object.keys(row || target.keys)
|
|
60
|
+
.filter(propName => !target.elements[propName]?.isAssociation)
|
|
61
|
+
.forEach(entry => {
|
|
62
|
+
columns.add(entry)
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
return Array.from(columns).map(c => ({ ref: [c] }))
|
|
66
|
+
} else {
|
|
67
|
+
// get all columns from current level
|
|
68
|
+
return Object.keys(data || target.keys)
|
|
69
|
+
.filter(propName => target.elements[propName] && !target.elements[propName].isAssociation)
|
|
70
|
+
.map(c => ({ ref: [c] }))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const _calculateExpandColumns = (target, data, expandColumns = [], elementMap = new Map()) => {
|
|
75
|
+
const compositions = target.compositions || {}
|
|
76
|
+
|
|
77
|
+
if (expandColumns.length === 0) {
|
|
78
|
+
// REVISIT: ensure that all keys are included in the expand columns
|
|
79
|
+
expandColumns.push(...getColumnsFromDataOrKeys(data, target))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const compName in compositions) {
|
|
83
|
+
let compositionData
|
|
84
|
+
if (data === null || (Array.isArray(data) && !data.length)) {
|
|
85
|
+
compositionData = null
|
|
86
|
+
} else {
|
|
87
|
+
compositionData = data[compName]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ignore not provided compositions as nothing happens with them (expect deep delete)
|
|
91
|
+
if (compositionData === undefined) {
|
|
92
|
+
// fill columns in case
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const composition = compositions[compName]
|
|
97
|
+
|
|
98
|
+
const fqn = composition.parent.name + ':' + composition.name
|
|
99
|
+
const seen = elementMap.get(fqn)
|
|
100
|
+
if (seen && seen >= DEEP_DELETE_MAX_RECURSION_DEPTH) {
|
|
101
|
+
// recursion -> abort
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let expandColumn = expandColumns.find(expandColumn => expandColumn.ref[0] === composition.name)
|
|
106
|
+
if (!expandColumn) {
|
|
107
|
+
expandColumn = {
|
|
108
|
+
ref: [composition.name],
|
|
109
|
+
expand: getColumnsFromDataOrKeys(compositionData, composition._target),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expandColumns.push(expandColumn)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// expand deep
|
|
116
|
+
// Make a copy and do not share the same map among brother compositions
|
|
117
|
+
// as we're only interested in deep recursions, not wide recursions.
|
|
118
|
+
const newElementMap = new Map(elementMap)
|
|
119
|
+
newElementMap.set(fqn, (seen && seen + 1) || 1)
|
|
120
|
+
|
|
121
|
+
if (composition.is2many) {
|
|
122
|
+
// expandColumn.expand = getColumnsFromDataOrKeys(compositionData, composition._target)
|
|
123
|
+
if (compositionData === null || compositionData.length === 0) {
|
|
124
|
+
// deep delete, get all subitems until recursion depth
|
|
125
|
+
_calculateExpandColumns(composition._target, null, expandColumn.expand, newElementMap)
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const row of compositionData) {
|
|
130
|
+
_calculateExpandColumns(composition._target, row, expandColumn.expand, newElementMap)
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// to one
|
|
134
|
+
_calculateExpandColumns(composition._target, compositionData, expandColumn.expand, newElementMap)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const getExpandForDeep = (query, target) => {
|
|
140
|
+
const from = query.DELETE?.from || query.UPDATE?.entity
|
|
141
|
+
const data = query.UPDATE?.data || null
|
|
142
|
+
const where = query.DELETE?.where || query.UPDATE?.where
|
|
143
|
+
|
|
144
|
+
const cqn = SELECT.from(from)
|
|
145
|
+
if (where) cqn.SELECT.where = where
|
|
146
|
+
|
|
147
|
+
const columns = []
|
|
148
|
+
_calculateExpandColumns(target, data, columns)
|
|
149
|
+
cqn.columns(columns)
|
|
150
|
+
return cqn
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const getDeepQueries = (query, dbData, target) => {
|
|
154
|
+
let queryData
|
|
155
|
+
if (query.INSERT) {
|
|
156
|
+
queryData = query.INSERT.entries
|
|
157
|
+
}
|
|
158
|
+
if (query.DELETE) {
|
|
159
|
+
queryData = []
|
|
160
|
+
}
|
|
161
|
+
if (query.UPDATE) {
|
|
162
|
+
queryData = [query.UPDATE.data]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let diff = compareJson(queryData, dbData, target)
|
|
166
|
+
if (!Array.isArray(diff)) {
|
|
167
|
+
diff = [diff]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return _getDeepQueries(diff, target)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const _hasManagedElements = target => {
|
|
174
|
+
return Object.keys(target.elements).filter(elementName => target.elements[elementName]['@cds.on.update']).length > 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const _getDeepQueries = (diff, target) => {
|
|
178
|
+
const queries = []
|
|
179
|
+
|
|
180
|
+
for (const diffEntry of diff) {
|
|
181
|
+
if (diffEntry === undefined) continue
|
|
182
|
+
const subQueries = []
|
|
183
|
+
|
|
184
|
+
for (const prop in diffEntry) {
|
|
185
|
+
// handle deep operations
|
|
186
|
+
|
|
187
|
+
const propData = diffEntry[prop]
|
|
188
|
+
|
|
189
|
+
if (target.elements[prop] && _hasPersistenceSkip(target.elements[prop]._target)) {
|
|
190
|
+
delete diffEntry[prop]
|
|
191
|
+
} else if (target.compositions?.[prop]) {
|
|
192
|
+
const arrayed = Array.isArray(propData) ? propData : [propData]
|
|
193
|
+
arrayed.forEach(subEntry => {
|
|
194
|
+
subQueries.push(..._getDeepQueries([subEntry], target.elements[prop]._target))
|
|
195
|
+
})
|
|
196
|
+
delete diffEntry[prop]
|
|
197
|
+
} else if (diffEntry[prop] === undefined) {
|
|
198
|
+
// restore current behavior, if property is undefined, not part of payload
|
|
199
|
+
delete diffEntry[prop]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// handle current entity level
|
|
204
|
+
const op = diffEntry._op
|
|
205
|
+
delete diffEntry._op
|
|
206
|
+
|
|
207
|
+
if (diffEntry._old != null) {
|
|
208
|
+
delete diffEntry._old
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// first calculate subqueries and rm their properties, then build root query
|
|
212
|
+
if (op === 'create') {
|
|
213
|
+
queries.push(INSERT.into(target).entries(diffEntry))
|
|
214
|
+
} else if (op === 'delete') {
|
|
215
|
+
queries.push(DELETE.from(target).where(diffEntry))
|
|
216
|
+
} else if (op === 'update' || (op === undefined && subQueries.length && _hasManagedElements(target))) {
|
|
217
|
+
// TODO do we need the where here?
|
|
218
|
+
const keys = target.keys
|
|
219
|
+
const cqn = UPDATE(target).with(diffEntry)
|
|
220
|
+
for (const key in keys) {
|
|
221
|
+
if (keys[key].virtual) continue
|
|
222
|
+
if (!keys[key].isAssociation) {
|
|
223
|
+
cqn.where(key + '=', diffEntry[key])
|
|
224
|
+
}
|
|
225
|
+
delete diffEntry[key]
|
|
226
|
+
}
|
|
227
|
+
cqn.with(diffEntry)
|
|
228
|
+
queries.push(cqn)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
queries.push(...subQueries)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
queries.forEach(q => {
|
|
235
|
+
Object.defineProperty(q, handledDeep, { value: true })
|
|
236
|
+
})
|
|
237
|
+
return queries
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
onDeep,
|
|
242
|
+
getDeepQueries,
|
|
243
|
+
getExpandForDeep,
|
|
244
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
3
|
+
// REVISIT: very deep & fragile dependencies to internal modules -> copy these into here
|
|
4
|
+
const propagateForeignKeys = require('@sap/cds/libx/_runtime/common/utils/propagateForeignKeys')
|
|
5
|
+
const { enrichDataWithKeysFromWhere } = require('@sap/cds/libx/_runtime/common/utils/keys')
|
|
6
|
+
|
|
7
|
+
const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
8
|
+
if (!data) return
|
|
9
|
+
const keys = target.keys
|
|
10
|
+
for (const key in keys) {
|
|
11
|
+
if (keys[key].type === 'cds.UUID' && !data[key] && event === 'CREATE') {
|
|
12
|
+
data[key] = cds.utils.uuid()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const elements = target.elements
|
|
16
|
+
for (const element in elements) {
|
|
17
|
+
// if assoc keys are structured, do not ignore them, as they need to be flattened in propagateForeignKeys
|
|
18
|
+
if (
|
|
19
|
+
elements[element].key &&
|
|
20
|
+
!(elements[element]._isAssociationStrict && elements[element].is2one && element in data)
|
|
21
|
+
) {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (elements[element].is2one || elements[element].is2many) {
|
|
26
|
+
let subData = data[element]
|
|
27
|
+
if (subData) {
|
|
28
|
+
if (!Array.isArray(subData)) {
|
|
29
|
+
subData = [subData]
|
|
30
|
+
}
|
|
31
|
+
for (const sub of subData) {
|
|
32
|
+
// For subData the event is set to 'CREATE' as require UUID generation
|
|
33
|
+
generateUUIDandPropagateKeys(elements[element]._target, sub, 'CREATE')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
|
|
38
|
+
deleteAssocs: true,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = async function fill_in_keys(req, next) {
|
|
45
|
+
// REVISIT dummy handler until we have input processing
|
|
46
|
+
if (!req.target || !this.model || req.target._unresolved) return next()
|
|
47
|
+
|
|
48
|
+
if (req.event === 'UPDATE') {
|
|
49
|
+
// REVISIT for deep update we need to inject the keys first
|
|
50
|
+
enrichDataWithKeysFromWhere(req.data, req, this)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// REVISIT no input processing for INPUT with rows/values
|
|
54
|
+
if (req.event !== 'DELETE' && !(req.query.INSERT?.rows || req.query.INSERT?.values)) {
|
|
55
|
+
if (Array.isArray(req.data)) {
|
|
56
|
+
for (const d of req.data) {
|
|
57
|
+
generateUUIDandPropagateKeys(req.target, d, req.event)
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
generateUUIDandPropagateKeys(req.target, req.data, req.event)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return next()
|
|
65
|
+
}
|