@sap/cds 6.1.3 → 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.
Files changed (206) hide show
  1. package/CHANGELOG.md +77 -8
  2. package/apis/cds.d.ts +18 -6
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/cqn.d.ts +1 -1
  5. package/apis/log.d.ts +23 -5
  6. package/apis/ql.d.ts +128 -61
  7. package/apis/services.d.ts +11 -0
  8. package/apis/test.d.ts +61 -0
  9. package/apis/utils.d.ts +15 -0
  10. package/app/fiori/preview.js +1 -0
  11. package/bin/build/buildTaskEngine.js +70 -22
  12. package/bin/build/buildTaskFactory.js +18 -11
  13. package/bin/build/buildTaskHandler.js +1 -1
  14. package/bin/build/buildTaskProviderFactory.js +3 -13
  15. package/bin/build/constants.js +0 -1
  16. package/bin/build/index.js +14 -6
  17. package/bin/build/provider/buildTaskHandlerEdmx.js +2 -3
  18. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -2
  19. package/bin/build/provider/buildTaskHandlerInternal.js +3 -6
  20. package/bin/build/provider/buildTaskProviderInternal.js +51 -39
  21. package/bin/build/provider/fiori/index.js +3 -3
  22. package/bin/build/provider/hana/2migration.js +1 -1
  23. package/bin/build/provider/hana/index.js +34 -27
  24. package/bin/build/provider/java/index.js +6 -7
  25. package/bin/build/provider/mtx/index.js +20 -18
  26. package/bin/build/provider/mtx/resourcesTarBuilder.js +8 -11
  27. package/bin/build/provider/mtx-sidecar/index.js +13 -17
  28. package/bin/build/provider/nodejs/index.js +8 -7
  29. package/bin/build/util.js +22 -4
  30. package/bin/cds.js +8 -4
  31. package/bin/deploy/to-hana/cfUtil.js +53 -18
  32. package/bin/mtx/in-cds.js +1 -0
  33. package/bin/serve.js +37 -30
  34. package/lib/auth/basic-auth.js +33 -0
  35. package/lib/auth/dummy-auth.js +7 -0
  36. package/lib/auth/ias-auth.js +2 -0
  37. package/lib/auth/index.js +31 -0
  38. package/lib/auth/jwt-auth.js +3 -0
  39. package/lib/auth/mocked-users.js +72 -0
  40. package/lib/auth/passport-basic.js +12 -0
  41. package/lib/auth/passport-digest.js +14 -0
  42. package/lib/auth/xsuaa-auth.js +3 -0
  43. package/lib/compile/cds-compile.js +3 -3
  44. package/lib/compile/to/cdl.js +5 -1
  45. package/lib/compile/to/edm.js +8 -0
  46. package/lib/compile/to/gql.js +1 -0
  47. package/lib/compile/to/json.js +30 -5
  48. package/lib/compile/to/sql.js +3 -1
  49. package/lib/core/index.js +5 -1
  50. package/lib/dbs/cds-deploy.js +36 -6
  51. package/lib/env/cds-env.js +15 -5
  52. package/lib/env/cds-requires.js +51 -58
  53. package/lib/env/defaults.js +1 -0
  54. package/lib/env/schemas/cds-package.json +4 -0
  55. package/lib/env/schemas/cds-rc.json +63 -77
  56. package/lib/i18n/localize.js +16 -5
  57. package/lib/index.js +9 -4
  58. package/lib/log/cds-error.js +4 -6
  59. package/lib/log/cds-log.js +89 -53
  60. package/lib/log/service/index.js +1 -0
  61. package/lib/ql/CREATE.js +2 -5
  62. package/lib/ql/DELETE.js +1 -1
  63. package/lib/ql/DROP.js +1 -3
  64. package/lib/ql/INSERT.js +3 -3
  65. package/lib/ql/Query.js +10 -23
  66. package/lib/ql/SELECT.js +1 -2
  67. package/lib/ql/UPDATE.js +2 -2
  68. package/lib/ql/Whereable.js +7 -15
  69. package/lib/ql/cds-ql.js +9 -3
  70. package/lib/req/cds-context.js +11 -3
  71. package/lib/req/context.js +29 -23
  72. package/lib/req/locale.js +9 -5
  73. package/lib/req/request.js +1 -0
  74. package/lib/req/user.js +2 -1
  75. package/lib/srv/cds-connect.js +1 -1
  76. package/lib/srv/cds-serve.js +21 -14
  77. package/lib/srv/middlewares/cds-context.js +29 -0
  78. package/lib/srv/middlewares/ctx-model.js +24 -0
  79. package/lib/srv/middlewares/errors.js +9 -0
  80. package/lib/srv/middlewares/index.js +22 -0
  81. package/lib/srv/middlewares/sap-statistics.js +13 -0
  82. package/lib/srv/middlewares/trace.js +102 -0
  83. package/lib/srv/protocols/_legacy.js +42 -0
  84. package/lib/srv/protocols/graphql.js +39 -0
  85. package/lib/srv/protocols/hcql.js +37 -0
  86. package/lib/srv/protocols/index.js +86 -0
  87. package/lib/srv/protocols/odata-v2-proxy.js +3767 -0
  88. package/lib/srv/protocols/odata-v2.js +26 -0
  89. package/lib/srv/protocols/odata-v4.js +16 -0
  90. package/lib/srv/protocols/rest.js +13 -0
  91. package/lib/srv/srv-api.js +5 -0
  92. package/lib/srv/srv-models.js +4 -6
  93. package/lib/utils/axios.js +3 -2
  94. package/lib/utils/cds-test.js +27 -21
  95. package/lib/utils/cds-utils.js +19 -20
  96. package/lib/utils/tar.js +175 -0
  97. package/libx/_runtime/audit/generic/personal/utils.js +18 -7
  98. package/libx/_runtime/audit/utils/v2.js +1 -0
  99. package/libx/_runtime/auth/index.js +4 -0
  100. package/libx/_runtime/auth/strategies/ias-auth.js +76 -0
  101. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +8 -3
  102. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +15 -4
  103. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  106. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +9 -0
  107. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriInfo.js +5 -1
  108. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +12 -0
  109. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +6 -2
  110. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/RequestValidator.js +47 -7
  111. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  112. package/libx/_runtime/cds-services/util/assert.js +4 -0
  113. package/libx/_runtime/common/aspects/relation.js +1 -1
  114. package/libx/_runtime/common/composition/data.js +61 -15
  115. package/libx/_runtime/common/composition/delete.js +0 -1
  116. package/libx/_runtime/common/composition/insert.js +0 -1
  117. package/libx/_runtime/common/composition/tree.js +4 -10
  118. package/libx/_runtime/common/composition/update.js +44 -21
  119. package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
  120. package/libx/_runtime/common/generic/crud.js +1 -2
  121. package/libx/_runtime/common/generic/etag.js +4 -4
  122. package/libx/_runtime/common/generic/input.js +4 -4
  123. package/libx/_runtime/common/generic/paging.js +3 -3
  124. package/libx/_runtime/common/generic/put.js +3 -3
  125. package/libx/_runtime/common/generic/sorting.js +4 -4
  126. package/libx/_runtime/common/generic/temporal.js +3 -3
  127. package/libx/_runtime/common/i18n/messages.properties +0 -7
  128. package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
  129. package/libx/_runtime/common/utils/csn.js +0 -28
  130. package/libx/_runtime/common/utils/draft.js +8 -1
  131. package/libx/_runtime/common/utils/path.js +7 -1
  132. package/libx/_runtime/common/utils/resolveView.js +2 -3
  133. package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
  134. package/libx/_runtime/db/generic/input.js +3 -3
  135. package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
  136. package/libx/_runtime/fiori/generic/activate.js +2 -2
  137. package/libx/_runtime/fiori/generic/before.js +40 -72
  138. package/libx/_runtime/fiori/generic/cancel.js +2 -2
  139. package/libx/_runtime/fiori/generic/delete.js +2 -2
  140. package/libx/_runtime/fiori/generic/edit.js +2 -2
  141. package/libx/_runtime/fiori/generic/new.js +2 -2
  142. package/libx/_runtime/fiori/generic/patch.js +49 -37
  143. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  144. package/libx/_runtime/fiori/generic/read.js +27 -37
  145. package/libx/_runtime/fiori/utils/where.js +4 -2
  146. package/libx/_runtime/hana/Service.js +1 -3
  147. package/libx/_runtime/hana/conversion.js +3 -0
  148. package/libx/_runtime/hana/driver.js +33 -3
  149. package/libx/_runtime/hana/dynatrace.js +1 -0
  150. package/libx/_runtime/hana/search2Contains.js +12 -1
  151. package/libx/_runtime/hana/search2cqn4sql.js +10 -27
  152. package/libx/_runtime/hana/streaming.js +1 -0
  153. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  154. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
  155. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
  156. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
  157. package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
  158. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  159. package/libx/_runtime/messaging/redis-messaging.js +1 -0
  160. package/libx/_runtime/remote/Service.js +2 -2
  161. package/libx/_runtime/remote/utils/client.js +8 -3
  162. package/libx/_runtime/remote/utils/data.js +7 -2
  163. package/libx/_runtime/sqlite/Service.js +18 -7
  164. package/libx/_runtime/sqlite/conversion.js +3 -0
  165. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
  166. package/libx/_runtime/sqlite/localized.js +8 -8
  167. package/libx/odata/afterburner.js +39 -7
  168. package/libx/odata/cqn2odata.js +6 -3
  169. package/libx/odata/grammar.pegjs +66 -18
  170. package/libx/odata/index.js +3 -2
  171. package/libx/odata/parser.js +1 -1
  172. package/libx/odata/utils.js +2 -0
  173. package/libx/rest/RestAdapter.js +62 -43
  174. package/libx/rest/middleware/parse.js +2 -1
  175. package/libx/rest/middleware/update.js +1 -1
  176. package/package.json +2 -2
  177. package/server.js +5 -4
  178. package/srv/mtx.cds +1 -1
  179. package/srv/mtx.js +4 -33
  180. package/lib/srv/adapters.js +0 -85
  181. package/lib/utils/resources/index.js +0 -48
  182. package/lib/utils/resources/tar.js +0 -49
  183. package/lib/utils/resources/utils.js +0 -11
  184. package/libx/_runtime/extensibility/activate.js +0 -69
  185. package/libx/_runtime/extensibility/add.js +0 -50
  186. package/libx/_runtime/extensibility/addExtension.js +0 -72
  187. package/libx/_runtime/extensibility/defaults.js +0 -34
  188. package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
  189. package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
  190. package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
  191. package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
  192. package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
  193. package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
  194. package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
  195. package/libx/_runtime/extensibility/linter.js +0 -32
  196. package/libx/_runtime/extensibility/push.js +0 -118
  197. package/libx/_runtime/extensibility/service.js +0 -38
  198. package/libx/_runtime/extensibility/token.js +0 -57
  199. package/libx/_runtime/extensibility/utils.js +0 -131
  200. package/libx/_runtime/extensibility/validation.js +0 -50
  201. package/libx/_runtime/extensibility/views.js +0 -12
  202. package/srv/extensibility-service.cds +0 -60
  203. package/srv/extensibility-service.js +0 -1
  204. package/srv/extensions.cds +0 -8
  205. package/srv/model-provider.cds +0 -61
  206. 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 = cds.mtx && !this.options.deployForProvider
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 (cds.mtx) {
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
 
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
1
2
  const redis = require('redis')
2
3
  const cds = require('../../../lib')
3
4
  const waitingTime = require('./common-utils/waitingTime')
@@ -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' || Buffer.isBuffer(reqOptions.data)) {
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 (type === 'cds.Decimal' || type === 'cds.DecimalFloat' || type === 'cds.Integer64') {
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.endsWith('.db')) {
106
- dbUrl = dbUrl.split('.db')[0] + '_' + tenant + '.db'
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
- const createEntities = cds.compile.to.sql(model, options)
157
- if (!createEntities || createEntities.length === 0) return // > nothing to deploy
157
+ let createEntities = cds.compile.to.sql(model, options)
158
+ if (createEntities.length === 0) return // > nothing to deploy
158
159
 
159
- const dropViews = []
160
- const dropTables = []
160
+ let dropViews = []
161
+ let dropTables = []
161
162
  for (const each of createEntities) {
162
- const [, table, entity] = each.match(/^\s*CREATE (?:(TABLE)|VIEW)\s+"?([^\s"(]+)"?/im) || []
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 _handler = function (req) {
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
- _handler._initial = true
52
+ sqliteConvertAssocToOneManaged._initial = true
53
53
 
54
- module.exports = _handler
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
- const localizedView =
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
- return localizedView ? localizedView.name : name
22
+ const view = model.definitions[`localized${locale in _translations ? '.' + locale : ''}.${name}`]
23
+ return view?.name || name
24
24
  }
25
25
 
26
- const _handler = function (req) {
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
- _handler._initial = true
47
+ sqliteLocalized._initial = true
48
48
 
49
- module.exports = _handler
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
- let params = ref[i].where && where2obj(ref[i].where)
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 (ref[i].where) {
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
- if (ref[i].where) {
270
- keyCount += addRefToWhereIfNecessary(ref[i].where, current)
271
- _resolveAliasesInXpr(ref[i].where, current)
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 and subsequent queries already have correct root
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
  */
@@ -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', 'args')) {
125
- return kind === 'odata-v2' ? _odataV2Func(cur.func, cur.args) : `${cur.func}(${_args(cur.args)})`
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, isCount) => cqnPart[0].where && $where(cqnPart[0].where, target, kind)
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) {
@@ -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 $apply (http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs02/odata-data-aggregation-ext-v4.0-cs02.html#_The_expand_Transformation)."
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("EXPAND_COUNT_UNSUPPORTED");
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("EXPAND_APPLY_UNSUPPORTED");
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("ORDERBY_LAMBDA_UNSUPPORTED");
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 "@alias=value" = a:aliasedParam "=" v:([^&]*) {return a + "=" + v}
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("ONLY_QUERY_PARAM_FORMAT_JSON_ALLOWED")
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:listRoundBrackets {
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:(val1:val val2:("," v:val { return v })* { return [val1, ...val2] }) CLOSE // > needs improvment
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 fnArgs:functionArgs CLOSE {
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:[fnArgs.a,...fnArgs.more] }
695
+ return { func: func.toLowerCase(), args }
650
696
  }
651
697
 
652
698
  functionArgs
653
- = a:operand more:( COMMA o:operand {return o} )* {return { a, more }}
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
- computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE //REVISIT: support compute - current implementation is deviating from odata
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
- identityTrafo = "identity" {return {identity: true }} //REVISIT: support identity
814
+ // REVISIT: support identity
815
+ identityTrafo = "identity" {return {identity: true }}
768
816
 
769
817
  topTrafo
770
818
  = OPEN o val:top o CLOSE {