@sap/cds 7.9.3 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/CHANGELOG.md +126 -3655
  2. package/_i18n/i18n_en_US_saptrc.properties +113 -0
  3. package/_i18n/i18n_zh_CN.properties +7 -4
  4. package/app/index.css +129 -0
  5. package/app/index.html +16 -64
  6. package/app/index.js +14 -9
  7. package/bin/args.js +34 -0
  8. package/bin/serve.js +18 -24
  9. package/bin/test.js +97 -0
  10. package/common.cds +5 -12
  11. package/eslint.config.mjs +133 -0
  12. package/lib/auth/basic-auth.js +16 -20
  13. package/lib/auth/dummy-auth.js +1 -1
  14. package/lib/auth/ias-auth.js +9 -41
  15. package/lib/auth/index.js +1 -14
  16. package/lib/auth/jwt-auth.js +10 -40
  17. package/lib/compile/cds-compile.js +1 -2
  18. package/lib/compile/cdsc.js +21 -26
  19. package/lib/compile/etc/_localized.js +1 -6
  20. package/lib/compile/etc/csv.js +1 -1
  21. package/lib/compile/etc/properties.js +1 -1
  22. package/lib/compile/for/java.js +1 -1
  23. package/lib/compile/for/lean_drafts.js +4 -6
  24. package/lib/compile/for/nodejs.js +1 -1
  25. package/lib/compile/parse.js +4 -0
  26. package/lib/compile/resolve.js +4 -4
  27. package/lib/compile/to/edm-files.js +16 -23
  28. package/lib/compile/to/hana.js +27 -0
  29. package/lib/compile/to/json.js +1 -1
  30. package/lib/compile/to/sql.js +5 -1
  31. package/lib/compile/to/yaml.js +3 -3
  32. package/lib/dbs/cds-deploy.js +4 -2
  33. package/lib/env/cds-env.js +10 -14
  34. package/lib/env/cds-requires.js +29 -13
  35. package/lib/env/defaults.js +46 -16
  36. package/lib/env/plugins.js +1 -1
  37. package/lib/env/schemas/cds-rc.js +8 -4
  38. package/lib/env/schemas/index.js +7 -7
  39. package/lib/env/serviceBindings.js +1 -1
  40. package/lib/index.js +12 -10
  41. package/lib/lazy.js +1 -1
  42. package/lib/linked/classes.js +36 -8
  43. package/lib/linked/entities.js +2 -10
  44. package/lib/linked/models.js +2 -1
  45. package/lib/linked/validate.js +292 -0
  46. package/lib/log/cds-error.js +0 -6
  47. package/lib/log/cds-log.js +3 -3
  48. package/lib/log/format/json.js +1 -1
  49. package/lib/log/service/index.js +0 -1
  50. package/lib/plugins.js +2 -2
  51. package/lib/ql/Query.js +2 -10
  52. package/lib/ql/SELECT.js +1 -1
  53. package/lib/ql/Whereable.js +3 -2
  54. package/lib/req/cds-context.js +14 -25
  55. package/lib/req/context.js +23 -25
  56. package/lib/req/request.js +1 -34
  57. package/lib/req/user.js +47 -35
  58. package/lib/srv/bindings.js +1 -1
  59. package/lib/srv/cds-connect.js +4 -4
  60. package/lib/srv/cds-serve.js +2 -2
  61. package/lib/srv/factory.js +1 -1
  62. package/lib/srv/middlewares/cds-context.js +11 -22
  63. package/lib/srv/middlewares/ctx-model.js +2 -3
  64. package/lib/srv/middlewares/errors.js +41 -8
  65. package/lib/srv/middlewares/index.js +3 -3
  66. package/lib/srv/middlewares/trace.js +0 -2
  67. package/lib/srv/protocols/hcql.js +15 -10
  68. package/lib/srv/protocols/http.js +44 -49
  69. package/lib/srv/protocols/index.js +1 -23
  70. package/lib/srv/protocols/odata-v4.js +12 -74
  71. package/lib/srv/protocols/rest.js +1 -13
  72. package/lib/srv/srv-api.js +0 -20
  73. package/lib/srv/srv-dispatch.js +3 -2
  74. package/lib/srv/srv-handlers.js +22 -11
  75. package/lib/srv/srv-methods.js +2 -2
  76. package/lib/srv/srv-models.js +3 -36
  77. package/lib/test/expect.js +343 -0
  78. package/lib/test/index.js +2 -0
  79. package/lib/test/reporter.js +176 -0
  80. package/lib/utils/axios.js +10 -9
  81. package/lib/utils/cds-test.js +85 -36
  82. package/lib/utils/cds-utils.js +54 -7
  83. package/lib/utils/check-version.js +0 -4
  84. package/lib/utils/colors.js +49 -0
  85. package/lib/utils/data.js +5 -4
  86. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -7
  87. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -30
  88. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +6 -12
  89. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  90. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -1
  91. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -7
  92. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +12 -6
  93. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +2 -4
  94. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +1 -0
  95. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  96. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +0 -1
  97. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -3
  98. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  99. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +1 -2
  100. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -0
  101. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  102. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +9 -43
  103. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +0 -1
  104. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -3
  105. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +4 -2
  106. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -3
  107. package/libx/_runtime/cds-services/util/assert.js +1 -1
  108. package/libx/_runtime/cds.js +10 -3
  109. package/libx/_runtime/common/Service.js +12 -32
  110. package/libx/_runtime/common/aspects/any.js +1 -0
  111. package/libx/_runtime/common/code-ext/execute.js +1 -1
  112. package/libx/_runtime/common/code-ext/worker.js +0 -1
  113. package/libx/_runtime/common/composition/data.js +0 -1
  114. package/libx/_runtime/common/composition/delete.js +0 -1
  115. package/libx/_runtime/common/composition/insert.js +2 -2
  116. package/libx/_runtime/common/composition/tree.js +0 -1
  117. package/libx/_runtime/common/composition/update.js +3 -3
  118. package/libx/_runtime/common/error/frontend.js +21 -12
  119. package/libx/_runtime/common/error/log.js +36 -0
  120. package/libx/_runtime/common/error/utils.js +2 -5
  121. package/libx/_runtime/common/generic/auth/autoexpose.js +18 -17
  122. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  123. package/libx/_runtime/common/generic/auth/readOnly.js +1 -2
  124. package/libx/_runtime/common/generic/auth/restrict.js +23 -42
  125. package/libx/_runtime/common/generic/auth/restrictions.js +2 -7
  126. package/libx/_runtime/common/generic/auth/utils.js +91 -88
  127. package/libx/_runtime/common/generic/crud.js +6 -5
  128. package/libx/_runtime/common/generic/etag.js +7 -12
  129. package/libx/_runtime/common/generic/input.js +70 -68
  130. package/libx/_runtime/common/generic/paging.js +1 -0
  131. package/libx/_runtime/common/generic/sorting.js +1 -0
  132. package/libx/_runtime/common/generic/temporal.js +8 -2
  133. package/libx/_runtime/common/i18n/index.js +1 -1
  134. package/libx/_runtime/common/i18n/messages.properties +3 -1
  135. package/libx/_runtime/common/utils/binary.js +8 -2
  136. package/libx/_runtime/common/utils/compareJson.js +5 -1
  137. package/libx/_runtime/common/utils/copy.js +6 -11
  138. package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -14
  139. package/libx/_runtime/common/utils/differ.js +3 -6
  140. package/libx/_runtime/common/utils/keys.js +77 -18
  141. package/libx/_runtime/common/utils/postProcess.js +12 -15
  142. package/libx/_runtime/common/utils/propagateForeignKeys.js +0 -1
  143. package/libx/_runtime/common/utils/resolveView.js +2 -3
  144. package/libx/_runtime/common/utils/restrictions.js +45 -17
  145. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -8
  146. package/libx/_runtime/common/utils/stream.js +3 -16
  147. package/libx/_runtime/common/utils/streamProp.js +8 -18
  148. package/libx/_runtime/common/utils/structured.js +1 -1
  149. package/libx/_runtime/common/utils/ucsn.js +0 -2
  150. package/libx/_runtime/db/Service.js +0 -72
  151. package/libx/_runtime/db/data-conversion/post-processing.js +0 -1
  152. package/libx/_runtime/db/expand/expandCQNToJoin.js +9 -9
  153. package/libx/_runtime/db/expand/rawToExpanded.js +0 -8
  154. package/libx/_runtime/db/generic/input.js +3 -8
  155. package/libx/_runtime/db/generic/rewrite.js +1 -0
  156. package/libx/_runtime/db/query/read.js +2 -2
  157. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -1
  158. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  159. package/libx/_runtime/db/utils/columns.js +2 -6
  160. package/libx/_runtime/fiori/lean-draft.js +138 -56
  161. package/libx/_runtime/hana/Service.js +0 -1
  162. package/libx/_runtime/hana/driver.js +1 -1
  163. package/libx/_runtime/hana/dynatrace.js +1 -2
  164. package/libx/_runtime/hana/pool.js +11 -21
  165. package/libx/_runtime/hana/streaming.js +0 -1
  166. package/libx/_runtime/messaging/common-utils/AMQPClient.js +0 -1
  167. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
  168. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
  169. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  170. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -33
  171. package/libx/_runtime/messaging/event-broker.js +0 -12
  172. package/libx/_runtime/messaging/file-based.js +3 -3
  173. package/libx/_runtime/messaging/http-utils/token.js +1 -1
  174. package/libx/_runtime/messaging/kafka.js +2 -2
  175. package/libx/_runtime/messaging/redis-messaging.js +0 -1
  176. package/libx/_runtime/remote/Service.js +25 -25
  177. package/libx/_runtime/remote/utils/client.js +4 -5
  178. package/libx/_runtime/remote/utils/cloudSdkProvider.js +0 -3
  179. package/libx/_runtime/remote/utils/data.js +0 -1
  180. package/libx/_runtime/sqlite/Service.js +1 -2
  181. package/libx/_runtime/ucl/Service.js +37 -78
  182. package/libx/common/assert/index.js +22 -21
  183. package/libx/common/assert/type-relaxed.js +39 -0
  184. package/libx/common/assert/utils.js +3 -2
  185. package/libx/common/assert/validation.js +3 -8
  186. package/libx/common/utils/index.js +5 -0
  187. package/libx/common/utils/path.js +51 -0
  188. package/libx/odata/ODataAdapter.js +126 -0
  189. package/libx/odata/index.js +15 -2
  190. package/libx/odata/middleware/batch.js +261 -72
  191. package/libx/odata/middleware/body-parser.js +33 -0
  192. package/libx/odata/middleware/create.js +44 -59
  193. package/libx/odata/middleware/delete.js +23 -12
  194. package/libx/odata/middleware/error.js +30 -6
  195. package/libx/odata/middleware/metadata.js +38 -26
  196. package/libx/odata/middleware/operation.js +93 -69
  197. package/libx/odata/middleware/parse.js +6 -8
  198. package/libx/odata/middleware/read.js +117 -93
  199. package/libx/odata/middleware/service-document.js +22 -19
  200. package/libx/odata/middleware/stream.js +54 -56
  201. package/libx/odata/middleware/update.js +79 -87
  202. package/libx/odata/parse/afterburner.js +191 -175
  203. package/libx/odata/parse/cqn2odata.js +8 -8
  204. package/libx/odata/parse/grammar.peggy +27 -20
  205. package/libx/odata/parse/multipartToJson.js +17 -9
  206. package/libx/odata/parse/parser.js +1 -1
  207. package/libx/odata/utils/etag.js +14 -6
  208. package/libx/odata/utils/index.js +84 -12
  209. package/libx/odata/utils/metadata.js +161 -0
  210. package/libx/odata/utils/postProcess.js +89 -0
  211. package/libx/odata/utils/readAfterWrite.js +134 -17
  212. package/libx/odata/utils/result.js +36 -142
  213. package/libx/outbox/index.js +4 -3
  214. package/libx/rest/RestAdapter.js +115 -182
  215. package/libx/rest/middleware/create.js +28 -24
  216. package/libx/rest/middleware/delete.js +7 -10
  217. package/libx/rest/middleware/error.js +19 -16
  218. package/libx/rest/middleware/operation.js +48 -41
  219. package/libx/rest/middleware/parse.js +128 -126
  220. package/libx/rest/middleware/read.js +20 -27
  221. package/libx/rest/middleware/update.js +26 -31
  222. package/package.json +17 -8
  223. package/server.js +4 -2
  224. package/tasks/enterprise-messaging-deploy.js +1 -1
  225. package/apis/cds.d.ts +0 -3
  226. package/apis/core.d.ts +0 -21
  227. package/apis/cqn.d.ts +0 -18
  228. package/apis/csn.d.ts +0 -21
  229. package/apis/events.d.ts +0 -18
  230. package/apis/internal/inference.d.ts +0 -18
  231. package/apis/linked.d.ts +0 -18
  232. package/apis/log.d.ts +0 -20
  233. package/apis/models.d.ts +0 -18
  234. package/apis/ql.d.ts +0 -18
  235. package/apis/reflect.d.ts +0 -32
  236. package/apis/server.d.ts +0 -18
  237. package/apis/services.d.ts +0 -22
  238. package/bin/cds-serve.js +0 -56
  239. package/lib/compile/to/gql.js +0 -15
  240. package/lib/srv/protocols/_legacy.js +0 -44
  241. package/lib/utils/jest.js +0 -43
  242. package/libx/_runtime/auth/index.js +0 -193
  243. package/libx/_runtime/auth/strategies/JWT.js +0 -37
  244. package/libx/_runtime/auth/strategies/basic.js +0 -20
  245. package/libx/_runtime/auth/strategies/dummy.js +0 -14
  246. package/libx/_runtime/auth/strategies/ias-auth.js +0 -1
  247. package/libx/_runtime/auth/strategies/mock.js +0 -77
  248. package/libx/_runtime/auth/strategies/xssecUtils.js +0 -93
  249. package/libx/_runtime/auth/strategies/xsuaa.js +0 -38
  250. package/libx/_runtime/common/perf/index.js +0 -19
  251. package/libx/_runtime/common/utils/ensureIEEE754.js +0 -29
  252. package/libx/_runtime/fiori/draft.js +0 -2
  253. package/libx/_runtime/fiori/generic/activate.js +0 -190
  254. package/libx/_runtime/fiori/generic/before.js +0 -201
  255. package/libx/_runtime/fiori/generic/cancel.js +0 -19
  256. package/libx/_runtime/fiori/generic/delete.js +0 -21
  257. package/libx/_runtime/fiori/generic/edit.js +0 -157
  258. package/libx/_runtime/fiori/generic/index.js +0 -25
  259. package/libx/_runtime/fiori/generic/new.js +0 -82
  260. package/libx/_runtime/fiori/generic/patch.js +0 -101
  261. package/libx/_runtime/fiori/generic/prepare.js +0 -57
  262. package/libx/_runtime/fiori/generic/read.js +0 -1340
  263. package/libx/_runtime/fiori/generic/readOverDraft.js +0 -146
  264. package/libx/_runtime/fiori/utils/csn.js +0 -13
  265. package/libx/_runtime/fiori/utils/delete.js +0 -114
  266. package/libx/_runtime/fiori/utils/handler.js +0 -264
  267. package/libx/_runtime/fiori/utils/lockInfo.js +0 -27
  268. package/libx/_runtime/fiori/utils/req.js +0 -23
  269. package/libx/_runtime/fiori/utils/stream.js +0 -36
  270. package/libx/_runtime/fiori/utils/where.js +0 -254
  271. package/libx/_runtime/index.js +0 -22
  272. package/libx/odata/utils/handler.js +0 -120
  273. package/libx/odata/utils/metaInfo.js +0 -410
  274. package/libx/odata/utils/path.js +0 -75
  275. package/libx/rest/RestRequest.js +0 -32
  276. package/libx/rest/index.js +0 -3
  277. package/libx/rest/readme.md +0 -1
  278. /package/libx/common/assert/{type.js → type-strict.js} +0 -0
@@ -1,6 +1,3 @@
1
- const getTemplate = require('../../_runtime/common/utils/template')
2
- const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
3
-
4
1
  const METADATA = {
5
2
  $context: '@odata.context',
6
3
  $count: '@odata.count',
@@ -26,6 +23,7 @@ const METADATA = {
26
23
  }
27
24
 
28
25
  const KEYSTOCLEANUP = {
26
+ // REVISIT: should probably be handled in RemoteService's handle()
29
27
  // do not set "@odata.context" as it may be inherited of remote service
30
28
  $context: true,
31
29
  // REVISIT: okra doesn't support content disposition
@@ -33,161 +31,57 @@ const KEYSTOCLEANUP = {
33
31
  $mediaContentDispositionType: true
34
32
  }
35
33
 
36
- const _metadataRoot = (result, odataResult) => {
37
- for (const key in METADATA) {
38
- if (!(key in result)) continue
39
- if (!KEYSTOCLEANUP[key]) odataResult[METADATA[key]] = result[key]
40
- }
41
- }
42
-
43
- const _metadata = (result, propertyName, odataResult) => {
34
+ const _rewriteMetadataDeep = result => {
44
35
  for (const key in result) {
45
- if (typeof result[key] === 'object') _metadata(result[key])
46
- if (!(key in METADATA)) continue
47
- if (!KEYSTOCLEANUP[key]) {
48
- if (propertyName) odataResult[METADATA[key]] = result[key]
49
- else result[METADATA[key]] = result[key]
50
- }
51
- if (!propertyName) delete result[key]
52
- }
53
- }
54
-
55
- const _cleanupMetadata = (propertyName, result) => {
56
- if (typeof result !== 'object') return odataResult
57
-
58
- const odataResult = {}
59
- if (propertyName) {
60
- odataResult.value = result[propertyName]
61
- } else {
62
- odataResult.value = result
63
- }
64
-
65
- if (Array.isArray(result)) _metadataRoot(result, odataResult)
66
- _metadata(result, propertyName, odataResult)
67
-
68
- return odataResult
69
- }
70
-
71
- const _setContext = (odataResult, info, isCollection) => {
72
- if (info && info.metadata) {
73
- const result = isCollection || info.metadata.propertyName ? odataResult : odataResult.value
74
-
75
- if (result != null) Object.assign(result, { [METADATA.$context]: info.metadata.contextUrl })
76
- }
77
- return odataResult
78
- }
79
-
80
- const _getParent = (model, name) => {
81
- const target = model.definitions[name]
82
-
83
- if (target && target.elements) {
84
- for (const elementName in target.elements) {
85
- const element = target.elements[elementName]
86
- if (element._anchor && element._anchor._isContained) return element._anchor
87
- }
88
- }
89
-
90
- return null
91
- }
92
-
93
- const addEtags = (row, key) => {
94
- if (!row[key]) return
95
- row['$etag'] = row[key].startsWith('W/') ? row[key] : `W/"${row[key]}"`
96
- }
97
-
98
- const _processCategory = (category, elementInfo) => {
99
- const { row, key } = elementInfo
100
-
101
- switch (category) {
102
- case '@odata.etag':
103
- addEtags(row, key)
104
- break
105
- case '@cds.api.ignore':
106
- delete row[key]
107
- break
108
- // no default
109
- }
110
- }
111
-
112
- const _processorFn = () => elementInfo => {
113
- const { row, key, plain } = elementInfo
114
- if (typeof row !== 'object' || !Object.prototype.hasOwnProperty.call(row, key)) return
115
- const categories = plain.categories
116
-
117
- for (const category of categories) {
118
- _processCategory(category, elementInfo)
119
- }
120
- }
121
-
122
- const _pick = element => {
123
- const categories = []
124
- if (element['@odata.etag']) categories.push('@odata.etag')
125
- if (element['@cds.api.ignore']) categories.push('@cds.api.ignore')
126
- if (categories.length) return { categories }
127
- }
128
-
129
- const postProcess = (target, service, result, isMinimal) => {
130
- const { model } = service
131
- if (!target || !result || !model || !model.definitions[target.name]) return
132
-
133
- const cacheKey = isMinimal ? 'postProcessMinimal' : 'postProcess'
134
- const parent = _getParent(model, target.name)
135
- const template = getTemplate(
136
- cacheKey,
137
- service,
138
- target,
139
- { pick: _pick, ignore: isMinimal ? el => el.isAssociation : undefined },
140
- parent
141
- )
142
-
143
- if (template.elements.size === 0) return
144
-
145
- // normalize result to rows
146
- result = result.value != null && Object.keys(result).filter(k => !k.match(/^\W/)).length === 1 ? result.value : result
147
-
148
- if (typeof result === 'object' && result != null) {
149
- const rows = Array.isArray(result) ? result : [result]
150
-
151
- // process each row
152
- const processFn = _processorFn()
153
- for (const row of rows) {
154
- templateProcessor({
155
- processFn,
156
- row,
157
- template
158
- })
36
+ if (typeof result[key] === 'object' && result[key] != null) _rewriteMetadataDeep(result[key])
37
+ if (key in METADATA && !KEYSTOCLEANUP[key]) {
38
+ result[METADATA[key]] = result[key]
39
+ delete result[key]
159
40
  }
160
41
  }
161
42
  }
162
43
 
163
44
  /**
164
- * Convert any result to the result object structure, which is expected of odata-v4.
45
+ * Constructs the odata result object from the result of the service call as well as the provided metadata and additional options.
165
46
  *
166
- * @param {*} result
167
- * @param {*} [info]
168
- * @returns {string | object}
47
+ * @param {*} result - the result of the service call, i.e., the payload to return to the client
48
+ * @param {object} metadata - odata metadata
49
+ * @param {string} metadata.context - @odata.context
50
+ * @param {object} [options] - additional options/ instructions
51
+ * @param {boolean} [options.isCollection] - whether the result shall be a collection
52
+ * @param {string} [options.property] - the name of the requested single property, if any
53
+ * @returns {object} - the odata result
169
54
  */
170
- const toODataResult = (result, info) => {
55
+ module.exports = function getODataResult(result, metadata, options = {}) {
171
56
  if (result == null) return ''
172
57
 
173
- let propertyName, isCollection
174
- if (info) {
175
- propertyName = info.metadata.propertyName
176
- isCollection = info.metadata.isCollection
177
- }
58
+ const { isCollection, property } = options
178
59
 
179
60
  if (isCollection && !Array.isArray(result)) result = [result]
180
61
  else if (!isCollection && Array.isArray(result)) result = result[0]
181
62
 
182
- const odataResult = _cleanupMetadata(propertyName, result)
63
+ // make sure @odata.context is the first element (per OData spec)
64
+ const odataResult = {
65
+ [METADATA.$context]: metadata.context
66
+ }
67
+
68
+ // copy metadata from result to odataResult
69
+ for (const key in METADATA) {
70
+ if (!(key in result)) continue
71
+ if (!KEYSTOCLEANUP[key]) odataResult[METADATA[key]] = result[key]
72
+ }
183
73
 
184
- // REVISIT: Support exponential decimals header
185
- // REVISIT: we always assume minimal metadata right now
186
- _setContext(odataResult, info, isCollection)
74
+ // rewrite metadata in result
75
+ _rewriteMetadataDeep(result)
187
76
 
188
- if (!isCollection && !propertyName) return odataResult.value
77
+ // add result to odataResult
78
+ if (isCollection) {
79
+ Object.assign(odataResult, { value: result })
80
+ } else if (property) {
81
+ Object.assign(odataResult, { value: result[property] })
82
+ } else {
83
+ Object.assign(odataResult, result)
84
+ }
189
85
 
190
86
  return odataResult
191
87
  }
192
-
193
- module.exports = { toODataResult, postProcess }
@@ -42,7 +42,7 @@ const hasPersistentOutbox = tenant => {
42
42
  const _safeJSONParse = string => {
43
43
  try {
44
44
  return string && JSON.parse(string)
45
- } catch (_e) {
45
+ } catch {
46
46
  // Don't throw
47
47
  }
48
48
  }
@@ -56,6 +56,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
56
56
  outboxRunner.run({ name, tenant }, () => {
57
57
  let letAppCrash = false
58
58
  const config = tenant ? { tenant, user: cds.User.privileged } : { user: cds.User.privileged }
59
+ config.after = 1 // make sure spawn puts its cb on the `timer` queue (via setTimeout), which is also used by `outboxRunner`
59
60
  const spawn = cds.spawn(async () => {
60
61
  let messages
61
62
  try {
@@ -149,7 +150,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
149
150
  if ((await _handleWithErr(msg)) === false) break
150
151
  }
151
152
  }
152
- } catch (e) {
153
+ } catch {
153
154
  letAppCrash = true
154
155
  }
155
156
 
@@ -277,7 +278,7 @@ function outboxed(srv, customOpts) {
277
278
  }
278
279
 
279
280
  if (!context[$stored_reqs]) {
280
- context[$stored_reqs] = []
281
+ context[$stored_reqs] = []
281
282
  context.on('succeeded', async () => {
282
283
  // REVISIT: Also allow maxAttempts for in-memory outbox?
283
284
  for (const _req of context[$stored_reqs]) {
@@ -1,201 +1,134 @@
1
1
  const cds = require('../_runtime/cds')
2
2
 
3
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
4
- const express = require('express')
3
+ const parse = require('./middleware/parse')
4
+ const create = require('./middleware/create')
5
+ const read = require('./middleware/read')
6
+ const update = require('./middleware/update')
7
+ const deleet = require('./middleware/delete')
8
+ const operation = require('./middleware/operation')
9
+ const error = require('./middleware/error')
5
10
 
6
- const parse_factory = require('./middleware/parse')
11
+ const { bufferToBase64 } = require('../_runtime/common/utils/binary')
7
12
 
8
- const create_factory = require('./middleware/create')
9
- const read_factory = require('./middleware/read')
10
- const update_factory = require('./middleware/update')
11
- const delete_factory = require('./middleware/delete')
12
- const operation_factory = require('./middleware/operation')
13
+ const HttpAdapter = require('../../lib/srv/protocols/http')
14
+ const bodyParser4 = require('../odata/middleware/body-parser')
13
15
 
14
- const error_factory = require('./middleware/error')
16
+ // REVISIT: ugly hack -> eliminate
17
+ const { NoaRequest } = require('../odata/ODataAdapter')
18
+ class RestRequest extends NoaRequest {
19
+ get protocol() {
20
+ return 'rest'
21
+ }
22
+ }
15
23
 
16
- const { bufferToBase64 } = require('../_runtime/common/utils/binary')
17
- const { getAccessRestrictions } = require('../_runtime/common/utils/restrictions')
18
-
19
- const RestAdapter = function (srv) {
20
- const router = express.Router()
21
-
22
- // -----------------------------------------------------------------------------------------
23
- // check @requires as soon as possible (DoS)
24
- //
25
- const accessRestrictions = getAccessRestrictions(srv)
26
- router.use((req, res, next) => {
27
- // ensure there always is a user going forward (not always the case with old or custom auth)
28
- if (!req.user) req.user = new cds.User.default()
29
-
30
- // check @restrict and @requires as soon as possible (DoS)
31
- if (!accessRestrictions.some(r => req.user.is(r))) {
32
- // > unauthorized or forbidden?
33
- if (req.user._is_anonymous) {
34
- // NOTE: "return req._login()" would not invoke custom error handlers
35
- if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
36
- else if (req.user._challenges) res.set('www-authenticate', req.user._challenges.join(';'))
37
- throw cds.error('Unauthorized', { statusCode: 401, code: '401' })
38
- }
39
- throw cds.error('Forbidden', { statusCode: 403, code: '403' })
40
- }
41
-
42
- next()
43
- })
44
-
45
- // -----------------------------------------------------------------------------------------
46
- // service root
47
- //
48
- router.head('/', (_, res) => res.json({}))
49
- router.get('/', (_, res) =>
50
- res.json({
51
- entities: Object.keys(srv.entities).map(e => ({ name: e, url: e }))
52
- })
53
- )
54
-
55
- // -----------------------------------------------------------------------------------------
56
- // parse / validate
57
- //
58
- // content-type check
59
- router.use((req, res, next) => {
60
- // REVISIT: move that into parse function
61
- if (req.method in { POST: 1, PUT: 1, PATCH: 1 }) {
62
- const contentType = req.headers['content-type'] && req.headers['content-type'].split(';')
63
- if (
64
- contentType &&
65
- (!contentType[0].match(/^application\/json$/) || (typeof contentType[1] === 'string' && !contentType[1]))
66
- ) {
67
- throw cds.error('INVALID_CONTENT_TYPE_ONLY_JSON', { statusCode: 415, code: '415' }) // FIXME: better i18n + use res.status
68
- }
24
+ class RestAdapter extends HttpAdapter {
25
+ request4(args) {
26
+ return new RestRequest(args)
27
+ }
69
28
 
70
- if (req.method in { PUT: 1, PATCH: 1 }) {
71
- if (Array.isArray(req.body)) {
72
- throw cds.error(`INVALID_${req.method}`, { statusCode: 400, code: '400' }) // FIXME: better i18n + use res.status
73
- }
29
+ get router() {
30
+ const srv = this.service
31
+ const router = super.router
32
+
33
+ const jsonBodyParser = bodyParser4(this)
34
+
35
+ // service root
36
+ router.head('/', (_, res) => res.json({}))
37
+ const entities = Object.keys(srv.entities).map(e => ({ name: e, url: e }))
38
+ router.get('/', (_, res) => res.json({ entities }))
74
39
 
75
- // REVISIT: empty object length is not 0
76
- // REVISIT: also check for POST?
77
- // check for empty payload body
40
+ // validate headers
41
+ router.use((req, res, next) => {
42
+ if (req.method in { POST: 1, PUT: 1, PATCH: 1 } && req.headers['content-type']) {
43
+ const parts = req.headers['content-type'].split(';')
44
+ if (!parts[0].match(/^application\/json$/) || parts[1] === '') {
45
+ throw cds.error('INVALID_CONTENT_TYPE_ONLY_JSON', { statusCode: 415, code: '415' }) // FIXME: better i18n + use res.status
46
+ }
47
+ }
48
+ if (req.method in { PUT: 1, PATCH: 1 }) {
78
49
  if (req.headers['content-length'] === '0') {
79
- res.status(400).json({ error: { message: 'Malformed patch document', statusCode: 400, code: '400' } })
50
+ res.status(400).json({ error: { message: 'Malformed document', statusCode: 400, code: '400' } })
80
51
  return
81
52
  }
82
53
  }
83
- }
84
- next()
85
- })
86
- router.use(express.json())
87
- router.use(parse_factory(srv))
88
-
89
- // -----------------------------------------------------------------------------------------
90
- // begin tx
91
- //
92
- router.use((req, res, next) => {
93
- // REVISIT: -> move to actual handler(s)
94
- const tenant = req.tenant || req.user?.tenant
95
- // create tx and set as cds.context
96
- cds.context = srv.tx(new cds.EventContext({ user: req.user, req, res, tenant }))
97
- next()
98
- })
99
-
100
- // -----------------------------------------------------------------------------------------
101
- // Actual handlers for HEAD, GET, PUT, POST, PATCH, DELETE
102
- //
103
- const operation = operation_factory(srv)
104
- const create = create_factory(srv)
105
- const read = read_factory(srv)
106
- const update = update_factory(srv)
107
- const deleet = delete_factory(srv)
108
- router.use(async (req, res, next) => {
109
- try {
110
- let result, status, location
111
-
112
- if (req._operation) {
113
- // actions and functions
114
- ;({ result, status } = await operation(req, res))
115
- } else {
116
- // CRUD
117
- switch (req.method) {
118
- case 'POST':
119
- ;({ result, status, location } = await create(req))
120
- break
121
- case 'HEAD':
122
- case 'GET':
123
- ;({ result, status } = await read(req))
124
- break
125
- case 'PUT':
126
- case 'PATCH':
127
- ;({ result, status } = await update(req))
128
- break
129
- case 'DELETE':
130
- ;({ result, status } = await deleet(req))
131
- break
54
+
55
+ return next()
56
+ })
57
+ router.use(jsonBodyParser)
58
+ router.use(parse(this))
59
+
60
+ // handle
61
+ const operation_middleware = operation(this)
62
+ const create_middleware = create(this)
63
+ const read_middleware = read(this)
64
+ const update_middleware = update(this)
65
+ const delete_middleware = deleet(this)
66
+ router.use(async function dispatch(req, res, next) {
67
+ try {
68
+ let result, status, location
69
+
70
+ if (req._operation) {
71
+ // actions and functions
72
+ ;({ result, status } = await operation_middleware(req, res))
73
+ } else {
74
+ // CRUD
75
+ switch (req.method) {
76
+ case 'POST':
77
+ ;({ result, status, location } = await create_middleware(req, res))
78
+ break
79
+ case 'HEAD':
80
+ case 'GET':
81
+ ;({ result, status } = await read_middleware(req, res))
82
+ break
83
+ case 'PUT':
84
+ case 'PATCH':
85
+ // eslint-disable-next-line no-case-declarations
86
+ const _res = await update_middleware(req, res, next)
87
+ if (_res) ({ result, status } = _res)
88
+ break
89
+ case 'DELETE':
90
+ ;({ result, status } = await delete_middleware(req, res))
91
+ break
92
+ }
93
+ }
94
+
95
+ if (status || result !== undefined) {
96
+ req._result = { result, status, location }
97
+ return next()
132
98
  }
99
+ } catch (e) {
100
+ next(e)
133
101
  }
102
+ })
134
103
 
135
- req._result = { result, status, location }
136
- return next()
137
- } catch (e) {
138
- next(e)
139
- }
140
- })
141
-
142
- // -----------------------------------------------------------------------------------------
143
- // end tx (i.e., commit or rollback)
144
- //
145
- router.use(async (req, res, next) => {
146
- const { result, status, location } = req._result // REVISIT: Ugly voodoo _req._result channel -> eliminate
147
-
148
- // unfortunately, express doesn't catch async errors -> try catch needed
149
- try {
150
- await cds.context?.tx?.commit(result)
151
- } catch (e) {
152
- return next(e)
153
- }
154
-
155
- // if authentication or something else within the processing of a cds.Request terminates the request, no need to continue
156
- if (res.headersSent) return
157
-
158
- // convert binaries
159
- let definition = req._operation || req._query.__target
160
- if (typeof definition === 'string')
161
- definition =
162
- srv.model.definitions[definition] ||
163
- srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
164
- if (result && srv && definition) bufferToBase64(result, srv, definition)
165
-
166
- // only set status if not yet modified
167
- if (status && res.statusCode === 200) res.status(status) // REVISIT: Why only when res.statusCode === 200?
168
- if (location) res.set('location', location) // REVISIT: When do we redirect?
169
- if (req.method === 'HEAD')
170
- // REVISIT: Move that to the implementation of HEAD
171
- res
172
- .set({
173
- 'content-type': 'application/json; charset=utf-8',
174
- 'content-length': JSON.stringify(result).length
175
- })
176
- .end()
177
- // need to convert number to string because express interprets integer as status code
178
- else res.send(typeof result === 'number' ? result.toString() : result) // REVISIT: use req.json() instead?
179
- })
180
-
181
- // -----------------------------------------------------------------------------------------
182
- // error handling
183
- //
184
- router.use(async (err, req, res, next) => {
185
- // REVISIT: should not be neccessary!
186
- // request may fail during processing or during commit -> both caught here
187
-
188
- // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
189
- await cds.context?.tx?.rollback(err).catch(() => {}) // REVISIT: silently ?!?
190
-
191
- next(err)
192
- })
193
-
194
- if (!cds.env.features.rest_error_handler) {
195
- router.use(error_factory(srv)) // FIXME: nope -> call next()
196
- }
104
+ // handle result
105
+ router.use((req, res) => {
106
+ const { result, status, location } = req._result // REVISIT: Ugly voodoo _req._result channel -> eliminate
197
107
 
198
- return router
108
+ // if authentication or something else within the processing of a cds.Request terminates the request, no need to continue
109
+ if (res.headersSent) return
110
+
111
+ // convert binaries
112
+ let definition = req._operation || req._query.__target
113
+ if (typeof definition === 'string')
114
+ definition =
115
+ srv.model.definitions[definition] ||
116
+ srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
117
+ if (result && srv && definition) bufferToBase64(result, srv, definition)
118
+
119
+ if (status && res.statusCode === 200) res.status(status) //> only set status if not yet modified
120
+ if (location && !res.getHeader('location')) res.set('location', location)
121
+
122
+ // prettier-ignore
123
+ if (req.method === 'HEAD') res.type('json').set({ 'content-length': JSON.stringify(result).length }).end()
124
+ else res.send(typeof result === 'number' ? result.toString() : result)
125
+ })
126
+
127
+ // error handling
128
+ router.use(error(this))
129
+
130
+ return router
131
+ }
199
132
  }
200
133
 
201
134
  module.exports = RestAdapter
@@ -1,36 +1,40 @@
1
1
  const cds = require('../../_runtime/cds')
2
2
  const { INSERT } = cds.ql
3
3
 
4
- const RestRequest = require('../RestRequest')
5
-
6
4
  const _error4 = rejected =>
7
5
  rejected.length > 1
8
6
  ? Object.assign(new Error('MULTIPLE_ERRORS'), { details: rejected.map(r => r.reason) })
9
7
  : rejected[0].reason
10
8
 
11
- module.exports = srv => async _req => {
12
- const { _query: query, _target, _data, _params } = _req
9
+ module.exports = adapter => {
10
+ const { service } = adapter
13
11
 
14
- let result, location
12
+ return async function create(req, res) {
13
+ const { _query: query, _data, _params: params } = req
15
14
 
16
- // add the data
17
- query.entries(_data)
18
- if (query.INSERT.entries.length > 1) {
19
- // > batch insert
20
- const reqs = query.INSERT.entries.map(
21
- entry => new RestRequest({ query: INSERT.into(query.INSERT.into).entries(entry), _target, params: _params })
22
- )
23
- const ress = await Promise.allSettled(reqs.map(req => srv.dispatch(req)))
24
- const rejected = ress.filter(r => r.status === 'rejected')
25
- if (rejected.length) throw _error4(rejected)
26
- result = ress.map(r => r.value)
27
- } else {
28
- // > single insert
29
- const req = new RestRequest({ query, _target, params: _params })
30
- result = await srv.dispatch(req)
31
- location = `../${req.entity.replace(srv.name + '.', '')}` // REVISIT: Is it guaranteed that the GET works? Why do we need relative urls?
32
- for (const k in req.target.keys) location += `/${result[k]}`
33
- }
15
+ let result, location
34
16
 
35
- return { result, status: 201, location }
17
+ // add the data
18
+ query.entries(_data)
19
+ if (query.INSERT.entries.length > 1) {
20
+ // > batch insert
21
+ const cdsReqs = query.INSERT.entries.map(entry => {
22
+ return adapter.request4({ query: INSERT.into(query.INSERT.into).entries(entry), params, req, res })
23
+ })
24
+ const ress = await Promise.allSettled(cdsReqs.map(req => service.dispatch(req)))
25
+ const rejected = ress.filter(r => r.status === 'rejected')
26
+ if (rejected.length) throw _error4(rejected)
27
+ result = ress.map(r => r.value)
28
+ } else {
29
+ // > single insert
30
+ const cdsReq = adapter.request4({ query, params, req, res })
31
+ result = await service.dispatch(cdsReq)
32
+ // REVISIT: location is a restful feature -> share with odata
33
+ // REVISIT: Is it guaranteed that the GET works? Why do we need relative urls?
34
+ location = `../${cdsReq.entity.replace(service.definition.name + '.', '')}`
35
+ for (const k in cdsReq.target.keys) location += `/${result[k]}`
36
+ }
37
+
38
+ return { result, status: 201, location }
39
+ }
36
40
  }
@@ -1,14 +1,11 @@
1
- const RestRequest = require('../RestRequest')
1
+ module.exports = adapter => {
2
+ const { service } = adapter
2
3
 
3
- module.exports = srv => async _req => {
4
- const { _query: query, _target, _data, _params } = _req
4
+ return async function deleet(req, res) {
5
+ const { _query: query, _data: data, _params: params } = req
5
6
 
6
- const req = new RestRequest({ query, _target, params: _params, data: _data })
7
+ await service.dispatch(adapter.request4({ query, data, params, req, res }))
7
8
 
8
- // req.data is filled with keys during read and delete
9
- if (_params) req.data = Object.assign(_data, _params[_params.length - 1]) // REVISIT: We should avoid that!
10
-
11
- await srv.dispatch(req)
12
-
13
- return { result: null, status: 204 }
9
+ return { result: null, status: 204 }
10
+ }
14
11
  }