@sap/cds 7.9.4 → 8.0.4
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.
- package/CHANGELOG.md +128 -3659
- package/_i18n/i18n_en_US_saptrc.properties +113 -0
- package/_i18n/i18n_zh_CN.properties +7 -4
- package/app/index.css +129 -0
- package/app/index.html +16 -64
- package/app/index.js +14 -9
- package/bin/args.js +34 -0
- package/bin/serve.js +18 -24
- package/bin/test.js +97 -0
- package/common.cds +5 -12
- package/eslint.config.mjs +133 -0
- package/lib/auth/basic-auth.js +16 -20
- package/lib/auth/dummy-auth.js +1 -1
- package/lib/auth/ias-auth.js +9 -41
- package/lib/auth/index.js +1 -14
- package/lib/auth/jwt-auth.js +10 -40
- package/lib/compile/cds-compile.js +1 -2
- package/lib/compile/cdsc.js +21 -26
- package/lib/compile/etc/_localized.js +1 -6
- package/lib/compile/etc/csv.js +1 -1
- package/lib/compile/etc/properties.js +1 -1
- package/lib/compile/for/java.js +1 -1
- package/lib/compile/for/lean_drafts.js +4 -6
- package/lib/compile/for/nodejs.js +1 -1
- package/lib/compile/parse.js +4 -0
- package/lib/compile/resolve.js +4 -4
- package/lib/compile/to/edm-files.js +16 -23
- package/lib/compile/to/hana.js +27 -0
- package/lib/compile/to/json.js +1 -1
- package/lib/compile/to/sql.js +5 -1
- package/lib/compile/to/yaml.js +3 -3
- package/lib/dbs/cds-deploy.js +4 -2
- package/lib/env/cds-env.js +10 -14
- package/lib/env/cds-requires.js +30 -13
- package/lib/env/defaults.js +46 -16
- package/lib/env/plugins.js +1 -1
- package/lib/env/schemas/cds-rc.js +8 -4
- package/lib/env/schemas/index.js +7 -7
- package/lib/env/serviceBindings.js +1 -1
- package/lib/index.js +12 -10
- package/lib/lazy.js +1 -1
- package/lib/linked/classes.js +36 -8
- package/lib/linked/entities.js +2 -10
- package/lib/linked/models.js +2 -1
- package/lib/linked/validate.js +292 -0
- package/lib/log/cds-error.js +0 -6
- package/lib/log/cds-log.js +3 -3
- package/lib/log/format/json.js +1 -1
- package/lib/log/service/index.js +0 -1
- package/lib/plugins.js +2 -2
- package/lib/ql/Query.js +2 -10
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/Whereable.js +3 -2
- package/lib/req/cds-context.js +14 -25
- package/lib/req/context.js +23 -25
- package/lib/req/request.js +1 -34
- package/lib/req/user.js +47 -35
- package/lib/srv/bindings.js +1 -1
- package/lib/srv/cds-connect.js +4 -4
- package/lib/srv/cds-serve.js +2 -2
- package/lib/srv/factory.js +1 -1
- package/lib/srv/middlewares/cds-context.js +11 -22
- package/lib/srv/middlewares/ctx-model.js +2 -3
- package/lib/srv/middlewares/errors.js +41 -8
- package/lib/srv/middlewares/index.js +3 -3
- package/lib/srv/middlewares/trace.js +0 -2
- package/lib/srv/protocols/hcql.js +15 -10
- package/lib/srv/protocols/http.js +44 -49
- package/lib/srv/protocols/index.js +1 -23
- package/lib/srv/protocols/odata-v4.js +12 -74
- package/lib/srv/protocols/rest.js +1 -13
- package/lib/srv/srv-api.js +0 -20
- package/lib/srv/srv-dispatch.js +3 -2
- package/lib/srv/srv-handlers.js +22 -11
- package/lib/srv/srv-methods.js +2 -2
- package/lib/srv/srv-models.js +3 -36
- package/lib/test/expect.js +343 -0
- package/lib/test/index.js +2 -0
- package/lib/test/reporter.js +176 -0
- package/lib/utils/axios.js +10 -9
- package/lib/utils/cds-test.js +85 -36
- package/lib/utils/cds-utils.js +54 -7
- package/lib/utils/check-version.js +0 -4
- package/lib/utils/colors.js +49 -0
- package/lib/utils/data.js +5 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -30
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +6 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +12 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +2 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +9 -43
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -3
- package/libx/_runtime/cds-services/util/assert.js +1 -1
- package/libx/_runtime/cds.js +10 -3
- package/libx/_runtime/common/Service.js +12 -32
- package/libx/_runtime/common/aspects/any.js +1 -0
- package/libx/_runtime/common/code-ext/execute.js +1 -1
- package/libx/_runtime/common/code-ext/worker.js +0 -1
- package/libx/_runtime/common/composition/data.js +0 -1
- package/libx/_runtime/common/composition/delete.js +0 -1
- package/libx/_runtime/common/composition/tree.js +0 -1
- package/libx/_runtime/common/composition/update.js +3 -3
- package/libx/_runtime/common/error/frontend.js +21 -12
- package/libx/_runtime/common/error/log.js +36 -0
- package/libx/_runtime/common/error/utils.js +2 -5
- package/libx/_runtime/common/generic/auth/autoexpose.js +18 -17
- package/libx/_runtime/common/generic/auth/expand.js +1 -1
- package/libx/_runtime/common/generic/auth/readOnly.js +1 -2
- package/libx/_runtime/common/generic/auth/restrict.js +23 -42
- package/libx/_runtime/common/generic/auth/restrictions.js +2 -7
- package/libx/_runtime/common/generic/auth/utils.js +91 -88
- package/libx/_runtime/common/generic/crud.js +6 -5
- package/libx/_runtime/common/generic/etag.js +7 -12
- package/libx/_runtime/common/generic/input.js +70 -68
- package/libx/_runtime/common/generic/paging.js +1 -0
- package/libx/_runtime/common/generic/sorting.js +1 -0
- package/libx/_runtime/common/generic/temporal.js +8 -2
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/i18n/messages.properties +3 -1
- package/libx/_runtime/common/utils/binary.js +8 -2
- package/libx/_runtime/common/utils/compareJson.js +5 -1
- package/libx/_runtime/common/utils/copy.js +6 -11
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -14
- package/libx/_runtime/common/utils/differ.js +3 -6
- package/libx/_runtime/common/utils/keys.js +77 -18
- package/libx/_runtime/common/utils/postProcess.js +12 -15
- package/libx/_runtime/common/utils/propagateForeignKeys.js +0 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -3
- package/libx/_runtime/common/utils/restrictions.js +45 -17
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -8
- package/libx/_runtime/common/utils/stream.js +3 -16
- package/libx/_runtime/common/utils/streamProp.js +8 -18
- package/libx/_runtime/common/utils/structured.js +1 -1
- package/libx/_runtime/common/utils/ucsn.js +0 -2
- package/libx/_runtime/db/Service.js +0 -72
- package/libx/_runtime/db/data-conversion/post-processing.js +0 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +9 -9
- package/libx/_runtime/db/expand/rawToExpanded.js +0 -8
- package/libx/_runtime/db/generic/input.js +3 -8
- package/libx/_runtime/db/generic/rewrite.js +1 -0
- package/libx/_runtime/db/query/read.js +2 -2
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -1
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
- package/libx/_runtime/db/utils/columns.js +2 -6
- package/libx/_runtime/fiori/lean-draft.js +138 -56
- package/libx/_runtime/hana/Service.js +0 -1
- package/libx/_runtime/hana/driver.js +1 -1
- package/libx/_runtime/hana/dynatrace.js +1 -2
- package/libx/_runtime/hana/pool.js +11 -21
- package/libx/_runtime/hana/streaming.js +0 -1
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +0 -1
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
- package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -33
- package/libx/_runtime/messaging/event-broker.js +54 -27
- package/libx/_runtime/messaging/file-based.js +3 -3
- package/libx/_runtime/messaging/http-utils/token.js +1 -1
- package/libx/_runtime/messaging/kafka.js +2 -2
- package/libx/_runtime/messaging/redis-messaging.js +0 -1
- package/libx/_runtime/remote/Service.js +25 -25
- package/libx/_runtime/remote/utils/client.js +4 -5
- package/libx/_runtime/remote/utils/cloudSdkProvider.js +0 -3
- package/libx/_runtime/remote/utils/data.js +0 -1
- package/libx/_runtime/sqlite/Service.js +1 -2
- package/libx/_runtime/ucl/Service.js +37 -78
- package/libx/common/assert/index.js +22 -21
- package/libx/common/assert/type-relaxed.js +39 -0
- package/libx/common/assert/utils.js +3 -2
- package/libx/common/assert/validation.js +3 -8
- package/libx/common/utils/index.js +5 -0
- package/libx/common/utils/path.js +51 -0
- package/libx/odata/ODataAdapter.js +126 -0
- package/libx/odata/index.js +15 -2
- package/libx/odata/middleware/batch.js +320 -84
- package/libx/odata/middleware/body-parser.js +33 -0
- package/libx/odata/middleware/create.js +44 -59
- package/libx/odata/middleware/delete.js +23 -12
- package/libx/odata/middleware/error.js +30 -6
- package/libx/odata/middleware/metadata.js +38 -26
- package/libx/odata/middleware/operation.js +93 -69
- package/libx/odata/middleware/parse.js +6 -8
- package/libx/odata/middleware/read.js +117 -93
- package/libx/odata/middleware/service-document.js +22 -19
- package/libx/odata/middleware/stream.js +54 -56
- package/libx/odata/middleware/update.js +79 -87
- package/libx/odata/parse/afterburner.js +191 -175
- package/libx/odata/parse/cqn2odata.js +5 -5
- package/libx/odata/parse/grammar.peggy +27 -20
- package/libx/odata/parse/multipartToJson.js +17 -9
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +14 -6
- package/libx/odata/utils/index.js +84 -12
- package/libx/odata/utils/metadata.js +161 -0
- package/libx/odata/utils/postProcess.js +89 -0
- package/libx/odata/utils/readAfterWrite.js +134 -17
- package/libx/odata/utils/result.js +36 -142
- package/libx/outbox/index.js +4 -3
- package/libx/rest/RestAdapter.js +115 -182
- package/libx/rest/middleware/create.js +28 -24
- package/libx/rest/middleware/delete.js +7 -10
- package/libx/rest/middleware/error.js +26 -16
- package/libx/rest/middleware/operation.js +48 -41
- package/libx/rest/middleware/parse.js +128 -126
- package/libx/rest/middleware/read.js +20 -27
- package/libx/rest/middleware/update.js +26 -31
- package/package.json +17 -8
- package/server.js +4 -2
- package/apis/cds.d.ts +0 -3
- package/apis/core.d.ts +0 -21
- package/apis/cqn.d.ts +0 -18
- package/apis/csn.d.ts +0 -21
- package/apis/events.d.ts +0 -18
- package/apis/internal/inference.d.ts +0 -18
- package/apis/linked.d.ts +0 -18
- package/apis/log.d.ts +0 -20
- package/apis/models.d.ts +0 -18
- package/apis/ql.d.ts +0 -18
- package/apis/reflect.d.ts +0 -32
- package/apis/server.d.ts +0 -18
- package/apis/services.d.ts +0 -22
- package/bin/cds-serve.js +0 -56
- package/lib/compile/to/gql.js +0 -15
- package/lib/srv/protocols/_legacy.js +0 -44
- package/lib/utils/jest.js +0 -43
- package/libx/_runtime/auth/index.js +0 -193
- package/libx/_runtime/auth/strategies/JWT.js +0 -37
- package/libx/_runtime/auth/strategies/basic.js +0 -20
- package/libx/_runtime/auth/strategies/dummy.js +0 -14
- package/libx/_runtime/auth/strategies/ias-auth.js +0 -1
- package/libx/_runtime/auth/strategies/mock.js +0 -77
- package/libx/_runtime/auth/strategies/xssecUtils.js +0 -93
- package/libx/_runtime/auth/strategies/xsuaa.js +0 -38
- package/libx/_runtime/common/perf/index.js +0 -19
- package/libx/_runtime/common/utils/ensureIEEE754.js +0 -29
- package/libx/_runtime/fiori/draft.js +0 -2
- package/libx/_runtime/fiori/generic/activate.js +0 -190
- package/libx/_runtime/fiori/generic/before.js +0 -201
- package/libx/_runtime/fiori/generic/cancel.js +0 -19
- package/libx/_runtime/fiori/generic/delete.js +0 -21
- package/libx/_runtime/fiori/generic/edit.js +0 -157
- package/libx/_runtime/fiori/generic/index.js +0 -25
- package/libx/_runtime/fiori/generic/new.js +0 -82
- package/libx/_runtime/fiori/generic/patch.js +0 -101
- package/libx/_runtime/fiori/generic/prepare.js +0 -57
- package/libx/_runtime/fiori/generic/read.js +0 -1340
- package/libx/_runtime/fiori/generic/readOverDraft.js +0 -146
- package/libx/_runtime/fiori/utils/csn.js +0 -13
- package/libx/_runtime/fiori/utils/delete.js +0 -114
- package/libx/_runtime/fiori/utils/handler.js +0 -264
- package/libx/_runtime/fiori/utils/lockInfo.js +0 -27
- package/libx/_runtime/fiori/utils/req.js +0 -23
- package/libx/_runtime/fiori/utils/stream.js +0 -36
- package/libx/_runtime/fiori/utils/where.js +0 -254
- package/libx/_runtime/index.js +0 -22
- package/libx/odata/utils/handler.js +0 -120
- package/libx/odata/utils/metaInfo.js +0 -410
- package/libx/odata/utils/path.js +0 -75
- package/libx/rest/RestRequest.js +0 -32
- package/libx/rest/index.js +0 -3
- package/libx/rest/readme.md +0 -1
- /package/libx/common/assert/{type.js → type-strict.js} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
|
-
const { AsyncResource } = require('async_hooks')
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
const { AsyncResource } = require('async_hooks')
|
|
5
4
|
const express = require('express')
|
|
6
5
|
const { STATUS_CODES } = require('http')
|
|
7
6
|
const qs = require('querystring')
|
|
8
7
|
const { URL } = require('url')
|
|
9
8
|
|
|
10
9
|
const multipartToJson = require('../parse/multipartToJson')
|
|
10
|
+
const { getBoundary } = require('../utils')
|
|
11
11
|
|
|
12
12
|
const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
|
|
13
13
|
const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
|
|
@@ -47,7 +47,7 @@ const _validateBatch = body => {
|
|
|
47
47
|
if (typeof request !== 'object')
|
|
48
48
|
throw _deserializationError(`Element of 'requests' array at index ${i} must be type of 'object'.`)
|
|
49
49
|
|
|
50
|
-
const { id, method, url, body, atomicityGroup
|
|
50
|
+
const { id, method, url, body, atomicityGroup } = request
|
|
51
51
|
|
|
52
52
|
_validateProperty('id', id, 'string')
|
|
53
53
|
|
|
@@ -83,20 +83,31 @@ const _validateBatch = body => {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
if (url.startsWith('$')) {
|
|
87
|
+
request.dependsOn ??= []
|
|
88
|
+
const dependencyId = url.split('/')[0].replace(/^\$/, '')
|
|
89
|
+
if (!request.dependsOn.includes(dependencyId)) {
|
|
90
|
+
request.dependsOn.push(dependencyId)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (request.dependsOn) {
|
|
95
|
+
_validateProperty('dependsOn', request.dependsOn, 'Array')
|
|
96
|
+
request.dependsOn.forEach(dependsOnId => {
|
|
89
97
|
_validateProperty('dependent request ID', dependsOnId, 'string')
|
|
90
98
|
|
|
91
99
|
const dependency = ids[dependsOnId]
|
|
92
|
-
if (!dependency)
|
|
93
|
-
throw _deserializationError(`Request ID '${dependsOnId}' used in dependsOn has not been defined before.`)
|
|
94
|
-
|
|
95
|
-
const dependencyAtomicityGroup = dependency.atomicityGroup
|
|
96
|
-
if (dependencyAtomicityGroup && !dependsOn.includes(dependencyAtomicityGroup))
|
|
100
|
+
if (!dependency) {
|
|
97
101
|
throw _deserializationError(
|
|
98
|
-
`
|
|
102
|
+
`"${dependsOnId}" does not match the id or atomicity group of any preceding request`
|
|
99
103
|
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// automatically add the atomicityGroup of the dependency as a dependency (actually a client error)
|
|
107
|
+
const dag = dependency.atomicityGroup
|
|
108
|
+
if (dag && dag !== atomicityGroup && !request.dependsOn.includes(dag)) {
|
|
109
|
+
request.dependsOn.push(dag)
|
|
110
|
+
}
|
|
100
111
|
})
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -108,6 +119,7 @@ const _validateBatch = body => {
|
|
|
108
119
|
return ids
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
// REVISIT: Why not simply use {__proto__:req, ...}?
|
|
111
123
|
const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
112
124
|
const { id, method, url } = request
|
|
113
125
|
const ret = { id }
|
|
@@ -123,11 +135,16 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
123
135
|
const u = new URL(url, 'http://cap')
|
|
124
136
|
req.query = qs.parse(u.search.slice(1))
|
|
125
137
|
req.headers = request.headers || {}
|
|
138
|
+
if (request.content_id) req.headers['content-id'] = request.content_id
|
|
126
139
|
req.body = request.body
|
|
140
|
+
if (_req._login) req._login = _req._login
|
|
127
141
|
|
|
128
142
|
const res = (ret.res = new express.response.constructor(req))
|
|
129
143
|
res.__proto__ = express.response
|
|
130
144
|
|
|
145
|
+
// REVISIT: mark as subrequest
|
|
146
|
+
req._subrequest = true
|
|
147
|
+
|
|
131
148
|
// express internals
|
|
132
149
|
res.app = _res.app
|
|
133
150
|
|
|
@@ -135,10 +152,11 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
135
152
|
req.res = res
|
|
136
153
|
|
|
137
154
|
// resolve promise for subrequest via res.end()
|
|
138
|
-
ret.promise = new Promise((resolve,
|
|
155
|
+
ret.promise = new Promise((resolve, reject) => {
|
|
139
156
|
res.end = (chunk, encoding) => {
|
|
140
157
|
res._chunk = chunk
|
|
141
158
|
res._encoding = encoding
|
|
159
|
+
if (res.statusCode >= 400) return reject(ret)
|
|
142
160
|
resolve(ret)
|
|
143
161
|
}
|
|
144
162
|
})
|
|
@@ -146,35 +164,156 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
146
164
|
return ret
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
const
|
|
167
|
+
const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
|
|
168
|
+
if (group) {
|
|
169
|
+
res.write(`--${boundary}${CRLF}`)
|
|
170
|
+
res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
|
|
171
|
+
}
|
|
172
|
+
const header = group || boundary
|
|
173
|
+
if (rejected) {
|
|
174
|
+
const resp = responses.find(r => r.status === 'fail')
|
|
175
|
+
if (resp.separator && res._writeSeparator) res.write(resp.separator)
|
|
176
|
+
resp.txt.forEach(txt => {
|
|
177
|
+
res.write(`--${header}${CRLF}`)
|
|
178
|
+
res.write(`${txt}`)
|
|
179
|
+
})
|
|
180
|
+
} else {
|
|
181
|
+
for (const resp of responses) {
|
|
182
|
+
if (resp.separator) res.write(resp.separator)
|
|
183
|
+
resp.txt.forEach(txt => {
|
|
184
|
+
res.write(`--${header}${CRLF}`)
|
|
185
|
+
res.write(`${txt}`)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (group) res.write(`${CRLF}--${group}--${CRLF}`)
|
|
190
|
+
// indicates that we need to write a potential separator before the next error response
|
|
191
|
+
res._writeSeparator = true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const _writeResponseJson = (responses, res) => {
|
|
195
|
+
for (const resp of responses) {
|
|
196
|
+
if (resp.separator) res.write(resp.separator)
|
|
197
|
+
resp.txt.forEach(txt => res.write(txt))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let error_mws
|
|
202
|
+
const _getNextForLookalike = lookalike => {
|
|
203
|
+
error_mws ??= cds.middlewares.after.filter(mw => mw.length === 4) // error middleware has 4 params
|
|
204
|
+
return err => {
|
|
205
|
+
let _err = err
|
|
206
|
+
let _next_called
|
|
207
|
+
const _next = e => {
|
|
208
|
+
_err = e
|
|
209
|
+
_next_called = true
|
|
210
|
+
}
|
|
211
|
+
for (const mw of error_mws) {
|
|
212
|
+
_next_called = false
|
|
213
|
+
mw(_err, lookalike.req, lookalike.res, _next)
|
|
214
|
+
if (!_next_called) break //> next chain was interrupted -> done
|
|
215
|
+
}
|
|
216
|
+
if (_next_called) {
|
|
217
|
+
// here, final error middleware called next (which actually shouldn't happen!)
|
|
218
|
+
if (_err.statusCode) lookalike.res.status(_err.statusCode)
|
|
219
|
+
if (typeof _err === 'object') lookalike.res.json({ error: _err })
|
|
220
|
+
else lookalike.res.send(_err)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// REVISIT: This looks frightening -> need to review
|
|
226
|
+
const _transaction = async srv => {
|
|
150
227
|
return new Promise(res => {
|
|
151
228
|
const ret = {}
|
|
152
|
-
srv.tx(
|
|
229
|
+
const _tx = (ret._tx = srv.tx(
|
|
153
230
|
async () =>
|
|
154
231
|
(ret.promise = new Promise((resolve, reject) => {
|
|
155
232
|
const proms = []
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
233
|
+
// It's important to run `makePromise` in the current execution context (cb of srv.tx),
|
|
234
|
+
// otherwise, it will use a different transaction.
|
|
235
|
+
// REVISIT: This looks frightening -> need to review
|
|
236
|
+
ret.add = AsyncResource.bind(function (makePromise) {
|
|
237
|
+
const p = makePromise()
|
|
238
|
+
proms.push(p)
|
|
239
|
+
return p
|
|
162
240
|
})
|
|
163
|
-
ret.done = function () {
|
|
164
|
-
|
|
241
|
+
ret.done = async function () {
|
|
242
|
+
const result = await Promise.allSettled(proms)
|
|
243
|
+
if (result.some(r => r.status === 'rejected')) {
|
|
244
|
+
reject()
|
|
245
|
+
// REVISIT: workaround to wait for commit/rollback
|
|
246
|
+
await _tx
|
|
247
|
+
return 'rejected'
|
|
248
|
+
}
|
|
249
|
+
resolve(result)
|
|
250
|
+
// REVISIT: workaround to wait for commit/rollback
|
|
251
|
+
await _tx
|
|
165
252
|
}
|
|
166
253
|
res(ret)
|
|
167
254
|
}))
|
|
168
|
-
)
|
|
255
|
+
))
|
|
169
256
|
})
|
|
170
257
|
}
|
|
171
258
|
|
|
259
|
+
const _tx_done = async (tx, responses, isJson) => {
|
|
260
|
+
let rejected
|
|
261
|
+
try {
|
|
262
|
+
rejected = await tx.done()
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
265
|
+
rejected = 'rejected'
|
|
266
|
+
// construct commit error
|
|
267
|
+
let statusCode = e.statusCode || e.status || (e.code && Number(e.code))
|
|
268
|
+
if (isNaN(statusCode)) statusCode = 500
|
|
269
|
+
const code = String(e.code || statusCode)
|
|
270
|
+
const message = e.message || 'Internal Server Error'
|
|
271
|
+
const error = { error: { ...e, code, message } }
|
|
272
|
+
// replace all responses with commit error
|
|
273
|
+
for (const res of responses) {
|
|
274
|
+
res.status = 'fail'
|
|
275
|
+
// REVISIT: should error go through any error middleware/ customization logic?
|
|
276
|
+
if (isJson) {
|
|
277
|
+
let txt = ''
|
|
278
|
+
for (let i = 0; i < res.txt.length; i++) txt += Buffer.isBuffer(res.txt[i]) ? res.txt[i].toString() : res.txt[i]
|
|
279
|
+
txt = JSON.parse(txt)
|
|
280
|
+
txt.status = statusCode
|
|
281
|
+
txt.body = error
|
|
282
|
+
// REVISIT: content-length needed? not there in multipart case...
|
|
283
|
+
delete txt.headers['content-length']
|
|
284
|
+
res.txt = [JSON.stringify(txt)]
|
|
285
|
+
} else {
|
|
286
|
+
let txt = res.txt[0]
|
|
287
|
+
txt = txt.replace(/HTTP\/1\.1 \d\d\d \w+/, `HTTP/1.1 ${statusCode} ${message}`)
|
|
288
|
+
txt = txt.split(/\r\n/)
|
|
289
|
+
txt.splice(-1, 1, JSON.stringify(error))
|
|
290
|
+
txt = txt.join('\r\n')
|
|
291
|
+
res.txt = [txt]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return rejected
|
|
296
|
+
}
|
|
297
|
+
|
|
172
298
|
const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
|
|
173
299
|
body ??= req.body
|
|
174
300
|
ct ??= 'JSON'
|
|
175
|
-
|
|
301
|
+
// respond with requested content type (i.e., accept) with fallback to the content type used in the request
|
|
302
|
+
let isJson = ct === 'JSON'
|
|
303
|
+
if (req.headers.accept) {
|
|
304
|
+
if (req.headers.accept.indexOf('multipart/mixed') > -1) isJson = false
|
|
305
|
+
else if (req.headers.accept.indexOf('application/json') > -1) isJson = true
|
|
306
|
+
}
|
|
176
307
|
const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
|
|
177
308
|
|
|
309
|
+
// continue-on-error defaults to true in json batch
|
|
310
|
+
let continue_on_error = req.headers.prefer?.match(/odata\.continue-on-error(=(\w+))?/)
|
|
311
|
+
if (!continue_on_error) {
|
|
312
|
+
continue_on_error = isJson ? true : false
|
|
313
|
+
} else {
|
|
314
|
+
continue_on_error = continue_on_error[2] === 'false' ? false : true
|
|
315
|
+
}
|
|
316
|
+
|
|
178
317
|
try {
|
|
179
318
|
const ids = _validateBatch(body) // REVISIT: we will not be able to validate the whole once we stream
|
|
180
319
|
|
|
@@ -183,41 +322,130 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
183
322
|
let previousAtomicityGroup
|
|
184
323
|
let separator
|
|
185
324
|
let tx
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
325
|
+
let responses
|
|
326
|
+
|
|
327
|
+
// IMPORTANT: Avoid sending headers and responses too eagerly, as we might still have to send a 401
|
|
328
|
+
let sendPreludeOnce = () => {
|
|
329
|
+
res.setHeader('Content-Type', isJson ? CT.JSON : CT.MULTIPART + ';boundary=' + boundary)
|
|
330
|
+
res.status(200)
|
|
331
|
+
res.write(isJson ? '{"responses":[' : '')
|
|
332
|
+
sendPreludeOnce = () => {} //> only once
|
|
333
|
+
}
|
|
191
334
|
|
|
192
335
|
const { requests } = body
|
|
193
336
|
for await (const request of requests) {
|
|
337
|
+
// for json payloads, normalize headers to lowercase
|
|
338
|
+
if (ct === 'JSON') {
|
|
339
|
+
request.headers = request.headers
|
|
340
|
+
? Object.keys(request.headers).reduce((acc, cur) => {
|
|
341
|
+
acc[cur.toLowerCase()] = request.headers[cur]
|
|
342
|
+
return acc
|
|
343
|
+
}, {})
|
|
344
|
+
: {}
|
|
345
|
+
}
|
|
346
|
+
|
|
194
347
|
const { atomicityGroup } = request
|
|
195
348
|
|
|
196
349
|
if (!atomicityGroup || atomicityGroup !== previousAtomicityGroup) {
|
|
197
|
-
if (tx)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
350
|
+
if (tx) {
|
|
351
|
+
// Each change in `atomicityGroup` results in a new transaction. We execute them in sequence to avoid too many database connections.
|
|
352
|
+
// In the future, we might make this configurable (e.g. allow X parallel connections per HTTP request).
|
|
353
|
+
const rejected = await _tx_done(tx, responses, isJson)
|
|
354
|
+
if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
|
|
355
|
+
else sendPreludeOnce()
|
|
356
|
+
isJson
|
|
357
|
+
? _writeResponseJson(responses, res)
|
|
358
|
+
: _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
|
|
359
|
+
if (rejected && !continue_on_error) {
|
|
360
|
+
tx = null
|
|
361
|
+
break
|
|
362
|
+
}
|
|
363
|
+
}
|
|
201
364
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
await Promise.allSettled(dependencies)
|
|
365
|
+
responses = []
|
|
366
|
+
tx = await _transaction(srv)
|
|
367
|
+
if (atomicityGroup) ids[atomicityGroup].promise = tx._tx
|
|
206
368
|
}
|
|
207
369
|
|
|
208
|
-
tx.add(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
370
|
+
tx.add(() => {
|
|
371
|
+
return (request.promise = (async () => {
|
|
372
|
+
const dependencies = request.dependsOn?.filter(id => id !== request.atomicityGroup).map(id => ids[id].promise)
|
|
373
|
+
if (dependencies) {
|
|
374
|
+
// first, wait for dependencies
|
|
375
|
+
const results = await Promise.allSettled(dependencies)
|
|
376
|
+
const dependendOnFailed = results.some(({ status }) => status === 'rejected')
|
|
377
|
+
if (dependendOnFailed) {
|
|
378
|
+
tx.id = request.id
|
|
379
|
+
tx.res = {
|
|
380
|
+
getHeaders: () => {},
|
|
381
|
+
statusCode: 424,
|
|
382
|
+
_chunk: JSON.stringify({
|
|
383
|
+
code: '424',
|
|
384
|
+
message: 'Failed Dependency'
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
throw tx
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const dependsOnId = request.url.split('/')[0].replace(/^\$/, '')
|
|
391
|
+
if (dependsOnId in ids) {
|
|
392
|
+
const dependentResult = results.find(r => r.value.id === dependsOnId)
|
|
393
|
+
const dependentOnUrl = dependentResult.value.req.originalUrl
|
|
394
|
+
const dependentOnResult = JSON.parse(dependentResult.value.res._chunk)
|
|
395
|
+
const recentUrl = request.url
|
|
396
|
+
const cqn = cds.odata.parse(dependentOnUrl, { service: srv, baseUrl: req.baseUrl, strict: true })
|
|
397
|
+
const { target } = cqn
|
|
398
|
+
const keyString =
|
|
399
|
+
'(' +
|
|
400
|
+
target.keys
|
|
401
|
+
.filter(k => !k.isAssociation)
|
|
402
|
+
.map(k => {
|
|
403
|
+
let v = dependentOnResult[k.name]
|
|
404
|
+
if (typeof v === 'string' && k._type !== 'cds.UUID') v = `'${v}'`
|
|
405
|
+
return k.name + '=' + v
|
|
406
|
+
})
|
|
407
|
+
.join(',') +
|
|
408
|
+
')'
|
|
409
|
+
request.url = recentUrl.replace(`$${dependsOnId}`, dependentOnUrl + keyString)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
213
412
|
|
|
214
|
-
|
|
413
|
+
// REVIST: That sends each request through the whole middleware chain again and again, including authentication and authorization.
|
|
414
|
+
// -> We should optimize this!
|
|
415
|
+
const lookalike = _createExpressReqResLookalike(request, req, res)
|
|
416
|
+
const lookalike_next = _getNextForLookalike(lookalike)
|
|
417
|
+
router.handle(lookalike.req, lookalike.res, lookalike_next)
|
|
418
|
+
return lookalike.promise
|
|
419
|
+
})())
|
|
420
|
+
})
|
|
421
|
+
.then(req => {
|
|
422
|
+
const resp = { status: 'ok' }
|
|
423
|
+
if (separator) resp.separator = separator
|
|
424
|
+
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
425
|
+
resp.txt = _formatResponse(req, atomicityGroup)
|
|
426
|
+
responses.push(resp)
|
|
427
|
+
})
|
|
428
|
+
.catch(failedReq => {
|
|
429
|
+
const resp = { status: 'fail' }
|
|
430
|
+
if (separator) resp.separator = separator
|
|
431
|
+
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
432
|
+
resp.txt = _formatResponse(failedReq, atomicityGroup)
|
|
433
|
+
tx.failed = failedReq
|
|
434
|
+
responses.push(resp)
|
|
435
|
+
})
|
|
215
436
|
|
|
216
437
|
previousAtomicityGroup = atomicityGroup
|
|
217
438
|
}
|
|
218
439
|
|
|
219
|
-
if (tx)
|
|
220
|
-
|
|
440
|
+
if (tx) {
|
|
441
|
+
// The last open transaction must be finished
|
|
442
|
+
const rejected = await _tx_done(tx, responses, isJson)
|
|
443
|
+
if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
|
|
444
|
+
else sendPreludeOnce()
|
|
445
|
+
isJson
|
|
446
|
+
? _writeResponseJson(responses, res)
|
|
447
|
+
: _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
|
|
448
|
+
} else sendPreludeOnce()
|
|
221
449
|
res.write(isJson ? ']}' : `${CRLF}--${boundary}--${CRLF}`)
|
|
222
450
|
res.end()
|
|
223
451
|
|
|
@@ -231,8 +459,8 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
231
459
|
* multipart/mixed
|
|
232
460
|
*/
|
|
233
461
|
|
|
234
|
-
const _multipartBatch = (srv, router
|
|
235
|
-
const boundary = req
|
|
462
|
+
const _multipartBatch = async (srv, router, req, res, next) => {
|
|
463
|
+
const boundary = getBoundary(req)
|
|
236
464
|
if (!boundary) return next(cds.error('No boundary found in Content-Type header', { code: 400 }))
|
|
237
465
|
|
|
238
466
|
try {
|
|
@@ -243,20 +471,22 @@ const _multipartBatch = (srv, router) => async (req, res, next) => {
|
|
|
243
471
|
}
|
|
244
472
|
}
|
|
245
473
|
|
|
246
|
-
const _formatResponseMultipart =
|
|
247
|
-
const {
|
|
474
|
+
const _formatResponseMultipart = request => {
|
|
475
|
+
const { res: response } = request
|
|
476
|
+
const content_id = request.req?.headers['content-id']
|
|
248
477
|
|
|
249
|
-
let txt =
|
|
478
|
+
let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
|
|
479
|
+
if (content_id) txt += `content-id: ${content_id}${CRLF}`
|
|
480
|
+
txt += CRLF
|
|
250
481
|
txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
|
|
251
482
|
|
|
252
483
|
// REVISIT: tests require specific sequence
|
|
253
484
|
const headers = {
|
|
254
|
-
|
|
255
|
-
'content-type': '
|
|
256
|
-
...response.getHeaders()
|
|
485
|
+
...response.getHeaders(),
|
|
486
|
+
'content-type': 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
|
|
257
487
|
}
|
|
258
|
-
headers['content-type'] = 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
|
|
259
488
|
delete headers['content-length'] //> REVISIT: expected by tests
|
|
489
|
+
|
|
260
490
|
for (const key in headers) {
|
|
261
491
|
txt += key + ': ' + headers[key] + CRLF
|
|
262
492
|
}
|
|
@@ -271,7 +501,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
|
|
|
271
501
|
let meta = [],
|
|
272
502
|
data = []
|
|
273
503
|
for (const [k, v] of Object.entries(_json)) {
|
|
274
|
-
if (k.startsWith('@')) meta.push(`"${k}"
|
|
504
|
+
if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
|
|
275
505
|
else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
|
|
276
506
|
}
|
|
277
507
|
const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
|
|
@@ -283,7 +513,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
|
|
|
283
513
|
}
|
|
284
514
|
}
|
|
285
515
|
|
|
286
|
-
|
|
516
|
+
return [txt]
|
|
287
517
|
}
|
|
288
518
|
|
|
289
519
|
/*
|
|
@@ -296,46 +526,52 @@ const _formatStatics = {
|
|
|
296
526
|
close: Buffer.from('}')
|
|
297
527
|
}
|
|
298
528
|
|
|
299
|
-
const _formatResponseJson = (request,
|
|
529
|
+
const _formatResponseJson = (request, atomicityGroup) => {
|
|
300
530
|
const { id, res: response } = request
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
531
|
+
|
|
532
|
+
const chunk = {
|
|
533
|
+
id,
|
|
534
|
+
status: response.statusCode,
|
|
535
|
+
headers: {
|
|
536
|
+
...response.getHeaders(),
|
|
537
|
+
'content-type': 'application/json' //> REVISIT: why?
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
|
|
541
|
+
const raw = Buffer.from(JSON.stringify(chunk))
|
|
542
|
+
|
|
543
|
+
// body?
|
|
544
|
+
if (!response._chunk) return [raw]
|
|
545
|
+
|
|
312
546
|
// change last "}" into ","
|
|
313
547
|
raw[raw.byteLength - 1] = _formatStatics.comma
|
|
314
|
-
|
|
315
|
-
res.write(_formatStatics.body)
|
|
316
|
-
res.write(response._chunk)
|
|
317
|
-
res.write(_formatStatics.close)
|
|
548
|
+
return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
|
|
318
549
|
}
|
|
319
550
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
551
|
+
/*
|
|
552
|
+
* exports
|
|
553
|
+
*/
|
|
554
|
+
|
|
555
|
+
module.exports = adapter => {
|
|
556
|
+
const { options: config, router, service } = adapter
|
|
557
|
+
const { max_content_length } = config //> max_content_length is unofficial config
|
|
323
558
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
559
|
+
const options = { type: '*/*' }
|
|
560
|
+
if (max_content_length) options.limit = max_content_length
|
|
561
|
+
const textBodyParser = express.text(options)
|
|
327
562
|
|
|
328
|
-
return function
|
|
563
|
+
return function odata_batch(req, res, next) {
|
|
329
564
|
if (req.headers['content-type'].includes('application/json')) {
|
|
330
|
-
return
|
|
565
|
+
return _processBatch(service, router, req, res, next)
|
|
331
566
|
}
|
|
332
567
|
|
|
333
568
|
if (req.headers['content-type'].includes('multipart/mixed')) {
|
|
334
|
-
return
|
|
335
|
-
|
|
569
|
+
return textBodyParser(req, res, function odata_batch_next(err) {
|
|
570
|
+
if (err) return next(err)
|
|
571
|
+
return _multipartBatch(service, router, req, res, next)
|
|
336
572
|
})
|
|
337
573
|
}
|
|
338
574
|
|
|
339
|
-
throw cds.error('Batch requests must have content type multipart/mixed or application/json', {
|
|
575
|
+
throw cds.error('Batch requests must have content type multipart/mixed or application/json', { statusCode: 400 })
|
|
340
576
|
}
|
|
341
577
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
|
|
3
|
+
// basically express.json() with string representation of body stored in req._raw for recovery
|
|
4
|
+
// REVISIT: why do we need our own body parser? Only because of req._raw?
|
|
5
|
+
module.exports = function bodyParser4(adapter, options = {}) {
|
|
6
|
+
if (!options.type) options.type = 'json'
|
|
7
|
+
const { max_content_length } = adapter.options //> max_content_length is unofficial config
|
|
8
|
+
if (!options.limit && max_content_length) options.limit = max_content_length
|
|
9
|
+
const textParser = express.text(options)
|
|
10
|
+
return function http_body_parser(req, res, next) {
|
|
11
|
+
if (typeof req.body === 'object') {
|
|
12
|
+
//> body already deserialized (e.g., batch subrequest or custom body parser)
|
|
13
|
+
if (!req._raw) req._raw = JSON.stringify(req.body) //> ensure req._raw is set
|
|
14
|
+
return next()
|
|
15
|
+
}
|
|
16
|
+
textParser(req, res, function http_body_parser_next(err) {
|
|
17
|
+
if (err) return next(Object.assign(err, { statusCode: 400 }))
|
|
18
|
+
if (typeof req.body !== 'string') return next()
|
|
19
|
+
|
|
20
|
+
req._raw = req.body || '{}'
|
|
21
|
+
try {
|
|
22
|
+
req.body = JSON.parse(req._raw)
|
|
23
|
+
if (typeof req.body !== 'object') {
|
|
24
|
+
res.status(400).json({ error: { message: 'Expected JSON object in body', statusCode: 400, code: '400' } })
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return next(Object.assign(e, { statusCode: 400 }))
|
|
29
|
+
}
|
|
30
|
+
next()
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|