@sap/cds 5.4.3 → 5.5.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 (228) hide show
  1. package/CHANGELOG.md +239 -2
  2. package/apis/ql.d.ts +17 -15
  3. package/app/index.js +1 -1
  4. package/bin/build/buildTaskEngine.js +26 -42
  5. package/bin/build/buildTaskFactory.js +6 -10
  6. package/bin/build/buildTaskHandler.js +2 -4
  7. package/bin/build/buildTaskProvider.js +3 -1
  8. package/bin/build/buildTaskProviderFactory.js +9 -15
  9. package/bin/build/constants.js +15 -3
  10. package/bin/build/index.js +5 -4
  11. package/bin/build/mtaUtil.js +8 -11
  12. package/bin/build/provider/buildTaskHandlerEdmx.js +63 -6
  13. package/bin/build/provider/buildTaskHandlerInternal.js +2 -34
  14. package/bin/build/provider/buildTaskProviderInternal.js +16 -42
  15. package/bin/build/provider/fiori/index.js +13 -24
  16. package/bin/build/provider/hana/2migration.js +17 -15
  17. package/bin/build/provider/hana/2tabledata.js +52 -48
  18. package/bin/build/provider/hana/index.js +27 -25
  19. package/bin/build/provider/hana/migrationtable.js +91 -67
  20. package/bin/build/provider/java-cf/index.js +14 -24
  21. package/bin/build/provider/mtx/index.js +12 -14
  22. package/bin/build/provider/node-cf/index.js +18 -32
  23. package/bin/cds.js +5 -5
  24. package/bin/serve.js +29 -23
  25. package/bin/version.js +0 -1
  26. package/lib/compile/etc/_localized.js +4 -9
  27. package/lib/compile/for/sql.js +5 -2
  28. package/lib/compile/parse.js +25 -17
  29. package/lib/compile/to/srvinfo.js +2 -1
  30. package/lib/connect/bindings.js +2 -1
  31. package/lib/connect/index.js +48 -49
  32. package/lib/core/classes.js +1 -1
  33. package/lib/core/reflect.js +10 -2
  34. package/lib/deploy.js +26 -23
  35. package/lib/env/defaults.js +13 -6
  36. package/lib/env/index.js +73 -78
  37. package/lib/env/requires.js +38 -19
  38. package/lib/index.js +9 -10
  39. package/lib/lazy.js +2 -2
  40. package/lib/log/index.js +33 -45
  41. package/lib/log/service/index.js +2 -2
  42. package/lib/ql/CREATE.js +14 -9
  43. package/lib/ql/DELETE.js +6 -5
  44. package/lib/ql/DROP.js +12 -9
  45. package/lib/ql/INSERT.js +40 -16
  46. package/lib/ql/Query.js +67 -40
  47. package/lib/ql/SELECT.js +162 -127
  48. package/lib/ql/UPDATE.js +74 -42
  49. package/lib/ql/Whereable.js +77 -87
  50. package/lib/ql/index.js +36 -24
  51. package/lib/ql/parse.js +35 -0
  52. package/lib/req/context.js +44 -8
  53. package/lib/req/locale.js +7 -7
  54. package/lib/serve/Service-api.js +21 -14
  55. package/lib/serve/Service-dispatch.js +28 -12
  56. package/lib/serve/Transaction.js +22 -10
  57. package/lib/serve/index.js +16 -11
  58. package/lib/utils/axios.js +23 -16
  59. package/lib/utils/data.js +35 -0
  60. package/lib/utils/tests.js +27 -18
  61. package/libx/_runtime/audit/generic/personal/access.js +81 -0
  62. package/libx/_runtime/audit/generic/personal/constants.js +4 -0
  63. package/libx/_runtime/audit/generic/personal/index.js +50 -0
  64. package/libx/_runtime/audit/generic/personal/modification.js +138 -0
  65. package/libx/_runtime/audit/generic/personal/utils.js +186 -0
  66. package/libx/_runtime/audit/utils/v2.js +10 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +5 -5
  68. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -7
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +5 -7
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +5 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +2 -3
  72. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +4 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +7 -4
  74. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +59 -8
  75. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -1
  76. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +6 -10
  77. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +3 -46
  78. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +2 -5
  79. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/createToCQN.js +2 -2
  80. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +4 -3
  81. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  82. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +0 -1
  83. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +1 -1
  84. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +2 -2
  85. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +16 -18
  86. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/EdmEntityType.js +6 -3
  87. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/format/RepresentationKind.js +4 -1
  88. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
  89. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/SerializerFactory.js +15 -2
  90. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/OperationValidator.js +1 -0
  91. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  92. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +8 -1
  93. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +6 -1
  94. package/libx/_runtime/cds-services/adapter/odata-v4/utils/omitValues.js +12 -5
  95. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -7
  96. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +7 -7
  97. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +14 -18
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +13 -13
  99. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +0 -1
  100. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +2 -1
  101. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +2 -2
  102. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -4
  103. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +4 -2
  104. package/libx/_runtime/cds-services/services/Service.js +40 -5
  105. package/libx/_runtime/cds-services/services/utils/columns.js +13 -7
  106. package/libx/_runtime/cds-services/services/utils/compareJson.js +88 -4
  107. package/libx/_runtime/cds-services/services/utils/differ.js +24 -6
  108. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +2 -2
  109. package/libx/_runtime/common/composition/data.js +66 -63
  110. package/libx/_runtime/common/composition/delete.js +97 -71
  111. package/libx/_runtime/common/composition/index.js +2 -1
  112. package/libx/_runtime/common/composition/insert.js +34 -11
  113. package/libx/_runtime/common/composition/tree.js +119 -92
  114. package/libx/_runtime/common/composition/update.js +12 -1
  115. package/libx/_runtime/common/composition/utils.js +1 -3
  116. package/libx/_runtime/common/constants/draft.js +12 -1
  117. package/libx/_runtime/common/generic/auth.js +53 -31
  118. package/libx/_runtime/common/generic/crud.js +14 -13
  119. package/libx/_runtime/common/generic/input.js +23 -26
  120. package/libx/_runtime/common/generic/put.js +1 -1
  121. package/libx/_runtime/common/generic/sorting.js +16 -16
  122. package/libx/_runtime/common/i18n/index.js +1 -1
  123. package/libx/_runtime/common/i18n/messages.properties +4 -0
  124. package/libx/_runtime/common/utils/backlinks.js +12 -5
  125. package/libx/_runtime/common/utils/cqn.js +6 -1
  126. package/libx/_runtime/common/utils/cqn2cqn4sql.js +123 -108
  127. package/libx/_runtime/common/utils/csn.js +56 -4
  128. package/libx/_runtime/common/utils/data.js +0 -37
  129. package/libx/_runtime/common/utils/enrichWithKeysFromWhere.js +1 -1
  130. package/libx/_runtime/common/utils/entityFromCqn.js +7 -24
  131. package/libx/_runtime/common/utils/foreignKeyPropagations.js +39 -7
  132. package/libx/_runtime/common/utils/generateOnCond.js +11 -12
  133. package/libx/_runtime/common/utils/onlyKeysRemain.js +10 -0
  134. package/libx/_runtime/common/utils/path.js +35 -0
  135. package/libx/_runtime/common/utils/postProcessing.js +86 -0
  136. package/libx/_runtime/common/utils/quotingStyles.js +37 -26
  137. package/libx/_runtime/common/utils/resolveView.js +227 -173
  138. package/libx/_runtime/common/utils/rewriteAsterisk.js +46 -26
  139. package/libx/_runtime/common/utils/structured.js +13 -13
  140. package/libx/_runtime/common/utils/template.js +10 -5
  141. package/libx/_runtime/common/utils/templateDelimiter.js +1 -0
  142. package/libx/_runtime/common/utils/templateProcessor.js +28 -72
  143. package/libx/_runtime/common/utils/union.js +31 -0
  144. package/libx/_runtime/common/utils/unionCqnTemplate.js +184 -0
  145. package/libx/_runtime/db/Service.js +1 -1
  146. package/libx/_runtime/db/data-conversion/timestamp.js +2 -9
  147. package/libx/_runtime/db/expand/expandCQNToJoin.js +204 -297
  148. package/libx/_runtime/db/expand/index.js +3 -3
  149. package/libx/_runtime/db/expand/rawToExpanded.js +36 -7
  150. package/libx/_runtime/db/generic/index.js +1 -1
  151. package/libx/_runtime/db/generic/input.js +5 -7
  152. package/libx/_runtime/db/generic/integrity.js +1 -1
  153. package/libx/_runtime/db/generic/rewrite.js +2 -10
  154. package/libx/_runtime/db/generic/update.js +13 -5
  155. package/libx/_runtime/db/generic/virtual.js +22 -58
  156. package/libx/_runtime/db/query/delete.js +7 -4
  157. package/libx/_runtime/db/query/insert.js +6 -4
  158. package/libx/_runtime/db/query/read.js +21 -8
  159. package/libx/_runtime/db/query/run.js +4 -1
  160. package/libx/_runtime/db/query/update.js +5 -4
  161. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +35 -2
  162. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +17 -2
  163. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -5
  164. package/libx/_runtime/db/sql-builder/ReferenceBuilder.js +10 -0
  165. package/libx/_runtime/db/sql-builder/SelectBuilder.js +35 -24
  166. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +14 -4
  167. package/libx/_runtime/db/sql-builder/arrayed.js +4 -0
  168. package/libx/_runtime/db/utils/deep.js +8 -0
  169. package/libx/_runtime/db/utils/generateAliases.js +2 -1
  170. package/libx/_runtime/fiori/generic/activate.js +19 -15
  171. package/libx/_runtime/fiori/generic/before.js +3 -11
  172. package/libx/_runtime/fiori/generic/cancel.js +1 -1
  173. package/libx/_runtime/fiori/generic/delete.js +3 -1
  174. package/libx/_runtime/fiori/generic/edit.js +12 -2
  175. package/libx/_runtime/fiori/generic/new.js +5 -5
  176. package/libx/_runtime/fiori/generic/patch.js +0 -18
  177. package/libx/_runtime/fiori/generic/read.js +261 -205
  178. package/libx/_runtime/fiori/utils/delete.js +36 -7
  179. package/libx/_runtime/fiori/utils/handler.js +43 -44
  180. package/libx/_runtime/fiori/utils/where.js +30 -15
  181. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +4 -6
  182. package/libx/_runtime/hana/execute.js +3 -3
  183. package/libx/_runtime/hana/localized.js +4 -4
  184. package/libx/_runtime/hana/pool.js +29 -14
  185. package/libx/_runtime/hana/search2cqn4sql.js +2 -1
  186. package/libx/_runtime/hana/searchToContains.js +18 -14
  187. package/libx/_runtime/index.js +0 -5
  188. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +13 -5
  189. package/libx/_runtime/messaging/common-utils/naming-conventions.js +4 -1
  190. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +31 -19
  191. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  192. package/libx/_runtime/messaging/enterprise-messaging.js +6 -4
  193. package/libx/_runtime/messaging/service.js +7 -6
  194. package/libx/_runtime/odata/cqn2odata.js +110 -43
  195. package/libx/_runtime/odata/index.js +26 -48
  196. package/libx/_runtime/odata/odata2cqn.js +1 -6154
  197. package/libx/_runtime/odata/odata2cqn.pegjs +559 -0
  198. package/libx/_runtime/odata/readToCqn.js +94 -64
  199. package/libx/_runtime/remote/Service.js +74 -21
  200. package/libx/_runtime/remote/cqn2odata/index.js +1 -5
  201. package/libx/_runtime/remote/utils/client.js +24 -101
  202. package/libx/_runtime/remote/utils/dataConversion.js +27 -12
  203. package/libx/_runtime/sqlite/Service.js +3 -5
  204. package/libx/_runtime/sqlite/execute.js +33 -27
  205. package/libx/_runtime/sqlite/localized.js +12 -7
  206. package/libx/_runtime/types/api.js +10 -0
  207. package/package.json +2 -2
  208. package/server.js +16 -2
  209. package/lib/ql/grammar.pegjs +0 -208
  210. package/lib/ql/parser.js +0 -1
  211. package/lib/ql/rt/DELETE.js +0 -29
  212. package/lib/ql/rt/INSERT.js +0 -23
  213. package/lib/ql/rt/Query.js +0 -84
  214. package/lib/ql/rt/SELECT.js +0 -174
  215. package/lib/ql/rt/UPDATE.js +0 -119
  216. package/lib/ql/rt/_helpers.js +0 -91
  217. package/lib/ql/rt/index.js +0 -32
  218. package/libx/_runtime/audit/generic/personal.js +0 -260
  219. package/libx/_runtime/cds-services/statements/BaseStatement.js +0 -72
  220. package/libx/_runtime/cds-services/statements/Create.js +0 -57
  221. package/libx/_runtime/cds-services/statements/Delete.js +0 -33
  222. package/libx/_runtime/cds-services/statements/Drop.js +0 -42
  223. package/libx/_runtime/cds-services/statements/Insert.js +0 -201
  224. package/libx/_runtime/cds-services/statements/Select.js +0 -826
  225. package/libx/_runtime/cds-services/statements/Update.js +0 -181
  226. package/libx/_runtime/cds-services/statements/Where.js +0 -726
  227. package/libx/_runtime/cds-services/statements/index.js +0 -25
  228. package/libx/_runtime/common/generic/resolve-mock.js +0 -9
@@ -0,0 +1,186 @@
1
+ const cds = require('../../../cds')
2
+
3
+ const { getDataSubject } = require('../../../common/utils/csn')
4
+ const { getBackLinks, isSelfManaged } = require('../../../common/utils/backlinks')
5
+
6
+ const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 }
7
+ const ASPECTS = { CREATE: '_auditCreate', READ: '_auditRead', UPDATE: '_auditUpdate', DELETE: '_auditDelete' }
8
+
9
+ const getMapKeyForCurrentRequest = req => {
10
+ // running in srv or db layer? -> srv's req.query used as key of diff and logs maps at req.context
11
+ return req._tx.constructor.name.match(/Database$/i) ? req._.query : req.query
12
+ }
13
+
14
+ const getRootEntity = element => {
15
+ let entity = element.parent
16
+ while (entity.kind !== 'entity') entity = entity.parent
17
+ return entity
18
+ }
19
+
20
+ const getPick = event => {
21
+ return (element, target) => {
22
+ if (!target[ASPECTS[event]]) return
23
+ const categories = []
24
+ if (!element.isAssociation && element.key) categories.push('ObjectID')
25
+ if (
26
+ element.parent['@PersonalData.EntitySemantics'] === 'DataSubject' &&
27
+ element['@PersonalData.FieldSemantics'] === 'DataSubjectID'
28
+ )
29
+ categories.push('DataSubjectID')
30
+ if (event in WRITE && element['@PersonalData.IsPotentiallyPersonal']) categories.push('IsPotentiallyPersonal')
31
+ if (element['@PersonalData.IsPotentiallySensitive']) categories.push('IsPotentiallySensitive')
32
+ if (categories.length) return { categories }
33
+ }
34
+ }
35
+
36
+ const _getHash = (entity, row) => {
37
+ return `${entity.name}(${Object.keys(entity.keys)
38
+ .map(k => `${k}=${row[k]}`)
39
+ .join(',')})`
40
+ }
41
+
42
+ const createLogEntry = (logs, entity, row) => {
43
+ const hash = _getHash(entity, row)
44
+ let log = logs[hash]
45
+ if (!log) {
46
+ logs[hash] = {
47
+ dataObject: { type: entity.name, id: [] },
48
+ dataSubject: { id: [], role: entity['@PersonalData.DataSubjectRole'] },
49
+ attributes: [],
50
+ attachments: []
51
+ }
52
+ log = logs[hash]
53
+ }
54
+ return log
55
+ }
56
+
57
+ const addObjectID = (log, row, key) => {
58
+ if (!log.dataObject.id.find(ele => ele.keyName === key)) log.dataObject.id.push({ keyName: key, value: row[key] })
59
+ }
60
+
61
+ const addDataSubject = (log, row, key, entity) => {
62
+ if (!log.dataSubject.type) log.dataSubject.type = entity.name
63
+ if (!log.dataSubject.id.find(ele => ele.key === key)) {
64
+ const value = row[key] || (row._old && row._old[key])
65
+ log.dataSubject.id.push({ keyName: key, value })
66
+ }
67
+ }
68
+
69
+ const _addKeysToWhere = (child, row) => {
70
+ const keysWithValue = []
71
+ Object.keys(child.keys).forEach(el => {
72
+ if (keysWithValue.length > 0) keysWithValue.push('and')
73
+ keysWithValue.push({ ref: [child.name, el] }, '=', {
74
+ val: row[el]
75
+ })
76
+ })
77
+ return keysWithValue
78
+ }
79
+
80
+ const _addForeignKeysToWhere = (parent, child, assoc) => {
81
+ const foreignKeys = []
82
+ const backlinks = getBackLinks(assoc)
83
+ let parentName, childName
84
+ if (assoc.isComposition && assoc.is2one && !assoc.on) {
85
+ // look for foreign keys in parent
86
+ parentName = parent.name
87
+ childName = child.name
88
+ } else if (assoc.is2many && isSelfManaged(assoc)) {
89
+ // look for foreign keys in child
90
+ parentName = child.name
91
+ childName = parent.name
92
+ }
93
+
94
+ backlinks.forEach(el => {
95
+ if (foreignKeys.length > 0) foreignKeys.push('and')
96
+ foreignKeys.push({ ref: [parentName, el.entityKey] }, '=', {
97
+ ref: [childName, el.targetKey]
98
+ })
99
+ })
100
+ return foreignKeys
101
+ }
102
+
103
+ const _buildWhere = (child, dataSubjectInfo, row) => {
104
+ const where = []
105
+ where.push(
106
+ ..._addForeignKeysToWhere(dataSubjectInfo.entity, child, dataSubjectInfo.assoc),
107
+ 'and',
108
+ ..._addKeysToWhere(child, row)
109
+ )
110
+ return where
111
+ }
112
+
113
+ const _buildSubSelect = (child, dataSubjectInfo, row) => {
114
+ let previousCqn
115
+ const childCqn = SELECT.from(child.name)
116
+ .columns(Object.keys(child.keys))
117
+ .where(_buildWhere(child, dataSubjectInfo.previous[0] ? dataSubjectInfo.previous[0] : dataSubjectInfo, row))
118
+ if (dataSubjectInfo.previous.length > 0) {
119
+ let currentCQN
120
+
121
+ dataSubjectInfo.previous.forEach((el, i) => {
122
+ const nextEl = dataSubjectInfo.previous[i + 1]
123
+ const args = nextEl
124
+ ? [nextEl.entity, el.entity, nextEl.assoc]
125
+ : [dataSubjectInfo.entity, el.entity, dataSubjectInfo.assoc]
126
+
127
+ currentCQN = SELECT.from(el.entity.name)
128
+ .columns(Object.keys(el.entity.keys))
129
+ .where([..._addForeignKeysToWhere(...args), 'and', 'exists', previousCqn || childCqn])
130
+
131
+ previousCqn = currentCQN
132
+ })
133
+ }
134
+ return previousCqn || childCqn
135
+ }
136
+
137
+ const _getDataSubjectIdPromise = (child, dataSubjectInfo, row, req) => {
138
+ const root = dataSubjectInfo.entity
139
+ const cqn = SELECT.from(root.name)
140
+ .columns(Object.keys(root.keys))
141
+ .where(['exists', _buildSubSelect(child, dataSubjectInfo, row)])
142
+ return cds
143
+ .tx(req)
144
+ .run(cqn)
145
+ .then(res => {
146
+ const id = []
147
+ for (const k in res[0]) id.push({ keyName: k, value: res[0][k] })
148
+ return id
149
+ })
150
+ }
151
+
152
+ const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
153
+ const role = entity['@PersonalData.DataSubjectRole']
154
+
155
+ const dataSubjectInfo = getDataSubject(entity, model, role)
156
+
157
+ log.dataSubject.type = dataSubjectInfo.entity.name
158
+
159
+ /*
160
+ * for each req (cf. $batch with atomicity) and data subject role (e.g., customer vs supplier),
161
+ * store (in audit data structure at context) and reuse a single promise to look up the respective data subject
162
+ */
163
+ const mapKey = getMapKeyForCurrentRequest(req)
164
+ if (!req.context._audit.dataSubjects) req.context._audit.dataSubjects = new Map()
165
+ if (!req.context._audit.dataSubjects.has(mapKey)) req.context._audit.dataSubjects.set(mapKey, new Map())
166
+ const map = req.context._audit.dataSubjects.get(mapKey)
167
+ if (map.has(role)) log.dataSubject.id = map.get(role)
168
+ else map.set(role, _getDataSubjectIdPromise(entity, dataSubjectInfo, row, req))
169
+ }
170
+
171
+ const resolveDataSubjectPromises = async logs => {
172
+ const dataSubjPromise = logs.filter(el => el.dataSubject.id instanceof Promise)
173
+ const res = await Promise.all(dataSubjPromise.map(el => el.dataSubject.id))
174
+ dataSubjPromise.forEach((el, i) => (el.dataSubject.id = res[i]))
175
+ }
176
+
177
+ module.exports = {
178
+ getMapKeyForCurrentRequest,
179
+ getRootEntity,
180
+ getPick,
181
+ createLogEntry,
182
+ addObjectID,
183
+ addDataSubject,
184
+ addDataSubjectForDetailsEntity,
185
+ resolveDataSubjectPromises
186
+ }
@@ -5,16 +5,22 @@ const { getObjectAndDataSubject, getAttributeToLog } = require('./log')
5
5
 
6
6
  function connect(credentials) {
7
7
  return new Promise((resolve, reject) => {
8
+ let auditLogging
9
+ try {
10
+ auditLogging = require('@sap/audit-logging')
11
+ } catch (e) {
12
+ LOG._warn &&
13
+ LOG.warn('Unable to require module @sap/audit-logging. Make sure it is installed if audit logging is required.')
14
+ return resolve()
15
+ }
8
16
  try {
9
- const auditLogging = require('@sap/audit-logging')
10
17
  auditLogging.v2(credentials, function (err, auditLog) {
11
18
  if (err) return reject(err)
12
19
  resolve(auditLog)
13
20
  })
14
21
  } catch (e) {
15
- LOG._warn &&
16
- LOG.warn('Unable to require module @sap/audit-logging. Make sure it is installed if audit logging is required.')
17
- resolve()
22
+ LOG._warn && LOG.warn('Unable to initialize audit-logging client with error:', e)
23
+ return resolve()
18
24
  }
19
25
  })
20
26
  }
@@ -3,16 +3,16 @@ const LOG = cds.log('odata')
3
3
 
4
4
  const OData = require('./OData')
5
5
 
6
- function _createNewService(csn, defaultOptions) {
6
+ function _createNewService(name, csn, defaultOptions) {
7
7
  const reflectedModel = cds.linked(cds.compile.for.odata(csn))
8
8
  const options = Object.assign({}, defaultOptions, { reflectedModel })
9
9
 
10
- const service = new cds.ApplicationService(csn, options)
10
+ const service = new cds.ApplicationService(name, csn, options)
11
11
  service.init()
12
12
  if (options.impl) service.impl(options.impl)
13
13
  service._isExtended = true
14
14
 
15
- const edm = cds.compile.to.edm(csn, { service: service.options.service })
15
+ const edm = cds.compile.to.edm(csn, { service: name })
16
16
  const odataService = new OData(edm, csn, options)
17
17
  odataService.addCDSServiceToChannel(service)
18
18
 
@@ -43,7 +43,7 @@ class Dispatcher {
43
43
 
44
44
  const csn = await cds.mtx.getCsn(tenant)
45
45
 
46
- resolve(_createNewService(csn, this._odata._options))
46
+ resolve(_createNewService(this._odata._cdsService.definition.name, csn, this._odata._options))
47
47
  } catch (e) {
48
48
  reject(e)
49
49
  }
@@ -68,7 +68,7 @@ class Dispatcher {
68
68
  const toggles = (req.features && Object.keys(req.features)) || []
69
69
  const csn = await this._mps.csn(tenant, 'dummy', toggles)
70
70
 
71
- resolve(_createNewService(csn, this._odata._options))
71
+ resolve(_createNewService(this._odata._cdsService.definition.name, csn, this._odata._options))
72
72
  } catch (e) {
73
73
  reject(e)
74
74
  }
@@ -102,7 +102,6 @@ class OData {
102
102
  * @param {object} [options] - optional object with options.
103
103
  * @param {object} [options.logger] - optional logger object to be used in the odata library.
104
104
  * @param {string} [options.logLevel] - optional log level to be used according to winston/npm specification.
105
- * @param {string} [options.service] - Service name as specified in CSN.
106
105
  * @param {boolean} [options.crashOnError] - Application should crash on error. Defaults to true.
107
106
  *
108
107
  * @throws Error in case no or an invalid csn model is provided.
@@ -214,14 +213,14 @@ class OData {
214
213
  })
215
214
 
216
215
  this._odataService.use(LOCALE_NEGOTIATOR, _language)
217
- this._odataService.use(METADATA_HANDLER, _metadata(cdsService, this._options))
216
+ this._odataService.use(METADATA_HANDLER, _metadata(cdsService))
218
217
 
219
- this._odataService.use(DATA_CREATE_HANDLER, _create(cdsService, this._options))
220
- this._odataService.use(DATA_READ_HANDLER, _read(cdsService, this._options))
221
- this._odataService.use(DATA_UPDATE_HANDLER, _update(cdsService, this._options))
222
- this._odataService.use(DATA_DELETE_HANDLER, _delete(cdsService, this._options))
218
+ this._odataService.use(DATA_CREATE_HANDLER, _create(cdsService))
219
+ this._odataService.use(DATA_READ_HANDLER, _read(cdsService))
220
+ this._odataService.use(DATA_UPDATE_HANDLER, _update(cdsService))
221
+ this._odataService.use(DATA_DELETE_HANDLER, _delete(cdsService))
223
222
 
224
- this._odataService.use(ACTION_EXECUTE_HANDLER, _action(cdsService, this._options))
223
+ this._odataService.use(ACTION_EXECUTE_HANDLER, _action(cdsService))
225
224
  }
226
225
 
227
226
  // _startPerfMeasurementOData (req) {
@@ -11,7 +11,8 @@ const { actionAndFunctionQueries, getActionOrFunctionReturnType } = require('../
11
11
  const { validateResourcePath } = require('../utils/request')
12
12
  const readAfterWrite = require('../utils/readAfterWrite')
13
13
  const { setStatusCodeAndHeader, getKeyProperty } = require('../../../../fiori/utils/handler')
14
- const { toODataResult, getVirtualsFromResult, postProcess } = require('../utils/result')
14
+ const { toODataResult, postProcess } = require('../utils/result')
15
+ const { mergeJson } = require('../../../services/utils/compareJson')
15
16
 
16
17
  /*
17
18
  * Get the returns object for the (un)bound action from CSN.
@@ -42,9 +43,7 @@ const _postProcessDraftActivate = async (req, result, service) => {
42
43
  // update req.data (keys needed in readAfterWrite)
43
44
  req.data = result
44
45
  const dataInDb = await readAfterWrite(req, service)
45
- // augment data in db so values of virtual properties are kept
46
- const virtuals = getVirtualsFromResult(req.target, result)
47
- result = Object.assign(dataInDb[0] || result, virtuals)
46
+ if (dataInDb.length) result = mergeJson(dataInDb[0], result, req.target)
48
47
 
49
48
  // add static draft columns
50
49
  result.IsActiveEntity = true
@@ -74,14 +73,13 @@ const _postProcess = async (req, odataReq, odataRes, tx, result) => {
74
73
  * The handler that will be registered with odata-v4.
75
74
  *
76
75
  * @param {import('../../../services/Service')} service
77
- * @param {object} options
78
76
  * @returns {function}
79
77
  */
80
- const action = (service, options) => {
78
+ const action = service => {
81
79
  return async (odataReq, odataRes, next) => {
82
80
  let req
83
81
  try {
84
- validateResourcePath(odataReq, options, service.model)
82
+ validateResourcePath(odataReq, service)
85
83
  req = new ODataRequest(ACTION_EXECUTE_HANDLER, service, odataReq, odataRes)
86
84
  } catch (e) {
87
85
  return next(e)
@@ -9,20 +9,20 @@ const { getSapMessages } = require('../../../../common/error/frontend')
9
9
  const { validateResourcePath } = require('../utils/request')
10
10
  const { isReturnMinimal } = require('../utils/handlerUtils')
11
11
  const readAfterWrite = require('../utils/readAfterWrite')
12
- const { toODataResult, getVirtualsFromResult, postProcess } = require('../utils/result')
12
+ const { toODataResult, postProcess } = require('../utils/result')
13
+ const { mergeJson } = require('../../../services/utils/compareJson')
13
14
 
14
15
  /**
15
16
  * The handler that will be registered with odata-v4.
16
17
  *
17
18
  * @param {import('../../../services/Service')} service
18
- * @param {object} options
19
19
  * @returns {function}
20
20
  */
21
- const create = (service, options) => {
21
+ const create = service => {
22
22
  return async (odataReq, odataRes, next) => {
23
23
  let req
24
24
  try {
25
- validateResourcePath(odataReq, options, service.model)
25
+ validateResourcePath(odataReq, service)
26
26
  req = new ODataRequest(DATA_CREATE_HANDLER, service, odataReq, odataRes)
27
27
  } catch (e) {
28
28
  return next(e)
@@ -40,9 +40,7 @@ const create = (service, options) => {
40
40
  // REVISIT: find better solution
41
41
  if (req._.readAfterWrite) {
42
42
  const dataInDb = await readAfterWrite(req, service)
43
- // augment data in db so values of virtual properties are kept
44
- const virtuals = getVirtualsFromResult(req.target, result)
45
- result = Object.assign(dataInDb[0] || result, virtuals)
43
+ if (dataInDb.length) result = mergeJson(dataInDb[0], result, req.target)
46
44
  }
47
45
 
48
46
  postProcess(req, odataRes, service, result)
@@ -12,14 +12,13 @@ const { validateResourcePath } = require('../utils/request')
12
12
  * The handler that will be registered with odata-v4.
13
13
  *
14
14
  * @param {import('../../../services/Service')} service
15
- * @param {object} options
16
15
  * @returns {function}
17
16
  */
18
- const del = (service, options) => {
17
+ const del = service => {
19
18
  return async (odataReq, odataRes, next) => {
20
19
  let req
21
20
  try {
22
- validateResourcePath(odataReq, options, service.model)
21
+ validateResourcePath(odataReq, service)
23
22
  req = new ODataRequest(DATA_DELETE_HANDLER, service, odataReq, odataRes)
24
23
  } catch (e) {
25
24
  return next(e)
@@ -112,6 +112,10 @@ const getErrorHandler = (crashOnError = true, srv) => {
112
112
  err = _betterOkraError(err)
113
113
  }
114
114
 
115
+ // add content id if not generated by okra ("~...")
116
+ const contentId = odataReq.getOdataRequestId()
117
+ if (contentId && !contentId.match(/^~/)) err['@Core.ContentID'] = contentId
118
+
115
119
  const { error, statusCode } = normalizeError(err, req)
116
120
 
117
121
  next(null, Object.assign(error, { statusCode }))
@@ -29,10 +29,9 @@ const _get4Toggles = async (tenant, locale, service, req) => {
29
29
  * Provide localized metadata handler.
30
30
  *
31
31
  * @param {object} service
32
- * @param {object} options
33
32
  * @returns {Function}
34
33
  */
35
- const metadata = (service, options) => {
34
+ const metadata = service => {
36
35
  return async (odataReq, odataRes, next) => {
37
36
  try {
38
37
  const req = odataReq.getIncomingRequest()
@@ -51,8 +50,12 @@ const metadata = (service, options) => {
51
50
  }
52
51
 
53
52
  if (!edmx) {
54
- // REVISIT: why options.service instead of service.name?
55
- edmx = cds.localize(service.model, locale, cds.compile.to.edmx(service.model, { service: options.service }))
53
+ edmx = cds.localize(
54
+ service.model,
55
+ locale,
56
+ // REVISIT: we could cache this in a weak map
57
+ cds.compile.to.edmx(service.model, { service: service.definition.name })
58
+ )
56
59
  }
57
60
 
58
61
  return next(null, toODataResult(edmx))
@@ -20,7 +20,7 @@ const { validateResourcePath } = require('../utils/request')
20
20
  const { toODataResult, postProcess } = require('../utils/result')
21
21
  const { isStreaming, getStreamProperties } = require('../utils/stream')
22
22
  const { resolveStructuredName } = require('../utils/handlerUtils')
23
- const { getVirtuals, postProcessVirtuals } = require('../../../../db/generic/virtual')
23
+ const { ensureNoDraftsSuffix } = require('../../../../common/utils/draft')
24
24
 
25
25
  /**
26
26
  * Checks whether a bound function or function import is invoked.
@@ -225,7 +225,7 @@ const _readEntityOrProperty = async (tx, req, segments) => {
225
225
  const name = resolveStructuredName(segments, segments.length - 2)
226
226
  const res = _getResult(name, result[0])
227
227
 
228
- const odataResult = toODataResult(res[propertyElement.getName()])
228
+ const odataResult = toODataResult(typeof res === 'object' ? res[propertyElement.getName()] : res)
229
229
  if (req.target._etag) odataResult['*@odata.etag'] = res[req.target._etag]
230
230
 
231
231
  // property is read via a to one association and last segment is not $value
@@ -373,6 +373,17 @@ const _readAndTransform = (tx, req, odataReq) => {
373
373
  return _readCollection(tx, req, odataReq)
374
374
  }
375
375
 
376
+ // REVISIT: move to afterburner
377
+ if (segments[segments.length - 1]._isStreamByDollarValue) {
378
+ for (const k in req.target.elements) {
379
+ if (req.target.elements[k]['@Core.MediaType']) {
380
+ req.query.SELECT.columns = [{ ref: [k] }]
381
+ break
382
+ }
383
+ }
384
+ return _readStream(tx, req, segments)
385
+ }
386
+
376
387
  if (isStreaming(segments)) {
377
388
  return _readStream(tx, req, segments)
378
389
  }
@@ -404,6 +415,41 @@ const _removeKeysForParams = result => {
404
415
  return options
405
416
  }
406
417
 
418
+ const _getTarget = (ref, target, definitions) => {
419
+ if (cds.env.effective.odata.proxies) {
420
+ const target_ = target.elements[ref[0]]
421
+
422
+ if (ref.length === 1) {
423
+ return definitions[ensureNoDraftsSuffix(target_.target)]
424
+ }
425
+
426
+ return _getTarget(ref.slice(1), target_, definitions)
427
+ }
428
+
429
+ const target_ = target.elements[ref.join('_')]
430
+ return definitions[ensureNoDraftsSuffix(target_.target)]
431
+ }
432
+
433
+ const _getRestrictedExpand = (columns, target, definitions) => {
434
+ if (!columns || !target) return
435
+
436
+ const annotation = target['@Capabilities.ExpandRestrictions.NonExpandableProperties']
437
+ const restrictions = annotation && annotation.map(element => element['='])
438
+
439
+ for (const col of columns) {
440
+ if (col.expand) {
441
+ if (restrictions && restrictions.length !== 0) {
442
+ const ref = col.ref.join('_')
443
+ const ref_ = restrictions.find(element => element.replace(/\./g, '_') === ref)
444
+ if (ref_) return ref_
445
+ }
446
+
447
+ const restricted = _getRestrictedExpand(col.expand, _getTarget(col.ref, target, definitions), definitions)
448
+ if (restricted) return restricted
449
+ }
450
+ }
451
+ }
452
+
407
453
  /**
408
454
  * The handler that will be registered with odata-v4.
409
455
  *
@@ -415,25 +461,31 @@ const _removeKeysForParams = result => {
415
461
  * In all other failure cases it calls next with error to return a 500.
416
462
  *
417
463
  * @param {import('../../../services/Service')} service
418
- * @param {object} options
419
464
  * @returns {function}
420
465
  */
421
- const read = (service, options) => {
466
+ const read = service => {
422
467
  return async (odataReq, odataRes, next) => {
423
468
  let req
424
469
  try {
425
- validateResourcePath(odataReq, options, service.model)
470
+ validateResourcePath(odataReq, service)
426
471
  req = new ODataRequest(DATA_READ_HANDLER, service, odataReq, odataRes)
427
472
  } catch (e) {
428
473
  return next(e)
429
474
  }
430
475
 
476
+ const restricted = _getRestrictedExpand(
477
+ req.query.SELECT && req.query.SELECT.columns,
478
+ req.target,
479
+ service.model.definitions
480
+ )
481
+ if (restricted) {
482
+ return next(getError(400, 'EXPAND_IS_RESTRICTED', [restricted]))
483
+ }
484
+
431
485
  const changeset = odataReq.getAtomicityGroupId()
432
486
  const tx = changeset ? odataReq.getBatchApplicationData().txs[changeset] : service.tx(req)
433
487
  cds.context = tx
434
488
 
435
- const virtuals = getVirtuals(req, service.model)
436
-
437
489
  let result, err, commit
438
490
  let additional = {}
439
491
  try {
@@ -444,7 +496,6 @@ const read = (service, options) => {
444
496
  result = { value: null }
445
497
  } else {
446
498
  _postProcess(odataReq, req, odataRes, service, result)
447
- postProcessVirtuals(virtuals, result)
448
499
  additional = _removeKeysForParams(result)
449
500
  }
450
501
 
@@ -11,7 +11,7 @@ module.exports = srv => {
11
11
  const req = odataReq.getBatchApplicationData()
12
12
  ? odataReq.getBatchApplicationData().req
13
13
  : odataReq.getIncomingRequest()
14
- const { res, user, path } = req
14
+ const { res, user, path, headers } = req
15
15
 
16
16
  const { protectMetadata } = cds.env.odata
17
17
  if (protectMetadata === false && (path === '/' || path.endsWith('/$metadata'))) {
@@ -42,6 +42,16 @@ module.exports = srv => {
42
42
  * that can be used in ATOMICITY_GROUP_START and ATOMICITY_GROUP_END
43
43
  */
44
44
  if (path.endsWith('/$batch')) {
45
+ // ensure content type
46
+ const ct = headers['content-type'] || ''
47
+ if (!ct.match(/multipart\/mixed/) && !ct.match(/application\/json/)) {
48
+ return next({
49
+ statusCode: 400,
50
+ code: '400',
51
+ message: 'Batch requests must have content type multipart/mixed or application/json'
52
+ })
53
+ }
54
+
45
55
  odataReq.setApplicationData({ req })
46
56
  }
47
57
 
@@ -11,8 +11,9 @@ const { validateResourcePath } = require('../utils/request')
11
11
  const { isReturnMinimal } = require('../utils/handlerUtils')
12
12
  const { foreignKeyPropagations } = require('../../../../common/utils/foreignKeyPropagations')
13
13
  const readAfterWrite = require('../utils/readAfterWrite')
14
- const { toODataResult, getVirtualsFromResult, postProcess } = require('../utils/result')
14
+ const { toODataResult, postProcess } = require('../utils/result')
15
15
  const { hasOmitValuesPreference } = require('../utils/omitValues')
16
+ const { mergeJson } = require('../../../services/utils/compareJson')
16
17
 
17
18
  const _isUpsertAllowed = target => {
18
19
  return !(cds.env.runtime && cds.env.runtime.allow_upsert === false) && !(target && target._isDraftEnabled)
@@ -21,7 +22,7 @@ const _isUpsertAllowed = target => {
21
22
  const _infoForeignKeyInParent = (req, odataReq, odataRes, tx) => {
22
23
  const info = {}
23
24
  // keys not in data
24
- if (Object.keys(req.target.keys).some(key => Object.keys(req.data).includes(key))) {
25
+ if (req.target.keys && Object.keys(req.target.keys).some(key => Object.keys(req.data).includes(key))) {
25
26
  return info
26
27
  }
27
28
 
@@ -119,11 +120,7 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
119
120
 
120
121
  const _readAfterWriteAndVirtuals = async (req, service, result) => {
121
122
  const dataInDb = await readAfterWrite(req, service)
122
-
123
- // REVISIT: may be a problem on direct update of a property (which we don't support yet)
124
- // augment data in db so values of virtual properties are kept
125
- const virtuals = getVirtualsFromResult(req.target, result)
126
- if (dataInDb.length) result = Object.assign(dataInDb[0] || result, virtuals)
123
+ if (dataInDb.length) result = mergeJson(dataInDb[0], result, req.target)
127
124
  return result
128
125
  }
129
126
 
@@ -137,14 +134,13 @@ const _shouldReadPreviousResult = req =>
137
134
  * In case of error it calls next with error.
138
135
  *
139
136
  * @param {import('../../../services/Service')} service
140
- * @param {object} options
141
137
  * @returns {function}
142
138
  */
143
- const update = (service, options) => {
139
+ const update = service => {
144
140
  return async (odataReq, odataRes, next) => {
145
141
  let req
146
142
  try {
147
- validateResourcePath(odataReq, options, service.model)
143
+ validateResourcePath(odataReq, service)
148
144
  req = new ODataRequest(DATA_UPDATE_HANDLER, service, odataReq, odataRes)
149
145
  } catch (e) {
150
146
  return next(e)
@@ -16,8 +16,6 @@ const _binaryOperatorToCQN = new Map([
16
16
  [BinaryOperatorKind.LT, '<']
17
17
  ])
18
18
 
19
- const toStrMethods = ['year', 'month', 'day', 'second', 'hour', 'minute']
20
-
21
19
  class ExpressionToCQN {
22
20
  constructor(entity, model, columns = []) {
23
21
  this._model = model
@@ -196,55 +194,14 @@ class ExpressionToCQN {
196
194
  }
197
195
  /* eslint-enable complexity */
198
196
 
199
- _fillAfterDot(val) {
200
- let [beforeDot, afterDot = ''] = val.split('.')
201
-
202
- while (afterDot.length < 3) {
203
- afterDot = afterDot.concat(0)
204
- }
205
-
206
- return `${beforeDot}.${afterDot}`
207
- }
208
-
209
- _convertValToString(valueObj) {
210
- return `${valueObj.val < 10 ? 0 : ''}${valueObj.val}`
211
- }
212
-
213
- _convertForIndexOf(left, right) {
214
- if (left.func === 'indexof') right.val++
215
- else if (right.func === 'indexof') left.val++
216
- }
217
-
218
- _convertDateFunctions(arg1, arg2) {
219
- if (arg1.func && toStrMethods.includes(arg1.func) && arg1.args) {
220
- arg2.val = this._convertValToString(arg2)
221
- if (arg1.func === 'second') {
222
- arg2.val = this._fillAfterDot(arg2.val)
223
- }
224
- }
225
- }
226
-
227
- _convertNumbersToStringForDateFunctions(left, right) {
228
- this._convertDateFunctions(left, right)
229
- this._convertDateFunctions(right, left)
230
- }
231
-
232
197
  _ensureArr(something) {
233
198
  return Array.isArray(something) ? something : [something]
234
199
  }
235
200
 
236
201
  _compare(operator, left, right, unary) {
237
- // special case:
238
- // odata indexof function returns the zero-based character position of the first occurrence
239
- // hana locate function counts position beginning with 1, not 0
240
- this._convertForIndexOf(left, right)
241
-
242
- // sqlite requires leading 0 for numbers, this works on hana as well so we generally add it
243
- this._convertNumbersToStringForDateFunctions(left, right)
244
- if (unary === 'not') {
245
- return [unary, left, _binaryOperatorToCQN.get(operator), right]
246
- }
247
- return [left, _binaryOperatorToCQN.get(operator), right]
202
+ return unary === 'not'
203
+ ? [unary, '(', left, _binaryOperatorToCQN.get(operator), right, ')']
204
+ : [left, _binaryOperatorToCQN.get(operator), right]
248
205
  }
249
206
 
250
207
  _binary(expression, unary) {