@sap/cds 6.8.3 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/CHANGELOG.md +61 -2
  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 +1 -1
  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 +1 -1
  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/srv-api.js +9 -3
  45. package/lib/srv/srv-dispatch.js +12 -9
  46. package/lib/srv/srv-models.js +4 -21
  47. package/lib/srv/srv-tx.js +15 -12
  48. package/lib/utils/cds-test.js +14 -9
  49. package/lib/utils/cds-utils.js +2 -12
  50. package/lib/utils/check-version.js +17 -0
  51. package/{bin/build → lib/utils}/csv-reader.js +23 -24
  52. package/libx/_runtime/auth/index.js +27 -23
  53. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +15 -72
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +0 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +33 -63
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +14 -18
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +15 -5
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +37 -40
  61. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +7 -1
  62. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +101 -38
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/AbstractError.js +5 -1
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +2 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +9 -8
  66. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +15 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +4 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +5 -2
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -123
  71. package/libx/_runtime/cds-services/services/Service.js +79 -107
  72. package/libx/_runtime/cds-services/services/utils/columns.js +23 -19
  73. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -1
  74. package/libx/_runtime/cds-services/services/utils/differ.js +7 -2
  75. package/libx/_runtime/cds-services/util/assert.js +65 -2
  76. package/libx/_runtime/common/composition/data.js +1 -0
  77. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  78. package/libx/_runtime/common/generic/auth/restrict.js +5 -10
  79. package/libx/_runtime/common/generic/auth/restrictions.js +40 -0
  80. package/libx/_runtime/common/generic/auth/utils.js +1 -2
  81. package/libx/_runtime/common/generic/crud.js +32 -16
  82. package/libx/_runtime/common/generic/etag.js +133 -104
  83. package/libx/_runtime/common/generic/input.js +6 -21
  84. package/libx/_runtime/common/generic/put.js +1 -1
  85. package/libx/_runtime/common/generic/stream.js +52 -0
  86. package/libx/_runtime/common/generic/temporal.js +25 -8
  87. package/libx/_runtime/common/i18n/messages.properties +0 -2
  88. package/libx/_runtime/common/utils/cqn.js +1 -1
  89. package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
  90. package/libx/_runtime/common/utils/csn.js +0 -51
  91. package/libx/_runtime/common/utils/etag.js +30 -0
  92. package/libx/_runtime/common/utils/keys.js +1 -1
  93. package/libx/_runtime/common/utils/normalizeTimestamp.js +25 -0
  94. package/libx/_runtime/common/utils/path.js +1 -1
  95. package/libx/_runtime/common/utils/resolveView.js +2 -1
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +6 -4
  97. package/libx/_runtime/common/utils/search2cqn4sql.js +12 -16
  98. package/libx/_runtime/common/utils/stream.js +140 -0
  99. package/libx/_runtime/common/utils/streamProp.js +29 -12
  100. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +0 -2
  101. package/libx/_runtime/db/generic/index.js +0 -2
  102. package/libx/_runtime/db/query/delete.js +2 -2
  103. package/libx/_runtime/db/query/insert.js +2 -2
  104. package/libx/_runtime/db/query/read.js +2 -2
  105. package/libx/_runtime/db/query/run.js +2 -2
  106. package/libx/_runtime/db/query/update.js +2 -2
  107. package/libx/_runtime/db/sql-builder/BaseBuilder.js +0 -6
  108. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +23 -12
  109. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +18 -6
  110. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -0
  111. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -7
  112. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +1 -0
  113. package/libx/_runtime/db/utils/normalizeTimeData.js +7 -3
  114. package/libx/_runtime/fiori/draft.js +2 -0
  115. package/libx/_runtime/fiori/generic/activate.js +8 -9
  116. package/libx/_runtime/fiori/generic/before.js +30 -20
  117. package/libx/_runtime/fiori/generic/cancel.js +5 -3
  118. package/libx/_runtime/fiori/generic/delete.js +5 -3
  119. package/libx/_runtime/fiori/generic/edit.js +7 -7
  120. package/libx/_runtime/fiori/generic/index.js +10 -16
  121. package/libx/_runtime/fiori/generic/new.js +5 -3
  122. package/libx/_runtime/fiori/generic/patch.js +11 -8
  123. package/libx/_runtime/fiori/generic/prepare.js +13 -6
  124. package/libx/_runtime/fiori/generic/read.js +12 -6
  125. package/libx/_runtime/fiori/lean-draft.js +207 -152
  126. package/libx/_runtime/fiori/utils/delete.js +10 -5
  127. package/libx/_runtime/fiori/utils/req.js +17 -5
  128. package/libx/_runtime/fiori/utils/stream.js +36 -0
  129. package/libx/_runtime/hana/Service.js +12 -9
  130. package/libx/_runtime/hana/conversion.js +10 -15
  131. package/libx/_runtime/hana/driver.js +2 -0
  132. package/libx/_runtime/hana/execute.js +28 -6
  133. package/libx/_runtime/hana/pool.js +36 -122
  134. package/libx/_runtime/hana/search2cqn4sql.js +34 -36
  135. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -6
  136. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -1
  137. package/libx/_runtime/messaging/enterprise-messaging.js +10 -58
  138. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  139. package/libx/_runtime/remote/Service.js +20 -1
  140. package/libx/_runtime/remote/utils/client.js +3 -5
  141. package/libx/_runtime/sqlite/Service.js +4 -6
  142. package/libx/_runtime/sqlite/conversion.js +3 -13
  143. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +9 -6
  144. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +6 -1
  145. package/libx/_runtime/sqlite/execute.js +5 -16
  146. package/libx/odata/afterburner.js +22 -6
  147. package/libx/odata/grammar.pegjs +6 -1
  148. package/libx/odata/parser.js +1 -1
  149. package/libx/rest/RestAdapter.js +16 -9
  150. package/libx/rest/RestRequest.js +1 -1
  151. package/libx/rest/middleware/input.js +2 -1
  152. package/libx/rest/middleware/operation.js +1 -0
  153. package/libx/rest/middleware/parse.js +3 -2
  154. package/libx/rest/middleware/payload.js +9 -8
  155. package/libx/rest/middleware/read.js +1 -0
  156. package/package.json +9 -16
  157. package/app/fiori/preview.js +0 -270
  158. package/app/fiori/routes.js +0 -59
  159. package/bin/build/buildTaskEngine.js +0 -360
  160. package/bin/build/buildTaskFactory.js +0 -283
  161. package/bin/build/buildTaskHandler.js +0 -241
  162. package/bin/build/buildTaskProvider.js +0 -22
  163. package/bin/build/buildTaskProviderFactory.js +0 -175
  164. package/bin/build/cds.js +0 -5
  165. package/bin/build/constants.js +0 -66
  166. package/bin/build/index.js +0 -58
  167. package/bin/build/provider/buildTaskHandlerEdmx.js +0 -82
  168. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +0 -131
  169. package/bin/build/provider/buildTaskHandlerInternal.js +0 -254
  170. package/bin/build/provider/buildTaskProviderInternal.js +0 -383
  171. package/bin/build/provider/fiori/index.js +0 -171
  172. package/bin/build/provider/hana/2migration.js +0 -179
  173. package/bin/build/provider/hana/index.js +0 -505
  174. package/bin/build/provider/hana/migrationtable.js +0 -472
  175. package/bin/build/provider/hana/template/.hdiconfig-haas +0 -163
  176. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +0 -137
  177. package/bin/build/provider/hana/template/.hdinamespace +0 -4
  178. package/bin/build/provider/hana/template/package.json +0 -12
  179. package/bin/build/provider/hana/template/undeploy.json +0 -5
  180. package/bin/build/provider/java/index.js +0 -111
  181. package/bin/build/provider/java-cf/index.js +0 -1
  182. package/bin/build/provider/mtx/index.js +0 -268
  183. package/bin/build/provider/mtx/resourcesTarBuilder.js +0 -95
  184. package/bin/build/provider/mtx-extension/index.js +0 -131
  185. package/bin/build/provider/mtx-sidecar/index.js +0 -137
  186. package/bin/build/provider/node-cf/index.js +0 -1
  187. package/bin/build/provider/nodejs/index.js +0 -192
  188. package/bin/build/util.js +0 -299
  189. package/bin/cds.js +0 -125
  190. package/bin/deploy/to-hana/cfUtil.js +0 -355
  191. package/bin/deploy/to-hana/gitUtil.js +0 -57
  192. package/bin/deploy/to-hana/hana.js +0 -306
  193. package/bin/deploy/to-hana/hdiDeployUtil.js +0 -153
  194. package/bin/deploy/to-hana/index.js +0 -16
  195. package/bin/deploy/to-hana/mtaUtil.js +0 -170
  196. package/bin/mtx/in-cds.js +0 -17
  197. package/bin/plugins.js +0 -32
  198. package/bin/run.js +0 -24
  199. package/bin/utils/log.js +0 -24
  200. package/bin/version.js +0 -178
  201. package/libx/_runtime/audit/Service.js +0 -222
  202. package/libx/_runtime/audit/generic/personal/access.js +0 -61
  203. package/libx/_runtime/audit/generic/personal/index.js +0 -56
  204. package/libx/_runtime/audit/generic/personal/modification.js +0 -132
  205. package/libx/_runtime/audit/generic/personal/utils.js +0 -186
  206. package/libx/_runtime/audit/utils/log.js +0 -23
  207. package/libx/_runtime/audit/utils/v2.js +0 -176
  208. package/libx/_runtime/db/data-conversion/timestamp.js +0 -9
  209. package/libx/_runtime/db/generic/integrity.js +0 -455
  210. package/srv/audit-log.cds +0 -87
  211. package/srv/mtx.cds +0 -2
  212. package/srv/mtx.js +0 -8
  213. /package/lib/{core → linked}/classes.js +0 -0
  214. /package/lib/{core → linked}/entities.js +0 -0
@@ -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
@@ -67,8 +67,8 @@ const convertPathExpressionToWhere = (fromClause, model, options) => {
67
67
  const currentEntityName = draft ? entityName && ensureDraftsSuffix(entityName) : entityName
68
68
  if (!currentEntityName) continue
69
69
  if (!draft && currentEntityName.endsWith('_drafts')) draft = true
70
- const tableAlias = `T${i}`
71
- const currentSelect = SELECT.from(`${currentEntityName} as ${tableAlias}`)
70
+ const tableAlias = options.tableAliasPrefix ? `${options.tableAliasPrefix}${i}` : `T${i}`
71
+ const currentSelect = SELECT.from({ ref: [currentEntityName], as: tableAlias })
72
72
 
73
73
  if (fromClause.ref[i].where) {
74
74
  currentSelect.where(addAliasToExpression(fromClause.ref[i].where, tableAlias))
@@ -969,13 +969,16 @@ const _convertUpdate = (query, model, options) => {
969
969
 
970
970
  if (alias) update.UPDATE.entity = { ref: [target], as: alias }
971
971
  if (where) update.where(where)
972
+
972
973
  const targetEntity = model.definitions[target]
974
+
973
975
  if (query.UPDATE.where) {
974
976
  update.where(addAliasToExpression(query.UPDATE.where, alias))
975
977
  _convertToOneEqNullInFilter(update.UPDATE, targetEntity)
976
978
  }
977
979
 
978
980
  if (!targetEntity) return update
981
+
979
982
  return resolveView(update, model, cds.db)
980
983
  }
981
984
 
@@ -39,56 +39,6 @@ const _getUps = (entity, model) => {
39
39
  return entity.set('__parents', ups)
40
40
  }
41
41
 
42
- const _ifDataSubject = (entity, role) => {
43
- return entity['@PersonalData.EntitySemantics'] === 'DataSubject' && entity['@PersonalData.DataSubjectRole'] === role
44
- }
45
-
46
- const _getDataSubjectUp = (role, model, entity, prev, next, result) => {
47
- for (const element of _getUps(entity, model)) {
48
- const me = { entity, relative: element.parent, element }
49
- if (prev) prev.next = me
50
- if (_ifDataSubject(element.parent, role)) {
51
- if (!result) result = { dataSubjectEntity: element.parent, subs: [] }
52
- result.subs.push(next || me)
53
- return result
54
- } else {
55
- // dfs is a must here
56
- result = _getDataSubjectUp(role, model, element.parent, me, next || me, result)
57
- }
58
- }
59
- return result
60
- }
61
-
62
- const _getDataSubjectDown = (role, entity, prev, next) => {
63
- const associations = Object.values(entity.associations || {}).filter(e => !e._isBacklink)
64
- for (const element of associations) {
65
- const me = { entity, relative: entity, element }
66
- if (_ifDataSubject(element._target, role)) {
67
- if (prev) prev.next = me
68
- return { dataSubjectEntity: element._target, subs: [next || me] }
69
- }
70
- }
71
- // bfs makes more sense here
72
- for (const element of associations) {
73
- const me = { entity, relative: entity, element }
74
- if (prev) prev.next = me
75
- const dataSubject = _getDataSubjectDown(role, element._target, me, next || me)
76
- if (dataSubject) return dataSubject
77
- }
78
- }
79
-
80
- const getDataSubject = (entity, model, role) => {
81
- const hash = '__dataSubject4' + role
82
- if (entity.own(hash)) return entity[hash]
83
- // entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
84
- // be always below or always above 'DataSubject' entity in CSN tree
85
- let dataSubject = _getDataSubjectUp(role, model, entity)
86
- if (!dataSubject) {
87
- dataSubject = _getDataSubjectDown(role, entity)
88
- }
89
- return entity.set(hash, dataSubject)
90
- }
91
-
92
42
  const _resolve = (edmName, model, namespace) => {
93
43
  const resolved = model._edmToCSNNameMap[namespace][edmName.replace(/\./g, '_')]
94
44
  // the edm name has an additional suffix 'Parameters' in case of views with parameters
@@ -249,7 +199,6 @@ module.exports = {
249
199
  getEtagElement,
250
200
  findCsnTargetFor,
251
201
  getElementDeep,
252
- getDataSubject,
253
202
  alias2ref,
254
203
  getComp2oneParents,
255
204
  prefixForStruct,