@sap/cds 9.6.3 → 9.7.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/bin/serve.js +38 -26
  3. package/lib/compile/for/flows.js +92 -19
  4. package/lib/compile/for/lean_drafts.js +0 -47
  5. package/lib/compile/for/nodejs.js +47 -14
  6. package/lib/compile/for/odata.js +20 -0
  7. package/lib/compile/load.js +22 -25
  8. package/lib/compile/minify.js +29 -11
  9. package/lib/compile/parse.js +1 -1
  10. package/lib/compile/resolve.js +133 -76
  11. package/lib/compile/to/csn.js +2 -2
  12. package/lib/dbs/cds-deploy.js +48 -43
  13. package/lib/env/cds-env.js +6 -0
  14. package/lib/env/cds-requires.js +9 -3
  15. package/lib/index.js +3 -1
  16. package/lib/plugins.js +1 -1
  17. package/lib/req/request.js +2 -2
  18. package/lib/srv/bindings.js +17 -5
  19. package/lib/srv/middlewares/auth/index.js +7 -5
  20. package/lib/srv/protocols/hcql.js +8 -3
  21. package/lib/srv/protocols/index.js +1 -0
  22. package/lib/utils/cds-utils.js +28 -1
  23. package/lib/utils/colors.js +1 -1
  24. package/libx/_runtime/common/generic/assert.js +1 -7
  25. package/libx/_runtime/common/generic/flows.js +14 -4
  26. package/libx/_runtime/common/utils/resolveView.js +4 -0
  27. package/libx/_runtime/fiori/lean-draft.js +8 -3
  28. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +4 -0
  29. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +12 -12
  30. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  31. package/libx/_runtime/messaging/http-utils/token.js +18 -3
  32. package/libx/_runtime/messaging/message-queuing.js +7 -7
  33. package/libx/_runtime/remote/Service.js +3 -1
  34. package/libx/_runtime/remote/utils/client.js +1 -0
  35. package/libx/_runtime/remote/utils/query.js +0 -1
  36. package/libx/odata/middleware/batch.js +128 -112
  37. package/libx/odata/middleware/error.js +7 -3
  38. package/libx/odata/parse/afterburner.js +10 -11
  39. package/libx/odata/parse/grammar.peggy +4 -2
  40. package/libx/odata/parse/parser.js +1 -1
  41. package/libx/odata/utils/odataBind.js +8 -2
  42. package/libx/queue/index.js +3 -1
  43. package/package.json +4 -7
  44. package/srv/outbox.cds +1 -1
  45. package/srv/ucl-service.cds +3 -5
  46. package/bin/colors.js +0 -2
  47. package/libx/_runtime/.eslintrc +0 -14
@@ -9,7 +9,7 @@ const { URL } = require('url')
9
9
  const multipartToJson = require('../parse/multipartToJson')
10
10
  const { getBoundary } = require('../utils')
11
11
 
12
- const { normalizeError } = require('./error')
12
+ const { odataError } = require('./error')
13
13
 
14
14
  const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
15
15
  const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
@@ -120,6 +120,34 @@ const _validateBatch = body => {
120
120
  return ids
121
121
  }
122
122
 
123
+ /*
124
+ * lookalike
125
+ */
126
+
127
+ let error_mws
128
+ const _getNextForLookalike = lookalike => {
129
+ error_mws ??= cds.middlewares.after.filter(mw => mw.length === 4) // error middleware has 4 params
130
+ return err => {
131
+ let _err = err
132
+ let _next_called
133
+ const _next = e => {
134
+ _err = e
135
+ _next_called = true
136
+ }
137
+ for (const mw of error_mws) {
138
+ _next_called = false
139
+ mw(_err, lookalike.req, lookalike.res, _next)
140
+ if (!_next_called) break //> next chain was interrupted -> done
141
+ }
142
+ if (_next_called) {
143
+ // here, final error middleware called next (which actually shouldn't happen!)
144
+ if (_err.statusCode) lookalike.res.status(_err.statusCode)
145
+ if (typeof _err === 'object') lookalike.res.json({ error: _err })
146
+ else lookalike.res.send(_err)
147
+ }
148
+ }
149
+ }
150
+
123
151
  // REVISIT: Why not simply use {__proto__:req, ...}?
124
152
  const _createExpressReqResLookalike = (request, _req, _res) => {
125
153
  const { id, method, url } = request
@@ -165,6 +193,59 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
165
193
  return ret
166
194
  }
167
195
 
196
+ /*
197
+ * multipart/mixed response
198
+ */
199
+
200
+ const _formatResponseMultipart = request => {
201
+ const { res: response } = request
202
+ const content_id = request.req?.headers['content-id']
203
+
204
+ let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
205
+ if (content_id) txt += `content-id: ${content_id}${CRLF}`
206
+ txt += CRLF
207
+ txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
208
+
209
+ // REVISIT: tests require specific sequence
210
+ const headers = {
211
+ ...response.getHeaders(),
212
+ ...(response.statusCode !== 204 && { 'content-type': 'application/json;odata.metadata=minimal' })
213
+ }
214
+ delete headers['content-length'] //> REVISIT: expected by tests
215
+
216
+ for (const key in headers) {
217
+ txt += key + ': ' + headers[key] + CRLF
218
+ }
219
+ txt += CRLF
220
+
221
+ const _tryParse = x => {
222
+ try {
223
+ return JSON.parse(x)
224
+ } catch {
225
+ return x
226
+ }
227
+ }
228
+
229
+ if (response._chunk) {
230
+ const chunk = _tryParse(response._chunk)
231
+ if (chunk && typeof chunk === 'object') {
232
+ let meta = [],
233
+ data = []
234
+ for (const [k, v] of Object.entries(chunk)) {
235
+ if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
236
+ else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
237
+ }
238
+ const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
239
+ txt += _json_as_txt
240
+ } else {
241
+ txt += chunk
242
+ txt = txt.replace('content-type: application/json;odata.metadata=minimal', 'content-type: text/plain')
243
+ }
244
+ }
245
+
246
+ return [txt]
247
+ }
248
+
168
249
  const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
169
250
  res.write(`--${boundary}${CRLF}`)
170
251
 
@@ -185,6 +266,38 @@ const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
185
266
  }
186
267
  }
187
268
 
269
+ /*
270
+ * application/json response
271
+ */
272
+
273
+ const _formatStatics = {
274
+ comma: ','.charCodeAt(0),
275
+ body: Buffer.from('"body":'),
276
+ close: Buffer.from('}')
277
+ }
278
+
279
+ const _formatResponseJson = (request, atomicityGroup) => {
280
+ const { id, res: response } = request
281
+
282
+ const chunk = {
283
+ id,
284
+ status: response.statusCode,
285
+ headers: {
286
+ ...response.getHeaders(),
287
+ 'content-type': 'application/json' //> REVISIT: why?
288
+ }
289
+ }
290
+ if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
291
+ const raw = Buffer.from(JSON.stringify(chunk))
292
+
293
+ // body?
294
+ if (!response._chunk) return [raw]
295
+
296
+ // change last "}" into ","
297
+ raw[raw.byteLength - 1] = _formatStatics.comma
298
+ return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
299
+ }
300
+
188
301
  const _writeResponseJson = (responses, res) => {
189
302
  for (const resp of responses) {
190
303
  if (resp.separator) res.write(resp.separator)
@@ -192,37 +305,25 @@ const _writeResponseJson = (responses, res) => {
192
305
  }
193
306
  }
194
307
 
195
- let error_mws
196
- const _getNextForLookalike = lookalike => {
197
- error_mws ??= cds.middlewares.after.filter(mw => mw.length === 4) // error middleware has 4 params
198
- return err => {
199
- let _err = err
200
- let _next_called
201
- const _next = e => {
202
- _err = e
203
- _next_called = true
204
- }
205
- for (const mw of error_mws) {
206
- _next_called = false
207
- mw(_err, lookalike.req, lookalike.res, _next)
208
- if (!_next_called) break //> next chain was interrupted -> done
209
- }
210
- if (_next_called) {
211
- // here, final error middleware called next (which actually shouldn't happen!)
212
- if (_err.statusCode) lookalike.res.status(_err.statusCode)
213
- if (typeof _err === 'object') lookalike.res.json({ error: _err })
214
- else lookalike.res.send(_err)
215
- }
216
- }
217
- }
308
+ /*
309
+ * process
310
+ */
311
+
312
+ const _txs = {}
313
+ cds.once('shutdown', () => Promise.all(Object.values(_txs).map(tx => tx.rollback().catch(() => {}))))
218
314
 
219
315
  // REVISIT: This looks frightening -> need to review
220
316
  const _transaction = async srv => {
221
317
  return new Promise(res => {
222
318
  const ret = {}
223
319
  const _tx = (ret._tx = srv.tx(
224
- async () =>
320
+ async tx =>
225
321
  (ret.promise = new Promise((resolve, reject) => {
322
+ // ensure rollback on shutdown so the db can disconnect
323
+ const _id = cds.utils.uuid()
324
+ _txs[_id] = tx
325
+ tx.context.on('done', () => delete _txs[_id])
326
+
226
327
  const proms = []
227
328
  // It's important to run `makePromise` in the current execution context (cb of srv.tx),
228
329
  // otherwise, it will use a different transaction.
@@ -258,7 +359,7 @@ const _tx_done = async (tx, responses, isJson) => {
258
359
  // here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
259
360
  rejected = 'rejected'
260
361
  // construct commit error (without modifying original error)
261
- const error = normalizeError(Object.create(e), {
362
+ const error = odataError(Object.create(e), {
262
363
  get locale() {
263
364
  return cds.context.locale
264
365
  },
@@ -463,7 +564,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
463
564
  }
464
565
 
465
566
  /*
466
- * multipart/mixed
567
+ * exports
467
568
  */
468
569
 
469
570
  const _multipartBatch = async (srv, router, req, res, next) => {
@@ -479,91 +580,6 @@ const _multipartBatch = async (srv, router, req, res, next) => {
479
580
  }
480
581
  }
481
582
 
482
- const _formatResponseMultipart = request => {
483
- const { res: response } = request
484
- const content_id = request.req?.headers['content-id']
485
-
486
- let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
487
- if (content_id) txt += `content-id: ${content_id}${CRLF}`
488
- txt += CRLF
489
- txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
490
-
491
- // REVISIT: tests require specific sequence
492
- const headers = {
493
- ...response.getHeaders(),
494
- ...(response.statusCode !== 204 && { 'content-type': 'application/json;odata.metadata=minimal' })
495
- }
496
- delete headers['content-length'] //> REVISIT: expected by tests
497
-
498
- for (const key in headers) {
499
- txt += key + ': ' + headers[key] + CRLF
500
- }
501
- txt += CRLF
502
-
503
- const _tryParse = x => {
504
- try {
505
- return JSON.parse(x)
506
- } catch {
507
- return x
508
- }
509
- }
510
-
511
- if (response._chunk) {
512
- const chunk = _tryParse(response._chunk)
513
- if (chunk && typeof chunk === 'object') {
514
- let meta = [],
515
- data = []
516
- for (const [k, v] of Object.entries(chunk)) {
517
- if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
518
- else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
519
- }
520
- const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
521
- txt += _json_as_txt
522
- } else {
523
- txt += chunk
524
- txt = txt.replace('content-type: application/json;odata.metadata=minimal', 'content-type: text/plain')
525
- }
526
- }
527
-
528
- return [txt]
529
- }
530
-
531
- /*
532
- * application/json
533
- */
534
-
535
- const _formatStatics = {
536
- comma: ','.charCodeAt(0),
537
- body: Buffer.from('"body":'),
538
- close: Buffer.from('}')
539
- }
540
-
541
- const _formatResponseJson = (request, atomicityGroup) => {
542
- const { id, res: response } = request
543
-
544
- const chunk = {
545
- id,
546
- status: response.statusCode,
547
- headers: {
548
- ...response.getHeaders(),
549
- 'content-type': 'application/json' //> REVISIT: why?
550
- }
551
- }
552
- if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
553
- const raw = Buffer.from(JSON.stringify(chunk))
554
-
555
- // body?
556
- if (!response._chunk) return [raw]
557
-
558
- // change last "}" into ","
559
- raw[raw.byteLength - 1] = _formatStatics.comma
560
- return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
561
- }
562
-
563
- /*
564
- * exports
565
- */
566
-
567
583
  module.exports = adapter => {
568
584
  const { router, service } = adapter
569
585
  const textBodyParser = express.text({
@@ -4,12 +4,16 @@ const { shutdown_on_uncaught_errors } = cds.env.server
4
4
  exports = module.exports = () =>
5
5
  function odata_error(err, req, res, next) {
6
6
  if (exports.pass_through(err)) return next(err)
7
- else req._is_odata = true
8
- if (err.details) err = _fioritized(err)
9
- exports.normalizeError(err, req)
7
+ exports.odataError(err, req)
10
8
  return next(err)
11
9
  }
12
10
 
11
+ exports.odataError = (err, req) => {
12
+ req._is_odata = true
13
+ if (err.details) err = _fioritized(err)
14
+ return exports.normalizeError(err, req)
15
+ }
16
+
13
17
  exports.pass_through = err => {
14
18
  if (err == 401 || err.code == 401) return true
15
19
  if (shutdown_on_uncaught_errors && !(err.status || err.statusCode) && cds.error.isSystemError(err)) return true
@@ -14,11 +14,13 @@ const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-
14
14
  let _isRelevantKey
15
15
 
16
16
  function _getDefinition(definition, name, namespace) {
17
- return (
18
- definition?.definitions?.[name] ||
19
- definition?.elements?.[name] ||
20
- (definition.actions && (definition.actions[name] || definition.actions[name.replace(namespace + '.', '')]))
21
- )
17
+ const def =
18
+ definition.definitions?.[name] ??
19
+ definition.elements?.[name] ??
20
+ definition.actions?.[name] ??
21
+ definition.actions?.[name.replace(namespace + '.', '')]
22
+
23
+ if (def && !def['@cds.api.ignore']) return def
22
24
  }
23
25
 
24
26
  function _resolveAliasesInRef(ref, target) {
@@ -150,7 +152,7 @@ function _convertVal(value, element) {
150
152
  case 'cds.Integer':
151
153
  case 'cds.Int16':
152
154
  case 'cds.Int32':
153
- if (!/^-?\+?\d+$/.test(value)) {
155
+ if (!/^[+-]?\d+$/.test(value)) {
154
156
  const msg = `Element "${element.name}" does not contain a valid Integer`
155
157
  cds.error({ status: 400, message: msg })
156
158
  }
@@ -238,7 +240,7 @@ const _getDataFromParams = (params, operation) => {
238
240
  return acc
239
241
  }, {})
240
242
  } catch (e) {
241
- throw Object.assign(e, { statusCode: 400, internal: e.message, message: 'Malformed parameters' })
243
+ cds.error(400, `Malformed Parameters`, { stack: e.stack, internal: e.message })
242
244
  }
243
245
  }
244
246
 
@@ -452,10 +454,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
452
454
  } else if (current.isAssociation) {
453
455
  if (current._target._service !== _get_service_of(target)) {
454
456
  // not exposed target
455
- cds.error({
456
- status: 400,
457
- message: `Property "${current.name}" does not exist in "${target.name.replace(namespace + '.', '')}"`
458
- })
457
+ _doesNotExistError(false, current.name, target.name.replace(namespace + '.', ''), current.kind)
459
458
  }
460
459
 
461
460
  // > navigation
@@ -339,13 +339,15 @@
339
339
  throw err
340
340
  }
341
341
  if (info.nodes?.length !== 1 || !info.nodes[0].filter) _throw()
342
+ // remove superfluous brackets
343
+ while (info.nodes[0].filter.length === 1 && info.nodes[0].filter[0]?.xpr)
344
+ info.nodes[0].filter = info.nodes[0].filter[0].xpr
342
345
  const rIdx = info.nodes[0].filter.findIndex(e => e.ref?.[0] === info.id)
343
- if (!rIdx === -1) _throw()
346
+ if (rIdx === -1) _throw()
344
347
  const op = info.nodes[0].filter[rIdx + 1]
345
348
  const val = info.nodes[0].filter[rIdx + 2]?.val
346
349
  // ignore additional filters (like `IsActiveEntity`?)
347
350
  if (op !== '=' || !val) _throw()
348
-
349
351
  const endVal = info.distance && direction * info.distance
350
352
  const distance = Number.isInteger(endVal) ? endVal : null
351
353
  const where = [{ func: 'DistanceTo', args: [{ val }, { val: distance }] }]