@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.
@@ -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
+ }