@cap-js/db-service 2.0.0 → 2.0.1
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 +15 -0
- package/lib/SQLService.js +21 -31
- package/lib/cqn2sql.js +6 -4
- package/lib/cqn4sql.js +1 -5
- package/lib/search.js +25 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.0.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.0.0...db-service-v2.0.1) (2025-05-27)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* **`search`:** do not search on non-projected elements ([#1198](https://github.com/cap-js/cds-dbs/issues/1198)) ([73d9e67](https://github.com/cap-js/cds-dbs/commit/73d9e67b1bc7d7727c04b4577cb73f4daaed852b))
|
|
13
|
+
* add shortcut for empty UPDATE.data ([#1203](https://github.com/cap-js/cds-dbs/issues/1203)) ([cf991ff](https://github.com/cap-js/cds-dbs/commit/cf991ff8179efee6a4621d2a2bd8bf6265e58893))
|
|
14
|
+
* hierarchies in quoted mode ([3465cba](https://github.com/cap-js/cds-dbs/commit/3465cbab579d4560d12d3b230c55b746d4d3f5a5))
|
|
15
|
+
* only sort by locale if locale is set ([#1193](https://github.com/cap-js/cds-dbs/issues/1193)) ([3465cba](https://github.com/cap-js/cds-dbs/commit/3465cbab579d4560d12d3b230c55b746d4d3f5a5))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
* remove stream_compat ([#1139](https://github.com/cap-js/cds-dbs/issues/1139)) ([#1144](https://github.com/cap-js/cds-dbs/issues/1144)) ([1b8b2d9](https://github.com/cap-js/cds-dbs/commit/1b8b2d9539cd97be2cef088c98d88ef9ec7dd1bf))
|
|
21
|
+
|
|
7
22
|
## [2.0.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v2.0.0) (2025-05-07)
|
|
8
23
|
|
|
9
24
|
|
package/lib/SQLService.js
CHANGED
|
@@ -11,6 +11,20 @@ const BINARY_TYPES = {
|
|
|
11
11
|
'cds.hana.BINARY': 1
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if parameter is an object that at least contains one property.
|
|
16
|
+
*
|
|
17
|
+
* @param {*} obj
|
|
18
|
+
* @returns Boolean
|
|
19
|
+
*/
|
|
20
|
+
const _hasProps = (obj) => {
|
|
21
|
+
if (!obj) return false
|
|
22
|
+
for (const p in obj) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
/** @typedef {import('@sap/cds/apis/services').Request} Request */
|
|
15
29
|
|
|
16
30
|
/**
|
|
@@ -57,24 +71,18 @@ class SQLService extends DatabaseService {
|
|
|
57
71
|
return super.init()
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
_changeToStreams(columns, rows, one
|
|
74
|
+
_changeToStreams(columns, rows, one) {
|
|
61
75
|
if (!rows || !columns) return
|
|
62
76
|
if (!Array.isArray(rows)) rows = [rows]
|
|
63
|
-
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
64
|
-
|
|
65
|
-
// REVISIT: remove after removing stream_compat feature flag
|
|
66
|
-
if (compat) {
|
|
67
|
-
rows[0][Object.keys(rows[0])[0]] = this._stream(Object.values(rows[0])[0])
|
|
68
|
-
return
|
|
69
|
-
}
|
|
77
|
+
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
70
78
|
|
|
71
79
|
let changes = false
|
|
72
80
|
for (let col of columns) {
|
|
73
81
|
const name = col.as || col.ref?.[col.ref.length - 1] || (typeof col === 'string' && col)
|
|
74
82
|
if (col.element?.isAssociation) {
|
|
75
|
-
if (one) this._changeToStreams(col.SELECT.columns, rows[0][name], false
|
|
83
|
+
if (one) this._changeToStreams(col.SELECT.columns, rows[0][name], false)
|
|
76
84
|
else
|
|
77
|
-
changes = rows.some(row => !this._changeToStreams(col.SELECT.columns, row[name], false
|
|
85
|
+
changes = rows.some(row => !this._changeToStreams(col.SELECT.columns, row[name], false))
|
|
78
86
|
} else if (col.element?.type === 'cds.LargeBinary') {
|
|
79
87
|
changes = true
|
|
80
88
|
if (one) rows[0][name] = this._stream(rows[0][name])
|
|
@@ -141,23 +149,7 @@ class SQLService extends DatabaseService {
|
|
|
141
149
|
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
142
150
|
|
|
143
151
|
if (!iterator) {
|
|
144
|
-
|
|
145
|
-
if (cds.env.features.stream_compat) {
|
|
146
|
-
if (query._streaming) {
|
|
147
|
-
if (!rows.length) return
|
|
148
|
-
this._changeToStreams(cqn.SELECT.columns, rows, true, true)
|
|
149
|
-
const result = rows[0]
|
|
150
|
-
|
|
151
|
-
// stream is always on position 0. Further properties like etag are inserted later.
|
|
152
|
-
let [key, val] = Object.entries(result)[0]
|
|
153
|
-
result.value = val
|
|
154
|
-
delete result[key]
|
|
155
|
-
|
|
156
|
-
return result
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
160
|
-
}
|
|
152
|
+
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
|
|
161
153
|
} else if (objectMode) {
|
|
162
154
|
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
163
155
|
const changeToStreams = new Transform({
|
|
@@ -216,8 +208,8 @@ class SQLService extends DatabaseService {
|
|
|
216
208
|
async onUPDATE(req) {
|
|
217
209
|
// noop if not a touch for @cds.on.update
|
|
218
210
|
if (
|
|
219
|
-
!req.query.UPDATE.data &&
|
|
220
|
-
!req.query.UPDATE.with &&
|
|
211
|
+
!_hasProps(req.query.UPDATE.data) &&
|
|
212
|
+
!_hasProps(req.query.UPDATE.with) &&
|
|
221
213
|
!Object.values(req.target?.elements || {}).some(e => e['@cds.on.update'])
|
|
222
214
|
)
|
|
223
215
|
return 0
|
|
@@ -404,8 +396,6 @@ class SQLService extends DatabaseService {
|
|
|
404
396
|
let kind = q.kind || Object.keys(q)[0]
|
|
405
397
|
if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
|
|
406
398
|
q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
|
|
407
|
-
let target = q[kind]._transitions?.[0].target
|
|
408
|
-
if (target) q._target = target // REVISIT: Why isn't that done in resolveView?
|
|
409
399
|
}
|
|
410
400
|
let cqn2sql = new this.class.CQN2SQL(this)
|
|
411
401
|
return cqn2sql.render(q, values)
|
package/lib/cqn2sql.js
CHANGED
|
@@ -338,8 +338,8 @@ class CQN2SQLRenderer {
|
|
|
338
338
|
const parentKeys = []
|
|
339
339
|
const association = target.elements[recurse.ref[0]]
|
|
340
340
|
association._foreignKeys.forEach(fk => {
|
|
341
|
-
nodeKeys.push(
|
|
342
|
-
parentKeys.push(
|
|
341
|
+
nodeKeys.push(fk.childElement.name)
|
|
342
|
+
parentKeys.push(fk.parentElement.name)
|
|
343
343
|
})
|
|
344
344
|
|
|
345
345
|
columnsIn.push(
|
|
@@ -364,11 +364,13 @@ class CQN2SQLRenderer {
|
|
|
364
364
|
// In the case of join operations make sure to compute the hierarchy from the source table only
|
|
365
365
|
const stableFrom = getStableFrom(from)
|
|
366
366
|
const alias = stableFrom.as
|
|
367
|
-
const source = () =>
|
|
367
|
+
const source = () => {
|
|
368
|
+
return ({
|
|
368
369
|
func: 'HIERARCHY',
|
|
369
370
|
args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
|
|
370
371
|
as: alias
|
|
371
372
|
})
|
|
373
|
+
}
|
|
372
374
|
|
|
373
375
|
const expandedByNr = { list: [] } // DistanceTo(...,null)
|
|
374
376
|
const expandedByOne = { list: [] } // DistanceTo(...,1)
|
|
@@ -724,7 +726,7 @@ class CQN2SQLRenderer {
|
|
|
724
726
|
*/
|
|
725
727
|
orderBy(orderBy, localized) {
|
|
726
728
|
return orderBy.map(c => {
|
|
727
|
-
const o = localized
|
|
729
|
+
const o = (localized && this.context.locale)
|
|
728
730
|
? this.expr(c) +
|
|
729
731
|
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
730
732
|
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
package/lib/cqn4sql.js
CHANGED
|
@@ -802,11 +802,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
802
802
|
})
|
|
803
803
|
} else {
|
|
804
804
|
outerAlias = transformedQuery.SELECT.from.as
|
|
805
|
-
|
|
806
|
-
subqueryFromRef = [
|
|
807
|
-
...(transformedQuery.SELECT.from.ref || /* subq in from */ [getInnermostTarget(transformedQuery).name]),
|
|
808
|
-
...ref,
|
|
809
|
-
]
|
|
805
|
+
subqueryFromRef = [transformedQuery._target.name, ...ref]
|
|
810
806
|
}
|
|
811
807
|
|
|
812
808
|
// this is the alias of the column which holds the correlated subquery
|
package/lib/search.js
CHANGED
|
@@ -79,7 +79,7 @@ const _getSearchableColumns = entity => {
|
|
|
79
79
|
const column = entity.elements[columnName]
|
|
80
80
|
if (column?.isAssociation || columnName.includes('.')) {
|
|
81
81
|
deepSearchCandidates.push({ ref: columnName.split('.') })
|
|
82
|
-
continue
|
|
82
|
+
continue
|
|
83
83
|
}
|
|
84
84
|
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
85
85
|
}
|
|
@@ -93,8 +93,8 @@ const _getSearchableColumns = entity => {
|
|
|
93
93
|
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
94
94
|
if (annotatedColumnValue) return true
|
|
95
95
|
|
|
96
|
-
// calculated elements are only searchable if requested through `@cds.search`
|
|
97
|
-
if(column.value) return false
|
|
96
|
+
// calculated elements are only searchable if requested through `@cds.search`
|
|
97
|
+
if (column.value) return false
|
|
98
98
|
|
|
99
99
|
// if at least one element is explicitly annotated as searchable, e.g.:
|
|
100
100
|
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
@@ -112,16 +112,23 @@ const _getSearchableColumns = entity => {
|
|
|
112
112
|
|
|
113
113
|
if (deepSearchCandidates.length) {
|
|
114
114
|
deepSearchCandidates.forEach(c => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
let element = entity
|
|
116
|
+
for (let i = 0; i < c.ref.length; ++i) {
|
|
117
|
+
const curr = c.ref[i]
|
|
118
|
+
const next = element.elements?.[curr] ?? element._target?.elements?.[curr]
|
|
119
|
+
|
|
120
|
+
if (!next) { // e.g. if a search element is not part of a projection
|
|
121
|
+
element = undefined
|
|
122
|
+
break
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
|
|
125
|
+
if (next.isAssociation && i === c.ref.length - 1) {
|
|
126
|
+
_getSearchableColumns(next._target).forEach(r => searchableColumns.push({ ref: c.ref.concat(...r.ref) }))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
element = next
|
|
130
|
+
}
|
|
131
|
+
|
|
125
132
|
if (element?.type === DEFAULT_SEARCHABLE_TYPE) {
|
|
126
133
|
searchableColumns.push({ ref: c.ref })
|
|
127
134
|
}
|
|
@@ -129,9 +136,8 @@ const _getSearchableColumns = entity => {
|
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
return searchableColumns.map(column => {
|
|
132
|
-
if(column.ref)
|
|
133
|
-
|
|
134
|
-
return { ref: [ column.name ] }
|
|
139
|
+
if (column.ref) return column
|
|
140
|
+
return { ref: [column.name] }
|
|
135
141
|
})
|
|
136
142
|
}
|
|
137
143
|
|
|
@@ -183,15 +189,16 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }) =
|
|
|
183
189
|
}
|
|
184
190
|
})
|
|
185
191
|
} else {
|
|
186
|
-
if(entity.kind === 'entity') {
|
|
192
|
+
if (entity.kind === 'entity') {
|
|
187
193
|
// first check cache
|
|
188
|
-
toBeSearched =
|
|
194
|
+
toBeSearched =
|
|
195
|
+
entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
189
196
|
} else {
|
|
190
197
|
// if we search on a subquery, we don't have a cache
|
|
191
198
|
toBeSearched = _getSearchableColumns(entity)
|
|
192
199
|
}
|
|
193
200
|
toBeSearched = toBeSearched.map(c => {
|
|
194
|
-
const column = {ref: [...c.ref]}
|
|
201
|
+
const column = { ref: [...c.ref] }
|
|
195
202
|
return column
|
|
196
203
|
})
|
|
197
204
|
}
|
package/package.json
CHANGED