@sap/cds 6.0.2 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -19
- package/apis/cds.d.ts +11 -7
- package/apis/log.d.ts +48 -0
- package/apis/ql.d.ts +72 -15
- package/bin/build/buildTaskHandler.js +5 -2
- package/bin/build/constants.js +4 -1
- package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
- package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
- package/bin/build/provider/buildTaskProviderInternal.js +22 -14
- package/bin/build/provider/hana/index.js +8 -7
- package/bin/build/provider/java/index.js +18 -8
- package/bin/build/provider/mtx/index.js +7 -4
- package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
- package/bin/build/provider/mtx-extension/index.js +57 -0
- package/bin/build/provider/mtx-sidecar/index.js +46 -18
- package/bin/build/provider/nodejs/index.js +34 -13
- package/bin/build/util.js +6 -4
- package/bin/deploy/to-hana/cfUtil.js +7 -2
- package/bin/deploy/to-hana/hana.js +6 -3
- package/bin/serve.js +8 -13
- package/lib/compile/{index.js → cds-compile.js} +0 -0
- package/lib/compile/extend.js +15 -5
- package/lib/compile/minify.js +1 -15
- package/lib/compile/parse.js +1 -1
- package/lib/compile/resolve.js +2 -2
- package/lib/compile/to/srvinfo.js +6 -4
- package/lib/{deploy.js → dbs/cds-deploy.js} +8 -8
- package/lib/env/{index.js → cds-env.js} +1 -17
- package/lib/env/{requires.js → cds-requires.js} +24 -3
- package/lib/env/defaults.js +7 -1
- package/lib/env/schemas/cds-package.json +11 -0
- package/lib/env/schemas/cds-rc.json +605 -0
- package/lib/index.js +20 -17
- package/lib/log/{errors.js → cds-error.js} +1 -1
- package/lib/log/{index.js → cds-log.js} +0 -0
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/{index.js → cds-ql.js} +0 -0
- package/lib/req/cds-context.js +1 -1
- package/lib/req/context.js +35 -7
- package/lib/req/locale.js +5 -1
- package/lib/{serve → srv}/adapters.js +23 -19
- package/lib/{connect → srv}/bindings.js +0 -0
- package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
- package/lib/{serve/index.js → srv/cds-serve.js} +1 -1
- package/lib/{serve → srv}/factory.js +2 -3
- package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
- package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
- package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
- package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
- package/lib/srv/srv-models.js +206 -0
- package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
- package/lib/utils/{tests.js → cds-test.js} +2 -2
- package/lib/utils/cds-utils.js +146 -0
- package/lib/utils/index.js +2 -136
- package/lib/utils/jest.js +43 -0
- package/lib/utils/resources/index.js +14 -24
- package/lib/utils/resources/tar.js +18 -41
- package/libx/_runtime/auth/index.js +13 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +9 -20
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +19 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +8 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -5
- package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
- package/libx/_runtime/cds-services/util/errors.js +1 -29
- package/libx/_runtime/common/constants/events.js +1 -3
- package/libx/_runtime/common/i18n/messages.properties +2 -1
- package/libx/_runtime/common/perf/index.js +10 -15
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
- package/libx/_runtime/common/utils/template.js +1 -1
- package/libx/_runtime/db/Service.js +2 -14
- package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
- package/libx/_runtime/db/generic/input.js +4 -0
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
- package/libx/_runtime/extensibility/activate.js +47 -47
- package/libx/_runtime/extensibility/add.js +19 -13
- package/libx/_runtime/extensibility/addExtension.js +17 -13
- package/libx/_runtime/extensibility/defaults.js +25 -30
- package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
- package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
- package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
- package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
- package/libx/_runtime/extensibility/linter.js +32 -0
- package/libx/_runtime/extensibility/push.js +78 -21
- package/libx/_runtime/extensibility/service.js +29 -12
- package/libx/_runtime/extensibility/token.js +56 -0
- package/libx/_runtime/extensibility/validation.js +6 -9
- package/libx/_runtime/fiori/generic/activate.js +0 -4
- package/libx/_runtime/fiori/generic/edit.js +1 -9
- package/libx/_runtime/fiori/generic/new.js +3 -28
- package/libx/_runtime/fiori/generic/patch.js +6 -7
- package/libx/_runtime/fiori/generic/prepare.js +11 -18
- package/libx/_runtime/fiori/generic/read.js +11 -1
- package/libx/_runtime/fiori/utils/handler.js +0 -17
- package/libx/_runtime/hana/Service.js +0 -1
- package/libx/_runtime/hana/conversion.js +12 -1
- package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
- package/libx/_runtime/hana/pool.js +6 -10
- package/libx/_runtime/hana/search2Contains.js +0 -5
- package/libx/_runtime/hana/search2cqn4sql.js +1 -0
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +18 -19
- package/libx/_runtime/messaging/file-based.js +1 -0
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +11 -6
- package/libx/_runtime/remote/utils/client.js +6 -2
- package/libx/_runtime/remote/utils/data.js +5 -0
- package/libx/_runtime/sqlite/Service.js +0 -1
- package/libx/odata/afterburner.js +79 -2
- package/libx/odata/cqn2odata.js +9 -7
- package/libx/odata/grammar.pegjs +161 -77
- package/libx/odata/index.js +9 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +39 -5
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/delete.js +4 -5
- package/libx/rest/middleware/parse.js +3 -2
- package/package.json +3 -3
- package/server.js +1 -1
- package/srv/extensibility-service.cds +6 -3
- package/srv/model-provider.cds +3 -1
- package/srv/model-provider.js +84 -104
- package/srv/mtx.js +7 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
|
@@ -7,16 +7,16 @@ const {
|
|
|
7
7
|
} = require('../okra/odata-server')
|
|
8
8
|
|
|
9
9
|
const { getSapMessages } = require('../../../../common/error/frontend')
|
|
10
|
-
const { getActionOrFunctionReturnType } = require('../utils/handlerUtils')
|
|
10
|
+
const { getActionOrFunctionReturnType, isReturnMinimal } = require('../utils/handlerUtils')
|
|
11
11
|
const { validateResourcePath } = require('../utils/request')
|
|
12
12
|
const { toODataResult, postProcess } = require('../utils/result')
|
|
13
|
-
const {
|
|
13
|
+
const { DRAFT_EVENTS } = require('../../../../common/constants/events')
|
|
14
14
|
const { readAfterWrite } = require('../utils/readAfterWrite')
|
|
15
15
|
|
|
16
16
|
const _postProcess = async (req, odataReq, odataRes, tx, result) => {
|
|
17
17
|
const returnType = getActionOrFunctionReturnType(odataReq.getUriInfo().getPathSegments(), tx.model.definitions)
|
|
18
18
|
// as of spec meeting: no generic support of $select/$expand for custom actions/functions
|
|
19
|
-
if (returnType && returnType.kind === 'entity' && req.event in
|
|
19
|
+
if (returnType && returnType.kind === 'entity' && (req.event in DRAFT_EVENTS || req.event === 'EDIT')) {
|
|
20
20
|
result = await readAfterWrite(req, tx, { operation: { result, returnType } })
|
|
21
21
|
if (result && !('IsActiveEntity' in result)) result.IsActiveEntity = req.event === 'draftActivate'
|
|
22
22
|
}
|
|
@@ -59,13 +59,25 @@ const action = service => {
|
|
|
59
59
|
} else {
|
|
60
60
|
await tx.commit(result)
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204)
|
|
64
|
+
else if (req.event === 'draftActivate' || req.event === 'EDIT') {
|
|
65
|
+
odataRes.setStatusCode(201)
|
|
66
|
+
const keys = Object.keys(req.target.keys).filter(k => {
|
|
67
|
+
return k !== 'IsActiveEntity' && !req.target.keys[k]._isAssociationStrict
|
|
68
|
+
})
|
|
69
|
+
const keysString = keys.map(key => `${key}=${result[key]}`).join(',')
|
|
70
|
+
odataRes.setHeader(
|
|
71
|
+
'location',
|
|
72
|
+
`../${req.target.name.replace(`${service.name}.`, '')}(${keysString},IsActiveEntity=${
|
|
73
|
+
req.event === 'draftActivate'
|
|
74
|
+
})`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
62
77
|
} catch (e) {
|
|
63
78
|
err = e
|
|
64
79
|
|
|
65
|
-
if (changeset) {
|
|
66
|
-
// for passing into rollback
|
|
67
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
68
|
-
} else {
|
|
80
|
+
if (!changeset) {
|
|
69
81
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
70
82
|
await tx.rollback(e).catch(() => {})
|
|
71
83
|
}
|
|
@@ -38,15 +38,15 @@ const create = service => {
|
|
|
38
38
|
try {
|
|
39
39
|
result = await tx.dispatch(req)
|
|
40
40
|
|
|
41
|
-
// REVISIT:
|
|
41
|
+
// REVISIT:
|
|
42
|
+
// Performance: For `isReturnMinimal` it's enough to just read the etag.
|
|
43
|
+
// Note: Without read access, one cannot return the etag.
|
|
44
|
+
if (req._.readAfterWrite) {
|
|
45
|
+
result = await readAfterWrite(req, service, { operation: { result } })
|
|
46
|
+
}
|
|
42
47
|
if (isReturnMinimal(req)) {
|
|
43
48
|
result = postProcessMinimal(req, service, result)
|
|
44
49
|
} else {
|
|
45
|
-
// REVISIT: find better solution
|
|
46
|
-
if (req._.readAfterWrite) {
|
|
47
|
-
result = await readAfterWrite(req, service)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
50
|
postProcess(req, odataRes, service, result)
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -57,16 +57,13 @@ const create = service => {
|
|
|
57
57
|
await tx.commit(result)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
if (isReturnMinimal(req)) {
|
|
60
|
+
if (isReturnMinimal(req) || result === null) {
|
|
61
61
|
odataRes.setStatusCode(204)
|
|
62
62
|
}
|
|
63
63
|
} catch (e) {
|
|
64
64
|
err = e
|
|
65
65
|
|
|
66
|
-
if (changeset) {
|
|
67
|
-
// for passing into rollback
|
|
68
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
69
|
-
} else {
|
|
66
|
+
if (!changeset) {
|
|
70
67
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
71
68
|
await tx.rollback(e).catch(() => {})
|
|
72
69
|
}
|
|
@@ -44,10 +44,7 @@ const del = service => {
|
|
|
44
44
|
} catch (e) {
|
|
45
45
|
err = e
|
|
46
46
|
|
|
47
|
-
if (changeset) {
|
|
48
|
-
// for passing into rollback
|
|
49
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
50
|
-
} else {
|
|
47
|
+
if (!changeset) {
|
|
51
48
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
52
49
|
await tx.rollback(e).catch(() => {})
|
|
53
50
|
}
|
|
@@ -114,13 +114,13 @@ const getErrorHandler = (crashOnError = true, srv) => {
|
|
|
114
114
|
|
|
115
115
|
// invoke srv.on('error', function (err, req) { ... }) here in special situations
|
|
116
116
|
// REVISIT: if for compat reasons, remove once cds^5.1
|
|
117
|
-
if (srv._handlers._error) {
|
|
117
|
+
if (srv._handlers._error.length) {
|
|
118
118
|
let ctx = cds.context
|
|
119
119
|
if (!ctx) {
|
|
120
120
|
// > error before req was dispatched
|
|
121
121
|
ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
|
|
122
122
|
for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
|
|
123
|
-
} else
|
|
123
|
+
} else {
|
|
124
124
|
// > error after req was dispatched, e.g., serialization error in okra
|
|
125
125
|
for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
|
|
126
126
|
}
|
|
@@ -5,10 +5,6 @@ const { toODataResult } = require('../utils/result')
|
|
|
5
5
|
const { normalizeError } = require('../../../../common/error/frontend')
|
|
6
6
|
const getError = require('../../../../common/error')
|
|
7
7
|
|
|
8
|
-
const _getMetadata4Tenant = async (tenant, locale, service) => {
|
|
9
|
-
return await cds.mtx.getEdmx(tenant, service.name, locale)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
/**
|
|
13
9
|
* Provide localized metadata handler.
|
|
14
10
|
*
|
|
@@ -23,21 +19,12 @@ const metadata = service => {
|
|
|
23
19
|
const locale = odataRes.getContract().getLocale()
|
|
24
20
|
|
|
25
21
|
try {
|
|
26
|
-
let edmx
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!edmx) {
|
|
33
|
-
edmx = cds.localize(
|
|
34
|
-
service.model,
|
|
35
|
-
locale,
|
|
36
|
-
// REVISIT: we could cache this in a weak map
|
|
37
|
-
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
22
|
+
let edmx = cds.localize(
|
|
23
|
+
service.model,
|
|
24
|
+
locale,
|
|
25
|
+
// REVISIT: we could cache this in model._cached
|
|
26
|
+
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
27
|
+
)
|
|
41
28
|
return next(null, toODataResult(edmx))
|
|
42
29
|
} catch (e) {
|
|
43
30
|
if (LOG._error) {
|
|
@@ -484,10 +484,7 @@ const read = service => {
|
|
|
484
484
|
} catch (e) {
|
|
485
485
|
err = e
|
|
486
486
|
|
|
487
|
-
if (changeset) {
|
|
488
|
-
// for passing into rollback
|
|
489
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
490
|
-
} else {
|
|
487
|
+
if (!changeset) {
|
|
491
488
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
492
489
|
await tx.rollback(e).catch(() => {})
|
|
493
490
|
}
|
|
@@ -6,7 +6,7 @@ module.exports = srv => {
|
|
|
6
6
|
const requires = getRequiresAsArray(srv.definition)
|
|
7
7
|
const restricted = isRestricted(srv)
|
|
8
8
|
|
|
9
|
-
return (odataReq, odataRes, next)
|
|
9
|
+
return function ODataRequestHandler(odataReq, odataRes, next) {
|
|
10
10
|
const req = odataReq.getBatchApplicationData()
|
|
11
11
|
? odataReq.getBatchApplicationData().req
|
|
12
12
|
: odataReq.getIncomingRequest()
|
|
@@ -51,7 +51,7 @@ module.exports = srv => {
|
|
|
51
51
|
})
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
odataReq.setApplicationData({ req })
|
|
54
|
+
odataReq.setApplicationData({ req, res })
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
next()
|
|
@@ -149,12 +149,13 @@ const update = service => {
|
|
|
149
149
|
// try UPDATE and, on 404 error, try CREATE
|
|
150
150
|
;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
|
|
151
151
|
|
|
152
|
+
if (!primitive && req._.readAfterWrite) {
|
|
153
|
+
// REVISIT:
|
|
154
|
+
// Performance: For `isReturnMinimal` it's enough to just read the etag.
|
|
155
|
+
// Note: Without read access, one cannot return the etag.
|
|
156
|
+
result = await readAfterWrite(req, service, { operation: { result } })
|
|
157
|
+
}
|
|
152
158
|
if (!isReturnMinimal(req)) {
|
|
153
|
-
// REVISIT: find better solution
|
|
154
|
-
if (!primitive && req._.readAfterWrite) {
|
|
155
|
-
result = await readAfterWrite(req, service)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
159
|
postProcess(req, odataRes, service, result, previousResult)
|
|
159
160
|
} else {
|
|
160
161
|
result = postProcessMinimal(req, service, result)
|
|
@@ -167,16 +168,13 @@ const update = service => {
|
|
|
167
168
|
await tx.commit(result)
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
if (isReturnMinimal(req)) {
|
|
171
|
+
if (isReturnMinimal(req) || result === null) {
|
|
171
172
|
odataRes.setStatusCode(204)
|
|
172
173
|
}
|
|
173
174
|
} catch (e) {
|
|
174
175
|
err = e
|
|
175
176
|
|
|
176
|
-
if (changeset) {
|
|
177
|
-
// for passing into rollback
|
|
178
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
179
|
-
} else {
|
|
177
|
+
if (!changeset) {
|
|
180
178
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
181
179
|
await tx.rollback(e).catch(() => {})
|
|
182
180
|
}
|
|
@@ -1,7 +1,41 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
|
|
2
|
+
const cds = require('../../../cds')
|
|
3
|
+
const OData = require('./OData')
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* This is the express handler for a specific OData endpoint.
|
|
7
|
+
* Note: the same service can be served at different endpoints.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = srv => {
|
|
10
|
+
const okra = new OkraAdapter(srv)
|
|
11
|
+
return okra.process.bind(okra)
|
|
5
12
|
}
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
function OkraAdapter(srv, model = srv.model) {
|
|
15
|
+
const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
|
|
16
|
+
alias2ref(srv, edm) // REVISIT: eliminate that -> done again and again -> search for _alias2ref
|
|
17
|
+
return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
21
|
+
//
|
|
22
|
+
// REVISIT: Move to ExtensibilityService
|
|
23
|
+
//
|
|
24
|
+
if (cds.mtx || cds.requires.extensibility || cds.requires.toggles)
|
|
25
|
+
module.exports = srv => {
|
|
26
|
+
const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
|
|
27
|
+
return function ODataAdapter(req, res) {
|
|
28
|
+
const model = cds.context?.model || srv.model
|
|
29
|
+
if (!model._cached) Object.defineProperty(model, '_cached', { value: {} })
|
|
30
|
+
|
|
31
|
+
// Note: cache is attached to model cache so they get disposed when models are evicted from cache
|
|
32
|
+
let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
|
|
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)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let unique = 0
|
|
@@ -16,9 +16,7 @@ const _keysOf = (row, target) => {
|
|
|
16
16
|
if (!keyElements.length) return
|
|
17
17
|
const keys = {}
|
|
18
18
|
for (const key of keyElements) {
|
|
19
|
-
if (key._isAssociationStrict
|
|
20
|
-
continue
|
|
21
|
-
}
|
|
19
|
+
if (key._isAssociationStrict) continue
|
|
22
20
|
keys[key.name] = key.elements ? { val: JSON.stringify(row[key.name]) } : row[key.name]
|
|
23
21
|
}
|
|
24
22
|
return keys
|
|
@@ -111,10 +109,8 @@ const _getColumns = (target, data, prefix = []) => {
|
|
|
111
109
|
* (depth determined by req.data)
|
|
112
110
|
*/
|
|
113
111
|
const getDeepSelect = req => {
|
|
114
|
-
|
|
115
|
-
const { target, data } = req
|
|
112
|
+
let { target, data } = req
|
|
116
113
|
const columns = _getColumns(target, data)
|
|
117
|
-
|
|
118
114
|
return getSimpleSelectCQN(target, data, columns)
|
|
119
115
|
}
|
|
120
116
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { removeDraftUUIDIfNecessary } = require('../../../../fiori/utils/handler')
|
|
1
2
|
const cds = require('../../../../cds')
|
|
2
3
|
const {
|
|
3
4
|
Request,
|
|
@@ -8,7 +9,7 @@ const { normalizeError } = require('../../../../common/error/frontend')
|
|
|
8
9
|
|
|
9
10
|
const { getDeepSelect, getSimpleSelectCQN } = require('./handlerUtils')
|
|
10
11
|
const { hasDeepUpdate } = require('../../../../common/composition/update')
|
|
11
|
-
const { WRITE_EVENTS, CDS_EVENTS
|
|
12
|
+
const { WRITE_EVENTS, CDS_EVENTS } = require('../../../../common/constants/events')
|
|
12
13
|
|
|
13
14
|
const setLocationHeader = (req, { model }) => {
|
|
14
15
|
const { odataRes } = req._
|
|
@@ -21,10 +22,8 @@ const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401
|
|
|
21
22
|
const _isNotFoundError = e => Number(e.code) === 404
|
|
22
23
|
const _isEntityNotReadableError = e => Number(e.code) === 405
|
|
23
24
|
|
|
24
|
-
const _handleReadError =
|
|
25
|
+
const _handleReadError = err => {
|
|
25
26
|
if (!(_isNoAccessError(err) || _isEntityNotReadableError(err) || _isNotFoundError(err))) throw err
|
|
26
|
-
const log = Object.assign(err, { level: 'ERROR', message: normalizeError(err, req).error.message })
|
|
27
|
-
process.env.NODE_ENV !== 'production' && LOG._warn && LOG.warn(log)
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const _getOperationQueryColumns = urlQueryOptions => {
|
|
@@ -38,7 +37,7 @@ const _getOperationQueryColumns = urlQueryOptions => {
|
|
|
38
37
|
return columns
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
const _isDraftAction = req => req.event in
|
|
40
|
+
const _isDraftAction = req => req.event in { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
|
|
42
41
|
const _isActionOrFunction = req => !(req.event in CDS_EVENTS) || _isDraftAction(req)
|
|
43
42
|
const _isWriteWithResponse = req => req.event in WRITE_EVENTS && !(req.event in { CANCEL: 1, DELETE: 1 })
|
|
44
43
|
|
|
@@ -49,6 +48,9 @@ const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: fa
|
|
|
49
48
|
const { result, returnType } = operation
|
|
50
49
|
query = getSimpleSelectCQN(returnType, result, _getOperationQueryColumns(req._queryOptions))
|
|
51
50
|
if (_isDraftAction(req)) query.where({ IsActiveEntity: req.event === 'draftActivate' })
|
|
51
|
+
} else if (req.event === 'NEW' || req.event === 'PATCH') {
|
|
52
|
+
const { result } = operation
|
|
53
|
+
query = getSimpleSelectCQN(req.target, result)
|
|
52
54
|
} else if (req.event === 'UPDATE' && !hasDeepUpdate(srv.model, req.query)) {
|
|
53
55
|
query = Array.isArray(req.data) ? SELECT.from(req.query.UPDATE.entity) : SELECT.one(req.query.UPDATE.entity)
|
|
54
56
|
} else {
|
|
@@ -60,6 +62,7 @@ const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: fa
|
|
|
60
62
|
try {
|
|
61
63
|
const _req = new Request({ query, event: 'READ', _: req._ })
|
|
62
64
|
result = await srv.dispatch(_req)
|
|
65
|
+
if (result && req.target._isDraftEnabled) removeDraftUUIDIfNecessary(req)(result)
|
|
63
66
|
if (result === null && !isBefore && (_isWriteWithResponse(req) || _isDraftAction(req))) {
|
|
64
67
|
// > something must be written and no READ error <=> @restrict or static where
|
|
65
68
|
_req.reject({
|
|
@@ -18,6 +18,10 @@ module.exports = class Differ {
|
|
|
18
18
|
_createSelectColumnsForDelete(entity) {
|
|
19
19
|
const columns = []
|
|
20
20
|
for (const element of Object.values(entity.elements)) {
|
|
21
|
+
// Don't take into account virtual or computed properties to make the diff result
|
|
22
|
+
// consistent with the ones for UPDATE/CREATE (where we don't have access to that
|
|
23
|
+
// information).
|
|
24
|
+
if (!element.key && (element.virtual || element['@Core.Computed'])) continue
|
|
21
25
|
if (element.isComposition) {
|
|
22
26
|
if (element._target._hasPersistenceSkip) continue
|
|
23
27
|
columns.push({
|
|
@@ -9,35 +9,7 @@ const getFeatureNotSupportedError = message => {
|
|
|
9
9
|
return getError(501, `Feature is not supported: ${message}`)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const getAuditLogNotWrittenError = (rootCauseError, phase, event) => {
|
|
13
|
-
const errorMessage =
|
|
14
|
-
!phase || event === 'READ' ? 'Audit log could not be written' : `Audit log could not be written ${phase}`
|
|
15
|
-
const error = new Error(errorMessage)
|
|
16
|
-
error.rootCause = rootCauseError
|
|
17
|
-
return error
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const hasBeenCalledError = (method, query) => {
|
|
21
|
-
return new Error(`Method ${method} has been called before. Invalid CQN: ${JSON.stringify(query)}`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const unexpectedFunctionCallError = (functionName, expectedFunction) => {
|
|
25
|
-
return new Error(`Cannot build CQN object. Invalid call of "${functionName}" before "${expectedFunction}"`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const invalidFunctionArgumentError = (statement, arg) => {
|
|
29
|
-
const details = JSON.stringify(arg, (key, value) => (value === undefined ? '__undefined__' : value)).replace(
|
|
30
|
-
/"__undefined__"/g,
|
|
31
|
-
'undefined'
|
|
32
|
-
)
|
|
33
|
-
return new Error(`Cannot build ${statement} statement. Invalid data provided: ${details}`)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
12
|
module.exports = {
|
|
37
13
|
getModelNotDefinedError,
|
|
38
|
-
getFeatureNotSupportedError
|
|
39
|
-
getAuditLogNotWrittenError,
|
|
40
|
-
hasBeenCalledError,
|
|
41
|
-
unexpectedFunctionCallError,
|
|
42
|
-
invalidFunctionArgumentError
|
|
14
|
+
getFeatureNotSupportedError
|
|
43
15
|
}
|
|
@@ -2,7 +2,6 @@ const MOD_EVENTS = { UPDATE: 1, DELETE: 1, EDIT: 1 }
|
|
|
2
2
|
const WRITE_EVENTS = Object.assign({ CREATE: 1, NEW: 1, PATCH: 1, CANCEL: 1 }, MOD_EVENTS)
|
|
3
3
|
const CRUD_EVENTS = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
4
4
|
const DRAFT_EVENTS = { PATCH: 1, CANCEL: 1, draftActivate: 1, draftPrepare: 1 }
|
|
5
|
-
const DRAFT_MOD_EVENTS = { draftActivate: 1, EDIT: 1 }
|
|
6
5
|
const CDS_EVENTS = Object.assign({}, CRUD_EVENTS, DRAFT_EVENTS)
|
|
7
6
|
|
|
8
7
|
module.exports = {
|
|
@@ -10,6 +9,5 @@ module.exports = {
|
|
|
10
9
|
WRITE_EVENTS,
|
|
11
10
|
CRUD_EVENTS,
|
|
12
11
|
DRAFT_EVENTS,
|
|
13
|
-
CDS_EVENTS
|
|
14
|
-
DRAFT_MOD_EVENTS
|
|
12
|
+
CDS_EVENTS
|
|
15
13
|
}
|
|
@@ -74,8 +74,9 @@ ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
|
|
|
74
74
|
ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
|
|
75
75
|
ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
|
|
76
76
|
EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
|
|
77
|
-
EXPAND_COUNT_UNSUPPORTED="
|
|
77
|
+
EXPAND_COUNT_UNSUPPORTED="/$count" is not supported for expand operation
|
|
78
78
|
ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
|
|
79
|
+
EXPAND_APPLY_UNSUPPORTED="$apply" is not supported for expand operation
|
|
79
80
|
|
|
80
81
|
# rest protocol adapter
|
|
81
82
|
INVALID_RESOURCE="{0}" is not a valid resource
|
|
@@ -4,21 +4,16 @@ const _statisticsRequested = req =>
|
|
|
4
4
|
(req.query && req.query['sap-statistics'] === 'true') ||
|
|
5
5
|
(req.headers && req.headers['sap-statistics'] === 'true' && (!req.query || !req.query['sap-statistics']))
|
|
6
6
|
|
|
7
|
-
module.exports =
|
|
8
|
-
if (
|
|
9
|
-
else app._perf_measured = true
|
|
7
|
+
module.exports = function sap_statistics(req, res, next) {
|
|
8
|
+
if (!_statisticsRequested(req)) return next()
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const t0 = performance.now()
|
|
11
|
+
const { writeHead } = res
|
|
12
|
+
res.writeHead = function (...args) {
|
|
13
|
+
const total = Number((performance.now() - t0) / 1000).toFixed(2)
|
|
14
|
+
if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
|
|
15
|
+
writeHead.call(this, ...args)
|
|
16
|
+
}
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
const { writeHead } = res
|
|
16
|
-
res.writeHead = function (...args) {
|
|
17
|
-
const total = Number((performance.now() - t0) / 1000).toFixed(2)
|
|
18
|
-
if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
|
|
19
|
-
writeHead.call(this, ...args)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
next()
|
|
23
|
-
})
|
|
18
|
+
next()
|
|
24
19
|
}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
const { ensureNoDraftsSuffix } = require('../../common/utils/draft')
|
|
2
2
|
|
|
3
|
-
const traverseFroms = (cqn, cb) => {
|
|
3
|
+
const traverseFroms = (cqn, cb, aliasForSet) => {
|
|
4
4
|
while (cqn.SELECT) cqn = cqn.SELECT.from
|
|
5
5
|
|
|
6
6
|
// Do the most likely first -> {ref}
|
|
7
7
|
if (cqn.ref) {
|
|
8
|
-
return cb(cqn)
|
|
8
|
+
return cb(cqn, aliasForSet)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
if (cqn.SET) {
|
|
12
|
-
|
|
12
|
+
// if a union has an alias, we should use it for the columns we get out of the union
|
|
13
|
+
return cqn.SET.args.map(a => traverseFroms(a, cb, cqn.as))
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
if (cqn.join) {
|
|
16
|
-
return cqn.args.map(a => traverseFroms(a, cb))
|
|
17
|
+
return cqn.args.map(a => traverseFroms(a, cb, aliasForSet))
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const getEntityNameFromCQN = cqn => {
|
|
21
22
|
const res = []
|
|
22
|
-
traverseFroms(cqn,
|
|
23
|
+
traverseFroms(cqn, (from, aliasForSet) =>
|
|
24
|
+
res.push({ entityName: from.ref[0].id || from.ref[0], alias: aliasForSet || from.as })
|
|
25
|
+
)
|
|
23
26
|
return res.length === 1 ? res[0] : res.find(n => n.entityName !== 'DRAFT.DraftAdministrativeData') || {}
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -134,7 +134,7 @@ const getCache = (anything, cache, newCacheFn) => {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
module.exports = (usecase, tx, target, ...args) => {
|
|
137
|
-
// get model first as it may be added to tx (cf. "_ensureModel")
|
|
137
|
+
// get model first as it may be added to tx (cf. "_ensureModel") // REVISIT: _ensureModel is gone
|
|
138
138
|
const model = tx.model
|
|
139
139
|
if (!model) return
|
|
140
140
|
|
|
@@ -24,28 +24,16 @@ class DatabaseService extends cds.Service {
|
|
|
24
24
|
this[`_${each}`] = generic[each]
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// REVISIT: ensures tenant-aware this.model if this is a transaction -> this should be fixed in mtx integration, not here
|
|
28
|
-
this._ensureModel = function (req) {
|
|
29
|
-
if (this.context) {
|
|
30
|
-
// if the tx was initiated in messaging, then this.context._model is not unfolded
|
|
31
|
-
// -> use this.context._model._4odata if present
|
|
32
|
-
const { _model } = this.context
|
|
33
|
-
if (_model) this.model = _model._4odata || _model
|
|
34
|
-
else this.model = req._model
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
this._ensureModel._initial = true
|
|
38
|
-
|
|
39
27
|
// REVISIT: how to generic handler registration?
|
|
40
28
|
}
|
|
41
29
|
|
|
42
30
|
/** Database services don't support custom-defined operations */
|
|
43
|
-
operations() {
|
|
31
|
+
get operations() {
|
|
44
32
|
return []
|
|
45
33
|
}
|
|
46
34
|
|
|
47
35
|
/** Database services don't support custom-defined events */
|
|
48
|
-
events() {
|
|
36
|
+
get events() {
|
|
49
37
|
return []
|
|
50
38
|
}
|
|
51
39
|
|
|
@@ -12,6 +12,8 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
|
12
12
|
|
|
13
13
|
const { filterKeys } = require('../../fiori/utils/handler')
|
|
14
14
|
|
|
15
|
+
const getError = require('../../common/error')
|
|
16
|
+
|
|
15
17
|
// Symbols are used to add extra information in response structure
|
|
16
18
|
const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
|
|
17
19
|
const TO_MANY = Symbol.for('sap.cds.toMany')
|
|
@@ -94,6 +96,16 @@ class JoinCQNFromExpanded {
|
|
|
94
96
|
return this._isDraft
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// There can be a limit/offset for the target entity.
|
|
100
|
+
// The current expand implementation applys a `DISTINCT` on
|
|
101
|
+
// `filterExpand`, which changes the sorting in absence of `ORDER BY`.
|
|
102
|
+
// Therefore, add an implicit `ORDER BY` in those cases.
|
|
103
|
+
_addImplicitOrderBy(cqn, entity, alias) {
|
|
104
|
+
if (cqn.orderBy || !cqn.limit || !entity) return // not needed
|
|
105
|
+
const orderByColumns = cqn.groupBy || getAllKeys(entity).map(key => ({ ref: [alias, key] }))
|
|
106
|
+
cqn.orderBy = orderByColumns
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
/**
|
|
98
110
|
* Build first level of expanding regarding to many and all to one if not part of a nested to many expand.
|
|
99
111
|
*
|
|
@@ -123,6 +135,7 @@ class JoinCQNFromExpanded {
|
|
|
123
135
|
})
|
|
124
136
|
// expand to one
|
|
125
137
|
const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
|
|
138
|
+
this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
|
|
126
139
|
const givenColumns = readToOneCQN.columns
|
|
127
140
|
readToOneCQN.columns = []
|
|
128
141
|
this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
|
|
@@ -130,7 +143,7 @@ class JoinCQNFromExpanded {
|
|
|
130
143
|
const table = unionTable || this._getRef(SELECT).table
|
|
131
144
|
const isDraftTree = this._isDraftTree(table)
|
|
132
145
|
const entity = this._getEntityForTable(table)
|
|
133
|
-
|
|
146
|
+
this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
|
|
134
147
|
if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
|
|
135
148
|
|
|
136
149
|
readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
|
|
@@ -455,14 +468,10 @@ class JoinCQNFromExpanded {
|
|
|
455
468
|
|
|
456
469
|
if (element.ref) {
|
|
457
470
|
element.ref[1] = Object.assign({}, element.ref[1])
|
|
458
|
-
element.ref[1].args = element.ref[1].args.map(arg =>
|
|
459
|
-
return this._mapArg(arg, cqn, tableAlias)
|
|
460
|
-
})
|
|
471
|
+
element.ref[1].args = element.ref[1].args.map(arg => this._mapArg(arg, cqn, tableAlias))
|
|
461
472
|
} else {
|
|
462
473
|
element.args = element.args.slice(0)
|
|
463
|
-
element.args = element.args.map(arg =>
|
|
464
|
-
return this._mapArg(arg, cqn, tableAlias)
|
|
465
|
-
})
|
|
474
|
+
element.args = element.args.map(arg => this._mapArg(arg, cqn, tableAlias))
|
|
466
475
|
}
|
|
467
476
|
}
|
|
468
477
|
|
|
@@ -684,6 +693,17 @@ class JoinCQNFromExpanded {
|
|
|
684
693
|
c => !expandedEntity.keys[c].isAssociation && !(c in DRAFT_COLUMNS_MAP)
|
|
685
694
|
)
|
|
686
695
|
const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
|
|
696
|
+
|
|
697
|
+
const assoc = entity.associations[column.ref[0]]
|
|
698
|
+
if (assoc.is2one && assoc.on) {
|
|
699
|
+
const onCond = expandedEntity._relations[assoc.name].join('target', 'source')
|
|
700
|
+
const xpr = onCond[0].xpr
|
|
701
|
+
const fks = (xpr && xpr.filter(e => e.ref && e.ref[0] === 'target').map(e => e.ref[1])) || []
|
|
702
|
+
for (const k of fks) {
|
|
703
|
+
if (!cols.includes(k)) cols.push(k)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
687
707
|
const unionFrom = getCQNUnionFrom(cols, expandedEntity.name, expandedEntity.name + '.drafts', ks, user)
|
|
688
708
|
readToOneCQN.from.args[1] = {
|
|
689
709
|
SELECT: {
|
|
@@ -1162,7 +1182,7 @@ class JoinCQNFromExpanded {
|
|
|
1162
1182
|
cqn.orderBy = this._copyOrderBy(column.orderBy, tableAlias, expandedEntity)
|
|
1163
1183
|
}
|
|
1164
1184
|
|
|
1165
|
-
if (column.limit)
|
|
1185
|
+
if (column.limit) throw getError(501, 'Pagination is not supported in expand')
|
|
1166
1186
|
|
|
1167
1187
|
cqn = this._adaptWhereOrderBy(cqn, tableAlias)
|
|
1168
1188
|
|
|
@@ -1227,23 +1247,6 @@ class JoinCQNFromExpanded {
|
|
|
1227
1247
|
return assoc.target + '_drafts'
|
|
1228
1248
|
}
|
|
1229
1249
|
|
|
1230
|
-
_getLimitInSelect(cqn, columns, limit, orderBy, expandedEntity) {
|
|
1231
|
-
const select = {
|
|
1232
|
-
SELECT: {
|
|
1233
|
-
columns: this._copyColumns(columns, 'limitFilter'),
|
|
1234
|
-
from: { ref: [cqn.from.args[0].ref[0]], as: 'limitFilter' },
|
|
1235
|
-
where: this._convertOnToWhere(cqn.from.on, cqn.from.args[0].as, 'limitFilter'),
|
|
1236
|
-
limit: limit
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (orderBy) {
|
|
1241
|
-
select.SELECT.orderBy = this._copyOrderBy(orderBy, 'limitFilter', expandedEntity)
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
return select
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
1250
|
_isPathExpressionToOne(ref, entity) {
|
|
1248
1251
|
const ref0 = ref[0]
|
|
1249
1252
|
const el = entity.elements[ref0]
|