@sap/cds 6.8.4 → 7.0.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 (217) hide show
  1. package/CHANGELOG.md +66 -4
  2. package/README.md +0 -1
  3. package/bin/cds-serve.js +50 -3
  4. package/bin/deploy/to-hana.js +1 -0
  5. package/bin/serve.js +16 -20
  6. package/lib/auth/basic-auth.js +6 -4
  7. package/lib/auth/index.js +4 -3
  8. package/lib/auth/jwt-auth.js +2 -5
  9. package/lib/compile/cds-compile.js +34 -89
  10. package/lib/compile/cdsc.js +11 -0
  11. package/lib/compile/etc/properties.js +2 -2
  12. package/lib/compile/for/lean_drafts.js +36 -69
  13. package/lib/compile/for/nodejs.js +2 -1
  14. package/lib/compile/load.js +3 -3
  15. package/lib/compile/minify.js +2 -0
  16. package/lib/compile/to/csn.js +74 -0
  17. package/{bin/build/provider/hana/2tabledata.js → lib/compile/to/hdbtabledata.js} +4 -6
  18. package/lib/compile/to/json.js +1 -1
  19. package/lib/compile/to/sql.js +8 -6
  20. package/lib/dbs/cds-deploy.js +174 -114
  21. package/lib/env/cds-env.js +64 -79
  22. package/lib/env/cds-requires.js +11 -28
  23. package/lib/env/defaults.js +13 -3
  24. package/lib/env/plugins.js +1 -12
  25. package/lib/env/presets.js +25 -21
  26. package/lib/index.js +121 -147
  27. package/lib/{core/reflect.js → linked/models.js} +2 -2
  28. package/lib/{core/infer.js → linked/queries.js} +2 -0
  29. package/lib/{core/index.js → linked/types.js} +2 -1
  30. package/lib/log/cds-error.js +13 -7
  31. package/lib/log/format/cf.js +1 -1
  32. package/lib/plugins.js +49 -0
  33. package/lib/ql/Query.js +0 -9
  34. package/lib/ql/STREAM.js +0 -1
  35. package/lib/req/context.js +2 -7
  36. package/lib/req/request.js +6 -2
  37. package/lib/req/response.js +23 -10
  38. package/lib/srv/middlewares/ctx-model.js +2 -2
  39. package/lib/srv/middlewares/errors.js +1 -1
  40. package/lib/srv/protocols/_legacy.js +1 -0
  41. package/lib/srv/protocols/graphql.js +7 -16
  42. package/lib/srv/protocols/index.js +59 -45
  43. package/lib/srv/protocols/odata-v2-proxy.js +2 -70
  44. package/lib/srv/protocols/odata-v4.js +9 -4
  45. package/lib/srv/srv-api.js +9 -3
  46. package/lib/srv/srv-dispatch.js +12 -9
  47. package/lib/srv/srv-models.js +4 -21
  48. package/lib/srv/srv-tx.js +15 -12
  49. package/lib/utils/cds-test.js +14 -9
  50. package/lib/utils/cds-utils.js +2 -12
  51. package/lib/utils/check-version.js +17 -0
  52. package/{bin/build → lib/utils}/csv-reader.js +23 -24
  53. package/libx/_runtime/auth/index.js +27 -23
  54. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +15 -72
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +0 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +33 -63
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +14 -18
  59. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +15 -5
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -4
  61. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +37 -40
  62. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +7 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +101 -38
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/AbstractError.js +5 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +2 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +9 -8
  68. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +15 -11
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +4 -0
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +5 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -123
  73. package/libx/_runtime/cds-services/services/Service.js +79 -107
  74. package/libx/_runtime/cds-services/services/utils/columns.js +23 -19
  75. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -1
  76. package/libx/_runtime/cds-services/services/utils/differ.js +7 -2
  77. package/libx/_runtime/cds-services/util/assert.js +65 -2
  78. package/libx/_runtime/common/composition/data.js +1 -0
  79. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  80. package/libx/_runtime/common/generic/auth/restrict.js +5 -10
  81. package/libx/_runtime/common/generic/auth/restrictions.js +40 -0
  82. package/libx/_runtime/common/generic/auth/utils.js +1 -2
  83. package/libx/_runtime/common/generic/crud.js +32 -16
  84. package/libx/_runtime/common/generic/etag.js +133 -104
  85. package/libx/_runtime/common/generic/input.js +6 -21
  86. package/libx/_runtime/common/generic/put.js +1 -1
  87. package/libx/_runtime/common/generic/stream.js +52 -0
  88. package/libx/_runtime/common/generic/temporal.js +25 -8
  89. package/libx/_runtime/common/i18n/messages.properties +0 -2
  90. package/libx/_runtime/common/utils/cqn.js +1 -1
  91. package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
  92. package/libx/_runtime/common/utils/csn.js +0 -51
  93. package/libx/_runtime/common/utils/etag.js +30 -0
  94. package/libx/_runtime/common/utils/keys.js +1 -1
  95. package/libx/_runtime/common/utils/normalizeTimestamp.js +25 -0
  96. package/libx/_runtime/common/utils/path.js +1 -1
  97. package/libx/_runtime/common/utils/resolveView.js +2 -1
  98. package/libx/_runtime/common/utils/rewriteAsterisks.js +6 -4
  99. package/libx/_runtime/common/utils/search2cqn4sql.js +12 -16
  100. package/libx/_runtime/common/utils/stream.js +140 -0
  101. package/libx/_runtime/common/utils/streamProp.js +29 -12
  102. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +0 -2
  103. package/libx/_runtime/db/generic/index.js +0 -2
  104. package/libx/_runtime/db/query/delete.js +2 -2
  105. package/libx/_runtime/db/query/insert.js +2 -2
  106. package/libx/_runtime/db/query/read.js +2 -2
  107. package/libx/_runtime/db/query/run.js +2 -2
  108. package/libx/_runtime/db/query/update.js +2 -2
  109. package/libx/_runtime/db/sql-builder/BaseBuilder.js +0 -6
  110. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +23 -12
  111. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +18 -6
  112. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -0
  113. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -7
  114. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +1 -0
  115. package/libx/_runtime/db/utils/normalizeTimeData.js +7 -3
  116. package/libx/_runtime/fiori/draft.js +2 -0
  117. package/libx/_runtime/fiori/generic/activate.js +8 -9
  118. package/libx/_runtime/fiori/generic/before.js +30 -20
  119. package/libx/_runtime/fiori/generic/cancel.js +5 -3
  120. package/libx/_runtime/fiori/generic/delete.js +5 -3
  121. package/libx/_runtime/fiori/generic/edit.js +7 -7
  122. package/libx/_runtime/fiori/generic/index.js +10 -16
  123. package/libx/_runtime/fiori/generic/new.js +5 -3
  124. package/libx/_runtime/fiori/generic/patch.js +11 -8
  125. package/libx/_runtime/fiori/generic/prepare.js +13 -6
  126. package/libx/_runtime/fiori/generic/read.js +12 -6
  127. package/libx/_runtime/fiori/lean-draft.js +207 -152
  128. package/libx/_runtime/fiori/utils/delete.js +10 -5
  129. package/libx/_runtime/fiori/utils/req.js +17 -5
  130. package/libx/_runtime/fiori/utils/stream.js +36 -0
  131. package/libx/_runtime/hana/Service.js +12 -9
  132. package/libx/_runtime/hana/conversion.js +10 -15
  133. package/libx/_runtime/hana/driver.js +2 -0
  134. package/libx/_runtime/hana/execute.js +28 -6
  135. package/libx/_runtime/hana/pool.js +36 -122
  136. package/libx/_runtime/hana/search2cqn4sql.js +34 -36
  137. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -6
  138. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -1
  139. package/libx/_runtime/messaging/enterprise-messaging.js +10 -58
  140. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  141. package/libx/_runtime/remote/Service.js +20 -1
  142. package/libx/_runtime/remote/utils/client.js +3 -5
  143. package/libx/_runtime/sqlite/Service.js +4 -6
  144. package/libx/_runtime/sqlite/conversion.js +3 -13
  145. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +9 -6
  146. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +6 -1
  147. package/libx/_runtime/sqlite/execute.js +5 -16
  148. package/libx/odata/afterburner.js +22 -6
  149. package/libx/odata/grammar.pegjs +6 -1
  150. package/libx/odata/parser.js +1 -1
  151. package/libx/rest/RestAdapter.js +16 -9
  152. package/libx/rest/RestRequest.js +1 -1
  153. package/libx/rest/middleware/input.js +2 -1
  154. package/libx/rest/middleware/operation.js +1 -0
  155. package/libx/rest/middleware/parse.js +3 -2
  156. package/libx/rest/middleware/payload.js +9 -8
  157. package/libx/rest/middleware/read.js +1 -0
  158. package/package.json +9 -16
  159. package/server.js +1 -1
  160. package/app/fiori/preview.js +0 -270
  161. package/app/fiori/routes.js +0 -59
  162. package/bin/build/buildTaskEngine.js +0 -360
  163. package/bin/build/buildTaskFactory.js +0 -283
  164. package/bin/build/buildTaskHandler.js +0 -241
  165. package/bin/build/buildTaskProvider.js +0 -22
  166. package/bin/build/buildTaskProviderFactory.js +0 -175
  167. package/bin/build/cds.js +0 -5
  168. package/bin/build/constants.js +0 -66
  169. package/bin/build/index.js +0 -58
  170. package/bin/build/provider/buildTaskHandlerEdmx.js +0 -82
  171. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +0 -131
  172. package/bin/build/provider/buildTaskHandlerInternal.js +0 -254
  173. package/bin/build/provider/buildTaskProviderInternal.js +0 -383
  174. package/bin/build/provider/fiori/index.js +0 -171
  175. package/bin/build/provider/hana/2migration.js +0 -179
  176. package/bin/build/provider/hana/index.js +0 -505
  177. package/bin/build/provider/hana/migrationtable.js +0 -472
  178. package/bin/build/provider/hana/template/.hdiconfig-haas +0 -163
  179. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +0 -137
  180. package/bin/build/provider/hana/template/.hdinamespace +0 -4
  181. package/bin/build/provider/hana/template/package.json +0 -12
  182. package/bin/build/provider/hana/template/undeploy.json +0 -5
  183. package/bin/build/provider/java/index.js +0 -111
  184. package/bin/build/provider/java-cf/index.js +0 -1
  185. package/bin/build/provider/mtx/index.js +0 -268
  186. package/bin/build/provider/mtx/resourcesTarBuilder.js +0 -95
  187. package/bin/build/provider/mtx-extension/index.js +0 -131
  188. package/bin/build/provider/mtx-sidecar/index.js +0 -137
  189. package/bin/build/provider/node-cf/index.js +0 -1
  190. package/bin/build/provider/nodejs/index.js +0 -192
  191. package/bin/build/util.js +0 -299
  192. package/bin/cds.js +0 -125
  193. package/bin/deploy/to-hana/cfUtil.js +0 -355
  194. package/bin/deploy/to-hana/gitUtil.js +0 -57
  195. package/bin/deploy/to-hana/hana.js +0 -306
  196. package/bin/deploy/to-hana/hdiDeployUtil.js +0 -153
  197. package/bin/deploy/to-hana/index.js +0 -16
  198. package/bin/deploy/to-hana/mtaUtil.js +0 -170
  199. package/bin/mtx/in-cds.js +0 -17
  200. package/bin/plugins.js +0 -32
  201. package/bin/run.js +0 -24
  202. package/bin/utils/log.js +0 -24
  203. package/bin/version.js +0 -178
  204. package/libx/_runtime/audit/Service.js +0 -222
  205. package/libx/_runtime/audit/generic/personal/access.js +0 -61
  206. package/libx/_runtime/audit/generic/personal/index.js +0 -56
  207. package/libx/_runtime/audit/generic/personal/modification.js +0 -132
  208. package/libx/_runtime/audit/generic/personal/utils.js +0 -186
  209. package/libx/_runtime/audit/utils/log.js +0 -23
  210. package/libx/_runtime/audit/utils/v2.js +0 -176
  211. package/libx/_runtime/db/data-conversion/timestamp.js +0 -9
  212. package/libx/_runtime/db/generic/integrity.js +0 -455
  213. package/srv/audit-log.cds +0 -87
  214. package/srv/mtx.cds +0 -2
  215. package/srv/mtx.js +0 -8
  216. /package/lib/{core → linked}/classes.js +0 -0
  217. /package/lib/{core → linked}/entities.js +0 -0
@@ -146,20 +146,15 @@ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
146
146
  req.query._draftRestrictions = resolvedApplicables
147
147
  return
148
148
  }
149
+ // in case of $apply take a query from sub SELECT//
150
+ const query = req.query.SELECT.from.SELECT?.from?.ref ? req.query.SELECT.from : req.query
149
151
 
150
- if (typeof req.query.SELECT.from === 'object')
151
- // in case of $apply take a ref from sub SELECT//
152
- req.query.SELECT.from.ref = _addWheresToRef(
153
- req.query.SELECT.from.ref || req.query.SELECT.from.SELECT?.from?.ref,
154
- model,
155
- resolvedApplicables
156
- )
152
+ query.SELECT.from.ref = _addWheresToRef(query.SELECT.from.ref, model, resolvedApplicables)
157
153
 
158
154
  const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
159
155
  if (!restrictionForTarget) return
160
156
 
161
- // apply restriction
162
- req.query.where(restrictionForTarget)
157
+ query.where(restrictionForTarget)
163
158
  }
164
159
 
165
160
  const _getFromWithIsActiveEntityRemoved = from => {
@@ -229,7 +224,7 @@ async function handler(req) {
229
224
  return
230
225
  }
231
226
 
232
- let restrictions = this.getRestrictions(definition, req.event, req.user)
227
+ let restrictions = this.getRestrictions.call(this, definition, req.event, req.user)
233
228
  if (restrictions instanceof Promise) restrictions = await restrictions
234
229
  if (!restrictions) {
235
230
  // > unrestricted
@@ -1,3 +1,42 @@
1
+ const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
2
+ const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
3
+
4
+ /**
5
+ * Returns the applicable restrictions for the current request as follows:
6
+ * - null: unrestricted access
7
+ * - []: no access
8
+ * - [{ grant: '...', to: ['...'], where: '...' }, ...]: applicable restrictions with grant normalized to strings,
9
+ * i.e., grant: ['CREATE', 'UPDATE'] in model becomes [{ grant: 'CREATE' }, { grant: 'UPDATE' }]
10
+ * - Promise resovling to any of the above (needed for CAS overrides)
11
+ *
12
+ * @param {object} definition - then csn definition of an entity or an (un)bound action or function
13
+ * @param {string} event - the event name
14
+ * @param {import('../../../../lib/req/user')} user - the current user
15
+ * @returns {Promise | Array | null}
16
+ */
17
+ function getRestrictions(definition, event, user) {
18
+ const { model } = this
19
+ let restrictions = getNormalizedRestrictions(definition, model.definitions, event)
20
+ if (!restrictions && (event in CRUD || !definition.parent)) {
21
+ // > unrestricted entity or unbound
22
+ return null
23
+ }
24
+ if (event in CRUD && restrictions.length && restrictions.every(r => r.grant !== '*' && !(r.grant in CRUD))) {
25
+ // > only bounds are restricted
26
+ return null
27
+ }
28
+ if (!(event in CRUD) && !restrictions && definition.parent) {
29
+ // > bound without own restrictions -> get from parent
30
+ restrictions = getNormalizedRestrictions(definition.parent, model.definitions, event)
31
+ if (!restrictions) {
32
+ // > unrestricted bound
33
+ return null
34
+ }
35
+ }
36
+ // return the applicable restrictions (grant and to fit to request and user)
37
+ return getApplicableRestrictions(restrictions, event, user)
38
+ }
39
+
1
40
  const _getLocalName = definition => {
2
41
  return definition._service ? definition.name.replace(`${definition._service.name}.`, '') : definition.name
3
42
  }
@@ -87,6 +126,7 @@ const getNormalizedPlainRestrictions = (restrictions, definition) => {
87
126
  }
88
127
 
89
128
  module.exports = {
129
+ getRestrictions,
90
130
  getNormalizedRestrictions,
91
131
  getApplicableRestrictions,
92
132
  getNormalizedPlainRestrictions
@@ -10,11 +10,10 @@ const reject = (req, reason = null) => {
10
10
  if (req.user._is_anonymous) {
11
11
  // REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
12
12
  // REVISIT: improve `req.http.req` check if this is an HTTP request
13
- if (req.http?.req && req.user._challenges && req.user._challenges.length > 0) {
13
+ if (req.http?.res && req.user._challenges && req.user._challenges.length > 0) {
14
14
  req.http.res.set('WWW-Authenticate', req.user._challenges.join(';'))
15
15
  }
16
16
 
17
- // REVISIT: security log in else case?
18
17
  return req.reject(401)
19
18
  }
20
19
 
@@ -2,25 +2,12 @@ const cds = require('../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
4
  const { deepCopyArray } = require('../utils/copy')
5
-
6
5
  const { getColumns } = require('../../cds-services/services/utils/columns')
7
-
6
+ const { enhanceStreamResult } = require('../utils/stream')
8
7
  const getError = require('../error')
9
8
 
10
9
  const _targetEntityDoesNotExist = async req => {
11
- const { query } = req
12
- const cqn = SELECT.from(query.UPDATE.entity, [1])
13
-
14
- if (query.UPDATE.entity.as) {
15
- cqn.SELECT.from.as = query.UPDATE.entity.as
16
- }
17
-
18
- // REVISIT: compat mode for service functions .update
19
- if (query.UPDATE && query.UPDATE.where) {
20
- cqn.where(query.UPDATE.where)
21
- }
22
-
23
- const exists = await cds.tx(req).run(cqn)
10
+ const exists = await cds.tx(req).run(SELECT.from(req.subject, [1]))
24
11
  return exists.length === 0
25
12
  }
26
13
 
@@ -80,22 +67,26 @@ exports.impl = cds.service.impl(function () {
80
67
  result = await cds.tx(req).run(req.query, req.data)
81
68
  }
82
69
 
70
+ // regarding etag validation: we do not want to execute an additional select to distinguish between 412 and 404
71
+
83
72
  if (req.event === 'READ') {
84
73
  if ((result == null || result.length === 0) && pathExistsQuery) {
85
74
  const res = await pathExistsQuery
86
75
  if (res.length === 0) req.reject(404)
87
76
  }
77
+ if (result == null && req._etagValidationType === 'if-match') req.reject(412)
88
78
  return result
89
79
  }
90
80
 
91
81
  if (req.event === 'DELETE') {
92
- if (result === 0) req.reject(404)
82
+ if (result === 0) req.reject(req._etagValidationType ? 412 : 404)
93
83
  return result
94
84
  }
95
85
 
96
86
  // case: no authorization check and payload more than just keys but no changes
97
87
  // -> affected rows === 0 -> no change or not exists?
98
88
  if (req.event === 'UPDATE' && result === 0 && !req._authChecked) {
89
+ if (req._etagValidationType) req.reject(412)
99
90
  if (await _targetEntityDoesNotExist(req)) req.reject(404)
100
91
  }
101
92
 
@@ -104,4 +95,29 @@ exports.impl = cds.service.impl(function () {
104
95
 
105
96
  return req.data
106
97
  })
98
+
99
+ this.after('READ', '*', async function ([result], req) {
100
+ if (req.query?._streaming) {
101
+ await enhanceStreamResult(req, req.query, result, this.model)
102
+ }
103
+ })
104
+
105
+ this.on('STREAM', '*', async function (req) {
106
+ if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
107
+ throw getError({
108
+ code: 501,
109
+ message: `Entity "${req.target.name}" is annotated with "@cds.persistence.skip" and cannot be served generically.`
110
+ })
111
+ }
112
+
113
+ if (req.query.STREAM.from) {
114
+ return { value: await cds.tx(req).run(req.query, req.data) }
115
+ } else {
116
+ return await cds.tx(req).run(req.query, req.data)
117
+ }
118
+ })
119
+
120
+ this.after(['STREAM'], '*', async function ([result], req) {
121
+ if (req.query.STREAM.from) await enhanceStreamResult(req, req.query, result, this.model)
122
+ })
107
123
  })
@@ -1,76 +1,66 @@
1
1
  const cds = require('../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
- // REVISIT: draft should not be handled here, e.g., target.name should be adjusted before
5
- const { isActiveEntityRequested } = require('../../fiori/utils/where')
6
- const { ensureDraftsSuffix } = require('../../fiori/utils/handler')
7
- const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
8
- const { isAsteriskColumn } = require('../../common/utils/rewriteAsterisks')
9
- const { resolveView } = require('../utils/resolveView')
10
-
11
- const C_U_ = {
12
- CREATE: 1,
13
- UPDATE: 1
14
- }
4
+ const { convertPathExpressionToWhere } = require('../utils/cqn2cqn4sql')
5
+ const { addEtagColumns } = require('../utils/etag')
15
6
 
16
- const getRequestedTarget = query => {
17
- if (query.SELECT) {
18
- return query.SELECT.from
19
- } else if (query.UPDATE) {
20
- return query.UPDATE.entity
21
- } else {
22
- return query.DELETE.from
7
+ const _getMatchHeaders = req => {
8
+ return {
9
+ ifMatch: req.headers['if-match']?.replace(/^"\*"$/, '*'),
10
+ ifNoneMatch: req.headers['if-none-match']?.replace(/^"\*"$/, '*')
23
11
  }
24
12
  }
25
13
 
26
- const getSelectCQN = (query, target, model, isActive, etag) => {
27
- const targetName = isActive ? target.name : ensureDraftsSuffix(target.name)
28
- const cqn = cqn2cqn4sql(SELECT.from(getRequestedTarget(query)), model)
29
- cqn.columns([etag || target._etag.name])
30
- cqn.SELECT.from.ref[0] = targetName
31
-
32
- return cqn
14
+ const _parseHeaderEtagValue = value => {
15
+ return value.split(',').map(str => {
16
+ let result = str.trim()
17
+ if (result === '*') return result
18
+ if (result.startsWith('W/')) result = result.substring(2)
19
+ return result.startsWith('"') && result.endsWith('"') ? result.substring(1, result.length - 1) : null
20
+ })
33
21
  }
34
22
 
35
- /**
36
- Recursively adds etag columns if a manual list of columns is specified.
37
- If asterisk columns or no columns are given, the database layer will
38
- add the etag columns anyway.
39
- */
40
- const _addEtagColumns = (columns, entity) => {
41
- if (!columns || !Array.isArray(columns)) return
42
- if (
43
- entity._etag &&
44
- !columns.some(c => isAsteriskColumn(c)) &&
45
- !(columns.length === 1 && columns[0].func === 'count') &&
46
- !columns.some(c => c.ref && c.ref[c.ref.length - 1] === entity._etag.name)
47
- ) {
48
- columns.push({ ref: [entity._etag.name] })
49
- }
50
- const expands = columns.filter(c => c.expand)
51
- for (const expand of expands) {
52
- const refName = expand.ref[expand.ref.length - 1]
53
- const targetEntity = refName && entity.elements[refName] && entity.elements[refName]._target
54
- if (targetEntity) {
55
- _addEtagColumns(expand.expand, targetEntity)
23
+ const _getValidationStmt = (ifMatchEtags, ifNoneMatchEtags, req, model) => {
24
+ const etagElement = req.target.elements[req.target._etag.name]
25
+
26
+ const { target, alias, where } = convertPathExpressionToWhere(req.subject, model, { tableAliasPrefix: 'ETAG_' })
27
+ const select = SELECT.from(target).columns(1).where(where)
28
+ if (alias) select.SELECT.from.as = alias
29
+ // tell resolveView to leave this select alone
30
+ select._doNotResolve = true
31
+ // tell db layer that this is a subselect for etag validation
32
+ // hacky solution for gap in @sap/hana-client
33
+ select._etagValidation = true
34
+
35
+ const cond = []
36
+ if (ifMatchEtags) {
37
+ if (ifMatchEtags.includes('*')) return true
38
+ // HANA does not allow malformed time values in prepared statements
39
+ if (etagElement.type === 'cds.Timestamp' || etagElement.type === 'cds.DateTime') {
40
+ ifMatchEtags = ifMatchEtags.filter(val => new Date(val).toString() !== 'Invalid Date')
41
+ if (!ifMatchEtags.length) return false
56
42
  }
43
+
44
+ cond.push({ ref: alias ? [alias, etagElement.name] : [etagElement.name] })
45
+ if (ifMatchEtags.length === 1) cond.push('=', { val: ifMatchEtags[0] })
46
+ else cond.push('in', { list: ifMatchEtags.map(val => ({ val })) })
47
+ } else {
48
+ if (ifNoneMatchEtags.includes('*')) return false
49
+ // if a malformed time value is present, it cannot match -> precondition true
50
+ if (
51
+ (etagElement.type === 'cds.Timestamp' || etagElement.type === 'cds.DateTime') &&
52
+ ifNoneMatchEtags.some(val => new Date(val).toString() === 'Invalid Date')
53
+ ) {
54
+ return true
55
+ }
56
+
57
+ cond.push({ ref: alias ? [alias, etagElement.name] : [etagElement.name] })
58
+ if (ifNoneMatchEtags.length === 1) cond.push('!=', { val: ifNoneMatchEtags[0] })
59
+ else cond.push('not', 'in', { list: ifNoneMatchEtags.map(val => ({ val })) })
57
60
  }
58
- }
61
+ if (!cond.length) return
59
62
 
60
- const _isConcurrentODataReq = req => {
61
- const isReadAfterDraftAction =
62
- req.event === 'READ' && req.target._isDraftEnabled && req.context.event in { draftActivate: 1, EDIT: 1 }
63
- // It's allowed to also delete drafts when actives are deleted
64
- if (
65
- cds.env.fiori?.lean_draft &&
66
- req.event === 'READ' &&
67
- req.context.event === 'DELETE' &&
68
- req.target?.name.endsWith('.drafts') &&
69
- !req.context?.target?.name.endsWith('.drafts')
70
- )
71
- return
72
- const _req = isReadAfterDraftAction ? req.context : req
73
- return _req._isOData && _req.isConcurrentResource
63
+ return select.where(cond)
74
64
  }
75
65
 
76
66
  /**
@@ -78,71 +68,110 @@ const _isConcurrentODataReq = req => {
78
68
  *
79
69
  * @param req
80
70
  */
81
- const commonGenericEtag = async function (req) {
82
- // REVISIT: The check for ODataRequest should be removed after etag logic is moved
83
- // from okra to commons and etag handling is also allowed for rest.
84
-
85
- if (_isConcurrentODataReq(req)) {
86
- const etagElement = req.target.elements[req.target._etag.name]
87
-
88
- // automatically add etag columns if not already there
89
- if (req.query.SELECT) _addEtagColumns(req.query.SELECT.columns, req.target)
90
-
91
- // validate
92
- if (req.isConditional && !req.query.INSERT) {
93
- let cqn
94
- const isActive = isActiveEntityRequested(getRequestedTarget(req.query).ref[0].where)
95
- if ((req.query.UPDATE || req.query.DELETE) && isActive) {
96
- const query_ = resolveView(req.query, this.model, cds.db)
97
- const transition = (query_.UPDATE && query_.UPDATE._transitions[0]) || query_.DELETE._transitions[0]
98
- const etag = transition.mapping.get(req.target._etag.name).ref[0]
99
- cqn = getSelectCQN(query_, transition.target, this.model, isActive, etag).forUpdate()
100
- } else {
101
- cqn = getSelectCQN(req.query, req.target, this.model, isActive)
102
- }
71
+ const commonGenericValidateETag = async function (req) {
72
+ /*
73
+ * currently, etag is only supported for OData requests
74
+ * REST requests should be added later
75
+ * other protocols, such as graphql, need to be checked
76
+ */
77
+ if (req.protocol !== 'odata-v4') return
78
+
79
+ // automatically add etag columns if not already there
80
+ if (req.query.SELECT) addEtagColumns(req.query.SELECT.columns, req.target)
81
+
82
+ // querying a collection?
83
+ if (req.event === 'READ' && !req.query.SELECT.one) return
84
+
85
+ // etag provided?
86
+ const { ifMatch, ifNoneMatch } = _getMatchHeaders(req)
87
+ if (!ifMatch && !ifNoneMatch) {
88
+ if (req.event === 'READ') return // > ok and nothing more to do
89
+ req.reject(428) // > on writes, an etag must be provided
90
+ }
103
91
 
104
- const result = await cds.tx(req).run(cqn)
92
+ // normalize
93
+ const ifMatchEtags = ifMatch && _parseHeaderEtagValue(ifMatch)
94
+ const ifNoneMatchEtags = ifNoneMatch && _parseHeaderEtagValue(ifNoneMatch)
105
95
 
106
- if (result.length === 1) {
107
- const etag = Object.values(result[0])[0]
108
- req.validateEtag(etag == null ? 'null' : etag)
109
- } else {
110
- req.validateEtag('*')
111
- }
112
- }
96
+ // get select for validation
97
+ const validationStmt = _getValidationStmt(ifMatchEtags, ifNoneMatchEtags, req, this.model)
113
98
 
114
- // generate new etag, if UUID
115
- if (C_U_[req.event] && etagElement.isUUID) {
116
- req.data[etagElement.name] = cds.utils.uuid()
99
+ // shortcuts
100
+ if (validationStmt === true) {
101
+ // wildcard -> nothing to do
102
+ return
103
+ } else if (validationStmt === false) {
104
+ // never true -> reject
105
+ req.reject(412)
106
+ }
107
+
108
+ // bound action or CRUD?
109
+ if (req.event === 'EDIT' || (req.target.actions && req.event in req.target.actions)) {
110
+ // REVISIT: how to not directly access db?
111
+ if (cds.db) {
112
+ const result = await cds.tx(req).run(validationStmt)
113
+ if (!result.length) req.reject(412)
117
114
  }
115
+ } else if (validationStmt) {
116
+ // add where clause for validation
117
+ const validationClause = ['exists', validationStmt]
118
+ req.query.where(validationClause)
119
+ // HACK for current draft impl
120
+ req._etagValidationClause = validationClause
121
+ req._etagValidationType = ifMatchEtags ? 'if-match' : 'if-none-match'
118
122
  }
119
123
  }
120
124
 
121
125
  /**
122
- * handler registration
126
+ * adds a new uuid for the etag element to the request payload
123
127
  *
128
+ * @param req
129
+ */
130
+ const commonGenericGenerateETag = function (req) {
131
+ const etagElement = req.target.elements[req.target._etag.name]
132
+ req.data[etagElement.name] = cds.utils.uuid()
133
+ }
134
+
135
+ /**
136
+ * handler registration
124
137
  */
125
138
  /* istanbul ignore next */
126
139
  module.exports = cds.service.impl(function () {
127
- commonGenericEtag._initial = true
140
+ commonGenericValidateETag._initial = true
141
+ commonGenericGenerateETag._initial = true
128
142
 
129
143
  for (const k in this.entities) {
130
144
  const entity = this.entities[k]
131
145
 
132
146
  if (!entity._etag) continue
133
147
 
134
- // handler for CREATE is registered for backwards compatibility w.r.t. ETag generation
135
- let events = ['CREATE', 'READ', 'UPDATE', 'DELETE']
136
-
137
- // if odata and fiori is separated, this will not be needed in the odata version
138
148
  if (entity._isDraftEnabled) {
139
- events = ['READ', 'NEW', 'DELETE', 'PATCH', 'EDIT', 'CANCEL']
149
+ if (cds.env.fiori?.lean_draft) {
150
+ this.before(['READ', 'DELETE'], entity, commonGenericValidateETag)
151
+ // if draft compat is on, the read handler is automatically registered for <entity> and <entity>.drafts
152
+ const events = cds.env.fiori.draft_compat ? ['UPDATE', 'CANCEL'] : ['READ', 'UPDATE', 'CANCEL']
153
+ this.before(events, entity.drafts, commonGenericValidateETag)
154
+ } else {
155
+ this.before(['READ', 'PATCH', 'CANCEL', 'DELETE'], entity, commonGenericValidateETag)
156
+ }
157
+ } else {
158
+ this.before(['READ', 'UPDATE', 'DELETE'], entity, commonGenericValidateETag)
140
159
  }
141
160
 
142
- this.before(events, entity, commonGenericEtag)
143
-
144
161
  for (const action in entity.actions) {
145
- this.before(action, entity, commonGenericEtag)
162
+ // etag not applicable to functions and unbound actions
163
+ if (entity.actions[action].kind !== 'action') continue
164
+ if (entity._isDraftEnabled) {
165
+ let _entity = entity
166
+ if (cds.env.fiori?.lean_draft) _entity = action === 'draftEdit' ? entity : entity.drafts
167
+ this.before(action === 'draftEdit' ? 'EDIT' : action, _entity, commonGenericValidateETag)
168
+ } else {
169
+ this.before(action, entity, commonGenericValidateETag)
170
+ }
146
171
  }
172
+
173
+ // for backwards compatibility w.r.t. ETag generation if type UUID
174
+ const etagElement = entity.elements[entity._etag.name]
175
+ if (etagElement.isUUID) this.before(['CREATE', 'UPDATE', 'NEW'], entity, commonGenericGenerateETag)
147
176
  }
148
177
  })
@@ -42,18 +42,6 @@ const _isDraftCoreComputed = (req, element, event) =>
42
42
  req._.event === 'draftActivate' &&
43
43
  !((event === 'CREATE' && element['@cds.on.insert']) || element['@cds.on.update'])
44
44
 
45
- const _isStreamingProperty = (elements, row, property) =>
46
- Object.values(elements).some(
47
- element => element['@Core.MediaType'] && element['@Core.MediaType']['='] === property && row[element.name]
48
- )
49
-
50
- const _getMediaTypeValue = () => {
51
- const ctx = cds.context
52
- return (
53
- !ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
54
- )
55
- }
56
-
57
45
  const _preProcessAssertTarget = (assocInfo, assertMap) => {
58
46
  const { element: assoc, row } = assocInfo
59
47
  const assocTarget = assoc._target
@@ -147,14 +135,6 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
147
135
  if ((event === 'UPDATE' || event === 'CREATE') && category === '@assert.target') {
148
136
  _preProcessAssertTarget(elementInfo, assertMap)
149
137
  }
150
-
151
- // set media type from content-type header if streaming
152
- if (category === 'stream') {
153
- if (_isStreamingProperty(element.parent.elements, row, key)) {
154
- const mtValue = _getMediaTypeValue()
155
- if (mtValue) row[key] = mtValue
156
- }
157
- }
158
138
  }
159
139
 
160
140
  const _getProcessorFn = (req, errors, assertMap) => {
@@ -186,7 +166,12 @@ const _pick = element => {
186
166
  categories.push({ category: 'propagateForeignKeys' })
187
167
  }
188
168
 
189
- if (element['@assert.range'] || element['@assert.enum'] || element['@assert.format']) {
169
+ if (
170
+ element['@assert.range'] ||
171
+ element['@assert.enum'] ||
172
+ element['@assert.format'] ||
173
+ element.type === 'cds.Decimal'
174
+ ) {
190
175
  categories.push('assert')
191
176
  }
192
177
 
@@ -24,7 +24,7 @@ const _fillStructure = (row, parts, element, category, args) => {
24
24
  }
25
25
 
26
26
  const _getProcessorFn = req => {
27
- const REST = req._isRest
27
+ const REST = req.protocol === 'rest'
28
28
 
29
29
  return ({ row, key, element, plain }) => {
30
30
  if (!row || row[key] !== undefined) return
@@ -0,0 +1,52 @@
1
+ const { isNewStream } = require('../utils/stream')
2
+
3
+ const cds = require('../../cds')
4
+
5
+ const _getStreamingProperties = elements => {
6
+ const result = []
7
+ for (const key in elements) {
8
+ const element = elements[key]
9
+ if (typeof element['@Core.MediaType'] === 'object') {
10
+ result.push({ stream: key, type: element['@Core.MediaType']['='] })
11
+ }
12
+ }
13
+
14
+ return result
15
+ }
16
+
17
+ const _getMediaTypeValue = () => {
18
+ const ctx = cds.context
19
+ return (
20
+ !ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
21
+ )
22
+ }
23
+
24
+ function _addContentType(req, mtValue) {
25
+ if (!req.data) return
26
+ const streamProp = _getStreamingProperties(req.target.elements).find(prop => req.data[prop.stream])
27
+ if (streamProp) req.data[streamProp.type] = mtValue
28
+ }
29
+
30
+ async function _addContentTypeStream(req, mtValue) {
31
+ if (req.query.UPDATE || req.query.STREAM?.from) return
32
+ if (req.target._hasPersistenceSkip) return
33
+ const streamProp = _getStreamingProperties(req.target.elements).find(prop => req.query.STREAM.column === prop.stream)
34
+ // REVISIT: move this update to after handler and add etag
35
+ if (streamProp) await UPDATE.entity(req.query.STREAM.into).data({ [streamProp.type]: mtValue })
36
+ }
37
+
38
+ async function addContentType(req) {
39
+ if (!req.query || !req.target) return
40
+ const mtValue = _getMediaTypeValue()
41
+ if (!mtValue) return
42
+
43
+ if (isNewStream()) {
44
+ _addContentTypeStream(req, mtValue)
45
+ } else {
46
+ _addContentType(req, mtValue)
47
+ }
48
+ }
49
+
50
+ module.exports = cds.service.impl(function () {
51
+ this.before(['PATCH', 'UPDATE', 'STREAM'], '*', addContentType)
52
+ })
@@ -1,10 +1,11 @@
1
1
  const cds = require('../../cds')
2
+ const normalizeTimestamp = require('../utils/normalizeTimestamp')
2
3
 
3
4
  const _getDateFromQueryOptions = str => {
4
5
  if (str) {
5
6
  const match = str.match(/^date'(.+)'$/)
6
7
  // REVISIT: What happens with invalid date values in query parameter? if match.length > 1
7
- return new Date(match ? match[1] : str)
8
+ return normalizeTimestamp(match ? match[1] : str)
8
9
  }
9
10
  }
10
11
 
@@ -51,15 +52,31 @@ const commonGenericTemporal = function (req) {
51
52
 
52
53
  if (_isAsOfNow(_queryOptions)) {
53
54
  const date = new Date()
54
- _['VALID-FROM'] = date
55
- _['VALID-TO'] = new Date(date.getTime() + _getTimeDelta(req.target))
55
+ _['VALID-FROM'] = normalizeTimestamp(date)
56
+ _['VALID-TO'] = normalizeTimestamp(date.getTime() + _getTimeDelta(req.target))
56
57
  } else if (_queryOptions['sap-valid-at']) {
57
- const date = _getDateFromQueryOptions(_queryOptions['sap-valid-at'])
58
- _['VALID-FROM'] = date
59
- _['VALID-TO'] = new Date(date.getTime() + _getTimeDelta(req.target, _queryOptions['sap-valid-at']))
58
+ const dateAsIsoString = _getDateFromQueryOptions(_queryOptions['sap-valid-at'])
59
+ _['VALID-FROM'] = dateAsIsoString
60
+
61
+ if (cds.env.features.precise_timestamps) {
62
+ const nanos = dateAsIsoString.slice(-5)
63
+ // we would lose the nano precision here, so we just cut it off before and attach it again here
64
+ _['VALID-TO'] =
65
+ normalizeTimestamp(
66
+ new Date(dateAsIsoString).getTime() + _getTimeDelta(req.target, _queryOptions['sap-valid-at'])
67
+ ).slice(0, -5) + nanos
68
+ } else {
69
+ _['VALID-TO'] = normalizeTimestamp(
70
+ new Date(dateAsIsoString).getTime() + _getTimeDelta(req.target, _queryOptions['sap-valid-at'])
71
+ )
72
+ }
60
73
  } else if (_queryOptions['sap-valid-from'] || _queryOptions['sap-valid-to']) {
61
- _['VALID-FROM'] = _getDateFromQueryOptions(_queryOptions['sap-valid-from']) || new Date('0001-01-01T00:00:00.000Z')
62
- _['VALID-TO'] = _getDateFromQueryOptions(_queryOptions['sap-valid-to']) || new Date('9999-12-31T23:59:59.999Z')
74
+ _['VALID-FROM'] = normalizeTimestamp(
75
+ _getDateFromQueryOptions(_queryOptions['sap-valid-from'] ?? normalizeTimestamp('0001-01-01T00:00:00.0000000Z'))
76
+ )
77
+ _['VALID-TO'] = normalizeTimestamp(
78
+ _getDateFromQueryOptions(_queryOptions['sap-valid-to'] ?? normalizeTimestamp('9999-12-31T23:59:59.9999999Z'))
79
+ )
63
80
  }
64
81
  }
65
82
 
@@ -45,9 +45,7 @@ ASSERT_FORMAT=Value "{0}" is not in specified format "{1}"
45
45
  ASSERT_DATA_TYPE=Value {0} is invalid according to type definition "{1}"
46
46
  ASSERT_ENUM=Value {0} is invalid according to enum declaration {{1}}
47
47
  ASSERT_NOT_NULL=Value is required
48
- ASSERT_REFERENCE_INTEGRITY=Reference integrity is violated for association "{0}"
49
48
  ASSERT_TARGET="Value doesn't exist"
50
- ASSERT_DEEP_ASSOCIATION=It is not allowed to modify sub documents in {0} Association "{1}"
51
49
 
52
50
  # db
53
51
  NO_DATABASE_CONNECTION=No database connection
@@ -83,7 +83,7 @@ function isPathToDraft(path, model) {
83
83
  for (const r of path) {
84
84
  if (r.id) {
85
85
  const isaIndex = r.where.findIndex(ele => ele.ref && ele.ref[0] === 'IsActiveEntity')
86
- if (isaIndex) draft = !r.where[isaIndex + 2].val
86
+ if (isaIndex !== -1) draft = !r.where[isaIndex + 2].val
87
87
  current = current ? definitions[current.elements[r.id].target] : definitions[r.id]
88
88
  } else {
89
89
  if (r === 'SiblingEntity') draft = !!draft