@sap/cds 6.8.4 → 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 +58 -5
  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
@@ -5,126 +5,75 @@ const { postProcess } = require('../../common/utils/postProcessing')
5
5
 
6
6
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
7
7
 
8
- // for getRestrictions()
9
- const { getNormalizedRestrictions, getApplicableRestrictions } = require('../../common/generic/auth/restrictions.js')
10
- const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
11
- const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
12
-
13
8
  /**
14
- * Generic Service Event Handler.
9
+ * Generic Application Service Provider
15
10
  */
16
11
  class ApplicationService extends cds.Service {
17
12
  init() {
18
- require('../../common/generic/auth').call(this, this)
19
- require('../../common/generic/etag').call(this, this)
20
- require('../../common/generic/input').call(this, this)
21
- require('../../common/generic/put').call(this, this)
22
- require('../../common/generic/temporal').call(this, this)
23
- require('../../common/generic/paging').call(this, this) // > paging must be executed before sorting
24
- require('../../common/generic/sorting').call(this, this)
13
+ const clazz = this.constructor,
14
+ { generics } = clazz
15
+ for (let each of generics) clazz[each].call(this)
16
+ return super.init()
17
+ }
25
18
 
26
- if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this, this)
19
+ static get generics() {
20
+ let generics = Reflect.getOwnPropertyDescriptor(this, '_generics')
21
+ if (generics) return generics.value
22
+ else {
23
+ const set = new Set(this.__proto__.generics)
24
+ for (let p of Reflect.ownKeys(this)) if (p.startsWith('handle_')) set.add(p)
25
+ Object.defineProperty(this, '_generics', { value: (generics = [...set]) })
26
+ }
27
+ return generics
28
+ }
27
29
 
28
- this.registerFioriHandlers(this)
29
- this.registerPersonalDataHandlers(this)
30
- this.registerCrudHandlers(this) // default .on handlers, have to go last
31
- return this
30
+ static handle_authorization() {
31
+ require('../../common/generic/auth').call(this)
32
32
  }
33
33
 
34
- /**
35
- * @param serviceImpl
36
- * @deprecated since version 1.11.0 - use Service.prepend instead
37
- */
38
- with(serviceImpl) {
39
- return this.prepend(serviceImpl)
34
+ static handle_etags() {
35
+ require('../../common/generic/etag').call(this)
40
36
  }
41
37
 
42
- /**
43
- * Registers custom handlers.
44
- * @param {string | object | Function} serviceImpl - init function to register custom handlers.
45
- */
46
- impl(serviceImpl) {
47
- if (typeof serviceImpl === 'string') serviceImpl = require(serviceImpl)
48
- return this.prepend(serviceImpl)
38
+ static handle_validations() {
39
+ require('../../common/generic/input').call(this)
49
40
  }
50
41
 
51
- registerPersonalDataHandlers() {
52
- return cds.env.features.audit_personal_data && require('../../audit/generic/personal').impl.call(this)
53
- }
54
-
55
- registerFioriHandlers() {
56
- if (cds.env.fiori.lean_draft) {
57
- const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
58
-
59
- for (const each of this.entities)
60
- if (each.drafts) {
61
- this.on('NEW', each.drafts, onNew)
62
- this.on('EDIT', each, onEdit)
63
- this.on('CANCEL', each.drafts, onCancel)
64
- this.on('draftPrepare', each.drafts, onPrepare)
65
- if (cds.env.fiori.draft_compat) {
66
- // register after read handlers to add `IsActiveEntity`,
67
- // so stakeholders have access to it when calling next()
68
-
69
- // to check if data contains a key value
70
- let _key
71
- for (const key in each.keys) {
72
- if (key === 'IsActiveEntity') continue
73
- _key = key
74
- break
75
- }
76
- const _addIsActiveEntity = (data, IsActiveEntity) => {
77
- if (!data) return
78
- if (Array.isArray(data)) return data.map(d => _addIsActiveEntity(d, IsActiveEntity))
79
- if (_key in data) data.IsActiveEntity = IsActiveEntity
80
- }
81
- this.on('READ', each, async (req, next) => {
82
- const data = await next()
83
- _addIsActiveEntity(data, !req.target?.isDraft)
84
- return data
85
- })
86
- }
87
- }
88
- } else return require('../../fiori/generic').impl.call(this)
89
- }
90
-
91
- registerCrudHandlers() {
92
- return require('../../common/generic/crud').impl.call(this)
42
+ static handle_stream_property() {
43
+ require('../../common/generic/stream').call(this)
93
44
  }
94
45
 
95
- /**
96
- * Returns the applicable restrictions for the current request as follows:
97
- * - null: unrestricted access
98
- * - []: no access
99
- * - [{ grant: '...', to: ['...'], where: '...' }, ...]: applicable restrictions with grant normalized to strings,
100
- * i.e., grant: ['CREATE', 'UPDATE'] in model becomes [{ grant: 'CREATE' }, { grant: 'UPDATE' }]
101
- * - Promise resovling to any of the above (needed for CAS overrides)
102
- *
103
- * @param {object} definition - then csn definition of an entity or an (un)bound action or function
104
- * @param {string} event - the event name
105
- * @param {import('../../../../lib/req/user')} user - the current user
106
- * @returns {Promise | Array | null}
107
- */
108
- getRestrictions(definition, event, user) {
109
- let restrictions = getNormalizedRestrictions(definition, this.model.definitions, event)
110
- if (!restrictions && (event in CRUD || !definition.parent)) {
111
- // > unrestricted entity or unbound
112
- return null
113
- }
114
- if (event in CRUD && restrictions.length && restrictions.every(r => r.grant !== '*' && !(r.grant in CRUD))) {
115
- // > only bounds are restricted
116
- return null
117
- }
118
- if (!(event in CRUD) && !restrictions && definition.parent) {
119
- // > bound without own restrictions -> get from parent
120
- restrictions = getNormalizedRestrictions(definition.parent, this.model.definitions, event)
121
- if (!restrictions) {
122
- // > unrestricted bound
123
- return null
124
- }
125
- }
126
- // return the applicable restrictions (grant and to fit to request and user)
127
- return getApplicableRestrictions(restrictions, event, user)
46
+ static handle_puts() {
47
+ require('../../common/generic/put').call(this)
48
+ }
49
+
50
+ static handle_temporal_data() {
51
+ require('../../common/generic/temporal').call(this)
52
+ }
53
+
54
+ static handle_localized_data() {
55
+ // TODO: can we move handling of localized data here?
56
+ }
57
+
58
+ static handle_managed_data() {
59
+ // TODO: can we move handling of managed data here?
60
+ }
61
+
62
+ static handle_paging() {
63
+ require('../../common/generic/paging').call(this) // > paging must be executed before sorting
64
+ require('../../common/generic/sorting').call(this)
65
+ }
66
+
67
+ static handle_code_ext() {
68
+ if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this)
69
+ }
70
+
71
+ static handle_fiori() {
72
+ require('../../fiori/draft').impl.call(this)
73
+ }
74
+
75
+ static handle_crud() {
76
+ require('../../common/generic/crud').impl.call(this)
128
77
  }
129
78
 
130
79
  // Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
@@ -139,7 +88,7 @@ class ApplicationService extends cds.Service {
139
88
  // - undefined/null in case of plain string queries
140
89
  if (this.model && _isSimpleCqnQuery(req.query)) {
141
90
  const q = resolveView(req.query, this.model, this)
142
- const t = findQueryTarget(q) || req.target
91
+ const t = findQueryTarget(q) || req.target // REVISIT: why is req.target not correct?
143
92
 
144
93
  // compat
145
94
  restoreLink(req)
@@ -156,6 +105,29 @@ class ApplicationService extends cds.Service {
156
105
 
157
106
  return super.handle(req)
158
107
  }
108
+
109
+ /**
110
+ * @param serviceImpl
111
+ * @deprecated since version 1.11.0 - use Service.prepend instead
112
+ */
113
+ with(serviceImpl) {
114
+ return this.prepend(serviceImpl)
115
+ }
116
+
117
+ /**
118
+ * Registers custom handlers.
119
+ * @param {string | object | Function} serviceImpl - init function to register custom handlers.
120
+ */
121
+ impl(serviceImpl) {
122
+ if (typeof serviceImpl === 'string') serviceImpl = require(serviceImpl)
123
+ return this.prepend(serviceImpl)
124
+ }
125
+ }
126
+
127
+ // NOTE: getRestrictions is VERY INOFFICIAL!!!
128
+ const { getRestrictions } = require('../../common/generic/auth/restrictions')
129
+ ApplicationService.prototype.getRestrictions = function (..._) {
130
+ return getRestrictions.call(this, ..._)
159
131
  }
160
132
 
161
133
  ApplicationService.prototype.isAppService = true
@@ -1,10 +1,8 @@
1
1
  const cds = require('../../../cds')
2
2
  // requesting logger without module on purpose!
3
3
  const LOG = cds.log()
4
-
5
4
  const { DRAFT_COLUMNS_UNION_MAP } = require('../../../common/constants/draft')
6
-
7
- const defaultSearchableType = 'cds.String'
5
+ const DEFAULT_SEARCHABLE_TYPE = 'cds.String'
8
6
 
9
7
  // REVISIT: Can we combine that with db/utils/columns.js?
10
8
  /**
@@ -18,6 +16,7 @@ const defaultSearchableType = 'cds.String'
18
16
  * @param [options.filterDraft=false] - indicates whether the draft columns should be filtered if the entity is draft enabled
19
17
  * @param [options.removeIgnore=false]
20
18
  * @param [options.filterVirtual=false]
19
+ * @param [options.keysOnly=false]
21
20
  * @returns {Array<object>} - array of columns
22
21
  */
23
22
  const getColumns = (
@@ -46,10 +45,12 @@ const _getSearchableColumns = entity => {
46
45
  const columns = getColumns(entity, columnsOptions)
47
46
  const cdsSearchTerm = '@cds.search'
48
47
  const cdsSearchKeys = []
48
+ const cdsSearchColumnMap = new Map()
49
+
49
50
  for (const key in entity) {
50
51
  if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
51
52
  }
52
- const cdsSearchColumnMap = new Map()
53
+
53
54
  let atLeastOneColumnIsSearchable = false
54
55
 
55
56
  // build a map of columns annotated with the @cds.search annotation
@@ -62,7 +63,7 @@ const _getSearchableColumns = entity => {
62
63
  continue
63
64
  }
64
65
 
65
- const annotationKey = cdsSearchTerm + '.' + columnName
66
+ const annotationKey = `${cdsSearchTerm}.${columnName}`
66
67
  const annotationValue = entity[annotationKey]
67
68
  if (annotationValue) atLeastOneColumnIsSearchable = true
68
69
  cdsSearchColumnMap.set(columnName, annotationValue)
@@ -71,21 +72,22 @@ const _getSearchableColumns = entity => {
71
72
  const searchableColumns = columns.filter(column => {
72
73
  const annotatedColumnValue = cdsSearchColumnMap.get(column.name)
73
74
 
74
- // A column is searchable if one of the following conditions evaluates to true.
75
- //
76
- // The @cds.search annotation is provided, and the column is annotated as searchable, e.g.:
77
- // @cds.search { column1: true } or just @cds.search { column1 }
75
+ // the element is searchable if it is annotated with the @cds.search, e.g.:
76
+ // `@cds.search { element1: true }` or `@cds.search { element1 }`
78
77
  if (annotatedColumnValue) return true
79
78
 
80
- // If at least one column is explicitly annotated as searchable, e.g.:
81
- // @cds.search { column1: true } or just @cds.search { column1 }
79
+ // if at least one element is explicitly annotated as searchable, e.g.:
80
+ // `@cds.search { element1: true }` or `@cds.search { element1 }`
82
81
  // and it is not the current column name, then it must be excluded from the search
83
82
  if (atLeastOneColumnIsSearchable) return false
84
83
 
85
- // - The @cds.search annotation is provided, the column name is not included, and the column
86
- // is typed as string.
87
- // - The @cds.search annotation is not provided, and the column is typed as string
88
- return annotatedColumnValue === undefined && column._type === defaultSearchableType
84
+ // the element is considered searchable if it is explicitly annotated as such or
85
+ // if it is not annotated and the column is typed as a string (excluding elements/elements expressions)
86
+ return (
87
+ annotatedColumnValue === undefined &&
88
+ column._type === DEFAULT_SEARCHABLE_TYPE &&
89
+ !entity?.query?.SELECT?.columns?.find(col => col.xpr && col.as === column.name)
90
+ )
89
91
  })
90
92
 
91
93
  // if the @cds.search annotation is provided -->
@@ -109,7 +111,8 @@ const _getSearchableColumns = entity => {
109
111
  if (!cds._deprecationWarningForDefaultSearchElement) {
110
112
  LOG._warn &&
111
113
  LOG.warn(
112
- 'Annotation "@Search.defaultSearchElement" is deprecated and will be removed in an upcoming release. Use "@cds.search" instead.'
114
+ 'Annotation "@Search.defaultSearchElement" is deprecated and will be removed in an upcoming release. ' +
115
+ 'Use "@cds.search" instead.'
113
116
  )
114
117
  cds._deprecationWarningForDefaultSearchElement = true
115
118
  }
@@ -137,8 +140,9 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
137
140
  cqn.SELECT.columns.length === 1 &&
138
141
  column.func === 'count' &&
139
142
  (column.as === '_counted_' || column.as === '$count')
140
- )
143
+ ) {
141
144
  return
145
+ }
142
146
 
143
147
  toBeSearched.push(column)
144
148
  return
@@ -146,7 +150,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
146
150
 
147
151
  const columnRef = column.ref
148
152
  if (columnRef) {
149
- if (entity.elements[columnRef[columnRef.length - 1]]?._type !== defaultSearchableType) return
153
+ if (entity.elements[columnRef[columnRef.length - 1]]?._type !== DEFAULT_SEARCHABLE_TYPE) return
150
154
  column = { ref: [...column.ref] }
151
155
  if (alias) column.ref.unshift(alias)
152
156
  toBeSearched.push(column)
@@ -154,7 +158,6 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
154
158
  })
155
159
  } else {
156
160
  toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
157
-
158
161
  if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
159
162
  toBeSearched = toBeSearched.map(c => {
160
163
  const col = { ref: [c] }
@@ -162,6 +165,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
162
165
  return col
163
166
  })
164
167
  }
168
+
165
169
  return toBeSearched
166
170
  }
167
171
 
@@ -154,10 +154,20 @@ const _skipToMany = (entity, prop) => {
154
154
  return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
155
155
  }
156
156
 
157
+ // Returns all property names from the new entry and add missing managed elements
158
+ const _propertiesAndManaged = (newEntry, entity) => {
159
+ return [
160
+ ...Object.getOwnPropertyNames(newEntry),
161
+ ...Object.keys(entity.elements).filter(
162
+ elementName => newEntry[elementName] === undefined && entity.elements[elementName]['@cds.on.update']
163
+ )
164
+ ]
165
+ }
166
+
157
167
  const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
158
168
  // On app-service layer, generated foreign keys are not enumerable,
159
169
  // include them here too.
160
- for (const prop of Object.getOwnPropertyNames(newEntry)) {
170
+ for (const prop of _propertiesAndManaged(newEntry, entity)) {
161
171
  if (keys.includes(prop)) {
162
172
  _addKeysToResult(result, prop, newEntry, oldEntry)
163
173
  continue
@@ -9,6 +9,7 @@ const { DRAFT_COLUMNS_MAP } = require('../../../common/constants/draft')
9
9
  const { cqn2cqn4sql, convertPathExpressionToWhere } = require('../../../common/utils/cqn2cqn4sql')
10
10
  const { revertData } = require('../../../common/utils/resolveView')
11
11
  const { removeIsActiveEntityRecursively } = require('../../../fiori/utils/where')
12
+ const { enrichDataWithKeysFromWhere } = require('../../../common/utils/keys')
12
13
 
13
14
  module.exports = class Differ {
14
15
  constructor(srv) {
@@ -37,8 +38,9 @@ module.exports = class Differ {
37
38
  }
38
39
 
39
40
  _diffDelete(req) {
40
- const { DELETE } = (req._ && req._.query) || req.query
41
- const query = SELECT.from(DELETE.from).columns(this._createSelectColumnsForDelete(req.target))
41
+ const { DELETE } = cds.env.fiori.lean_draft ? req.query : (req._ && req._.query) || req.query
42
+ const target = DELETE._transitions?.[DELETE._transitions.length - 1]?.target || req.target
43
+ const query = SELECT.from(DELETE.from).columns(this._createSelectColumnsForDelete(target))
42
44
  if (DELETE.where) query.where(DELETE.where)
43
45
 
44
46
  return cds
@@ -57,6 +59,7 @@ module.exports = class Differ {
57
59
  if (cds.db) await this._addPartialPersistentState(req)
58
60
  const newQuery = cqn2cqn4sql(req.query, this._srv.model)
59
61
  const combinedData = providedData || Object.assign({}, req.query.UPDATE.data || {}, req.query.UPDATE.with || {})
62
+ enrichDataWithKeysFromWhere(combinedData, req, this._srv)
60
63
  const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
61
64
  const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
62
65
  return compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
@@ -85,6 +88,8 @@ module.exports = class Differ {
85
88
  ? req.query.INSERT.entries[0]
86
89
  : req.query.INSERT.entries
87
90
 
91
+ enrichDataWithKeysFromWhere(originalData, req, this._srv)
92
+
88
93
  return compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
89
94
  }
90
95
 
@@ -263,11 +263,70 @@ const _checkFormatElement = (element, value, errors, key, pathSegments) => {
263
263
  }
264
264
  }
265
265
 
266
+ const getNormalizedDecimal = value => {
267
+ let val = `${value}`
268
+ const cgs = val.match(/^(\d*\.*\d*)e([+|-]*)(\d*)$/)
269
+ if (cgs) {
270
+ let [l, r = ''] = cgs[1].split('.')
271
+ const dir = cgs[2] || '+'
272
+ const exp = Number(cgs[3])
273
+ if (dir === '+') {
274
+ // move decimal point to the right
275
+ r = r.padEnd(exp, '0')
276
+ l += r.substring(0, exp)
277
+ r = r.slice(exp)
278
+ val = `${l}${r ? '.' + r : ''}`
279
+ } else {
280
+ // move decimal point to the left
281
+ l = l.padStart(exp, '0')
282
+ r = l.substring(0, exp) + r
283
+ l = l.slice(exp)
284
+ val = `${l ? l : '0'}.${r}`
285
+ }
286
+ }
287
+ return val
288
+ }
289
+
290
+ const _checkDecimalElement = (element, value, errors, key, path) => {
291
+ const { precision, scale } = element
292
+ let val = getNormalizedDecimal(value)
293
+ if (precision != null && scale != null) {
294
+ let isValid = true
295
+ if (!val.match(/\./)) val += '.0'
296
+
297
+ if (precision === scale) {
298
+ if (!val.match(new RegExp(`^-?0\\.\\d{0,${scale}}$`, 'g'))) isValid = false
299
+ } else if (scale === 0) {
300
+ if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.0{0,1}$`, 'g'))) isValid = false
301
+ } else if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.\\d{0,${scale}}$`, 'g'))) {
302
+ isValid = false
303
+ }
304
+
305
+ if (!isValid)
306
+ errors.push(
307
+ assertError(
308
+ { code: ASSERT_DATA_TYPE, args: [value, `Decimal(${precision},${scale})`] },
309
+ element,
310
+ value,
311
+ key,
312
+ path
313
+ )
314
+ )
315
+ } else if (precision != null) {
316
+ if (!val.match(new RegExp(`^-?\\d{1,${precision}}$`, 'g'))) {
317
+ errors.push(
318
+ assertError({ code: ASSERT_DATA_TYPE, args: [value, `Decimal(${precision})`] }, element, value, key, path)
319
+ )
320
+ }
321
+ }
322
+ }
323
+
266
324
  /**
267
325
  * @param {import('../../types/api').InputConstraints} constraints
268
326
  */
269
327
  const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }) => {
270
328
  if (!element) return errors
329
+
271
330
  let path
272
331
 
273
332
  if (pathSegmentsInfo?.length) path = templatePathSerializer(element.name || key, pathSegmentsInfo)
@@ -278,6 +337,9 @@ const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }
278
337
  _checkEnumElement(element, value, errors, key, path)
279
338
  _checkRangeElement(element, value, errors, key, path)
280
339
  _checkFormatElement(element, value, errors, key, path)
340
+
341
+ if (element.type === 'cds.Decimal') _checkDecimalElement(element, value, errors, key, path)
342
+
281
343
  return errors
282
344
  }
283
345
 
@@ -336,7 +398,7 @@ const assertTargets = async (assertMap, errors) => {
336
398
 
337
399
  targetsExistsResults.forEach((txPromise, index) => {
338
400
  const isPromiseRejected = txPromise.status === 'rejected'
339
- const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value === null) || isPromiseRejected
401
+ const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value == null) || isPromiseRejected
340
402
  if (!shouldAssertError) return
341
403
 
342
404
  const target = targets[index]
@@ -374,5 +436,6 @@ module.exports = {
374
436
  assertError,
375
437
  checkStaticElementByKey,
376
438
  assertNotNullError,
377
- assertTargets
439
+ assertTargets,
440
+ getNormalizedDecimal
378
441
  }
@@ -135,6 +135,7 @@ const _getWhereObj = (row, links) => {
135
135
  return links.reduce((res, currentLink) => {
136
136
  if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey) && row[currentLink.targetKey] !== null)
137
137
  res[currentLink.entityKey] = row[currentLink.targetKey]
138
+ else if (currentLink.targetVal !== undefined) res[currentLink.entityKey] = currentLink.targetVal
138
139
  return res
139
140
  }, {})
140
141
  }
@@ -10,7 +10,7 @@ const _getTarget = (ref, target, definitions) => {
10
10
  if (ref.length === 1) return definitions[ensureNoDraftsSuffix(target_.target)]
11
11
  return _getTarget(ref.slice(1), target_, definitions)
12
12
  }
13
- const target_ = target.elements[ref.join('_')]
13
+ const target_ = target.elements[ref.map(x => x.id || x).join('_')]
14
14
  return definitions[ensureNoDraftsSuffix(target_.target)]
15
15
  }
16
16
 
@@ -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