@sap/cds 8.6.2 → 8.7.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 +35 -0
- package/_i18n/i18n_en_US_saptrc.properties +4 -7
- package/bin/serve.js +3 -1
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/for/nodejs.js +1 -0
- package/lib/compile/to/sql.js +12 -8
- package/lib/core/classes.js +3 -4
- package/lib/core/types.js +1 -0
- package/lib/env/cds-env.js +2 -2
- package/lib/env/cds-requires.js +2 -2
- package/lib/ql/cds-ql.js +8 -1
- package/lib/ql/cds.ql-Query.js +9 -2
- package/lib/req/validate.js +1 -2
- package/lib/srv/cds-connect.js +2 -2
- package/lib/srv/cds-serve.js +2 -9
- package/lib/srv/cds.Service.js +0 -1
- package/lib/srv/factory.js +59 -71
- package/lib/srv/middlewares/auth/ias-auth.js +44 -14
- package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/srv/middlewares/errors.js +8 -10
- package/lib/utils/cds-utils.js +5 -1
- package/lib/utils/tar-lib.js +58 -0
- package/libx/_runtime/common/Service.js +0 -4
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/utils/csn.js +5 -1
- package/libx/_runtime/fiori/lean-draft.js +6 -5
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
- package/libx/common/utils/path.js +2 -0
- package/libx/odata/middleware/create.js +7 -3
- package/libx/odata/middleware/delete.js +2 -0
- package/libx/odata/middleware/operation.js +2 -0
- package/libx/odata/middleware/read.js +4 -0
- package/libx/odata/middleware/stream.js +4 -0
- package/libx/odata/middleware/update.js +4 -0
- package/libx/odata/parse/afterburner.js +2 -2
- package/libx/odata/parse/multipartToJson.js +0 -1
- package/libx/odata/utils/normalizeTimeData.js +43 -0
- package/libx/odata/utils/readAfterWrite.js +1 -1
- package/libx/outbox/index.js +1 -1
- package/libx/rest/RestAdapter.js +2 -2
- package/package.json +6 -2
- package/lib/srv/protocols/odata-v2.js +0 -26
- package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
- package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
- package/libx/_runtime/common/code-ext/config.js +0 -13
- package/libx/_runtime/common/code-ext/execute.js +0 -123
- package/libx/_runtime/common/code-ext/handlers.js +0 -50
- package/libx/_runtime/common/code-ext/worker.js +0 -70
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +0 -37
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../../index.js')
|
|
2
2
|
const LOG = cds.log('auth')
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
let xssec = require('./xssec')
|
|
5
5
|
|
|
6
6
|
module.exports = function jwt_auth(config) {
|
|
7
7
|
const { kind, credentials } = config
|
|
@@ -42,21 +42,50 @@ module.exports = function jwt_auth(config) {
|
|
|
42
42
|
|
|
43
43
|
return new cds.User({ id, roles, attr, tokenInfo })
|
|
44
44
|
}
|
|
45
|
+
if (xssec.v3 && cds.env.features.xssec_compat !== true) { // no official flag!
|
|
46
|
+
const { createSecurityContext, XsuaaService, errors: { ValidationError } } = xssec
|
|
47
|
+
const authService = new XsuaaService(credentials)
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
return async function jwt_auth(req, _, next) {
|
|
50
|
+
if (!req.headers.authorization) return next()
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const secContext = await createSecurityContext(authService, { req })
|
|
54
|
+
const tokenInfo = secContext.token
|
|
55
|
+
const ctx = cds.context
|
|
56
|
+
ctx.user = getUser(tokenInfo)
|
|
57
|
+
ctx.tenant = tokenInfo.getZoneId()
|
|
58
|
+
req.authInfo = secContext //> compat req.authInfo
|
|
59
|
+
} catch(e) {
|
|
60
|
+
if(e instanceof ValidationError) {
|
|
61
|
+
LOG.warn("Unauthenticated request: ", e);
|
|
62
|
+
return next(401)
|
|
63
|
+
}
|
|
64
|
+
LOG.error("Error while authenticating user: ", e);
|
|
65
|
+
return next(500)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
next()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} else {
|
|
72
|
+
xssec = xssec.v3 || xssec
|
|
73
|
+
|
|
74
|
+
// NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
|
|
75
|
+
return function jwt_auth (req, _, next) {
|
|
76
|
+
if (!req.headers.authorization) return next()
|
|
77
|
+
const token = req.headers.authorization.slice(7) // skip /^bearer /
|
|
78
|
+
xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
|
|
79
|
+
|
|
80
|
+
if (err) LOG.warn('User could not be authenticated due to error:', err)
|
|
81
|
+
if (!securityContext) return next(401)
|
|
82
|
+
else req.authInfo = securityContext //> compat req.authInfo
|
|
83
|
+
|
|
84
|
+
const ctx = cds.context
|
|
85
|
+
ctx.user = getUser(tokenInfo)
|
|
86
|
+
ctx.tenant = tokenInfo.getZoneId()
|
|
87
|
+
next()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
61
90
|
}
|
|
62
91
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
try {
|
|
2
2
|
const xssec = require('@sap/xssec')
|
|
3
|
-
module.exports = xssec
|
|
3
|
+
module.exports = xssec // use v3 compat api // REVISIT: why ???
|
|
4
4
|
} catch (e) {
|
|
5
5
|
if (e.code === 'MODULE_NOT_FOUND') e.message = `Cannot find '@sap/xssec'. Make sure to install it with 'npm i @sap/xssec'\n` + e.message
|
|
6
6
|
throw e
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
const { isStandardError } = require('../../../libx/_runtime/common/error/standardError')
|
|
2
2
|
|
|
3
3
|
const production = process.env.NODE_ENV === 'production'
|
|
4
|
-
const cds = require
|
|
4
|
+
const cds = require('../..')
|
|
5
5
|
const LOG = cds.log('error')
|
|
6
6
|
const { inspect } = cds.utils
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
module.exports = () => {
|
|
9
|
+
// eslint-disable-next-line no-unused-vars
|
|
10
10
|
return async function http_error(error, req, res, next) {
|
|
11
11
|
if (isStandardError(error) && cds.env.server.shutdown_on_uncaught_errors) {
|
|
12
12
|
cds.log().error('❗️Uncaught', error)
|
|
13
13
|
await cds.shutdown(error)
|
|
14
|
-
return
|
|
14
|
+
return
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// In case of 401 require login if available by auth strategy
|
|
@@ -26,11 +26,13 @@ module.exports = () => {
|
|
|
26
26
|
error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g, '')
|
|
27
27
|
|
|
28
28
|
if (400 <= status && status < 500) {
|
|
29
|
-
LOG.warn
|
|
29
|
+
LOG.warn(status, '>', inspect(error))
|
|
30
30
|
} else {
|
|
31
|
-
LOG.error
|
|
31
|
+
LOG.error(status, '>', inspect(error))
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
if (res.headersSent) return
|
|
35
|
+
|
|
34
36
|
// Expose as little information as possible in production, and as much as possible in development
|
|
35
37
|
if (production) {
|
|
36
38
|
Object.defineProperties(error, {
|
|
@@ -44,14 +46,10 @@ module.exports = () => {
|
|
|
44
46
|
})
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
if (res.headersSent) {
|
|
48
|
-
return next(error)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
49
|
// Send the error response
|
|
52
50
|
return res.status(status).json({ error })
|
|
53
51
|
|
|
54
52
|
// Note: express returns errors as XML, we prefer JSON
|
|
55
|
-
//
|
|
53
|
+
// next(error)
|
|
56
54
|
}
|
|
57
55
|
}
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const cwd = process.env._original_cwd || process.cwd()
|
|
2
2
|
const cds = require('../index')
|
|
3
3
|
|
|
4
|
+
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
const _tarLib = () => { try { return require('tar') } catch(_) {} }
|
|
7
|
+
|
|
4
8
|
module.exports = exports = new class {
|
|
5
9
|
get colors() { return super.colors = require('./colors') }
|
|
6
10
|
get inflect() { return super.inflect = require('./inflect') }
|
|
@@ -16,7 +20,7 @@ module.exports = exports = new class {
|
|
|
16
20
|
get uuid() { return super.uuid = require('crypto').randomUUID }
|
|
17
21
|
get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
|
|
18
22
|
get pool() { return super.pool = require('@sap/cds-foss').pool }
|
|
19
|
-
get tar()
|
|
23
|
+
get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') }
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** @type {import('node:path')} */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const cds = require('../index'), { path, mkdirp } = cds.utils
|
|
2
|
+
const tar = require('tar')
|
|
3
|
+
const { Readable } = require('stream')
|
|
4
|
+
const cons = require('stream/consumers')
|
|
5
|
+
|
|
6
|
+
const _resolve = (...x) => path.resolve (cds.root,...x)
|
|
7
|
+
|
|
8
|
+
exports.create = async (root, ...args) => {
|
|
9
|
+
if (typeof root === 'string') root = _resolve(root)
|
|
10
|
+
if (Array.isArray(root)) [ root, ...args ] = [ cds.root, root, ...args ]
|
|
11
|
+
|
|
12
|
+
const options = {}
|
|
13
|
+
if (args.includes('-z')) options.gzip = true
|
|
14
|
+
const index = args.findIndex(el => el === '-f')
|
|
15
|
+
if (index>=0) options.file = _resolve(args[index+1])
|
|
16
|
+
options.cwd = root
|
|
17
|
+
|
|
18
|
+
let dirs = []
|
|
19
|
+
for (let i=0; i<args.length; i++) {
|
|
20
|
+
if (args[i] === '-z' || args[i] === '-f') break
|
|
21
|
+
if (Array.isArray(args[i])) args[i].forEach(a => dirs.push(_resolve(a)))
|
|
22
|
+
else if (typeof args[i] === 'string') dirs.push(_resolve(args[i]))
|
|
23
|
+
}
|
|
24
|
+
if (!dirs.length) dirs.push(root)
|
|
25
|
+
dirs = dirs.map(d => path.relative(root, d))
|
|
26
|
+
|
|
27
|
+
const stream = await tar.c(options, dirs)
|
|
28
|
+
|
|
29
|
+
return stream && await cons.buffer(stream)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.extract = (archive, ...args) => ({
|
|
33
|
+
async to (dest) {
|
|
34
|
+
if (typeof dest === 'string') dest = _resolve(dest)
|
|
35
|
+
const stream = Readable.from(archive)
|
|
36
|
+
|
|
37
|
+
const options = { C: dest }
|
|
38
|
+
if (args.includes('-z')) options.gzip = true
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const tr = tar.x(options)
|
|
42
|
+
stream.pipe(tr)
|
|
43
|
+
tr.on('close', () => resolve())
|
|
44
|
+
tr.on('error', e => reject(e))
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const tar_ = exports
|
|
50
|
+
exports.c = tar_.create
|
|
51
|
+
exports.cz = (d,...args) => tar_.c (d, ...args, '-z')
|
|
52
|
+
exports.cf = (t,d,...args) => tar_.c (d, ...args, '-f',t)
|
|
53
|
+
exports.czf = (t,d,...args) => tar_.c (d, ...args, '-z', '-f',t)
|
|
54
|
+
exports.czfd = (t,...args) => mkdirp(path.dirname(t)).then (()=> tar_.czf (t,...args))
|
|
55
|
+
exports.x = tar_.xf = tar_.extract
|
|
56
|
+
exports.xz = tar_.xzf = a => tar_.x (a, '-z')
|
|
57
|
+
exports.xv = tar_.xvf = a => tar_.x (a)
|
|
58
|
+
exports.xvz = tar_.xvzf = a => tar_.x (a, '-z')
|
|
@@ -62,10 +62,6 @@ class ApplicationService extends cds.Service {
|
|
|
62
62
|
require('./generic/sorting').call(this)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
static handle_code_ext() {
|
|
66
|
-
if (cds.env.requires.extensibility?.code) require('./code-ext/handlers').call(this)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
static handle_fiori() {
|
|
70
66
|
require('../fiori/lean-draft').impl.call(this)
|
|
71
67
|
}
|
|
@@ -212,7 +212,7 @@ const _pick = element => {
|
|
|
212
212
|
// should be a db feature, as we cannot handle completely on service level (cf. deep update)
|
|
213
213
|
// -> add to attic env behavior once new dbs handle this
|
|
214
214
|
// also happens in validate but because of draft activate we have to do it twice (where cleansing is suppressed)
|
|
215
|
-
if (element['@Core.Immutable']) {
|
|
215
|
+
if (element['@Core.Immutable'] && !element.key) {
|
|
216
216
|
categories.push('immutable')
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -115,19 +115,23 @@ const prefixForStruct = element => {
|
|
|
115
115
|
function getDraftTreeRoot(entity, model) {
|
|
116
116
|
if (entity.own('__draftTreeRoot')) return entity.__draftTreeRoot
|
|
117
117
|
|
|
118
|
+
const previous = new Set() // track visited entities to identify hierarchies
|
|
118
119
|
let parent
|
|
119
120
|
let current = entity
|
|
120
121
|
while (current && !current['@Common.DraftRoot.ActivationAction']) {
|
|
122
|
+
previous.add(current.name)
|
|
121
123
|
const parents = []
|
|
122
124
|
for (const k in model.definitions) {
|
|
125
|
+
if (previous.has(k)) continue
|
|
123
126
|
const e = model.definitions[k]
|
|
124
127
|
if (e.kind !== 'entity' || !e.compositions) continue
|
|
125
128
|
for (const c in e.compositions)
|
|
126
129
|
if (
|
|
127
130
|
e.compositions[c].target === current.name ||
|
|
128
131
|
e.compositions[c].target === current.name.replace(/\.drafts/, '')
|
|
129
|
-
)
|
|
132
|
+
) {
|
|
130
133
|
parents.push(e)
|
|
134
|
+
}
|
|
131
135
|
}
|
|
132
136
|
if (parents.length > 1 && parents.some(p => p !== parents[0])) {
|
|
133
137
|
// > unable to determine single parent
|
|
@@ -407,10 +407,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
if (req.event === 'READ') {
|
|
410
|
-
// apply paging and sorting on original query for protocol adapters relying on it
|
|
411
|
-
commonGenericPaging(req)
|
|
412
|
-
commonGenericSorting(req)
|
|
413
|
-
|
|
414
410
|
if (
|
|
415
411
|
!Object.keys(draftParams).length &&
|
|
416
412
|
!req.query._target.name?.endsWith('DraftAdministrativeData') &&
|
|
@@ -419,6 +415,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
419
415
|
req.query = query
|
|
420
416
|
return handle(req)
|
|
421
417
|
}
|
|
418
|
+
|
|
419
|
+
// apply paging and sorting on original query for protocol adapters relying on it
|
|
420
|
+
commonGenericPaging(req)
|
|
421
|
+
commonGenericSorting(req)
|
|
422
|
+
|
|
422
423
|
const read =
|
|
423
424
|
draftParams.IsActiveEntity === false &&
|
|
424
425
|
_hasStreaming(query.SELECT.columns, query._target) &&
|
|
@@ -1509,7 +1510,7 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
|
|
|
1509
1510
|
}
|
|
1510
1511
|
|
|
1511
1512
|
async function onNewCleanse(req) {
|
|
1512
|
-
cds.validate(req.data, req.target, {})
|
|
1513
|
+
cds.validate(req.data, req.target, { insert: true })
|
|
1513
1514
|
}
|
|
1514
1515
|
onNewCleanse._initial = true
|
|
1515
1516
|
async function onNew(req) {
|
|
@@ -14,13 +14,17 @@ class EnterpriseMessagingShared extends AMQPWebhookMessaging {
|
|
|
14
14
|
|
|
15
15
|
getClient() {
|
|
16
16
|
if (this.client) return this.client
|
|
17
|
+
this.client = new AMQPClient(this.getClientOptions())
|
|
18
|
+
return this.client
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getClientOptions() {
|
|
17
22
|
const optionsAMQP = optionsMessagingAMQP(this.options)
|
|
18
|
-
|
|
23
|
+
return {
|
|
19
24
|
optionsAMQP,
|
|
20
25
|
prefix: { topic: 'topic:', queue: 'queue:' },
|
|
21
26
|
service: this
|
|
22
|
-
}
|
|
23
|
-
return this.client
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
getManagement() {
|
|
@@ -25,6 +25,8 @@ const getKeysAndParamsFromPath = (from, { model }) => {
|
|
|
25
25
|
const seg_keys = where2obj(ref.where)
|
|
26
26
|
Object.assign(keys, seg_keys)
|
|
27
27
|
params[i] = seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys
|
|
28
|
+
} else if (ref.args) {
|
|
29
|
+
params[i] = Object.fromEntries(Object.entries(ref.args).map(([k, v]) => [k, 'val' in v ? v.val : v]))
|
|
28
30
|
}
|
|
29
31
|
if (lastElement.isAssociation && from.ref.length > 1) {
|
|
30
32
|
// add keys for navigation from path
|
|
@@ -6,6 +6,7 @@ const getODataMetadata = require('../utils/metadata')
|
|
|
6
6
|
const postProcess = require('../utils/postProcess')
|
|
7
7
|
const readAfterWrite4 = require('../utils/readAfterWrite')
|
|
8
8
|
const getODataResult = require('../utils/result')
|
|
9
|
+
const normalizeTimeData = require('../utils/normalizeTimeData')
|
|
9
10
|
|
|
10
11
|
const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
11
12
|
|
|
@@ -32,6 +33,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
32
33
|
|
|
33
34
|
// payload & params
|
|
34
35
|
const data = req.body
|
|
36
|
+
normalizeTimeData(data, model, target)
|
|
35
37
|
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
36
38
|
// add keys from url into payload (overwriting if already present)
|
|
37
39
|
Object.assign(data, keys)
|
|
@@ -66,16 +68,18 @@ module.exports = (adapter, isUpsert) => {
|
|
|
66
68
|
})
|
|
67
69
|
})
|
|
68
70
|
.then(result => {
|
|
69
|
-
|
|
71
|
+
if (res.headersSent) return
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
if (result == null) return res.sendStatus(204)
|
|
73
|
+
handleSapMessages(cdsReq, req, res)
|
|
73
74
|
|
|
74
75
|
if (!target._isSingleton) {
|
|
75
76
|
// determine calculation based on result with req.data as fallback
|
|
76
77
|
res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// case: read after write returns no results, e.g., due to auth (academic but possible)
|
|
81
|
+
if (result == null) return res.sendStatus(204)
|
|
82
|
+
|
|
79
83
|
const preference = getPreferReturnHeader(req)
|
|
80
84
|
postProcess(cdsReq.target, model, result, preference === 'minimal')
|
|
81
85
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
@@ -101,6 +101,8 @@ module.exports = adapter => {
|
|
|
101
101
|
return service
|
|
102
102
|
.run(() => service.dispatch(cdsReq))
|
|
103
103
|
.then(result => {
|
|
104
|
+
if (res.headersSent) return
|
|
105
|
+
|
|
104
106
|
handleSapMessages(cdsReq, req, res)
|
|
105
107
|
|
|
106
108
|
if (operation.returns?.items && result == null) result = []
|
|
@@ -128,6 +128,8 @@ const _handleArrayOfQueriesFactory = adapter => {
|
|
|
128
128
|
})
|
|
129
129
|
})
|
|
130
130
|
.then(result => {
|
|
131
|
+
if (res.headersSent) return
|
|
132
|
+
|
|
131
133
|
handleSapMessages(cdsReq, req, res)
|
|
132
134
|
|
|
133
135
|
if (req.url.match(/\/\$count/)) return res.set('Content-Type', 'text/plain').send(_count(result).toString())
|
|
@@ -238,6 +240,8 @@ module.exports = adapter => {
|
|
|
238
240
|
})
|
|
239
241
|
})
|
|
240
242
|
.then(result => {
|
|
243
|
+
if (res.headersSent) return
|
|
244
|
+
|
|
241
245
|
handleSapMessages(cdsReq, req, res)
|
|
242
246
|
|
|
243
247
|
// 204
|
|
@@ -218,6 +218,8 @@ module.exports = adapter => {
|
|
|
218
218
|
return service
|
|
219
219
|
.run(() => {
|
|
220
220
|
return service.dispatch(cdsReq).then(async result => {
|
|
221
|
+
if (res.headersSent) return
|
|
222
|
+
|
|
221
223
|
_validateStream(req, result)
|
|
222
224
|
|
|
223
225
|
if (validateIfNoneMatch(cdsReq.target, req.headers?.['if-none-match'], result)) return res.sendStatus(304)
|
|
@@ -241,6 +243,8 @@ module.exports = adapter => {
|
|
|
241
243
|
})
|
|
242
244
|
})
|
|
243
245
|
.then(() => {
|
|
246
|
+
if (res.headersSent) return
|
|
247
|
+
|
|
244
248
|
handleSapMessages(cdsReq, req, res)
|
|
245
249
|
|
|
246
250
|
res.end()
|
|
@@ -6,6 +6,7 @@ const getODataMetadata = require('../utils/metadata')
|
|
|
6
6
|
const postProcess = require('../utils/postProcess')
|
|
7
7
|
const readAfterWrite4 = require('../utils/readAfterWrite')
|
|
8
8
|
const getODataResult = require('../utils/result')
|
|
9
|
+
const normalizeTimeData = require('../utils/normalizeTimeData')
|
|
9
10
|
|
|
10
11
|
const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
11
12
|
|
|
@@ -69,6 +70,7 @@ module.exports = adapter => {
|
|
|
69
70
|
|
|
70
71
|
// payload & params
|
|
71
72
|
const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
|
|
73
|
+
normalizeTimeData(data, model, target)
|
|
72
74
|
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
73
75
|
// add keys from url into payload (overwriting if already present)
|
|
74
76
|
if (!_propertyAccess) Object.assign(data, keys)
|
|
@@ -112,6 +114,8 @@ module.exports = adapter => {
|
|
|
112
114
|
})
|
|
113
115
|
})
|
|
114
116
|
.then(result => {
|
|
117
|
+
if (res.headersSent) return
|
|
118
|
+
|
|
115
119
|
handleSapMessages(cdsReq, req, res)
|
|
116
120
|
|
|
117
121
|
// case: read after write returns no results, e.g., due to auth (academic but possible)
|
|
@@ -447,7 +447,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
447
447
|
_resolveAliasesInXpr(ref[i].where, current)
|
|
448
448
|
_processWhere(ref[i].where, current)
|
|
449
449
|
}
|
|
450
|
-
} else if (current.kind === 'element' && current.elements && i < ref.length - 1) {
|
|
450
|
+
} else if (current.kind === 'element' && current.type !== 'cds.Map' && current.elements && i < ref.length - 1) {
|
|
451
451
|
// > structured
|
|
452
452
|
continue
|
|
453
453
|
} else {
|
|
@@ -474,7 +474,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
474
474
|
Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
|
|
475
475
|
|
|
476
476
|
// if we end up with structured, keep path as is, if we end up with property in structured, cut off property
|
|
477
|
-
if (!current.elements) from.ref.splice(-1)
|
|
477
|
+
if (!current.elements || current.type === 'cds.Map') from.ref.splice(-1)
|
|
478
478
|
break
|
|
479
479
|
} else if (Object.keys(target.elements).includes(current.name)) {
|
|
480
480
|
if (!cqn.SELECT.columns) cqn.SELECT.columns = []
|
|
@@ -138,7 +138,6 @@ const _parseStream = async function* (body, boundary) {
|
|
|
138
138
|
|
|
139
139
|
if (typeof ret !== 'number') {
|
|
140
140
|
if (ret.message === 'Parse Error') {
|
|
141
|
-
// console.trace(ret, ret.bytesParsed ? `\n\nin:\n${changed.substr(0, ret.bytesParsed + 1)}\n\n` : '')
|
|
142
141
|
ret.statusCode = 400
|
|
143
142
|
ret.message = `Error while parsing batch body at position ${ret.bytesParsed}: ${ret.reason}`
|
|
144
143
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const normalizeTimestamp = require('../../_runtime/common/utils/normalizeTimestamp')
|
|
2
|
+
const getTemplate = require('../../_runtime/common/utils/template')
|
|
3
|
+
|
|
4
|
+
const _processorFn = elementInfo => {
|
|
5
|
+
const { row, plain } = elementInfo
|
|
6
|
+
if (typeof row !== 'object') return
|
|
7
|
+
for (const category of plain.categories) {
|
|
8
|
+
const { row, key } = elementInfo
|
|
9
|
+
if (!(row[key] == null) && row[key] !== '$now') {
|
|
10
|
+
switch (category) {
|
|
11
|
+
case 'cds.DateTime':
|
|
12
|
+
row[key] = new Date(row[key]).toISOString().replace(/\.\d\d\d/, '')
|
|
13
|
+
break
|
|
14
|
+
case 'cds.Timestamp':
|
|
15
|
+
row[key] = normalizeTimestamp(row[key])
|
|
16
|
+
break
|
|
17
|
+
// no default
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const _pick = element => {
|
|
24
|
+
const categories = []
|
|
25
|
+
if (element.type === 'cds.DateTime') categories.push('cds.DateTime')
|
|
26
|
+
if (element.type === 'cds.Timestamp') categories.push('cds.Timestamp')
|
|
27
|
+
if (categories.length) return { categories }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = function normalizeTimeData(data, model, target) {
|
|
31
|
+
if (
|
|
32
|
+
!data ||
|
|
33
|
+
(Array.isArray(data) && data.length === 0) ||
|
|
34
|
+
(typeof data === 'object' && Object.keys(data).length === 0)
|
|
35
|
+
) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const template = getTemplate('normalize-datetime', { model }, target, { pick: _pick })
|
|
39
|
+
|
|
40
|
+
if (template.elements.size === 0) return
|
|
41
|
+
|
|
42
|
+
template.process(data, _processorFn)
|
|
43
|
+
}
|
|
@@ -88,7 +88,7 @@ const _getColumns = (target, data, prefix = []) => {
|
|
|
88
88
|
if (each in DRAFT_COLUMNS_MAP) continue
|
|
89
89
|
if (!cds.env.features.stream_compat && target.elements[each].type === 'cds.LargeBinary') continue
|
|
90
90
|
const element = target.elements[each]
|
|
91
|
-
if (element.elements && data[each]) {
|
|
91
|
+
if (element.elements && data[each] && element.type !== 'cds.Map') {
|
|
92
92
|
prefix.push(element.name)
|
|
93
93
|
columns.push(..._getColumns(element, data[each], prefix))
|
|
94
94
|
prefix.pop()
|
package/libx/outbox/index.js
CHANGED
|
@@ -188,7 +188,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
|
|
|
188
188
|
if (toBeDeleted.length === opts.chunkSize) {
|
|
189
189
|
processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
|
|
190
190
|
} else {
|
|
191
|
-
LOG.
|
|
191
|
+
LOG._debug && LOG.debug(`${name}: All messages processed`)
|
|
192
192
|
}
|
|
193
193
|
}, config)
|
|
194
194
|
spawn.on('done', () => {
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -115,11 +115,11 @@ class RestAdapter extends HttpAdapter {
|
|
|
115
115
|
|
|
116
116
|
// handle result
|
|
117
117
|
router.use((req, res) => {
|
|
118
|
-
const { result, status, location } = req._result // REVISIT: Ugly voodoo _req._result channel -> eliminate
|
|
119
|
-
|
|
120
118
|
// if authentication or something else within the processing of a cds.Request terminates the request, no need to continue
|
|
121
119
|
if (res.headersSent) return
|
|
122
120
|
|
|
121
|
+
const { result, status, location } = req._result // REVISIT: Ugly voodoo _req._result channel -> eliminate
|
|
122
|
+
|
|
123
123
|
// post process
|
|
124
124
|
let definition = req._operation || req._query.__target
|
|
125
125
|
if (typeof definition === 'string')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/cds",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.7.1",
|
|
4
4
|
"description": "SAP Cloud Application Programming Model - CDS for Node.js",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -39,11 +39,15 @@
|
|
|
39
39
|
"@sap/cds-foss": "^5.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"express": "
|
|
42
|
+
"express": "^4",
|
|
43
|
+
"tar": "^7"
|
|
43
44
|
},
|
|
44
45
|
"peerDependenciesMeta": {
|
|
45
46
|
"express": {
|
|
46
47
|
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"tar": {
|
|
50
|
+
"optional": true
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const cds = require('../../index'), { decodeURIComponent } = cds.utils
|
|
2
|
-
const LOG = cds.log('odata-v2')
|
|
3
|
-
const logger = function cap_legacy_req_logger (req,_,next) {
|
|
4
|
-
if (/\$batch$/.test(req.url)) {
|
|
5
|
-
const prefix = decodeURIComponent(req.originalUrl).replace('$batch','')
|
|
6
|
-
req.on ('dispatch', (req) => {
|
|
7
|
-
LOG && LOG (req.event, prefix+decodeURIComponent(req._path), req._query||'')
|
|
8
|
-
if (LOG._debug && req.query) LOG.debug (req.query)
|
|
9
|
-
})
|
|
10
|
-
} else {
|
|
11
|
-
LOG && LOG (req.method, decodeURIComponent(req.originalUrl), req.body||'')
|
|
12
|
-
}
|
|
13
|
-
next()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const ODataV2Proxy = require('./odata-v2-proxy') // ('@sap/cds-odata-v2-adapter-proxy')
|
|
17
|
-
module.exports = function ODataV2Adapter (srv) {
|
|
18
|
-
const proxy = new ODataV2Proxy ({
|
|
19
|
-
sourcePath: srv.path,
|
|
20
|
-
targetPath: '/odata/v4',
|
|
21
|
-
target: 'auto', // to detect server url + port dynamically
|
|
22
|
-
logLevel: 'warn',
|
|
23
|
-
...srv.options, path:""
|
|
24
|
-
})
|
|
25
|
-
return [ logger, proxy ]
|
|
26
|
-
}
|