@sap/cds 6.1.0 → 6.1.3

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 (64) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/apis/log.d.ts +112 -36
  3. package/apis/services.d.ts +13 -2
  4. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -3
  5. package/bin/build/provider/hana/index.js +4 -2
  6. package/bin/build/provider/mtx/resourcesTarBuilder.js +4 -8
  7. package/bin/deploy/to-hana/hana.js +20 -25
  8. package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
  9. package/lib/dbs/cds-deploy.js +2 -2
  10. package/lib/env/schemas/cds-rc.json +10 -1
  11. package/lib/index.js +1 -1
  12. package/lib/log/format/kibana.js +19 -1
  13. package/lib/ql/Query.js +9 -3
  14. package/lib/ql/SELECT.js +1 -1
  15. package/lib/ql/UPDATE.js +2 -2
  16. package/lib/ql/cds-ql.js +4 -10
  17. package/lib/req/context.js +15 -11
  18. package/lib/srv/srv-api.js +8 -0
  19. package/lib/srv/srv-dispatch.js +11 -7
  20. package/lib/srv/srv-models.js +4 -3
  21. package/lib/srv/srv-tx.js +52 -40
  22. package/lib/utils/cds-utils.js +3 -3
  23. package/lib/utils/resources/index.js +5 -5
  24. package/lib/utils/resources/tar.js +1 -1
  25. package/libx/_runtime/auth/index.js +2 -2
  26. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
  27. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
  28. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
  29. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -2
  30. package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -1
  31. package/libx/_runtime/cds-services/util/assert.js +3 -0
  32. package/libx/_runtime/common/generic/input.js +17 -2
  33. package/libx/_runtime/common/generic/put.js +4 -1
  34. package/libx/_runtime/common/generic/temporal.js +0 -3
  35. package/libx/_runtime/common/utils/binary.js +3 -4
  36. package/libx/_runtime/common/utils/keys.js +14 -6
  37. package/libx/_runtime/common/utils/propagateForeignKeys.js +122 -0
  38. package/libx/_runtime/common/utils/resolveView.js +1 -1
  39. package/libx/_runtime/common/utils/template.js +2 -3
  40. package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
  41. package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
  42. package/libx/_runtime/db/generic/input.js +7 -4
  43. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  44. package/libx/_runtime/extensibility/add.js +3 -0
  45. package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
  46. package/libx/_runtime/extensibility/push.js +11 -11
  47. package/libx/_runtime/extensibility/token.js +2 -1
  48. package/libx/_runtime/extensibility/utils.js +8 -6
  49. package/libx/_runtime/fiori/generic/new.js +1 -3
  50. package/libx/_runtime/fiori/generic/patch.js +1 -7
  51. package/libx/_runtime/fiori/utils/where.js +1 -1
  52. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
  53. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +1 -2
  54. package/libx/_runtime/remote/utils/client.js +29 -10
  55. package/libx/_runtime/sqlite/Service.js +7 -5
  56. package/libx/_runtime/sqlite/execute.js +41 -28
  57. package/libx/odata/cqn2odata.js +6 -2
  58. package/libx/rest/RestAdapter.js +3 -6
  59. package/libx/rest/middleware/input.js +2 -3
  60. package/package.json +1 -1
  61. package/srv/extensibility-service.cds +4 -3
  62. package/srv/model-provider.js +1 -1
  63. package/srv/mtx.js +18 -9
  64. package/libx/_runtime/db/utils/propagateForeignKeys.js +0 -93
@@ -12,11 +12,15 @@ const { EventEmitter } = require('events')
12
12
  class EventContext {
13
13
 
14
14
  /** Creates a new instance that inherits from cds.context */
15
- static for (_) {
15
+ static for (_,_as_root) {
16
16
  const ctx = new this (_)
17
- if (features.cds_tx_inheritance) {
18
- const base = cds.context
19
- if (base) ctx._set('_propagated', base)
17
+ const base = cds.context
18
+ if (base) {
19
+ ctx._set('_propagated', base) // we inherit from former cds.currents
20
+ if (!_as_root) {
21
+ if (!ctx.context) ctx._set('context', base.context) // all transaction handling works with root contexts
22
+ if (!ctx.tx && base.tx) ctx.tx = base.tx
23
+ }
20
24
  }
21
25
  return ctx
22
26
  }
@@ -38,7 +42,7 @@ class EventContext {
38
42
  //
39
43
 
40
44
  get emitter() {
41
- return this.context._emitter || (this.context._emitter = new EventEmitter)
45
+ return this.context._emitter || this.context._set('_emitter', new EventEmitter)
42
46
  }
43
47
 
44
48
  async emit (event,...args) {
@@ -115,7 +119,8 @@ class EventContext {
115
119
  }
116
120
 
117
121
  get model() {
118
- return super.model = this._propagated.model || this.http?.req.__model // IMPORTANT: Never use that anywhere else
122
+ const m = this._propagated.model || this.http?.req.__model // IMPORTANT: Never use that anywhere else
123
+ return this._set('model',m)
119
124
  }
120
125
  set model(m) {
121
126
  super.model = m
@@ -156,12 +161,11 @@ class EventContext {
156
161
  */
157
162
  set tx (tx) {
158
163
  Object.defineProperty (this,'tx',{value:tx}) //> allowed only once!
159
- const ctx = tx.context
160
- if (ctx && ctx !== this) {
161
- if (!this.hasOwnProperty('context')) this.context = ctx // eslint-disable-line no-prototype-builtins
162
-
164
+ const root = tx.context?.context
165
+ if (root && root !== this) {
166
+ if (!this.hasOwnProperty('context')) this.context = root // eslint-disable-line no-prototype-builtins
163
167
  if (features.assert_integrity && features.assert_integrity_type == 'RT') {
164
- const reqs = ctx._children || ctx._set('_children', {})
168
+ const reqs = root._children || root._set('_children', {})
165
169
  const all = reqs[tx.name] || (reqs[tx.name] = [])
166
170
  all.push(this)
167
171
  }
@@ -56,6 +56,14 @@ class Service extends require('./srv-handlers') {
56
56
  * Querying API to send synchronous requests...
57
57
  */
58
58
  run (query, data) {
59
+ if (typeof query === 'function') {
60
+ const ctx = cds.context, fn = query
61
+ if (ctx?.tx && !ctx.tx._done) {
62
+ return fn (this.tx(ctx)) // with nested tx
63
+ } else {
64
+ return this.tx (fn) // with root tx
65
+ }
66
+ }
59
67
  const req = new Request ({ query, data })
60
68
  return this.dispatch (req)
61
69
  }
@@ -13,18 +13,23 @@ exports.dispatch = async function dispatch (req) { //NOSONAR
13
13
 
14
14
  // Ensure we are in a proper transaction
15
15
  if (!this.context) {
16
- const gc = cds.context
17
- if (gc && gc.tx && !gc.tx._done) return this.tx(gc).dispatch(req) // with nested tx
18
- else return this.tx(tx => tx.dispatch(req)) // as root tx
16
+ const ctx = cds.context
17
+ if (ctx?.tx && !ctx.tx._done) {
18
+ return this.tx (ctx) .dispatch(req) // with nested tx
19
+ } else {
20
+ return this.tx (tx => tx.dispatch(req)) // with root tx
21
+ }
19
22
  }
20
23
  if (!req.tx) req.tx = this // `this` is a tx from now on...
21
24
 
22
25
  // Inform potential listeners // REVISIT: -> this should move into protocol adapters
23
- if (_is_root(req)) req._.req.emit ('dispatch',req)
26
+ let _is_root = req.constructor.name in { ODataRequest:1, RestRequest:2 }
27
+ if (_is_root) req._.req.emit ('dispatch',req)
24
28
 
25
29
  // Handle batches of queries
26
- if (_is_array(req.query))
27
- return Promise.all (req.query.map (q => this.dispatch ({query:q,__proto__:req,context:req.context})))
30
+ if (_is_array(req.query)) return Promise.all (req.query.map (
31
+ q => this.dispatch ({ query:q, context: req.context, __proto__:req })
32
+ ))
28
33
 
29
34
  // Ensure target and fqns
30
35
  if (!req.target) _ensure_target (this,req)
@@ -88,7 +93,6 @@ exports.handle = async function handle (req) {
88
93
  }
89
94
 
90
95
 
91
- const _is_root = (req) => /OData|REST/i.test(req.constructor.name)
92
96
  const _is_array = Array.isArray
93
97
  const _dummy = ()=>{} // REVISIT: required for some messaging tests which obviously still expect and call next()
94
98
 
@@ -22,13 +22,13 @@ class ExtendedModels {
22
22
  */
23
23
  static middleware4 (srv) {
24
24
  if (!(srv instanceof cds.ApplicationService)) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
25
- if (srv.name?.startsWith('cds.xt.')) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
26
- else return async (req,res,next) => {
25
+ if (!srv.isExtensible) return [] //> no middleware to add
26
+ else return async function cds_context_model (req,res,next) {
27
27
  if (!req.user?.tenant) return next()
28
28
  const ctx = cds.context = cds.EventContext.for({ http: { req, res } })
29
29
  ctx.user = req.user // REVISIT: should move to auth middleware?
30
30
  try {
31
- ctx.model = req.__model = await this.model4 (ctx.tenant, ctx.features)
31
+ ctx.model = req.__model = await ExtendedModels.model4 (ctx.tenant, ctx.features)
32
32
  } catch (e) {
33
33
  LOG.error (Object.assign(e, { message: 'Unable to get service from service map due to error: ' + e.message }))
34
34
 
@@ -99,6 +99,7 @@ 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
102
103
  if (Date.now() - _cached.touched < interval) return model //> checked recently
103
104
 
104
105
  else return this[key] = (async()=>{ // temporarily replace cache entry by promise to avoid race conditions...
package/lib/srv/srv-tx.js CHANGED
@@ -1,6 +1,18 @@
1
1
  const cds = require('../index'), { cds_tx_protection } = cds.env.features
2
2
  const EventContext = require('../req/context')
3
- class RootContext extends EventContext {}
3
+ class RootContext extends EventContext {
4
+ static for(_) {
5
+ if (_ instanceof EventContext) return _
6
+ else return super.for(_,'as root')
7
+ }
8
+ }
9
+ class NestedContext extends EventContext {
10
+ static for(_) {
11
+ if (_ instanceof EventContext) return _
12
+ else return super.for(_)
13
+ }
14
+ }
15
+
4
16
 
5
17
  /**
6
18
  * This is the implementation of the `srv.tx(req)` method. It constructs
@@ -8,35 +20,28 @@ class RootContext extends EventContext {}
8
20
  * @returns { Transaction & import('./srv-api') }
9
21
  * @param { EventContext } ctx
10
22
  */
11
- module.exports = function tx (ctx,fn) { const srv = this
23
+ function srv_tx (ctx,fn) { const srv = this
12
24
 
13
25
  if (srv.context) return srv // srv.tx().tx() -> idempotent
26
+ if (!ctx) return RootTransaction.for (srv)
27
+
28
+ // Creating root or nested txes for existing contexts
29
+ if (ctx instanceof EventContext) {
30
+ if (ctx.tx) return NestedTransaction.for (srv, ctx)
31
+ else return RootTransaction.for (srv, ctx)
32
+ }
14
33
 
15
34
  // Last arg may be a function -> srv.tx (tx => { ... })
16
35
  if (typeof ctx === 'function') [ ctx, fn ] = [ undefined, ctx ]
17
36
  if (typeof fn === 'function') {
18
- const tx = srv.tx(ctx), fx = ()=> Promise.resolve(fn(tx)).then(tx.commit,tx.rollback)
19
- const gc = cds.context, _has_tx = gc && gc.tx && !gc.tx._done
20
- return _has_tx ? fx() : cds._context.run(tx,fx)
37
+ const tx = RootTransaction.for (srv, ctx)
38
+ return cds._context.run (tx, ()=> Promise.resolve(fn(tx)) .then (tx.commit, tx.rollback))
21
39
  }
22
40
 
23
- if (ctx) {
24
- if (ctx.context) ctx = ctx.context
25
-
26
- // REVISIT: This is for compatibility with former srv.tx(req) usages
27
- if (ctx instanceof EventContext) {
28
- if (ctx.tx) return NestedTransaction.for (srv, ctx)
29
- else return RootTransaction.for (srv, ctx)
30
- }
31
-
32
- // REVISIT: This is for compatibility with AFC only
33
- if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
34
- else Object.defineProperty(ctx, '_txed_before', { value: ctx = RootContext.for(ctx) })
35
- return RootTransaction.for (srv, ctx)
36
- }
37
-
38
- // `ctx` is a plain context object or undefined
39
- return RootTransaction.for (srv, RootContext.for(ctx))
41
+ // REVISIT: following is for compatibility with AFC only -> we should get rid of that
42
+ if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
43
+ else Object.defineProperty (ctx, '_txed_before', { value: ctx = RootContext.for(ctx) })
44
+ return RootTransaction.for (srv, ctx)
40
45
  }
41
46
 
42
47
 
@@ -45,17 +50,16 @@ class Transaction {
45
50
  /**
46
51
  * Returns an already started tx for given srv, or creates a new instance
47
52
  */
48
- static for (srv,root) {
49
- let txs = root.transactions
50
- if (!txs) Object.defineProperty(root, 'transactions', {value: txs = new Map})
51
- if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a qirty hack for current Okra Adapters
53
+ static for (srv, ctx) {
54
+ const txs = ctx.context.transactions || ctx.context._set ('transactions', new Map)
55
+ if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a dirty hack for current Okra Adapters
52
56
  let tx = txs.get (srv)
53
- if (!tx) txs.set (srv, tx = new this (srv,root))
57
+ if (!tx) txs.set (srv, tx = new this (srv,ctx))
54
58
  return tx
55
59
  }
56
60
 
57
- constructor (srv,root) {
58
- const tx = { __proto__:srv, context:root }
61
+ constructor (srv, ctx) {
62
+ const tx = { __proto__:srv, _kind: new.target.name, context: ctx }
59
63
  const proto = new.target.prototype
60
64
  tx.commit = proto.commit.bind(tx)
61
65
  tx.rollback = proto.rollback.bind(tx)
@@ -107,11 +111,12 @@ class Transaction {
107
111
  class RootTransaction extends Transaction {
108
112
 
109
113
  /**
110
- * Register the new transaction with the root context.
111
- * @param {EventContext} root
114
+ * Register the new transaction with the given context.
115
+ * @param {EventContext} ctx
112
116
  */
113
- static for (srv,root) {
114
- return root.tx = super.for (srv,root)
117
+ static for (srv, ctx) {
118
+ ctx = RootContext.for (ctx?.tx?._done ? {} : ctx)
119
+ return ctx.tx = super.for (srv, ctx)
115
120
  }
116
121
 
117
122
  /**
@@ -153,16 +158,21 @@ class RootTransaction extends Transaction {
153
158
 
154
159
  class NestedTransaction extends Transaction {
155
160
 
161
+ static for (srv,ctx) {
162
+ ctx = NestedContext.for (ctx)
163
+ return super.for (srv, ctx)
164
+ }
165
+
156
166
  /**
157
- * Registers event listeners with the root context, to commit or rollback
167
+ * Registers event listeners with the given context, to commit or rollback
158
168
  * when the root tx is about to commit or rollback.
159
- * @param {import ('../req/context')} root
169
+ * @param {EventContext} ctx
160
170
  */
161
- constructor (srv,root) {
162
- super (srv,root)
163
- root.before ('succeeded', ()=> this.commit())
164
- root.before ('failed', ()=> this.rollback())
165
- if ('end' in srv) root.once ('done', ()=> srv.end())
171
+ constructor (srv,ctx) {
172
+ super (srv,ctx)
173
+ ctx.before ('succeeded', ()=> this.commit())
174
+ ctx.before ('failed', ()=> this.rollback())
175
+ if ('end' in srv) ctx.once ('done', ()=> srv.end())
166
176
  }
167
177
 
168
178
  }
@@ -193,3 +203,5 @@ const _begin = async function (req) {
193
203
  delete this.dispatch
194
204
  return this.dispatch (req)
195
205
  }
206
+
207
+ module.exports = srv_tx
@@ -70,17 +70,17 @@ exports.mkdirp = async function (...path) {
70
70
 
71
71
  exports.rmdir = (...path) => {
72
72
  const d = resolve (cds.root,...path)
73
- return (fs.promises.rm || fs.promises.rmdir) (d, {recursive:true})
73
+ return fs.promises.rm (d, {recursive:true})
74
74
  }
75
75
 
76
76
  exports.rimraf = (...path) => {
77
77
  const d = resolve (cds.root,...path)
78
- return (fs.promises.rm || fs.promises.rmdir) (d, {recursive:true,force:true})
78
+ return fs.promises.rm (d, {recursive:true,force:true})
79
79
  }
80
80
 
81
81
  exports.rm = async function rm (x) {
82
82
  const y = resolve (cds.root,x)
83
- return (fs.promises.rm || fs.promises.unlink)(y)
83
+ return fs.promises.rm(y)
84
84
  }
85
85
 
86
86
  exports.copy = async function copy (x,y) {
@@ -8,22 +8,22 @@ const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
8
8
 
9
9
  const packTarArchive = async (files, root, flat = false) => {
10
10
  if (typeof files === 'string') return await packArchiveCLI(files)
11
-
11
+
12
12
  let tgzBuffer, temp
13
13
  try {
14
- temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
14
+ temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
15
15
  for (const file of files) {
16
16
  const fname = flat ? path.basename(file) : path.relative(root, file)
17
17
  const destination = path.join(temp, fname)
18
18
  const dirname = path.dirname(destination)
19
19
  if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
20
- await fs.promises.copyFile(file, destination)
20
+ await fs.promises.copyFile(file, destination)
21
21
  }
22
22
 
23
23
  tgzBuffer = await packArchiveCLI(temp)
24
24
  } finally {
25
25
  if (await exists(temp)) {
26
- await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
26
+ await fs.promises.rm(temp, { recursive: true, force: true })
27
27
  }
28
28
  }
29
29
 
@@ -38,7 +38,7 @@ const unpackTarArchive = async (buffer, folder) => {
38
38
  try {
39
39
  await unpackArchiveCLI(tgz, folder)
40
40
  } finally {
41
- if (await exists(temp)) await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
41
+ if (await exists(temp)) await fs.promises.rm(temp, { recursive: true, force: true })
42
42
  }
43
43
  }
44
44
 
@@ -27,7 +27,7 @@ const packArchiveCLI = async (root) => {
27
27
  }
28
28
  finally {
29
29
  if (await exists(temp)) {
30
- await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
30
+ await fs.promises.rm(temp, { recursive: true, force: true })
31
31
  }
32
32
  }
33
33
  }
@@ -183,7 +183,7 @@ module.exports = (srv, options = srv.options) => {
183
183
  (process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
184
184
  ) {
185
185
  if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
186
- app.use(_enforce_authenticated_user)
186
+ app.use(cap_enforce_login)
187
187
  }
188
188
 
189
189
  // so we don't log the same stuff multiple times
@@ -199,7 +199,7 @@ const _strategy4 = config => {
199
199
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
200
200
  }
201
201
 
202
- const _enforce_authenticated_user = (req, res, next) => {
202
+ const cap_enforce_login = (req, res, next) => {
203
203
  if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
204
204
  if (!req.user || req.user._is_anonymous) {
205
205
  if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
@@ -81,7 +81,8 @@ const _getCount = async (tx, readReq) => {
81
81
  // REVISIT: this process appears to be rather clumsy
82
82
  // Copy CQN including from, where and search + changing columns
83
83
  const select = SELECT.from(readReq.query.SELECT.from)
84
- select.SELECT.columns = [{ func: 'count', args: [{ val: '1' }], as: '$count' }]
84
+ // { val: 1 } is used on purpose, as "numbers" are not used as param in prepared stmt
85
+ select.SELECT.columns = [{ func: 'count', args: [{ val: 1 }], as: '$count' }]
85
86
 
86
87
  if (readReq.query.SELECT.where) select.SELECT.where = readReq.query.SELECT.where
87
88
  if (readReq.query.SELECT.search) select.SELECT.search = readReq.query.SELECT.search
@@ -68,8 +68,6 @@ const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\(
68
68
  const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i')
69
69
  const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i')
70
70
 
71
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
72
-
73
71
  function _getBase64(val) {
74
72
  if (isInvalidBase64string(val)) return
75
73
  // convert url-safe to standard base64
@@ -40,8 +40,10 @@ class DeserializerFactory {
40
40
  let additionalInformation = { hasDelta: false }
41
41
  const deserializer = new ResourceJsonDeserializer(edm, jsonContentTypeInfo)
42
42
  return (edmObject, value) => {
43
+ const body = deserializer[name](edmObject, value, expand, additionalInformation)
44
+
43
45
  return {
44
- body: deserializer[name](edmObject, value, expand, additionalInformation),
46
+ body,
45
47
  expand,
46
48
  additionalInformation
47
49
  }
@@ -4,8 +4,6 @@ const {
4
4
  Request,
5
5
  ql: { SELECT }
6
6
  } = cds
7
- const LOG = cds.log()
8
- const { normalizeError } = require('../../../../common/error/frontend')
9
7
 
10
8
  const { getDeepSelect, getSimpleSelectCQN } = require('./handlerUtils')
11
9
  const { hasDeepUpdate } = require('../../../../common/composition/update')
@@ -155,7 +155,9 @@ const _skipToMany = (entity, prop) => {
155
155
  }
156
156
 
157
157
  const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
158
- for (const prop in newEntry) {
158
+ // On app-service layer, generated foreign keys are not enumerable,
159
+ // include them here too.
160
+ for (const prop of Object.getOwnPropertyNames(newEntry)) {
159
161
  if (keys.includes(prop)) {
160
162
  _addKeysToResult(result, prop, newEntry, oldEntry)
161
163
  continue
@@ -277,6 +277,9 @@ const checkIfAssocDeep = (element, value, req) => {
277
277
  // managed to one
278
278
  Object.keys(value).forEach(prop => {
279
279
  if (typeof value[prop] !== 'object') {
280
+ const foreignKey = element._foreignKeys.find(fk => fk.childElement.name === prop)
281
+ if (foreignKey) return
282
+
280
283
  const key = element.keys.find(element => element.ref[0] === prop)
281
284
  if (key) return
282
285
 
@@ -11,6 +11,7 @@ const cds = require('../../cds')
11
11
  const LOG = cds.log('app')
12
12
  const { enrichDataWithKeysFromWhere } = require('../utils/keys')
13
13
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
14
+ const propagateForeignKeys = require('../utils/propagateForeignKeys')
14
15
  const { checkInputConstraints, assertTargets } = require('../../cds-services/util/assert')
15
16
  const getTemplate = require('../utils/template')
16
17
  const templateProcessor = require('../utils/templateProcessor')
@@ -126,6 +127,11 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
126
127
  const { row, key, element, isRoot } = elementInfo
127
128
  category = _getSimpleCategory(category)
128
129
 
130
+ if (category === 'propagateForeignKeys') {
131
+ propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { enumerable: false })
132
+ return
133
+ }
134
+
129
135
  // remember mandatory
130
136
  if (category === 'mandatory') {
131
137
  value.mandatory = true
@@ -186,6 +192,11 @@ const _pick = element => {
186
192
  // collect actions to apply
187
193
  const categories = []
188
194
 
195
+ // REVISIT: element._foreignKeys.length seems to be a very broad check
196
+ if (element.isAssociation && element._foreignKeys.length) {
197
+ categories.push({ category: 'propagateForeignKeys' })
198
+ }
199
+
189
200
  if (element['@assert.range'] || element['@assert.enum'] || element['@assert.format']) {
190
201
  categories.push('assert')
191
202
  }
@@ -241,7 +252,10 @@ async function _handler(req) {
241
252
  if (!req.query) return // FIXME: the code below expects req.query to be defined
242
253
  if (!req.target) return
243
254
 
244
- const template = getTemplate('app-input', this, req.target, { pick: _pick })
255
+ const template = getTemplate('app-input', this, req.target, {
256
+ pick: _pick,
257
+ ignore: element => element._isAssociationStrict
258
+ })
245
259
  if (template.elements.size === 0) return
246
260
 
247
261
  const errors = []
@@ -301,7 +315,8 @@ const _processActionFunctionRow = (row, param, key, errors, event, service) => {
301
315
 
302
316
  // structured
303
317
  const template = getTemplate('app-input-operation', service, param, {
304
- pick: _pick
318
+ pick: _pick,
319
+ ignore: element => element._isAssociationStrict
305
320
  })
306
321
 
307
322
  if (template && template.elements.size) {
@@ -64,7 +64,10 @@ function _handler(req) {
64
64
  const { elements } = req.target
65
65
  for (const k in req.data) if (k in elements && elements[k]['@Core.MediaType']) return
66
66
 
67
- const template = getTemplate('app-put', this, req.target, { pick: _pick })
67
+ const template = getTemplate('app-put', this, req.target, {
68
+ pick: _pick,
69
+ ignore: element => element._isAssociationStrict
70
+ })
68
71
  if (template.elements.size === 0) return
69
72
 
70
73
  // REVISIT: req.data should point into req.query
@@ -1,5 +1,4 @@
1
1
  const cds = require('../../cds')
2
- const LOG = cds.log('app')
3
2
 
4
3
  const _getDateFromQueryOptions = str => {
5
4
  if (str) {
@@ -11,8 +10,6 @@ const _getDateFromQueryOptions = str => {
11
10
 
12
11
  const _isDate = dateStr => !dateStr.includes(':')
13
12
  const _isTimestamp = dateStr => dateStr.includes('.')
14
- const _isWarningRequired = (warning, queryOptions) =>
15
- !warning && queryOptions && (queryOptions['sap-valid-from'] || queryOptions['sap-valid-to'])
16
13
  const _isAsOfNow = queryOptions =>
17
14
  !queryOptions || (!queryOptions['sap-valid-at'] && !queryOptions['sap-valid-to'] && !queryOptions['sap-valid-from'])
18
15
 
@@ -1,8 +1,6 @@
1
1
  const getTemplate = require('./template')
2
2
  const templateProcessor = require('./templateProcessor')
3
3
 
4
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}={0,1}|[A-Za-z0-9+/]{2}={0,2})$/
5
-
6
4
  // convert the standard base64 encoding to the URL-safe variant
7
5
  const toBase64url = value =>
8
6
  (Buffer.isBuffer(value) ? value.toString('base64') : value).replace(/\//g, '_').replace(/\+/g, '-')
@@ -17,12 +15,13 @@ const isInvalidBase64string = value => {
17
15
  if (Buffer.isBuffer(value)) return // ok
18
16
 
19
17
  // convert to standard base64 string; let it crash if typeof value !== 'string'
20
- const base64 = value.replace(/_/g, '/').replace(/-/g, '+')
18
+ const base64value = value.replace(/_/g, '/').replace(/-/g, '+')
21
19
  const normalized = normalizeBase64string(value)
22
20
 
23
21
  // example of invalid base64 string --> 'WTGTdDsD/k21LnFRb+uNcAi=' <-- '...i=' must be '...g='
24
22
  // see https://datatracker.ietf.org/doc/html/rfc4648#section-4
25
- return !base64.match(BASE64) || base64.replace(/=/g, '') !== normalized.replace(/=/g, '')
23
+ if (base64value.replace(/=/g, '') !== normalized.replace(/=/g, '')) return true
24
+ return base64value.length > normalized.length
26
25
  }
27
26
 
28
27
  const _picker = element => {
@@ -27,13 +27,18 @@ function _getOnCondElements(onCond, onCondElements = []) {
27
27
  return onCondElements
28
28
  }
29
29
 
30
- function _modifyWhereWithNavigations(where, newWhere, entityKey, targetKey) {
31
- if (where) {
30
+ function _mergeWhere(base, additional) {
31
+ if (additional?.length) {
32
32
  // copy where else query will be modified
33
- const whereCopy = deepCopyArray(where)
34
- if (newWhere.length > 0) newWhere.push('and')
35
- newWhere.push(...whereCopy)
33
+ const whereCopy = deepCopyArray(additional)
34
+ if (base.length > 0) base.push('and')
35
+ base.push(...whereCopy)
36
36
  }
37
+ return base
38
+ }
39
+
40
+ function _modifyWhereWithNavigations(where, newWhere, entityKey, targetKey) {
41
+ _mergeWhere(newWhere, where)
37
42
 
38
43
  newWhere.forEach(element => {
39
44
  if (element.ref && element.ref[0] === targetKey) {
@@ -85,7 +90,10 @@ function _getWhereFromUpdate(query, target, model) {
85
90
  return where
86
91
  }
87
92
 
88
- return query.UPDATE.where
93
+ const where = query.UPDATE.where || []
94
+ if (query.UPDATE.entity.ref?.length === 1 && query.UPDATE.entity.ref[0].where)
95
+ return _mergeWhere(query.UPDATE.entity.ref[0].where, where)
96
+ return where
89
97
  }
90
98
 
91
99
  // params: data, req, service/tx