@sap/cds 8.4.1 → 8.5.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 +41 -1
- package/_i18n/messages.properties +99 -0
- package/bin/serve.js +2 -2
- package/lib/compile/cdsc.js +9 -4
- package/lib/compile/to/srvinfo.js +4 -4
- package/lib/core/classes.js +5 -1
- package/lib/core/entities.js +1 -0
- package/lib/core/types.js +1 -1
- package/lib/dbs/cds-deploy.js +4 -1
- package/lib/env/defaults.js +7 -6
- package/lib/env/schemas/cds-rc.js +132 -22
- package/lib/i18n/bundles.js +111 -0
- package/lib/i18n/files.js +134 -0
- package/lib/i18n/index.js +63 -0
- package/lib/i18n/localize.js +101 -237
- package/lib/i18n/resources.js +150 -0
- package/lib/index.js +1 -0
- package/lib/log/format/aspects/cls.js +6 -1
- package/lib/log/format/json.js +1 -1
- package/lib/ql/CREATE.js +1 -0
- package/lib/ql/DELETE.js +1 -0
- package/lib/ql/DROP.js +1 -0
- package/lib/ql/INSERT.js +9 -8
- package/lib/ql/Query.js +18 -8
- package/lib/ql/SELECT.js +1 -0
- package/lib/ql/UPDATE.js +2 -1
- package/lib/ql/UPSERT.js +1 -1
- package/lib/ql/Whereable.js +3 -3
- package/lib/ql/cds-ql.js +12 -18
- package/lib/req/user.js +1 -0
- package/lib/req/validate.js +12 -3
- package/lib/srv/factory.js +2 -2
- package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
- package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
- package/lib/srv/middlewares/auth/ias-auth.js +96 -0
- package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
- package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
- package/lib/srv/middlewares/auth/xssec.js +7 -0
- package/lib/srv/middlewares/index.js +1 -1
- package/lib/utils/cds-utils.js +15 -19
- package/lib/utils/tar.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -6
- package/libx/_runtime/common/error/log.js +7 -8
- package/libx/_runtime/common/error/utils.js +3 -7
- package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
- package/libx/_runtime/common/generic/input.js +41 -6
- package/libx/_runtime/common/i18n/index.js +8 -15
- package/libx/_runtime/common/utils/compareJson.js +10 -1
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +77 -26
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
- package/libx/_runtime/messaging/kafka.js +1 -1
- package/libx/odata/index.js +3 -0
- package/libx/odata/middleware/create.js +8 -6
- package/libx/odata/middleware/update.js +24 -21
- package/libx/odata/parse/afterburner.js +15 -2
- package/libx/odata/parse/grammar.peggy +24 -7
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/postProcess.js +4 -1
- package/libx/rest/RestAdapter.js +2 -1
- package/libx/rest/middleware/error.js +0 -50
- package/package.json +1 -1
- package/lib/auth/ias-auth.js +0 -68
- package/lib/auth/ias-claims.js +0 -34
- package/lib/auth/jwt-auth.js +0 -70
- package/libx/_runtime/common/i18n/messages.properties +0 -99
- package/libx/_runtime/common/utils/require.js +0 -9
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
const cds = require('../../cds')
|
|
11
11
|
const LOG = cds.log('app')
|
|
12
12
|
|
|
13
|
+
const { Readable } = require('node:stream')
|
|
14
|
+
|
|
13
15
|
const { enrichDataWithKeysFromWhere } = require('../utils/keys')
|
|
14
16
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
15
17
|
const propagateForeignKeys = require('../utils/propagateForeignKeys')
|
|
@@ -27,6 +29,13 @@ const _shouldSuppressErrorPropagation = (event, value) => {
|
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
const _sliceBase64 = function* (str) {
|
|
33
|
+
const chunkSize = 1 << 16
|
|
34
|
+
for (let i = 0; i < str.length; i += chunkSize) {
|
|
35
|
+
yield Buffer.from(str.slice(i, i + chunkSize), 'base64')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
const _getSimpleCategory = category => {
|
|
31
40
|
if (typeof category === 'object') {
|
|
32
41
|
category = category.category
|
|
@@ -142,6 +151,16 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
|
142
151
|
if ((event === 'UPDATE' || event === 'CREATE') && category === '@assert.target') {
|
|
143
152
|
_preProcessAssertTarget(elementInfo, assertMap)
|
|
144
153
|
}
|
|
154
|
+
|
|
155
|
+
if (category === 'binary' && typeof row[key] === 'string') {
|
|
156
|
+
row[key] = Buffer.from(row[key], 'base64')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (category === 'largebinary' && typeof row[key] === 'string') {
|
|
161
|
+
row[key] = Readable.from(_sliceBase64(row[key]), { objectMode: false })
|
|
162
|
+
return
|
|
163
|
+
}
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
const _getProcessorFn = (req, errors, assertMap) => {
|
|
@@ -192,6 +211,7 @@ const _pick = element => {
|
|
|
192
211
|
// REVISIT: cleanse @Core.Immutable
|
|
193
212
|
// should be a db feature, as we cannot handle completely on service level (cf. deep update)
|
|
194
213
|
// -> add to attic env behavior once new dbs handle this
|
|
214
|
+
// also happens in validate but because of draft activate we have to do it twice (where cleansing is suppressed)
|
|
195
215
|
if (element['@Core.Immutable']) {
|
|
196
216
|
categories.push('immutable')
|
|
197
217
|
}
|
|
@@ -200,10 +220,6 @@ const _pick = element => {
|
|
|
200
220
|
categories.push('uuid')
|
|
201
221
|
}
|
|
202
222
|
|
|
203
|
-
if (element['@Core.IsMediaType']) {
|
|
204
|
-
categories.push('stream')
|
|
205
|
-
}
|
|
206
|
-
|
|
207
223
|
if (
|
|
208
224
|
element._isAssociationStrict &&
|
|
209
225
|
!element.on && // managed assoc
|
|
@@ -213,6 +229,14 @@ const _pick = element => {
|
|
|
213
229
|
categories.push('@assert.target')
|
|
214
230
|
}
|
|
215
231
|
|
|
232
|
+
if (element.type === 'cds.Binary' && !cds.env.features.base64_binaries) {
|
|
233
|
+
categories.push('binary')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (element.type === 'cds.LargeBinary' && !cds.env.features.base64_binaries) {
|
|
237
|
+
categories.push('largebinary')
|
|
238
|
+
}
|
|
239
|
+
|
|
216
240
|
if (categories.length) return { categories }
|
|
217
241
|
}
|
|
218
242
|
|
|
@@ -226,7 +250,7 @@ async function commonGenericInput(req) {
|
|
|
226
250
|
|
|
227
251
|
// validate data
|
|
228
252
|
if (cds.env.features.cds_validate) {
|
|
229
|
-
const assertOptions = { mandatories: req.event === 'CREATE' || req.
|
|
253
|
+
const assertOptions = { mandatories: req.event === 'CREATE' || req.method === 'PUT' }
|
|
230
254
|
|
|
231
255
|
const _is_activate = req._?.event === 'draftActivate' && cds.env.features.preserve_computed !== false
|
|
232
256
|
const _is_create_after_new = req.target.isDraft && req.event === 'CREATE'
|
|
@@ -346,7 +370,10 @@ function _actionFunctionHandler(req) {
|
|
|
346
370
|
|
|
347
371
|
// validate data
|
|
348
372
|
if (cds.env.features.cds_validate) {
|
|
349
|
-
const assertOptions = {
|
|
373
|
+
const assertOptions = {
|
|
374
|
+
mandatories: true,
|
|
375
|
+
cleanse: !(operation.kind === 'action' || operation.kind === 'function')
|
|
376
|
+
}
|
|
350
377
|
let errs = cds.validate(data, operation, assertOptions)
|
|
351
378
|
if (errs) {
|
|
352
379
|
if (errs.length === 1) throw errs[0]
|
|
@@ -361,6 +388,14 @@ function _actionFunctionHandler(req) {
|
|
|
361
388
|
const arrayData = Array.isArray(data) ? data : [data]
|
|
362
389
|
for (const row of arrayData) _processActionFunction(row, operation.params, errors, req.event, this)
|
|
363
390
|
if (errors.length) for (const error of errors) req.error(error)
|
|
391
|
+
|
|
392
|
+
// convert binaries
|
|
393
|
+
operation.params &&
|
|
394
|
+
!cds.env.features.base64_binaries &&
|
|
395
|
+
Object.keys(operation.params).forEach(key => {
|
|
396
|
+
if (operation.params[key].type === 'cds.Binary' && typeof data[key] === 'string')
|
|
397
|
+
data[key] = Buffer.from(data[key], 'base64')
|
|
398
|
+
})
|
|
364
399
|
}
|
|
365
400
|
|
|
366
401
|
commonGenericInput._initial = true
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
//
|
|
2
|
+
// REVISIT: Not used any longer -> move to @sap/cds-attic ...
|
|
3
|
+
//
|
|
1
4
|
const fs = require('fs')
|
|
2
5
|
const path = require('path')
|
|
3
6
|
|
|
@@ -53,7 +56,7 @@ function init(locale, file) {
|
|
|
53
56
|
i18ns[locale] = props
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
init('default', path.
|
|
59
|
+
init('default', path.resolve(__dirname, '../../../../_i18n/messages.properties'))
|
|
57
60
|
init('')
|
|
58
61
|
|
|
59
62
|
module.exports = (key, locale = '', args = {}) => {
|
|
@@ -69,18 +72,8 @@ module.exports = (key, locale = '', args = {}) => {
|
|
|
69
72
|
|
|
70
73
|
// for locale OR app default OR cds default
|
|
71
74
|
let text = i18ns[locale][key] || i18ns[''][key] || i18ns.default[key]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
for (const match of matches) {
|
|
77
|
-
const arg = args[match.slice(1, -1)]
|
|
78
|
-
const argtext = i18ns[locale][arg] || i18ns[''][arg] || i18ns.default[arg]
|
|
79
|
-
text = text.replace(match, argtext || (arg != null ? arg : 'NULL'))
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
// nothing to do
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return text
|
|
75
|
+
return text?.replace(/{(\w+)}/g, (_, k) => {
|
|
76
|
+
let x = args[k]
|
|
77
|
+
return i18ns[locale][x] || i18ns[''][x] || i18ns.default[x] || (x ?? 'NULL') // REVISIT: i'm afraid this twofold localization is a rather bad idea
|
|
78
|
+
})
|
|
86
79
|
}
|
|
@@ -2,6 +2,7 @@ const cds = require('../../cds')
|
|
|
2
2
|
const { DRAFT_COLUMNS_MAP } = require('../constants/draft')
|
|
3
3
|
|
|
4
4
|
const _deepEqual = (val1, val2) => {
|
|
5
|
+
if (Buffer.isBuffer(val1) && Buffer.isBuffer(val2)) return val1.equals(val2)
|
|
5
6
|
if (val1 && typeof val1 === 'object' && val2 && typeof val2 === 'object') {
|
|
6
7
|
for (const key in val1) {
|
|
7
8
|
if (!_deepEqual(val1[key], val2[key])) return false
|
|
@@ -179,7 +180,15 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts,
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
// if value did not change --> ignored
|
|
182
|
-
if (
|
|
183
|
+
if (
|
|
184
|
+
(Buffer.isBuffer(newEntry[prop]) &&
|
|
185
|
+
oldEntry &&
|
|
186
|
+
Buffer.isBuffer(oldEntry[prop]) &&
|
|
187
|
+
newEntry[prop].equals(oldEntry[prop])) ||
|
|
188
|
+
newEntry[prop] === oldEntry?.[prop]
|
|
189
|
+
) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
183
192
|
|
|
184
193
|
// existing immutable --> ignored
|
|
185
194
|
if (oldEntry && cache.immutables.includes(prop)) continue
|
|
@@ -715,7 +715,7 @@ const resolveView = (query, model, service) => {
|
|
|
715
715
|
|
|
716
716
|
// If the query is a projection, one must follow it
|
|
717
717
|
// to let the underlying service know its true entity.
|
|
718
|
-
if (query.
|
|
718
|
+
if (query.kind) _event = query.kind
|
|
719
719
|
else if (query.SELECT) _event = 'SELECT'
|
|
720
720
|
else if (query.INSERT) _event = 'INSERT'
|
|
721
721
|
else if (query.UPSERT) _event = 'UPSERT'
|
|
@@ -624,20 +624,20 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
-
if (cds.env.features.odata_new_adapter) {
|
|
627
|
+
if (cds.env.features.odata_new_adapter && req.res) {
|
|
628
628
|
const read_result = await _readAfterDraftAction.bind(this)({
|
|
629
629
|
req,
|
|
630
630
|
payload: res,
|
|
631
631
|
action: 'draftActivate'
|
|
632
632
|
})
|
|
633
|
-
req.res
|
|
633
|
+
req.res.set(
|
|
634
634
|
'location',
|
|
635
635
|
'../' + calculateLocationHeader(req.target, this, read_result || { ...res, IsActiveEntity: true })
|
|
636
636
|
)
|
|
637
637
|
return read_result
|
|
638
|
-
} else {
|
|
639
|
-
return Object.assign(result, { IsActiveEntity: true })
|
|
640
638
|
}
|
|
639
|
+
|
|
640
|
+
return Object.assign(result, { IsActiveEntity: true })
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
@@ -826,8 +826,12 @@ const Read = {
|
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
828
|
Read.merge(query._target, actives, drafts, (row, other) => {
|
|
829
|
-
if (other)
|
|
830
|
-
|
|
829
|
+
if (other) {
|
|
830
|
+
if ('DraftAdministrativeData' in other) row.DraftAdministrativeData = other.DraftAdministrativeData
|
|
831
|
+
if ('DraftAdministrativeData_DraftUUID' in other)
|
|
832
|
+
row.DraftAdministrativeData_DraftUUID = other.DraftAdministrativeData_DraftUUID
|
|
833
|
+
Object.assign(row, { HasActiveEntity: false, HasDraftEntity: true })
|
|
834
|
+
} else
|
|
831
835
|
Object.assign(row, {
|
|
832
836
|
HasActiveEntity: false,
|
|
833
837
|
HasDraftEntity: false,
|
|
@@ -1235,44 +1239,61 @@ function _cleansed(query, model) {
|
|
|
1235
1239
|
const q = _cleanseQuery(query, draftParams, model)
|
|
1236
1240
|
if (query.SELECT) {
|
|
1237
1241
|
const getDrafts = () => {
|
|
1238
|
-
|
|
1239
|
-
draftsQuery
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
+
// could just clone `query` but the latter is ruined by database layer
|
|
1243
|
+
const draftsQuery = _cleanseQuery(query, {}, model)
|
|
1244
|
+
|
|
1245
|
+
// set the target to null to ensure cds.infer(...) correctly infer the
|
|
1246
|
+
// target after query modifications
|
|
1247
|
+
draftsQuery._target = null
|
|
1248
|
+
let draftSelect = draftsQuery.SELECT
|
|
1249
|
+
let querySelect = query.SELECT
|
|
1250
|
+
|
|
1251
|
+
// in the $apply scenario, only the most inner nested SELECT data structure must be cleansed
|
|
1252
|
+
while (draftSelect.from.SELECT) {
|
|
1253
|
+
draftSelect = draftSelect.from.SELECT
|
|
1254
|
+
querySelect = querySelect.from.SELECT
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (!draftSelect.from.ref) return // invalid draft request
|
|
1258
|
+
|
|
1259
|
+
const [root, ...tail] = draftSelect.from.ref
|
|
1242
1260
|
const draft = model.definitions[root.id || root].drafts
|
|
1243
1261
|
if (!draft) return
|
|
1244
|
-
|
|
1262
|
+
draftSelect.from = {
|
|
1245
1263
|
ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
1246
1264
|
}
|
|
1247
1265
|
cds.infer(draftsQuery, model.definitions)
|
|
1266
|
+
|
|
1248
1267
|
// draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
|
|
1249
|
-
if (
|
|
1268
|
+
if (querySelect.columns && query._target.drafts) {
|
|
1250
1269
|
if (draftsQuery._target.isDraft)
|
|
1251
|
-
|
|
1252
|
-
else
|
|
1270
|
+
draftSelect.columns = _cleanseCols(querySelect.columns, REDUCED_DRAFT_ELEMENTS, draft)
|
|
1271
|
+
else draftSelect.columns = _cleanseCols(querySelect.columns, DRAFT_ELEMENTS, draft)
|
|
1253
1272
|
}
|
|
1254
1273
|
|
|
1255
|
-
if (
|
|
1274
|
+
if (querySelect.where && query._target.drafts) {
|
|
1256
1275
|
if (draftsQuery._target.isDraft)
|
|
1257
|
-
|
|
1258
|
-
else
|
|
1276
|
+
draftSelect.where = _cleanseWhere(querySelect.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
|
|
1277
|
+
else draftSelect.where = _cleanseWhere(querySelect.where, {}, DRAFT_ELEMENTS)
|
|
1259
1278
|
}
|
|
1260
1279
|
|
|
1261
|
-
if (
|
|
1280
|
+
if (querySelect.orderBy && query._target.drafts) {
|
|
1262
1281
|
if (draftsQuery._target.isDraft)
|
|
1263
|
-
|
|
1264
|
-
else
|
|
1282
|
+
draftSelect.orderBy = _cleanseWhere(querySelect.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
|
|
1283
|
+
else draftSelect.orderBy = _cleanseWhere(querySelect.orderBy, {}, DRAFT_ELEMENTS)
|
|
1265
1284
|
}
|
|
1266
1285
|
|
|
1267
1286
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
1268
|
-
|
|
1287
|
+
draftSelect.columns = _tweakAdminCols(draftSelect.columns)
|
|
1269
1288
|
} else if (draftsQuery._target?.name.endsWith('.drafts')) {
|
|
1270
|
-
|
|
1289
|
+
draftSelect.columns = _tweakAdminExpand(draftSelect.columns)
|
|
1271
1290
|
}
|
|
1291
|
+
|
|
1272
1292
|
draftsQuery[DRAFT_PARAMS] = draftParams
|
|
1273
1293
|
Object.defineProperty(q, '_drafts', { value: draftsQuery })
|
|
1274
1294
|
return draftsQuery
|
|
1275
1295
|
}
|
|
1296
|
+
|
|
1276
1297
|
Object.defineProperty(q, '_drafts', {
|
|
1277
1298
|
configurable: true,
|
|
1278
1299
|
get() {
|
|
@@ -1481,6 +1502,10 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
|
|
|
1481
1502
|
return columns
|
|
1482
1503
|
}
|
|
1483
1504
|
|
|
1505
|
+
async function onNewCleanse(req) {
|
|
1506
|
+
cds.validate(req.data, req.target, {})
|
|
1507
|
+
}
|
|
1508
|
+
onNewCleanse._initial = true
|
|
1484
1509
|
async function onNew(req) {
|
|
1485
1510
|
LOG.debug('new draft')
|
|
1486
1511
|
|
|
@@ -1658,17 +1683,17 @@ async function onEdit(req) {
|
|
|
1658
1683
|
activeLockCQN[DRAFT_PARAMS] = draftParams
|
|
1659
1684
|
|
|
1660
1685
|
try {
|
|
1661
|
-
await
|
|
1686
|
+
await activeLockCQN
|
|
1662
1687
|
} catch (error) {
|
|
1663
1688
|
LOG._debug && LOG.debug('Failed to acquire database lock:', error)
|
|
1664
|
-
const draft = await
|
|
1689
|
+
const draft = await existingDraft
|
|
1665
1690
|
if (draft) req.reject({ code: 409, statusCode: 409, message: 'DRAFT_ALREADY_EXISTS' })
|
|
1666
1691
|
req.reject({ code: 409, statusCode: 409, message: 'ENTITY_LOCKED' })
|
|
1667
1692
|
}
|
|
1668
1693
|
|
|
1669
1694
|
const cqns = [
|
|
1670
1695
|
cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
|
|
1671
|
-
|
|
1696
|
+
existingDraft
|
|
1672
1697
|
]
|
|
1673
1698
|
|
|
1674
1699
|
;[res, draft] = await _promiseAll(cqns)
|
|
@@ -1739,7 +1764,7 @@ async function onEdit(req) {
|
|
|
1739
1764
|
req.res.status(201)
|
|
1740
1765
|
}
|
|
1741
1766
|
|
|
1742
|
-
if (cds.env.features.odata_new_adapter) {
|
|
1767
|
+
if (cds.env.features.odata_new_adapter && req.res) {
|
|
1743
1768
|
const read_result = await _readAfterDraftAction.bind(this)({
|
|
1744
1769
|
req,
|
|
1745
1770
|
payload: res,
|
|
@@ -1877,6 +1902,30 @@ const _readAfterDraftAction = async function ({ req, payload, action }) {
|
|
|
1877
1902
|
|
|
1878
1903
|
module.exports = {
|
|
1879
1904
|
impl() {
|
|
1905
|
+
if (!this.new)
|
|
1906
|
+
this.new = function (entity, data) {
|
|
1907
|
+
return this.send({ event: 'NEW', query: INSERT.into(entity).entries(data ?? {}) })
|
|
1908
|
+
}
|
|
1909
|
+
if (!this.cancel)
|
|
1910
|
+
this.cancel = function (entity, data) {
|
|
1911
|
+
return this.send({ event: 'CANCEL', query: DELETE(entity, data) })
|
|
1912
|
+
}
|
|
1913
|
+
if (!this.edit)
|
|
1914
|
+
this.edit = function (entity, data) {
|
|
1915
|
+
if ((typeof entity === 'string' && entity.endsWith('.drafts')) || entity.isDraft)
|
|
1916
|
+
throw new Error('Action `edit` must be called on the active entity')
|
|
1917
|
+
return this.send({ event: 'EDIT', query: SELECT.from(entity, data).where({ IsActiveEntity: true }) })
|
|
1918
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ makes sure draftParams are set
|
|
1919
|
+
}
|
|
1920
|
+
if (!this.save)
|
|
1921
|
+
this.save = function (entity, data) {
|
|
1922
|
+
// a bit fishy to demand registering it on drafts since `SAVE` is usually just a shortcut for `['CREATE', 'UPDATE']`
|
|
1923
|
+
// and is typically registered for active entities. Hence, we allow to register it on both and redirect to drafts
|
|
1924
|
+
const _entity =
|
|
1925
|
+
typeof entity === 'string' ? (entity.endsWith('.drafts') ? entity : entity + '.drafts') : entity?.drafts
|
|
1926
|
+
return this.send({ event: 'draftActivate', query: SELECT.from(_entity, data) })
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1880
1929
|
if (!this._datasource) this._datasource = cds.db
|
|
1881
1930
|
|
|
1882
1931
|
function _wrapped(handler, isActiveEntity) {
|
|
@@ -1885,10 +1934,12 @@ module.exports = {
|
|
|
1885
1934
|
return next.call(this)
|
|
1886
1935
|
return handler.call(this, req, next)
|
|
1887
1936
|
}
|
|
1937
|
+
if (handler._initial) fn._initial = true
|
|
1888
1938
|
return fn
|
|
1889
1939
|
}
|
|
1890
1940
|
|
|
1891
1941
|
// Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
|
|
1942
|
+
this.before('NEW', '*', _wrapped(onNewCleanse, false))
|
|
1892
1943
|
this.on('NEW', '*', _wrapped(onNew, false))
|
|
1893
1944
|
this.on('EDIT', '*', _wrapped(onEdit, true))
|
|
1894
1945
|
this.on('CANCEL', '*', _wrapped(onCancel, false))
|
|
@@ -15,9 +15,13 @@ class EndpointRegistry {
|
|
|
15
15
|
if (cds.requires.auth.impl) {
|
|
16
16
|
cds.app.use(basePath, cds.middlewares.before) // contains auth, trace, context
|
|
17
17
|
} else {
|
|
18
|
-
const jwt_auth = require('../../../../lib/auth/jwt-auth.js')
|
|
18
|
+
const jwt_auth = require('../../../../lib/srv/middlewares/auth/jwt-auth.js')
|
|
19
19
|
cds.app.use(basePath, cds.middlewares.context())
|
|
20
20
|
cds.app.use(basePath, jwt_auth(cds.requires.auth))
|
|
21
|
+
cds.app.use(basePath, (err, req, res, next) => {
|
|
22
|
+
if (err === 401) res.send(401)
|
|
23
|
+
else next(err)
|
|
24
|
+
})
|
|
21
25
|
}
|
|
22
26
|
// unsuccessful auth doesn't automatically reject!
|
|
23
27
|
cds.app.use(basePath, (req, res, next) => {
|
|
@@ -18,7 +18,7 @@ class KafkaService extends cds.MessagingService {
|
|
|
18
18
|
|
|
19
19
|
if (!this.options.local && !this.options.credentials) {
|
|
20
20
|
throw new Error(
|
|
21
|
-
'No Kafka credentials found.\n\nHint: You need to bind your application to a
|
|
21
|
+
'No Kafka credentials found.\n\nHint: You need to bind your application to a Kafka service instance.'
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
package/libx/odata/index.js
CHANGED
|
@@ -58,9 +58,10 @@ module.exports = (adapter, isUpsert) => {
|
|
|
58
58
|
return service
|
|
59
59
|
.run(() => {
|
|
60
60
|
return service.dispatch(cdsReq).then(result => {
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
if (cdsReq._.readAfterWrite
|
|
61
|
+
// cdsReq._.readAfterWrite is only true if generic handler served the request
|
|
62
|
+
// If minimal requested and not etag, skip read after write
|
|
63
|
+
if (cdsReq._.readAfterWrite && (target._etag || getPreferReturnHeader(req) !== 'minimal'))
|
|
64
|
+
return _readAfterWrite(cdsReq)
|
|
64
65
|
return result
|
|
65
66
|
})
|
|
66
67
|
})
|
|
@@ -75,10 +76,11 @@ module.exports = (adapter, isUpsert) => {
|
|
|
75
76
|
res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
const
|
|
79
|
-
postProcess(cdsReq.target, model, result,
|
|
79
|
+
const preference = getPreferReturnHeader(req)
|
|
80
|
+
postProcess(cdsReq.target, model, result, preference === 'minimal')
|
|
80
81
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
81
|
-
if (
|
|
82
|
+
if (preference === 'minimal') return res.append('Preference-Applied', 'return=minimal').sendStatus(204)
|
|
83
|
+
else if (preference === 'representation') res.append('Preference-Applied', 'return=representation')
|
|
82
84
|
|
|
83
85
|
const metadata = getODataMetadata(query, { result })
|
|
84
86
|
result = getODataResult(result, metadata)
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { UPDATE } = cds.ql
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
calculateLocationHeader,
|
|
6
|
-
handleSapMessages,
|
|
7
|
-
getPreferReturnHeader,
|
|
8
|
-
extractIfNoneMatch,
|
|
9
|
-
isStream
|
|
10
|
-
} = require('../utils')
|
|
4
|
+
const { handleSapMessages, getPreferReturnHeader, extractIfNoneMatch, isStream } = require('../utils')
|
|
11
5
|
const getODataMetadata = require('../utils/metadata')
|
|
12
6
|
const postProcess = require('../utils/postProcess')
|
|
13
7
|
const readAfterWrite4 = require('../utils/readAfterWrite')
|
|
@@ -65,7 +59,9 @@ module.exports = adapter => {
|
|
|
65
59
|
throw Object.assign(new Error(`Method ${req.method} is not allowed for entity collections`), { statusCode: 405 })
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
const _isStream = isStream(req._query)
|
|
63
|
+
|
|
64
|
+
if (_propertyAccess && req.method === 'PATCH' && !_isStream) {
|
|
69
65
|
throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
|
|
70
66
|
}
|
|
71
67
|
|
|
@@ -78,7 +74,6 @@ module.exports = adapter => {
|
|
|
78
74
|
if (!_propertyAccess) Object.assign(data, keys)
|
|
79
75
|
|
|
80
76
|
const _isDraft = target.drafts && data.IsActiveEntity !== true
|
|
81
|
-
|
|
82
77
|
// query
|
|
83
78
|
let query = UPDATE.entity(from).with(data)
|
|
84
79
|
|
|
@@ -86,7 +81,14 @@ module.exports = adapter => {
|
|
|
86
81
|
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
87
82
|
|
|
88
83
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
89
|
-
const cdsReq = adapter.request4({
|
|
84
|
+
const cdsReq = adapter.request4({
|
|
85
|
+
method: _propertyAccess ? 'PATCH' : req.method,
|
|
86
|
+
query,
|
|
87
|
+
params,
|
|
88
|
+
headers,
|
|
89
|
+
req,
|
|
90
|
+
res
|
|
91
|
+
})
|
|
90
92
|
|
|
91
93
|
// rewrite event for draft-enabled entities
|
|
92
94
|
if (_isDraft) cdsReq.event = 'PATCH'
|
|
@@ -99,10 +101,13 @@ module.exports = adapter => {
|
|
|
99
101
|
return service
|
|
100
102
|
.run(() => {
|
|
101
103
|
return service.dispatch(cdsReq).then(result => {
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// cdsReq._.readAfterWrite is only true if generic handler served the request
|
|
105
|
+
// If minimal requested or property access and not etag, skip read after write
|
|
106
|
+
if (
|
|
107
|
+
cdsReq._.readAfterWrite &&
|
|
108
|
+
(target._etag || (!_propertyAccess && getPreferReturnHeader(req) !== 'minimal'))
|
|
109
|
+
)
|
|
110
|
+
return _readAfterWrite(cdsReq)
|
|
106
111
|
return result
|
|
107
112
|
})
|
|
108
113
|
})
|
|
@@ -112,16 +117,14 @@ module.exports = adapter => {
|
|
|
112
117
|
// case: read after write returns no results, e.g., due to auth (academic but possible)
|
|
113
118
|
if (result == null) return res.sendStatus(204)
|
|
114
119
|
|
|
115
|
-
const
|
|
116
|
-
postProcess(cdsReq.target, model, result,
|
|
120
|
+
const preference = getPreferReturnHeader(req)
|
|
121
|
+
postProcess(cdsReq.target, model, result, preference === 'minimal')
|
|
117
122
|
|
|
118
|
-
if (isMinimal && !target._isSingleton) {
|
|
119
|
-
// determine calculation based on result with req.data as fallback
|
|
120
|
-
res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
|
|
121
|
-
}
|
|
122
123
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
123
124
|
|
|
124
|
-
if (
|
|
125
|
+
if (preference === 'minimal') res.append('Preference-Applied', 'return=minimal')
|
|
126
|
+
else if (preference === 'representation') res.append('Preference-Applied', 'return=representation')
|
|
127
|
+
if (preference === 'minimal' || (_propertyAccess && result[_propertyAccess] == null) || isStream(req._query)) {
|
|
125
128
|
return res.sendStatus(204)
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -328,6 +328,10 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
328
328
|
keyCount += addRefToWhereIfNecessary(ref[i].where, current)
|
|
329
329
|
_resolveAliasesInXpr(ref[i].where, current)
|
|
330
330
|
_processWhere(ref[i].where, current)
|
|
331
|
+
} else {
|
|
332
|
+
// parentheses are missing
|
|
333
|
+
const msg = `Invalid call to "${current.name}". Parentheses are missing`
|
|
334
|
+
throw cds.error(msg, { code: '400', statusCode: 400 })
|
|
331
335
|
}
|
|
332
336
|
|
|
333
337
|
_addDefaultParams(ref[i], current)
|
|
@@ -381,7 +385,18 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
381
385
|
}
|
|
382
386
|
|
|
383
387
|
ref[i] = { operation: current.name }
|
|
388
|
+
|
|
384
389
|
if (params) ref[i].args = _getDataFromParams(params, current)
|
|
390
|
+
// REVISIT: SELECT.from._params is a temporary hack
|
|
391
|
+
else if (from._params && current.kind === 'function') {
|
|
392
|
+
// only take known params to allow additional instructions like sap-language, etc.
|
|
393
|
+
ref[i].args = current['@open']
|
|
394
|
+
? Object.assign({}, from._params)
|
|
395
|
+
: Object.keys(from._params).reduce((acc, cur) => {
|
|
396
|
+
if (current.params && cur in current.params) acc[cur] = from._params[cur]
|
|
397
|
+
return acc
|
|
398
|
+
}, {})
|
|
399
|
+
}
|
|
385
400
|
if (current.returns && current.returns._type) one = true
|
|
386
401
|
|
|
387
402
|
if (current.returns) {
|
|
@@ -572,8 +587,6 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
572
587
|
// view with params
|
|
573
588
|
if (params === undefined) {
|
|
574
589
|
throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { code: '400', statusCode: 400 })
|
|
575
|
-
} else if (Object.keys(params).length === 0) {
|
|
576
|
-
throw new Error('KEY_EXPECTED')
|
|
577
590
|
}
|
|
578
591
|
|
|
579
592
|
keysOfEntity = Object.keys(entity.params)
|
|
@@ -504,11 +504,11 @@
|
|
|
504
504
|
where_clause = p:( n:NOT? {return n?[n]:[]} )(
|
|
505
505
|
OPEN xpr:where_clause CLOSE {p.push({xpr})}
|
|
506
506
|
/ comp:comparison {p.push(...comp)}
|
|
507
|
-
/
|
|
508
|
-
if (p[p.length - 1] === 'not' &&
|
|
509
|
-
p.push(
|
|
507
|
+
/ xpr:lambda {
|
|
508
|
+
if (p[p.length - 1] === 'not' && xpr[0] === 'not') {
|
|
509
|
+
p.push({ xpr })
|
|
510
510
|
} else {
|
|
511
|
-
p.push(...
|
|
511
|
+
p.push(...xpr)
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
/ func:boolish {p.push(func)}
|
|
@@ -631,8 +631,25 @@
|
|
|
631
631
|
|
|
632
632
|
aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerListParam "]" { return { list } }
|
|
633
633
|
|
|
634
|
-
custom
|
|
635
|
-
|
|
634
|
+
custom = k:$([a-zA-Z0-9-_.~!\[\]]+) "="? v:$([^&]*)? {
|
|
635
|
+
// normalize value
|
|
636
|
+
if (v === 'null') v = null
|
|
637
|
+
else if (v === 'true') v = true
|
|
638
|
+
else if (v === 'false') v = false
|
|
639
|
+
// set value in structure
|
|
640
|
+
// REVISIT: SELECT.from._params is a temporary hack
|
|
641
|
+
const params = SELECT.from._params ??= {}
|
|
642
|
+
let t = params
|
|
643
|
+
let x = k.match(/^(\w+)\[(.*)\]$/)
|
|
644
|
+
while (x) {
|
|
645
|
+
if (!(x[1] in t)) t[x[1]] = x[2] === '' ? [] : {}
|
|
646
|
+
t = t[x[1]]
|
|
647
|
+
k = x[2]
|
|
648
|
+
x = k.match(/^(\w+)\[(.*)\]$/)
|
|
649
|
+
}
|
|
650
|
+
if (Array.isArray(t)) t.push(v)
|
|
651
|
+
else t[k] = v
|
|
652
|
+
}
|
|
636
653
|
|
|
637
654
|
aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
|
|
638
655
|
aliasedParamEqualsVal = alias:aliasedParam "=" !aliasedParam value:aliasedParamVal {
|
|
@@ -912,7 +929,7 @@
|
|
|
912
929
|
= $( [a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+ )
|
|
913
930
|
|
|
914
931
|
binary "a binary" // > url-safe base64
|
|
915
|
-
= "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return standardBase64(s) }
|
|
932
|
+
= "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return cds.env.features.base64_binaries ? standardBase64(s) : Buffer.from(s, 'base64') }
|
|
916
933
|
|
|
917
934
|
//
|
|
918
935
|
// ---------- Punctuation ----------
|