@sap/cds 7.8.1 → 7.9.0

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 (137) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/_i18n/i18n_ar.properties +3 -0
  3. package/_i18n/i18n_cs.properties +3 -0
  4. package/_i18n/i18n_da.properties +3 -0
  5. package/_i18n/i18n_es_MX.properties +3 -0
  6. package/_i18n/i18n_fi.properties +3 -0
  7. package/_i18n/i18n_hu.properties +6 -0
  8. package/_i18n/i18n_ko.properties +3 -0
  9. package/_i18n/i18n_ms.properties +3 -0
  10. package/_i18n/i18n_nl.properties +3 -0
  11. package/_i18n/i18n_no.properties +3 -0
  12. package/_i18n/i18n_ro.properties +3 -0
  13. package/_i18n/i18n_sv.properties +3 -0
  14. package/_i18n/i18n_th.properties +3 -0
  15. package/_i18n/i18n_tr.properties +6 -0
  16. package/_i18n/i18n_zh_TW.properties +3 -0
  17. package/bin/serve.js +5 -5
  18. package/lib/auth/basic-auth.js +1 -1
  19. package/lib/compile/cdsc.js +33 -6
  20. package/lib/compile/etc/_localized.js +14 -7
  21. package/lib/compile/for/lean_drafts.js +9 -0
  22. package/lib/compile/to/edm-files.js +116 -0
  23. package/lib/compile/to/edm.js +8 -1
  24. package/lib/compile/to/hdbtabledata.js +3 -3
  25. package/lib/compile/to/sql.js +4 -2
  26. package/lib/compile/to/yaml.js +22 -21
  27. package/lib/dbs/cds-deploy.js +5 -6
  28. package/lib/env/cds-env.js +7 -0
  29. package/lib/env/cds-requires.js +20 -1
  30. package/lib/env/defaults.js +21 -5
  31. package/lib/env/schemas/cds-package.js +1 -1
  32. package/lib/env/schemas/cds-rc.js +85 -4
  33. package/lib/index.js +1 -1
  34. package/lib/linked/classes.js +2 -2
  35. package/lib/linked/entities.js +10 -0
  36. package/lib/linked/models.js +1 -1
  37. package/lib/plugins.js +1 -1
  38. package/lib/ql/INSERT.js +17 -3
  39. package/lib/ql/Query.js +4 -0
  40. package/lib/ql/infer.js +1 -1
  41. package/lib/req/request.js +1 -1
  42. package/lib/srv/cds-serve.js +1 -0
  43. package/lib/srv/middlewares/cds-context.js +1 -1
  44. package/lib/srv/protocols/odata-v4.js +5 -6
  45. package/lib/srv/srv-models.js +9 -2
  46. package/lib/utils/cds-test.js +2 -0
  47. package/lib/utils/cds-utils.js +9 -4
  48. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  65. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  66. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  67. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  68. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  69. package/libx/_runtime/common/generic/auth/index.js +2 -0
  70. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  71. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  72. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  73. package/libx/_runtime/common/generic/crud.js +5 -8
  74. package/libx/_runtime/common/generic/etag.js +8 -6
  75. package/libx/_runtime/common/generic/sorting.js +2 -2
  76. package/libx/_runtime/common/i18n/messages.properties +1 -0
  77. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  78. package/libx/_runtime/common/utils/compareJson.js +274 -0
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  80. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  81. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  82. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  83. package/libx/_runtime/common/utils/resolveView.js +0 -16
  84. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  85. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  86. package/libx/_runtime/common/utils/streamProp.js +9 -2
  87. package/libx/_runtime/common/utils/ucsn.js +1 -1
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  89. package/libx/_runtime/db/generic/rewrite.js +7 -13
  90. package/libx/_runtime/fiori/generic/activate.js +1 -1
  91. package/libx/_runtime/fiori/generic/edit.js +1 -1
  92. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  93. package/libx/_runtime/fiori/lean-draft.js +151 -46
  94. package/libx/_runtime/fiori/utils/handler.js +1 -1
  95. package/libx/_runtime/hana/execute.js +6 -2
  96. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  97. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  98. package/libx/_runtime/messaging/event-broker.js +212 -0
  99. package/libx/_runtime/remote/Service.js +9 -32
  100. package/libx/_runtime/remote/utils/client.js +13 -21
  101. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  102. package/libx/_runtime/sqlite/execute.js +8 -3
  103. package/libx/_runtime/ucl/Service.js +259 -0
  104. package/libx/common/assert/index.js +6 -11
  105. package/libx/common/assert/validation.js +6 -1
  106. package/libx/odata/index.js +47 -25
  107. package/libx/odata/middleware/batch.js +8 -7
  108. package/libx/odata/middleware/create.js +42 -16
  109. package/libx/odata/middleware/delete.js +18 -11
  110. package/libx/odata/middleware/metadata.js +15 -14
  111. package/libx/odata/middleware/operation.js +30 -40
  112. package/libx/odata/middleware/parse.js +2 -3
  113. package/libx/odata/middleware/read.js +59 -52
  114. package/libx/odata/middleware/service-document.js +7 -7
  115. package/libx/odata/middleware/stream.js +26 -24
  116. package/libx/odata/middleware/update.js +53 -92
  117. package/libx/odata/parse/afterburner.js +45 -47
  118. package/libx/odata/parse/grammar.peggy +3 -3
  119. package/libx/odata/parse/multipartToJson.js +10 -22
  120. package/libx/odata/parse/parser.js +1 -1
  121. package/libx/odata/utils/etag.js +13 -0
  122. package/libx/odata/utils/handler.js +120 -0
  123. package/libx/odata/utils/index.js +15 -2
  124. package/libx/odata/utils/metaInfo.js +410 -0
  125. package/libx/odata/utils/path.js +5 -2
  126. package/libx/odata/utils/readAfterWrite.js +23 -0
  127. package/libx/odata/utils/result.js +4 -5
  128. package/libx/rest/RestAdapter.js +4 -13
  129. package/libx/rest/middleware/parse.js +40 -7
  130. package/package.json +1 -1
  131. package/server.js +2 -1
  132. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  133. package/libx/_runtime/common/utils/thenable.js +0 -51
  134. package/libx/_runtime/rest/service.js +0 -2
  135. package/libx/odata/parse/parseToCqn.js +0 -39
  136. package/libx/rest/middleware/input.js +0 -54
  137. package/libx/rest/middleware/payload.js +0 -13
@@ -3,8 +3,8 @@ const cds = require('../cds')
3
3
  const { run, getReqOptions } = require('./utils/client')
4
4
  const { getCloudSdk, getCloudSdkConnectivity, getCloudSdkResilience } = require('./utils/cloudSdkProvider')
5
5
  const { hasAliasedColumns } = require('./utils/data')
6
- const { resolveView, getTransition, restoreLink, findQueryTarget } = require('../common/utils/resolveView')
7
- const { postProcess } = require('../common/utils/postProcessing')
6
+ const { resolveView, getTransition, findQueryTarget } = require('../common/utils/resolveView')
7
+ const postProcess = require('../common/utils/postProcess')
8
8
  const { formatVal } = require('../../odata/utils')
9
9
 
10
10
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
@@ -268,51 +268,28 @@ class RemoteService extends cds.Service {
268
268
  })
269
269
  }
270
270
 
271
- // FIXME: This is a dirty hack for this situation:
272
- // - This PR has cds.Service.model setter to always consistently apply cds.compile.for.odata, also for RemoteServices, which wasn't the case before
273
- // - because of that tests/_runtime/remote/__tests__/integration/odata.test.js fails, which relies on the former behavior of RemoteServices
274
- // NOTE: that test would never have worked for RemoteServices bootstrapped from single cds.model, which is always cds.compiled.for.odata
275
- // REVISIT: should become obsolete with Universal CSN
276
- // set model(m) {
277
- // const fn = cds.compile.for.odata
278
- // try {
279
- // cds.compile.for.odata = m => m
280
- // super.model = m
281
- // } finally {
282
- // cds.compile.for.odata = fn
283
- // }
284
- // }
285
-
286
271
  // Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
287
272
  // Result is post processed according to the inverse projection in order to reflect the correct result of the original query.
288
273
  async handle(req) {
289
- // compat mode
290
- if (req._resolved || cds.env.features.resolve_views === false) return super.handle(req)
291
-
292
- if (req.target && req.target.name && this.definition && req.target.name.startsWith(this.definition.name + '.')) {
293
- const result = await super.handle(req)
274
+ if (req._resolved) return super.handle(req)
294
275
 
276
+ if (req.target?.name?.startsWith(this.definition?.name + '.')) {
277
+ let result = await super.handle(req)
295
278
  // only post process if alias was explicitly set in query
296
- if (_selectOnlyWithAlias(req.query)) {
297
- return postProcess(req.query, result, this, true)
298
- }
299
-
279
+ if (_selectOnlyWithAlias(req.query)) result = postProcess(req.query, result, this, true)
300
280
  return result
301
281
  }
302
282
 
303
283
  // req.query can be:
304
284
  // - empty object in case of unbound action/function
305
285
  // - undefined/null in case of plain string queries
306
- if (_isSimpleCqnQuery(req.query) && this.model) {
286
+ if (this.model && _isSimpleCqnQuery(req.query)) {
307
287
  const q = resolveView(req.query, this.model, this)
308
288
  const t = findQueryTarget(q) || req.target
309
289
 
310
- // compat
311
- restoreLink(req)
312
-
313
290
  // REVISIT: We need to provide target explicitly because it's cached already within ensure_target
314
- const newReq = new cds.Request({ query: q, target: t, headers: req.headers, _resolved: true, method: req.method })
315
- const result = await super.dispatch(newReq)
291
+ const _req = new cds.Request({ query: q, target: t, _resolved: true, headers: req.headers, method: req.method })
292
+ const result = await super.dispatch(_req)
316
293
  return postProcess(q, result, this, true)
317
294
  }
318
295
 
@@ -166,27 +166,17 @@ const TYPES_TO_REMOVE = { function: 1, object: 1 }
166
166
  const PROPS_TO_IGNORE = { cause: 1, name: 1 }
167
167
 
168
168
  const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBody: false, batchRequest: false }) => {
169
- e.request = {
169
+ const request = {
170
170
  method: reqOptions.method,
171
171
  url: e.config ? e.config.baseURL + e.config.url : reqOptions.url,
172
172
  headers: e.config ? e.config.headers : reqOptions.headers
173
173
  }
174
-
175
- if (options.batchRequest) {
176
- e.request.body = reqOptions.data
177
- }
174
+ if (options.batchRequest) request.body = reqOptions.data
175
+ e.request = request
178
176
 
179
177
  if (e.response) {
180
- const response = {
181
- status: e.response.status,
182
- statusText: e.response.statusText,
183
- headers: e.response.headers
184
- }
185
-
186
- if (e.response.data && !options.suppressRemoteResponseBody) {
187
- response.body = e.response.data
188
- }
189
-
178
+ const response = { status: e.response.status, statusText: e.response.statusText, headers: e.response.headers }
179
+ if (e.response.data && !options.suppressRemoteResponseBody) response.body = e.response.data
190
180
  e.response = response
191
181
  }
192
182
 
@@ -214,6 +204,11 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
214
204
  e = _e
215
205
  }
216
206
 
207
+ // AxiosError's toJSON() method doesn't include the request and response objects
208
+ e.toJSON = function () {
209
+ return { ...this.__proto__.toJSON(), request: this.request, response: this.response }
210
+ }
211
+
217
212
  return e
218
213
  }
219
214
 
@@ -232,7 +227,7 @@ const run = async (requestConfig, options) => {
232
227
  } catch (e) {
233
228
  // > axios received status >= 400 -> gateway error
234
229
  const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
235
- e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
230
+ e.message = msg ? 'Error during request to remote service: ' + msg : 'Request to remote service failed.'
236
231
  const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
237
232
  const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
238
233
  LOG._warn && LOG.warn(err)
@@ -251,11 +246,8 @@ const run = async (requestConfig, options) => {
251
246
  const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
252
247
  e.response = response
253
248
  const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
254
- const err = Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
255
- statusCode: 502,
256
- reason: sanitizedError
257
- })
258
-
249
+ const message = 'Error during request to remote service: ' + e.message
250
+ const err = Object.assign(new Error(message), { statusCode: 502, reason: sanitizedError })
259
251
  LOG._warn && LOG.warn(err)
260
252
  throw err
261
253
  }
@@ -28,8 +28,14 @@ const _convert = (refEntries, req) => {
28
28
  // only check refs in format {ref: ['assoc', 'id']}
29
29
  continue
30
30
  }
31
+ let element
32
+ if (req.target.elements[refEntry.ref[0]]) {
33
+ element = req.target.elements[refEntry.ref[0]]
34
+ } else if (req.target.elements[refEntry.ref[1]]?.isAssociation) {
35
+ // fallback: if first ref is not an element and second is an association, we assume first is an alias
36
+ element = req.target.elements[refEntry.ref[1]]
37
+ }
31
38
 
32
- const element = req.target.elements[refEntry.ref[0]]
33
39
  if (!element || !element.is2one) return
34
40
 
35
41
  _convertRefForAssocToOneManaged(element, refEntry)
@@ -302,7 +302,7 @@ function executeGenericCQN(model, dbc, cqn, user, locale, txTimestamp) {
302
302
 
303
303
  // REVISIT: consider deleting this function after removing stream_compat
304
304
  async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimestamp }) {
305
- const result = await executeSelectCQN(model, dbc, query, user, locale, txTimestamp)
305
+ let result = await executeSelectCQN(model, dbc, query, user, locale, txTimestamp)
306
306
 
307
307
  if (result == null || result.length === 0) {
308
308
  return
@@ -313,7 +313,9 @@ async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimes
313
313
  return result
314
314
  }
315
315
 
316
- let val = Array.isArray(result) ? Object.values(result[0])[0] : Object.values(result)[0]
316
+ // REVISIT: following code to be deleted after cds.env.features.stream_compat is removed
317
+ if (Array.isArray(result)) result = result[0]
318
+ let [key, val] = Object.entries(result)[0]
317
319
  if (val === null) {
318
320
  return null
319
321
  }
@@ -325,7 +327,10 @@ async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimes
325
327
  stream_.push(val)
326
328
  stream_.push(null)
327
329
 
328
- return { value: stream_ }
330
+ result.value = stream_
331
+ delete result[key]
332
+
333
+ return result
329
334
  }
330
335
 
331
336
  module.exports = {
@@ -0,0 +1,259 @@
1
+ const cds = require('../cds')
2
+ const LOG = cds.log('ucl')
3
+
4
+ const https = require('https')
5
+
6
+ class UCLService extends cds.Service {
7
+ async init() {
8
+ await super.init()
9
+ this.validate()
10
+ this._register()
11
+ this.agent = this.getAgent()
12
+ }
13
+
14
+ getAgent() {
15
+ try {
16
+ if (this.options.x509.certPath && this.options.x509.pkeyPath) {
17
+ return new https.Agent({
18
+ cert: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.certPath)),
19
+ key: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.pkeyPath))
20
+ })
21
+ }
22
+ } catch (error) {
23
+ if (LOG) LOG.error('GetCredentials', { error: error.message })
24
+ throw error
25
+ }
26
+ }
27
+
28
+ async _registerProvisioningEvents() {
29
+ var provisioning
30
+ try {
31
+ provisioning = await cds.connect.to('cds.xt.SaasProvisioningService')
32
+ } catch (error) {
33
+ throw new Error(
34
+ "Provisioning service 'cds.xt.SaasProvisioningService' can not be found, therefore mode is not multitenant. Single tenant applications are not supported."
35
+ )
36
+ }
37
+ if (provisioning) {
38
+ provisioning.prepend(() => {
39
+ provisioning.on('dependencies', async (_, next) => {
40
+ let dependencies = await next()
41
+ const xsappnameCMPClone = await this._getUCLDependency()
42
+ dependencies.push({ xsappname: xsappnameCMPClone })
43
+ return dependencies
44
+ })
45
+ })
46
+ }
47
+ }
48
+
49
+ validate() {
50
+ if (!this.options.namespace) {
51
+ throw new Error(
52
+ 'UCL integrator requires an application namespace. You can set environment variable SAP_APPLICATION_NAMESPACE or you can give namespace as an option in your cds.requires section as described in documentation'
53
+ )
54
+ }
55
+ if (!cds.requires.multitenancy && cds.env.profile !== 'mtx-sidecar') {
56
+ throw new Error('[ucl] - Currently only multitenant applications are supported.')
57
+ }
58
+ if (!this.options.systemType || !this.options.systemDescription) {
59
+ throw new Error(
60
+ 'systemType and systemDescription is obligatory parameters, please fill as shown in documentation'
61
+ )
62
+ }
63
+ }
64
+
65
+ async readTemplate() {
66
+ const xsappname = this.options.credentials.xsappname
67
+ const query = `
68
+ query ($key: String!, $value: String!) {
69
+ applicationTemplates(filter: { key: $key, query: $value }) {
70
+ data {
71
+ id
72
+ name
73
+ description
74
+ placeholders {
75
+ name
76
+ description
77
+ }
78
+ applicationInput
79
+ labels
80
+ webhooks {
81
+ type
82
+ }
83
+ }
84
+ }
85
+ }
86
+ `
87
+ const variables = { key: 'xsappname', value: `"${xsappname}"` }
88
+ return (await this.request(query, variables)).applicationTemplates.data[0]
89
+ }
90
+
91
+ async _createTemplate() {
92
+ const xsappname = this.options.credentials.xsappname
93
+ const query = `mutation {
94
+ result: createApplicationTemplate (
95
+ in: {
96
+ name: "${this.options.systemType}"
97
+ description: "${this.options.systemDescription}"
98
+ applicationInput: {
99
+ name: "${this.options.systemType}"
100
+ description: "${this.options.systemDescription}"
101
+ providerName: "${this.options.provider}"
102
+ localTenantID: "{{tenant-id}}"
103
+ labels: {
104
+ displayName: "{{subdomain}}"
105
+ }
106
+ }
107
+ placeholders: [
108
+ { name: "subdomain", description: "The subdomain of the consumer tenant" }
109
+ { name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedSubaccountId" }
110
+ ]
111
+ labels: {
112
+ managed_app_provisioning: true
113
+ xsappname: "${xsappname}"
114
+ }
115
+ applicationNamespace: "${this.options.namespace}"
116
+ accessLevel: GLOBAL
117
+ }
118
+ ) {
119
+ id
120
+ name
121
+ labels
122
+ applicationInput
123
+ applicationNamespace
124
+ }
125
+ }`
126
+ try {
127
+ return this.handleResponse(await this.request(query))
128
+ } catch (e) {
129
+ this.handleResponse(e)
130
+ }
131
+ }
132
+
133
+ handleResponse(result) {
134
+ if (result.response && result.response.errors) {
135
+ let errorMessage = result.response.errors[0].message
136
+ throw new Error(errorMessage)
137
+ } else {
138
+ return result.result
139
+ }
140
+ }
141
+
142
+ async deleteTemplate() {
143
+ const template = await this.readTemplate()
144
+ if (!template) return
145
+ const query = `mutation {
146
+ result: deleteApplicationTemplate(
147
+ id: "${template.id}"
148
+ ){
149
+ id
150
+ name
151
+ description
152
+ }
153
+ }`
154
+ return this.handleResponse(await this.request(query))
155
+ }
156
+
157
+ async _getUCLDependency() {
158
+ if (!this.template) {
159
+ throw Error('Application template not found on UCL!')
160
+ }
161
+ return this.template.labels.xsappnameCMPClone
162
+ }
163
+
164
+ // Replace with fetch
165
+ async request(query, variables) {
166
+ const opts = {
167
+ host: this.options.host,
168
+ path: this.options.path,
169
+ agent: this.agent,
170
+ method: 'POST',
171
+ headers: {
172
+ 'Content-Type': 'application/json'
173
+ }
174
+ }
175
+ return new Promise((resolve, reject) => {
176
+ const req = https.request(opts, res => {
177
+ const chunks = []
178
+
179
+ res.on('data', chunk => {
180
+ chunks.push(chunk)
181
+ })
182
+
183
+ res.on('end', () => {
184
+ const response = {
185
+ statusCode: res.statusCode,
186
+ headers: res.headers,
187
+ body: Buffer.concat(chunks).toString()
188
+ }
189
+ resolve(JSON.parse(response.body).data)
190
+ })
191
+ })
192
+
193
+ req.on('error', error => {
194
+ reject(error)
195
+ })
196
+
197
+ if (query) {
198
+ req.write(JSON.stringify({ query, variables }))
199
+ }
200
+ req.end()
201
+ })
202
+ }
203
+
204
+ async _registerApplicationTemplate() {
205
+ this.template = await this.readTemplate()
206
+ if (!this.template) {
207
+ LOG.info('Application Template cannot be found therefore created.')
208
+ await this._createTemplate()
209
+ } else {
210
+ await this._updateTemplate(this.template)
211
+ }
212
+ }
213
+
214
+ async _updateTemplate(template) {
215
+ const query = `mutation {
216
+ result: updateApplicationTemplate(
217
+ id: "${template.id}"
218
+ in: {
219
+ name: "${this.options.systemType}"
220
+ description: "${this.options.systemDescription}"
221
+ applicationInput: {
222
+ name: "${this.options.systemType}"
223
+ description: "${this.options.systemDescription}"
224
+ providerName: "${this.options.provider}"
225
+ localTenantID: "{{tenant-id}}"
226
+ labels: { displayName: "{{subdomain}}" }
227
+ }
228
+ applicationNamespace: "${this.options.namespace}"
229
+ placeholders: [
230
+ { name: "subdomain", description: "The subdomain of the consumer tenant" }
231
+ { name: "tenant-id", description: "The tenant id as it's known in the product's domain", jsonPath: "$.subscribedSubaccountId" }
232
+ ]
233
+ accessLevel: GLOBAL
234
+ }
235
+ ) {
236
+ id
237
+ name
238
+ description
239
+ applicationInput
240
+ }
241
+ }`
242
+ try {
243
+ const response = this.handleResponse(await this.request(query))
244
+ LOG.info('Application template updated successfully.')
245
+ return response
246
+ } catch (e) {
247
+ this.handleResponse(e)
248
+ }
249
+ }
250
+
251
+ _register() {
252
+ cds.once('listening', async () => {
253
+ await this._registerApplicationTemplate()
254
+ this._registerProvisioningEvents()
255
+ })
256
+ }
257
+ }
258
+
259
+ module.exports = UCLService
@@ -2,14 +2,14 @@ const { cds } = global
2
2
 
3
3
  const typeCheckers = require('./type')
4
4
  const { checkMandatory, checkEnum, checkRange, checkFormat } = require('./validation')
5
- const { getNested, getTarget, resolveCDSType, resolveSegment } = require('./utils')
5
+ const { getNested, getNormalizedDecimal, getTarget, resolveCDSType, resolveSegment } = require('./utils')
6
6
 
7
7
  const NUMBER_TYPES = new Set(['cds.UInt8', 'cds.Int16', 'cds.Int32', 'cds.Integer', 'cds.Double'])
8
8
 
9
9
  const _no_op = () => {}
10
10
 
11
11
  const _reject_unknown = (_, k, def, errs) =>
12
- errs.push(new cds.error(`Property ${k} does not exist in ${def.name}`, { statusCode: 400, code: '400' }))
12
+ errs.push(new cds.error(`Property "${k}" does not exist in ${def.name}`, { statusCode: 400, code: '400' }))
13
13
 
14
14
  const _filter_unknown = (obj, k) => delete obj[k]
15
15
 
@@ -57,6 +57,7 @@ function _process(obj, def, errs, opts) {
57
57
 
58
58
  for (let [k, v] of Object.entries(obj)) {
59
59
  let ele = def.elements?.[k] || def.params?.[k] || def.items
60
+ if (typeof ele !== 'object') ele = undefined //> ignore non-object elements, e.g., functions of prototypes
60
61
 
61
62
  /*
62
63
  * TODO: should we support this? with or without transformation?
@@ -140,15 +141,9 @@ function _process(obj, def, errs, opts) {
140
141
  // if used in protocol adapter, adjust val/ checker if necessary
141
142
  if (opts.http) {
142
143
  if (typeof v !== 'boolean') {
143
- if (NUMBER_TYPES.has(type)) v = Number(v)
144
- else if (type === 'cds.Double') v = parseFloat(v)
145
-
146
- // REVISIT: consider ieee754 and exp dec headers?
147
- // const ieee = opts.http.req?.headers['content-type'].match(/IEEE754Compatible=(\w+)/i)
148
- // const exp = opts.http.req?.headers['content-type'].match(/ExponentialDecimals=(\w+)/i)
149
- // if (type === 'cds.Decimal') {
150
- // TODO
151
- // }
144
+ if (type === 'cds.Decimal') v = getNormalizedDecimal(v)
145
+ else if (type === 'cds.Int64') v = String(v)
146
+ else if (NUMBER_TYPES.has(type)) v = Number(v)
152
147
  }
153
148
  }
154
149
 
@@ -73,7 +73,12 @@ const checkMandatory = (v, ele, errs, path, k) => {
73
73
  const checkEnum = (v, ele, errs, path, k) => {
74
74
  const enumElements = _getEnumElement(ele)
75
75
  const enumValues = enumElements && _enumValues(enumElements)
76
- if (enumElements && !enumValues.includes(v)) {
76
+ const includes = (enumValues, v) => {
77
+ if (ele._type in { 'cds.Decimal': 1, 'cds.Int64': 1 }) {
78
+ return enumValues.map(ev => String(ev)).includes(String(v))
79
+ } else return enumValues.includes(v)
80
+ }
81
+ if (enumElements && !includes(enumValues, v)) {
77
82
  const args =
78
83
  typeof v === 'string'
79
84
  ? ['"' + v + '"', enumValues.map(ele => '"' + ele + '"').join(', ')]
@@ -9,28 +9,59 @@ const afterburner = require('./parse/afterburner')
9
9
  const { getSafeNumber: safeNumber } = require('./utils')
10
10
  const getError = require('../_runtime/common/error')
11
11
 
12
+ // used for function validation in peggy parser
13
+ // ----- should all be lowercase, as peggy compares to lowercase -----
14
+ // (plus: odata is case insensitive)
12
15
  const strict = {
13
16
  functions: {
17
+ // --- String + Collection: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360980
18
+ concat: 1,
14
19
  contains: 1,
15
- startswith: 1,
16
20
  endswith: 1,
17
- tolower: 1,
18
- toupper: 1,
19
- length: 1,
20
21
  indexof: 1,
22
+ length: 1,
23
+ startswith: 1,
21
24
  substring: 1,
25
+ // --- Collection: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360988
26
+ // REVISIT: not supported
27
+ // hassubset:1,
28
+ // hassubsequence:1,
29
+ // --- String: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360991
30
+ matchespattern: 1,
31
+ tolower: 1,
32
+ toupper: 1,
22
33
  trim: 1,
23
- concat: 1,
24
- year: 1,
25
- month: 1,
34
+ // --- Date + Time: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360996
35
+ date: 1,
26
36
  day: 1,
37
+ fractionalseconds: 1,
27
38
  hour: 1,
39
+ maxdatetime: 1,
40
+ mindatetime: 1,
28
41
  minute: 1,
42
+ month: 1,
43
+ now: 1,
29
44
  second: 1,
30
45
  time: 1,
31
- now: 1,
46
+ totaloffsetminutes: 1,
47
+ totalseconds: 1,
48
+ year: 1,
49
+ // --- Arithemetic: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361011
50
+ ceiling: 1,
51
+ floor: 1,
32
52
  round: 1,
33
- date: 1
53
+ // --- Type: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361015
54
+ // REVISIT: not supported
55
+ // cast: 1,
56
+ // REVISIT: has to be implemented inside the odata adapter
57
+ // isof: 1,
58
+ // --- Geo: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361018
59
+ // REVISIT: not supported
60
+ // 'geo.distance': 1,
61
+ // 'geo.intersects': 1,
62
+ // 'geo.length': 1,
63
+ // --- Conditional: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31361022
64
+ case: 1
34
65
  }
35
66
  }
36
67
 
@@ -67,10 +98,8 @@ module.exports = {
67
98
  parse: (url, options = {}) => {
68
99
  // first arg may also be req
69
100
  if (url.url) url = url.url
70
- // REVISIT: for okra, remove when no longer needed
71
- else if (url.getIncomingRequest) url = url.getIncomingRequest().url
72
101
 
73
- url = decodeURIComponent(url) // REVISIT: do we need that?
102
+ url = decodeURIComponent(url)
74
103
 
75
104
  options = options === 'strict' ? { strict } : options.strict ? { ...options, strict } : options
76
105
  if (options.service) Object.assign(options, { minimal: true, afterburner: afterburner.for(options.service) })
@@ -81,18 +110,13 @@ module.exports = {
81
110
  try {
82
111
  cqn = odata2cqn(url, options)
83
112
  } catch (err) {
84
- if (err.statusCode === 501) {
85
- throw getError(err.statusCode, err.message)
86
- }
113
+ if (err.statusCode === 501) throw getError(err.statusCode, err.message)
87
114
 
88
115
  let offset = err.location && err.location.start.offset
89
- if (!offset && err.statusCode && err.message) {
90
- throw err
91
- }
92
- if (options.baseUrl) {
93
- // we need to add the number of chars from base url to the offset
94
- offset += options.baseUrl.length
95
- }
116
+ if (!offset && err.statusCode && err.message) throw err
117
+
118
+ // we need to add the number of chars from base url to the offset
119
+ offset += options.baseUrl ? options.baseUrl.length : 0
96
120
 
97
121
  // TODO adjust this to behave like above
98
122
  err.message = `Parsing URL failed at position ${offset}: ${err.message}`
@@ -102,9 +126,7 @@ module.exports = {
102
126
 
103
127
  // cqn is an array, if concat is used
104
128
  if (Array.isArray(cqn)) {
105
- for (let i = 0; i < cqn.length; i++) {
106
- cqn[i] = enhanceCqn(cqn[i], options)
107
- }
129
+ for (let i = 0; i < cqn.length; i++) cqn[i] = enhanceCqn(cqn[i], options)
108
130
  } else {
109
131
  cqn = enhanceCqn(cqn, options)
110
132
  }
@@ -4,6 +4,8 @@ const { AsyncResource } = require('async_hooks')
4
4
  // eslint-disable-next-line cds/no-missing-dependencies
5
5
  const express = require('express')
6
6
  const { STATUS_CODES } = require('http')
7
+ const qs = require('querystring')
8
+ const { URL } = require('url')
7
9
 
8
10
  const multipartToJson = require('../parse/multipartToJson')
9
11
 
@@ -35,6 +37,9 @@ const _validateBatch = body => {
35
37
 
36
38
  _validateProperty('requests', requests, 'Array')
37
39
 
40
+ if (requests.length > cds.env.odata.batch_limit)
41
+ cds.error('BATCH_TOO_MANY_REQ', { code: 'BATCH_TOO_MANY_REQ', statusCode: 429 })
42
+
38
43
  const ids = {}
39
44
 
40
45
  let previousAtomicityGroup
@@ -115,15 +120,11 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
115
120
 
116
121
  req.method = method.toUpperCase()
117
122
  req.url = url
118
- req.query = {}
123
+ const u = new URL(url, 'http://cap')
124
+ req.query = qs.parse(u.search.slice(1))
119
125
  req.headers = request.headers || {}
120
126
  req.body = request.body
121
127
 
122
- // propagate user, tenant and locale
123
- req.user = _req.user
124
- req.tenant = _req.tenant
125
- req.locale = _req.locale
126
-
127
128
  const res = (ret.res = new express.response.constructor(req))
128
129
  res.__proto__ = express.response
129
130
 
@@ -270,7 +271,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
270
271
  let meta = [],
271
272
  data = []
272
273
  for (const [k, v] of Object.entries(_json)) {
273
- if (k.startsWith('@')) meta.push(`"${k}":"${v}"`)
274
+ if (k.startsWith('@')) meta.push(`"${k}":"${v.replaceAll('"', '\\"')}"`)
274
275
  else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
275
276
  }
276
277
  const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'