@sap/cds 8.3.0 → 8.4.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 (59) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/bin/serve.js +9 -2
  3. package/lib/auth/ias-auth.js +4 -1
  4. package/lib/auth/jwt-auth.js +4 -1
  5. package/lib/compile/cdsc.js +1 -1
  6. package/lib/compile/etc/_localized.js +1 -0
  7. package/lib/compile/extend.js +23 -23
  8. package/lib/compile/for/lean_drafts.js +5 -0
  9. package/lib/compile/to/srvinfo.js +3 -1
  10. package/lib/{linked → core}/classes.js +8 -6
  11. package/lib/{linked/models.js → core/linked-csn.js} +4 -0
  12. package/lib/env/defaults.js +4 -1
  13. package/lib/i18n/localize.js +2 -2
  14. package/lib/index.js +43 -59
  15. package/lib/log/cds-error.js +21 -21
  16. package/lib/ql/cds-ql.js +5 -5
  17. package/lib/req/cds-context.js +5 -0
  18. package/lib/req/context.js +2 -2
  19. package/lib/req/locale.js +25 -21
  20. package/lib/{linked → req}/validate.js +11 -9
  21. package/lib/srv/cds-serve.js +1 -1
  22. package/lib/srv/middlewares/cds-context.js +1 -1
  23. package/lib/srv/middlewares/errors.js +20 -7
  24. package/lib/srv/protocols/hcql.js +106 -43
  25. package/lib/srv/protocols/http.js +2 -2
  26. package/lib/srv/protocols/index.js +14 -10
  27. package/lib/srv/protocols/odata-v4.js +2 -26
  28. package/lib/srv/protocols/okra.js +24 -0
  29. package/lib/srv/srv-models.js +6 -8
  30. package/lib/{utils → test}/cds-test.js +5 -5
  31. package/lib/utils/check-version.js +8 -15
  32. package/lib/utils/extend.js +20 -0
  33. package/lib/utils/lazify.js +33 -0
  34. package/lib/utils/tar.js +39 -1
  35. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -1
  36. package/libx/_runtime/common/error/frontend.js +18 -4
  37. package/libx/_runtime/common/generic/auth/restrict.js +1 -3
  38. package/libx/_runtime/common/generic/sorting.js +1 -1
  39. package/libx/_runtime/common/utils/compareJson.js +139 -53
  40. package/libx/_runtime/common/utils/resolveView.js +19 -23
  41. package/libx/_runtime/fiori/lean-draft.js +2 -2
  42. package/libx/_runtime/messaging/kafka.js +7 -1
  43. package/libx/_runtime/remote/utils/data.js +30 -24
  44. package/libx/odata/ODataAdapter.js +12 -7
  45. package/libx/odata/middleware/batch.js +3 -0
  46. package/libx/odata/middleware/error.js +6 -0
  47. package/libx/odata/parse/afterburner.js +5 -6
  48. package/libx/odata/parse/multipartToJson.js +12 -8
  49. package/libx/odata/utils/index.js +3 -2
  50. package/libx/odata/utils/metadata.js +31 -1
  51. package/libx/outbox/index.js +5 -1
  52. package/package.json +3 -4
  53. package/server.js +18 -0
  54. package/lib/lazy.js +0 -51
  55. package/lib/test/index.js +0 -2
  56. /package/lib/{linked → core}/entities.js +0 -0
  57. /package/lib/{linked → core}/types.js +0 -0
  58. /package/lib/{utils → test}/axios.js +0 -0
  59. /package/lib/{utils → test}/data.js +0 -0
@@ -1,6 +1,9 @@
1
+ const cds = require('../../../')
2
+
1
3
  const { parsers, freeParser, HTTPParser } = require('_http_common')
2
4
  const { PassThrough, Readable } = require('stream')
3
5
  const streamConsumers = require('stream/consumers')
6
+
4
7
  const { getBoundary } = require('../utils')
5
8
 
6
9
  const CRLF = '\r\n'
@@ -42,16 +45,17 @@ const parseStream = async function* (body, boundary) {
42
45
  body: streamConsumers.json(wrapper).catch(() => {})
43
46
  }
44
47
 
45
- const dependencies = [...req.url.matchAll(/\$(\d+)/g)]
48
+ const dependencies = [...req.url.matchAll(/^\/?\$([\d.\-_~a-zA-Z]+)/g)]
46
49
  if (dependencies.length) {
47
50
  request.dependsOn = []
48
51
  for (const dependency of dependencies) {
49
- const index = Number.parseInt(dependency[1], 10)
50
- if (Number.isNaN(index)) continue
51
- const i = requests.findIndex(r => r.content_id == index) //> prefer content-id
52
- const id = requests[i > -1 ? i : index - 1].id
53
- request.dependsOn.push(id)
54
- request.url = request.url.replace(`$${index}`, `$${id}`)
52
+ const dependencyId = dependency[1]
53
+ const dependsOnRequest = requests.findLast(r => r.content_id == dependencyId) //> prefer content-id
54
+ if (!dependsOnRequest) {
55
+ continue
56
+ }
57
+ request.dependsOn.push(dependsOnRequest.id)
58
+ request.url = request.url.replace(`$${dependencyId}`, `$${dependsOnRequest.id}`)
55
59
  }
56
60
  if (request.url[1] === '$') request.url = request.url.slice(1)
57
61
  }
@@ -62,7 +66,7 @@ const parseStream = async function* (body, boundary) {
62
66
  requests.push(request)
63
67
  }
64
68
 
65
- parser.initialize(HTTPParser.REQUEST, { type: 'HTTPINCOMINGMESSAGE' })
69
+ parser.initialize(HTTPParser.REQUEST, { type: 'HTTPINCOMINGMESSAGE' }, cds.env.odata.max_batch_header_size)
66
70
 
67
71
  if (typeof body === 'string') body = [body]
68
72
 
@@ -278,9 +278,11 @@ const skipToken = (token, cqn) => {
278
278
  const calculateLocationHeader = (target, srv, result) => {
279
279
  const targetName = target.name.replace(`${srv.definition.name}.`, '')
280
280
  const filteredKeys = [...target.keys].filter(k => !k.isAssociation).map(k => k.name)
281
+ if (!filteredKeys.every(k => k in result)) return
282
+
281
283
  const keyValuePairs = filteredKeys.reduce((acc, key) => {
282
284
  const value = result[key]
283
- if (value === undefined) return
285
+ if (value === undefined) return acc
284
286
  if (Buffer.isBuffer(value)) {
285
287
  acc[key] = value.toString('base64')
286
288
  } else {
@@ -290,7 +292,6 @@ const calculateLocationHeader = (target, srv, result) => {
290
292
  }
291
293
  return acc
292
294
  }, {})
293
- if (!keyValuePairs) return
294
295
  let keys
295
296
  const entries = Object.entries(keyValuePairs)
296
297
  if (entries.length === 1) {
@@ -1,5 +1,5 @@
1
1
  const { cds2edm } = require('./index')
2
-
2
+ const cds = require('../../../lib')
3
3
  const { where2obj } = require('../../_runtime/common/utils/cqn')
4
4
 
5
5
  const _isNavToDraftAdmin = path => path.length > 1 && path[path.length - 1] === 'DraftAdministrativeData'
@@ -109,6 +109,36 @@ const _odataContext = (query, options) => {
109
109
  path += '/' + lastRef
110
110
  }
111
111
 
112
+ if (cds.env.odata.context_with_columns && query.SELECT && !isSingleton && !propertyAccess) {
113
+ const { columns } = query.SELECT
114
+ let containments = []
115
+
116
+ function processColumns(column) {
117
+ const refName = column.ref?.[0]
118
+ if (!refName) return ''
119
+
120
+ if (column.expand) {
121
+ // Process nested expands recursively
122
+ const expandSelects = column.expand.map(exp => processColumns(exp)).filter(Boolean)
123
+ return `${refName}(${expandSelects.join(',')})`
124
+ } else if (column.xpr) {
125
+ // Handle xpr cases
126
+ const xprRefName = column.xpr.find(item => item.ref?.[0])?.ref?.[0]
127
+ return xprRefName || ''
128
+ } else if (refName !== '*') {
129
+ return refName
130
+ }
131
+ }
132
+
133
+ columns.forEach(column => {
134
+ const result = processColumns(column)
135
+ if (result) {
136
+ containments.push(result)
137
+ }
138
+ })
139
+ if (containments.length) path += `(${containments.join(',')})`
140
+ }
141
+
112
142
  if ((!isCollection && !isSingleton && !propertyAccess) || (isNavToDraftAdmin && !propertyAccess)) {
113
143
  path += '/$entity'
114
144
  }
@@ -287,7 +287,11 @@ function outboxed(srv, customOpts) {
287
287
  else await originalSrv.emit(_req)
288
288
  } catch (e) {
289
289
  LOG.error('Emit failed', { event: _req.event, cause: e })
290
- if (isStandardError(e)) cds.exit(1)
290
+ if (isStandardError(e)) {
291
+ cds.log().error('❗️Uncaught', e)
292
+ await cds.shutdown(e)
293
+ return
294
+ }
291
295
  }
292
296
  }
293
297
  delete context[$stored_reqs]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -25,11 +25,10 @@
25
25
  "libx/",
26
26
  "srv/",
27
27
  "tasks/",
28
- "server.js",
29
28
  "common.cds",
29
+ "server.js",
30
30
  "eslint.config.mjs",
31
- "CHANGELOG.md",
32
- "LICENSE"
31
+ "CHANGELOG.md"
33
32
  ],
34
33
  "engines": {
35
34
  "node": ">=18"
package/server.js CHANGED
@@ -17,6 +17,24 @@ module.exports = async function cds_server (options) {
17
17
 
18
18
  // prepare express app
19
19
  const o = { ...options, __proto__:defaults }
20
+
21
+ // handle cluster mode
22
+ let WORKERS = o.workers || process.env.WORKERS || process.env.CDS_WORKERS
23
+ if (WORKERS) {
24
+
25
+ const LOG = cds.log('cluster|server')
26
+ const cluster = require('cluster')
27
+ if (cluster.isMaster) {
28
+ if (WORKERS in {true:1}) WORKERS = require('os').availableParallelism()
29
+ LOG.info (`Spawning ${WORKERS} workers...`)
30
+ for (let i=0; i < WORKERS; ) cluster.fork ({ id: ++i })
31
+ return { on(){}, once(){}, close(){} } // dummy http server
32
+ }
33
+
34
+ else LOG.info(`Worker ${process.env.id} started - pid: ${process.pid}`)
35
+ // and continue boostrapping cds.server below...
36
+ }
37
+
20
38
  const app = cds.app = o.app || express()
21
39
  cds.emit ('bootstrap', app)
22
40
 
package/lib/lazy.js DELETED
@@ -1,51 +0,0 @@
1
- /** @type <T> (target:T) => ({
2
- with <X,Y,Z> (x:X, y:Y, z:Z): ( T & X & Y & Z )
3
- with <X,Y> (x:X, y:Y): ( T & X & Y )
4
- with <X> (x:X): ( T & X )
5
- }) */
6
- const extend = (target) => ({
7
- with(...aspects) {
8
- const excludes = _excludes[typeof target] || {}
9
- for (let each of aspects) {
10
- for (let p of Reflect.ownKeys(each)) {
11
- if (p in excludes) continue
12
- Reflect.defineProperty (target,p, Reflect.getOwnPropertyDescriptor(each,p))
13
- }
14
- if (is_class(target) && is_class(each)) {
15
- extend(target.prototype).with(each.prototype)
16
- }
17
- }
18
- return target
19
- },
20
- })
21
-
22
- const _excludes = {
23
- function: Object.assign(Object.create(null),{ name: 1, length: 2, arguments: 3, caller: 4, prototype: 5 }),
24
- object: Object.assign(Object.create(null),{ constructor: 1 }),
25
- }
26
-
27
- /** @type <T>(target:T) => T */
28
- const lazify = (o) => {
29
- if (o.constructor === module.constructor) return lazify_module(o)
30
- for (let p of Reflect.ownKeys(o)) {
31
- const d = Reflect.getOwnPropertyDescriptor(o,p)
32
- if (is_lazy(d.value)) Reflect.defineProperty (o,p,{
33
- set(v) { Reflect.defineProperty (this,p,{value:v,__proto__:d}) },
34
- get() { return this[p] = d.value.call(this,p,this) },
35
- configurable: true,
36
- })
37
- }
38
- return o
39
- }
40
-
41
- const lazify_module = (module) => {
42
- extend(module).with({ set exports(all) {
43
- extend(module).with({ exports: lazify(all) })
44
- }})
45
- return (id) => (lazy) => module.require(id) // eslint-disable-line no-unused-vars
46
- }
47
-
48
- const is_lazy = (x) => typeof x === 'function' && /^(function\s?)?\(?lazy[,)\t =]/.test(x)
49
- const is_class = (x) => typeof x === 'function' && x.prototype && /^class\b/.test(x)
50
-
51
- module.exports = { extend, lazify, lazified:lazify }
package/lib/test/index.js DELETED
@@ -1,2 +0,0 @@
1
- // we might move cds-test.js here, but for now we just re-export it
2
- module.exports = require('../utils/cds-test')
File without changes
File without changes
File without changes
File without changes