@sap/cds 5.6.2 → 5.7.2
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 +133 -0
- package/_i18n/i18n_fr.properties +4 -4
- package/apis/cds.d.ts +7 -10
- package/apis/connect.d.ts +3 -3
- package/apis/core.d.ts +2 -4
- package/apis/models.d.ts +2 -3
- package/apis/ql.d.ts +0 -1
- package/apis/services.d.ts +7 -3
- package/bin/build/buildTaskFactory.js +16 -10
- package/bin/build/buildTaskProviderFactory.js +3 -3
- package/bin/build/constants.js +2 -1
- package/bin/build/provider/buildTaskProviderInternal.js +14 -14
- package/bin/build/provider/hana/2migration.js +2 -3
- package/bin/build/provider/hana/index.js +34 -0
- package/bin/build/provider/hana/migrationtable.js +90 -22
- package/bin/build/provider/hana/template/undeploy.json +5 -0
- package/bin/build/provider/node-cf/index.js +9 -2
- package/bin/serve.js +16 -18
- package/lib/compile/cdsc.js +15 -5
- package/lib/compile/etc/_localized.js +4 -4
- package/lib/compile/extend.js +8 -0
- package/lib/compile/index.js +3 -1
- package/lib/compile/minify.js +61 -0
- package/lib/compile/resolve.js +4 -1
- package/lib/compile/to/gql.js +9 -0
- package/lib/compile/to/sql.js +26 -30
- package/lib/connect/index.js +1 -1
- package/lib/core/entities.js +0 -3
- package/lib/core/infer.js +1 -0
- package/lib/core/reflect.js +0 -34
- package/lib/deploy.js +25 -17
- package/lib/env/defaults.js +3 -1
- package/lib/env/index.js +13 -4
- package/lib/env/presets.js +38 -0
- package/lib/env/requires.js +16 -11
- package/lib/index.js +13 -11
- package/lib/log/format/kibana.js +4 -2
- package/lib/log/index.js +2 -2
- package/lib/ql/Whereable.js +1 -0
- package/lib/req/cds-context.js +79 -0
- package/lib/req/context.js +5 -77
- package/lib/req/request.js +1 -1
- package/lib/serve/Service-api.js +8 -4
- package/lib/serve/Service-dispatch.js +0 -7
- package/lib/serve/Service-methods.js +6 -8
- package/lib/serve/Transaction.js +35 -30
- package/lib/serve/adapters.js +1 -4
- package/lib/utils/axios.js +1 -1
- package/libx/_runtime/audit/Service.js +44 -20
- package/libx/_runtime/audit/generic/personal/access.js +16 -11
- package/libx/_runtime/audit/generic/personal/modification.js +5 -5
- package/libx/_runtime/audit/generic/personal/utils.js +46 -37
- package/libx/_runtime/{common/auth → auth}/index.js +21 -7
- package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
- package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
- package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
- package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +26 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
- package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
- package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
- package/libx/_runtime/cds-services/services/Service.js +0 -6
- package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
- package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
- package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
- package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
- package/libx/_runtime/cds-services/util/assert.js +1 -262
- package/libx/_runtime/cds.js +6 -9
- package/libx/_runtime/common/aspects/entity.js +1 -1
- package/libx/_runtime/common/composition/delete.js +4 -2
- package/libx/_runtime/common/composition/update.js +22 -35
- package/libx/_runtime/common/composition/utils.js +3 -7
- package/libx/_runtime/common/error/standardError.js +11 -0
- package/libx/_runtime/common/generic/auth.js +63 -33
- package/libx/_runtime/common/generic/crud.js +11 -23
- package/libx/_runtime/common/generic/input.js +20 -0
- package/libx/_runtime/common/generic/paging.js +2 -2
- package/libx/_runtime/common/generic/put.js +4 -10
- package/libx/_runtime/common/generic/sorting.js +12 -30
- package/libx/_runtime/common/perf/index.js +24 -0
- package/libx/_runtime/common/utils/cqn.js +58 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +297 -121
- package/libx/_runtime/common/utils/csn.js +38 -56
- package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
- package/libx/_runtime/common/utils/resolveView.js +4 -5
- package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
- package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
- package/libx/_runtime/common/utils/structured.js +35 -25
- package/libx/_runtime/db/Service.js +0 -6
- package/libx/_runtime/db/expand/expand-v2.js +130 -0
- package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
- package/libx/_runtime/db/expand/index.js +3 -1
- package/libx/_runtime/db/generic/arrayed.js +3 -1
- package/libx/_runtime/db/generic/input.js +52 -10
- package/libx/_runtime/db/generic/integrity.js +367 -26
- package/libx/_runtime/db/generic/virtual.js +51 -13
- package/libx/_runtime/db/query/update.js +9 -3
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
- package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
- package/libx/_runtime/fiori/generic/activate.js +1 -0
- package/libx/_runtime/fiori/generic/before.js +2 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -0
- package/libx/_runtime/fiori/generic/patch.js +1 -1
- package/libx/_runtime/fiori/generic/read.js +155 -57
- package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
- package/libx/_runtime/fiori/uiflex/index.js +1 -1
- package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
- package/libx/_runtime/fiori/utils/delete.js +7 -1
- package/libx/_runtime/hana/Service.js +1 -8
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
- package/libx/_runtime/hana/execute.js +10 -4
- package/libx/_runtime/hana/pool.js +55 -45
- package/libx/_runtime/hana/search.js +7 -6
- package/libx/_runtime/hana/search2cqn4sql.js +8 -5
- package/libx/_runtime/hana/searchToContains.js +3 -1
- package/libx/_runtime/index.js +5 -5
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
- package/libx/_runtime/messaging/Outbox.js +53 -0
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
- package/libx/_runtime/messaging/common-utils/connections.js +14 -9
- package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
- package/libx/_runtime/messaging/file-based.js +5 -5
- package/libx/_runtime/messaging/message-queuing.js +2 -3
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
- package/libx/_runtime/messaging/outbox/utils.js +192 -0
- package/libx/_runtime/messaging/service.js +16 -30
- package/libx/_runtime/remote/Service.js +15 -0
- package/libx/_runtime/remote/utils/client.js +15 -3
- package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
- package/libx/_runtime/sqlite/Service.js +7 -10
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
- package/libx/_runtime/sqlite/execute.js +18 -12
- package/libx/_runtime/types/api.js +2 -1
- package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
- package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
- package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
- package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
- package/libx/odata/index.js +18 -15
- package/libx/odata/parser.js +1 -0
- package/libx/odata/utils.js +57 -0
- package/libx/rest/RestAdapter.js +2 -6
- package/libx/rest/utils/data.js +1 -6
- package/package.json +4 -3
- package/server.js +4 -5
- package/srv/audit-log.cds +87 -0
- package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
- package/srv/flex.js +1 -0
- package/srv/outbox.cds +11 -0
- package/srv/outbox.js +0 -0
- package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
- package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
- package/libx/odata/odata2cqn/index.js +0 -3
- package/libx/odata/odata2cqn/parser.js +0 -1
- package/libx/odata/readme.md +0 -1
- package/libx/odata/utils/index.js +0 -64
|
@@ -6,10 +6,9 @@ const hana = require('./driver')
|
|
|
6
6
|
|
|
7
7
|
const _require = require('../common/utils/require')
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
function multiTenantInstanceManager(config = cds.env.requires.db) {
|
|
10
|
+
const { credentials } = config
|
|
10
11
|
|
|
11
|
-
function multiTenantInstanceManager(db = cds.env.requires.db) {
|
|
12
|
-
const credentials = db.credentials
|
|
13
12
|
if (
|
|
14
13
|
!credentials ||
|
|
15
14
|
typeof credentials !== 'object' ||
|
|
@@ -22,10 +21,23 @@ function multiTenantInstanceManager(db = cds.env.requires.db) {
|
|
|
22
21
|
return new Promise((resolve, reject) => {
|
|
23
22
|
// REVISIT: better cache settings? current copied from old cds-hana...
|
|
24
23
|
// note: may need to be low for mtx tests -> configurable?
|
|
25
|
-
const opts =
|
|
24
|
+
const opts = {
|
|
26
25
|
cache_max_items: 1,
|
|
27
26
|
cache_item_expire_seconds: 1
|
|
28
|
-
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// check binding to both managed-hana and service-manager for instance migration
|
|
30
|
+
if (cds.env.features.hybrid_instance_manager && process.env.VCAP_SERVICES) {
|
|
31
|
+
const vcap = JSON.parse(process.env.VCAP_SERVICES)
|
|
32
|
+
if (vcap['managed-hana'] && vcap['service-manager']) {
|
|
33
|
+
opts.smOpts = vcap['service-manager'][0].credentials
|
|
34
|
+
opts.imOpts = vcap['managed-hana'][0].credentials
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// no double config -> take passed credentials (= cds.env.requires.db.credentials)
|
|
39
|
+
if (!opts.smOpts) Object.assign(opts, credentials)
|
|
40
|
+
|
|
29
41
|
// REVISIT: should be relative
|
|
30
42
|
// const mtxPath = require.resolve('@sap/cds-mtx', { paths: [process.env.pwd(), __dirname] })
|
|
31
43
|
// const imPath = require.resolve('@sap/instance-manager', { paths: [mtxPath] })
|
|
@@ -37,29 +49,29 @@ function multiTenantInstanceManager(db = cds.env.requires.db) {
|
|
|
37
49
|
})
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
function singleTenantInstanceManager(
|
|
41
|
-
const credentials =
|
|
52
|
+
function singleTenantInstanceManager(config = cds.env.requires.db) {
|
|
53
|
+
const { credentials } = config
|
|
42
54
|
|
|
43
55
|
if (!credentials || typeof credentials !== 'object' || !credentials.host) {
|
|
44
|
-
throw Object.assign(new Error('No or malformed db credentials'), { credentials
|
|
56
|
+
throw Object.assign(new Error('No or malformed db credentials'), { credentials })
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
// mock instance manager
|
|
48
60
|
return {
|
|
49
|
-
get: (_, cb) => {
|
|
50
|
-
cb(null, { credentials: credentials })
|
|
51
|
-
}
|
|
61
|
+
get: (_, cb) => cb(null, { credentials })
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
async function credentials4(tenant,
|
|
56
|
-
if (!
|
|
57
|
-
const opts = credentials ?
|
|
58
|
-
|
|
65
|
+
async function credentials4(tenant, db) {
|
|
66
|
+
if (!db._instance_manager) {
|
|
67
|
+
const opts = db.options && db.options.credentials ? db.options : undefined
|
|
68
|
+
db._instance_manager = cds.env.requires.db.multiTenant
|
|
69
|
+
? await multiTenantInstanceManager(opts)
|
|
70
|
+
: singleTenantInstanceManager(opts)
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
return new Promise((resolve, reject) => {
|
|
62
|
-
|
|
74
|
+
db._instance_manager.get(tenant, (err, res) => {
|
|
63
75
|
if (err) return reject(err)
|
|
64
76
|
if (!res)
|
|
65
77
|
return reject(Object.assign(new Error(`There is no instance for tenant "${tenant}"`), { statusCode: 404 }))
|
|
@@ -85,29 +97,12 @@ function factory4(creds, tenant) {
|
|
|
85
97
|
/*
|
|
86
98
|
* default generic-pool config
|
|
87
99
|
*/
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
// REVISIT: copied from old cds-hana
|
|
91
|
-
const _getMassagedCreds = function (creds) {
|
|
92
|
-
if (!('ca' in creds) && creds.certificate) {
|
|
93
|
-
creds.ca = creds.certificate
|
|
94
|
-
}
|
|
95
|
-
if ('encrypt' in creds && !('useTLS' in creds)) {
|
|
96
|
-
creds.useTLS = creds.encrypt
|
|
97
|
-
}
|
|
98
|
-
if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
|
|
99
|
-
creds.sslHostNameInCertificate = creds.hostname_in_certificate
|
|
100
|
-
}
|
|
101
|
-
if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
|
|
102
|
-
creds.sslValidateCertificate = creds.validate_certificate
|
|
103
|
-
}
|
|
104
|
-
return creds
|
|
105
|
-
}
|
|
100
|
+
const defaultConfig = { min: 0, max: 100, testOnBorrow: true }
|
|
106
101
|
|
|
107
102
|
const _getPoolConfig = function () {
|
|
108
103
|
const { pool: poolConfig } = cds.env.requires.db
|
|
109
104
|
|
|
110
|
-
const mergedConfig = Object.assign({},
|
|
105
|
+
const mergedConfig = Object.assign({}, defaultConfig, poolConfig)
|
|
111
106
|
|
|
112
107
|
// defaults
|
|
113
108
|
if (!poolConfig) {
|
|
@@ -134,14 +129,31 @@ const _getPoolConfig = function () {
|
|
|
134
129
|
return mergedConfig
|
|
135
130
|
}
|
|
136
131
|
|
|
132
|
+
// REVISIT: copied from old cds-hana
|
|
133
|
+
const _getMassagedCreds = function (creds) {
|
|
134
|
+
if (!('ca' in creds) && creds.certificate) {
|
|
135
|
+
creds.ca = creds.certificate
|
|
136
|
+
}
|
|
137
|
+
if ('encrypt' in creds && !('useTLS' in creds)) {
|
|
138
|
+
creds.useTLS = creds.encrypt
|
|
139
|
+
}
|
|
140
|
+
if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
|
|
141
|
+
creds.sslHostNameInCertificate = creds.hostname_in_certificate
|
|
142
|
+
}
|
|
143
|
+
if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
|
|
144
|
+
creds.sslValidateCertificate = creds.validate_certificate
|
|
145
|
+
}
|
|
146
|
+
return creds
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
const pools = new Map()
|
|
138
150
|
|
|
139
|
-
async function pool4(tenant,
|
|
151
|
+
async function pool4(tenant, db) {
|
|
140
152
|
if (!pools.get(tenant)) {
|
|
141
153
|
pools.set(
|
|
142
154
|
tenant,
|
|
143
155
|
new Promise((resolve, reject) => {
|
|
144
|
-
credentials4(tenant,
|
|
156
|
+
credentials4(tenant, db)
|
|
145
157
|
.then(creds => {
|
|
146
158
|
const config = _getPoolConfig()
|
|
147
159
|
LOG._info && LOG.info('effective pool configuration:', config)
|
|
@@ -217,8 +229,8 @@ async function resilientAcquire(pool, attempts = 1) {
|
|
|
217
229
|
}
|
|
218
230
|
|
|
219
231
|
module.exports = {
|
|
220
|
-
acquire: async (tenant,
|
|
221
|
-
const pool = await pool4(tenant,
|
|
232
|
+
acquire: async (tenant, db) => {
|
|
233
|
+
const pool = await pool4(tenant, db)
|
|
222
234
|
const _attempts = cds.env.requires.db.connection_attempts
|
|
223
235
|
const attempts = _attempts && !isNaN(_attempts) && parseInt(_attempts)
|
|
224
236
|
const client = await resilientAcquire(pool, attempts)
|
|
@@ -229,12 +241,10 @@ module.exports = {
|
|
|
229
241
|
return client._pool.release(client)
|
|
230
242
|
},
|
|
231
243
|
drain: async tenant => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
const p = await pool4(tenant)
|
|
244
|
+
const pool = pools.get(tenant)
|
|
245
|
+
if (!pool) return
|
|
236
246
|
pools.delete(tenant)
|
|
237
|
-
await
|
|
238
|
-
await
|
|
247
|
+
await pool.drain()
|
|
248
|
+
await pool.clear()
|
|
239
249
|
}
|
|
240
250
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
const search2cqn4sql = require('./search2cqn4sql')
|
|
3
3
|
|
|
4
|
+
const _setSearchOptions = (query, _searchOptions) => {
|
|
5
|
+
if (!(query && query.SELECT)) return
|
|
6
|
+
if (query.SELECT.search) Object.defineProperty(query, '_searchOptions', { value: _searchOptions })
|
|
7
|
+
if (query.SELECT.from.SELECT) _setSearchOptions(query.SELECT.from, _searchOptions)
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
function searchHandler(req) {
|
|
5
11
|
// REVISIT: remove feature toggle optimized_search after grace period
|
|
6
12
|
// inject the search2cqn4sql module into the rewrite handler only when
|
|
7
13
|
// the optimized search feature toggle is turned on
|
|
8
14
|
if (!cds.env.features.optimized_search) return
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
const search = query && query.SELECT && query.SELECT.search
|
|
12
|
-
|
|
13
|
-
if (search) {
|
|
14
|
-
Object.defineProperty(req.query, '_searchOptions', { value: { search2cqn4sql, locale: req.locale } })
|
|
15
|
-
}
|
|
16
|
+
_setSearchOptions(req.query, { search2cqn4sql, locale: req.locale })
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
// handlers marked with `._initial = true` run in sequence
|
|
@@ -28,12 +28,13 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
28
28
|
|
|
29
29
|
// If the localized association is defined for the target entity,
|
|
30
30
|
// there should be at least one localized element.
|
|
31
|
-
const
|
|
31
|
+
const resolveLocalizedDataAtRuntime = !!localizedAssociation
|
|
32
32
|
|
|
33
33
|
// suppress the localize handler from redirecting the query's target to the localized view
|
|
34
34
|
Object.defineProperty(query, '_suppressLocalization', { value: true })
|
|
35
35
|
|
|
36
|
-
if
|
|
36
|
+
// do not join if subquery exists - already done in there
|
|
37
|
+
if (resolveLocalizedDataAtRuntime && !query.SELECT.from.SELECT) {
|
|
37
38
|
const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
|
|
38
39
|
|
|
39
40
|
// replace $user_locale placeholder with the user locale or the HANA session context
|
|
@@ -52,15 +53,17 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
52
53
|
let expression
|
|
53
54
|
|
|
54
55
|
if (useContains) {
|
|
55
|
-
|
|
56
|
+
const funcCols = columnsToBeSearched.filter(col => col.func)
|
|
57
|
+
const refCols = columnsToBeSearched.filter(col => !col.func)
|
|
58
|
+
expression = [searchToContains(cqnSearchPhrase, refCols)]
|
|
59
|
+
if (funcCols.length) expression.push('or', ...searchToLike(cqnSearchPhrase, funcCols))
|
|
56
60
|
} else {
|
|
57
61
|
// No CONTAINS optimization possible. The search implementation for localized
|
|
58
62
|
// texts falls back to the LIKE predicate.
|
|
59
63
|
expression = searchToLike(cqnSearchPhrase, columnsToBeSearched)
|
|
60
64
|
}
|
|
61
|
-
|
|
62
65
|
// REVISIT: find out here if where or having must be used
|
|
63
|
-
query._aggregated ? query.having(expression) : query.where(expression)
|
|
66
|
+
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
64
67
|
return query
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -67,9 +67,11 @@ const searchToContains = (cqnSearchPhrase, columns) => {
|
|
|
67
67
|
const isContainsPredicateSupported = query => {
|
|
68
68
|
const cqnSearchPhrase = query.SELECT.search
|
|
69
69
|
|
|
70
|
+
if (cqnSearchPhrase && cqnSearchPhrase[0] && cqnSearchPhrase[0].val === ' ') return false
|
|
71
|
+
|
|
70
72
|
// REVISIT: In the future, to further optimize search queries, you might
|
|
71
73
|
// want to remove the following condition(s).
|
|
72
|
-
if (query._aggregated) return false
|
|
74
|
+
if (query._aggregated || /* new parser */ query.SELECT.groupBy) return false
|
|
73
75
|
|
|
74
76
|
// REVISIT: search terms starting with whitespace after a `NOT` operator does not
|
|
75
77
|
// return the expected result on SAP HANA (BCP 2180256508). In addition, double
|
package/libx/_runtime/index.js
CHANGED
|
@@ -14,13 +14,13 @@ module.exports = {
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
|
|
17
|
-
/** @type {import('./
|
|
17
|
+
/** @type {import('./auth')} */
|
|
18
18
|
get auth() {
|
|
19
|
-
return this._auth || (this._auth = require('./
|
|
19
|
+
return this._auth || (this._auth = require('./auth'))
|
|
20
20
|
},
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
get
|
|
24
|
-
return this._perf || (this._perf = require('./
|
|
22
|
+
/** @type {import('./common/perf')} */
|
|
23
|
+
get perf() {
|
|
24
|
+
return this._perf || (this._perf = require('./common/perf'))
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -21,11 +21,11 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
21
21
|
return super.init()
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
async emit(
|
|
25
|
-
const
|
|
24
|
+
async emit(msg) {
|
|
25
|
+
const _msg = this.message4(msg)
|
|
26
26
|
const client = this.getClient()
|
|
27
27
|
await this.queued(() => {})()
|
|
28
|
-
return client.emit(
|
|
28
|
+
return client.emit(_msg)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
startListening(opt = {}) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
processMessages,
|
|
5
|
+
registerMessageProcessor,
|
|
6
|
+
writeInOutbox,
|
|
7
|
+
hasPersistentOutbox,
|
|
8
|
+
isUnrecoverable
|
|
9
|
+
} = require('./outbox/utils')
|
|
10
|
+
|
|
11
|
+
class OutboxService extends cds.Service {
|
|
12
|
+
// eslint-disable-next-line require-await
|
|
13
|
+
async init() {
|
|
14
|
+
// REVISIT: add 'outbox' to list of module names?
|
|
15
|
+
const LOG = cds.log(this.name)
|
|
16
|
+
|
|
17
|
+
// REVISIT: Also allow to overwrite this.send
|
|
18
|
+
this._emitImmediate = this.emit
|
|
19
|
+
this.emit = async function (...args) {
|
|
20
|
+
const msg = typeof args[0] === 'object' ? args[0] : { event: args[0], data: args[1], headers: args[2] }
|
|
21
|
+
const context = this.context || cds.context
|
|
22
|
+
if (this.options.outbox && context && typeof context.on === 'function') {
|
|
23
|
+
const outboxOpts = Object.assign(
|
|
24
|
+
{},
|
|
25
|
+
(typeof cds.requires.outbox === 'object' && cds.requires.outbox) || {},
|
|
26
|
+
(this.options && typeof this.options.outbox === 'object' && this.options.outbox) || {}
|
|
27
|
+
)
|
|
28
|
+
if (hasPersistentOutbox(this, context.tenant)) {
|
|
29
|
+
// returns true if not yet registered
|
|
30
|
+
if (registerMessageProcessor(this.name, context)) {
|
|
31
|
+
context.on('succeeded', () => processMessages(this, context.tenant, outboxOpts))
|
|
32
|
+
}
|
|
33
|
+
await writeInOutbox(this.name, msg, context)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
// Revisit: Also allow maxAttempts?
|
|
37
|
+
context.on('succeeded', async () => {
|
|
38
|
+
try {
|
|
39
|
+
await this._emitImmediate(msg)
|
|
40
|
+
} catch (e) {
|
|
41
|
+
LOG._error && LOG.error('Emit failed', { event: msg.event, cause: e })
|
|
42
|
+
// opts.crashOnError is not official!!!
|
|
43
|
+
if (isUnrecoverable(this, e) && outboxOpts.crashOnError !== false) process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
return this._emitImmediate(msg)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = OutboxService
|
|
@@ -2,6 +2,7 @@ const cds = require('../../cds.js')
|
|
|
2
2
|
const LOG = cds.log('messaging')
|
|
3
3
|
const ClientAmqp = require('@sap/xb-msg-amqp-v100').Client
|
|
4
4
|
const { connect, disconnect } = require('./connections')
|
|
5
|
+
const { hasPersistentOutbox } = require('../outbox/utils')
|
|
5
6
|
|
|
6
7
|
const _JSONorString = string => {
|
|
7
8
|
try {
|
|
@@ -33,16 +34,19 @@ const addDataListener = (client, queue, prefix, cb) =>
|
|
|
33
34
|
})
|
|
34
35
|
})
|
|
35
36
|
|
|
36
|
-
const sender = (client, optionsApp) => client.sender(`${optionsApp.appName}-${optionsApp.appID}`)
|
|
37
|
+
const sender = (client, optionsApp) => client.sender(`${optionsApp.appName}-${optionsApp.appID}`)
|
|
37
38
|
|
|
38
|
-
const emit = ({ data, event: topic, headers = {} },
|
|
39
|
+
const emit = ({ data, event: topic, headers = {} }, stream, prefix) =>
|
|
39
40
|
new Promise((resolve, reject) => {
|
|
40
41
|
LOG._info && LOG.info('Emit', { topic })
|
|
41
42
|
const message = { ...headers, data }
|
|
42
43
|
const payload = { chunks: [Buffer.from(JSON.stringify(message))], type: 'application/json' }
|
|
43
44
|
const msg = {
|
|
44
45
|
done: resolve,
|
|
45
|
-
failed:
|
|
46
|
+
failed: e => {
|
|
47
|
+
if (e.condition === 'amqp:not-allowed') e.unrecoverable = true
|
|
48
|
+
reject(e)
|
|
49
|
+
},
|
|
46
50
|
payload,
|
|
47
51
|
target: {
|
|
48
52
|
properties: {
|
|
@@ -50,21 +54,21 @@ const emit = ({ data, event: topic, headers = {} }, sender, prefix) =>
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
stream.write(msg)
|
|
54
58
|
})
|
|
55
59
|
|
|
56
60
|
class AMQPClient {
|
|
57
|
-
constructor({ optionsAMQP,
|
|
61
|
+
constructor({ optionsAMQP, prefix, service, keepAlive = true }) {
|
|
58
62
|
this.optionsAMQP = optionsAMQP
|
|
59
|
-
this.optionsApp = optionsApp
|
|
60
|
-
this.queueName = queueName
|
|
61
63
|
this.prefix = prefix
|
|
62
64
|
this.keepAlive = keepAlive
|
|
65
|
+
this.service = service
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
connect() {
|
|
66
69
|
this.client = new ClientAmqp(this.optionsAMQP)
|
|
67
|
-
this.sender = sender(this.client, this.optionsApp)
|
|
70
|
+
this.sender = sender(this.client, this.service.optionsApp)
|
|
71
|
+
this.stream = this.sender.attach('')
|
|
68
72
|
return connect(this.client, this.keepAlive)
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -77,12 +81,15 @@ class AMQPClient {
|
|
|
77
81
|
|
|
78
82
|
async emit(msg) {
|
|
79
83
|
if (!this.client) await this.connect()
|
|
80
|
-
|
|
84
|
+
// REVISIT: Is this a robust way to find out if the connection is working?
|
|
85
|
+
if (hasPersistentOutbox(this.service, cds.context && cds.context.tenant) && !this.sender.opened())
|
|
86
|
+
throw new Error('AMQP: Sender is not open')
|
|
87
|
+
await emit(msg, this.stream, this.prefix.topic)
|
|
81
88
|
if (!this.keepAlive) return this.disconnect()
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
listen(cb) {
|
|
85
|
-
return addDataListener(this.client, this.queueName, this.prefix.queue, cb)
|
|
92
|
+
return addDataListener(this.client, this.service.queueName, this.prefix.queue, cb)
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const LOG = cds.log('messaging')
|
|
3
|
-
|
|
4
|
-
const MAX_WAITING_TIME = 1480000
|
|
5
|
-
|
|
6
|
-
const _waitingTime = x => (x > 18 ? MAX_WAITING_TIME : (Math.pow(1.5, x) + Math.random()) * 1000)
|
|
3
|
+
const waitingTime = require('./waitingTime')
|
|
7
4
|
|
|
8
5
|
const _connectUntilConnected = (client, x) => {
|
|
6
|
+
const _waitingTime = waitingTime(x)
|
|
9
7
|
setTimeout(() => {
|
|
10
|
-
connect(client, true)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
connect(client, true)
|
|
9
|
+
.then(() => {
|
|
10
|
+
LOG._warn && LOG.warn('Reconnected to Enterprise Messaging Client')
|
|
11
|
+
})
|
|
12
|
+
.catch(e => {
|
|
13
|
+
LOG._warn &&
|
|
14
|
+
LOG.warn(
|
|
15
|
+
`Connection to Enterprise Messaging Client lost: Reconnecting in ${Math.round(_waitingTime / 1000)} s`
|
|
16
|
+
)
|
|
17
|
+
_connectUntilConnected(client, x + 1)
|
|
18
|
+
})
|
|
19
|
+
}, _waitingTime)
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const connect = (client, keepAlive) => {
|
|
@@ -17,9 +17,8 @@ class EnterpriseMessagingShared extends AMQPWebhookMessaging {
|
|
|
17
17
|
const optionsAMQP = optionsMessaging(this.options, 'amqp10ws')
|
|
18
18
|
this.client = new AMQPClient({
|
|
19
19
|
optionsAMQP,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
prefix: { topic: 'topic:', queue: 'queue:' }
|
|
20
|
+
prefix: { topic: 'topic:', queue: 'queue:' },
|
|
21
|
+
service: this
|
|
23
22
|
})
|
|
24
23
|
return this.client
|
|
25
24
|
}
|
|
@@ -3,7 +3,7 @@ const LOG = cds.log('messaging')
|
|
|
3
3
|
const express = require('express')
|
|
4
4
|
const getTenantInfo = require('./getTenantInfo.js')
|
|
5
5
|
const isSecured = () => cds.requires.uaa && cds.requires.uaa.credentials
|
|
6
|
-
const { getTenant } = require('../../
|
|
6
|
+
const { getTenant } = require('../../auth/strategies/utils/xssec.js')
|
|
7
7
|
|
|
8
8
|
const _isAll = a => a && a.includes('all')
|
|
9
9
|
const _hasScope = (scope, req) =>
|
|
@@ -16,7 +16,7 @@ class EndpointRegistry {
|
|
|
16
16
|
this.webhookCallbacks = new Map()
|
|
17
17
|
this.deployCallbacks = new Map()
|
|
18
18
|
if (isSecured()) {
|
|
19
|
-
const JWTStrategy = require('../../
|
|
19
|
+
const JWTStrategy = require('../../auth/strategies/JWT.js')
|
|
20
20
|
const passport = require('passport')
|
|
21
21
|
passport.use(new JWTStrategy(cds.requires.uaa))
|
|
22
22
|
paths.forEach(path => {
|
|
@@ -162,21 +162,27 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
162
162
|
|
|
163
163
|
await this.queued(() => {})()
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
165
|
+
try {
|
|
166
|
+
await authorizedRequest({
|
|
167
|
+
method: 'POST',
|
|
168
|
+
uri: optionsMessagingREST.uri,
|
|
169
|
+
path: `/messagingrest/v1/topics/${encodeURIComponent(topic)}/messages`,
|
|
170
|
+
oa2: optionsMessagingREST.oa2,
|
|
171
|
+
tenant,
|
|
172
|
+
dataObj: message,
|
|
173
|
+
headers: {
|
|
174
|
+
'x-qos': 1
|
|
175
|
+
},
|
|
176
|
+
attemptInfo: () => LOG._info && LOG.info('Emit', { topic }),
|
|
177
|
+
errMsg,
|
|
178
|
+
target: { kind: 'MESSAGE', topic },
|
|
179
|
+
tokenStore: {}
|
|
180
|
+
})
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Note: If the topic rules don't allow the topic, we get a 403 (which is a strange choice by Event Mesh)
|
|
183
|
+
if (e.response && (e.response.statusCode === 400 || e.response.statusCode === 403)) e.unrecoverable = true
|
|
184
|
+
throw e
|
|
185
|
+
}
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
wildcarded(topic) {
|
|
@@ -20,14 +20,14 @@ class FileBasedMessaging extends MessagingService {
|
|
|
20
20
|
return super.init()
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async emit(
|
|
24
|
-
const
|
|
25
|
-
const e =
|
|
26
|
-
delete
|
|
23
|
+
async emit(msg) {
|
|
24
|
+
const _msg = this.message4(msg)
|
|
25
|
+
const e = _msg.event
|
|
26
|
+
delete _msg.event
|
|
27
27
|
await this.queued(lock)(this.file)
|
|
28
28
|
LOG._debug && LOG.debug('Emit', { topic: e, file: this.file })
|
|
29
29
|
try {
|
|
30
|
-
await fs.appendFile(this.file, `\n${e} ${JSON.stringify(
|
|
30
|
+
await fs.appendFile(this.file, `\n${e} ${JSON.stringify(_msg)}`)
|
|
31
31
|
} catch (e) {
|
|
32
32
|
LOG._debug && LOG.debug('Error', e)
|
|
33
33
|
} finally {
|
|
@@ -156,9 +156,8 @@ class MessageQueuing extends AMQPWebhookMessaging {
|
|
|
156
156
|
const optionsAMQP = optionsMessaging(this.options)
|
|
157
157
|
this.client = new AMQPClient({
|
|
158
158
|
optionsAMQP,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
prefix: { topic: 'topic://', queue: 'queue://' }
|
|
159
|
+
prefix: { topic: 'topic://', queue: 'queue://' },
|
|
160
|
+
service: this
|
|
162
161
|
})
|
|
163
162
|
return this.client
|
|
164
163
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const PROCESSING = 'processing'
|
|
2
|
+
const LOCKED = 'locked'
|
|
3
|
+
const QUEUED = 'queued'
|
|
4
|
+
const SCHEDULED = 'scheduled'
|
|
5
|
+
|
|
6
|
+
class OutboxRunner {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.states = new Map()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_setStateProp(prop, state, { name, tenant }) {
|
|
12
|
+
const statesSrv = this.states.get(name)
|
|
13
|
+
if (!statesSrv) {
|
|
14
|
+
const newStatesSrv = new Map()
|
|
15
|
+
newStatesSrv.set(tenant, { [prop]: state })
|
|
16
|
+
this.states.set(name, newStatesSrv)
|
|
17
|
+
return state
|
|
18
|
+
}
|
|
19
|
+
const obj = statesSrv.get(tenant)
|
|
20
|
+
if (!obj) {
|
|
21
|
+
statesSrv.set(tenant, { [prop]: state })
|
|
22
|
+
return state
|
|
23
|
+
}
|
|
24
|
+
obj[prop] = state
|
|
25
|
+
return state
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_getStateProp(prop, { name, tenant }) {
|
|
29
|
+
const statesSrv = this.states.get(name)
|
|
30
|
+
if (!statesSrv) return
|
|
31
|
+
const obj = statesSrv.get(tenant)
|
|
32
|
+
return obj && obj[prop]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
run({ name, tenant }, cb) {
|
|
36
|
+
const scheduled = this._getStateProp(SCHEDULED, { name, tenant })
|
|
37
|
+
if (scheduled) return // maybe make that configurable, we can also 'refresh' the current try
|
|
38
|
+
const processingState = this._getStateProp(PROCESSING, { name, tenant })
|
|
39
|
+
if (processingState === LOCKED) {
|
|
40
|
+
this._setStateProp(PROCESSING, QUEUED, { name, tenant })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (processingState === QUEUED) return
|
|
44
|
+
if (!processingState) this._setStateProp(PROCESSING, LOCKED, { name, tenant })
|
|
45
|
+
return cb()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
schedule({ name, tenant, waitingTime }, cb) {
|
|
49
|
+
if (this._getStateProp(SCHEDULED, { name, tenant })) return
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
this._setStateProp(SCHEDULED, undefined, { name, tenant })
|
|
52
|
+
return cb()
|
|
53
|
+
}, waitingTime)
|
|
54
|
+
this._setStateProp(SCHEDULED, timer, { name, tenant })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
end({ name, tenant }, cb) {
|
|
58
|
+
const processingState = this._getStateProp(PROCESSING, { name, tenant })
|
|
59
|
+
this._setStateProp(PROCESSING, undefined, { name, tenant })
|
|
60
|
+
if (processingState === QUEUED) {
|
|
61
|
+
return cb()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
success({ name, tenant }) {
|
|
66
|
+
const timer = this._getStateProp(SCHEDULED, { name, tenant })
|
|
67
|
+
if (timer) {
|
|
68
|
+
// once successful, we don't want to have another scheduled run
|
|
69
|
+
clearTimeout(timer)
|
|
70
|
+
this._setStateProp(SCHEDULED, undefined, { name, tenant })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = OutboxRunner
|