@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.
- package/CHANGELOG.md +44 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/classes.js +2 -2
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +6 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +2 -1
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- 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,
|
|
7
|
-
const
|
|
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
|
-
|
|
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)
|
|
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
|
|
315
|
-
const result = await super.dispatch(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
144
|
-
else if (type === 'cds.
|
|
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
|
-
|
|
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(', ')]
|
package/libx/odata/index.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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(',') + '}'
|