@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.
- package/CHANGELOG.md +126 -3655
- 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 +29 -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/insert.js +2 -2
- 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 +0 -12
- 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 +261 -72
- 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 +8 -8
- 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 +19 -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/tasks/enterprise-messaging-deploy.js +1 -1
- 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,18 +1,20 @@
|
|
|
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' }
|
|
14
14
|
const CRLF = '\r\n'
|
|
15
15
|
|
|
16
|
+
const CONTINUE_ON_ERROR = /odata\.continue-on-error/
|
|
17
|
+
|
|
16
18
|
/*
|
|
17
19
|
* common
|
|
18
20
|
*/
|
|
@@ -47,7 +49,7 @@ const _validateBatch = body => {
|
|
|
47
49
|
if (typeof request !== 'object')
|
|
48
50
|
throw _deserializationError(`Element of 'requests' array at index ${i} must be type of 'object'.`)
|
|
49
51
|
|
|
50
|
-
const { id, method, url, body, atomicityGroup
|
|
52
|
+
const { id, method, url, body, atomicityGroup } = request
|
|
51
53
|
|
|
52
54
|
_validateProperty('id', id, 'string')
|
|
53
55
|
|
|
@@ -83,9 +85,17 @@ const _validateBatch = body => {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
if (url.startsWith('$')) {
|
|
89
|
+
request.dependsOn ??= []
|
|
90
|
+
const dependencyId = url.split('/')[0].replace(/^\$/, '')
|
|
91
|
+
if (!request.dependsOn.includes(dependencyId)) {
|
|
92
|
+
request.dependsOn.push(dependencyId)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (request.dependsOn) {
|
|
97
|
+
_validateProperty('dependsOn', request.dependsOn, 'Array')
|
|
98
|
+
request.dependsOn.forEach(dependsOnId => {
|
|
89
99
|
_validateProperty('dependent request ID', dependsOnId, 'string')
|
|
90
100
|
|
|
91
101
|
const dependency = ids[dependsOnId]
|
|
@@ -93,10 +103,12 @@ const _validateBatch = body => {
|
|
|
93
103
|
throw _deserializationError(`Request ID '${dependsOnId}' used in dependsOn has not been defined before.`)
|
|
94
104
|
|
|
95
105
|
const dependencyAtomicityGroup = dependency.atomicityGroup
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
106
|
+
if (
|
|
107
|
+
dependencyAtomicityGroup &&
|
|
108
|
+
dependencyAtomicityGroup !== atomicityGroup &&
|
|
109
|
+
!request.dependsOn.includes(dependencyAtomicityGroup)
|
|
110
|
+
)
|
|
111
|
+
request.dependsOn.push(dependencyAtomicityGroup)
|
|
100
112
|
})
|
|
101
113
|
}
|
|
102
114
|
|
|
@@ -108,6 +120,7 @@ const _validateBatch = body => {
|
|
|
108
120
|
return ids
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
// REVISIT: Why not simply use {__proto__:req, ...}?
|
|
111
124
|
const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
112
125
|
const { id, method, url } = request
|
|
113
126
|
const ret = { id }
|
|
@@ -123,11 +136,16 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
123
136
|
const u = new URL(url, 'http://cap')
|
|
124
137
|
req.query = qs.parse(u.search.slice(1))
|
|
125
138
|
req.headers = request.headers || {}
|
|
139
|
+
if (request.content_id) req.headers['content-id'] = request.content_id
|
|
126
140
|
req.body = request.body
|
|
141
|
+
if (_req._login) req._login = _req._login
|
|
127
142
|
|
|
128
143
|
const res = (ret.res = new express.response.constructor(req))
|
|
129
144
|
res.__proto__ = express.response
|
|
130
145
|
|
|
146
|
+
// REVISIT: mark as subrequest
|
|
147
|
+
req._subrequest = true
|
|
148
|
+
|
|
131
149
|
// express internals
|
|
132
150
|
res.app = _res.app
|
|
133
151
|
|
|
@@ -135,10 +153,11 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
135
153
|
req.res = res
|
|
136
154
|
|
|
137
155
|
// resolve promise for subrequest via res.end()
|
|
138
|
-
ret.promise = new Promise((resolve,
|
|
156
|
+
ret.promise = new Promise((resolve, reject) => {
|
|
139
157
|
res.end = (chunk, encoding) => {
|
|
140
158
|
res._chunk = chunk
|
|
141
159
|
res._encoding = encoding
|
|
160
|
+
if (res.statusCode >= 400) return reject(ret)
|
|
142
161
|
resolve(ret)
|
|
143
162
|
}
|
|
144
163
|
})
|
|
@@ -146,22 +165,91 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
146
165
|
return ret
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
const
|
|
168
|
+
const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
|
|
169
|
+
if (group) {
|
|
170
|
+
res.write(`--${boundary}${CRLF}`)
|
|
171
|
+
res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
|
|
172
|
+
}
|
|
173
|
+
const header = group || boundary
|
|
174
|
+
if (rejected) {
|
|
175
|
+
const resp = responses.find(r => r.status === 'fail')
|
|
176
|
+
if (resp.separator && res._writeSeparator) res.write(resp.separator)
|
|
177
|
+
resp.txt.forEach(txt => {
|
|
178
|
+
res.write(`--${header}${CRLF}`)
|
|
179
|
+
res.write(`${txt}`)
|
|
180
|
+
})
|
|
181
|
+
} else {
|
|
182
|
+
for (const resp of responses) {
|
|
183
|
+
if (resp.separator) res.write(resp.separator)
|
|
184
|
+
resp.txt.forEach(txt => {
|
|
185
|
+
res.write(`--${header}${CRLF}`)
|
|
186
|
+
res.write(`${txt}`)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (group) res.write(`${CRLF}--${group}--${CRLF}`)
|
|
191
|
+
// indicates that we need to write a potential separator before the next error response
|
|
192
|
+
res._writeSeparator = true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const _writeResponseJson = (responses, res) => {
|
|
196
|
+
for (const resp of responses) {
|
|
197
|
+
if (resp.separator) res.write(resp.separator)
|
|
198
|
+
resp.txt.forEach(txt => res.write(txt))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let error_mws
|
|
203
|
+
const _getNextForLookalike = lookalike => {
|
|
204
|
+
error_mws ??= cds.middlewares.after.filter(mw => mw.length === 4) // error middleware has 4 params
|
|
205
|
+
return err => {
|
|
206
|
+
let _err = err
|
|
207
|
+
let _next_called
|
|
208
|
+
const _next = e => {
|
|
209
|
+
_err = e
|
|
210
|
+
_next_called = true
|
|
211
|
+
}
|
|
212
|
+
for (const mw of error_mws) {
|
|
213
|
+
_next_called = false
|
|
214
|
+
mw(_err, lookalike.req, lookalike.res, _next)
|
|
215
|
+
if (!_next_called) break //> next chain was interrupted -> done
|
|
216
|
+
}
|
|
217
|
+
if (_next_called) {
|
|
218
|
+
// here, final error middleware called next (which actually shouldn't happen!)
|
|
219
|
+
if (_err.statusCode) lookalike.res.status(_err.statusCode)
|
|
220
|
+
if (typeof _err === 'object') lookalike.res.json({ error: _err })
|
|
221
|
+
else lookalike.res.send(_err)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// REVISIT: This looks frightening -> need to review
|
|
227
|
+
const _transaction = async srv => {
|
|
150
228
|
return new Promise(res => {
|
|
151
229
|
const ret = {}
|
|
152
|
-
srv.tx(
|
|
230
|
+
const _tx = srv.tx(
|
|
153
231
|
async () =>
|
|
154
232
|
(ret.promise = new Promise((resolve, reject) => {
|
|
155
233
|
const proms = []
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
234
|
+
// It's important to run `makePromise` in the current execution context (cb of srv.tx),
|
|
235
|
+
// otherwise, it will use a different transaction.
|
|
236
|
+
// REVISIT: This looks frightening -> need to review
|
|
237
|
+
ret.add = AsyncResource.bind(function (makePromise) {
|
|
238
|
+
const p = makePromise()
|
|
239
|
+
proms.push(p)
|
|
240
|
+
return p
|
|
162
241
|
})
|
|
163
|
-
ret.done = function () {
|
|
164
|
-
|
|
242
|
+
ret.done = async function () {
|
|
243
|
+
const result = await Promise.allSettled(proms)
|
|
244
|
+
if (result.some(r => r.status === 'rejected')) {
|
|
245
|
+
reject()
|
|
246
|
+
// REVISIT: workaround to wait for commit/rollback
|
|
247
|
+
await _tx
|
|
248
|
+
return 'rejected'
|
|
249
|
+
}
|
|
250
|
+
resolve(result)
|
|
251
|
+
// REVISIT: workaround to wait for commit/rollback
|
|
252
|
+
await _tx
|
|
165
253
|
}
|
|
166
254
|
res(ret)
|
|
167
255
|
}))
|
|
@@ -172,7 +260,12 @@ const _transaction = async (srv, router) => {
|
|
|
172
260
|
const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
|
|
173
261
|
body ??= req.body
|
|
174
262
|
ct ??= 'JSON'
|
|
175
|
-
|
|
263
|
+
// respond with requested content type (i.e., accept) with fallback to the content type used in the request
|
|
264
|
+
let isJson = ct === 'JSON'
|
|
265
|
+
if (req.headers.accept) {
|
|
266
|
+
if (req.headers.accept.indexOf('multipart/mixed') > -1) isJson = false
|
|
267
|
+
else if (req.headers.accept.indexOf('application/json') > -1) isJson = true
|
|
268
|
+
}
|
|
176
269
|
const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
|
|
177
270
|
|
|
178
271
|
try {
|
|
@@ -183,41 +276,129 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
183
276
|
let previousAtomicityGroup
|
|
184
277
|
let separator
|
|
185
278
|
let tx
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
279
|
+
let responses
|
|
280
|
+
|
|
281
|
+
// IMPORTANT: Avoid sending headers and responses too eagerly, as we might still have to send a 401
|
|
282
|
+
let sendPreludeOnce = () => {
|
|
283
|
+
res.setHeader('Content-Type', isJson ? CT.JSON : CT.MULTIPART + ';boundary=' + boundary)
|
|
284
|
+
res.status(200)
|
|
285
|
+
res.write(isJson ? '{"responses":[' : '')
|
|
286
|
+
sendPreludeOnce = () => {} //> only once
|
|
287
|
+
}
|
|
191
288
|
|
|
192
289
|
const { requests } = body
|
|
193
290
|
for await (const request of requests) {
|
|
291
|
+
// for json payloads, normalize headers to lowercase
|
|
292
|
+
if (ct === 'JSON') {
|
|
293
|
+
request.headers = request.headers
|
|
294
|
+
? Object.keys(request.headers).reduce((acc, cur) => {
|
|
295
|
+
acc[cur.toLowerCase()] = request.headers[cur]
|
|
296
|
+
return acc
|
|
297
|
+
}, {})
|
|
298
|
+
: {}
|
|
299
|
+
}
|
|
300
|
+
|
|
194
301
|
const { atomicityGroup } = request
|
|
195
302
|
|
|
196
303
|
if (!atomicityGroup || atomicityGroup !== previousAtomicityGroup) {
|
|
197
|
-
if (tx)
|
|
198
|
-
|
|
304
|
+
if (tx) {
|
|
305
|
+
// Each change in `atomicityGroup` results in a new transaction. We execute them in sequence to avoid too many database connections.
|
|
306
|
+
// In the future, we might make this configurable (e.g. allow X parallel connections per HTTP request).
|
|
307
|
+
const rejected = await tx.done()
|
|
308
|
+
if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
|
|
309
|
+
else sendPreludeOnce()
|
|
310
|
+
isJson
|
|
311
|
+
? _writeResponseJson(responses, res)
|
|
312
|
+
: _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
|
|
313
|
+
if (rejected && !CONTINUE_ON_ERROR.test(req.headers.prefer)) {
|
|
314
|
+
tx = null
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
responses = []
|
|
320
|
+
tx = await _transaction(srv)
|
|
199
321
|
if (atomicityGroup) ids[atomicityGroup].promise = tx.promise
|
|
200
322
|
}
|
|
201
323
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
324
|
+
tx.add(() => {
|
|
325
|
+
return (request.promise = (async () => {
|
|
326
|
+
const dependencies = request.dependsOn?.filter(id => id !== request.atomicityGroup).map(id => ids[id].promise)
|
|
327
|
+
if (dependencies) {
|
|
328
|
+
// first, wait for dependencies
|
|
329
|
+
const results = await Promise.allSettled(dependencies)
|
|
330
|
+
const dependendOnFailed = results.some(({ status }) => status === 'rejected')
|
|
331
|
+
if (dependendOnFailed) {
|
|
332
|
+
tx.res = {
|
|
333
|
+
getHeaders: () => {},
|
|
334
|
+
statusCode: 404,
|
|
335
|
+
_chunk: JSON.stringify({
|
|
336
|
+
status: 404,
|
|
337
|
+
message: `Dependency for Request ${request.id} failed to execute`
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
throw tx
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const dependsOnId = request.url.split('/')[0].replace(/^\$/, '')
|
|
344
|
+
if (dependsOnId in ids) {
|
|
345
|
+
const dependentResult = results.find(r => r.value.id === dependsOnId)
|
|
346
|
+
const dependentOnUrl = dependentResult.value.req.originalUrl
|
|
347
|
+
const dependentOnResult = JSON.parse(dependentResult.value.res._chunk)
|
|
348
|
+
const recentUrl = request.url
|
|
349
|
+
const cqn = cds.odata.parse(dependentOnUrl, { service: srv, baseUrl: req.baseUrl, strict: true })
|
|
350
|
+
const { target } = cqn
|
|
351
|
+
const keyString =
|
|
352
|
+
'(' +
|
|
353
|
+
target.keys
|
|
354
|
+
.filter(k => !k.isAssociation)
|
|
355
|
+
.map(k => {
|
|
356
|
+
let v = dependentOnResult[k.name]
|
|
357
|
+
if (typeof v === 'string' && k._type !== 'cds.UUID') v = `'${v}'`
|
|
358
|
+
return k.name + '=' + v
|
|
359
|
+
})
|
|
360
|
+
.join(',') +
|
|
361
|
+
')'
|
|
362
|
+
request.url = recentUrl.replace(`$${dependsOnId}`, dependentOnUrl + keyString)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
207
365
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
366
|
+
// REVIST: That sends each request through the whole middleware chain again and again, including authentication and authorization.
|
|
367
|
+
// -> We should optimize this!
|
|
368
|
+
const lookalike = _createExpressReqResLookalike(request, req, res)
|
|
369
|
+
const lookalike_next = _getNextForLookalike(lookalike)
|
|
370
|
+
router.handle(lookalike.req, lookalike.res, lookalike_next)
|
|
371
|
+
return lookalike.promise
|
|
372
|
+
})())
|
|
212
373
|
})
|
|
213
|
-
|
|
214
|
-
|
|
374
|
+
.then(req => {
|
|
375
|
+
const resp = { status: 'ok' }
|
|
376
|
+
if (separator) resp.separator = separator
|
|
377
|
+
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
378
|
+
resp.txt = _formatResponse(req, boundary)
|
|
379
|
+
responses.push(resp)
|
|
380
|
+
})
|
|
381
|
+
.catch(failedReq => {
|
|
382
|
+
const resp = { status: 'fail' }
|
|
383
|
+
if (separator) resp.separator = separator
|
|
384
|
+
else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
|
|
385
|
+
resp.txt = _formatResponse(failedReq, boundary)
|
|
386
|
+
tx.failed = failedReq
|
|
387
|
+
responses.push(resp)
|
|
388
|
+
})
|
|
215
389
|
|
|
216
390
|
previousAtomicityGroup = atomicityGroup
|
|
217
391
|
}
|
|
218
392
|
|
|
219
|
-
if (tx)
|
|
220
|
-
|
|
393
|
+
if (tx) {
|
|
394
|
+
// The last open transaction must be finished
|
|
395
|
+
const rejected = await tx.done()
|
|
396
|
+
if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
|
|
397
|
+
else sendPreludeOnce()
|
|
398
|
+
isJson
|
|
399
|
+
? _writeResponseJson(responses, res)
|
|
400
|
+
: _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
|
|
401
|
+
} else sendPreludeOnce()
|
|
221
402
|
res.write(isJson ? ']}' : `${CRLF}--${boundary}--${CRLF}`)
|
|
222
403
|
res.end()
|
|
223
404
|
|
|
@@ -231,8 +412,8 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
231
412
|
* multipart/mixed
|
|
232
413
|
*/
|
|
233
414
|
|
|
234
|
-
const _multipartBatch = (srv, router
|
|
235
|
-
const boundary = req
|
|
415
|
+
const _multipartBatch = async (srv, router, req, res, next) => {
|
|
416
|
+
const boundary = getBoundary(req)
|
|
236
417
|
if (!boundary) return next(cds.error('No boundary found in Content-Type header', { code: 400 }))
|
|
237
418
|
|
|
238
419
|
try {
|
|
@@ -243,20 +424,22 @@ const _multipartBatch = (srv, router) => async (req, res, next) => {
|
|
|
243
424
|
}
|
|
244
425
|
}
|
|
245
426
|
|
|
246
|
-
const _formatResponseMultipart =
|
|
247
|
-
const {
|
|
427
|
+
const _formatResponseMultipart = request => {
|
|
428
|
+
const { res: response } = request
|
|
429
|
+
const content_id = request.req?.headers['content-id']
|
|
248
430
|
|
|
249
|
-
let txt =
|
|
431
|
+
let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
|
|
432
|
+
if (content_id) txt += `content-id: ${content_id}${CRLF}`
|
|
433
|
+
txt += CRLF
|
|
250
434
|
txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
|
|
251
435
|
|
|
252
436
|
// REVISIT: tests require specific sequence
|
|
253
437
|
const headers = {
|
|
254
|
-
|
|
255
|
-
'content-type': '
|
|
256
|
-
...response.getHeaders()
|
|
438
|
+
...response.getHeaders(),
|
|
439
|
+
'content-type': 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
|
|
257
440
|
}
|
|
258
|
-
headers['content-type'] = 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
|
|
259
441
|
delete headers['content-length'] //> REVISIT: expected by tests
|
|
442
|
+
|
|
260
443
|
for (const key in headers) {
|
|
261
444
|
txt += key + ': ' + headers[key] + CRLF
|
|
262
445
|
}
|
|
@@ -271,7 +454,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
|
|
|
271
454
|
let meta = [],
|
|
272
455
|
data = []
|
|
273
456
|
for (const [k, v] of Object.entries(_json)) {
|
|
274
|
-
if (k.startsWith('@')) meta.push(`"${k}"
|
|
457
|
+
if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
|
|
275
458
|
else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
|
|
276
459
|
}
|
|
277
460
|
const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
|
|
@@ -283,7 +466,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
|
|
|
283
466
|
}
|
|
284
467
|
}
|
|
285
468
|
|
|
286
|
-
|
|
469
|
+
return [txt]
|
|
287
470
|
}
|
|
288
471
|
|
|
289
472
|
/*
|
|
@@ -296,46 +479,52 @@ const _formatStatics = {
|
|
|
296
479
|
close: Buffer.from('}')
|
|
297
480
|
}
|
|
298
481
|
|
|
299
|
-
const _formatResponseJson =
|
|
482
|
+
const _formatResponseJson = request => {
|
|
300
483
|
const { id, res: response } = request
|
|
484
|
+
|
|
301
485
|
const raw = Buffer.from(
|
|
302
486
|
JSON.stringify({
|
|
303
487
|
id,
|
|
304
488
|
status: response.statusCode,
|
|
305
489
|
headers: {
|
|
306
|
-
|
|
307
|
-
'content-type': 'application/json'
|
|
308
|
-
...response.getHeaders()
|
|
490
|
+
...response.getHeaders(),
|
|
491
|
+
'content-type': 'application/json' //> REVISIT: why?
|
|
309
492
|
}
|
|
310
493
|
})
|
|
311
494
|
)
|
|
495
|
+
|
|
496
|
+
// body?
|
|
497
|
+
if (!response._chunk) return [raw]
|
|
498
|
+
|
|
312
499
|
// change last "}" into ","
|
|
313
500
|
raw[raw.byteLength - 1] = _formatStatics.comma
|
|
314
|
-
|
|
315
|
-
res.write(_formatStatics.body)
|
|
316
|
-
res.write(response._chunk)
|
|
317
|
-
res.write(_formatStatics.close)
|
|
501
|
+
return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
|
|
318
502
|
}
|
|
319
503
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
504
|
+
/*
|
|
505
|
+
* exports
|
|
506
|
+
*/
|
|
507
|
+
|
|
508
|
+
module.exports = adapter => {
|
|
509
|
+
const { options: config, router, service } = adapter
|
|
510
|
+
const { max_content_length } = config //> max_content_length is unofficial config
|
|
323
511
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
512
|
+
const options = { type: '*/*' }
|
|
513
|
+
if (max_content_length) options.limit = max_content_length
|
|
514
|
+
const textBodyParser = express.text(options)
|
|
327
515
|
|
|
328
|
-
return function
|
|
516
|
+
return function odata_batch(req, res, next) {
|
|
329
517
|
if (req.headers['content-type'].includes('application/json')) {
|
|
330
|
-
return
|
|
518
|
+
return _processBatch(service, router, req, res, next)
|
|
331
519
|
}
|
|
332
520
|
|
|
333
521
|
if (req.headers['content-type'].includes('multipart/mixed')) {
|
|
334
|
-
return
|
|
335
|
-
|
|
522
|
+
return textBodyParser(req, res, function odata_batch_next(err) {
|
|
523
|
+
if (err) return next(err)
|
|
524
|
+
return _multipartBatch(service, router, req, res, next)
|
|
336
525
|
})
|
|
337
526
|
}
|
|
338
527
|
|
|
339
|
-
throw cds.error('Batch requests must have content type multipart/mixed or application/json', {
|
|
528
|
+
throw cds.error('Batch requests must have content type multipart/mixed or application/json', { statusCode: 400 })
|
|
340
529
|
}
|
|
341
530
|
}
|
|
@@ -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
|
+
}
|