@sap/cds 6.1.3 → 6.2.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 +83 -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/util/assert.js +4 -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 +4 -4
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/generic/put.js +3 -3
- package/libx/_runtime/common/generic/sorting.js +4 -4
- package/libx/_runtime/common/generic/temporal.js +3 -3
- 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/resolveView.js +2 -3
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
- package/libx/_runtime/db/generic/input.js +3 -3
- 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 +2 -2
- package/libx/_runtime/fiori/generic/patch.js +49 -37
- 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 +8 -3
- 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/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 -33
- 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/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 -60
- 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
|
@@ -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
|
})
|
|
@@ -59,12 +59,13 @@ 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
|
+
const maxBodyLength = cds.env?.remote?.max_body_length
|
|
62
63
|
requestConfig = {
|
|
63
|
-
...(cds.env?.remote?.max_body_length && { maxBodyLength: cds.env.remote.max_body_length }),
|
|
64
64
|
...requestConfig,
|
|
65
65
|
headers: {
|
|
66
66
|
custom: { ...requestConfig.headers }
|
|
67
|
-
}
|
|
67
|
+
},
|
|
68
|
+
...(maxBodyLength && { maxBodyLength })
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
|
|
@@ -72,6 +73,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
72
73
|
|
|
73
74
|
const cloudSdk = () => {
|
|
74
75
|
if (_cloudSdk) return _cloudSdk
|
|
76
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
75
77
|
_cloudSdk = require('@sap-cloud-sdk/http-client')
|
|
76
78
|
return _cloudSdk
|
|
77
79
|
}
|
|
@@ -472,8 +474,11 @@ const getReqOptions = (req, query, service) => {
|
|
|
472
474
|
if (typeof reqOptions.data === 'object' && !Buffer.isBuffer(reqOptions.data)) {
|
|
473
475
|
reqOptions.headers['content-type'] = 'application/json'
|
|
474
476
|
reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
|
|
475
|
-
} 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)) {
|
|
476
480
|
reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
|
|
481
|
+
if (!_hasHeader(req.headers, 'content-type')) reqOptions.headers['content-type'] = 'application/octet-stream'
|
|
477
482
|
}
|
|
478
483
|
}
|
|
479
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
|
|
|
@@ -37,7 +37,7 @@ const _convert = (refEntries, req) => {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// REVISIT once sql can handle structured keys properly, this handler should not be required anymore
|
|
40
|
-
const
|
|
40
|
+
const sqliteConvertAssocToOneManaged = function (req) {
|
|
41
41
|
// do simple checks upfront and exit early
|
|
42
42
|
if (!(req.query && req.query.SELECT) || typeof req.query === 'string') return
|
|
43
43
|
if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
|
|
@@ -49,6 +49,6 @@ const _handler = function (req) {
|
|
|
49
49
|
_convert(_getConvertibleEntries(req), req)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
sqliteConvertAssocToOneManaged._initial = true
|
|
53
53
|
|
|
54
|
-
module.exports =
|
|
54
|
+
module.exports = sqliteConvertAssocToOneManaged
|
|
@@ -7,6 +7,8 @@ if (cds.env.i18n && Array.isArray(cds.env.i18n.for_sqlite) && !cds.env.i18n.for_
|
|
|
7
7
|
LOG._warn && LOG.warn('No language configuration found in cds.env.i18n.for_sqlite')
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const _translations = cds.env.i18n.for_sqlite.reduce((all, l) => ((all[l] = true), all), {})
|
|
11
|
+
|
|
10
12
|
// REVISIT: this is actually configurable
|
|
11
13
|
// there is no localized.en.<name>
|
|
12
14
|
const getLocalize = (locale, model) => name => {
|
|
@@ -15,15 +17,13 @@ const getLocalize = (locale, model) => name => {
|
|
|
15
17
|
// if we get here via onReadDraft, target is already localized
|
|
16
18
|
// because of subrequest using SELECT.from as new target
|
|
17
19
|
const target = model.definitions[ensureUnlocalized(name)]
|
|
18
|
-
|
|
19
|
-
target &&
|
|
20
|
-
target['@cds.localized'] !== false &&
|
|
21
|
-
model.definitions[`localized.${locale !== 'en' ? locale + '.' : ''}${name}`]
|
|
20
|
+
if (target?.['@cds.localized'] === false) return name
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
const view = model.definitions[`localized${locale in _translations ? '.' + locale : ''}.${name}`]
|
|
23
|
+
return view?.name || name
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const sqliteLocalized = function (req) {
|
|
27
27
|
const { query } = req
|
|
28
28
|
|
|
29
29
|
// do simple checks upfront and exit early
|
|
@@ -44,6 +44,6 @@ const _handler = function (req) {
|
|
|
44
44
|
redirect(query, getLocalize(req.locale, this.model))
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
sqliteLocalized._initial = true
|
|
48
48
|
|
|
49
|
-
module.exports =
|
|
49
|
+
module.exports = sqliteLocalized
|
|
@@ -163,20 +163,27 @@ function _convertVal(element, value) {
|
|
|
163
163
|
if (value === null) return value
|
|
164
164
|
switch (element._type) {
|
|
165
165
|
case 'cds.Integer':
|
|
166
|
+
case 'cds.UInt8':
|
|
167
|
+
case 'cds.Int16':
|
|
168
|
+
case 'cds.Int32':
|
|
166
169
|
// eslint-disable-next-line no-case-declarations
|
|
167
170
|
const n = Number(value)
|
|
168
171
|
if (Number.isSafeInteger(n)) return n
|
|
169
172
|
throw new Error('Not a valid integer') // TODO
|
|
173
|
+
|
|
170
174
|
case 'cds.String':
|
|
171
175
|
case 'cds.LargeString':
|
|
172
176
|
case 'cds.Decimal':
|
|
173
177
|
case 'cds.DecimalFloat':
|
|
174
178
|
case 'cds.Double':
|
|
179
|
+
case 'cds.Int64':
|
|
175
180
|
case 'cds.Integer64':
|
|
176
181
|
if (typeof value === 'string') return value
|
|
177
182
|
return String(value)
|
|
183
|
+
|
|
178
184
|
case 'cds.Boolean':
|
|
179
185
|
return typeof value === 'string' ? value === 'true' : value
|
|
186
|
+
|
|
180
187
|
default:
|
|
181
188
|
return value
|
|
182
189
|
}
|
|
@@ -189,12 +196,13 @@ function _processSegments(from, model, namespace) {
|
|
|
189
196
|
let path
|
|
190
197
|
let keys = null
|
|
191
198
|
let keyCount = 0
|
|
192
|
-
let incompleteKeys
|
|
199
|
+
let incompleteKeys = false
|
|
193
200
|
let one
|
|
194
201
|
let target
|
|
195
202
|
for (let i = 0; i < ref.length; i++) {
|
|
196
203
|
const seg = ref[i].id || ref[i]
|
|
197
|
-
|
|
204
|
+
const whereRef = ref[i].where
|
|
205
|
+
let params = whereRef && where2obj(whereRef)
|
|
198
206
|
|
|
199
207
|
if (incompleteKeys) {
|
|
200
208
|
// > key
|
|
@@ -234,7 +242,7 @@ function _processSegments(from, model, namespace) {
|
|
|
234
242
|
if (current.params && current.kind === 'entity') {
|
|
235
243
|
// > View with params
|
|
236
244
|
target = current
|
|
237
|
-
if (
|
|
245
|
+
if (whereRef) {
|
|
238
246
|
keyCount += addRefToWhereIfNecessary(ref[i].where, current)
|
|
239
247
|
_resolveAliasesInXpr(ref[i].where, current)
|
|
240
248
|
_resolveAliasInParams(params, current)
|
|
@@ -265,10 +273,28 @@ function _processSegments(from, model, namespace) {
|
|
|
265
273
|
// > entity
|
|
266
274
|
target = current
|
|
267
275
|
one = !!(ref[i].where || current._isSingleton)
|
|
276
|
+
|
|
277
|
+
let action
|
|
278
|
+
if (current.actions) {
|
|
279
|
+
const nextRef = typeof ref[i + 1] === 'string' && ref[i + 1]
|
|
280
|
+
const shortName = nextRef && nextRef.replace(namespace + '.', '')
|
|
281
|
+
action = shortName && current.actions[shortName]
|
|
282
|
+
}
|
|
283
|
+
|
|
268
284
|
incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
285
|
+
|
|
286
|
+
if (incompleteKeys && action) {
|
|
287
|
+
if (action['@cds.odata.bindingparameter.collection']) {
|
|
288
|
+
incompleteKeys = false
|
|
289
|
+
} else {
|
|
290
|
+
const msg = `Bound operations are not supported on entity collections.`
|
|
291
|
+
throw Object.assign(new Error(msg), { statusCode: 501 })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (whereRef) {
|
|
296
|
+
keyCount += addRefToWhereIfNecessary(whereRef, current)
|
|
297
|
+
_resolveAliasesInXpr(whereRef, current)
|
|
272
298
|
_resolveAliasInParams(params, current)
|
|
273
299
|
// in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
|
|
274
300
|
if (!Object.keys(params).length) params = where2obj(ref[i].where)
|
|
@@ -277,6 +303,11 @@ function _processSegments(from, model, namespace) {
|
|
|
277
303
|
}
|
|
278
304
|
} else if ({ action: 1, function: 1 }[current.kind]) {
|
|
279
305
|
// > action or function
|
|
306
|
+
if (current.kind === 'action' && ref && ref[ref.length - 1]?.where?.length === 0) {
|
|
307
|
+
const msg = `Round brackets (parentheses) are not allowed for action calls.`
|
|
308
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
if (i !== ref.length - 1) {
|
|
281
312
|
const msg = `${i ? 'Unbound' : 'Bound'} ${current.kind} are only supported as the last path segment.`
|
|
282
313
|
throw Object.assign(new Error(msg), { statusCode: 501 })
|
|
@@ -401,7 +432,8 @@ function _4service(service) {
|
|
|
401
432
|
const { ref } = from
|
|
402
433
|
|
|
403
434
|
// REVISIT: shouldn't be necessary
|
|
404
|
-
//Second findCsnTargetFor is required for concat query, where the root is already identified with the first query
|
|
435
|
+
// Second findCsnTargetFor is required for concat query, where the root is already identified with the first query
|
|
436
|
+
// and subsequent queries already have correct root
|
|
405
437
|
/*
|
|
406
438
|
* make first path segment fully qualified
|
|
407
439
|
*/
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -121,8 +121,11 @@ const _format = (cur, elementName, target, kind, isLambda) => {
|
|
|
121
121
|
if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind))
|
|
122
122
|
if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
|
|
123
123
|
// REVISIT: How to detect the types for all functions?
|
|
124
|
-
if (hasValidProps(cur, 'func'
|
|
125
|
-
|
|
124
|
+
if (hasValidProps(cur, 'func')) {
|
|
125
|
+
if (cur.args?.length) {
|
|
126
|
+
return kind === 'odata-v2' ? _odataV2Func(cur.func, cur.args) : `${cur.func}(${_args(cur.args)})`
|
|
127
|
+
}
|
|
128
|
+
return `${cur.func}()`
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -474,7 +477,7 @@ const parsers = {
|
|
|
474
477
|
count: (cqnPart, url, kind, target, isCount) => !isCount && $count(cqnPart, kind),
|
|
475
478
|
limit: (cqnPart, url, kind, target, isCount) => !isCount && $limit(cqnPart),
|
|
476
479
|
one: (cqnPart, url, kind, target, isCount) => !isCount && $one(cqnPart, url, kind),
|
|
477
|
-
ref: (cqnPart, url, kind, target,
|
|
480
|
+
ref: (cqnPart, url, kind, target, _isCount) => cqnPart[0].where && $where(cqnPart[0].where, target, kind)
|
|
478
481
|
}
|
|
479
482
|
|
|
480
483
|
function getOptions(cqnPart, url, kind, target, isCount) {
|
package/libx/odata/grammar.pegjs
CHANGED
|
@@ -238,6 +238,35 @@
|
|
|
238
238
|
}
|
|
239
239
|
return elements
|
|
240
240
|
}
|
|
241
|
+
|
|
242
|
+
const _replaceAliasedInWhere = (where, alias, value, isFromWhere = false) => {
|
|
243
|
+
where?.forEach(element => {
|
|
244
|
+
if (element.val === alias) {
|
|
245
|
+
// TODO check if we want to store replaced aliases/values for req.data in actions/functions in CQN
|
|
246
|
+
element.val = value.val
|
|
247
|
+
} else if (element.list === alias) {
|
|
248
|
+
element.list = value.list
|
|
249
|
+
} else if (element.func) {
|
|
250
|
+
element.args.forEach((arg, i) => {
|
|
251
|
+
if (arg.val === alias) {
|
|
252
|
+
arg.val = value.val
|
|
253
|
+
} else if (arg.func) {
|
|
254
|
+
_replaceAliasedInWhere(arg.args, alias, value, isFromWhere)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
} else if (element.SELECT) {
|
|
258
|
+
_replaceAliased(element.SELECT, alias, value, isFromWhere)
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const _replaceAliased = (select, alias, value, isFromWhere = false) => {
|
|
263
|
+
const {where, from} = select
|
|
264
|
+
_replaceAliasedInWhere(where, alias, value);
|
|
265
|
+
|
|
266
|
+
from?.ref?.forEach(element => {
|
|
267
|
+
_replaceAliasedInWhere(element.where, alias, value, true);
|
|
268
|
+
})
|
|
269
|
+
}
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
// ---------- Entity Paths ---------------
|
|
@@ -334,11 +363,13 @@
|
|
|
334
363
|
|
|
335
364
|
QueryOption = option:ExpandOption { if(option && option.apply) SELECT.apply = option.apply} /
|
|
336
365
|
"$skiptoken=" o skiptoken /
|
|
366
|
+
temporal /
|
|
337
367
|
format /
|
|
338
368
|
custom /
|
|
339
369
|
aliasedParamEqualsVal
|
|
340
370
|
// @OData spec for $expand:
|
|
341
|
-
// "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, $expand and
|
|
371
|
+
// "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, $expand and
|
|
372
|
+
// $apply (https://go.sap.corp/0jzs)."
|
|
342
373
|
ExpandOption =
|
|
343
374
|
"$select=" o select ( COMMA select )* /
|
|
344
375
|
"$expand=" o expand ( COMMA expand )* expandCount? /
|
|
@@ -350,6 +381,7 @@
|
|
|
350
381
|
"$count=" o count /
|
|
351
382
|
"$apply=" o trafos:transformations { return trafos }
|
|
352
383
|
|
|
384
|
+
temporal = ("$at" / "$from" / "$toInclusive" / "$to") "=" date
|
|
353
385
|
|
|
354
386
|
select
|
|
355
387
|
= col:('*' / ref) {
|
|
@@ -360,7 +392,7 @@
|
|
|
360
392
|
|
|
361
393
|
expandCount
|
|
362
394
|
= "/$count" {
|
|
363
|
-
const err = new Error("
|
|
395
|
+
const err = new Error('"/$count" is not supported for expand operation');
|
|
364
396
|
err.statusCode=501;
|
|
365
397
|
throw err;
|
|
366
398
|
}
|
|
@@ -370,7 +402,7 @@
|
|
|
370
402
|
expandOptions:(option:ExpandOption o ";"? { return option })*
|
|
371
403
|
{
|
|
372
404
|
if (expandOptions.find(option => option && option.apply !== undefined)) {
|
|
373
|
-
const err = new Error("
|
|
405
|
+
const err = new Error('"$apply" is not supported for expand operation');
|
|
374
406
|
err.statusCode=501;
|
|
375
407
|
throw err;
|
|
376
408
|
}
|
|
@@ -468,12 +500,16 @@
|
|
|
468
500
|
for (let i=0, k=0; i<any.length; ++i) {
|
|
469
501
|
let each = any[i]
|
|
470
502
|
if (each.ref && each.ref.length === 0 && any[i+1] === '=') {
|
|
471
|
-
xpr[k++] = { func:'contains', args:[{ref:id}, any[i+=2]] }
|
|
503
|
+
xpr[k++] = { func:'contains', args:[{ref:[id]}, any[i+=2]] }
|
|
472
504
|
} else {
|
|
473
505
|
xpr[k++] = each
|
|
474
506
|
}
|
|
475
507
|
}
|
|
476
508
|
if (xpr.length < any.length) {
|
|
509
|
+
if (!nav.length) {
|
|
510
|
+
// no navigation
|
|
511
|
+
return xpr
|
|
512
|
+
}
|
|
477
513
|
id = nav.pop()
|
|
478
514
|
return ['exists', { ref: [...nav, { id, where: xpr }] }]
|
|
479
515
|
} else {
|
|
@@ -493,7 +529,7 @@
|
|
|
493
529
|
/ comp:comparison { p.push(...comp) }
|
|
494
530
|
/ func:function { p.push(func) }
|
|
495
531
|
/ lambda:lambda { p.push(...lambda)}
|
|
496
|
-
/ list:listFilter {p.push(...list)}
|
|
532
|
+
/ list:listFilter { p.push(...list) }
|
|
497
533
|
)
|
|
498
534
|
( ao:(AND/OR) more:inner_lambda { p.push(ao, ...more) } )*
|
|
499
535
|
{ return p }
|
|
@@ -512,7 +548,7 @@
|
|
|
512
548
|
orderby
|
|
513
549
|
= ref:(
|
|
514
550
|
lambda {
|
|
515
|
-
const err = new Error("
|
|
551
|
+
const err = new Error('"$orderby" does not support lambda');
|
|
516
552
|
err.statusCode=501;
|
|
517
553
|
throw err;
|
|
518
554
|
} /
|
|
@@ -560,14 +596,18 @@
|
|
|
560
596
|
return {apply: mainTransformation}
|
|
561
597
|
}
|
|
562
598
|
|
|
599
|
+
aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerList "]" { return { list } }
|
|
600
|
+
|
|
563
601
|
custom
|
|
564
602
|
= [a-zA-Z0-9-_.~]+ "=" [^&]*
|
|
565
603
|
aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
|
|
566
|
-
aliasedParamEqualsVal
|
|
604
|
+
aliasedParamEqualsVal = alias:aliasedParam "=" !aliasedParam value:aliasedParamVal {
|
|
605
|
+
_replaceAliased(SELECT, alias, value);
|
|
606
|
+
}
|
|
567
607
|
|
|
568
608
|
format = "$format=" f:$([^&]*) {
|
|
569
609
|
if (f.toLowerCase() !== "json") {
|
|
570
|
-
const err = new Error("
|
|
610
|
+
const err = new Error('Only query parameter "json" is allowed in "$format".')
|
|
571
611
|
err.statusCode = 501
|
|
572
612
|
throw err;
|
|
573
613
|
}
|
|
@@ -581,10 +621,13 @@
|
|
|
581
621
|
return [ a, op, b ]
|
|
582
622
|
}
|
|
583
623
|
|
|
624
|
+
listFilterParam = aliased:aliasedParam { return { list: aliased } } / listRoundBrackets
|
|
625
|
+
|
|
584
626
|
listFilter
|
|
585
|
-
= a:operand _ "in" _ b:
|
|
627
|
+
= a:operand _ "in" _ b:listFilterParam {
|
|
586
628
|
return [ a, "in", b ]
|
|
587
629
|
}
|
|
630
|
+
|
|
588
631
|
mathCalc
|
|
589
632
|
= operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
|
|
590
633
|
|
|
@@ -621,36 +664,39 @@
|
|
|
621
664
|
|
|
622
665
|
null "null" = "null" {return {val: null }}
|
|
623
666
|
|
|
667
|
+
// REVISIT why not JSON.parse() and return JS object?
|
|
624
668
|
jsonObject "a json object"
|
|
625
669
|
= val:$("{" (jsonObject / [^}])* "}") {return {val}}
|
|
626
670
|
|
|
671
|
+
// REVISIT why not JSON.parse() and return JS array?
|
|
627
672
|
jsonArray "a json array"
|
|
628
673
|
= val:$("[" o "]" / "[" o "{" (jsonArray / [^\]])* "]") {return {val}}
|
|
629
674
|
|
|
675
|
+
// REVISIT: only used for contains(identifier, ["searchterm"]) <- use innerList instead?
|
|
630
676
|
list "a list"
|
|
631
677
|
= "[" any:$([^\]])* "]" // > needs improvment
|
|
632
678
|
{ return { list: any.replace(/"/g,'').split(',').map(ele => ({ val: ele })) } }
|
|
633
679
|
|
|
680
|
+
innerList = (val1:val val2:("," v:val { return v })* { return [val1, ...val2] })
|
|
681
|
+
|
|
634
682
|
listRoundBrackets "a list"
|
|
635
|
-
= OPEN list:
|
|
636
|
-
{
|
|
637
|
-
return { list }
|
|
638
|
-
}
|
|
683
|
+
= OPEN list:innerList CLOSE // > needs improvment
|
|
684
|
+
{ return {list} }
|
|
639
685
|
|
|
640
686
|
|
|
641
687
|
functionName "a function name"
|
|
642
688
|
= $[a-zA-Z]+
|
|
643
689
|
|
|
644
690
|
function
|
|
645
|
-
= func:functionName OPEN
|
|
691
|
+
= func:functionName OPEN args:functionArgs CLOSE {
|
|
646
692
|
if (strict && !(func.toLowerCase() in strict.functions)) {
|
|
647
693
|
throw Object.assign(new Error(`"${func}" is an unknown function in OData URL spec (strict mode)`), { statusCode: 400 })
|
|
648
694
|
}
|
|
649
|
-
return { func: func.toLowerCase(), args
|
|
695
|
+
return { func: func.toLowerCase(), args }
|
|
650
696
|
}
|
|
651
697
|
|
|
652
698
|
functionArgs
|
|
653
|
-
= a:operand more:( COMMA o:operand {return o} )* {return
|
|
699
|
+
= args:(a:operand more:( COMMA o:operand { return o } )* { return [ a, ...more ] })* { return args.length ? args[0] : args }
|
|
654
700
|
|
|
655
701
|
boolish
|
|
656
702
|
= func:("contains"i/"endswith"i/"startswith"i) OPEN a:operand COMMA b:operand CLOSE
|
|
@@ -758,13 +804,15 @@
|
|
|
758
804
|
return {concat: [trafo1, ...trafo2]}
|
|
759
805
|
}
|
|
760
806
|
|
|
761
|
-
|
|
807
|
+
// REVISIT: support compute - current implementation is deviating from odata
|
|
808
|
+
computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
|
|
762
809
|
|
|
763
810
|
computeExpr = where_clause asAlias
|
|
764
811
|
|
|
765
812
|
commonFuncTrafo = OPEN o first:operand o COMMA o second:operand o CLOSE { return [first, second] }
|
|
766
813
|
|
|
767
|
-
|
|
814
|
+
// REVISIT: support identity
|
|
815
|
+
identityTrafo = "identity" {return {identity: true }}
|
|
768
816
|
|
|
769
817
|
topTrafo
|
|
770
818
|
= OPEN o val:top o CLOSE {
|