@sap/cds 8.1.0 → 8.1.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/index.js +3 -2
- package/lib/linked/validate.js +3 -2
- package/lib/req/context.js +1 -0
- package/lib/req/locale.js +1 -1
- package/lib/srv/srv-tx.js +1 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +10 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +33 -2
- package/libx/_runtime/fiori/lean-draft.js +15 -6
- package/libx/_runtime/remote/Service.js +3 -1
- package/libx/odata/middleware/create.js +5 -0
- package/libx/odata/middleware/delete.js +5 -0
- package/libx/odata/middleware/error.js +1 -0
- package/libx/odata/middleware/operation.js +6 -0
- package/libx/odata/middleware/read.js +10 -1
- package/libx/odata/middleware/update.js +9 -4
- package/libx/odata/parse/multipartToJson.js +1 -1
- package/libx/rest/middleware/error.js +1 -0
- 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
|
+
## Version 8.1.1 - 2024-08-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- For `accept-language`, ignore additional options
|
|
12
|
+
- Global `describe`, `before`, `beforeAll`, `afterAll` hooks are now writable again. They were accidentally made read-only in 8.0.0.
|
|
13
|
+
- Expand to `DraftAdministrativeData` for active instances of draft-enabled entities over drafts
|
|
14
|
+
- Deduplication of columns for certain on conditions for the legacy database driver
|
|
15
|
+
- For legacy-sqlite/-hana: Add keys to expands with only non-key elements to ensure not returning null for expand.
|
|
16
|
+
- New parser was to restrictive regarding an empty line at the end of batch body.
|
|
17
|
+
- Error target for operations with complex parameters
|
|
18
|
+
- Remote services: JWT gets found in authorization header
|
|
19
|
+
- Search with invalid characters
|
|
20
|
+
- Invoke `srv.on('error')` for each failing batch subrequest
|
|
21
|
+
|
|
7
22
|
## Version 8.1.0 - 2024-07-26
|
|
8
23
|
|
|
9
24
|
### Added
|
package/lib/index.js
CHANGED
|
@@ -137,8 +137,9 @@ extend (global) .with (class {
|
|
|
137
137
|
|
|
138
138
|
// ensure cds.test is loaded for running tests w/ node --test
|
|
139
139
|
'describe' in global || ['describe','before','beforeAll','afterAll'].forEach (p => {
|
|
140
|
-
Object.defineProperty (global,p, { configurable:1,
|
|
141
|
-
|
|
140
|
+
Object.defineProperty (global,p, { configurable:1,
|
|
141
|
+
set(v){ Object.defineProperty (global,p,{ value:v, writable:true }) },
|
|
142
|
+
get(){ cds.test; return global[p] }
|
|
142
143
|
})
|
|
143
144
|
})
|
|
144
145
|
|
package/lib/linked/validate.js
CHANGED
|
@@ -26,15 +26,16 @@ class Validation {
|
|
|
26
26
|
const err = (this.errors ??= new ValidationErrors).add (code)
|
|
27
27
|
if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefic 'in/' for actions
|
|
28
28
|
if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p,n)=> (
|
|
29
|
-
n?.row ? p + this.filter4(n) :
|
|
29
|
+
n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
|
|
30
30
|
typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
|
|
31
|
-
p && n ? p+'/'+n : n //> some/element
|
|
31
|
+
p && n ? p+'/'+n : n //> some/element...
|
|
32
32
|
),'')
|
|
33
33
|
if (val) err.args = [ val, ...args ]
|
|
34
34
|
return err
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
filter4 ({ def, row, index }) {
|
|
38
|
+
if (this.target.kind in { 'action': 1, 'function': 1 }) return '' //> no filter for operations
|
|
38
39
|
const entity = def._target || def, filter=[]
|
|
39
40
|
for (let k in entity.keys) {
|
|
40
41
|
let v = row[k]
|
package/lib/req/context.js
CHANGED
|
@@ -132,6 +132,7 @@ class EventContext {
|
|
|
132
132
|
if (h) super.headers = h
|
|
133
133
|
}
|
|
134
134
|
get headers() {
|
|
135
|
+
// REVISIT: isn't "this._.req?.headers" deprecated? shouldn't it be "this.http?.req?.headers"?
|
|
135
136
|
let headers = this._.req?.headers
|
|
136
137
|
if (!headers) { headers={}
|
|
137
138
|
const outer = this._propagated.headers
|
package/lib/req/locale.js
CHANGED
|
@@ -22,7 +22,7 @@ const from_req = req => req && (
|
|
|
22
22
|
|
|
23
23
|
function req_locale (req) {
|
|
24
24
|
const locale = from_req(req); if (!locale) return i18n.default_language
|
|
25
|
-
const loc = locale.replace(/-/g,'_')
|
|
25
|
+
const loc = locale.replace(/-/g,'_').match(/^[^,; ]*/)[0]
|
|
26
26
|
return INCLUDE_LIST[loc]
|
|
27
27
|
|| /^([a-z]+)/i.test(loc) && RegExp.$1.toLowerCase()
|
|
28
28
|
|| i18n.default_language
|
package/lib/srv/srv-tx.js
CHANGED
|
@@ -96,6 +96,7 @@ class Transaction {
|
|
|
96
96
|
* synchronous modification of passed error only
|
|
97
97
|
* err is undefined if nested tx (cf. "root.before ('failed', ()=> this.rollback())")
|
|
98
98
|
*/
|
|
99
|
+
// FIXME: with noa, this.context === cds.context and not the individual cds.Request
|
|
99
100
|
if (err) for (const each of this._handlers._error) each.handler.call(this, err, this.context)
|
|
100
101
|
|
|
101
102
|
if (this.ready) { //> nothing to do if no transaction started at all
|
|
@@ -760,7 +760,16 @@ const _convertSelect = (query, model, _options) => {
|
|
|
760
760
|
query.SELECT.search = query.SELECT.search[0].val
|
|
761
761
|
.match(/("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/g)
|
|
762
762
|
.filter(el => el.length)
|
|
763
|
-
.map(el =>
|
|
763
|
+
.map(el => {
|
|
764
|
+
if (el[0] === '"' && el.at(-1) === '"') {
|
|
765
|
+
try {
|
|
766
|
+
return JSON.parse(el)
|
|
767
|
+
} catch {
|
|
768
|
+
return el
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return el
|
|
772
|
+
})
|
|
764
773
|
.reduce((arr, val, i) => {
|
|
765
774
|
if (i > 0) arr.push('and')
|
|
766
775
|
arr.push({ val })
|
|
@@ -779,6 +779,20 @@ class JoinCQNFromExpanded {
|
|
|
779
779
|
})
|
|
780
780
|
|
|
781
781
|
const targetEntity = this._getEntityForTable(target)
|
|
782
|
+
|
|
783
|
+
// ignore structured keys for now
|
|
784
|
+
let targetKeys = entity_keys(targetEntity).filter(key => !targetEntity.keys[key]._isStructured)
|
|
785
|
+
// ignore groupBy for now
|
|
786
|
+
if (targetKeys.length > 0 && !readToOneCQN.groupBy) {
|
|
787
|
+
const notOnlyExpandInColumns = !givenColumns.some(col => col.expand)
|
|
788
|
+
if (notOnlyExpandInColumns) {
|
|
789
|
+
const missingKeys = targetKeys.filter(keyName => !givenColumns.some(col => keyName === col.ref?.[0]))
|
|
790
|
+
if (missingKeys.length) {
|
|
791
|
+
givenColumns.push(...missingKeys.map(keyName => ({ ref: [keyName] })))
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
782
796
|
if (
|
|
783
797
|
'IsActiveEntity' in targetEntity.elements &&
|
|
784
798
|
this._isNotIncludedIn(givenColumns)('IsActiveEntity') &&
|
|
@@ -1365,11 +1379,28 @@ class JoinCQNFromExpanded {
|
|
|
1365
1379
|
const columns = []
|
|
1366
1380
|
const outerColumns = []
|
|
1367
1381
|
|
|
1382
|
+
const _sameRef = (col1, col2) => {
|
|
1383
|
+
if (!col1.ref || !col2.ref) return false // only handle refs
|
|
1384
|
+
if (col1.ref.length !== col2.ref.length) return false
|
|
1385
|
+
if (col1.as !== col2.as) return false
|
|
1386
|
+
for (let i = 0; i < col1.ref.length; i++) {
|
|
1387
|
+
if (col1.ref[i] !== col2.ref[i]) return false
|
|
1388
|
+
}
|
|
1389
|
+
return true
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1368
1392
|
for (const entry of on) {
|
|
1369
1393
|
if (entry.xpr) {
|
|
1370
1394
|
const { columns: cols, outerColumns: outerCols } = this._getFilterColumns(readToOneCQN, entry.xpr, parentAlias)
|
|
1371
|
-
|
|
1372
|
-
|
|
1395
|
+
|
|
1396
|
+
// de-duplicate
|
|
1397
|
+
for (const col of cols) {
|
|
1398
|
+
if (!columns.some(c => _sameRef(c, col))) columns.push(col)
|
|
1399
|
+
}
|
|
1400
|
+
for (const col of outerCols) {
|
|
1401
|
+
if (!outerColumns.some(c => _sameRef(c, col))) outerColumns.push(col)
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1373
1404
|
continue
|
|
1374
1405
|
}
|
|
1375
1406
|
|
|
@@ -1081,14 +1081,23 @@ function _cleansed(query, model) {
|
|
|
1081
1081
|
}
|
|
1082
1082
|
cds.infer(draftsQuery, model.definitions)
|
|
1083
1083
|
// draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
|
|
1084
|
-
if (query.SELECT.columns && query._target.drafts)
|
|
1085
|
-
|
|
1084
|
+
if (query.SELECT.columns && query._target.drafts) {
|
|
1085
|
+
if (draftsQuery._target.isDraft)
|
|
1086
|
+
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
|
|
1087
|
+
else draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, DRAFT_ELEMENTS, draft)
|
|
1088
|
+
}
|
|
1086
1089
|
|
|
1087
|
-
if (query.SELECT.where && query._target.drafts)
|
|
1088
|
-
|
|
1090
|
+
if (query.SELECT.where && query._target.drafts) {
|
|
1091
|
+
if (draftsQuery._target.isDraft)
|
|
1092
|
+
draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
|
|
1093
|
+
else draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS)
|
|
1094
|
+
}
|
|
1089
1095
|
|
|
1090
|
-
if (query.SELECT.orderBy && query._target.drafts)
|
|
1091
|
-
|
|
1096
|
+
if (query.SELECT.orderBy && query._target.drafts) {
|
|
1097
|
+
if (draftsQuery._target.isDraft)
|
|
1098
|
+
draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
|
|
1099
|
+
else draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, DRAFT_ELEMENTS)
|
|
1100
|
+
}
|
|
1092
1101
|
|
|
1093
1102
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
1094
1103
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
@@ -261,7 +261,9 @@ class RemoteService extends cds.Service {
|
|
|
261
261
|
const returnType = req._returnType
|
|
262
262
|
const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions }
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
// REVISIT: i don't believe req.context.headers is an official API
|
|
265
|
+
let jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
|
|
266
|
+
if (!jwt) jwt = req?.context?.http?.req?.headers?.authorization?.split(/^bearer /i)[1]
|
|
265
267
|
if (jwt) additionalOptions.jwt = jwt
|
|
266
268
|
|
|
267
269
|
// hidden compat flag in order to suppress logging response body of failed request
|
|
@@ -85,6 +85,11 @@ module.exports = (adapter, isUpsert) => {
|
|
|
85
85
|
.catch(err => {
|
|
86
86
|
handleSapMessages(cdsReq, req, res)
|
|
87
87
|
|
|
88
|
+
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
89
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
|
|
90
|
+
for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
next(err)
|
|
89
94
|
})
|
|
90
95
|
}
|
|
@@ -62,6 +62,11 @@ module.exports = adapter => {
|
|
|
62
62
|
.catch(err => {
|
|
63
63
|
handleSapMessages(cdsReq, req, res)
|
|
64
64
|
|
|
65
|
+
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
66
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
|
|
67
|
+
for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
next(err)
|
|
66
71
|
})
|
|
67
72
|
}
|
|
@@ -7,6 +7,7 @@ const { normalizeError, unwrapMultipleErrors } = require('../../_runtime/common/
|
|
|
7
7
|
module.exports = () => {
|
|
8
8
|
return function odata_error(err, req, res, next) {
|
|
9
9
|
if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
|
|
10
|
+
|
|
10
11
|
// REVISIT: keep?
|
|
11
12
|
// log the error (4xx -> warn)
|
|
12
13
|
_log(err)
|
|
@@ -134,6 +134,12 @@ module.exports = adapter => {
|
|
|
134
134
|
})
|
|
135
135
|
.catch(err => {
|
|
136
136
|
handleSapMessages(cdsReq, req, res)
|
|
137
|
+
|
|
138
|
+
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
139
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
|
|
140
|
+
for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
next(err)
|
|
138
144
|
})
|
|
139
145
|
}
|
|
@@ -164,6 +164,11 @@ const _handleArrayOfQueriesFactory = adapter => {
|
|
|
164
164
|
.catch(err => {
|
|
165
165
|
handleSapMessages(cdsReq, req, res)
|
|
166
166
|
|
|
167
|
+
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
168
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
|
|
169
|
+
for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
|
|
170
|
+
}
|
|
171
|
+
|
|
167
172
|
next(err)
|
|
168
173
|
})
|
|
169
174
|
}
|
|
@@ -264,9 +269,13 @@ module.exports = adapter => {
|
|
|
264
269
|
res.send(result)
|
|
265
270
|
})
|
|
266
271
|
.catch(err => {
|
|
267
|
-
// REVISIT: move error middleware -> applies to all these anti patterns
|
|
268
272
|
handleSapMessages(cdsReq, req, res)
|
|
269
273
|
|
|
274
|
+
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
275
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
|
|
276
|
+
for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
|
|
277
|
+
}
|
|
278
|
+
|
|
270
279
|
next(err)
|
|
271
280
|
})
|
|
272
281
|
}
|
|
@@ -127,13 +127,13 @@ module.exports = adapter => {
|
|
|
127
127
|
result = getODataResult(result, metadata, { property: _propertyAccess })
|
|
128
128
|
res.send(result)
|
|
129
129
|
})
|
|
130
|
-
.catch(
|
|
130
|
+
.catch(err => {
|
|
131
131
|
handleSapMessages(cdsReq, req, res)
|
|
132
132
|
|
|
133
133
|
// if UPSERT is allowed, redirect to POST
|
|
134
|
-
const is404 =
|
|
134
|
+
const is404 = err.code === 404 || err.status === 404 || err.statusCode === 404
|
|
135
135
|
const isForcedInsert =
|
|
136
|
-
(
|
|
136
|
+
(err.code === 412 || err.status === 412 || err.statusCode === 412) &&
|
|
137
137
|
extractIfNoneMatch(req.headers?.['if-none-match']) === '*'
|
|
138
138
|
if (!_propertyAccess && (is404 || isForcedInsert) && _isUpsertAllowed({ target, data, event: req.method })) {
|
|
139
139
|
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
|
|
@@ -147,8 +147,13 @@ module.exports = adapter => {
|
|
|
147
147
|
return next()
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// REVISIT: invoke service.on('error') for failed batch subrequests
|
|
151
|
+
if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
|
|
152
|
+
for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
|
|
153
|
+
}
|
|
154
|
+
|
|
150
155
|
// continue with caught error
|
|
151
|
-
next(
|
|
156
|
+
next(err)
|
|
152
157
|
})
|
|
153
158
|
}
|
|
154
159
|
}
|
|
@@ -105,7 +105,7 @@ const parseStream = async function* (body, boundary) {
|
|
|
105
105
|
chunk = `${leftover}${chunk}`
|
|
106
106
|
const lastBoundary = chunk.lastIndexOf('--')
|
|
107
107
|
const lastCRLF = chunk.lastIndexOf(CRLF)
|
|
108
|
-
if (lastBoundary > lastCRLF) {
|
|
108
|
+
if (lastBoundary > lastCRLF && lastBoundary + 2 < chunk.length) {
|
|
109
109
|
leftover = chunk.slice(lastBoundary)
|
|
110
110
|
chunk = chunk.slice(0, lastBoundary)
|
|
111
111
|
} else {
|
|
@@ -54,6 +54,7 @@ module.exports = () => {
|
|
|
54
54
|
|
|
55
55
|
return function rest_error(err, req, res, next) {
|
|
56
56
|
if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
|
|
57
|
+
|
|
57
58
|
// REVISIT: keep?
|
|
58
59
|
// log the error (4xx -> warn)
|
|
59
60
|
_log(err)
|