@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/_i18n/i18n_en_US_saptrc.properties +4 -7
  3. package/bin/serve.js +3 -1
  4. package/lib/compile/for/lean_drafts.js +1 -1
  5. package/lib/compile/for/nodejs.js +1 -0
  6. package/lib/compile/to/sql.js +12 -8
  7. package/lib/core/classes.js +3 -4
  8. package/lib/core/types.js +1 -0
  9. package/lib/env/cds-env.js +2 -2
  10. package/lib/env/cds-requires.js +2 -2
  11. package/lib/ql/cds-ql.js +8 -1
  12. package/lib/ql/cds.ql-Query.js +9 -2
  13. package/lib/req/validate.js +1 -2
  14. package/lib/srv/cds-connect.js +2 -2
  15. package/lib/srv/cds-serve.js +2 -9
  16. package/lib/srv/cds.Service.js +0 -1
  17. package/lib/srv/factory.js +59 -71
  18. package/lib/srv/middlewares/auth/ias-auth.js +44 -14
  19. package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
  20. package/lib/srv/middlewares/auth/xssec.js +1 -1
  21. package/lib/srv/middlewares/errors.js +8 -10
  22. package/lib/utils/cds-utils.js +5 -1
  23. package/lib/utils/tar-lib.js +58 -0
  24. package/libx/_runtime/common/Service.js +0 -4
  25. package/libx/_runtime/common/generic/input.js +1 -1
  26. package/libx/_runtime/common/utils/csn.js +5 -1
  27. package/libx/_runtime/fiori/lean-draft.js +6 -5
  28. package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
  29. package/libx/common/utils/path.js +2 -0
  30. package/libx/odata/middleware/create.js +7 -3
  31. package/libx/odata/middleware/delete.js +2 -0
  32. package/libx/odata/middleware/operation.js +2 -0
  33. package/libx/odata/middleware/read.js +4 -0
  34. package/libx/odata/middleware/stream.js +4 -0
  35. package/libx/odata/middleware/update.js +4 -0
  36. package/libx/odata/parse/afterburner.js +2 -2
  37. package/libx/odata/parse/multipartToJson.js +0 -1
  38. package/libx/odata/utils/normalizeTimeData.js +43 -0
  39. package/libx/odata/utils/readAfterWrite.js +1 -1
  40. package/libx/outbox/index.js +1 -1
  41. package/libx/rest/RestAdapter.js +2 -2
  42. package/package.json +6 -2
  43. package/lib/srv/protocols/odata-v2.js +0 -26
  44. package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
  45. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
  46. package/libx/_runtime/common/code-ext/config.js +0 -13
  47. package/libx/_runtime/common/code-ext/execute.js +0 -123
  48. package/libx/_runtime/common/code-ext/handlers.js +0 -50
  49. package/libx/_runtime/common/code-ext/worker.js +0 -70
  50. 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
- const xssec = require('./xssec')
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
- // NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
47
- return function jwt_auth (req, _, next) {
48
- if (!req.headers.authorization) return next()
49
- const token = req.headers.authorization.slice(7) // skip /^bearer /
50
- xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
51
-
52
- if (err) LOG.warn('User could not be authenticated due to error:', err)
53
- if (!securityContext) return next(401)
54
- else req.authInfo = securityContext //> compat req.authInfo
55
-
56
- const ctx = cds.context
57
- ctx.user = getUser(tokenInfo)
58
- ctx.tenant = tokenInfo.getZoneId()
59
- next()
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.v3 || xssec // use v3 compat api // REVISIT: why ???
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 (status, '>', inspect(error))
29
+ LOG.warn(status, '>', inspect(error))
30
30
  } else {
31
- LOG.error (status, '>', inspect(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
- // _next (error)
53
+ // next(error)
56
54
  }
57
55
  }
@@ -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() { return super.tar = require('./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
- this.client = new AMQPClient({
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
- handleSapMessages(cdsReq, req, res)
71
+ if (res.headersSent) return
70
72
 
71
- // case: read after write returns no results, e.g., due to auth (academic but possible)
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
@@ -57,6 +57,8 @@ module.exports = adapter => {
57
57
  })
58
58
  })
59
59
  .then(() => {
60
+ if (res.headersSent) return
61
+
60
62
  handleSapMessages(cdsReq, req, res)
61
63
 
62
64
  res.sendStatus(204)
@@ -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()
@@ -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._trace && LOG.trace(`${name}: All messages processed`)
191
+ LOG._debug && LOG.debug(`${name}: All messages processed`)
192
192
  }
193
193
  }, config)
194
194
  spawn.on('done', () => {
@@ -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.6.2",
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": ">=4"
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
- }