@sap/cds 6.1.2 → 6.2.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 +92 -8
- package/apis/cds.d.ts +18 -6
- package/apis/connect.d.ts +1 -1
- package/apis/cqn.d.ts +1 -1
- package/apis/log.d.ts +23 -5
- package/apis/ql.d.ts +128 -61
- package/apis/services.d.ts +11 -0
- package/apis/test.d.ts +61 -0
- package/apis/utils.d.ts +15 -0
- package/app/fiori/preview.js +1 -0
- package/bin/build/buildTaskEngine.js +70 -22
- package/bin/build/buildTaskFactory.js +18 -11
- package/bin/build/buildTaskHandler.js +1 -1
- package/bin/build/buildTaskProviderFactory.js +3 -13
- package/bin/build/constants.js +0 -1
- package/bin/build/index.js +14 -6
- package/bin/build/provider/buildTaskHandlerEdmx.js +2 -3
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -2
- package/bin/build/provider/buildTaskHandlerInternal.js +3 -6
- package/bin/build/provider/buildTaskProviderInternal.js +51 -39
- package/bin/build/provider/fiori/index.js +3 -3
- package/bin/build/provider/hana/2migration.js +1 -1
- package/bin/build/provider/hana/index.js +34 -27
- package/bin/build/provider/java/index.js +6 -7
- package/bin/build/provider/mtx/index.js +20 -18
- package/bin/build/provider/mtx/resourcesTarBuilder.js +8 -11
- package/bin/build/provider/mtx-sidecar/index.js +13 -17
- package/bin/build/provider/nodejs/index.js +8 -7
- package/bin/build/util.js +22 -4
- package/bin/cds.js +8 -4
- package/bin/deploy/to-hana/cfUtil.js +53 -18
- package/bin/mtx/in-cds.js +1 -0
- package/bin/serve.js +37 -30
- package/lib/auth/basic-auth.js +33 -0
- package/lib/auth/dummy-auth.js +7 -0
- package/lib/auth/ias-auth.js +2 -0
- package/lib/auth/index.js +31 -0
- package/lib/auth/jwt-auth.js +3 -0
- package/lib/auth/mocked-users.js +72 -0
- package/lib/auth/passport-basic.js +12 -0
- package/lib/auth/passport-digest.js +14 -0
- package/lib/auth/xsuaa-auth.js +3 -0
- package/lib/compile/cds-compile.js +3 -3
- package/lib/compile/to/cdl.js +5 -1
- package/lib/compile/to/edm.js +8 -0
- package/lib/compile/to/gql.js +1 -0
- package/lib/compile/to/json.js +30 -5
- package/lib/compile/to/sql.js +3 -1
- package/lib/core/index.js +5 -1
- package/lib/dbs/cds-deploy.js +36 -6
- package/lib/env/cds-env.js +15 -5
- package/lib/env/cds-requires.js +51 -58
- package/lib/env/defaults.js +1 -0
- package/lib/env/schemas/cds-package.json +4 -0
- package/lib/env/schemas/cds-rc.json +63 -77
- package/lib/i18n/localize.js +16 -5
- package/lib/index.js +9 -4
- package/lib/log/cds-error.js +4 -6
- package/lib/log/cds-log.js +89 -53
- package/lib/log/service/index.js +1 -0
- package/lib/ql/CREATE.js +2 -5
- package/lib/ql/DELETE.js +1 -1
- package/lib/ql/DROP.js +1 -3
- package/lib/ql/INSERT.js +3 -3
- package/lib/ql/Query.js +10 -23
- package/lib/ql/SELECT.js +1 -2
- package/lib/ql/UPDATE.js +2 -2
- package/lib/ql/Whereable.js +7 -15
- package/lib/ql/cds-ql.js +9 -3
- package/lib/req/cds-context.js +11 -3
- package/lib/req/context.js +29 -23
- package/lib/req/locale.js +9 -5
- package/lib/req/request.js +1 -0
- package/lib/req/user.js +2 -1
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/cds-serve.js +21 -14
- package/lib/srv/middlewares/cds-context.js +29 -0
- package/lib/srv/middlewares/ctx-model.js +24 -0
- package/lib/srv/middlewares/errors.js +9 -0
- package/lib/srv/middlewares/index.js +22 -0
- package/lib/srv/middlewares/sap-statistics.js +13 -0
- package/lib/srv/middlewares/trace.js +102 -0
- package/lib/srv/protocols/_legacy.js +42 -0
- package/lib/srv/protocols/graphql.js +39 -0
- package/lib/srv/protocols/hcql.js +37 -0
- package/lib/srv/protocols/index.js +86 -0
- package/lib/srv/protocols/odata-v2-proxy.js +3767 -0
- package/lib/srv/protocols/odata-v2.js +26 -0
- package/lib/srv/protocols/odata-v4.js +16 -0
- package/lib/srv/protocols/rest.js +13 -0
- package/lib/srv/srv-api.js +5 -0
- package/lib/srv/srv-models.js +4 -6
- package/lib/utils/axios.js +3 -2
- package/lib/utils/cds-test.js +27 -21
- package/lib/utils/cds-utils.js +19 -20
- package/lib/utils/tar.js +175 -0
- package/libx/_runtime/audit/generic/personal/utils.js +18 -7
- package/libx/_runtime/audit/utils/v2.js +1 -0
- package/libx/_runtime/auth/index.js +4 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +76 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +8 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +15 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +9 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriInfo.js +5 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +12 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/RequestValidator.js +47 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -2
- package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -1
- package/libx/_runtime/cds-services/util/assert.js +7 -0
- package/libx/_runtime/common/aspects/relation.js +1 -1
- package/libx/_runtime/common/composition/data.js +61 -15
- package/libx/_runtime/common/composition/delete.js +0 -1
- package/libx/_runtime/common/composition/insert.js +0 -1
- package/libx/_runtime/common/composition/tree.js +4 -10
- package/libx/_runtime/common/composition/update.js +44 -21
- package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
- package/libx/_runtime/common/generic/crud.js +1 -2
- package/libx/_runtime/common/generic/etag.js +4 -4
- package/libx/_runtime/common/generic/input.js +21 -6
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/generic/put.js +7 -4
- package/libx/_runtime/common/generic/sorting.js +4 -4
- package/libx/_runtime/common/generic/temporal.js +3 -6
- package/libx/_runtime/common/i18n/messages.properties +0 -7
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
- package/libx/_runtime/common/utils/csn.js +0 -28
- package/libx/_runtime/common/utils/draft.js +8 -1
- package/libx/_runtime/common/utils/path.js +7 -1
- package/libx/_runtime/common/utils/propagateForeignKeys.js +122 -0
- package/libx/_runtime/common/utils/resolveView.js +2 -3
- package/libx/_runtime/common/utils/template.js +2 -3
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
- package/libx/_runtime/db/generic/input.js +6 -6
- package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
- package/libx/_runtime/fiori/generic/activate.js +2 -2
- package/libx/_runtime/fiori/generic/before.js +40 -72
- package/libx/_runtime/fiori/generic/cancel.js +2 -2
- package/libx/_runtime/fiori/generic/delete.js +2 -2
- package/libx/_runtime/fiori/generic/edit.js +2 -2
- package/libx/_runtime/fiori/generic/new.js +3 -5
- package/libx/_runtime/fiori/generic/patch.js +49 -43
- package/libx/_runtime/fiori/generic/prepare.js +2 -2
- package/libx/_runtime/fiori/generic/read.js +27 -37
- package/libx/_runtime/fiori/utils/where.js +4 -2
- package/libx/_runtime/hana/Service.js +1 -3
- package/libx/_runtime/hana/conversion.js +3 -0
- package/libx/_runtime/hana/driver.js +33 -3
- package/libx/_runtime/hana/dynatrace.js +1 -0
- package/libx/_runtime/hana/search2Contains.js +12 -1
- package/libx/_runtime/hana/search2cqn4sql.js +10 -27
- package/libx/_runtime/hana/streaming.js +1 -0
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/redis-messaging.js +1 -0
- package/libx/_runtime/remote/Service.js +2 -2
- package/libx/_runtime/remote/utils/client.js +35 -11
- package/libx/_runtime/remote/utils/data.js +7 -2
- package/libx/_runtime/sqlite/Service.js +18 -7
- package/libx/_runtime/sqlite/conversion.js +3 -0
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
- package/libx/_runtime/sqlite/localized.js +8 -8
- package/libx/odata/afterburner.js +39 -7
- package/libx/odata/cqn2odata.js +6 -3
- package/libx/odata/grammar.pegjs +66 -18
- package/libx/odata/index.js +3 -2
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +2 -0
- package/libx/rest/RestAdapter.js +62 -43
- package/libx/rest/middleware/input.js +2 -3
- package/libx/rest/middleware/parse.js +2 -1
- package/libx/rest/middleware/update.js +1 -1
- package/package.json +2 -2
- package/server.js +5 -4
- package/srv/mtx.cds +1 -1
- package/srv/mtx.js +4 -24
- package/lib/srv/adapters.js +0 -85
- package/lib/utils/resources/index.js +0 -48
- package/lib/utils/resources/tar.js +0 -49
- package/lib/utils/resources/utils.js +0 -11
- package/libx/_runtime/db/utils/propagateForeignKeys.js +0 -93
- package/libx/_runtime/extensibility/activate.js +0 -69
- package/libx/_runtime/extensibility/add.js +0 -50
- package/libx/_runtime/extensibility/addExtension.js +0 -72
- package/libx/_runtime/extensibility/defaults.js +0 -34
- package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
- package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
- package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
- package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
- package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
- package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
- package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
- package/libx/_runtime/extensibility/linter.js +0 -32
- package/libx/_runtime/extensibility/push.js +0 -118
- package/libx/_runtime/extensibility/service.js +0 -38
- package/libx/_runtime/extensibility/token.js +0 -57
- package/libx/_runtime/extensibility/utils.js +0 -131
- package/libx/_runtime/extensibility/validation.js +0 -50
- package/libx/_runtime/extensibility/views.js +0 -12
- package/srv/extensibility-service.cds +0 -59
- package/srv/extensibility-service.js +0 -1
- package/srv/extensions.cds +0 -8
- package/srv/model-provider.cds +0 -61
- package/srv/model-provider.js +0 -143
|
@@ -129,9 +129,37 @@ function _connectHanaClient(creds, tenant) {
|
|
|
129
129
|
|
|
130
130
|
let driver
|
|
131
131
|
|
|
132
|
-
const _getHanaDriver =
|
|
132
|
+
const _getHanaDriver = name => {
|
|
133
133
|
if (driver) return driver
|
|
134
134
|
|
|
135
|
+
let isConfigured = false
|
|
136
|
+
if (!name) {
|
|
137
|
+
let packageJson
|
|
138
|
+
try {
|
|
139
|
+
packageJson = require(cds.root + '/package.json')
|
|
140
|
+
} catch (e) {
|
|
141
|
+
LOG._debug && LOG.debug(`Could not find package.json. Trying to lookup hana driver automatically.`)
|
|
142
|
+
name = 'hdb'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (packageJson?.dependencies?.hdb) {
|
|
146
|
+
LOG._debug && LOG.debug(`"hdb" found in dependencies of "${cds.root}".`)
|
|
147
|
+
name = 'hdb'
|
|
148
|
+
isConfigured = true
|
|
149
|
+
} else if (packageJson?.dependencies?.['@sap/hana-client']) {
|
|
150
|
+
LOG._debug && LOG.debug(`"@sap/hana-client" found in dependencies of "${cds.root}".`)
|
|
151
|
+
name = '@sap/hana-client'
|
|
152
|
+
isConfigured = true
|
|
153
|
+
} else if (!name) {
|
|
154
|
+
LOG._debug &&
|
|
155
|
+
LOG.debug(
|
|
156
|
+
`Neither "hdb" nor "@sap/hana-client" found in dependencies of "${cds.root}". Trying to lookup hana driver automatically.`
|
|
157
|
+
)
|
|
158
|
+
// fallback to hdb in case both are not provided, which will fallback to @sap/hana-client if hdb is not installed
|
|
159
|
+
name = 'hdb'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
try {
|
|
136
164
|
driver = Object.assign({ name }, require(name))
|
|
137
165
|
|
|
@@ -161,9 +189,11 @@ const _getHanaDriver = (name = 'hdb') => {
|
|
|
161
189
|
|
|
162
190
|
return driver
|
|
163
191
|
} catch (e) {
|
|
164
|
-
if (name === 'hdb') {
|
|
192
|
+
if (name === 'hdb' && !isConfigured) {
|
|
165
193
|
LOG._debug && LOG.debug(`Failed to require "hdb" with error "${e.message}". Trying "@sap/hana-client" next.`)
|
|
166
194
|
return _getHanaDriver('@sap/hana-client')
|
|
195
|
+
} else if (isConfigured) {
|
|
196
|
+
throw new Error(`"${name}" could not be required. Please make sure it is installed.`)
|
|
167
197
|
} else {
|
|
168
198
|
throw new Error(
|
|
169
199
|
'Neither "hdb" nor "@sap/hana-client" could be required. Please make sure one of them is installed.'
|
|
@@ -172,4 +202,4 @@ const _getHanaDriver = (name = 'hdb') => {
|
|
|
172
202
|
}
|
|
173
203
|
}
|
|
174
204
|
|
|
175
|
-
module.exports = _getHanaDriver(
|
|
205
|
+
module.exports = _getHanaDriver()
|
|
@@ -59,7 +59,7 @@ const search2Contains = (cqnSearchPhrase, columns) => {
|
|
|
59
59
|
return expression
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const isContainsPredicateSupported = query => {
|
|
62
|
+
const isContainsPredicateSupported = (query, entity, columns2Search) => {
|
|
63
63
|
const cqnSearchPhrase = query.SELECT.search
|
|
64
64
|
|
|
65
65
|
if (cqnSearchPhrase && cqnSearchPhrase[0] && cqnSearchPhrase[0].val === ' ') return false
|
|
@@ -84,9 +84,20 @@ const isContainsPredicateSupported = query => {
|
|
|
84
84
|
// brackets are not supported as search operators in SAP HANA
|
|
85
85
|
if (cqnSearchPhrase.some(searchXpr => searchXpr.xpr)) return false
|
|
86
86
|
|
|
87
|
+
// join not optimized
|
|
88
|
+
if (entity.query?.SELECT.from.join) return false
|
|
89
|
+
|
|
90
|
+
// the CONTAINS function does not interoperate with columns that use the CONCAT function
|
|
91
|
+
if (_isColumnFunc(columns2Search, entity.query?.SELECT.columns)) return false
|
|
87
92
|
return true
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
const _isColumnFunc = (columns2Search, columnsDefs) =>
|
|
96
|
+
columns2Search.some(column2Search => {
|
|
97
|
+
if (column2Search.func) return true
|
|
98
|
+
return columnsDefs?.some(columnDef => columnDef.func && columnDef.as === column2Search.ref[0])
|
|
99
|
+
})
|
|
100
|
+
|
|
90
101
|
module.exports = {
|
|
91
102
|
isContainsPredicateSupported,
|
|
92
103
|
search2Contains
|
|
@@ -25,35 +25,29 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
25
25
|
if (!cqnSearchPhrase) return query
|
|
26
26
|
|
|
27
27
|
let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
|
|
28
|
-
const localizedAssociation =
|
|
28
|
+
const localizedAssociation = entity.associations?.localized
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
// there should be at least one localized element.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// suppress the localize handler from redirecting the query's target to the localized view
|
|
35
|
-
Object.defineProperty(query, '_suppressLocalization', { value: true })
|
|
36
|
-
|
|
37
|
-
const columnsDefs = entity.query && entity.query.SELECT.columns
|
|
38
|
-
const isSomeColumn2SearchUseFunction = _isSomeColumn2SearchAFunction(columns2Search, columnsDefs)
|
|
39
|
-
|
|
40
|
-
if (resolveLocalizedDataAtRuntime) {
|
|
30
|
+
// Resolve localized data at runtime if the localized association is defined for the target entity.
|
|
31
|
+
// Notice that if the localized association is defined, there should be at least one localized element.
|
|
32
|
+
if (localizedAssociation) {
|
|
41
33
|
const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
|
|
42
34
|
|
|
43
35
|
// REVISIT this is dirty but works for now
|
|
44
36
|
// replace $user_locale placeholder with the user locale or the HANA session context
|
|
45
37
|
onCondition[0].xpr[onCondition[0].xpr.length - 1] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
|
|
46
38
|
|
|
47
|
-
// inner join the target table with the _texts table (the _texts table contains
|
|
48
|
-
// the translated texts)
|
|
39
|
+
// inner join the target table with the _texts table (the _texts table contains the translated texts)
|
|
49
40
|
const localizedEntityName = localizedAssociation.target
|
|
50
41
|
query.join(localizedEntityName).on(onCondition)
|
|
51
42
|
|
|
52
43
|
// prevent SQL ambiguity error for columns with the same name
|
|
53
44
|
columns2Search = _addAliasToQuery(query, entity, columns2Search)
|
|
45
|
+
|
|
46
|
+
// suppress the localize handler from redirecting the query's target to the localized view
|
|
47
|
+
Object.defineProperty(query, '_suppressLocalization', { value: true })
|
|
54
48
|
} // else --> resolve localized texts via localized view (default)
|
|
55
49
|
|
|
56
|
-
const useContains =
|
|
50
|
+
const useContains = !!localizedAssociation && isContainsPredicateSupported(query, entity, columns2Search)
|
|
57
51
|
let expression
|
|
58
52
|
|
|
59
53
|
if (useContains) {
|
|
@@ -70,23 +64,12 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
70
64
|
return query
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
const _isSomeColumn2SearchAFunction = (columns2Search, columnsDefs) =>
|
|
74
|
-
columns2Search.some(column2Search => {
|
|
75
|
-
if (column2Search.func) return true
|
|
76
|
-
return columnsDefs && columnsDefs.some(columnDef => columnDef.func && columnDef.as === column2Search.ref[0])
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const _getLocalizedAssociation = entity => {
|
|
80
|
-
const associations = entity.associations
|
|
81
|
-
return associations && associations.localized
|
|
82
|
-
}
|
|
83
|
-
|
|
84
67
|
// The inner join modifies the original SELECT ... FROM query and adds ambiguity,
|
|
85
68
|
// therefore add the table/entity name (as a preceding element) to the columns ref
|
|
86
69
|
// to prevent a SQL ambiguity error.
|
|
87
70
|
const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
|
|
88
71
|
const SELECT = query.SELECT
|
|
89
|
-
const localizedEntityName =
|
|
72
|
+
const localizedEntityName = entity.associations?.localized.target
|
|
90
73
|
const elements = entity.elements
|
|
91
74
|
const entityName = entity.name
|
|
92
75
|
const getEntityName = columnRef => {
|
|
@@ -9,6 +9,7 @@ const STREAM_PLACEHOLDER = '[<stream>]'
|
|
|
9
9
|
const _loadStreamExtensionIfNeeded = () => {
|
|
10
10
|
const hana = require('./driver')
|
|
11
11
|
if (hana.name !== 'hdb') {
|
|
12
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
12
13
|
const extension = require('@sap/hana-client/extension/Stream.js')
|
|
13
14
|
return isDynatraceEnabled() ? dynatraceStreamingExtension(extension) : extension
|
|
14
15
|
}
|
|
@@ -39,8 +39,10 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
39
39
|
|
|
40
40
|
startListening(opt = {}) {
|
|
41
41
|
if (!this.subscribedTopics.size) return
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
if (!opt.doNotDeploy) {
|
|
43
|
+
const management = this.getManagement()
|
|
44
|
+
this.queued(management.createQueueAndSubscriptions.bind(management))()
|
|
45
|
+
}
|
|
44
46
|
this.queued(this.listenToClient.bind(this))(async (_topic, _payload, _other, { done, failed }) => {
|
|
45
47
|
const msg = Object.assign(normalizeIncomingMessage(_payload), _other || {})
|
|
46
48
|
msg.event = _topic
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds.js')
|
|
2
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
2
3
|
const ClientAmqp = require('@sap/xb-msg-amqp-v100').Client
|
|
3
4
|
const { connect, disconnect } = require('./connections')
|
|
4
5
|
const { hasPersistentOutbox } = require('../outbox/utils')
|
|
@@ -2,11 +2,14 @@ const cds = require('../../cds')
|
|
|
2
2
|
const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribedTenantId })
|
|
3
3
|
|
|
4
4
|
const getTenantInfo = async tenant => {
|
|
5
|
-
const
|
|
5
|
+
const provisioningServiceName = cds.mtx ? 'ProvisioningService' : 'cds.xt.SaasProvisioningService'
|
|
6
|
+
const primaryKey = cds.mtx ? 'ID' : 'subscribedTenantId'
|
|
7
|
+
|
|
8
|
+
const provisioning = await cds.connect.to(provisioningServiceName)
|
|
6
9
|
const tx = provisioning.tx({ user: new cds.User.Privileged() })
|
|
7
10
|
try {
|
|
8
11
|
const result = tenant
|
|
9
|
-
? _transform(await tx.get(`tenant`, {
|
|
12
|
+
? _transform(await tx.get(`tenant`, { [primaryKey]: tenant }))
|
|
10
13
|
: (await tx.read('tenant')).map(o => _transform(o))
|
|
11
14
|
await tx.commit()
|
|
12
15
|
return result
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds.js')
|
|
2
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
2
3
|
const express = require('express')
|
|
3
4
|
const getTenantInfo = require('./getTenantInfo.js')
|
|
4
5
|
const isSecured = () => cds.requires.auth && cds.requires.auth.credentials
|
|
@@ -14,6 +15,7 @@ class EndpointRegistry {
|
|
|
14
15
|
this.deployCallbacks = new Map()
|
|
15
16
|
if (isSecured()) {
|
|
16
17
|
const JWTStrategy = require('../../auth/strategies/JWT.js')
|
|
18
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
17
19
|
const passport = require('passport')
|
|
18
20
|
// REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
|
|
19
21
|
// In principle, user-facing endpoints might differ from messaging ones.
|
|
@@ -5,6 +5,7 @@ const optionsManagement = require('./enterprise-messaging-utils/options-manageme
|
|
|
5
5
|
const EMManagement = require('./enterprise-messaging-utils/EMManagement.js')
|
|
6
6
|
const optionsForSubdomain = require('./common-utils/optionsForSubdomain.js')
|
|
7
7
|
const authorizedRequest = require('./common-utils/authorizedRequest')
|
|
8
|
+
const getTenantInfo = require('./enterprise-messaging-utils/getTenantInfo')
|
|
8
9
|
const sleep = require('util').promisify(setTimeout)
|
|
9
10
|
const {
|
|
10
11
|
registerDeployEndpoints,
|
|
@@ -26,6 +27,9 @@ const _checkAppURL = appURL => {
|
|
|
26
27
|
)
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
const _oldMtx = () => cds.mtx
|
|
31
|
+
const _multitenancyEnabled = () => cds.requires.multitenancy || _oldMtx()
|
|
32
|
+
|
|
29
33
|
// REVISIT: It's bad to have to rely on the subdomain.
|
|
30
34
|
// For all interactions where we perform the token exchange ourselves,
|
|
31
35
|
// we will be able to use the zoneId instead of the subdomain.
|
|
@@ -77,8 +81,62 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
77
81
|
})
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
// New mtx based on @sap/cds-mtxs
|
|
85
|
+
async addMTXSHandlers() {
|
|
86
|
+
const deploymentSrv = await cds.connect.to('cds.xt.DeploymentService')
|
|
87
|
+
const provisioningSrv = await cds.connect.to('cds.xt.SaasProvisioningService')
|
|
88
|
+
deploymentSrv.impl(() => {
|
|
89
|
+
deploymentSrv.on('subscribe', async (req, next) => {
|
|
90
|
+
const res = await next()
|
|
91
|
+
const { tenant } = req.data
|
|
92
|
+
let subdomain
|
|
93
|
+
try {
|
|
94
|
+
const tenantInfo = await getTenantInfo(tenant) // @sap/cds-mtxs must provide that info
|
|
95
|
+
subdomain = tenantInfo.subdomain
|
|
96
|
+
} catch (e) {
|
|
97
|
+
this.LOG.error("'subscribe' is not yet implemented for @sap/cds-mtxs")
|
|
98
|
+
throw e
|
|
99
|
+
}
|
|
100
|
+
const management = await this.getManagement(subdomain).waitUntilReady()
|
|
101
|
+
await management.deploy()
|
|
102
|
+
return res
|
|
103
|
+
})
|
|
104
|
+
deploymentSrv.on('unsubscribe', async (req, next) => {
|
|
105
|
+
const res = await next()
|
|
106
|
+
const { tenant } = req.data
|
|
107
|
+
let subdomain
|
|
108
|
+
try {
|
|
109
|
+
const tenantInfo = await getTenantInfo(tenant) // @sap/cds-mtxs must provide that info
|
|
110
|
+
subdomain = tenantInfo.subdomain
|
|
111
|
+
} catch (e) {
|
|
112
|
+
this.LOG.error("'unsubscribe' is not yet implemented for @sap/cds-mtxs")
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const management = await this.getManagement(subdomain).waitUntilReady()
|
|
117
|
+
await management.undeploy()
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.LOG.error('Failed to delete messaging artifacts for subdomain', subdomain, '(', error, ')')
|
|
120
|
+
}
|
|
121
|
+
return res
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
provisioningSrv.impl(() => {
|
|
125
|
+
provisioningSrv.on('dependencies', async (req, next) => {
|
|
126
|
+
this.LOG._info && this.LOG.info('Include Enterprise-Messaging as SaaS dependency')
|
|
127
|
+
const res = (await next()) || []
|
|
128
|
+
const xsappname = this.options.credentials?.xsappname
|
|
129
|
+
if (xsappname) {
|
|
130
|
+
const exists = res.some(d => d.xsappname === xsappname)
|
|
131
|
+
if (!exists) res.push({ xsappname })
|
|
132
|
+
}
|
|
133
|
+
return res
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
80
138
|
startListening() {
|
|
81
|
-
const doNotDeploy =
|
|
139
|
+
const doNotDeploy = _multitenancyEnabled() && !this.options.deployForProvider
|
|
82
140
|
if (doNotDeploy) this.LOG._info && this.LOG.info('Skipping deployment of messaging artifacts for provider account')
|
|
83
141
|
super.startListening({ doNotDeploy })
|
|
84
142
|
if (!doNotDeploy && this.subscribedTopics.size) {
|
|
@@ -97,8 +155,9 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
|
|
|
97
155
|
async listenToClient(cb) {
|
|
98
156
|
_checkAppURL(this.optionsApp.appURL)
|
|
99
157
|
registerWebhookEndpoints(BASE_PATH, this.queueName, this.LOG, cb)
|
|
100
|
-
if (
|
|
101
|
-
await this.addMTXHandlers()
|
|
158
|
+
if (_multitenancyEnabled()) {
|
|
159
|
+
if (_oldMtx()) await this.addMTXHandlers()
|
|
160
|
+
else await this.addMTXSHandlers()
|
|
102
161
|
registerDeployEndpoints(BASE_PATH, this.queueName, async (tenantInfo, options) => {
|
|
103
162
|
const result = { queue: this.queueName, succeeded: [], failed: [] }
|
|
104
163
|
await Promise.all(
|
|
@@ -32,7 +32,7 @@ const hasPersistentOutbox = (srv, tenant) => {
|
|
|
32
32
|
if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
|
|
33
33
|
if (srv.options && srv.options.outbox && srv.options.outbox.kind && srv.options.outbox.kind !== 'persistent-outbox')
|
|
34
34
|
return false
|
|
35
|
-
if (cds.mtx && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
|
|
35
|
+
if ((cds.mtx || cds.requires.multitenancy) && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
|
|
36
36
|
return true
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -6,6 +6,7 @@ const LOG = cds.log('remote')
|
|
|
6
6
|
// disable sdk logger if not in debug mode
|
|
7
7
|
if (!LOG._debug) {
|
|
8
8
|
try {
|
|
9
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
9
10
|
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
10
11
|
sdkUtils.setGlobalLogLevel('error')
|
|
11
12
|
} catch (err) {
|
|
@@ -143,8 +144,7 @@ const _addHandlerActionFunction = (srv, def, target) => {
|
|
|
143
144
|
if (target) {
|
|
144
145
|
srv.on(event, target, async function (req) {
|
|
145
146
|
const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
|
|
146
|
-
if (this.kind === 'odata-v2')
|
|
147
|
-
return _handleV2BoundActionFunction(srv, def, req, `${shortEntityName}_${event}`, this.kind)
|
|
147
|
+
if (this.kind === 'odata-v2') return _handleV2BoundActionFunction(srv, def, req, event, this.kind)
|
|
148
148
|
const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
|
|
149
149
|
return _handleBoundActionFunction(srv, def, req, url)
|
|
150
150
|
})
|
|
@@ -24,8 +24,8 @@ const _sanitizeHeaders = headers => {
|
|
|
24
24
|
|
|
25
25
|
const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
|
|
26
26
|
const { executeHttpRequestWithOrigin } = cloudSdk()
|
|
27
|
-
|
|
28
27
|
const destinationName = typeof destination === 'string' && destination
|
|
28
|
+
|
|
29
29
|
if (destinationName) {
|
|
30
30
|
destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) || {}) }
|
|
31
31
|
} else if (destination.forwardAuthToken) {
|
|
@@ -59,13 +59,21 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
59
59
|
|
|
60
60
|
// cloud sdk requires a new mechanism to differentiate the priority of headers
|
|
61
61
|
// "custom" keeps the highest priority as before
|
|
62
|
-
|
|
62
|
+
const maxBodyLength = cds.env?.remote?.max_body_length
|
|
63
|
+
requestConfig = {
|
|
64
|
+
...requestConfig,
|
|
65
|
+
headers: {
|
|
66
|
+
custom: { ...requestConfig.headers }
|
|
67
|
+
},
|
|
68
|
+
...(maxBodyLength && { maxBodyLength })
|
|
69
|
+
}
|
|
63
70
|
|
|
64
71
|
return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
const cloudSdk = () => {
|
|
68
75
|
if (_cloudSdk) return _cloudSdk
|
|
76
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
69
77
|
_cloudSdk = require('@sap-cloud-sdk/http-client')
|
|
70
78
|
return _cloudSdk
|
|
71
79
|
}
|
|
@@ -145,10 +153,12 @@ function _defineProperty(obj, property, value) {
|
|
|
145
153
|
const map = (..._) => _defineProperty(_map.call(obj, ..._), property, value)
|
|
146
154
|
props.map = { value: map, enumerable: false, configurable: true, writable: true }
|
|
147
155
|
}
|
|
156
|
+
|
|
148
157
|
props[property] = { value: value, enumerable: false, configurable: true, writable: true }
|
|
149
158
|
for (const prop in props) {
|
|
150
159
|
Object.defineProperty(obj, prop, props[prop])
|
|
151
160
|
}
|
|
161
|
+
|
|
152
162
|
return obj
|
|
153
163
|
}
|
|
154
164
|
|
|
@@ -156,14 +166,17 @@ function _normalizeMetadata(prefix, data, results) {
|
|
|
156
166
|
const target = results !== undefined ? results : data
|
|
157
167
|
if (typeof target !== 'object' || target === null) return target
|
|
158
168
|
const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
|
|
169
|
+
|
|
159
170
|
for (const k of metadataKeys) {
|
|
160
171
|
const $ = k.replace(prefix, '$')
|
|
161
172
|
_defineProperty(target, $, data[k])
|
|
162
173
|
delete target[k]
|
|
163
174
|
}
|
|
175
|
+
|
|
164
176
|
if (Array.isArray(target)) {
|
|
165
177
|
return target.map(row => _normalizeMetadata(prefix, row))
|
|
166
178
|
}
|
|
179
|
+
|
|
167
180
|
// check properties for all and prop.results for odata v2
|
|
168
181
|
for (const [key, value] of Object.entries(target)) {
|
|
169
182
|
if (value && typeof value === 'object') {
|
|
@@ -171,6 +184,7 @@ function _normalizeMetadata(prefix, data, results) {
|
|
|
171
184
|
target[key] = _normalizeMetadata(prefix, value, nestedResults)
|
|
172
185
|
}
|
|
173
186
|
}
|
|
187
|
+
|
|
174
188
|
return target
|
|
175
189
|
}
|
|
176
190
|
const _getPurgedRespActionFunc = (data, returnType) => {
|
|
@@ -181,6 +195,7 @@ const _getPurgedRespActionFunc = (data, returnType) => {
|
|
|
181
195
|
return data[key]
|
|
182
196
|
}
|
|
183
197
|
}
|
|
198
|
+
|
|
184
199
|
return data
|
|
185
200
|
}
|
|
186
201
|
|
|
@@ -198,6 +213,7 @@ const _purgeODataV2 = (data, target, returnType, reqHeaders) => {
|
|
|
198
213
|
ieee754Compatible,
|
|
199
214
|
exponentialDecimals
|
|
200
215
|
)
|
|
216
|
+
|
|
201
217
|
return _normalizeMetadata(/^__/, data, convertedResponse)
|
|
202
218
|
}
|
|
203
219
|
|
|
@@ -217,6 +233,7 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
217
233
|
url: e.config ? e.config.baseURL + e.config.url : reqOptions.url,
|
|
218
234
|
headers: e.config ? e.config.headers : reqOptions.headers
|
|
219
235
|
}
|
|
236
|
+
|
|
220
237
|
if (options.batchRequest) {
|
|
221
238
|
e.request.body = reqOptions.data
|
|
222
239
|
}
|
|
@@ -227,9 +244,11 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
227
244
|
statusText: e.response.statusText,
|
|
228
245
|
headers: e.response.headers
|
|
229
246
|
}
|
|
247
|
+
|
|
230
248
|
if (e.response.data && !options.suppressRemoteResponseBody) {
|
|
231
249
|
response.body = e.response.data
|
|
232
250
|
}
|
|
251
|
+
|
|
233
252
|
e.response = response
|
|
234
253
|
}
|
|
235
254
|
|
|
@@ -270,12 +289,7 @@ const run = async (
|
|
|
270
289
|
response = await _executeHttpRequest({ requestConfig, destination, destinationOptions, jwt })
|
|
271
290
|
} catch (e) {
|
|
272
291
|
// > axios received status >= 400 -> gateway error
|
|
273
|
-
const msg =
|
|
274
|
-
(e.response &&
|
|
275
|
-
e.response.data &&
|
|
276
|
-
e.response.data.error &&
|
|
277
|
-
((e.response.data.error.message && e.response.data.error.message.value) || e.response.data.error.message)) ||
|
|
278
|
-
e.message
|
|
292
|
+
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
279
293
|
e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
|
|
280
294
|
|
|
281
295
|
const sanitizedError = _getSanitizedError(e, requestConfig, {
|
|
@@ -284,7 +298,6 @@ const run = async (
|
|
|
284
298
|
|
|
285
299
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
286
300
|
LOG._warn && LOG.warn(err)
|
|
287
|
-
|
|
288
301
|
throw err
|
|
289
302
|
}
|
|
290
303
|
|
|
@@ -312,7 +325,6 @@ const run = async (
|
|
|
312
325
|
})
|
|
313
326
|
|
|
314
327
|
LOG._warn && LOG.warn(err)
|
|
315
|
-
|
|
316
328
|
throw err
|
|
317
329
|
}
|
|
318
330
|
|
|
@@ -331,6 +343,7 @@ const run = async (
|
|
|
331
343
|
if (responseDataSplitted[1].startsWith('HTTP/1.1 2')) {
|
|
332
344
|
response.data = contentJSON
|
|
333
345
|
}
|
|
346
|
+
|
|
334
347
|
if (responseDataSplitted[1].startsWith('HTTP/1.1 4') || responseDataSplitted[1].startsWith('HTTP/1.1 5')) {
|
|
335
348
|
const innerError = contentJSON.error || contentJSON
|
|
336
349
|
innerError.status = Number(responseDataSplitted[1].match(/HTTP.*(\d{3})/m)[1])
|
|
@@ -357,6 +370,7 @@ const run = async (
|
|
|
357
370
|
}
|
|
358
371
|
return _purgeODataV4(response.data)
|
|
359
372
|
}
|
|
373
|
+
|
|
360
374
|
return response.data
|
|
361
375
|
}
|
|
362
376
|
|
|
@@ -368,6 +382,7 @@ const getJwt = req => {
|
|
|
368
382
|
return token[1]
|
|
369
383
|
}
|
|
370
384
|
}
|
|
385
|
+
|
|
371
386
|
return null
|
|
372
387
|
}
|
|
373
388
|
|
|
@@ -382,9 +397,11 @@ const _cqnToReqOptions = (query, service, req) => {
|
|
|
382
397
|
.replace(/\( /g, '(')
|
|
383
398
|
.replace(/ \)/g, ')')
|
|
384
399
|
}
|
|
400
|
+
|
|
385
401
|
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
386
402
|
reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, req.target) : queryObject.body
|
|
387
403
|
}
|
|
404
|
+
|
|
388
405
|
return reqOptions
|
|
389
406
|
}
|
|
390
407
|
|
|
@@ -395,9 +412,11 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
395
412
|
method: cleanQuery.substring(0, blankIndex).toUpperCase(),
|
|
396
413
|
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
397
414
|
}
|
|
415
|
+
|
|
398
416
|
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
399
417
|
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
400
418
|
}
|
|
419
|
+
|
|
401
420
|
return reqOptions
|
|
402
421
|
}
|
|
403
422
|
|
|
@@ -412,10 +431,12 @@ const _pathToReqOptions = (method, path, data, target) => {
|
|
|
412
431
|
// normalize in case parts[2] already starts with /
|
|
413
432
|
url = url.replace(/^\/\//, '/')
|
|
414
433
|
}
|
|
434
|
+
|
|
415
435
|
const reqOptions = { method, url }
|
|
416
436
|
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
417
437
|
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
418
438
|
}
|
|
439
|
+
|
|
419
440
|
return reqOptions
|
|
420
441
|
}
|
|
421
442
|
|
|
@@ -453,8 +474,11 @@ const getReqOptions = (req, query, service) => {
|
|
|
453
474
|
if (typeof reqOptions.data === 'object' && !Buffer.isBuffer(reqOptions.data)) {
|
|
454
475
|
reqOptions.headers['content-type'] = 'application/json'
|
|
455
476
|
reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
|
|
456
|
-
} else if (typeof reqOptions.data === 'string'
|
|
477
|
+
} else if (typeof reqOptions.data === 'string') {
|
|
478
|
+
reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
|
|
479
|
+
} else if (Buffer.isBuffer(reqOptions.data)) {
|
|
457
480
|
reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
|
|
481
|
+
if (!_hasHeader(req.headers, 'content-type')) reqOptions.headers['content-type'] = 'application/octet-stream'
|
|
458
482
|
}
|
|
459
483
|
}
|
|
460
484
|
reqOptions.url = formatPath(reqOptions.url)
|
|
@@ -78,9 +78,14 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
|
|
|
78
78
|
} else if (value === 'false') {
|
|
79
79
|
value = false
|
|
80
80
|
}
|
|
81
|
-
} else if (type === 'cds.Integer') {
|
|
81
|
+
} else if (type === 'cds.Integer' || type === 'cds.UInt8' || type === 'cds.Int16' || type === 'cds.Int32') {
|
|
82
82
|
value = parseInt(value, 10)
|
|
83
|
-
} else if (
|
|
83
|
+
} else if (
|
|
84
|
+
type === 'cds.Decimal' ||
|
|
85
|
+
type === 'cds.DecimalFloat' ||
|
|
86
|
+
type === 'cds.Integer64' ||
|
|
87
|
+
type === 'cds.Int64'
|
|
88
|
+
) {
|
|
84
89
|
const bigValue = big(value)
|
|
85
90
|
if (ieee754Compatible) {
|
|
86
91
|
// TODO test with arrayed => element.items.scale?
|
|
@@ -17,6 +17,7 @@ const execute = require('./execute')
|
|
|
17
17
|
|
|
18
18
|
const _new = url => {
|
|
19
19
|
if (url && url !== ':memory:') url = cds.utils.path.resolve(cds.root, url)
|
|
20
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
20
21
|
if (!_sqlite) _sqlite = require('sqlite3')
|
|
21
22
|
return new Promise((resolve, reject) => {
|
|
22
23
|
const dbc = new _sqlite.Database(url, err => {
|
|
@@ -102,8 +103,8 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
102
103
|
const credentials = this.options.credentials || this.options || {}
|
|
103
104
|
let dbUrl = credentials.database || credentials.url || credentials.host || ':memory:'
|
|
104
105
|
|
|
105
|
-
if (tenant && dbUrl
|
|
106
|
-
dbUrl = dbUrl.
|
|
106
|
+
if (tenant && dbUrl !== ':memory:') {
|
|
107
|
+
dbUrl = dbUrl.replace(/\.(db|sqlite)$/, `-${tenant}.$1`)
|
|
107
108
|
}
|
|
108
109
|
return dbUrl
|
|
109
110
|
}
|
|
@@ -153,13 +154,13 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
153
154
|
*/
|
|
154
155
|
// REVISIT: make tenant aware
|
|
155
156
|
async deploy(model, options = {}) {
|
|
156
|
-
|
|
157
|
-
if (
|
|
157
|
+
let createEntities = cds.compile.to.sql(model, options)
|
|
158
|
+
if (createEntities.length === 0) return // > nothing to deploy
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
let dropViews = []
|
|
161
|
+
let dropTables = []
|
|
161
162
|
for (const each of createEntities) {
|
|
162
|
-
const [, table, entity] = each.match(
|
|
163
|
+
const [, table, entity] = each.match(/^CREATE (?:(TABLE)|VIEW)\s+"?([^\s"(]+)"?/im) || []
|
|
163
164
|
if (table) dropTables.push({ DROP: { entity } })
|
|
164
165
|
else dropViews.push({ DROP: { view: entity } })
|
|
165
166
|
}
|
|
@@ -190,6 +191,12 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
190
191
|
await this.run(async tx => {
|
|
191
192
|
// This starts a new transaction if called from CLI, while joining
|
|
192
193
|
// existing root tx, e.g. when called from DeploymenrService
|
|
194
|
+
const [ext] = await tx.run(`SELECT 1 from sqlite_master where name='cds_xt_Extensions'`)
|
|
195
|
+
if (ext) {
|
|
196
|
+
// Poor man's schema evolution for MTX upgrade operations
|
|
197
|
+
createEntities = createEntities.filter(ct => !ct.match(/^CREATE TABLE cds_xt_Extensions/im))
|
|
198
|
+
dropTables = dropTables.filter(dt => dt.DROP.entity !== 'cds_xt_Extensions')
|
|
199
|
+
}
|
|
193
200
|
await tx.run(dropViews)
|
|
194
201
|
await tx.run(dropTables)
|
|
195
202
|
await tx.run(createEntities)
|
|
@@ -197,4 +204,8 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
197
204
|
|
|
198
205
|
return true
|
|
199
206
|
}
|
|
207
|
+
|
|
208
|
+
async disconnect(tenant) {
|
|
209
|
+
this.dbcs.delete(tenant)
|
|
210
|
+
}
|
|
200
211
|
}
|
|
@@ -39,15 +39,18 @@ const SQLITE_TYPE_CONVERSION_MAP = new Map([
|
|
|
39
39
|
['cds.Boolean', convertToBoolean],
|
|
40
40
|
['cds.Date', convertToDateString],
|
|
41
41
|
['cds.Integer64', convertInt64ToString],
|
|
42
|
+
['cds.Int64', convertInt64ToString],
|
|
42
43
|
['cds.DateTime', convertToISONoMillis],
|
|
43
44
|
['cds.Timestamp', convertToISOTime]
|
|
44
45
|
])
|
|
45
46
|
|
|
46
47
|
if (cds.env.features.bigjs) {
|
|
48
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
47
49
|
const Big = require('big.js')
|
|
48
50
|
const convertToBig = value => new Big(value)
|
|
49
51
|
|
|
50
52
|
SQLITE_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
|
|
53
|
+
SQLITE_TYPE_CONVERSION_MAP.set('cds.Int64', convertToBig)
|
|
51
54
|
SQLITE_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
|
|
52
55
|
}
|
|
53
56
|
|