@sap/cds 6.3.2 → 6.4.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 +95 -0
- package/apis/cds.d.ts +3 -1
- package/apis/core.d.ts +118 -90
- package/apis/cqn.d.ts +11 -2
- package/apis/internal/inference.d.ts +7 -2
- package/apis/ql.d.ts +49 -11
- package/apis/serve.d.ts +8 -1
- package/apis/services.d.ts +311 -305
- package/bin/build/buildTaskEngine.js +28 -36
- package/bin/build/buildTaskFactory.js +32 -81
- package/bin/build/buildTaskHandler.js +3 -2
- package/bin/build/buildTaskProvider.js +2 -2
- package/bin/build/buildTaskProviderFactory.js +5 -14
- package/bin/build/constants.js +0 -1
- package/bin/build/provider/buildTaskHandlerEdmx.js +7 -6
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +6 -5
- package/bin/build/provider/buildTaskHandlerInternal.js +9 -30
- package/bin/build/provider/buildTaskProviderInternal.js +70 -58
- package/bin/build/provider/fiori/index.js +6 -5
- package/bin/build/provider/hana/2migration.js +20 -3
- package/bin/build/provider/hana/2tabledata.js +1 -0
- package/bin/build/provider/hana/index.js +40 -17
- package/bin/build/provider/java/index.js +10 -10
- package/bin/build/provider/mtx/index.js +25 -16
- package/bin/build/provider/mtx/resourcesTarBuilder.js +22 -27
- package/bin/build/provider/mtx-extension/index.js +3 -2
- package/bin/build/provider/mtx-sidecar/index.js +16 -15
- package/bin/build/provider/nodejs/index.js +14 -56
- package/bin/build/util.js +56 -16
- package/bin/deploy/to-hana/cfUtil.js +2 -0
- package/bin/deploy/to-hana/gitUtil.js +1 -1
- package/bin/deploy/to-hana/hana.js +45 -38
- package/bin/deploy/to-hana/hdiDeployUtil.js +17 -12
- package/bin/deploy/to-hana/mtaUtil.js +13 -14
- package/bin/mtx/in-cds.js +3 -1
- package/bin/serve.js +1 -1
- package/bin/version.js +2 -1
- package/lib/auth/index.js +17 -15
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/cdsc.js +1 -0
- package/lib/compile/etc/_localized.js +2 -2
- package/lib/compile/for/lean_drafts.js +83 -0
- package/lib/compile/for/nodejs.js +1 -0
- package/lib/compile/minify.js +2 -1
- package/lib/compile/to/gql.js +1 -1
- package/lib/compile/to/sql.js +11 -1
- package/lib/core/entities.js +1 -1
- package/lib/core/index.js +9 -9
- package/lib/core/infer.js +1 -0
- package/lib/dbs/cds-deploy.js +97 -41
- package/lib/env/cds-env.js +9 -10
- package/lib/env/cds-requires.js +8 -2
- package/lib/env/defaults.js +0 -4
- package/lib/env/schemas/cds-rc.json +38 -0
- package/lib/ql/SELECT.js +10 -4
- package/lib/srv/bindings.js +1 -1
- package/lib/srv/factory.js +1 -1
- package/lib/srv/middlewares/cds-context.js +0 -2
- package/lib/srv/middlewares/ctx-auth.js +11 -0
- package/lib/srv/middlewares/ctx-model.js +22 -20
- package/lib/srv/middlewares/index.js +7 -9
- package/lib/srv/protocols/_legacy.js +4 -0
- package/lib/srv/protocols/graphql.js +2 -2
- package/lib/srv/protocols/index.js +7 -3
- package/lib/srv/srv-api.js +1 -0
- package/lib/srv/srv-methods.js +1 -1
- package/lib/utils/cds-utils.js +11 -0
- package/lib/utils/data.js +2 -2
- package/lib/utils/inflect.js +13 -12
- package/lib/utils/tar.js +43 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
- 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 +1 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
- package/libx/_runtime/cds-services/services/Service.js +23 -1
- package/libx/_runtime/cds-services/util/assert.js +0 -41
- package/libx/_runtime/common/composition/data.js +5 -1
- package/libx/_runtime/common/generic/auth/utils.js +3 -3
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/input.js +4 -24
- package/libx/_runtime/common/generic/paging.js +10 -9
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -0
- package/libx/_runtime/common/utils/csn.js +21 -15
- package/libx/_runtime/common/utils/draft.js +2 -1
- package/libx/_runtime/common/utils/resolveView.js +27 -4
- package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
- package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
- package/libx/_runtime/common/utils/templateProcessor.js +12 -15
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
- package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
- package/libx/_runtime/db/generic/input.js +7 -13
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
- package/libx/_runtime/db/sql-builder/UpsertBuilder.js +24 -0
- package/libx/_runtime/db/sql-builder/annotations.js +6 -3
- package/libx/_runtime/db/sql-builder/index.js +2 -0
- package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
- package/libx/_runtime/db/utils/columns.js +4 -2
- package/libx/_runtime/fiori/generic/read.js +1 -12
- package/libx/_runtime/fiori/lean-draft.js +657 -0
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/execute.js +5 -5
- package/libx/_runtime/hana/pool.js +16 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
- package/libx/_runtime/messaging/outbox/utils.js +109 -70
- package/libx/_runtime/messaging/service.js +16 -7
- package/libx/_runtime/remote/Service.js +15 -2
- package/libx/_runtime/remote/utils/client.js +41 -11
- package/libx/_runtime/sqlite/Service.js +4 -1
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +41 -0
- package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
- package/libx/_runtime/sqlite/execute.js +1 -1
- package/libx/_runtime/types/api.js +2 -2
- package/libx/rest/RestAdapter.js +15 -13
- package/package.json +1 -1
- package/server.js +2 -19
|
@@ -7,6 +7,17 @@ const hana = require('./driver')
|
|
|
7
7
|
const _require = require('../common/utils/require')
|
|
8
8
|
const getError = require('../common/error')
|
|
9
9
|
|
|
10
|
+
function multiTenantServiceManager() {
|
|
11
|
+
try {
|
|
12
|
+
// Make sure cds-mtxs APIs are loaded
|
|
13
|
+
require('@sap/cds-mtxs/lib') // eslint-disable-line cds/no-missing-dependencies
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (e.code === 'MODULE_NOT_FOUND') return null
|
|
16
|
+
else throw e
|
|
17
|
+
}
|
|
18
|
+
return cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager'] ? null : cds.xt?.serviceManager
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
function multiTenantInstanceManager(config = cds.env.requires.db) {
|
|
11
22
|
const { credentials } = config
|
|
12
23
|
|
|
@@ -63,10 +74,14 @@ async function credentials4(tenant, db) {
|
|
|
63
74
|
if (!db._instance_manager) {
|
|
64
75
|
const opts = db.options && db.options.credentials ? db.options : undefined
|
|
65
76
|
db._instance_manager = cds.requires.multitenancy
|
|
66
|
-
? await multiTenantInstanceManager(opts)
|
|
77
|
+
? multiTenantServiceManager() ?? (await multiTenantInstanceManager(opts))
|
|
67
78
|
: singleTenantInstanceManager(opts)
|
|
68
79
|
}
|
|
69
80
|
|
|
81
|
+
if (cds.xt?.serviceManager && !cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager']) {
|
|
82
|
+
return (await db._instance_manager.get(tenant)).credentials
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
return new Promise((resolve, reject) => {
|
|
71
86
|
db._instance_manager.get(tenant, (err, res) => {
|
|
72
87
|
if (err) return reject(err)
|
|
@@ -4,12 +4,13 @@ const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribe
|
|
|
4
4
|
const getTenantInfo = async tenant => {
|
|
5
5
|
const provisioningServiceName = cds.mtx ? 'ProvisioningService' : 'cds.xt.SaasProvisioningService'
|
|
6
6
|
const primaryKey = cds.mtx ? 'ID' : 'subscribedTenantId'
|
|
7
|
+
const path = cds.mtx ? 'tenant' : '/tenant' // HACK, otherwise the API doesn't work
|
|
7
8
|
|
|
8
9
|
const provisioning = await cds.connect.to(provisioningServiceName)
|
|
9
10
|
const tx = provisioning.tx({ user: new cds.User.Privileged() })
|
|
10
11
|
try {
|
|
11
12
|
const result = tenant
|
|
12
|
-
? _transform(await tx.get(
|
|
13
|
+
? _transform(await tx.get(path, { [primaryKey]: tenant }))
|
|
13
14
|
: (await tx.read('tenant')).map(o => _transform(o))
|
|
14
15
|
await tx.commit()
|
|
15
16
|
return result
|
|
@@ -72,7 +72,7 @@ class EndpointRegistry {
|
|
|
72
72
|
const tenantId = getTenant(authInfo)
|
|
73
73
|
const other = authInfo
|
|
74
74
|
? {
|
|
75
|
-
_: { req
|
|
75
|
+
_: { req, res }, // For `cds.context.http`
|
|
76
76
|
tenant: tenantId
|
|
77
77
|
}
|
|
78
78
|
: {}
|
|
@@ -33,8 +33,7 @@ const _multitenancyEnabled = () => cds.requires.multitenancy || _oldMtx()
|
|
|
33
33
|
// REVISIT: It's bad to have to rely on the subdomain.
|
|
34
34
|
// For all interactions where we perform the token exchange ourselves,
|
|
35
35
|
// we will be able to use the zoneId instead of the subdomain.
|
|
36
|
-
const _subdomainFromContext = context =>
|
|
37
|
-
context && context._ && context._.req && context._.req.authInfo && context._.req.authInfo.getSubdomain()
|
|
36
|
+
const _subdomainFromContext = context => context?.http.req?.authInfo?.getSubdomain()
|
|
38
37
|
|
|
39
38
|
class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
40
39
|
init() {
|
|
@@ -102,7 +101,6 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
102
101
|
return res
|
|
103
102
|
})
|
|
104
103
|
deploymentSrv.on('unsubscribe', async (req, next) => {
|
|
105
|
-
const res = await next()
|
|
106
104
|
const { tenant } = req.data
|
|
107
105
|
let subdomain
|
|
108
106
|
try {
|
|
@@ -112,6 +110,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
112
110
|
this.LOG.error("'unsubscribe' is not yet implemented for @sap/cds-mtxs")
|
|
113
111
|
throw e
|
|
114
112
|
}
|
|
113
|
+
const res = await next()
|
|
115
114
|
try {
|
|
116
115
|
const management = await this.getManagement(subdomain).waitUntilReady()
|
|
117
116
|
await management.undeploy()
|
|
@@ -42,29 +42,48 @@ const isUnrecoverable = (service, error) => {
|
|
|
42
42
|
return unrecoverable || isStandardError(error)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
delete msg[cdsUser]
|
|
50
|
-
// Promise resolve is necessary because we want to set `cds.context` only
|
|
51
|
-
// inside this call
|
|
52
|
-
return Promise.resolve().then(async () => {
|
|
53
|
-
if (userId) cds.context = { user: new cds.User.Privileged(userId) }
|
|
45
|
+
const processDefault = async (messages, { toBeDeleted, toBeUpdated, options, service }) => {
|
|
46
|
+
/** throws if an emit failed due to a programming error
|
|
47
|
+
* returns false if an emit failed due to temporary issues **/
|
|
48
|
+
const run = async ({ ID, process }) => {
|
|
54
49
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
await process()
|
|
51
|
+
toBeDeleted.push(ID)
|
|
57
52
|
} catch (e) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
if (isStandardError(e)) {
|
|
54
|
+
LOG.error(`${service.name}: Programming error detected:`, e)
|
|
55
|
+
toBeDeleted.push(ID)
|
|
56
|
+
throw new Error(`${service.name}: Programming error detected.`)
|
|
57
|
+
}
|
|
58
|
+
if (e.unrecoverable) {
|
|
59
|
+
LOG.error(`${service.name}: Unrecoverable error:`, e)
|
|
60
|
+
if (options.maxAttempts) {
|
|
61
|
+
const _msg = { ID, attempts: options.maxAttempts }
|
|
62
|
+
if (options.storeLastError !== false) _msg.lastError = e
|
|
63
|
+
toBeUpdated.push(_msg)
|
|
64
|
+
} else toBeDeleted.push(ID)
|
|
65
|
+
} else {
|
|
66
|
+
LOG.error(`${service.name}: Emit failed:`, e)
|
|
67
|
+
const _msg = { ID }
|
|
68
|
+
if (options.storeLastError !== false) _msg.lastError = e
|
|
69
|
+
toBeUpdated.push(_msg)
|
|
70
|
+
return false
|
|
64
71
|
}
|
|
65
|
-
throw Object.assign(new Error('processing failed'), { failedMessage })
|
|
66
72
|
}
|
|
67
|
-
}
|
|
73
|
+
}
|
|
74
|
+
if (options.parallel) {
|
|
75
|
+
const first = messages.next()?.value // First try to see if message can be emitted
|
|
76
|
+
if (first && (await run(first)) === false) return // No need to process the rest if the emit failed
|
|
77
|
+
const res = await Promise.allSettled([...messages].map(run))
|
|
78
|
+
const errors = res.filter(r => r.status === 'rejected').map(r => r.reason)
|
|
79
|
+
if (errors.length) {
|
|
80
|
+
throw new Error(`${service.name}: Programming errors detected.`)
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
for (const msg of messages) {
|
|
84
|
+
if ((await run(msg)) === false) break
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
// Note: This function can also run for each tenant on startup
|
|
@@ -89,15 +108,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
|
|
|
89
108
|
} catch (e) {
|
|
90
109
|
// could potentially be a timeout
|
|
91
110
|
const _waitingTime = waitingTime(opts.attempt)
|
|
92
|
-
LOG.error(
|
|
93
|
-
'Outbox SELECT FOR UPDATE failed',
|
|
94
|
-
opts.attempt > 0
|
|
95
|
-
? ''
|
|
96
|
-
: {
|
|
97
|
-
cause: e
|
|
98
|
-
},
|
|
99
|
-
`Retrying in ${Math.round(_waitingTime / 1000)} s`
|
|
100
|
-
)
|
|
111
|
+
LOG.error(`${name}: Message retrieval failed`, e, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
|
|
101
112
|
outboxRunner.schedule(
|
|
102
113
|
{
|
|
103
114
|
name,
|
|
@@ -108,57 +119,85 @@ const processMessages = async (service, tenant, _opts = {}) => {
|
|
|
108
119
|
)
|
|
109
120
|
return
|
|
110
121
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
let currMaxAttempts = 0
|
|
123
|
+
const messagesGen = function* () {
|
|
124
|
+
for (const _message of messages) {
|
|
125
|
+
const msg = _safeJSONParse(_message.msg)
|
|
126
|
+
const userId = msg[cdsUser]
|
|
127
|
+
delete msg[cdsUser]
|
|
128
|
+
currMaxAttempts = Math.max(_message.attempts || 0, currMaxAttempts)
|
|
129
|
+
const user = new cds.User.Privileged(userId)
|
|
130
|
+
if (!msg) continue
|
|
131
|
+
const res = {
|
|
132
|
+
process: () =>
|
|
133
|
+
Promise.resolve().then(async () => {
|
|
134
|
+
if (userId) cds.context = { user }
|
|
135
|
+
try {
|
|
136
|
+
return service._emitImmediate && (await service._emitImmediate(msg))
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (isUnrecoverable(service, e)) e.unrecoverable = true
|
|
139
|
+
throw e
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
ID: _message.ID,
|
|
143
|
+
msg,
|
|
144
|
+
user,
|
|
145
|
+
opts
|
|
146
|
+
}
|
|
147
|
+
yield res
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
|
|
151
|
+
const process = service.outbox?.process || this.process || processDefault
|
|
152
|
+
const toBeDeleted = []
|
|
153
|
+
const toBeUpdated = []
|
|
154
|
+
try {
|
|
155
|
+
await process(messagesGen(), {
|
|
156
|
+
toBeDeleted,
|
|
157
|
+
toBeUpdated,
|
|
158
|
+
service,
|
|
159
|
+
emit: service._emitImmediate.bind(service),
|
|
160
|
+
options: opts
|
|
161
|
+
})
|
|
162
|
+
} catch (e) {
|
|
123
163
|
if (opts.crashOnError !== false) letAppCrash = true
|
|
124
|
-
messagesToBeDeleted.push(error.failedMessage.ID)
|
|
125
164
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
|
|
166
|
+
const queries = []
|
|
167
|
+
const _waitingTime = waitingTime(currMaxAttempts)
|
|
168
|
+
if (toBeDeleted.length) queries.push(DELETE.from(messagesEntity).where('ID in', toBeDeleted))
|
|
169
|
+
if (toBeUpdated.length) {
|
|
170
|
+
for (const toBeUpdatedMsg of toBeUpdated) {
|
|
171
|
+
if (toBeDeleted.includes(toBeUpdatedMsg.ID)) continue
|
|
133
172
|
const data = {
|
|
134
173
|
attempts: { '+=': 1 }
|
|
135
174
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
await UPDATE(messagesEntity).where({ ID: error.failedMessage.ID }).set(data)
|
|
140
|
-
outboxRunner.schedule(
|
|
141
|
-
{
|
|
142
|
-
name,
|
|
143
|
-
tenant,
|
|
144
|
-
waitingTime: _waitingTime
|
|
145
|
-
},
|
|
146
|
-
() => processMessages(service, tenant, opts)
|
|
147
|
-
)
|
|
148
|
-
} else {
|
|
149
|
-
LOG.error(
|
|
150
|
-
'Emit failed',
|
|
151
|
-
{ service: name, event: error.failedMessage.event, cause: error.failedMessage.error },
|
|
152
|
-
'Unrecoverable, outbox entry deleted'
|
|
153
|
-
)
|
|
175
|
+
Object.assign(data, toBeUpdatedMsg)
|
|
176
|
+
if (data.lastError && typeof data.lastError !== 'string') data.lastError = util.inspect(data.lastError)
|
|
177
|
+
queries.push(UPDATE(messagesEntity).where({ ID: toBeUpdatedMsg.ID }).set(data))
|
|
154
178
|
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await Promise.all(queries)
|
|
182
|
+
|
|
183
|
+
if (letAppCrash) return
|
|
184
|
+
|
|
185
|
+
if (toBeUpdated.length) {
|
|
186
|
+
LOG.error(`${name}: Some messages could not be emitted, retrying in ${Math.round(_waitingTime / 1000)} s`)
|
|
187
|
+
return outboxRunner.schedule(
|
|
188
|
+
{
|
|
189
|
+
name,
|
|
190
|
+
tenant,
|
|
191
|
+
waitingTime: _waitingTime
|
|
192
|
+
},
|
|
193
|
+
() => processMessages(service, tenant, opts)
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
outboxRunner.success({ name, tenant })
|
|
197
|
+
if (toBeDeleted.length === opts.chunkSize) {
|
|
198
|
+
processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
|
|
155
199
|
} else {
|
|
156
|
-
|
|
157
|
-
if (messages.length === opts.chunkSize) {
|
|
158
|
-
processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
|
|
159
|
-
} else {
|
|
160
|
-
LOG._trace && LOG.trace(`All persistent-outbox messages processed for service '${service.name}'`)
|
|
161
|
-
}
|
|
200
|
+
LOG._trace && LOG.trace(`${name}: All messages processed`)
|
|
162
201
|
}
|
|
163
202
|
}, config)
|
|
164
203
|
spawn.on('done', () => {
|
|
@@ -4,6 +4,7 @@ const OutboxService = require('./Outbox')
|
|
|
4
4
|
const ExtendedModels = require('../../../lib/srv/srv-models')
|
|
5
5
|
|
|
6
6
|
const appId = require('./common-utils/appId')
|
|
7
|
+
const { context } = require('../../../lib/core/classes')
|
|
7
8
|
|
|
8
9
|
const _topic = declared => declared['@topic'] || declared.name
|
|
9
10
|
|
|
@@ -81,14 +82,22 @@ class MessagingService extends OutboxService {
|
|
|
81
82
|
const _msg = typeof event === 'object' ? event : { event, data, headers }
|
|
82
83
|
if (_msg instanceof cds.Event) return super.emit(_msg)
|
|
83
84
|
if (_msg.inbound && !cds.context) {
|
|
84
|
-
return cds._context.run(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
return cds._context.run(
|
|
86
|
+
{
|
|
87
|
+
tenant: _msg.tenant,
|
|
88
|
+
user: cds.User.privileged,
|
|
89
|
+
...(_msg._?.req && { req: _msg._.req }),
|
|
90
|
+
...(_msg._?.res && { res: _msg._.res })
|
|
91
|
+
},
|
|
92
|
+
async () => {
|
|
93
|
+
if (cds.model) {
|
|
94
|
+
const ctx = cds.context
|
|
95
|
+
ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
|
|
96
|
+
}
|
|
97
|
+
const msg = new cds.Event(this.message4(_msg))
|
|
98
|
+
return super.emit(msg)
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
return super.emit(msg)
|
|
91
|
-
})
|
|
100
|
+
)
|
|
92
101
|
}
|
|
93
102
|
const msg = new cds.Event(this.message4(_msg))
|
|
94
103
|
return super.emit(msg)
|
|
@@ -164,7 +164,7 @@ const resolvedTargetOfQuery = q => {
|
|
|
164
164
|
const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
|
|
165
165
|
return transitions.length && [transitions.length - 1].target
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
let logged
|
|
168
168
|
class RemoteService extends cds.Service {
|
|
169
169
|
init() {
|
|
170
170
|
if (!this.options.credentials) {
|
|
@@ -178,6 +178,17 @@ class RemoteService extends cds.Service {
|
|
|
178
178
|
getDestination((this.definition && this.definition.name) || this.datasource, this.options.credentials)
|
|
179
179
|
this.requestTimeout = this.options.credentials.requestTimeout
|
|
180
180
|
if (this.requestTimeout == null) this.requestTimeout = 60000
|
|
181
|
+
if (cds.env.features.fetch_csrf && !logged) {
|
|
182
|
+
// for logging once for all remote services
|
|
183
|
+
logged = true
|
|
184
|
+
LOG._warn &&
|
|
185
|
+
LOG.warn(
|
|
186
|
+
'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
// REVISIT: remove cds.env.features.fetch_csrf in next major ^7
|
|
190
|
+
this.csrf = cds.env.features.fetch_csrf || this.options.csrf
|
|
191
|
+
this.csrfInBatch = this.options.csrfInBatch
|
|
181
192
|
this.path = this.options.credentials.path
|
|
182
193
|
this.kind = getKind(this.options) // TODO: Simplify
|
|
183
194
|
|
|
@@ -210,7 +221,9 @@ class RemoteService extends cds.Service {
|
|
|
210
221
|
this.kind,
|
|
211
222
|
resolvedTarget,
|
|
212
223
|
returnType,
|
|
213
|
-
this.destinationOptions
|
|
224
|
+
this.destinationOptions,
|
|
225
|
+
this.csrf,
|
|
226
|
+
this.csrfInBatch
|
|
214
227
|
)
|
|
215
228
|
|
|
216
229
|
// hidden compat flag in order to suppress logging response body of failed request
|
|
@@ -22,7 +22,7 @@ const _sanitizeHeaders = headers => {
|
|
|
22
22
|
return headers
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
|
|
25
|
+
const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt, csrf, csrfInBatch }) => {
|
|
26
26
|
const { executeHttpRequestWithOrigin } = cloudSdk()
|
|
27
27
|
const destinationName = typeof destination === 'string' && destination
|
|
28
28
|
|
|
@@ -44,11 +44,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
44
44
|
|
|
45
45
|
let requestOptions
|
|
46
46
|
if (PPPD[requestConfig.method]) {
|
|
47
|
-
|
|
48
|
-
// Once we support batch requests (other than autoBatched GET requests),
|
|
49
|
-
// we must check the respective subrequests.
|
|
50
|
-
const csrfRequired = requestConfig._autoBatch ? false : cds.env.features.fetch_csrf === true
|
|
51
|
-
requestOptions = { fetchCsrfToken: csrfRequired }
|
|
47
|
+
requestOptions = { fetchCsrfToken: requestConfig._autoBatch ? csrfInBatch === true : csrf === true }
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
LOG._debug &&
|
|
@@ -282,11 +278,28 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
282
278
|
// eslint-disable-next-line complexity
|
|
283
279
|
const run = async (
|
|
284
280
|
requestConfig,
|
|
285
|
-
{
|
|
281
|
+
{
|
|
282
|
+
destination,
|
|
283
|
+
jwt,
|
|
284
|
+
kind,
|
|
285
|
+
resolvedTarget,
|
|
286
|
+
returnType,
|
|
287
|
+
suppressRemoteResponseBody,
|
|
288
|
+
destinationOptions,
|
|
289
|
+
csrf,
|
|
290
|
+
csrfInBatch
|
|
291
|
+
}
|
|
286
292
|
) => {
|
|
287
293
|
let response
|
|
288
294
|
try {
|
|
289
|
-
response = await _executeHttpRequest({
|
|
295
|
+
response = await _executeHttpRequest({
|
|
296
|
+
requestConfig,
|
|
297
|
+
destination,
|
|
298
|
+
destinationOptions,
|
|
299
|
+
jwt,
|
|
300
|
+
csrf,
|
|
301
|
+
csrfInBatch
|
|
302
|
+
})
|
|
290
303
|
} catch (e) {
|
|
291
304
|
// > axios received status >= 400 -> gateway error
|
|
292
305
|
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
@@ -466,7 +479,7 @@ const getReqOptions = (req, query, service) => {
|
|
|
466
479
|
|
|
467
480
|
// forward all dwc-* headers
|
|
468
481
|
if (service.options.forward_dwc_headers) {
|
|
469
|
-
const originalHeaders =
|
|
482
|
+
const originalHeaders = req.context?.http.req.headers || {}
|
|
470
483
|
for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
|
|
471
484
|
}
|
|
472
485
|
|
|
@@ -516,9 +529,26 @@ const getReqOptions = (req, query, service) => {
|
|
|
516
529
|
return reqOptions
|
|
517
530
|
}
|
|
518
531
|
|
|
519
|
-
const getAdditionalOptions = (
|
|
532
|
+
const getAdditionalOptions = (
|
|
533
|
+
req,
|
|
534
|
+
destination,
|
|
535
|
+
kind,
|
|
536
|
+
resolvedTarget,
|
|
537
|
+
returnType,
|
|
538
|
+
destinationOptions,
|
|
539
|
+
csrf,
|
|
540
|
+
csrfInBatch
|
|
541
|
+
) => {
|
|
520
542
|
const jwt = getJwt(req)
|
|
521
|
-
const additionalOptions = {
|
|
543
|
+
const additionalOptions = {
|
|
544
|
+
destination,
|
|
545
|
+
kind,
|
|
546
|
+
resolvedTarget,
|
|
547
|
+
returnType,
|
|
548
|
+
destinationOptions,
|
|
549
|
+
csrf,
|
|
550
|
+
csrfInBatch
|
|
551
|
+
}
|
|
522
552
|
if (jwt) additionalOptions.jwt = jwt
|
|
523
553
|
return additionalOptions
|
|
524
554
|
}
|
|
@@ -9,6 +9,7 @@ let _sqlite
|
|
|
9
9
|
*/
|
|
10
10
|
const localized = require('./localized')
|
|
11
11
|
const convertAssocToOneManaged = require('./convertAssocToOneManaged')
|
|
12
|
+
const convertDraftAdminPathExpression = require('./convertDraftAdminPathExpression')
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
15
|
* sqlite-specific execution
|
|
@@ -72,8 +73,10 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
72
73
|
|
|
73
74
|
_registerBeforeHandlers() {
|
|
74
75
|
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
75
|
-
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
|
|
76
|
+
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
|
|
76
77
|
|
|
78
|
+
if (cds.env.features.lean_draft && cds.db?.kind !== 'better-sqlite')
|
|
79
|
+
this.before('READ', '*', convertDraftAdminPathExpression)
|
|
77
80
|
this.before('READ', '*', convertAssocToOneManaged)
|
|
78
81
|
this.before('READ', '*', localized) // > has to run after rewrite
|
|
79
82
|
this.before('READ', '*', this._virtual)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
|
|
3
|
+
function sqliteConvertDraftAdminPathExpression(req) {
|
|
4
|
+
if (
|
|
5
|
+
!req.query?.SELECT ||
|
|
6
|
+
!req.query?._target?.name?.endsWith('.drafts') ||
|
|
7
|
+
req.query?.SELECT?.from?.args?.some(a => a.ref?.[0] === 'DRAFT_DraftAdministrativeData')
|
|
8
|
+
)
|
|
9
|
+
return
|
|
10
|
+
let hasDraftAdminPathExpression = false
|
|
11
|
+
|
|
12
|
+
const alias = req.query.SELECT.from.as
|
|
13
|
+
|
|
14
|
+
const _modifyCols = cols => {
|
|
15
|
+
return cols.map(col => {
|
|
16
|
+
if (col.ref?.length > 1 && col.ref[0] === 'DraftAdministrativeData') {
|
|
17
|
+
hasDraftAdminPathExpression = true
|
|
18
|
+
const newCol = { ...col }
|
|
19
|
+
newCol.ref = [...col.ref]
|
|
20
|
+
newCol.ref[0] = 'filterAdmin'
|
|
21
|
+
return newCol
|
|
22
|
+
} else if (col.ref?.length > 1 && alias && col.ref[0] === alias && col.ref[1] === 'DraftAdministrativeData') {
|
|
23
|
+
hasDraftAdminPathExpression = true
|
|
24
|
+
const newCol = { ...col }
|
|
25
|
+
newCol.ref = [...col.ref]
|
|
26
|
+
newCol.ref.shift()
|
|
27
|
+
newCol.ref[0] = 'filterAdmin'
|
|
28
|
+
return newCol
|
|
29
|
+
}
|
|
30
|
+
if (col.expand) {
|
|
31
|
+
const newCol = { ...col }
|
|
32
|
+
newCol.expand = _modifyCols(col.expand)
|
|
33
|
+
return newCol
|
|
34
|
+
}
|
|
35
|
+
return col
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clone = cds.ql.clone(req.query)
|
|
40
|
+
|
|
41
|
+
if (clone.SELECT.columns) clone.SELECT.columns = _modifyCols(req.query.SELECT.columns)
|
|
42
|
+
if (clone.SELECT.where) clone.SELECT.where = _modifyCols(req.query.SELECT.where)
|
|
43
|
+
|
|
44
|
+
if (hasDraftAdminPathExpression) {
|
|
45
|
+
clone
|
|
46
|
+
.join('DRAFT_DraftAdministrativeData', 'filterAdmin')
|
|
47
|
+
.on([
|
|
48
|
+
{ ref: alias ? [alias, 'DraftAdministrativeData_DraftUUID'] : ['DraftAdministrativeData_DraftUUID'] },
|
|
49
|
+
'=',
|
|
50
|
+
{ ref: ['filterAdmin', 'DraftUUID'] }
|
|
51
|
+
])
|
|
52
|
+
req.query = clone
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
sqliteConvertDraftAdminPathExpression._initial = true
|
|
56
|
+
module.exports = sqliteConvertDraftAdminPathExpression
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const InsertBuilder = require('../../db/sql-builder').InsertBuilder
|
|
2
|
+
const getAnnotatedColumns = require('../../db/sql-builder/annotations')
|
|
3
|
+
|
|
4
|
+
class CustomUpsertBuilder extends InsertBuilder {
|
|
5
|
+
annotatedColumns(entityName, csn) {
|
|
6
|
+
const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
|
|
7
|
+
|
|
8
|
+
if (updateAnnotatedColumns?.size) {
|
|
9
|
+
this.managedCols = Array.from(updateAnnotatedColumns.keys())
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return { insertAnnotatedColumns: updateAnnotatedColumns }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// REVISIT: We need to copy over the implementation for annotation handling
|
|
16
|
+
build() {
|
|
17
|
+
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
18
|
+
super.build()
|
|
19
|
+
const csnKeys =
|
|
20
|
+
(this._obj._target ? this._obj._target.keys : this._csn.definitions[this._obj.INSERT.into].keys) || {}
|
|
21
|
+
const keys = Object.keys(csnKeys).filter(k => !csnKeys[k].isAssociation)
|
|
22
|
+
const updates = []
|
|
23
|
+
const columns = this._obj.INSERT.columns || Object.keys(this._obj.INSERT.entries[0])
|
|
24
|
+
if (this.managedCols) {
|
|
25
|
+
columns.push(...this.managedCols)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
columns.forEach(col => {
|
|
29
|
+
const col_ = col.replace(/\./g, '_')
|
|
30
|
+
if (!keys.includes(col_)) updates.push(`${col_}=excluded.${col_}`)
|
|
31
|
+
})
|
|
32
|
+
const conflict = updates.length
|
|
33
|
+
? ` ON CONFLICT(${keys}) DO UPDATE SET ` + updates.join(', ')
|
|
34
|
+
: ` ON CONFLICT(${keys}) DO NOTHING`
|
|
35
|
+
|
|
36
|
+
this._outputObj.sql = this._outputObj.sql + conflict
|
|
37
|
+
return this._outputObj
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = CustomUpsertBuilder
|
|
@@ -23,6 +23,11 @@ const dependencies = {
|
|
|
23
23
|
const CustomUpdateBuilder = require('./CustomUpdateBuilder')
|
|
24
24
|
Object.defineProperty(dependencies, 'UpdateBuilder', { value: CustomUpdateBuilder })
|
|
25
25
|
return CustomUpdateBuilder
|
|
26
|
+
},
|
|
27
|
+
get UpsertBuilder() {
|
|
28
|
+
const CustomUpsertBuilder = require('./CustomUpsertBuilder')
|
|
29
|
+
Object.defineProperty(dependencies, 'UpsertBuilder', { value: CustomUpsertBuilder })
|
|
30
|
+
return CustomUpsertBuilder
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -206,7 +206,7 @@ function executePlainSQL(dbc, sql, values, isOne, postMapper) {
|
|
|
206
206
|
return executeSelectSQL(dbc, sql, values, isOne, postMapper)
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
if (/^\s*insert/i.test(sql)) {
|
|
209
|
+
if (/^\s*insert/i.test(sql) || /^\s*upsert/i.test(sql)) {
|
|
210
210
|
return executeInsertSQL(dbc, sql, values)
|
|
211
211
|
}
|
|
212
212
|
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* @typedef {object} TemplateProcessorPathOptions
|
|
51
|
-
* @property {object} [
|
|
52
|
-
* @property {function} [
|
|
51
|
+
* @property {object} [draftKeys]
|
|
52
|
+
* @property {function} [rowUUIDGenerator]
|
|
53
53
|
* @property {string[]} [segments=[]] - Path segments to relate the error message.
|
|
54
54
|
* @property {boolean} [includeKeyValues=false] Indicates whether the key values are included in the path segments
|
|
55
55
|
* The path segments are used to build the error target (a relative resource path)
|