@sap/cds 6.1.2 → 6.2.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 (212) hide show
  1. package/CHANGELOG.md +92 -8
  2. package/apis/cds.d.ts +18 -6
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/cqn.d.ts +1 -1
  5. package/apis/log.d.ts +23 -5
  6. package/apis/ql.d.ts +128 -61
  7. package/apis/services.d.ts +11 -0
  8. package/apis/test.d.ts +61 -0
  9. package/apis/utils.d.ts +15 -0
  10. package/app/fiori/preview.js +1 -0
  11. package/bin/build/buildTaskEngine.js +70 -22
  12. package/bin/build/buildTaskFactory.js +18 -11
  13. package/bin/build/buildTaskHandler.js +1 -1
  14. package/bin/build/buildTaskProviderFactory.js +3 -13
  15. package/bin/build/constants.js +0 -1
  16. package/bin/build/index.js +14 -6
  17. package/bin/build/provider/buildTaskHandlerEdmx.js +2 -3
  18. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -2
  19. package/bin/build/provider/buildTaskHandlerInternal.js +3 -6
  20. package/bin/build/provider/buildTaskProviderInternal.js +51 -39
  21. package/bin/build/provider/fiori/index.js +3 -3
  22. package/bin/build/provider/hana/2migration.js +1 -1
  23. package/bin/build/provider/hana/index.js +34 -27
  24. package/bin/build/provider/java/index.js +6 -7
  25. package/bin/build/provider/mtx/index.js +20 -18
  26. package/bin/build/provider/mtx/resourcesTarBuilder.js +8 -11
  27. package/bin/build/provider/mtx-sidecar/index.js +13 -17
  28. package/bin/build/provider/nodejs/index.js +8 -7
  29. package/bin/build/util.js +22 -4
  30. package/bin/cds.js +8 -4
  31. package/bin/deploy/to-hana/cfUtil.js +53 -18
  32. package/bin/mtx/in-cds.js +1 -0
  33. package/bin/serve.js +37 -30
  34. package/lib/auth/basic-auth.js +33 -0
  35. package/lib/auth/dummy-auth.js +7 -0
  36. package/lib/auth/ias-auth.js +2 -0
  37. package/lib/auth/index.js +31 -0
  38. package/lib/auth/jwt-auth.js +3 -0
  39. package/lib/auth/mocked-users.js +72 -0
  40. package/lib/auth/passport-basic.js +12 -0
  41. package/lib/auth/passport-digest.js +14 -0
  42. package/lib/auth/xsuaa-auth.js +3 -0
  43. package/lib/compile/cds-compile.js +3 -3
  44. package/lib/compile/to/cdl.js +5 -1
  45. package/lib/compile/to/edm.js +8 -0
  46. package/lib/compile/to/gql.js +1 -0
  47. package/lib/compile/to/json.js +30 -5
  48. package/lib/compile/to/sql.js +3 -1
  49. package/lib/core/index.js +5 -1
  50. package/lib/dbs/cds-deploy.js +36 -6
  51. package/lib/env/cds-env.js +15 -5
  52. package/lib/env/cds-requires.js +51 -58
  53. package/lib/env/defaults.js +1 -0
  54. package/lib/env/schemas/cds-package.json +4 -0
  55. package/lib/env/schemas/cds-rc.json +63 -77
  56. package/lib/i18n/localize.js +16 -5
  57. package/lib/index.js +9 -4
  58. package/lib/log/cds-error.js +4 -6
  59. package/lib/log/cds-log.js +89 -53
  60. package/lib/log/service/index.js +1 -0
  61. package/lib/ql/CREATE.js +2 -5
  62. package/lib/ql/DELETE.js +1 -1
  63. package/lib/ql/DROP.js +1 -3
  64. package/lib/ql/INSERT.js +3 -3
  65. package/lib/ql/Query.js +10 -23
  66. package/lib/ql/SELECT.js +1 -2
  67. package/lib/ql/UPDATE.js +2 -2
  68. package/lib/ql/Whereable.js +7 -15
  69. package/lib/ql/cds-ql.js +9 -3
  70. package/lib/req/cds-context.js +11 -3
  71. package/lib/req/context.js +29 -23
  72. package/lib/req/locale.js +9 -5
  73. package/lib/req/request.js +1 -0
  74. package/lib/req/user.js +2 -1
  75. package/lib/srv/cds-connect.js +1 -1
  76. package/lib/srv/cds-serve.js +21 -14
  77. package/lib/srv/middlewares/cds-context.js +29 -0
  78. package/lib/srv/middlewares/ctx-model.js +24 -0
  79. package/lib/srv/middlewares/errors.js +9 -0
  80. package/lib/srv/middlewares/index.js +22 -0
  81. package/lib/srv/middlewares/sap-statistics.js +13 -0
  82. package/lib/srv/middlewares/trace.js +102 -0
  83. package/lib/srv/protocols/_legacy.js +42 -0
  84. package/lib/srv/protocols/graphql.js +39 -0
  85. package/lib/srv/protocols/hcql.js +37 -0
  86. package/lib/srv/protocols/index.js +86 -0
  87. package/lib/srv/protocols/odata-v2-proxy.js +3767 -0
  88. package/lib/srv/protocols/odata-v2.js +26 -0
  89. package/lib/srv/protocols/odata-v4.js +16 -0
  90. package/lib/srv/protocols/rest.js +13 -0
  91. package/lib/srv/srv-api.js +5 -0
  92. package/lib/srv/srv-models.js +4 -6
  93. package/lib/utils/axios.js +3 -2
  94. package/lib/utils/cds-test.js +27 -21
  95. package/lib/utils/cds-utils.js +19 -20
  96. package/lib/utils/tar.js +175 -0
  97. package/libx/_runtime/audit/generic/personal/utils.js +18 -7
  98. package/libx/_runtime/audit/utils/v2.js +1 -0
  99. package/libx/_runtime/auth/index.js +4 -0
  100. package/libx/_runtime/auth/strategies/ias-auth.js +76 -0
  101. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +8 -3
  102. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +15 -4
  103. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  106. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +9 -0
  107. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriInfo.js +5 -1
  108. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +12 -0
  109. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +6 -2
  110. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/RequestValidator.js +47 -7
  111. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  112. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -2
  113. package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -1
  114. package/libx/_runtime/cds-services/util/assert.js +7 -0
  115. package/libx/_runtime/common/aspects/relation.js +1 -1
  116. package/libx/_runtime/common/composition/data.js +61 -15
  117. package/libx/_runtime/common/composition/delete.js +0 -1
  118. package/libx/_runtime/common/composition/insert.js +0 -1
  119. package/libx/_runtime/common/composition/tree.js +4 -10
  120. package/libx/_runtime/common/composition/update.js +44 -21
  121. package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
  122. package/libx/_runtime/common/generic/crud.js +1 -2
  123. package/libx/_runtime/common/generic/etag.js +4 -4
  124. package/libx/_runtime/common/generic/input.js +21 -6
  125. package/libx/_runtime/common/generic/paging.js +3 -3
  126. package/libx/_runtime/common/generic/put.js +7 -4
  127. package/libx/_runtime/common/generic/sorting.js +4 -4
  128. package/libx/_runtime/common/generic/temporal.js +3 -6
  129. package/libx/_runtime/common/i18n/messages.properties +0 -7
  130. package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
  131. package/libx/_runtime/common/utils/csn.js +0 -28
  132. package/libx/_runtime/common/utils/draft.js +8 -1
  133. package/libx/_runtime/common/utils/path.js +7 -1
  134. package/libx/_runtime/common/utils/propagateForeignKeys.js +122 -0
  135. package/libx/_runtime/common/utils/resolveView.js +2 -3
  136. package/libx/_runtime/common/utils/template.js +2 -3
  137. package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
  138. package/libx/_runtime/db/generic/input.js +6 -6
  139. package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
  140. package/libx/_runtime/fiori/generic/activate.js +2 -2
  141. package/libx/_runtime/fiori/generic/before.js +40 -72
  142. package/libx/_runtime/fiori/generic/cancel.js +2 -2
  143. package/libx/_runtime/fiori/generic/delete.js +2 -2
  144. package/libx/_runtime/fiori/generic/edit.js +2 -2
  145. package/libx/_runtime/fiori/generic/new.js +3 -5
  146. package/libx/_runtime/fiori/generic/patch.js +49 -43
  147. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  148. package/libx/_runtime/fiori/generic/read.js +27 -37
  149. package/libx/_runtime/fiori/utils/where.js +4 -2
  150. package/libx/_runtime/hana/Service.js +1 -3
  151. package/libx/_runtime/hana/conversion.js +3 -0
  152. package/libx/_runtime/hana/driver.js +33 -3
  153. package/libx/_runtime/hana/dynatrace.js +1 -0
  154. package/libx/_runtime/hana/search2Contains.js +12 -1
  155. package/libx/_runtime/hana/search2cqn4sql.js +10 -27
  156. package/libx/_runtime/hana/streaming.js +1 -0
  157. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  158. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
  159. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
  160. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
  161. package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
  162. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  163. package/libx/_runtime/messaging/redis-messaging.js +1 -0
  164. package/libx/_runtime/remote/Service.js +2 -2
  165. package/libx/_runtime/remote/utils/client.js +35 -11
  166. package/libx/_runtime/remote/utils/data.js +7 -2
  167. package/libx/_runtime/sqlite/Service.js +18 -7
  168. package/libx/_runtime/sqlite/conversion.js +3 -0
  169. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
  170. package/libx/_runtime/sqlite/localized.js +8 -8
  171. package/libx/odata/afterburner.js +39 -7
  172. package/libx/odata/cqn2odata.js +6 -3
  173. package/libx/odata/grammar.pegjs +66 -18
  174. package/libx/odata/index.js +3 -2
  175. package/libx/odata/parser.js +1 -1
  176. package/libx/odata/utils.js +2 -0
  177. package/libx/rest/RestAdapter.js +62 -43
  178. package/libx/rest/middleware/input.js +2 -3
  179. package/libx/rest/middleware/parse.js +2 -1
  180. package/libx/rest/middleware/update.js +1 -1
  181. package/package.json +2 -2
  182. package/server.js +5 -4
  183. package/srv/mtx.cds +1 -1
  184. package/srv/mtx.js +4 -24
  185. package/lib/srv/adapters.js +0 -85
  186. package/lib/utils/resources/index.js +0 -48
  187. package/lib/utils/resources/tar.js +0 -49
  188. package/lib/utils/resources/utils.js +0 -11
  189. package/libx/_runtime/db/utils/propagateForeignKeys.js +0 -93
  190. package/libx/_runtime/extensibility/activate.js +0 -69
  191. package/libx/_runtime/extensibility/add.js +0 -50
  192. package/libx/_runtime/extensibility/addExtension.js +0 -72
  193. package/libx/_runtime/extensibility/defaults.js +0 -34
  194. package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
  195. package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
  196. package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
  197. package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
  198. package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
  199. package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
  200. package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
  201. package/libx/_runtime/extensibility/linter.js +0 -32
  202. package/libx/_runtime/extensibility/push.js +0 -118
  203. package/libx/_runtime/extensibility/service.js +0 -38
  204. package/libx/_runtime/extensibility/token.js +0 -57
  205. package/libx/_runtime/extensibility/utils.js +0 -131
  206. package/libx/_runtime/extensibility/validation.js +0 -50
  207. package/libx/_runtime/extensibility/views.js +0 -12
  208. package/srv/extensibility-service.cds +0 -59
  209. package/srv/extensibility-service.js +0 -1
  210. package/srv/extensions.cds +0 -8
  211. package/srv/model-provider.cds +0 -61
  212. package/srv/model-provider.js +0 -143
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,16 @@
1
+ const cds = require('../../index'), { User } = cds, { decodeURIComponent } = cds.utils
2
+ const libx = require('../../../libx/_runtime')
3
+ const LOG = cds.log('odata')
4
+
5
+ module.exports = function ODataAdapter (srv) { return [
6
+ (req, _, next) => {
7
+ let u = req.user
8
+ req.user = u instanceof User ? u : new User(u)
9
+ req.on ('dispatch', (eve) => {
10
+ LOG && LOG (req.method, req.baseUrl + decodeURIComponent(eve._path), eve._query||'')
11
+ if (LOG._debug && eve.query) LOG.debug (eve.query)
12
+ })
13
+ next()
14
+ },
15
+ libx.to.odata_v4 (srv)
16
+ ]}
@@ -0,0 +1,13 @@
1
+ const cds = require('../../index'), { User } = cds, { decodeURIComponent } = cds.utils
2
+ const libx = require('../../../libx/_runtime')
3
+ const LOG = cds.log('rest')
4
+
5
+ module.exports = function RestAdapter (srv) { return [
6
+ (req, _, next) => {
7
+ let u = req.user
8
+ req.user = u instanceof User ? u : new User(u)
9
+ LOG (req.method, decodeURIComponent(req.originalUrl), req.body||'')
10
+ next()
11
+ },
12
+ libx.to.rest (srv)
13
+ ]}
@@ -15,6 +15,11 @@ class Service extends require('./srv-handlers') {
15
15
  if (model) this.model = model
16
16
  }
17
17
 
18
+ /**
19
+ * Subclasses commonly override this to register event handlers
20
+ */
21
+ init() {}
22
+
18
23
  /**
19
24
  * Subclasses may override this to prepare the given model appropriately
20
25
  */
@@ -53,7 +53,7 @@ class ExtendedModels {
53
53
 
54
54
  const {cache} = ExtendedModels, key = cache.key4 (tenant, features)
55
55
  const cached = cache.at(key); if (cached) return cached
56
- if (key === ':') return cache.add (':', cds.compile.for.nodejs(cds.model))
56
+ else if (key === ':') return cache.add (':', cds.compile.for.nodejs(cds.model))
57
57
  else return cache[key] = (async()=>{ // temporarily add promise to cache to avoid race conditions...
58
58
 
59
59
  // If tenant doesn't have extensions check cache with tenant = undefined
@@ -99,7 +99,6 @@ class ExtendedModels {
99
99
  if (model.then) return model //> promised model to avoid race conditions
100
100
 
101
101
  const {_cached} = model, interval = ExtendedModels.checkInterval
102
- if (!_cached.touched) return model
103
102
  if (Date.now() - _cached.touched < interval) return model //> checked recently
104
103
 
105
104
  else return this[key] = (async()=>{ // temporarily replace cache entry by promise to avoid race conditions...
@@ -132,7 +131,7 @@ class ExtendedModels {
132
131
  */
133
132
  add (key, model, touched = Date.now()) {
134
133
  if (model) {
135
- if (!model._cached) Object.defineProperty (model,'_cached',{ value:{key,touched} })
134
+ if (!model._cached) Object.defineProperty (model,'_cached',{ value: { touched } })
136
135
  return this[key] = model
137
136
  }
138
137
  }
@@ -192,8 +191,7 @@ if (!extensibility) {
192
191
  // helper to get model for tenant/features
193
192
  const _is_extended = old_mtx ? t => old_mtx.isExtended(t) : extensibility ? ()=> cds.db.exists('cds.xt.Extensions') : ()=> false
194
193
  const _get_model4 = old_mtx ? async (tenant) => {
195
- const isExtended = tenant && await old_mtx.isExtended(tenant) // REVISIT: avoid await
196
- if (isExtended) return old_mtx.getCsn(tenant) .then (cds.compile.for.nodejs)
194
+ return old_mtx.getCsn(tenant) .then (cds.compile.for.nodejs)
197
195
  } : (tenant, toggles) => {
198
196
  const { 'cds.xt.ModelProviderService':mps } = cds.services
199
197
  return mps.getCsn (tenant, toggles) .then (cds.compile.for.nodejs)
@@ -203,5 +201,5 @@ const _get_model4 = old_mtx ? async (tenant) => {
203
201
  // ---------------------------------------------------------------------------
204
202
  // Optimizations for single-tenancy modes
205
203
 
206
- if (cds.requires.multitenancy && typeof global.it === 'undefined') ExtendedModels.cache.startSentinel()
204
+ if (cds.requires.multitenancy && typeof global.it === 'undefined') cds.once ('listening', ()=> ExtendedModels.cache.startSentinel())
207
205
  // REVISIT: how to do ^that^ correctly with jest?
@@ -1,7 +1,8 @@
1
1
  class Axios {
2
2
 
3
3
  get axios() {
4
- return super.axios = require('axios').default.create ({
4
+ // eslint-disable-next-line cds/no-missing-dependencies
5
+ return super.axios = require('axios').create ({
5
6
  headers: { 'Content-Type': 'application/json' },
6
7
  baseURL: this.url,
7
8
  })
@@ -37,7 +38,7 @@ const _args = (args) => {
37
38
 
38
39
  const _error = (e) => {
39
40
  Error.captureStackTrace (e,_error) //> adds the stack trace from caller code
40
- if (e.code === 'ECONNREFUSED' && e.port === 80 /* default port */) throw Object.assign (e, {
41
+ if (e.code && e.port === 80 /* default port */) throw Object.assign (e, {
41
42
  message: e.message + '\nIt seems that the server was not started. Make sure to call \'cds.test(...)\' or \'cds.test.run(...)\'.',
42
43
  stack: null // stack is just clutter here
43
44
  })
@@ -1,4 +1,4 @@
1
- const { is_mocha } = support_jest_and_mocha()
1
+ const { is_mocha, is_jest } = support_jest_and_mocha()
2
2
 
3
3
  class Test extends require('./axios') {
4
4
 
@@ -64,9 +64,8 @@ class Test extends require('./axios') {
64
64
  /**
65
65
  * Switch on/off console log output.
66
66
  */
67
- verbose(v) {
68
- v === false ? delete process.env.CDS_TEST_VERBOSE : process.env.CDS_TEST_VERBOSE=v
69
- initLogging()
67
+ verbose (v) {
68
+ initLogging({ is_mocha, is_jest, verbose: v })
70
69
  return this
71
70
  }
72
71
 
@@ -79,6 +78,10 @@ class Test extends require('./axios') {
79
78
  get cds() { return require('../index') }
80
79
  get spy() { return spy }
81
80
 
81
+ then(r) {
82
+ const {cds} = this
83
+ cds.once('listening',r)
84
+ }
82
85
  }
83
86
 
84
87
  function support_jest_and_mocha() {
@@ -110,9 +113,8 @@ function support_jest_and_mocha() {
110
113
  const repl = global.cds.repl
111
114
  repl && repl.on('exit',fn)
112
115
  }
113
- process.env.CDS_TEST_VERBOSE = true
114
116
  }
115
- initLogging()
117
+ initLogging ({ is_jest, is_mocha })
116
118
  return { is_jest, is_mocha }
117
119
  }
118
120
 
@@ -122,24 +124,28 @@ function load_chai() {
122
124
  Failed to load required package '${mod}'. Please add it thru:
123
125
  npm add -D chai chai-as-promised chai-subset
124
126
  `)}}
125
- const chai = require('chai')
126
- chai.use (require('chai-subset'))
127
- chai.use (require('chai-as-promised'))
127
+ const chai = require('chai') // eslint-disable-line cds/no-missing-dependencies
128
+ chai.use (require('chai-subset')) // eslint-disable-line cds/no-missing-dependencies
129
+ chai.use (require('chai-as-promised')) // eslint-disable-line cds/no-missing-dependencies
128
130
  return chai
129
131
  }
130
132
 
131
- function initLogging() {
132
- const levels = process.env.CDS_TEST_VERBOSE
133
- ? { deploy:'info', serve:'info', server:'info',cds:'info' }
134
- : { deploy:'warn', serve:'warn', server:'warn',cds:'silent' /* silences provoked request errors */ }
135
-
136
- const cds = require('../index')
137
- const env = Reflect.getOwnPropertyDescriptor(cds,'env')
138
- for (const id of Object.keys(levels)) {
139
- if (env && env.value)
140
- cds.log(id, { level:levels[id] })
141
- else // uninitialized cds.env -> set env variables to avoid initializing cds.env eagerly
142
- process.env['cds_log_levels_'+id] = levels[id]
133
+ function initLogging ({ verbose }={}) {
134
+ if (verbose && global.console.logs) return global.console = global.console.__proto__
135
+ if (process.env.CDS_TEST_SILENT) {
136
+ const console = global.console, logs = []
137
+ const {format} = require('util')
138
+ global.console = { __proto__: console, logs,
139
+ time: ()=>{}, timeEnd: (...args)=> logs.push(args),
140
+ debug: (...args)=> logs.push(args),
141
+ info: (...args)=> logs.push(args),
142
+ log: (...args)=> logs.push(args),
143
+ warn: (...args)=> logs.push(args),
144
+ trace: (...args)=> logs.push(args),
145
+ error: (...args)=> logs.push(args),
146
+ dump(){ for (let each of logs) process.stdout.write (format(...each)+'\n') },
147
+ }
148
+ afterAll (()=> global.console = console)
143
149
  }
144
150
  }
145
151
 
@@ -1,15 +1,17 @@
1
1
  const cwd = process.env._original_cwd || process.cwd()
2
- const path = require ('path'), { dirname, extname, join, resolve, relative } = path
3
- const fs = require('fs')
4
2
  const cds = require('../index')
5
3
 
6
- const all = module.exports = exports = { ...fs,
7
- get inspect() { return $set (this, 'inspect', require('util').inspect) },
8
- get uuid() { return $set (this, 'uuid', require('@sap/cds-foss').uuid) },
4
+ const ux = module.exports = exports = new class {
5
+ get inspect() { return super.inspect = require('util').inspect }
6
+ get uuid() { return super.uuid = require('@sap/cds-foss').uuid }
7
+ get tar() { return super.tar = require('./tar') }
9
8
  }
10
9
 
11
- exports.fs = all
12
- exports.path = path
10
+ const path = exports.path = require('path'), { dirname, extname, join, resolve, relative } = path
11
+ const fs = exports.fs = Object.assign (ux,require('fs')) //> for compatibility
12
+
13
+ exports.decodeURIComponent = s => { try { return decodeURIComponent(s) } catch { return s } }
14
+ exports.decodeURI = s => { try { return decodeURI(s) } catch { return s } }
13
15
 
14
16
  exports.local = (file) => relative(cwd,file)
15
17
 
@@ -59,7 +61,7 @@ exports.write = function write (file, data, o) {
59
61
  if (typeof data === 'object' && !Buffer.isBuffer(data))
60
62
  data = JSON.stringify(data, null, ' '.repeat(o && o.spaces))
61
63
  const f = resolve (cds.root,file)
62
- return mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
64
+ return fs.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
63
65
  }
64
66
 
65
67
  exports.mkdirp = async function (...path) {
@@ -87,8 +89,8 @@ exports.copy = async function copy (x,y) {
87
89
  const src = resolve (cds.root,x)
88
90
  const dst = resolve (cds.root,y)
89
91
  if (fs.promises.cp) return fs.promises.cp (src,dst,{recursive:true})
90
- await mkdirp (dirname(dst))
91
- if (isdir(src)) {
92
+ await fs.mkdirp (dirname(dst))
93
+ if (fs.isdir(src)) {
92
94
  const entries = await fs.promises.readdir(src)
93
95
  return Promise.all (entries.map (async each => {
94
96
  const e = join (src,each)
@@ -133,14 +135,11 @@ exports.find = function find (base, patterns='*', filter=()=>true) {
133
135
 
134
136
 
135
137
  // internal utility to load a file through ESM or CommonJs. TODO find a better place.
136
- const { pathToFileURL } = require('url')
137
- exports._import = async function(filePath) {
138
- return typeof jest !== 'undefined' || extname(filePath) === '.ts' // ts-node w/ ESM not working (cap/issues#11980)
139
- ? require(filePath)
140
- : import (pathToFileURL(filePath).href) // must use a file: URL, esp. on Windows for C:\... paths
138
+ exports._import = id => require(id)
139
+ if (!global.test && !global.it) {
140
+ const { pathToFileURL } = require('url')
141
+ exports._import = id => {
142
+ if (extname(id) === '.ts') return require(id) // ts-node w/ ESM not working (cap/issues#11980)
143
+ else return import (pathToFileURL(id).href) // must use a file: URL, esp. on Windows for C:\... paths
144
+ }
141
145
  }
142
-
143
-
144
- /** @type <T>(o,p,v:T) => T */
145
- const $set = (o,p,v) => { Object.defineProperty(o,p,{value:v}); return v }
146
- const { mkdirp, isdir } = exports
@@ -0,0 +1,175 @@
1
+ const child_process = require('child_process')
2
+ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
3
+ Error.captureStackTrace(spawn,spawn)
4
+ process.stderr.write(cmd +' ', args.join(' ') +' '+ spawn.stack.slice(7) + '\n')
5
+ return child_process.spawn(cmd, args, options)
6
+ } : child_process.spawn
7
+
8
+ const cds = require('../index'), { fs, path, mkdirp } = cds.utils
9
+ const _resolve = (...x) => path.resolve (cds.root,...x)
10
+
11
+ // tar does not work properly on Windows (by npm/jest tests) w/o this change
12
+ const win = path => {
13
+ if (!path) return path
14
+ if (typeof path === 'string') return path.replace('C:', '//localhost/c$')
15
+ if (Array.isArray(path)) return path.map(el => win(el))
16
+ }
17
+
18
+ /**
19
+ * Creates a tar archive, to an in-memory Buffer, or piped to write stream or file.
20
+ * @example ```js
21
+ * const buffer = await tar.c('src/dir')
22
+ * await tar.c('src/dir') .to (fs.createWriteStream('t.tar'))
23
+ * await tar.c('src/dir') .to ('t.tar')
24
+ * await tar.c('src/dir','-f t.tar *')
25
+ * ```
26
+ * @param {string} dir - the directory to archive, used as `cwd` for the tar process
27
+ * @param {string} [args] - additional arguments passed to tar (default: `'*'`)
28
+ * @param {string[]} [more] - more of such additional arguments like `args`
29
+ * @example ```js
30
+ * // Passing additional arguments to tar
31
+ * tar.c('src/dir','-v *')
32
+ * tar.c('src/dir','-v -f t.tar *')
33
+ * tar.c('src/dir','-vf','t.tar','*')
34
+ * tar.c('src/dir','-vf','t.tar','file1','file2')
35
+ * ```
36
+ * @returns A `ChildProcess` as returned by [`child_process.spawn()`](
37
+ * https://nodejs.org/api/child_process.html#child_processspawncommand-args-options),
38
+ * augmented by two methods:
39
+ * - `.then()` collects the tar output into an in-memory `Buffer`
40
+ * - `.to()` is a convenient shortcut to pipe the output into a write stream
41
+ */
42
+ exports.create = (dir='.', ...args) => {
43
+
44
+ if (typeof dir === 'string') dir = _resolve(dir)
45
+ if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
46
+ if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
47
+ else args.push('.')
48
+ const c = process.platform === 'linux'
49
+ ? spawn (`tar c -C ${dir}`, args, { shell:true })
50
+ : spawn (`tar cf - -C ${win(dir)}`, args, { shell:true })
51
+
52
+ return {__proto__:c, // returning a thenable + fluent ChildProcess...
53
+
54
+ /**
55
+ * Turns the returned `ChildProcess` into a thenable, resolving to an
56
+ * in-memory Buffer holding the tar output, hence enabling this usage:
57
+ * @example const buffer = await tar.c('src/dir')
58
+ */
59
+ then (r,e) {
60
+ const bb=[]; c.stdout.on('data', b => bb.push(b))
61
+ c.on('close', ()=>r(Buffer.concat(bb)))
62
+ c.on('error', e)
63
+ },
64
+
65
+ /**
66
+ * Turns the returned `ChildProcess` into fluent API, allowing to pipe
67
+ * the tar's `stdout` into a write stream. If the argument is a string,
68
+ * it will be interpreted as a filename and a write stream opened on it.
69
+ * In that case, more filenames can be specified which are path.joined.
70
+ */
71
+ to (out, ...etc) {
72
+ if (typeof out === 'string') {
73
+ // fs.mkdirSync(path.dirname(out),{recursive:true})
74
+ out = fs.createWriteStream (_resolve(out,...etc))
75
+ }
76
+ // Returning a thenable ChildProcess.stdout
77
+ return {__proto__: c.stdout.pipe (out),
78
+ then(r,e) {
79
+ out.on('close',r)
80
+ c.on('error',e)
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Extracts a tar archive, from an in-memory Buffer, or piped from a read stream or file.
89
+ * @example ```js
90
+ * await tar.x(buffer) .to ('dest')
91
+ * await tar.x(fs.createReadStream('t.tar')) .to ('dest')
92
+ * await tar.x('t.tar') .to ('dest')
93
+ * await tar.x('t.tar','-C dest')
94
+ * ```
95
+ * @param {String|Buffer|ReadableStream} [archive] - the tar file or content to extract
96
+ * @param {String[]} [args] - additional arguments passed to tar, .e.g. '-C dest'
97
+ */
98
+ exports.extract = (archive, ...args) => ({
99
+
100
+ /**
101
+ * Fluent API method to actually start the tar x command.
102
+ * @param {...string} dest - path names to a target dir → get `path.resolved` from `cds.root`.
103
+ * @returns A `ChildProcess` as returned by [`child_process.spawn()`](
104
+ * https://nodejs.org/api/child_process.html#child_processspawncommand-args-options),
105
+ * augmented by a method `.then()` to allow `await`ing finish.
106
+ */
107
+ to (...dest) {
108
+ if (typeof dest === 'string') dest = _resolve(...dest)
109
+ const input = typeof archive !== 'string' || archive == '-' ? '-' : _resolve(archive)
110
+ const x = spawn(`tar xf ${win(input)} -C ${win(dest)}`, args, { shell:true })
111
+ if (archive === '-') return x.stdin
112
+ if (Buffer.isBuffer(archive)) archive = require('stream').Readable.from (archive)
113
+ if (typeof archive !== 'string') archive.pipe (x.stdin)
114
+ if (args.includes('-v')) {
115
+ var list = '';
116
+ ;(process.platform === 'linux' ? x.stdout : x.stderr) .on ('data', d => list+=d)
117
+ }
118
+ return {__proto__:x,
119
+ then(r,e) {
120
+ x.on('close',()=>r(list ? list.split('\n').slice(0,-1).map(x => x.replace(/^x |\r/g,'')) : undefined))
121
+ x.on('error',e)
122
+ }
123
+ }
124
+ },
125
+
126
+ /**
127
+ * Shortcut to extract to current working directory, i.e. `cds.root`,
128
+ * or for this kind of usage:
129
+ * @example await tar.x(...,'-C _out')
130
+ * @returns `stdin` of the tar child process
131
+ */
132
+ then (r,e) { return this.to('.') .then(r,e) },
133
+
134
+ })
135
+
136
+
137
+ exports.list = (archive, ...more) => {
138
+ const input = typeof archive !== 'string' ? '-' : archive === '-' ? archive : _resolve(archive)
139
+ const x = spawn(`tar tf`, [ input, ...more ], { shell:true })
140
+ let list = ''; x.stdout.on ('data', d => list+=d)
141
+ return {__proto__:x,
142
+ then(r,e) {
143
+ x.on('close',()=>r(list.split('\n').slice(0,-1)))
144
+ x.on('error',e)
145
+ }
146
+ }
147
+ }
148
+
149
+ // Common tar command shortcuts
150
+ const tar = exports
151
+ exports.c = tar.create
152
+ exports.cz = (d,...args) => tar.c (d, ...args, '-z')
153
+ exports.cf = (t,d,...args) => tar.c (d, ...args, '-f',t)
154
+ exports.czf = (t,d,...args) => tar.c (d, ...args, '-z', '-f',t)
155
+ exports.czfd = (t,...args) => mkdirp(path.dirname(t)).then (()=> tar.czf (t,...args))
156
+ exports.x = tar.xf = tar.extract
157
+ exports.xz = tar.xzf = (a,...args) => tar.x (a, ...args, '-z')
158
+ exports.xv = tar.xvf = (a,...args) => tar.x (a, ...args, '-v')
159
+ exports.xvz = tar.xvzf = (a,...args) => tar.x (a, ...args, '-v', '-z')
160
+ exports.t = tar.tf = tar.list
161
+
162
+ /**
163
+ * Shortcut for that kind of usage:
164
+ * @example fs.createReadStream('t.tar') .pipe (tar.x.to('dest/dir'))
165
+ * @returns `stdin` of the tar child process
166
+ */
167
+ exports.extract.to = function (..._) { return this().to(..._) }
168
+
169
+
170
+
171
+ // ---------------------------------------------------------------------------------
172
+ // Compatibility...
173
+
174
+ exports.packTarArchive = (files,d) => d ? tar.cz (d,files) : tar.cz (files)
175
+ exports.unpackTarArchive = (x,dir) => tar.xz(x).to(dir)
@@ -16,15 +16,26 @@ const getRootEntity = element => {
16
16
  return entity
17
17
  }
18
18
 
19
+ const _hasPersonalData = e => {
20
+ if (!e['@PersonalData.DataSubjectRole']) return
21
+ if (!e['@PersonalData.EntitySemantics']) return
22
+ return !!Object.values(e.elements).some(
23
+ e => e['@PersonalData.IsPotentiallyPersonal'] || e['@PersonalData.IsPotentiallySensitive']
24
+ )
25
+ }
26
+
27
+ const auditAnnotations = {
28
+ CREATE: '@AuditLog.Operation.Insert',
29
+ UPDATE: '@AuditLog.Operation.Update',
30
+ DELETE: '@AuditLog.Operation.Delete',
31
+ READ: '@AuditLog.Operation.Read'
32
+ }
33
+
19
34
  const getPick = event => {
20
35
  return (element, target) => {
21
- const annotation = {
22
- CREATE: '@AuditLog.Operation.Insert',
23
- UPDATE: '@AuditLog.Operation.Update',
24
- DELETE: '@AuditLog.Operation.Delete',
25
- READ: '@AuditLog.Operation.Read'
26
- }[event]
27
- if (!annotation || !target[annotation]) return
36
+ if (!_hasPersonalData(target)) return
37
+ if (!auditAnnotations[event] || !target[auditAnnotations[event]]) return
38
+
28
39
  const categories = []
29
40
  if (!element.isAssociation && element.key) categories.push('ObjectID')
30
41
  if (
@@ -7,6 +7,7 @@ function connect(credentials) {
7
7
  return new Promise((resolve, reject) => {
8
8
  let auditLogging
9
9
  try {
10
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
10
11
  auditLogging = require('@sap/audit-logging')
11
12
  } catch (e) {
12
13
  // not able to require lib -> no audit logging ootb
@@ -167,6 +167,10 @@ module.exports = (srv, options = srv.options) => {
167
167
  if (config.impl) {
168
168
  // mount custom authentication middleware
169
169
  _mountCustomAuth(srv, app, config)
170
+ } else if (config.kind === 'ias-auth') {
171
+ // ias-auth follows the new implementation pattern for auth middlewares
172
+ const iasAuth = require('./strategies/ias-auth')(config)
173
+ if (iasAuth) app.use(iasAuth)
170
174
  } else {
171
175
  // mount our authentication strategies (legacy style)
172
176
  const strategy = _strategy4(config)
@@ -0,0 +1,76 @@
1
+ const cds = require('../../../../lib')
2
+ const _require = require('../../common/utils/require')
3
+ // _require for better error message
4
+ const express = _require('express')
5
+ const passport = _require('passport')
6
+ const { JWTStrategy } = _require('@sap/xssec')
7
+ const LOG = cds.log('auth')
8
+
9
+ const RESERVED_ATTRIBUTES = new Set([
10
+ 'aud',
11
+ 'azp',
12
+ 'exp',
13
+ 'ext_attr',
14
+ 'iat',
15
+ 'ias_iss',
16
+ 'iss',
17
+ 'jti',
18
+ 'sub',
19
+ 'user_uuid',
20
+ 'zone_uuid',
21
+ 'zid'
22
+ ])
23
+
24
+ module.exports = function ias_auth(config) {
25
+ // warn if no credentials
26
+ if (!config.credentials) {
27
+ LOG._warn &&
28
+ LOG.warn(`
29
+ No IAS instance bound to application, but "${config.kind}" configured.
30
+ This is NOT recommended in production!
31
+ `)
32
+
33
+ return
34
+ }
35
+
36
+ passport.use('IAS', new JWTStrategy(config.credentials))
37
+ return express
38
+ .Router()
39
+ .use(passport.authenticate('IAS', { session: false, failWithError: true }))
40
+ .use((req, res, next) => {
41
+ // grant_type === client_credentials or x509
42
+ if (req.tokenInfo.getClientId() === req.tokenInfo.getSubject()) {
43
+ req.user = new cds.User({
44
+ id: 'system',
45
+ roles: ['system-user', 'authenticated-user'],
46
+ attr: {}
47
+ })
48
+ } else {
49
+ // add all unknown attributes to req.user.attr in order to keep public API small
50
+ const payload = req.tokenInfo.getPayload()
51
+ const attributes = Object.keys(payload)
52
+ .filter(k => !RESERVED_ATTRIBUTES.has(k))
53
+ .reduce((attrs, k) => {
54
+ attrs[k] = payload[k]
55
+ return attrs
56
+ }, {})
57
+
58
+ req.user = new cds.User({
59
+ id: req.user.id,
60
+ roles: ['authenticated-user'],
61
+ attr: attributes
62
+ })
63
+ }
64
+
65
+ req.tenant = req.tokenInfo.getZoneId()
66
+ next()
67
+ })
68
+ .use((err, req, res, next) => {
69
+ if (req.tokenInfo) {
70
+ LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
71
+ }
72
+ // REVISIT: reject request immediately as our other auth strategies do
73
+ // should we call next(err)? -> I don't think so; it's not an error, is it?
74
+ res.status(401).json({ code: '401', message: 'Unauthorized' }) // REVISIT: this is OData style?
75
+ })
76
+ }
@@ -115,14 +115,19 @@ const getErrorHandler = (crashOnError = true, srv) => {
115
115
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
116
116
  // REVISIT: if for compat reasons, remove once cds^5.1
117
117
  if (srv._handlers._error.length) {
118
+ // REVISIT: move to error middleware
118
119
  let ctx = cds.context
119
120
  if (!ctx) {
121
+ // FIXME: this implementation only worked because Okra's BufferedWriter.on('finish')
122
+ // lost cds.context -> as we fixed that we don't get into this if branch anymore,
123
+ // but then the ctx in the else branch below isn't the ODataRequest anymore
120
124
  // > error before req was dispatched
121
- ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
122
- for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
125
+ const creq = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
126
+ for (const each of srv._handlers._error) each.handler.call(srv, err, creq)
123
127
  } else {
124
128
  // > error after req was dispatched, e.g., serialization error in okra
125
- for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
129
+ const creq = /* odataReq.req || */ new cds.Request({ req, res: req.res, user: ctx.user, tenant: ctx.tenant })
130
+ for (const each of srv._handlers._error) each.handler.call(srv, err, creq)
126
131
  }
127
132
  }
128
133
 
@@ -19,16 +19,27 @@ module.exports = srv => {
19
19
  }
20
20
 
21
21
  // in case of $batch we need to challenge directly, as the header is not processed if in $batch response body
22
- if (path.endsWith('/$batch') && user && user._challenges && restricted) {
23
- res.set('WWW-Authenticate', user._challenges.join(';'))
24
- return next(UNAUTHORIZED)
22
+ if (restricted && path.endsWith('/$batch')) {
23
+ if (user?._challenges) {
24
+ res.set('WWW-Authenticate', user._challenges.join(';'))
25
+ return next(UNAUTHORIZED)
26
+ } else if (user._is_anonymous && req.login) {
27
+ res.set('WWW-Authenticate', `Basic realm="Users"`) // REVISIT: should be req.login(), and fiori app works with that but cds/tests/_runtime/auth/__tests__/strategies.test.js fails
28
+ return next(UNAUTHORIZED)
29
+ // return req.login()
30
+ }
25
31
  }
26
32
 
27
33
  // check @requires as soon as possible (DoS)
28
34
  if (requires && !requires.some(r => user.is(r))) {
29
35
  // > unauthorized or forbidden?
30
36
  if (user._is_anonymous) {
31
- if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
37
+ if (user._challenges) {
38
+ res.set('WWW-Authenticate', user._challenges.join(';'))
39
+ } else if (req.login) {
40
+ res.set('WWW-Authenticate', `Basic realm="Users"`) // REVISIT: should be req.login(), and fiori app works with that but cds/tests/_runtime/auth/__tests__/strategies.test.js fails
41
+ // return req.login()
42
+ }
32
43
  // REVISIT: security log in else case?
33
44
  return next(UNAUTHORIZED)
34
45
  }
@@ -28,7 +28,7 @@ const _getExpandItem = (isAll, expandItems, name) => {
28
28
  return expandItems.find(item => {
29
29
  const pathSegments = item.getPathSegments()
30
30
  if (pathSegments[pathSegments.length - 1].getKind() === 'COUNT') {
31
- throw getError(501, 'EXPAND_COUNT_UNSUPPORTED')
31
+ throw getError(501, '"/$count" is not supported for expand operation')
32
32
  }
33
33
 
34
34
  const navigation = pathSegments[pathSegments.length - 1].getNavigationProperty()