@sap/cds 5.5.5 → 5.6.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 +139 -1
- package/apis/services.d.ts +31 -1
- package/app/index.js +22 -11
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +1 -1
- package/bin/build/provider/fiori/index.js +1 -1
- package/bin/build/provider/hana/2migration.js +8 -7
- package/bin/build/provider/java-cf/index.js +1 -1
- package/bin/deploy/to-hana/hana.js +1 -17
- package/common.cds +8 -0
- package/lib/compile/to/sql.js +22 -2
- package/lib/connect/bindings.js +2 -1
- package/lib/core/reflect.js +4 -1
- package/lib/env/index.js +180 -42
- package/lib/env/requires.js +16 -1
- package/lib/i18n/localize.js +33 -5
- package/lib/index.js +3 -3
- package/lib/log/format/kibana.js +6 -2
- package/lib/ql/Query.js +1 -0
- package/lib/ql/SELECT.js +15 -8
- package/lib/ql/Whereable.js +5 -0
- package/lib/req/context.js +13 -5
- package/lib/serve/Service-dispatch.js +8 -1
- package/lib/utils/axios.js +7 -0
- package/lib/utils/data.js +1 -1
- package/lib/utils/tests.js +1 -1
- package/libx/_runtime/audit/Service.js +18 -18
- package/libx/_runtime/audit/generic/personal/access.js +1 -1
- package/libx/_runtime/audit/generic/personal/modification.js +3 -2
- package/libx/_runtime/audit/generic/personal/utils.js +23 -63
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +4 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +37 -35
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +5 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +13 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +84 -34
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +12 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +9 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +8 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +13 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +11 -95
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +17 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetResponseHeadersCommand.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +3 -34
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +64 -31
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +10 -5
- package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +1 -1
- package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +1 -1
- package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +1 -3
- package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +20 -21
- package/libx/_runtime/cds-services/services/utils/columns.js +6 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +1 -8
- package/libx/_runtime/cds-services/services/utils/differ.js +7 -26
- package/libx/_runtime/cds-services/services/utils/handlerUtils.js +2 -4
- package/libx/_runtime/cds-services/util/assert.js +29 -13
- package/libx/_runtime/cds.js +2 -1
- package/libx/_runtime/common/aspects/Association.js +72 -0
- package/libx/_runtime/common/aspects/any.js +8 -45
- package/libx/_runtime/common/aspects/entity.js +0 -1
- package/libx/_runtime/common/aspects/relation.js +40 -0
- package/libx/_runtime/common/aspects/utils.js +73 -1
- package/libx/_runtime/common/auth/strategies/utils/uaa.js +10 -14
- package/libx/_runtime/common/composition/data.js +3 -2
- package/libx/_runtime/common/composition/delete.js +3 -1
- package/libx/_runtime/common/composition/tree.js +23 -18
- package/libx/_runtime/common/composition/update.js +9 -1
- package/libx/_runtime/common/composition/utils.js +34 -8
- package/libx/_runtime/common/error/frontend.js +6 -1
- package/libx/_runtime/common/generic/auth.js +5 -9
- package/libx/_runtime/common/generic/crud.js +2 -2
- package/libx/_runtime/common/generic/etag.js +11 -8
- package/libx/_runtime/common/generic/input.js +3 -3
- package/libx/_runtime/common/generic/paging.js +9 -5
- package/libx/_runtime/common/generic/put.js +3 -2
- package/libx/_runtime/common/generic/sorting.js +3 -3
- package/libx/_runtime/common/generic/temporal.js +3 -3
- package/libx/_runtime/common/utils/cqn.js +20 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +125 -139
- package/libx/_runtime/common/utils/csn.js +50 -52
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +41 -176
- package/libx/_runtime/common/utils/generateOnCond.js +40 -70
- package/libx/_runtime/common/utils/{enrichWithKeysFromWhere.js → keys.js} +29 -28
- package/libx/_runtime/common/utils/postProcessing.js +3 -0
- package/libx/_runtime/common/utils/propagateForeignKeys.js +84 -0
- package/libx/_runtime/common/utils/resolveStructured.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +7 -5
- package/libx/_runtime/common/utils/rewriteAsterisks.js +94 -0
- package/libx/_runtime/common/utils/search2cqn4sql.js +9 -8
- package/libx/_runtime/common/utils/template.js +54 -46
- package/libx/_runtime/db/Service.js +9 -2
- package/libx/_runtime/db/expand/expandCQNToJoin.js +54 -33
- package/libx/_runtime/db/expand/rawToExpanded.js +2 -1
- package/libx/_runtime/db/generic/arrayed.js +13 -28
- package/libx/_runtime/db/generic/create.js +1 -0
- package/libx/_runtime/db/generic/input.js +7 -11
- package/libx/_runtime/db/generic/integrity.js +2 -2
- package/libx/_runtime/db/generic/rewrite.js +2 -5
- package/libx/_runtime/db/generic/update.js +1 -0
- package/libx/_runtime/db/query/read.js +9 -4
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -2
- package/libx/_runtime/db/sql-builder/annotations.js +1 -0
- package/libx/_runtime/db/utils/columns.js +14 -43
- package/libx/_runtime/fiori/generic/activate.js +3 -2
- package/libx/_runtime/fiori/generic/before.js +2 -2
- package/libx/_runtime/fiori/generic/cancel.js +3 -2
- package/libx/_runtime/fiori/generic/delete.js +3 -2
- package/libx/_runtime/fiori/generic/edit.js +3 -3
- package/libx/_runtime/fiori/generic/new.js +2 -2
- package/libx/_runtime/fiori/generic/patch.js +2 -2
- package/libx/_runtime/fiori/generic/prepare.js +2 -2
- package/libx/_runtime/fiori/generic/read.js +45 -63
- package/libx/_runtime/fiori/generic/readOverDraft.js +4 -4
- package/libx/_runtime/fiori/uiflex/extensibility/index.cds +15 -0
- package/libx/_runtime/fiori/uiflex/extensibility/index.js +148 -0
- package/libx/_runtime/fiori/uiflex/handler/transformREAD.js +119 -0
- package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +43 -0
- package/libx/_runtime/fiori/uiflex/handler/transformWRITE.js +62 -0
- package/libx/_runtime/fiori/uiflex/index.js +35 -0
- package/libx/_runtime/fiori/uiflex/utils.js +78 -0
- package/libx/_runtime/fiori/utils/handler.js +3 -13
- package/libx/_runtime/fiori/utils/where.js +6 -1
- package/libx/_runtime/hana/pool.js +12 -11
- package/libx/_runtime/hana/search2cqn4sql.js +34 -43
- package/libx/_runtime/hana/searchToContains.js +3 -3
- package/libx/_runtime/index.js +5 -2
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +16 -3
- package/libx/_runtime/messaging/common-utils/connections.js +11 -14
- package/libx/_runtime/messaging/common-utils/naming-conventions.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -1
- package/libx/_runtime/messaging/message-queuing.js +18 -0
- package/libx/_runtime/remote/Service.js +20 -4
- package/libx/_runtime/remote/utils/client-types.d.ts +7 -0
- package/libx/_runtime/remote/utils/client.js +117 -23
- package/libx/_runtime/sqlite/Service.js +2 -2
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +1 -3
- package/libx/gql/GraphQLAdapter.js +33 -0
- package/libx/gql/constants/adapter.js +69 -0
- package/libx/gql/constants/cds.js +18 -0
- package/libx/gql/constants/graphql.js +33 -0
- package/libx/gql/resolvers/crud/create.js +15 -0
- package/libx/gql/resolvers/crud/delete.js +24 -0
- package/libx/gql/resolvers/crud/index.js +6 -0
- package/libx/gql/resolvers/crud/read.js +25 -0
- package/libx/gql/resolvers/crud/update.js +31 -0
- package/libx/gql/resolvers/crud/utils/index.js +36 -0
- package/libx/gql/resolvers/field.js +5 -0
- package/libx/gql/resolvers/index.js +7 -0
- package/libx/gql/resolvers/mutation.js +23 -0
- package/libx/gql/resolvers/parse/ast/enrich.js +51 -0
- package/libx/gql/resolvers/parse/ast/fragment.js +11 -0
- package/libx/gql/resolvers/parse/ast/fromObject.js +39 -0
- package/libx/gql/resolvers/parse/ast/index.js +3 -0
- package/libx/gql/resolvers/parse/ast/meta.js +4 -0
- package/libx/gql/resolvers/parse/ast/variable.js +7 -0
- package/libx/gql/resolvers/parse/ast2cqn/columns.js +42 -0
- package/libx/gql/resolvers/parse/ast2cqn/entries.js +31 -0
- package/libx/gql/resolvers/parse/ast2cqn/index.js +8 -0
- package/libx/gql/resolvers/parse/ast2cqn/limit.js +6 -0
- package/libx/gql/resolvers/parse/ast2cqn/orderBy.js +24 -0
- package/libx/gql/resolvers/parse/ast2cqn/utils/index.js +3 -0
- package/libx/gql/resolvers/parse/ast2cqn/where.js +70 -0
- package/libx/gql/resolvers/parse/utils/index.js +8 -0
- package/libx/gql/resolvers/query.js +13 -0
- package/libx/gql/resolvers/root.js +34 -0
- package/libx/gql/schema/generate.js +18 -0
- package/libx/gql/schema/index.js +5 -0
- package/libx/gql/schema/mutation.js +76 -0
- package/libx/gql/schema/query.js +108 -0
- package/libx/gql/schema/typeDefMap.js +45 -0
- package/libx/gql/schema/utils/index.js +54 -0
- package/libx/gql/utils/index.js +12 -0
- package/libx/{_runtime/odata/cqn2odata.js → odata/cqn2odata/index.js} +39 -100
- package/libx/odata/index.js +80 -0
- package/libx/odata/odata2cqn/afterburner.js +170 -0
- package/libx/{_runtime/odata/odata2cqn.pegjs → odata/odata2cqn/grammar.pegjs} +102 -123
- package/libx/odata/odata2cqn/index.js +3 -0
- package/libx/odata/odata2cqn/parser.js +1 -0
- package/libx/odata/utils/index.js +64 -0
- package/libx/rest/RestAdapter.js +101 -0
- package/libx/rest/RestRequest.js +30 -0
- package/libx/rest/index.js +3 -0
- package/libx/rest/middleware/auth.js +22 -0
- package/libx/rest/middleware/content.js +15 -0
- package/libx/rest/middleware/create.js +40 -0
- package/libx/rest/middleware/delete.js +20 -0
- package/libx/rest/middleware/error.js +56 -0
- package/libx/rest/middleware/operation.js +39 -0
- package/libx/rest/middleware/parse.js +90 -0
- package/libx/rest/middleware/read.js +29 -0
- package/libx/rest/middleware/update.js +42 -0
- package/libx/rest/utils/data.js +65 -0
- package/package.json +4 -1
- package/server.js +29 -7
- package/libx/_runtime/cds-services/services/utils/diff.js +0 -53
- package/libx/_runtime/cds-services/util/auditlog.js +0 -247
- package/libx/_runtime/cds-services/util/xsenv.js +0 -51
- package/libx/_runtime/common/utils/backlinks.js +0 -83
- package/libx/_runtime/common/utils/rewriteAsterisk.js +0 -72
- package/libx/_runtime/odata/index.js +0 -55
- package/libx/_runtime/odata/odata2cqn.js +0 -1
- package/libx/_runtime/odata/readToCqn.js +0 -129
- package/libx/_runtime/remote/cqn2odata/index.js +0 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const cds = require('../_runtime/cds')
|
|
2
|
+
|
|
3
|
+
const express = require('express')
|
|
4
|
+
|
|
5
|
+
const auth = require('./middleware/auth')
|
|
6
|
+
const content = require('./middleware/content')
|
|
7
|
+
const parse = require('./middleware/parse')
|
|
8
|
+
|
|
9
|
+
const create = require('./middleware/create')
|
|
10
|
+
const read = require('./middleware/read')
|
|
11
|
+
const update = require('./middleware/update')
|
|
12
|
+
const deleet = require('./middleware/delete')
|
|
13
|
+
const operation = require('./middleware/operation')
|
|
14
|
+
|
|
15
|
+
const error = require('./middleware/error')
|
|
16
|
+
|
|
17
|
+
// REVISIT: _commit_attempted workaround to avoid double rollback leading to release error
|
|
18
|
+
const _commit_attempted = Symbol()
|
|
19
|
+
|
|
20
|
+
class RestAdapter extends express.Router {
|
|
21
|
+
constructor(srv) {
|
|
22
|
+
super()
|
|
23
|
+
|
|
24
|
+
this.use(express.json())
|
|
25
|
+
|
|
26
|
+
// pass srv-reated stuff to middlewares via req
|
|
27
|
+
this.use('/', (req, res, next) => {
|
|
28
|
+
req._srv = srv
|
|
29
|
+
next()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// check @requires as soon as possible (DoS)
|
|
33
|
+
this.use('/', auth)
|
|
34
|
+
|
|
35
|
+
// content-type check
|
|
36
|
+
this.use('/', content)
|
|
37
|
+
|
|
38
|
+
// parse
|
|
39
|
+
this.use('/', parse)
|
|
40
|
+
|
|
41
|
+
// begin tx
|
|
42
|
+
this.use('/', (req, res, next) => {
|
|
43
|
+
// create tx and set as cds.context
|
|
44
|
+
// REVISIT: _model should not be necessary
|
|
45
|
+
req._tx = cds.context = srv.tx({ user: req.user, req, res, _model: req._srv.model })
|
|
46
|
+
next()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// POST
|
|
50
|
+
this.post('/*', (req, res, next) => {
|
|
51
|
+
if (req._operation) operation(req, res, next)
|
|
52
|
+
else create(req, res, next)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// GET
|
|
56
|
+
this.get('/*', (req, res, next) => {
|
|
57
|
+
if (req._operation) operation(req, res, next)
|
|
58
|
+
else read(req, res, next)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// PUT, PATCH, DELETE
|
|
62
|
+
this.put('/*', update)
|
|
63
|
+
this.patch('/*', update)
|
|
64
|
+
this.delete('/*', deleet)
|
|
65
|
+
|
|
66
|
+
// end tx (i.e., commit or rollback)
|
|
67
|
+
this.use('/', async (req, res, next) => {
|
|
68
|
+
const { result, status, location } = req._result
|
|
69
|
+
|
|
70
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
71
|
+
try {
|
|
72
|
+
req._tx[_commit_attempted] = true
|
|
73
|
+
await req._tx.commit(result)
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return next(e)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// TODO: cf. bufferToBase64() in old rest adapter
|
|
79
|
+
|
|
80
|
+
// only set status if not yet modified
|
|
81
|
+
if (status && res.statusCode === 200) res.status(status)
|
|
82
|
+
if (location) res.set('location', location)
|
|
83
|
+
res.send(result)
|
|
84
|
+
})
|
|
85
|
+
this.use('/', (err, req, res, next) => {
|
|
86
|
+
// request may fail during processing or during commit -> both caught here
|
|
87
|
+
|
|
88
|
+
// ignore rollback error, which should never happen
|
|
89
|
+
if (req._tx && !req._tx[_commit_attempted]) req._tx.rollback(err).catch(() => {})
|
|
90
|
+
|
|
91
|
+
next(err)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* error handling
|
|
96
|
+
*/
|
|
97
|
+
this.use(error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = RestAdapter
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const cds = require('../_runtime/cds')
|
|
2
|
+
|
|
3
|
+
class RestRequest extends cds.Request {
|
|
4
|
+
constructor(args) {
|
|
5
|
+
super(args)
|
|
6
|
+
|
|
7
|
+
// REVISIT: should not be necessary
|
|
8
|
+
/*
|
|
9
|
+
* propagate _ (i.e., req._ and, hence, req._.req/res)
|
|
10
|
+
* -> in the old adapters this is also set in OdataRequest/RestRequest
|
|
11
|
+
*/
|
|
12
|
+
Object.setPrototypeOf(this._, cds.context._)
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* new req.res api [work in progress -> not official]:
|
|
16
|
+
* - req.res.status(202)
|
|
17
|
+
* - req.res.set('location', '/Books/301')
|
|
18
|
+
* this way is $batch compatible, i.e., the status and headers can be set on "subresponse"
|
|
19
|
+
*/
|
|
20
|
+
this._status = null
|
|
21
|
+
this._headers = {}
|
|
22
|
+
const that = this
|
|
23
|
+
this.res = {
|
|
24
|
+
status: s => (that._status = s),
|
|
25
|
+
set: (k, v) => (that._headers[k] = v)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = RestRequest
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { UNAUTHORIZED, FORBIDDEN } = require('../../_runtime/common/utils/auth')
|
|
2
|
+
|
|
3
|
+
const { getRequiresAsArray } = require('../../_runtime/common/utils/auth')
|
|
4
|
+
|
|
5
|
+
module.exports = (req, res, next) => {
|
|
6
|
+
if (!req._srv._requires) req._srv._requires = getRequiresAsArray(req._srv.definition)
|
|
7
|
+
|
|
8
|
+
const { _requires: requires } = req._srv
|
|
9
|
+
|
|
10
|
+
if (req.path !== '/' && requires.length > 0 && !requires.some(r => req.user.is(r))) {
|
|
11
|
+
// > unauthorized or forbidden?
|
|
12
|
+
if (req.user._is_anonymous) {
|
|
13
|
+
if (req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
14
|
+
// REVISIT: security log in else case?
|
|
15
|
+
throw UNAUTHORIZED
|
|
16
|
+
}
|
|
17
|
+
// REVISIT: security log?
|
|
18
|
+
throw FORBIDDEN
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
next()
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const PPP = { POST: 1, PUT: 1, PATCH: 1 }
|
|
2
|
+
const UPDATE = { PUT: 1, PATCH: 1 }
|
|
3
|
+
|
|
4
|
+
module.exports = (req, res, next) => {
|
|
5
|
+
if (PPP[req.method]) {
|
|
6
|
+
if (req.headers['content-type'] && req.headers['content-type'] !== 'application/json') {
|
|
7
|
+
throw { statusCode: 415, code: '415', message: 'INVALID_CONTENT_TYPE_ONLY_JSON' }
|
|
8
|
+
}
|
|
9
|
+
if (UPDATE[req.method] && Array.isArray(req.body)) {
|
|
10
|
+
throw { statusCode: 400, code: '400', message: `INVALID_${req.method}` }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
next()
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
const { INSERT } = cds.ql
|
|
3
|
+
|
|
4
|
+
const RestRequest = require('../RestRequest')
|
|
5
|
+
|
|
6
|
+
const _error4 = rejected =>
|
|
7
|
+
rejected.length > 1 ? { message: 'MULTIPLE_ERRORS', details: rejected.map(r => r.reason) } : rejected[0].reason
|
|
8
|
+
|
|
9
|
+
module.exports = async (_req, _res, next) => {
|
|
10
|
+
const { _srv: srv, _query: query, _target, _data } = _req
|
|
11
|
+
|
|
12
|
+
let result, location
|
|
13
|
+
|
|
14
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
15
|
+
try {
|
|
16
|
+
// add the data
|
|
17
|
+
query.entries(_data)
|
|
18
|
+
if (query.INSERT.entries.length > 1) {
|
|
19
|
+
// > batch insert
|
|
20
|
+
const reqs = query.INSERT.entries.map(
|
|
21
|
+
entry => new RestRequest({ query: INSERT.into(query.INSERT.into).entries(entry), _target })
|
|
22
|
+
)
|
|
23
|
+
const ress = await Promise.allSettled(reqs.map(req => srv.dispatch(req)))
|
|
24
|
+
const rejected = ress.filter(r => r.status === 'rejected')
|
|
25
|
+
if (rejected.length) throw _error4(rejected)
|
|
26
|
+
result = ress.map(r => r.value)
|
|
27
|
+
} else {
|
|
28
|
+
// > single insert
|
|
29
|
+
const req = new RestRequest({ query, _target })
|
|
30
|
+
result = await srv.dispatch(req)
|
|
31
|
+
location = `../${req.entity.replace(srv.name + '.', '')}`
|
|
32
|
+
for (const k in req.target.keys) location += `/${result[k]}`
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return next(e)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_req._result = { result, status: 201, location }
|
|
39
|
+
next()
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const RestRequest = require('../RestRequest')
|
|
2
|
+
|
|
3
|
+
module.exports = async (_req, _res, next) => {
|
|
4
|
+
const { _srv: srv, _query: query, _target, _params } = _req
|
|
5
|
+
|
|
6
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
7
|
+
try {
|
|
8
|
+
const req = new RestRequest({ query, _target })
|
|
9
|
+
|
|
10
|
+
// req.data is filled with keys during read and delete
|
|
11
|
+
if (_params) req.data = _params[_params.length - 1]
|
|
12
|
+
|
|
13
|
+
await srv.dispatch(req)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return next(e)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_req._result = { result: null, status: 204 }
|
|
19
|
+
next()
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
|
|
3
|
+
// requesting logger without module on purpose!
|
|
4
|
+
const LOG = cds.log()
|
|
5
|
+
|
|
6
|
+
let _i18n
|
|
7
|
+
const i18n = (...args) => {
|
|
8
|
+
if (!_i18n) _i18n = require('../../_runtime/common/i18n')
|
|
9
|
+
return _i18n(...args)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { normalizeError, isClientError } = require('../../_runtime/common/error/frontend')
|
|
13
|
+
|
|
14
|
+
const _log = err => {
|
|
15
|
+
const level = isClientError(err) ? 'warn' : 'error'
|
|
16
|
+
if ((level === 'warn' && !LOG._warn) || (level === 'error' && !LOG._error)) return
|
|
17
|
+
|
|
18
|
+
// replace messages in toLog with developer texts (i.e., undefined locale)
|
|
19
|
+
const _message = err.message
|
|
20
|
+
const _details = err.details
|
|
21
|
+
err.message = i18n(err.message || err.code, undefined, err.args) || err.message
|
|
22
|
+
if (err.details) {
|
|
23
|
+
const details = []
|
|
24
|
+
for (const d of err.details) {
|
|
25
|
+
details.push(Object.assign({}, d, { message: i18n(d.message || d.code, undefined, d.args) || d.message }))
|
|
26
|
+
}
|
|
27
|
+
err.details = details
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// log it
|
|
31
|
+
LOG[level](err)
|
|
32
|
+
|
|
33
|
+
// restore
|
|
34
|
+
err.message = _message
|
|
35
|
+
if (_details) err.details = _details
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line no-unused-vars
|
|
39
|
+
module.exports = (err, req, res, next) => {
|
|
40
|
+
const { _srv: srv } = req
|
|
41
|
+
|
|
42
|
+
// invoke srv.on('error', function (err, req) { ... }) here in special situations
|
|
43
|
+
let ctx = cds.context
|
|
44
|
+
if (!ctx) {
|
|
45
|
+
// > error before req was dispatched
|
|
46
|
+
ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
|
|
47
|
+
for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// log the error (4xx -> warn)
|
|
51
|
+
_log(err)
|
|
52
|
+
|
|
53
|
+
const { error, statusCode } = normalizeError(err, req)
|
|
54
|
+
|
|
55
|
+
res.status(statusCode).send({ error })
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { validateReturnType } = require('../../_runtime/cds-services/adapter/rest/utils/validation-checks')
|
|
2
|
+
|
|
3
|
+
const RestRequest = require('../RestRequest')
|
|
4
|
+
|
|
5
|
+
module.exports = async (_req, _res, next) => {
|
|
6
|
+
const { _srv: srv, _query: query, _operation: operation, _data: data } = _req
|
|
7
|
+
|
|
8
|
+
let result
|
|
9
|
+
|
|
10
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
11
|
+
try {
|
|
12
|
+
const req = query
|
|
13
|
+
? new RestRequest({ query, event: operation.name, data })
|
|
14
|
+
: new RestRequest({ event: operation.name.replace(`${srv.name}.`, ''), data })
|
|
15
|
+
result = await srv.dispatch(req)
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return next(e)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!operation.returns) {
|
|
21
|
+
_req._result = { status: 204 }
|
|
22
|
+
} else {
|
|
23
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
24
|
+
try {
|
|
25
|
+
// REVISIT: do not use from old rest adapter
|
|
26
|
+
// REVISIT: new impl should return instead of throwing to avoid try catch
|
|
27
|
+
validateReturnType(operation, result)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return next(e)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// REVISIT: still needed?
|
|
33
|
+
if (!operation.returns.items && Array.isArray(result)) result = result[0]
|
|
34
|
+
|
|
35
|
+
_req._result = { result }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
next()
|
|
39
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
const { INSERT, SELECT, UPDATE, DELETE } = cds.ql
|
|
3
|
+
|
|
4
|
+
const { getDeepCopy } = require('../utils/data')
|
|
5
|
+
|
|
6
|
+
const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
7
|
+
|
|
8
|
+
module.exports = (req, res, next) => {
|
|
9
|
+
const { _srv: service } = req
|
|
10
|
+
const { model } = service
|
|
11
|
+
|
|
12
|
+
let query = cds.odata.parse(req.url, { service })
|
|
13
|
+
|
|
14
|
+
// parser always produces selects
|
|
15
|
+
const _target = (req._target = query.SELECT && query.SELECT.from)
|
|
16
|
+
if (!_target) return next()
|
|
17
|
+
|
|
18
|
+
// REVISIT: __target is the csn target definition
|
|
19
|
+
const {
|
|
20
|
+
__target: definition,
|
|
21
|
+
SELECT: { one }
|
|
22
|
+
} = query
|
|
23
|
+
delete query.__target
|
|
24
|
+
|
|
25
|
+
// REVISIT: hack for actions and functions
|
|
26
|
+
let operation, args
|
|
27
|
+
const last = _target.ref[_target.ref.length - 1]
|
|
28
|
+
if (last.operation) {
|
|
29
|
+
operation = last.operation
|
|
30
|
+
if (last.args) args = last.args
|
|
31
|
+
_target.ref.pop()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const unbound = _target.ref.length === 0
|
|
35
|
+
|
|
36
|
+
// query based on method
|
|
37
|
+
switch (req.method) {
|
|
38
|
+
case 'GET':
|
|
39
|
+
if (operation) {
|
|
40
|
+
// function
|
|
41
|
+
req._operation = operation = definition.kind === 'function' ? definition : definition.actions[operation]
|
|
42
|
+
if (!unbound) query = one ? SELECT.one(_target) : SELECT.from(_target)
|
|
43
|
+
else query = undefined
|
|
44
|
+
} else {
|
|
45
|
+
// read (nothing to do)
|
|
46
|
+
}
|
|
47
|
+
break
|
|
48
|
+
case 'POST':
|
|
49
|
+
if (operation) {
|
|
50
|
+
// action
|
|
51
|
+
req._operation = operation = definition.kind === 'action' ? definition : definition.actions[operation]
|
|
52
|
+
if (!unbound) query = one ? SELECT.one(_target) : SELECT.from(_target)
|
|
53
|
+
else query = undefined
|
|
54
|
+
} else {
|
|
55
|
+
// create
|
|
56
|
+
if (one) cds.error('POST not allowed on entity', { code: 400 })
|
|
57
|
+
query = INSERT.into(_target)
|
|
58
|
+
}
|
|
59
|
+
break
|
|
60
|
+
case 'PUT':
|
|
61
|
+
case 'PATCH':
|
|
62
|
+
if (!one) throw { statusCode: 400, code: '400', message: `INVALID_${req.method}` }
|
|
63
|
+
query = UPDATE(_target)
|
|
64
|
+
break
|
|
65
|
+
case 'DELETE':
|
|
66
|
+
if (!one) cds.error('DELETE not allowed on collection', { code: 400 })
|
|
67
|
+
query = DELETE.from(_target)
|
|
68
|
+
break
|
|
69
|
+
default:
|
|
70
|
+
// anything to do?
|
|
71
|
+
}
|
|
72
|
+
req._query = query
|
|
73
|
+
|
|
74
|
+
// REVISIT: query._data hack
|
|
75
|
+
// deep copy of body (incl. validations such as correct data type)
|
|
76
|
+
if ((query && (query.INSERT || query.UPDATE)) || (operation && operation.kind === 'action') || args) {
|
|
77
|
+
const [validations, copy] = getDeepCopy(args || req.body, operation || definition, model, req.method !== 'POST')
|
|
78
|
+
if (validations) throw validations
|
|
79
|
+
req._data = copy
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// REVISIT: req.params as documented
|
|
83
|
+
for (let i = 0; i < _target.ref.length; i++) {
|
|
84
|
+
req._params = req._params || []
|
|
85
|
+
if (_target.ref[i].where) req._params.push(where2obj(_target.ref[i].where))
|
|
86
|
+
else req._params.push({})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
next()
|
|
90
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const RestRequest = require('../RestRequest')
|
|
2
|
+
|
|
3
|
+
module.exports = async (_req, _res, next) => {
|
|
4
|
+
const { _srv: srv, _query: query, _target, _params } = _req
|
|
5
|
+
|
|
6
|
+
let result,
|
|
7
|
+
status = 200
|
|
8
|
+
|
|
9
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
10
|
+
try {
|
|
11
|
+
const req = new RestRequest({ query, _target })
|
|
12
|
+
|
|
13
|
+
// req.data is filled with keys during read and delete
|
|
14
|
+
if (_params) req.data = _params[_params.length - 1]
|
|
15
|
+
|
|
16
|
+
result = await srv.dispatch(req)
|
|
17
|
+
|
|
18
|
+
// 204 or 404?
|
|
19
|
+
if (result === null && query.SELECT.one) {
|
|
20
|
+
if (_target.ref.length > 1) status = 204
|
|
21
|
+
else throw { code: 404 }
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return next(e)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_req._result = { result, status }
|
|
28
|
+
next()
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
const { INSERT } = cds.ql
|
|
3
|
+
|
|
4
|
+
const RestRequest = require('../RestRequest')
|
|
5
|
+
|
|
6
|
+
const UPSERT_ALLOWED = !(cds.env.runtime && cds.env.runtime.allow_upsert === false)
|
|
7
|
+
|
|
8
|
+
const { deepCopyObject } = require('../../_runtime/common/utils/copy')
|
|
9
|
+
|
|
10
|
+
module.exports = async (_req, _res, next) => {
|
|
11
|
+
let { _srv: srv, _query: query, _target, _data, _params } = _req
|
|
12
|
+
|
|
13
|
+
let result,
|
|
14
|
+
status = 200
|
|
15
|
+
|
|
16
|
+
// unfortunately, express doesn't catch async errors -> try catch needed
|
|
17
|
+
try {
|
|
18
|
+
// if upsert it allowed, we need to catch 404 and retry with create
|
|
19
|
+
try {
|
|
20
|
+
// add the data (as copy, if upsert allowed)
|
|
21
|
+
query.with(UPSERT_ALLOWED ? deepCopyObject(_data) : _data)
|
|
22
|
+
// REVISIT: if PUT, req.method should be PUT -> Crud2Http maps UPSERT to PUT
|
|
23
|
+
result = await srv.dispatch(new RestRequest({ query, _target, method: _req.method }))
|
|
24
|
+
if (_params) Object.assign(result, _params[_params.length - 1])
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if ((e.code === 404 || e.status === 404 || e.statusCode === 404) && UPSERT_ALLOWED) {
|
|
27
|
+
query = INSERT.into(query.UPDATE.entity).entries(
|
|
28
|
+
_params ? Object.assign(_data, _params[_params.length - 1]) : _data
|
|
29
|
+
)
|
|
30
|
+
result = await srv.dispatch(new RestRequest({ query, _target }))
|
|
31
|
+
status = 201
|
|
32
|
+
} else {
|
|
33
|
+
throw e
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return next(e)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_req._result = { result, status }
|
|
41
|
+
next()
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const cds = require('../../_runtime/cds')
|
|
2
|
+
|
|
3
|
+
const { deepCopyObject } = require('../../_runtime/common/utils/copy')
|
|
4
|
+
const { checkKeys, checkStatic } = require('../../_runtime/cds-services/util/assert')
|
|
5
|
+
const { MULTIPLE_ERRORS } = require('../../_runtime/common/error/constants')
|
|
6
|
+
|
|
7
|
+
// this can be reused for flattening data on db layer, if necessary
|
|
8
|
+
// const _getFlattenedCopy = (data, key, entity) => {
|
|
9
|
+
// const d = {}
|
|
10
|
+
// const prefix = key + '_'
|
|
11
|
+
// // TODO: ignore or preserve unknown?
|
|
12
|
+
// const matches = Object.keys(entity.elements)
|
|
13
|
+
// .filter(ele => ele.startsWith(prefix))
|
|
14
|
+
// .map(ele => ele.replace(prefix, ''))
|
|
15
|
+
// if (matches.length) {
|
|
16
|
+
// const current = data[key]
|
|
17
|
+
// for (const k of matches) if (current[k] !== undefined) d[prefix + k] = current[k]
|
|
18
|
+
// const nested = Object.keys(current).filter(k => current[k] && typeof current[k] === 'object')
|
|
19
|
+
// for (const k of nested) Object.assign(d, _getFlattenedCopy({ [prefix + k]: current[k] }, prefix + k, entity))
|
|
20
|
+
// }
|
|
21
|
+
// return d
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
const _getDeepCopy = (data, definition, model, validations, skipKeys) => {
|
|
25
|
+
skipKeys || (definition.keys && validations.push(...checkKeys(definition, data)))
|
|
26
|
+
validations.push(...checkStatic(definition, data, true))
|
|
27
|
+
|
|
28
|
+
const d = {}
|
|
29
|
+
for (const k in data) {
|
|
30
|
+
const element = (definition.elements && definition.elements[k]) || (definition.params && definition.params[k])
|
|
31
|
+
if (!element) {
|
|
32
|
+
const { additional_properties } = cds.env.features
|
|
33
|
+
if (additional_properties === 'ignore' || !additional_properties) {
|
|
34
|
+
// ignore input (the default)
|
|
35
|
+
} else if (additional_properties === 'error') {
|
|
36
|
+
validations.push({ message: `Unknown property "${k}"`, code: 400 })
|
|
37
|
+
} else {
|
|
38
|
+
d[k] = data[k] && typeof data[k] === 'object' ? deepCopyObject(data[k]) : data[k]
|
|
39
|
+
}
|
|
40
|
+
} else if (element.isAssociation) {
|
|
41
|
+
d[k] = Array.isArray(data[k])
|
|
42
|
+
? data[k].map(d => _getDeepCopy(d, model.definitions[element.target], model, validations))
|
|
43
|
+
: _getDeepCopy(data[k], model.definitions[element.target], model, validations)
|
|
44
|
+
} else if (element._isStructured) {
|
|
45
|
+
d[k] = _getDeepCopy(data[k], element, model, validations)
|
|
46
|
+
} else {
|
|
47
|
+
d[k] = data[k]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return d
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getDeepCopy = (data, definition, model, skipKeys) => {
|
|
54
|
+
const validations = []
|
|
55
|
+
const copy = Array.isArray(data)
|
|
56
|
+
? data.map(d => _getDeepCopy(d, definition, model, validations, skipKeys))
|
|
57
|
+
: _getDeepCopy(data, definition, model, validations, skipKeys)
|
|
58
|
+
if (!validations.length) return [undefined, copy]
|
|
59
|
+
if (validations.length === 1) return [validations[0]]
|
|
60
|
+
return [Object.assign(new Error(MULTIPLE_ERRORS), { details: validations })]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
getDeepCopy
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap/cds",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.3",
|
|
4
4
|
"description": "SAP Cloud Application Programming Model - CDS for Node.js",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"lint-staged": {
|
|
43
|
+
"libx/odata/odata2cqn/grammar.pegjs": [
|
|
44
|
+
"npm run pegjs:odata2cqn && git add libx/odata/odata2cqn/parser.js"
|
|
45
|
+
],
|
|
43
46
|
"{libx,tests/_runtime}/**/*.js": [
|
|
44
47
|
"npx prettier --write"
|
|
45
48
|
]
|
package/server.js
CHANGED
|
@@ -40,16 +40,23 @@ module.exports = async function cds_server (options, o = { ...options, __proto__
|
|
|
40
40
|
if (o.logger) app.use (o.logger) //> basic request logging
|
|
41
41
|
if (o.toggler) app.use (o.toggler) //> feature toggler
|
|
42
42
|
|
|
43
|
+
// give uiflex a chance to plug into everything
|
|
44
|
+
if (cds.requires.extensibility) await require('./libx/_runtime/fiori/uiflex')() // REVISIT: later this should be a ext umbrella service
|
|
45
|
+
|
|
43
46
|
// load specified models or all in project
|
|
44
47
|
const csn = await cds.load (o.from||'*', {mocked:o.mocked})
|
|
45
|
-
|
|
48
|
+
const m = cds.linked(csn).minified()
|
|
49
|
+
cds.model = o.from = cds.linked (cds.compile.for.odata(m))
|
|
46
50
|
|
|
47
51
|
// connect to essential framework services if required
|
|
48
|
-
const _init = o.in_memory && (db => cds.deploy(
|
|
52
|
+
const _init = o.in_memory && (db => cds.deploy(m).to(db,o))
|
|
49
53
|
if (cds.requires.db) cds.db = await cds.connect.to ('db') .then (_init)
|
|
50
54
|
if (cds.requires.messaging) await cds.connect.to ('messaging')
|
|
51
55
|
if (cds.requires.multitenancy) await cds.mtx.in (app)
|
|
52
56
|
|
|
57
|
+
// serve graphql
|
|
58
|
+
if (cds.env.features.graphql) serve_graphql(app)
|
|
59
|
+
|
|
53
60
|
// serve all services declared in models
|
|
54
61
|
await cds.serve (o.service,o).in (app)
|
|
55
62
|
await cds.emit ('served', cds.services) //> hook for listeners
|
|
@@ -113,6 +120,16 @@ const _app_serve = function (endpoint) { return {
|
|
|
113
120
|
}}
|
|
114
121
|
|
|
115
122
|
|
|
123
|
+
// register graphql router on served event
|
|
124
|
+
function serve_graphql (app) {
|
|
125
|
+
cds.on('served', services => {
|
|
126
|
+
const GraphQLAdapter = require('./libx/gql/GraphQLAdapter')
|
|
127
|
+
app.use(new GraphQLAdapter(services, { graphiql: true }))
|
|
128
|
+
cds.log()("serving GraphQL endpoint for all services { at: '/graphql' }")
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
116
133
|
function cors (req, res, next) {
|
|
117
134
|
const { origin } = req.headers
|
|
118
135
|
if (origin) res.set('access-control-allow-origin', origin)
|
|
@@ -121,17 +138,22 @@ function cors (req, res, next) {
|
|
|
121
138
|
next()
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
function correlate (req,
|
|
125
|
-
|
|
126
|
-
const id =
|
|
127
|
-
|| req.headers['x-correlation-id'] || req.headers['x-correlationid']
|
|
141
|
+
function correlate (req, res, next) {
|
|
142
|
+
// derive correlation id from req
|
|
143
|
+
const id = req.headers['x-correlation-id'] || req.headers['x-correlationid']
|
|
128
144
|
|| req.headers['x-request-id'] || req.headers['x-vcap-request-id']
|
|
129
145
|
|| cds.utils.uuid()
|
|
130
|
-
cds.context
|
|
146
|
+
// new intermediate cds.context, if necessary
|
|
147
|
+
if (!cds.context) cds.context = { id }
|
|
148
|
+
// guarantee x-correlation-id going forward and set on res
|
|
149
|
+
req.headers['x-correlation-id'] = id
|
|
150
|
+
res.set('x-correlation-id', id)
|
|
151
|
+
// guaranteed access to cds.context._.req -> REVISIT
|
|
131
152
|
if (!cds.context._) cds.context._ = {}
|
|
132
153
|
if (!cds.context._.req) cds.context._.req = req
|
|
133
154
|
next()
|
|
134
155
|
}
|
|
135
156
|
|
|
157
|
+
|
|
136
158
|
// -------------------------------------------------------------------------
|
|
137
159
|
if (!module.parent) module.exports ({from:process.argv[2]})
|