@sap/cds 7.8.2 → 7.9.1
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 +45 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/srvinfo.js +6 -5
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- 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 +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/pool.js +3 -0
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +5 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +1 -0
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
package/lib/utils/cds-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cwd = process.env._original_cwd || process.cwd()
|
|
2
2
|
const cds = require('../index')
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
module.exports = exports = new class {
|
|
5
5
|
get inflect() { return super.inflect = require('./inflect') }
|
|
6
6
|
get inspect() { return super.inspect = require('util').inspect }
|
|
7
7
|
get uuid() { return super.uuid = require('crypto').randomUUID }
|
|
@@ -11,7 +11,7 @@ const ux = module.exports = exports = new class {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const path = exports.path = require('path'), { dirname, extname, join, resolve, relative } = path
|
|
14
|
-
const fs = exports.fs = Object.assign (
|
|
14
|
+
const fs = exports.fs = Object.assign (exports,require('fs')) //> for compatibility
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -98,7 +98,12 @@ exports.readdir = async function (x) {
|
|
|
98
98
|
exports.read = async function read (file, _encoding) {
|
|
99
99
|
const f = resolve (cds.root,file)
|
|
100
100
|
const src = await fs.promises.readFile (f, _encoding !== 'json' && _encoding || 'utf8')
|
|
101
|
-
|
|
101
|
+
if (_encoding === 'json' || !_encoding && f.endsWith('.json')) try {
|
|
102
|
+
return JSON.parse(src)
|
|
103
|
+
} catch(e) {
|
|
104
|
+
throw new Error (`Failed to parse JSON in ${f}: ${e.message}`)
|
|
105
|
+
}
|
|
106
|
+
else return src
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
exports.write = function write (file, data, o) {
|
|
@@ -206,7 +211,7 @@ exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) =>
|
|
|
206
211
|
'\nDEPRECATED:', old, '\n',
|
|
207
212
|
'\n ', (kind ? `${kind} ${old}` : old), 'is deprecated and will be removed in upcoming releases!',
|
|
208
213
|
use ? `\n => Please use ${use} instead.` : '', '\n',
|
|
209
|
-
o.stack.replace(/^Error
|
|
214
|
+
o.stack.replace(/^Error:\s*at.*\n/,'\n'), '\n',
|
|
210
215
|
'\n------------------------------------------------------------------------------\n',
|
|
211
216
|
reset
|
|
212
217
|
)
|
|
@@ -95,7 +95,7 @@ function _handleStreamProperties(target, query, model) {
|
|
|
95
95
|
* @extends cds.Request
|
|
96
96
|
*
|
|
97
97
|
* @param {string} type - The OData request type (a.k.a. "Component")
|
|
98
|
-
* @param {import('
|
|
98
|
+
* @param {import('../../../common/Service')} service - The underlying CAP service
|
|
99
99
|
* @param {import('./okra/odata-server/core/OdataRequest')} odataReq - OKRA's req
|
|
100
100
|
* @param {import('./okra/odata-server/core/OdataResponse')} odataRes - OKRA's res
|
|
101
101
|
*/
|
|
@@ -29,7 +29,7 @@ const _postProcess = async (req, odataReq, odataRes, tx, result) => {
|
|
|
29
29
|
/**
|
|
30
30
|
* The handler that will be registered with odata-v4.
|
|
31
31
|
*
|
|
32
|
-
* @param {import('
|
|
32
|
+
* @param {import('../../../../common/Service')} service
|
|
33
33
|
* @returns {function}
|
|
34
34
|
*/
|
|
35
35
|
const action = service => {
|
|
@@ -15,7 +15,7 @@ const { getSapMessages } = require('../../../../common/error/frontend')
|
|
|
15
15
|
/**
|
|
16
16
|
* The handler that will be registered with odata-v4.
|
|
17
17
|
*
|
|
18
|
-
* @param {import('
|
|
18
|
+
* @param {import('../../../../common/Service')} service
|
|
19
19
|
* @returns {function}
|
|
20
20
|
*/
|
|
21
21
|
const create = service => {
|
|
@@ -11,7 +11,7 @@ const { validateResourcePath } = require('../utils/request')
|
|
|
11
11
|
/**
|
|
12
12
|
* The handler that will be registered with odata-v4.
|
|
13
13
|
*
|
|
14
|
-
* @param {import('
|
|
14
|
+
* @param {import('../../../../common/Service')} service
|
|
15
15
|
* @returns {function}
|
|
16
16
|
*/
|
|
17
17
|
const del = service => {
|
|
@@ -39,12 +39,9 @@ const metadata = service => {
|
|
|
39
39
|
edmx = await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
|
|
40
40
|
}
|
|
41
41
|
} else {
|
|
42
|
-
edmx = cds.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// REVISIT: we could cache this in model._cached
|
|
46
|
-
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
47
|
-
)
|
|
42
|
+
edmx = await cds.compile.to.edmx.files.get(service)
|
|
43
|
+
edmx ??= cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
44
|
+
edmx = cds.localize(service.model, locale, edmx)
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
return next(null, toODataResult(edmx))
|
|
@@ -150,12 +150,22 @@ const _getResult = (nameArr, result) => {
|
|
|
150
150
|
return _getResult(nameArr.slice(1), result[nameArr[0]])
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const validateIfNoneMatch = (req, result) => {
|
|
154
|
+
if (req.target._etag && req.headers['if-none-match']) {
|
|
155
|
+
let header = req.headers['if-none-match']
|
|
156
|
+
if (header.startsWith('W/')) header = header.substring(2)
|
|
157
|
+
if (header.startsWith('"') && header.endsWith('"')) header = header.substring(1, header.length - 1)
|
|
158
|
+
if (header === '*') return true
|
|
159
|
+
if (result[req.target._etag.name] === header) return true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
153
163
|
/**
|
|
154
164
|
* Reads the entire entity or only property of it is alike.
|
|
155
165
|
*
|
|
156
166
|
* In case of an entity, odata-v4 wants the value an object structure, in case of a property as scalar.
|
|
157
167
|
*
|
|
158
|
-
* @param {import('../../../../
|
|
168
|
+
* @param {import('../../../../common/Service')} tx
|
|
159
169
|
* @param {import('../../../../cds-services/adapter/odata-v4/ODataRequest')} req
|
|
160
170
|
* @param {Array<import('../okra/odata-commons/uri/UriResource')>} segments
|
|
161
171
|
* @returns {Promise}
|
|
@@ -175,14 +185,15 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
175
185
|
* If no entity is related, the service returns 204 No Content.
|
|
176
186
|
*/
|
|
177
187
|
if (result == null) {
|
|
178
|
-
if (req.headers['if-none-match']) {
|
|
179
|
-
req._.odataRes.setStatusCode(304)
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
188
|
if (_isNavigationToOne(segments)) return toODataResult(null)
|
|
183
189
|
throw getError(404)
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
if (validateIfNoneMatch(req, result)) {
|
|
193
|
+
req._.odataRes.setStatusCode(304)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
186
197
|
if (!Array.isArray(result)) result = [result]
|
|
187
198
|
|
|
188
199
|
if (result.length === 0 && _isNavigationToOne(segments)) return toODataResult(null)
|
|
@@ -373,10 +384,6 @@ const _readStream = async (tx, req) => {
|
|
|
373
384
|
|
|
374
385
|
// Reading one entity or a property of it should yield only a result length of one.
|
|
375
386
|
if (result.length === 0 || result[0] === undefined) {
|
|
376
|
-
if (req.headers['if-none-match']) {
|
|
377
|
-
req._.odataRes.setStatusCode(304)
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
387
|
throw getError(404)
|
|
381
388
|
}
|
|
382
389
|
|
|
@@ -386,6 +393,11 @@ const _readStream = async (tx, req) => {
|
|
|
386
393
|
|
|
387
394
|
result = result[0]
|
|
388
395
|
|
|
396
|
+
if (validateIfNoneMatch(req, result)) {
|
|
397
|
+
req._.odataRes.setStatusCode(304)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
389
401
|
const readable = cds.env.features.stream_compat
|
|
390
402
|
? result.value
|
|
391
403
|
: Object.values(result).find(v => v instanceof Readable)
|
|
@@ -511,7 +523,7 @@ const _ensureStream = result => {
|
|
|
511
523
|
* If the single entity to be read does not exist, calls next with error to return a 404.
|
|
512
524
|
* In all other failure cases it calls next with error to return a 500.
|
|
513
525
|
*
|
|
514
|
-
* @param {import('
|
|
526
|
+
* @param {import('../../../../common/Service')} service
|
|
515
527
|
* @returns {function}
|
|
516
528
|
*/
|
|
517
529
|
const read = service => {
|
|
@@ -27,8 +27,8 @@ module.exports = srv => {
|
|
|
27
27
|
// in case of $batch we need to challenge directly, as the header is not processed if in $batch response body
|
|
28
28
|
if (containsRestrictions && path.endsWith('/$batch') && req.user._is_anonymous) {
|
|
29
29
|
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
30
|
-
if (req._login) res.set('
|
|
31
|
-
else if (user._challenges) res.set('
|
|
30
|
+
if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
|
|
31
|
+
else if (user._challenges) res.set('www-authenticate', user._challenges.join(';'))
|
|
32
32
|
return next(ODATA_UNAUTHORIZED)
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -37,8 +37,8 @@ module.exports = srv => {
|
|
|
37
37
|
// > unauthorized or forbidden?
|
|
38
38
|
if (req.user._is_anonymous) {
|
|
39
39
|
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
40
|
-
if (req._login) res.set('
|
|
41
|
-
else if (user._challenges) res.set('
|
|
40
|
+
if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
|
|
41
|
+
else if (user._challenges) res.set('www-authenticate', user._challenges.join(';'))
|
|
42
42
|
return next(ODATA_UNAUTHORIZED)
|
|
43
43
|
}
|
|
44
44
|
return next(ODATA_FORBIDDEN)
|
|
@@ -105,9 +105,10 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
|
|
|
105
105
|
(e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
|
|
106
106
|
if ((is404 || isForcedInsert) && _isUpsertAllowed(req)) {
|
|
107
107
|
// PUT/ PATCH with if-match header means "only if already exists", i.e., no insert if not
|
|
108
|
-
if (req.headers['if-match'])
|
|
108
|
+
if (req.headers['if-match']) {
|
|
109
109
|
throw Object.assign(new Error('412'), { statusCode: 412 })
|
|
110
|
-
|
|
110
|
+
}
|
|
111
|
+
// REVISIT: remove error (and child?) from tx.context? -> would require a unique req.id
|
|
111
112
|
;[result, req] = await _create(req, odataReq, odataRes, tx)
|
|
112
113
|
odataRes.setStatusCode(201, { overwrite: true })
|
|
113
114
|
} else {
|
|
@@ -148,7 +149,7 @@ const _getStructValue = (prop, result, segments) => {
|
|
|
148
149
|
* In case of success it calls next with the number of updated entries as result.
|
|
149
150
|
* In case of error it calls next with error.
|
|
150
151
|
*
|
|
151
|
-
* @param {import('
|
|
152
|
+
* @param {import('../../../../common/Service')} service
|
|
152
153
|
* @returns {function}
|
|
153
154
|
*/
|
|
154
155
|
const update = service => {
|
|
@@ -57,7 +57,7 @@ class ExpressionToCQN {
|
|
|
57
57
|
return { val: new Date(value).toISOString().replace(/\.\d\d\dZ$/, 'Z') }
|
|
58
58
|
return { val: normalizeTimestamp(value) }
|
|
59
59
|
} catch (e) {
|
|
60
|
-
throw Object.assign(new Error(`The type
|
|
60
|
+
throw Object.assign(new Error(`The type Edm.DateTimeOffset is not compatible with "${value}"`), {
|
|
61
61
|
status: 400
|
|
62
62
|
})
|
|
63
63
|
}
|
|
@@ -84,6 +84,7 @@ const _addCount = aggregateExpression => {
|
|
|
84
84
|
|
|
85
85
|
const _createColumnsForAggregateExpressions = (aggregateExpressions, entity) => {
|
|
86
86
|
const columns = []
|
|
87
|
+
let defaultAggregation
|
|
87
88
|
for (const aggregateExpression of aggregateExpressions) {
|
|
88
89
|
// custom aggregates
|
|
89
90
|
if (aggregateExpression.getPathSegments() && aggregateExpression.getPathSegments().length === 1) {
|
|
@@ -97,7 +98,9 @@ const _createColumnsForAggregateExpressions = (aggregateExpressions, entity) =>
|
|
|
97
98
|
entity.elements[name][AGGREGATION_DEFAULT] &&
|
|
98
99
|
entity.elements[name][AGGREGATION_DEFAULT]['#']
|
|
99
100
|
) {
|
|
100
|
-
|
|
101
|
+
defaultAggregation = entity.elements[name][AGGREGATION_DEFAULT]['#'].toLowerCase()
|
|
102
|
+
if (defaultAggregation === 'count_distinct') defaultAggregation = 'countdistinct'
|
|
103
|
+
columns.push({ [`${defaultAggregation}(${name})`]: name })
|
|
101
104
|
continue
|
|
102
105
|
}
|
|
103
106
|
}
|
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
const { getFeatureNotSupportedError } = require('../../../util/errors')
|
|
10
10
|
const orderByToCQN = require('./orderByToCQN')
|
|
11
11
|
const ExpressionToCQN = require('./ExpressionToCQN')
|
|
12
|
-
const { getColumns } = require('
|
|
12
|
+
const { getColumns } = require('../../../../common/utils/columns')
|
|
13
13
|
const { addLimit, isSameArray } = require('./utils')
|
|
14
14
|
const { findCsnTargetFor } = require('../../../../common/utils/csn')
|
|
15
15
|
const getError = require('../../../../common/error')
|
|
@@ -9,7 +9,44 @@ const readToCQN = require('./readToCQN')
|
|
|
9
9
|
const updateToCQN = require('./updateToCQN')
|
|
10
10
|
const createToCQN = require('./createToCQN')
|
|
11
11
|
const deleteToCQN = require('./deleteToCQN')
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
const parseToCqn = (component, service, target, data, odataReq, upsert) => {
|
|
14
|
+
let query = cds.odata.parse(odataReq.getIncomingRequest().url, { service })
|
|
15
|
+
|
|
16
|
+
// for concat
|
|
17
|
+
if (component === 'READ' && Array.isArray(query)) return query
|
|
18
|
+
|
|
19
|
+
const _target = query.SELECT && query.SELECT.from
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
SELECT: { one }
|
|
23
|
+
} = query
|
|
24
|
+
|
|
25
|
+
switch (component) {
|
|
26
|
+
case 'CREATE':
|
|
27
|
+
// create
|
|
28
|
+
// error in cases like `POST Books(1)` i.e. `POST` with navigation to single entity
|
|
29
|
+
if (one && !upsert) cds.error('POST not allowed on entity', { code: 400 })
|
|
30
|
+
return INSERT.into(_target).entries(data)
|
|
31
|
+
case 'DELETE':
|
|
32
|
+
if (!one) cds.error('DELETE not allowed on collection', { code: 400 })
|
|
33
|
+
// eslint-disable-next-line no-case-declarations
|
|
34
|
+
const last = query._propertyAccess || (_target.ref && _target.ref[_target.ref.length - 1])
|
|
35
|
+
if (target.elements[last] || target.elements[query._propertyAccess]) {
|
|
36
|
+
// delete simple property
|
|
37
|
+
const ref = { ref: query._propertyAccess ? _target.ref : _target.ref.slice(0, -1) }
|
|
38
|
+
return UPDATE(ref).data({ [last]: null })
|
|
39
|
+
} else {
|
|
40
|
+
return DELETE.from(_target)
|
|
41
|
+
}
|
|
42
|
+
case 'UPDATE':
|
|
43
|
+
// eslint-disable-next-line no-throw-literal
|
|
44
|
+
if (!one) throw { statusCode: 400, code: '400', message: `INVALID_${odataReq.getMethod()}` }
|
|
45
|
+
return UPDATE(_target).data(data)
|
|
46
|
+
default:
|
|
47
|
+
return query
|
|
48
|
+
}
|
|
49
|
+
}
|
|
13
50
|
|
|
14
51
|
/**
|
|
15
52
|
* This method transforms an odata request into a CQN object.
|
|
@@ -217,12 +217,12 @@ const _checkViewWithParamCall = (isView, segments, kind, name) => {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
if (segments.length < 2) {
|
|
220
|
-
throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, {
|
|
220
|
+
throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { statusCode: 400, code: 400 })
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
// if the last segment is count, check if previous segment is Set, otherwise check if the last segment equals Set
|
|
224
224
|
if (!_isSet(segments[segments.length - (_isCount(kind) ? 2 : 1)])) {
|
|
225
|
-
throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, {
|
|
225
|
+
throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { statusCode: 400, code: 400 })
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
const cds = require('../../../cds')
|
|
2
|
+
// prettier-ignore
|
|
3
|
+
const OkraAdapter = Object.assign (require('./OData'), {
|
|
4
|
+
for (srv, model, edm) {
|
|
5
|
+
return new this (edm, model, srv.options).addCDSServiceToChannel(srv)
|
|
6
|
+
}
|
|
7
|
+
})
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* This is the express handler for a specific OData endpoint.
|
|
5
11
|
* Note: the same service can be served at different endpoints.
|
|
6
12
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// prettier-ignore
|
|
14
|
+
const adapter4 = module.exports = function (srv) {
|
|
15
|
+
const csn = srv.model
|
|
16
|
+
let okra, pre_compiled_edm = cds.compile.to.edmx.files.get (srv, 'edm.json', csn)
|
|
17
|
+
if (pre_compiled_edm) okra = { // using a lazy-loading okra proxy
|
|
18
|
+
process: (...args) => pre_compiled_edm.then (edm => {
|
|
19
|
+
okra = OkraAdapter.for (srv, csn, edm) // replacing the proxy for subsequent calls
|
|
20
|
+
return okra.process (...args) // delegating the first call
|
|
21
|
+
})
|
|
22
|
+
}; else {
|
|
23
|
+
let edm = cds.compile.to.edm (csn, { service: srv.definition?.name || srv.name })
|
|
24
|
+
okra = OkraAdapter.for (srv, csn, edm)
|
|
25
|
+
}
|
|
26
|
+
return (...args) => okra.process (...args)
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -21,21 +31,22 @@ function OkraAdapter(srv, model = srv.model) {
|
|
|
21
31
|
// REVISIT: Move to ExtensibilityService
|
|
22
32
|
//
|
|
23
33
|
if (cds.requires.extensibility || cds.requires.toggles) {
|
|
24
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Creates new OkraAdapter for new models with tenant-specific extensions.
|
|
36
|
+
* The created adapters are cached in `model._cache`.
|
|
37
|
+
* Note: As the adapters cache is attached to the model, they get disposed
|
|
38
|
+
* when models are evicted from the models cache.
|
|
39
|
+
*/
|
|
25
40
|
module.exports = srv => {
|
|
26
41
|
const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
|
|
42
|
+
// prettier-ignore
|
|
27
43
|
return function ODataAdapter(req, res) {
|
|
28
44
|
const model = cds.context?.model || srv.model
|
|
29
|
-
if (!model._cached) Object.defineProperty(model, '_cached', { value: { touched: Date.now() } })
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let okra = adapters[id]
|
|
34
|
-
if (!okra) {
|
|
35
|
-
const _srv = { __proto__: srv, _real_srv: srv, model } // REVISIT: we need to do that better in new adapters
|
|
36
|
-
okra = adapters[id] = new OkraAdapter(_srv, model)
|
|
37
|
-
}
|
|
38
|
-
return okra.process(req, res)
|
|
45
|
+
if (!model._cached) Object.defineProperty (model, '_cached', { value: { touched: Date.now() } })
|
|
46
|
+
const adapters = model._cached._odata_adapters ??= {}
|
|
47
|
+
const okra = adapters[id] ??= new adapter4 ({ __proto__: srv, _real_srv: srv, model })
|
|
48
|
+
return okra (req, res)
|
|
39
49
|
}
|
|
40
50
|
}
|
|
51
|
+
let unique = 0
|
|
41
52
|
}
|
|
@@ -249,7 +249,7 @@ const _getCopiedData = (odataReq, segments, service, target) => {
|
|
|
249
249
|
*
|
|
250
250
|
* @param {string} component - odata-v4 component which processes this request
|
|
251
251
|
* @param {import('../okra/odata-server/core/OdataRequest')} odataReq - OKRA's req
|
|
252
|
-
* @param {import('
|
|
252
|
+
* @param {import('../../../../common/Service')} service - Service, which will process this request
|
|
253
253
|
* @returns {object | Array}
|
|
254
254
|
* @private
|
|
255
255
|
*/
|
|
@@ -375,8 +375,7 @@ const _getQueryInfo = (query, service, data, eventType) => {
|
|
|
375
375
|
|
|
376
376
|
// store original columns before they are polluted by drafts, db and so on
|
|
377
377
|
const columns = _partialCopyColumns(Array.isArray(query) ? query[0] : query)
|
|
378
|
-
const isServiceEntity =
|
|
379
|
-
_findEdmNameFor(returnType, namespace) in model.entities(namespace) && !returnType._isContained
|
|
378
|
+
const isServiceEntity = _findEdmNameFor(returnType, namespace) in service.entities && !returnType._isContained
|
|
380
379
|
const event = _getEvent(eventType, namespace, data, _pathInfo)
|
|
381
380
|
return Object.assign(_pathInfo, {
|
|
382
381
|
columns,
|
|
@@ -83,16 +83,6 @@ const readAfterWrite = async (req, srv, { operation, isBefore, columns } = { isB
|
|
|
83
83
|
const _req = new Request({ query, event: 'READ', _: req._, params: req.params })
|
|
84
84
|
result = await srv.dispatch(_req)
|
|
85
85
|
if (result && req.target._isDraftEnabled) removeDraftUUIDIfNecessary(req)(result)
|
|
86
|
-
if (result == null && !isBefore && (_isWriteWithResponse(req) || _isDraftAction(req))) {
|
|
87
|
-
// > something must be written and no READ error <=> @restrict or static where
|
|
88
|
-
_req.reject({
|
|
89
|
-
code: 404,
|
|
90
|
-
internal: {
|
|
91
|
-
reason: `No data found for "READ" after "${req.method}" of "${query._target.name}"`,
|
|
92
|
-
source: `"@restrict" or "where" of "${query._target.name}" or underlying entities`
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
86
|
} catch (e) {
|
|
97
87
|
_handleReadError(e, req)
|
|
98
88
|
result = null
|
|
@@ -49,7 +49,9 @@ const validateResourcePath = (odataReq, service) => {
|
|
|
49
49
|
// combination GET && @cds.autoexposed && @cds.autoexpose is OK
|
|
50
50
|
return
|
|
51
51
|
}
|
|
52
|
-
throw getError(405, 'ENTITY_IS_AUTOEXPOSED', [
|
|
52
|
+
throw getError(405, entity['@cds.autoexpose'] ? 'ENTITY_IS_AUTOEXPOSE_READONLY' : 'ENTITY_IS_AUTOEXPOSED', [
|
|
53
|
+
entity.name
|
|
54
|
+
])
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
}
|