@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
@@ -241,14 +241,16 @@ class Association extends struct {
241
241
  /** Compositions are like nested entities, validating deep input against their target entity definitions. */
242
242
  class Composition extends entity {
243
243
  validate (data, path, ctx) { if (!data) return
244
- const elements = this._target.elements
245
- const uplinks = {} // statically determine the uplinks for this composition
246
- if (this.on) for (let {ref} of this.on) if (ref?.[0] === this.name) {
247
- const fk = ref[1], fk_ = fk+'_'; uplinks[fk] = true
248
- for (let e in elements) if (e.startsWith(fk_)) uplinks[e] = true
249
- }
250
- this.validate = (data, path, ctx) => super.validate (data, path, ctx, elements, uplinks)
251
- this.validate (data, path, ctx) // call first time
244
+ const _validate = this.own('_validate', () => {
245
+ const elements = this._target.elements
246
+ const uplinks = {} // statically determine the uplinks for this composition
247
+ if (this.on) for (let {ref} of this.on) if (ref?.[0] === this.name) {
248
+ const fk = ref[1], fk_ = fk+'_'; uplinks[fk] = true
249
+ for (let e in elements) if (e.startsWith(fk_)) uplinks[e] = true
250
+ }
251
+ return (data, path, ctx) => super.validate (data, path, ctx, elements, uplinks)
252
+ })
253
+ _validate (data, path, ctx)
252
254
  }
253
255
  }
254
256
 
@@ -312,4 +314,4 @@ $.LargeBinary.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 's
312
314
  $.LargeString.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 'string' || v instanceof Readable
313
315
 
314
316
  // Mixin above class extensions to cds.linked.classes
315
- $.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
317
+ $.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
@@ -1,6 +1,5 @@
1
1
  const cds = require ('..')
2
2
  const { Service } = cds.service.factory
3
- const { serve } = cds.service.protocols
4
3
  const _pending = cds.services._pending ??= {}
5
4
  const _ready = Symbol()
6
5
  const TRACE = cds.debug('trace')
@@ -102,6 +101,7 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
102
101
 
103
102
  /** Fluent method to serve constructed providers to express app */
104
103
  in (app) {
104
+ const { serve } = cds.service.protocols
105
105
  if (!cds.env.features.odata_new_adapter && cds.edmxs) ready = ready.then (()=> cds.edmxs)
106
106
  ready = ready.then (()=> all.forEach (each => {
107
107
  const d = each.definition
@@ -9,7 +9,7 @@ const { EventContext } = cds
9
9
  module.exports = () => {
10
10
  /** @type { import('express').Handler } */
11
11
  return function cds_context (req, res, next) {
12
- const id = req.headers[corr_id] ??= req.headers[req_id] || req.headers[vr_id] || req.headers[crippled_corr_id] || uuid()
12
+ const id = req.headers[corr_id] ??= req.headers[crippled_corr_id] || req.headers[req_id] || req.headers[vr_id] || uuid()
13
13
  const ctx = EventContext.for ({ id, http: { req, res } })
14
14
  res.set ('X-Correlation-ID', id) // Note: we use capitalized style here as that's common standard in HTTP world
15
15
  cds._context.run (ctx, next)
@@ -1,10 +1,18 @@
1
+ const { isStandardError } = require('../../../libx/_runtime/common/error/standardError')
2
+
1
3
  const production = process.env.NODE_ENV === 'production'
2
4
  const cds = require ('../..')
3
5
  const LOG = cds.log('error')
4
6
  const { inspect } = cds.utils
5
7
 
8
+
6
9
  module.exports = () => {
7
- return function http_error (error, req, res, _next) { // eslint-disable-line no-unused-vars
10
+ return async function http_error(error, req, res, next) {
11
+ if (isStandardError(error) && cds.env.server.shutdown_on_uncaught_errors) {
12
+ cds.log().error('❗️Uncaught', error)
13
+ await cds.shutdown(error)
14
+ return;
15
+ }
8
16
 
9
17
  // In case of 401 require login if available by auth strategy
10
18
  if (typeof error === 'number') error = { code: error }
@@ -14,7 +22,8 @@ module.exports = () => {
14
22
  const status = error.statusCode || error.status || Number(error.code) || 500
15
23
  delete error.statusCode
16
24
  delete error.status
17
- if (!production && error.stack) error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g,'')
25
+ if (!production && error.stack)
26
+ error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g, '')
18
27
 
19
28
  if (400 <= status && status < 500) {
20
29
  LOG.warn (status, '>', inspect(error))
@@ -24,19 +33,23 @@ module.exports = () => {
24
33
 
25
34
  // Expose as little information as possible in production, and as much as possible in development
26
35
  if (production) {
27
- Object.defineProperties (error, {
36
+ Object.defineProperties(error, {
28
37
  message: { enumerable: true },
29
- user: { enumerable: false },
38
+ user: { enumerable: false }
30
39
  })
31
40
  } else {
32
- Object.defineProperties (error, {
41
+ Object.defineProperties(error, {
33
42
  message: { enumerable: true },
34
- stack: { enumerable: true },
43
+ stack: { enumerable: true }
35
44
  })
36
45
  }
37
46
 
47
+ if (res.headersSent) {
48
+ return next(error)
49
+ }
50
+
38
51
  // Send the error response
39
- return res.status(status).json({error})
52
+ return res.status(status).json({ error })
40
53
 
41
54
  // Note: express returns errors as XML, we prefer JSON
42
55
  // _next (error)
@@ -1,57 +1,120 @@
1
+ const cds = require('../../index'), {inspect} = cds.utils
1
2
  const express = require('express')
2
- const cds = require('../../index')
3
- const DEBUG = cds.debug('hcql')
4
- const { inspect } = require('util')
3
+ const LOG = cds.log('hcql')
5
4
 
6
5
  class HCQLAdapter extends require('./http') {
7
6
 
8
7
  get router() {
8
+
9
9
  const srv = this.service
10
- return super.router
11
-
12
- /**
13
- * Return CSN schema in response to /<srv>/$csn requests
14
- */
15
- .get('/\\$csn', (_, res) => res.json(this.schema))
16
-
17
- .use(express.json(this.body_parser_options)) //> for application/json -> cqn
18
- .use(express.text(this.body_parser_options)) //> for text/plain -> cql -> cqn
19
-
20
- /**
21
- * Convenience route for REST-style request formats like that:
22
- * GET /browse/Books { ID, title, author.name as author } where stock < 100
23
- * GET /browse/Books/201 { ID, title, author.name as author }
24
- */
25
- .get('/:entity/:id?(%20:tail)?', (req, _, next) => {
26
- let { entity, id, tail } = req.params, q = SELECT.from(entity, id)
27
- if (is_string(req.body)) tail = req.body
28
- else if (is_array(req.body)) q.columns(req.body)
29
- else Object.assign(q.SELECT, req.body)
30
- if (tail) q = { SELECT: { ...CQL(`SELECT from _ ${tail}`).SELECT, ...q.SELECT } }
31
- req.body = q; next() // delegating to main handler
32
- })
33
-
34
- /**
35
- * The actual protocol adapter, handling all requests.
36
- */
37
- .use((req, res, next) => {
38
- let q = this.query4(req)
39
- DEBUG?.(inspect(q,{depth:11,colors:true}))
40
- return srv.run(q).then(r => res.json(r)).catch(next)
41
- })
10
+ const router = super.router
11
+ .get ('/\\$csn', this.schema.bind(this)) //> return the CSN as schema
12
+ .use (express.json(this.body_parser_options)) //> for application/json -> cqn
13
+ .use (express.text(this.body_parser_options)) //> for text/plain -> cql -> cqn
14
+
15
+ // Route for custom actions and functions ...
16
+ const action = this.action.bind(this)
17
+ router.param('action', (req,_,next,a) => { (req.action = a) in srv.actions ? next() : next('route') })
18
+ router.route('/:action')
19
+ .post (action)
20
+ .get (action)
21
+ .all ((req,res,next) => next(501))
22
+
23
+ // Route for REST-style convenience shortcuts with queries in URL + body ...
24
+ if (process.env.NODE_ENV !== 'production') {
25
+ const $ = cb => (req,_,next) => { req.body = cb(req.params,req); next() }
26
+ router.param('entity', (req,_,next,a) => { a in srv.entities ? next() : next(404) })
27
+ router.route('/:entity/:id?(%20?:tail)?')
28
+ .get ($(({entity,id,tail}, req) => {
29
+ const body = typeof req.body === 'string' ? req.body : ''
30
+ return tail || body ? {SELECT:{
31
+ ...CQL(`SELECT from _ ${body} ${tail||''}`).SELECT,
32
+ ...SELECT.from (entity,id).SELECT
33
+ }} : SELECT.from (entity,id)
34
+ }))
35
+ .post ($(({entity}, {query,body}) => INSERT.into (entity) .entries ({...query,...body})))
36
+ .put ($(({entity,id}, {query,body}) => UPDATE (entity,id) .with ({...query,...body})))
37
+ .patch ($(({entity,id}, {query,body}) => UPDATE (entity,id) .with ({...query,...body})))
38
+ .delete ($(({entity,id}) => DELETE.from (entity, id)))
39
+ }
40
+
41
+ // The ultimate handler for CRUD requests
42
+ router.use (this.crud.bind(this))
43
+ return router
42
44
  }
43
45
 
44
- get schema() {
45
- return cds.minify (cds.model, { service: this.service.name })
46
+
47
+ /**
48
+ * Handle requests to custom actions and functions.
49
+ */
50
+ action (req, res, next) {
51
+ return this.service.send (req.action, { ...req.query, ...req.body })
52
+ .then (results => this.reply (results, res))
53
+ .catch (next)
46
54
  }
47
55
 
48
- query4 (req) {
49
- if (typeof req.body === 'string') return req.body = cds.parse.cql(req.body)
50
- return req.body //> a plain CQN object
56
+
57
+ /**
58
+ * The ultimate handler for all CRUD requests.
59
+ */
60
+ crud (req, res, next) {
61
+ let query = this.query4 (req)
62
+ return this.service.run (query)
63
+ .then (results => this.reply (results, res))
64
+ .catch (next)
51
65
  }
52
- }
53
66
 
54
- const is_string = x => typeof x === 'string'
55
- const is_array = Array.isArray
67
+
68
+ /**
69
+ * Constructs an instance of cds.ql.Query from an incoming request body,
70
+ * which is expected to be a plain CQN object or a CQL string.
71
+ */
72
+ query4 (/** @type express.Request */ req) {
73
+ let b = req.body; if (typeof b === 'string') b = cds.parse.cql(b)
74
+ let q = req.body = cds.ql.query(b); if (!q) return this.error (400, 'Invalid query', { query: req.body })
75
+ // assert valid target entity
76
+ if (q.target?._unresolved && this.service.definition) {
77
+ q.target = q._target = this.service.entities [q.target.name]
78
+ || this.error (404, `'${q.target.name}' is not an entity served by '${this.service.name}'.`, { query:q })
79
+ }
80
+ // handle request headers
81
+ if (q.SELECT) {
82
+ if (req.get('Accept-Language')) q.SELECT.localized = true
83
+ if (req.get('X-Total-Count')) q.SELECT.count = true
84
+ }
85
+ // got a valid query
86
+ if (LOG._debug) LOG.debug (inspect(q))
87
+ return q
88
+ }
89
+
90
+ /**
91
+ * Serialize the results into response.
92
+ */
93
+ reply (results, /** @type express.Response */ res) {
94
+ if (!results) return res.end()
95
+ if (results.$count) res.set ('X-Total-Count', results.$count)
96
+ if (typeof results === 'object') return res.json (results)
97
+ else res.send (results)
98
+ }
99
+
100
+ /**
101
+ * Throw an Error with given status and message.
102
+ */
103
+ error (status, message, details) {
104
+ if (typeof status === 'string') [ message, details, status ] = [ status, message ]
105
+ let err = Object.assign (new Error(message), details)
106
+ if (status) err.status = status
107
+ if (new.target) return err
108
+ else throw err
109
+ }
110
+
111
+ /**
112
+ * Return the CSN as schema in response to /<srv>/$csn requests
113
+ */
114
+ schema (_, res) {
115
+ let csn = cds.minify (this.service.model, { service: this.service.name })
116
+ return res.json (csn)
117
+ }
118
+ }
56
119
 
57
120
  module.exports = HCQLAdapter
@@ -15,9 +15,9 @@ class HttpAdapter {
15
15
  return this.router //> constructed by getter
16
16
  }
17
17
 
18
- /** The actual Router factory. Subclasses override this to add specific handlers. */
18
+ /** The actual Router factory. Subclasses override this to add specific handlers. @returns {express.Router} */
19
19
  get router() {
20
- let router = super.router = (new express.Router)
20
+ let router = super.router = new express.Router
21
21
  this.use (this.http_log)
22
22
  this.use (this.requires_check)
23
23
  return router
@@ -46,7 +46,7 @@ class Protocols {
46
46
  */
47
47
  serve (srv, /* in: */ app, { before, after } = cds.middlewares) {
48
48
 
49
- const endpoints = srv.endpoints = this.endpoints4(srv)
49
+ const endpoints = srv.endpoints ??= this.endpoints4(srv)
50
50
  const cached = srv._adapters ??= {}
51
51
  let n = 0
52
52
 
@@ -101,7 +101,7 @@ class Protocols {
101
101
  // get @protocol annotations from service definition
102
102
  let annos = o?.to || def['@protocol']
103
103
  if (annos) {
104
- if (annos === 'none') return
104
+ if (annos === 'none' || annos['='] === 'none') return []
105
105
  if (!annos.reduce) annos = [annos]
106
106
  }
107
107
  // get @odata, @rest annotations
@@ -117,13 +117,14 @@ class Protocols {
117
117
  // canonicalize to { kind, path } objects
118
118
  const endpoints = annos.map (each => {
119
119
  let { kind = each['='] || each, path } = each
120
- if (!(kind in this)) return cds.log('adapters').warn ('ignoring unknown protocol:', kind)
120
+ if (!(kind in this))
121
+ return cds.log('adapters').warn ('ignoring unknown protocol:', kind)
121
122
  if (typeof path !== 'string') path = o?.at || o?.path || def['@path'] || _slugified(srv.name)
122
123
  if (path[0] !== '/') path = this[kind].path + '/' + path // prefix with protocol path
123
124
  return { kind, path }
124
125
  }) .filter (e => e) //> skipping unknown protocols
125
126
 
126
- return endpoints.length && endpoints
127
+ return endpoints //.length ? endpoints : null
127
128
  }
128
129
 
129
130
 
@@ -141,7 +142,8 @@ class Protocols {
141
142
  */
142
143
  path4 (srv,o) {
143
144
  if (!srv.definition) srv = { definition: srv, name: srv.name } // fake srv object
144
- return this.endpoints4(srv,o)?.[0]?.path
145
+ const endpoints = srv.endpoints ??= this.endpoints4(srv,o)
146
+ return endpoints[0]?.path
145
147
  }
146
148
 
147
149
  /**
@@ -152,11 +154,13 @@ class Protocols {
152
154
  const protocols={}; let any
153
155
 
154
156
  // check @protocol annotation -> deprecated, only for 'none'
155
- const anno = def['@protocol']
156
-
157
- if (anno === 'none') return protocols
158
- if (typeof anno === 'string') any = protocols [anno] = 1
159
- else if (anno) for (let p of anno) any = protocols[p.kind||p] = 1
157
+ let a = def['@protocol']
158
+ if (a) {
159
+ const pa = a['='] || a
160
+ if (pa === 'none') return protocols
161
+ if (typeof pa === 'string') any = protocols[pa] = 1
162
+ else for (let p of pa) any = protocols[p.kind||p] = 1
163
+ }
160
164
 
161
165
  // @odata, @rest, ... annotations -> preferred
162
166
  else for (let p in this) if (def['@'+p] || def['@protocol.'+p]) any = protocols[p] = 1
@@ -1,31 +1,7 @@
1
- const cds = require('../../index'), { decodeURI } = cds.utils
2
-
1
+ const cds = require('../../index')
3
2
  if (cds.env.features.odata_new_adapter) {
4
3
  cds.log().info('using new OData adapter')
5
-
6
4
  module.exports = require('../../../libx/odata/ODataAdapter')
7
5
  } else {
8
- cds.log().info('using legacy OData adapter')
9
-
10
- const legacy_adapter_factory = require('../../../libx/_runtime/cds-services/adapter/odata-v4/to')
11
- const LOG = cds.log('odata')
12
-
13
- module.exports = function ODataAdapter(srv) {
14
- const router = require('express').Router()
15
-
16
- router.use(function odata_log(req, _, next) {
17
- let url = decodeURI(req.originalUrl)
18
- LOG && LOG(req.method, url, req.body || '')
19
- if (/\$batch/.test(req.url))
20
- req.on('dispatch', req => {
21
- let path = decodeURI(req._.odataReq?._rawODataPath || '')
22
- LOG && LOG('>', req.event, path, req._queryOptions || '')
23
- if (LOG._debug && req.query) LOG.debug(req.query) //> why only for batch subrequests?
24
- })
25
-
26
- next()
27
- })
28
- router.use(legacy_adapter_factory(srv))
29
- return router
30
- }
6
+ module.exports = require('./okra')
31
7
  }
@@ -0,0 +1,24 @@
1
+ const cds = require('../../index'), { decodeURI } = cds.utils
2
+ cds.log().info('using legacy OData adapter')
3
+
4
+ const legacy_adapter4 = require('../../../libx/_runtime/cds-services/adapter/odata-v4/to')
5
+ const LOG = cds.log('okra')
6
+
7
+ module.exports = function ODataAdapter(srv) {
8
+ const router = require('express').Router()
9
+
10
+ router.use(function odata_log(req, _, next) {
11
+ let url = decodeURI(req.originalUrl)
12
+ LOG && LOG(req.method, url, req.body || '')
13
+ if (/\$batch/.test(req.url))
14
+ req.on('dispatch', req => {
15
+ let path = decodeURI(req._.odataReq?._rawODataPath || '')
16
+ LOG && LOG('>', req.event, path, req._queryOptions || '')
17
+ if (LOG._debug && req.query) LOG.debug(req.query) //> why only for batch subrequests?
18
+ })
19
+
20
+ next()
21
+ })
22
+ router.use(legacy_adapter4(srv))
23
+ return router
24
+ }
@@ -73,13 +73,13 @@ class ExtendedModels {
73
73
  const model = this[key]; if (!model) return
74
74
  if (model.then) return model //> promised model to avoid race conditions
75
75
 
76
- const {_cached} = model, interval = ExtendedModels.checkInterval
77
- if (Date.now() - _cached.touched < interval) return model //> checked recently
76
+ const { $touched: touched } = model, interval = ExtendedModels.checkInterval
77
+ if (Date.now() - touched < interval) return model //> checked recently
78
78
 
79
79
  else return this[key] = (async()=>{ // temporarily replace cache entry by promise to avoid race conditions...
80
80
 
81
81
  const has_new_extensions = await cds.db.exists('cds.xt.Extensions') .where ({
82
- timestamp: { '>': new Date(_cached.touched).toISOString() } // REVISIT: better store epoc time in db?
82
+ timestamp: { '>': new Date(touched).toISOString() } // REVISIT: better store epoc time in db?
83
83
  // REVISIT: GAP: CAP runtime should allow Date objects + Date.now() for all date+time types !
84
84
  })
85
85
  if (has_new_extensions) { // new extensions arrived -> refresh model in cache
@@ -87,7 +87,7 @@ class ExtendedModels {
87
87
  cds.emit('cds.xt.TENANT_UPDATED', { tenant })
88
88
  return _get_model4 (tenant, toggles.split(','))
89
89
  } else { // no new extensions...
90
- _cached.touched = Date.now() // check again in 1 min or so
90
+ model.$touched = Date.now() // check again in 1 min or so
91
91
  return model // keep cached model in cache
92
92
  }
93
93
 
@@ -107,7 +107,7 @@ class ExtendedModels {
107
107
  */
108
108
  add (key, model, touched = Date.now()) {
109
109
  if (model) {
110
- if (!model._cached) Object.defineProperty (model,'_cached',{ value: { touched } })
110
+ model.$touched ??= touched
111
111
  return this[key] = model
112
112
  }
113
113
  }
@@ -118,10 +118,8 @@ class ExtendedModels {
118
118
  */
119
119
  startSentinel(){
120
120
  this.sentinel = setInterval (()=>{ for (let [key,m] of Object.entries(this)) {
121
- if (!m._cached) continue // `m` can also be `this.sentinel`
122
- if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
121
+ if (Date.now() - m.$touched > ExtendedModels.sentinelInterval)
123
122
  delete this [key]
124
- }
125
123
  }}, ExtendedModels.sentinelInterval).unref()
126
124
  }
127
125
 
@@ -35,13 +35,13 @@ class Test extends require('./axios') {
35
35
  })
36
36
 
37
37
  // gracefully shutdown cds server...
38
- after (()=>{
39
- this.server && cds.shutdown()
38
+ after (async ()=>{
39
+ await cds.shutdown()
40
40
  // cds.service.providers = []
41
41
  // delete cds.services
42
42
  // delete cds.plugins
43
43
  // delete cds.env
44
- return cds.utils.rimraf(process.env.cds_test_temp)
44
+ await cds.utils.rimraf (process.env.cds_test_temp)
45
45
  })
46
46
 
47
47
  return this
@@ -186,7 +186,7 @@ let _expect = undefined
186
186
  global.afterAll = global.after = (msg,fn) => repl.on?.('exit',fn||msg)
187
187
  global.beforeEach = global.afterEach = ()=>{}
188
188
  global.describe = ()=>{}
189
- global.expect = _expect = require('../test/expect')
189
+ global.expect = _expect = require('./expect')
190
190
 
191
191
  } else if (is_mocha) { // it's mocha
192
192
 
@@ -220,7 +220,7 @@ let _expect = undefined
220
220
  global.afterAll = global.after = (msg,fn) => after(fn||msg)
221
221
  global.beforeEach = beforeEach
222
222
  global.afterEach = afterEach
223
- global.expect = _expect = require('../test/expect')
223
+ global.expect = _expect = require('./expect')
224
224
  // suite was introduced in Node 22
225
225
  suite?.('<next>', ()=>{}) //> to signal the start of a test file
226
226
 
@@ -1,16 +1,9 @@
1
- const required = _major_minor(
2
- require('../../package.json').engines.node.match(/>=(.*)/)[1]
3
- )
4
- const given = _major_minor(
5
- process.version.match(/^v(\d+\.\d+)/)[1]
6
- )
7
-
8
- if (given.major < required.major || given.major === required.major && given.minor < required.minor) process.exit (process.stderr.write (`
9
- Node.js v${required.version} or higher is required for @sap/cds.
10
- Current v${given.version} does not satisfy this.
11
- \n`) || 1)
12
-
13
- function _major_minor (version) {
14
- let [ major, minor ] = version.split('.').map(x => +x)
15
- return { version, major, minor }
1
+ let version = /(\d+)(?:\.(\d+).*)?/ .exec (require('../../package.json').engines.node)
2
+ let given = /(\d+)(?:\.(\d+).*)?/ .exec (process.version)
3
+ if (+given[1] < +version[1] || given[1] == version[1] && +given[2] < +version[2]) {
4
+ process.stderr.write (`
5
+ Node.js version ${version[0]} or higher is required for @sap/cds.
6
+ Current version ${given[0]} does not satisfy this.
7
+ \n`)
8
+ process.exit(1)
16
9
  }
@@ -0,0 +1,20 @@
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
+ module.exports = (target) => ({ with (...aspects) {
7
+ const t = is_class(target) ? target.prototype : target
8
+ const excludes = _excludes [typeof t] || {}
9
+ for (let each of aspects) {
10
+ const a = is_class(each) ? each.prototype : each
11
+ for (let p of Reflect.ownKeys(a)) {
12
+ if (p in excludes) continue
13
+ Reflect.defineProperty (t,p, Reflect.getOwnPropertyDescriptor(a,p))
14
+ }
15
+ }
16
+ return target
17
+ }})
18
+
19
+ const _excludes = { object:{}, function: function(){}, }
20
+ const is_class = (x) => typeof x === 'function' && x.prototype && /^class\b/.test(x)
@@ -0,0 +1,33 @@
1
+ /** @type <T>(target:T) => T */
2
+ const lazify = module.exports = (o) => {
3
+ if (o.constructor === module.constructor) return lazify_module(o)
4
+ for (let p of Reflect.ownKeys(o)) {
5
+ const d = Reflect.getOwnPropertyDescriptor(o,p)
6
+ if (is_lazy(d.value)) Reflect.defineProperty (o,p,{
7
+ set(v) { Reflect.defineProperty (this,p,{value:v,__proto__:d}) },
8
+ get() { return this[p] = d.value.call(this,p,this) },
9
+ configurable: true,
10
+ })
11
+ }
12
+ return o
13
+ }
14
+
15
+ /**
16
+ * Used to lazify a module's exports.
17
+ * @example
18
+ * require = lazify (module)
19
+ * module.exports = {
20
+ * foo: require ('foo') // will be lazy-loaded
21
+ * }
22
+ * @returns {(id:string)=>{}} a funtion to use instead of require
23
+ */
24
+ const lazify_module = (module) => {
25
+ // monkey-patch module.exports setter to lazify all
26
+ Object.defineProperty (module, 'exports',{ set(all) {
27
+ Object.defineProperty (module, 'exports',{ value:lazify(all) })
28
+ }})
29
+ // return a function to use instead of require
30
+ return (id) => (lazy) => module.require(id) // eslint-disable-line no-unused-vars
31
+ }
32
+
33
+ const is_lazy = (x) => typeof x === 'function' && /^(function\s?)?\(?lazy[,)\t =]/.test(x)
package/lib/utils/tar.js CHANGED
@@ -51,6 +51,44 @@ const createTemp = async (root, resources) => {
51
51
  return temp
52
52
  }
53
53
 
54
+ const tarInfo = async (info) => {
55
+ let cmd, param
56
+ if (info === 'version') {
57
+ cmd = 'tar'
58
+ param = ['--version']
59
+ } else {
60
+ cmd = process.platform === 'win32' ? 'where' : 'which'
61
+ param = ['tar']
62
+ }
63
+
64
+ const c = spawn (cmd, param)
65
+
66
+ return {__proto__:c,
67
+ then (resolve, reject) {
68
+ let data=[], stderr=''
69
+ c.stdout.on('data', d => {
70
+ data.push(d)
71
+ })
72
+ c.stderr.on('data', d => stderr += d)
73
+ c.on('close', code => {
74
+ code ? reject(new Error(stderr)) : resolve(Buffer.concat(data).toString().replace(/\n/g,'').replace(/\r/g,''))
75
+ })
76
+ c.on('error', reject)
77
+ }
78
+ }
79
+ }
80
+
81
+ const logDebugTar = async () => {
82
+ const LOG = cds.log('tar')
83
+ if (!LOG?._debug) return
84
+ try {
85
+ LOG (`tar version: ${await tarInfo('version')}`)
86
+ LOG (`tar path: ${await tarInfo('path')}`)
87
+ } catch (err) {
88
+ LOG('tar error', err)
89
+ }
90
+ }
91
+
54
92
  /**
55
93
  * Creates a tar archive, to an in-memory Buffer, or piped to write stream or file.
56
94
  * @example ```js
@@ -76,7 +114,7 @@ const createTemp = async (root, resources) => {
76
114
  * - `.to()` is a convenient shortcut to pipe the output into a write stream
77
115
  */
78
116
  exports.create = async (dir='.', ...args) => {
79
-
117
+ logDebugTar()
80
118
  if (typeof dir === 'string') dir = _resolve(dir)
81
119
  if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
82
120
 
@@ -42,7 +42,6 @@ if (cds.requires.extensibility || cds.requires.toggles) {
42
42
  // prettier-ignore
43
43
  return function ODataAdapter(req, res) {
44
44
  const model = cds.context?.model || srv.model
45
- if (!model._cached) Object.defineProperty (model, '_cached', { value: { touched: Date.now() } })
46
45
  const adapters = model._cached._odata_adapters ??= {}
47
46
  const okra = adapters[id] ??= new adapter4 ({ __proto__: srv, _real_srv: srv, model })
48
47
  return okra (req, res)