@sap/cds 7.8.2 → 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 (136) hide show
  1. package/CHANGELOG.md +37 -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/entities.js +10 -0
  35. package/lib/linked/models.js +1 -1
  36. package/lib/plugins.js +1 -1
  37. package/lib/ql/INSERT.js +17 -3
  38. package/lib/ql/Query.js +4 -0
  39. package/lib/ql/infer.js +1 -1
  40. package/lib/req/request.js +1 -1
  41. package/lib/srv/cds-serve.js +1 -0
  42. package/lib/srv/middlewares/cds-context.js +1 -1
  43. package/lib/srv/protocols/odata-v4.js +5 -6
  44. package/lib/srv/srv-models.js +9 -2
  45. package/lib/utils/cds-test.js +2 -0
  46. package/lib/utils/cds-utils.js +9 -4
  47. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  55. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  65. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  66. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  67. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  68. package/libx/_runtime/common/generic/auth/index.js +2 -0
  69. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  70. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  71. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  72. package/libx/_runtime/common/generic/crud.js +5 -8
  73. package/libx/_runtime/common/generic/etag.js +8 -6
  74. package/libx/_runtime/common/generic/sorting.js +2 -2
  75. package/libx/_runtime/common/i18n/messages.properties +1 -0
  76. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  77. package/libx/_runtime/common/utils/compareJson.js +274 -0
  78. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  79. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  80. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  81. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  82. package/libx/_runtime/common/utils/resolveView.js +0 -16
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  85. package/libx/_runtime/common/utils/streamProp.js +9 -2
  86. package/libx/_runtime/common/utils/ucsn.js +1 -1
  87. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  88. package/libx/_runtime/db/generic/rewrite.js +7 -13
  89. package/libx/_runtime/fiori/generic/activate.js +1 -1
  90. package/libx/_runtime/fiori/generic/edit.js +1 -1
  91. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  92. package/libx/_runtime/fiori/lean-draft.js +151 -46
  93. package/libx/_runtime/fiori/utils/handler.js +1 -1
  94. package/libx/_runtime/hana/execute.js +6 -2
  95. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  96. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  97. package/libx/_runtime/messaging/event-broker.js +212 -0
  98. package/libx/_runtime/remote/Service.js +9 -32
  99. package/libx/_runtime/remote/utils/client.js +13 -21
  100. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  101. package/libx/_runtime/sqlite/execute.js +8 -3
  102. package/libx/_runtime/ucl/Service.js +259 -0
  103. package/libx/common/assert/index.js +5 -11
  104. package/libx/common/assert/validation.js +6 -1
  105. package/libx/odata/index.js +47 -25
  106. package/libx/odata/middleware/batch.js +8 -7
  107. package/libx/odata/middleware/create.js +42 -16
  108. package/libx/odata/middleware/delete.js +18 -11
  109. package/libx/odata/middleware/metadata.js +15 -14
  110. package/libx/odata/middleware/operation.js +30 -40
  111. package/libx/odata/middleware/parse.js +2 -3
  112. package/libx/odata/middleware/read.js +59 -52
  113. package/libx/odata/middleware/service-document.js +7 -7
  114. package/libx/odata/middleware/stream.js +26 -24
  115. package/libx/odata/middleware/update.js +53 -92
  116. package/libx/odata/parse/afterburner.js +45 -47
  117. package/libx/odata/parse/grammar.peggy +3 -3
  118. package/libx/odata/parse/multipartToJson.js +10 -22
  119. package/libx/odata/parse/parser.js +1 -1
  120. package/libx/odata/utils/etag.js +13 -0
  121. package/libx/odata/utils/handler.js +120 -0
  122. package/libx/odata/utils/index.js +15 -2
  123. package/libx/odata/utils/metaInfo.js +410 -0
  124. package/libx/odata/utils/path.js +5 -2
  125. package/libx/odata/utils/readAfterWrite.js +23 -0
  126. package/libx/odata/utils/result.js +4 -5
  127. package/libx/rest/RestAdapter.js +4 -13
  128. package/libx/rest/middleware/parse.js +40 -7
  129. package/package.json +1 -1
  130. package/server.js +1 -0
  131. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  132. package/libx/_runtime/common/utils/thenable.js +0 -51
  133. package/libx/_runtime/rest/service.js +0 -2
  134. package/libx/odata/parse/parseToCqn.js +0 -39
  135. package/libx/rest/middleware/input.js +0 -54
  136. package/libx/rest/middleware/payload.js +0 -13
@@ -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
 
@@ -141,15 +141,9 @@ function _process(obj, def, errs, opts) {
141
141
  // if used in protocol adapter, adjust val/ checker if necessary
142
142
  if (opts.http) {
143
143
  if (typeof v !== 'boolean') {
144
- if (NUMBER_TYPES.has(type)) v = Number(v)
145
- else if (type === 'cds.Double') v = parseFloat(v)
146
-
147
- // REVISIT: consider ieee754 and exp dec headers?
148
- // const ieee = opts.http.req?.headers['content-type'].match(/IEEE754Compatible=(\w+)/i)
149
- // const exp = opts.http.req?.headers['content-type'].match(/ExponentialDecimals=(\w+)/i)
150
- // if (type === 'cds.Decimal') {
151
- // TODO
152
- // }
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)
153
147
  }
154
148
  }
155
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(',') + '}'
@@ -2,13 +2,18 @@ const cds = require('../../../')
2
2
  const { INSERT } = cds.ql
3
3
 
4
4
  const { toODataResult, postProcess } = require('../utils/result')
5
- const { calculateLocationHeader, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
5
+ const {
6
+ calculateLocationHeader,
7
+ getKeysAndParamsFromPath,
8
+ handleSapMessages,
9
+ getPreferReturnHeader
10
+ } = require('../utils')
11
+ const { getDeepSelect, getSimpleSelectCQN } = require('../utils/handler')
6
12
 
7
13
  const { deepCopy } = require('../../_runtime/common/utils/copy')
8
14
 
9
- // REVISIT: move to or rewrite in libx/odata
10
- const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
11
- const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
15
+ const readAfterWrite = require('../utils/readAfterWrite')
16
+ const metaInfo = require('../utils/metaInfo')
12
17
 
13
18
  module.exports = srv =>
14
19
  function create(req, res, next) {
@@ -17,12 +22,12 @@ module.exports = srv =>
17
22
  target
18
23
  } = req._query
19
24
 
20
- if (one) {
21
- // REVISIT: don't use "SINGLETON" or "ENTITY" as that are okra terms
22
- throw Object.assign(
23
- new Error(`Method ${req.method} not allowed for ${target._isSingleton ? 'SINGLETON' : 'ENTITY'}`),
24
- { statusCode: 405 }
25
- )
25
+ // req.__proto__.method is set in case of upsert
26
+ const isUpsert = req.__proto__.method in { PUT: 1, PATCH: 1 }
27
+
28
+ if (one && !isUpsert) {
29
+ const msg = 'Method POST is not allowed for singletons and individual entities'
30
+ throw Object.assign(new Error(msg), { statusCode: 405 })
26
31
  }
27
32
 
28
33
  // payload & params
@@ -42,10 +47,17 @@ module.exports = srv =>
42
47
  // query
43
48
  const query = INSERT.into(from).entries(data)
44
49
 
50
+ // cdsReq.headers should contain merged headers of envelope and subreq
51
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
52
+
45
53
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
46
- const cdsReq = new cds.Request({ query, params, req, res })
54
+ const cdsReq = new cds.Request({ query, params, headers, req, res })
47
55
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
48
56
 
57
+ // API for subrequests of $batch (or incoming request)
58
+ cdsReq.req = req
59
+ cdsReq.res = res
60
+
49
61
  // rewrite event for draft-enabled entities
50
62
  if (target._isDraftEnabled) cdsReq.event = 'NEW'
51
63
 
@@ -57,8 +69,12 @@ module.exports = srv =>
57
69
  return srv.dispatch(cdsReq).then(result => {
58
70
  handleSapMessages(cdsReq, req, res)
59
71
 
72
+ // generic handlers indicate that read after write is required
60
73
  if (cdsReq._.readAfterWrite) {
61
- return readAfterWrite(cdsReq, srv, { operation: { result } })
74
+ // const keys = cdsReq.target.keys?.filter(k => !k.isAssociation)?.reduce((prev, k) => { prev[k] = result[k]; return prev}, {} )
75
+ // const query = SELECT.one(cdsReq.query.INSERT.into, keys)
76
+ const query = cdsReq.event === 'NEW' ? getSimpleSelectCQN(cdsReq.target, result) : getDeepSelect(cdsReq)
77
+ return readAfterWrite(cdsReq, srv, query)
62
78
  }
63
79
 
64
80
  return result
@@ -66,15 +82,25 @@ module.exports = srv =>
66
82
  })
67
83
  .then(result => {
68
84
  // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
85
+
86
+ // determine calculation based on result with req.data as fallback
87
+ if (!target._isSingleton)
88
+ res.set('location', calculateLocationHeader(cdsReq.target, srv, result || cdsReq.data))
89
+
69
90
  if (result == null) return res.sendStatus(204)
70
- const isMinimal = req._preferReturn === 'minimal'
91
+ const isMinimal = getPreferReturnHeader(req) === 'minimal'
71
92
  postProcess(cdsReq.target, srv, result, isMinimal)
93
+
72
94
  if (result['$etag']) res.set('etag', result['$etag'])
73
- res.set('location', calculateLocationHeader(cdsReq.target, srv, result))
74
95
  if (isMinimal) return res.sendStatus(204)
96
+
75
97
  const info = metaInfo(query, 'CREATE', srv, result, req)
76
98
  result = toODataResult(result, info)
77
- res.status(201).set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
99
+ res.set('content-type', 'application/json;IEEE754Compatible=true')
100
+ res.status(201).send(result)
101
+ })
102
+ .catch(err => {
103
+ handleSapMessages(cdsReq, req, res)
104
+ next(err)
78
105
  })
79
- .catch(next) // should be outside, so tx can be rolled back in case of errors
80
106
  }
@@ -1,14 +1,13 @@
1
1
  const cds = require('../../../')
2
2
  const { UPDATE, DELETE } = cds.ql
3
3
 
4
- const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
4
+ const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
5
5
 
6
6
  module.exports = srv =>
7
7
  function deleete(req, res, next) {
8
- if (req._preferReturn) {
9
- throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
10
- statusCode: 400
11
- })
8
+ if (getPreferReturnHeader(req)) {
9
+ const msg = "The 'return' preference is not allowed in DELETE requests"
10
+ throw Object.assign(new Error(msg), { statusCode: 400 })
12
11
  }
13
12
 
14
13
  // REVISIT: better solution for query._propertyAccess
@@ -19,8 +18,7 @@ module.exports = srv =>
19
18
  } = req._query
20
19
 
21
20
  if (!one) {
22
- // REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
23
- throw Object.assign(new Error('Method DELETE not allowed for ENTITY.COLLECTION'), { statusCode: 405 })
21
+ throw Object.assign(new Error('Method DELETE is not allowed for entity collections'), { statusCode: 405 })
24
22
  }
25
23
 
26
24
  // payload & params
@@ -31,20 +29,29 @@ module.exports = srv =>
31
29
  // query
32
30
  const query = _propertyAccess ? UPDATE(from).set({ [_propertyAccess]: null }) : DELETE.from(from)
33
31
 
32
+ // cdsReq.headers should contain merged headers of envelope and subreq
33
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
34
+
34
35
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
35
- const cdsReq = new cds.Request({ query, data, params, req, res })
36
+ const cdsReq = new cds.Request({ query, data, headers, params, req, res })
36
37
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
37
38
 
39
+ // API for subrequests of $batch (or incoming request)
40
+ cdsReq.req = req
41
+ cdsReq.res = res
42
+
38
43
  // rewrite event for draft-enabled entities
39
44
  if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
40
45
 
41
46
  return srv
42
47
  .dispatch(cdsReq)
43
- .then(result => {
48
+ .then(() => {
44
49
  handleSapMessages(cdsReq, req, res)
45
50
 
46
- if (result === 0) throw Object.assign(new Error('Not found'), { statusCode: 404 })
47
51
  res.sendStatus(204)
48
52
  })
49
- .catch(next)
53
+ .catch(err => {
54
+ handleSapMessages(cdsReq, req, res)
55
+ next(err)
56
+ })
50
57
  }