@sap/cds 6.5.0 → 6.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -2
- package/README.md +5 -0
- package/apis/services.d.ts +5 -0
- package/bin/build/buildTaskEngine.js +0 -2
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +10 -6
- package/bin/build/provider/fiori/index.js +5 -10
- package/bin/build/provider/hana/2migration.js +11 -2
- package/bin/build/provider/hana/index.js +17 -14
- package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
- package/bin/build/provider/mtx-extension/index.js +18 -1
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +1 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/deploy/to-hana/hdiDeployUtil.js +24 -12
- package/bin/serve.js +32 -20
- package/lib/auth/jwt-auth.js +4 -4
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/dbs/cds-deploy.js +6 -8
- package/lib/env/schemas/cds-rc.json +4 -0
- package/lib/index.js +4 -2
- package/lib/req/cds-context.js +3 -3
- package/lib/srv/bindings.js +1 -2
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/trace.js +31 -15
- package/lib/srv/protocols/odata-v2-proxy.js +8 -8
- package/lib/srv/srv-handlers.js +26 -7
- package/lib/srv/srv-methods.js +2 -2
- package/lib/srv/srv-models.js +4 -3
- package/lib/utils/cds-test.js +7 -5
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -0
- 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/readToCQN.js +11 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +7 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
- package/libx/_runtime/cds-services/services/Service.js +8 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +7 -4
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +3 -1
- package/libx/_runtime/common/code-ext/execute.js +9 -2
- package/libx/_runtime/common/code-ext/handlers.js +2 -2
- package/libx/_runtime/common/code-ext/worker.js +9 -5
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +5 -2
- package/libx/_runtime/common/composition/data.js +5 -2
- package/libx/_runtime/common/composition/tree.js +2 -0
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/etag.js +22 -10
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -11
- package/libx/_runtime/common/utils/path.js +0 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +4 -1
- package/libx/_runtime/common/utils/structured.js +1 -0
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
- package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
- package/libx/_runtime/db/generic/input.js +2 -2
- package/libx/_runtime/db/generic/integrity.js +1 -0
- package/libx/_runtime/db/generic/virtual.js +1 -0
- package/libx/_runtime/db/query/read.js +3 -2
- package/libx/_runtime/fiori/generic/activate.js +3 -1
- package/libx/_runtime/fiori/generic/before.js +1 -0
- package/libx/_runtime/fiori/generic/edit.js +6 -1
- package/libx/_runtime/fiori/generic/new.js +2 -0
- package/libx/_runtime/fiori/generic/patch.js +2 -0
- package/libx/_runtime/fiori/generic/prepare.js +2 -0
- package/libx/_runtime/fiori/generic/read.js +8 -2
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +498 -245
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- package/libx/_runtime/messaging/Outbox.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
- package/libx/_runtime/messaging/file-based.js +1 -2
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +0 -1
- package/libx/_runtime/remote/Service.js +1 -0
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
- package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
- package/libx/odata/afterburner.js +17 -5
- package/libx/odata/grammar.pegjs +3 -4
- package/libx/odata/index.js +5 -1
- package/libx/odata/parseToCqn.js +3 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +58 -1
- package/package.json +1 -1
- package/server.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
- package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
- package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
- /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
|
@@ -253,8 +253,12 @@ class OData {
|
|
|
253
253
|
this._odataService.process(req, res).catch(err => {
|
|
254
254
|
LOG.warn(err)
|
|
255
255
|
// REVISIT: use i18n
|
|
256
|
-
//do not reply with error, if response already processed (streaming)
|
|
257
|
-
|
|
256
|
+
// do not reply with error, if response already processed (streaming)
|
|
257
|
+
// destroy response socket instead
|
|
258
|
+
if (res.headersSent) {
|
|
259
|
+
// REVISIT: temp solution until streaming is switched to express middlewares
|
|
260
|
+
res.socket.destroy()
|
|
261
|
+
} else {
|
|
258
262
|
const { error, statusCode } = normalizeError(err, req)
|
|
259
263
|
res.status(statusCode).send({ error })
|
|
260
264
|
}
|
|
@@ -251,6 +251,14 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
251
251
|
return odataResult
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
const _reliablePagingPossible = req => {
|
|
255
|
+
if (req.target._isDraftEnabled) return false
|
|
256
|
+
if (cds.context?.http.req.query.$apply) return false
|
|
257
|
+
if (req.query.SELECT.limit.offset?.val ?? req.query.SELECT.limit.offset > 0) return false
|
|
258
|
+
if (req.query.SELECT.orderBy.some(o => !o.ref)) return false
|
|
259
|
+
return req.query.SELECT.orderBy.every(o => req.query.SELECT.columns.some(c => o.ref[0] === c.ref[0]))
|
|
260
|
+
}
|
|
261
|
+
|
|
254
262
|
/**
|
|
255
263
|
* Read an entity collection without including the count of the total amount of entities.
|
|
256
264
|
*
|
|
@@ -260,6 +268,7 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
260
268
|
* @returns {Promise}
|
|
261
269
|
* @private
|
|
262
270
|
*/
|
|
271
|
+
// eslint-disable-next-line complexity
|
|
263
272
|
const _readCollection = async (tx, req, odataReq) => {
|
|
264
273
|
const result = (await tx.dispatch(req)) || []
|
|
265
274
|
if (Array.isArray(req.query)) {
|
|
@@ -284,7 +293,23 @@ const _readCollection = async (tx, req, odataReq) => {
|
|
|
284
293
|
const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)
|
|
285
294
|
if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
|
|
286
295
|
const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN)
|
|
287
|
-
|
|
296
|
+
if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
|
|
297
|
+
const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
|
|
298
|
+
const skipToken = {
|
|
299
|
+
r: (decoded?.r || 0) + limit,
|
|
300
|
+
c: req.query.SELECT.orderBy.map(o => ({
|
|
301
|
+
a: o.sort ? o.sort === 'asc' : true,
|
|
302
|
+
k: o.ref[0],
|
|
303
|
+
v: result[result.length - 1][o.ref[0]]
|
|
304
|
+
}))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (limit + (decoded?.r || 0) !== top) {
|
|
308
|
+
result.$nextLink = Buffer.from(JSON.stringify(skipToken)).toString('base64')
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
result.$nextLink = (token ? parseInt(token) : 0) + limit
|
|
312
|
+
}
|
|
288
313
|
}
|
|
289
314
|
|
|
290
315
|
const odataResult = toODataResult(result, req)
|
|
@@ -11,6 +11,7 @@ const { isReturnMinimal } = require('../utils/handlerUtils')
|
|
|
11
11
|
const { readAfterWrite } = require('../utils/readAfterWrite')
|
|
12
12
|
const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
|
|
13
13
|
const { hasOmitValuesPreference } = require('../utils/omitValues')
|
|
14
|
+
const { isStreaming } = require('../utils/stream')
|
|
14
15
|
|
|
15
16
|
const { getSapMessages } = require('../../../../common/error/frontend')
|
|
16
17
|
|
|
@@ -187,7 +187,7 @@ class ExpressionToCQN {
|
|
|
187
187
|
// ignore
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
return { func: `${
|
|
190
|
+
return operator ? [operator, { func: `${methodName}`, args }] : { func: `${methodName}`, args }
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/* eslint-disable complexity */
|
|
@@ -41,6 +41,8 @@ const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks'
|
|
|
41
41
|
|
|
42
42
|
const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
|
|
43
43
|
|
|
44
|
+
const { skipToken: handleSkipToken } = require('../../../../../odata/utils')
|
|
45
|
+
|
|
44
46
|
const _applyOnlyContainsFilter = apply => Object.keys(apply).length === 1 && apply.filter
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -136,9 +138,9 @@ const _apply = (uriInfo, queryOptions, entity, model) => {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
const _topSkip = (queryOptions, target, cqn) => {
|
|
139
|
-
if (queryOptions && (queryOptions.$top || queryOptions.$skip
|
|
141
|
+
if (queryOptions && (queryOptions.$top || queryOptions.$skip)) {
|
|
140
142
|
const top = queryOptions.$top ? parseInt(queryOptions.$top) : getDefaultPageSize(target)
|
|
141
|
-
const skip = parseInt(queryOptions.$skip || 0)
|
|
143
|
+
const skip = parseInt(queryOptions.$skip || 0)
|
|
142
144
|
cqn.limit(Math.min(top, getMaxPageSize(target)), skip)
|
|
143
145
|
}
|
|
144
146
|
}
|
|
@@ -324,6 +326,10 @@ const _handleApply = (apply, select) => {
|
|
|
324
326
|
select.push(...mergedArray)
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
const _skipToken = (token, cqn) => {
|
|
330
|
+
handleSkipToken(token, cqn)
|
|
331
|
+
}
|
|
332
|
+
|
|
327
333
|
/**
|
|
328
334
|
* Transform odata READ request into a CQN object.
|
|
329
335
|
*
|
|
@@ -393,6 +399,9 @@ const readToCQN = (service, target, odataReq) => {
|
|
|
393
399
|
if (isCollectionOrToMany) {
|
|
394
400
|
_topSkip(queryOptions, target, cqn)
|
|
395
401
|
_orderby(uriInfo, cqn)
|
|
402
|
+
|
|
403
|
+
const skipToken = queryOptions?.$skiptoken
|
|
404
|
+
if (skipToken) _skipToken(skipToken, cqn)
|
|
396
405
|
}
|
|
397
406
|
|
|
398
407
|
if (!isCollectionOrToMany || entity._isSingleton) cqn.SELECT.one = true
|
|
@@ -166,7 +166,7 @@ class PrimitiveValueDecoder {
|
|
|
166
166
|
throw new IllegalArgumentError(
|
|
167
167
|
'Invalid value ' +
|
|
168
168
|
value +
|
|
169
|
-
' (
|
|
169
|
+
' (' +
|
|
170
170
|
typeof value +
|
|
171
171
|
'). ' +
|
|
172
172
|
'A JSON string must be specified as value for type ' +
|
|
@@ -180,7 +180,7 @@ class PrimitiveValueDecoder {
|
|
|
180
180
|
throw new IllegalArgumentError(
|
|
181
181
|
'Invalid value ' +
|
|
182
182
|
value +
|
|
183
|
-
' (
|
|
183
|
+
' (' +
|
|
184
184
|
typeof value +
|
|
185
185
|
') ' +
|
|
186
186
|
'as value for type ' +
|
|
@@ -209,7 +209,7 @@ class PrimitiveValueDecoder {
|
|
|
209
209
|
throw new IllegalArgumentError(
|
|
210
210
|
'Invalid value ' +
|
|
211
211
|
value +
|
|
212
|
-
' (
|
|
212
|
+
' (' +
|
|
213
213
|
typeof value +
|
|
214
214
|
') ' +
|
|
215
215
|
'as value for type ' +
|
|
@@ -226,7 +226,7 @@ class PrimitiveValueDecoder {
|
|
|
226
226
|
throw new IllegalArgumentError(
|
|
227
227
|
'Invalid value ' +
|
|
228
228
|
value +
|
|
229
|
-
' (
|
|
229
|
+
' (' +
|
|
230
230
|
typeof value +
|
|
231
231
|
') ' +
|
|
232
232
|
'as value for type ' +
|
|
@@ -247,7 +247,7 @@ class PrimitiveValueDecoder {
|
|
|
247
247
|
throw new IllegalArgumentError(
|
|
248
248
|
'Invalid value ' +
|
|
249
249
|
value +
|
|
250
|
-
' (
|
|
250
|
+
' (' +
|
|
251
251
|
typeof value +
|
|
252
252
|
'). A JSON string must be specified ' +
|
|
253
253
|
'as value for type ' +
|
|
@@ -262,7 +262,7 @@ class PrimitiveValueDecoder {
|
|
|
262
262
|
throw new IllegalArgumentError(
|
|
263
263
|
'Invalid value ' +
|
|
264
264
|
value +
|
|
265
|
-
' (
|
|
265
|
+
' (' +
|
|
266
266
|
typeof value +
|
|
267
267
|
') ' +
|
|
268
268
|
'as value for type ' +
|
|
@@ -311,7 +311,7 @@ class PrimitiveValueDecoder {
|
|
|
311
311
|
throw new IllegalArgumentError(
|
|
312
312
|
'Invalid value ' +
|
|
313
313
|
value +
|
|
314
|
-
' (
|
|
314
|
+
' (' +
|
|
315
315
|
typeof value +
|
|
316
316
|
'). A JSON ' +
|
|
317
317
|
kind +
|
|
@@ -339,7 +339,7 @@ class PrimitiveValueDecoder {
|
|
|
339
339
|
throw new IllegalArgumentError(
|
|
340
340
|
'Invalid value ' +
|
|
341
341
|
value +
|
|
342
|
-
' (
|
|
342
|
+
' (' +
|
|
343
343
|
typeof value +
|
|
344
344
|
') for enumeration type ' +
|
|
345
345
|
type.getFullQualifiedName() +
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js
CHANGED
|
@@ -260,7 +260,7 @@ class ValueValidator {
|
|
|
260
260
|
throw new IllegalArgumentError(
|
|
261
261
|
'Invalid value ' +
|
|
262
262
|
value +
|
|
263
|
-
' (
|
|
263
|
+
' (' +
|
|
264
264
|
typeof value +
|
|
265
265
|
')' +
|
|
266
266
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -350,7 +350,7 @@ class ValueValidator {
|
|
|
350
350
|
throw new IllegalArgumentError(
|
|
351
351
|
'Invalid value ' +
|
|
352
352
|
value +
|
|
353
|
-
' (
|
|
353
|
+
' (' +
|
|
354
354
|
typeof value +
|
|
355
355
|
')' +
|
|
356
356
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -383,7 +383,7 @@ class ValueValidator {
|
|
|
383
383
|
throw new IllegalArgumentError(
|
|
384
384
|
'Invalid value ' +
|
|
385
385
|
value +
|
|
386
|
-
' (
|
|
386
|
+
' (' +
|
|
387
387
|
typeof value +
|
|
388
388
|
')' +
|
|
389
389
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -406,7 +406,7 @@ class ValueValidator {
|
|
|
406
406
|
throw new IllegalArgumentError(
|
|
407
407
|
'Invalid value ' +
|
|
408
408
|
value +
|
|
409
|
-
' (
|
|
409
|
+
' (' +
|
|
410
410
|
typeof value +
|
|
411
411
|
')' +
|
|
412
412
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -424,7 +424,7 @@ class ValueValidator {
|
|
|
424
424
|
throw new IllegalArgumentError(
|
|
425
425
|
'Invalid value ' +
|
|
426
426
|
value +
|
|
427
|
-
' (
|
|
427
|
+
' (' +
|
|
428
428
|
typeof value +
|
|
429
429
|
')' +
|
|
430
430
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -442,7 +442,7 @@ class ValueValidator {
|
|
|
442
442
|
throw new IllegalArgumentError(
|
|
443
443
|
'Invalid value ' +
|
|
444
444
|
value +
|
|
445
|
-
' (
|
|
445
|
+
' (' +
|
|
446
446
|
typeof value +
|
|
447
447
|
')' +
|
|
448
448
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -510,7 +510,7 @@ class ValueValidator {
|
|
|
510
510
|
throw this._valueError(
|
|
511
511
|
value,
|
|
512
512
|
'Edm.GeographyPoint or Edm.GeometryPoint',
|
|
513
|
-
'
|
|
513
|
+
'Object with type and coordinates'
|
|
514
514
|
)
|
|
515
515
|
}
|
|
516
516
|
}
|
|
@@ -528,7 +528,7 @@ class ValueValidator {
|
|
|
528
528
|
throw this._valueError(
|
|
529
529
|
value,
|
|
530
530
|
'Edm.GeographyLineString or Edm.GeometryLineString',
|
|
531
|
-
'
|
|
531
|
+
'Object with type and coordinates'
|
|
532
532
|
)
|
|
533
533
|
}
|
|
534
534
|
}
|
|
@@ -543,7 +543,7 @@ class ValueValidator {
|
|
|
543
543
|
throw this._valueError(
|
|
544
544
|
value,
|
|
545
545
|
'Edm.GeographyPolygon or Edm.GeometryPolygon',
|
|
546
|
-
'
|
|
546
|
+
'Object with type and coordinates'
|
|
547
547
|
)
|
|
548
548
|
}
|
|
549
549
|
}
|
|
@@ -561,7 +561,7 @@ class ValueValidator {
|
|
|
561
561
|
throw this._valueError(
|
|
562
562
|
value,
|
|
563
563
|
'Edm.GeographyMultiPoint or Edm.GeometryMultiPoint',
|
|
564
|
-
'
|
|
564
|
+
'Object with type and coordinates'
|
|
565
565
|
)
|
|
566
566
|
}
|
|
567
567
|
}
|
|
@@ -579,7 +579,7 @@ class ValueValidator {
|
|
|
579
579
|
throw this._valueError(
|
|
580
580
|
value,
|
|
581
581
|
'Edm.GeographyMultiLineString or Edm.GeometryMultiLineString',
|
|
582
|
-
'
|
|
582
|
+
'Object with type and coordinates'
|
|
583
583
|
)
|
|
584
584
|
}
|
|
585
585
|
}
|
|
@@ -597,7 +597,7 @@ class ValueValidator {
|
|
|
597
597
|
throw this._valueError(
|
|
598
598
|
value,
|
|
599
599
|
'Edm.GeographyMultiPolygon or Edm.GeometryMultiPolygon',
|
|
600
|
-
'
|
|
600
|
+
'Object with type and coordinates'
|
|
601
601
|
)
|
|
602
602
|
}
|
|
603
603
|
}
|
|
@@ -629,7 +629,7 @@ class ValueValidator {
|
|
|
629
629
|
throw this._valueError(
|
|
630
630
|
value,
|
|
631
631
|
'Edm.GeographyCollection or Edm.GeometryCollection',
|
|
632
|
-
'
|
|
632
|
+
'Object with type and geometries'
|
|
633
633
|
)
|
|
634
634
|
}
|
|
635
635
|
}
|
|
@@ -725,7 +725,7 @@ class ValueValidator {
|
|
|
725
725
|
const msg =
|
|
726
726
|
'Invalid value ' +
|
|
727
727
|
(typeName.includes('Geo') ? JSON.stringify(value) : value) +
|
|
728
|
-
' (
|
|
728
|
+
' (' +
|
|
729
729
|
typeof value +
|
|
730
730
|
')' +
|
|
731
731
|
(this.property ? ' for property "' + this.property + '"' : '') +
|
|
@@ -127,15 +127,14 @@ class DeserializerFactory {
|
|
|
127
127
|
static createBinaryDeserializer () {
|
|
128
128
|
return (request, next) => {
|
|
129
129
|
let type = request.getUriInfo() && request.getUriInfo().getFinalEdmType()
|
|
130
|
-
if (type && type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
|
|
130
|
+
if (type && type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
|
|
131
131
|
if (type && type === EdmPrimitiveTypeKind.Stream) {
|
|
132
|
-
|
|
133
|
-
null,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
132
|
+
if (request.getIncomingRequest().complete) { // empty or NULL
|
|
133
|
+
next(null, request.getIncomingRequest())
|
|
134
|
+
} else {
|
|
135
|
+
const streamPipeline = stream.pipeline(request.getIncomingRequest(), new stream.PassThrough(), () => {})
|
|
136
|
+
next(null, streamPipeline)
|
|
137
|
+
}
|
|
139
138
|
} else {
|
|
140
139
|
request
|
|
141
140
|
.getIncomingRequest()
|
|
@@ -950,6 +950,7 @@ class ResourceJsonSerializer {
|
|
|
950
950
|
const [identifier, star, annotation] = this._getPropertyNameAndAnnotation(entityProp)
|
|
951
951
|
|
|
952
952
|
if (identifier && !type.getProperty(identifier)) {
|
|
953
|
+
if (identifier === 'DraftAdministrativeData_DraftUUID') continue
|
|
953
954
|
throw new SerializationError(
|
|
954
955
|
"The entity contains data for '" +
|
|
955
956
|
entityProp +
|
|
@@ -1042,6 +1043,8 @@ class ResourceJsonSerializer {
|
|
|
1042
1043
|
_serializeNullValue (propertyOrReturnType) {
|
|
1043
1044
|
const nullable = propertyOrReturnType.isNullable()
|
|
1044
1045
|
if (nullable === undefined || nullable) return null
|
|
1046
|
+
const name = propertyOrReturnType.getName()
|
|
1047
|
+
if (name === 'IsActiveEntity' || name === 'HasActiveEntity' || name === 'HasDraftEntity') return
|
|
1045
1048
|
throw new SerializationError(
|
|
1046
1049
|
'Not nullable value ' +
|
|
1047
1050
|
(propertyOrReturnType.getName ? "for '" + propertyOrReturnType.getName() + "' " : '') +
|
|
@@ -83,10 +83,11 @@ class UriHelper {
|
|
|
83
83
|
const property = keyPropertyRef.getProperty()
|
|
84
84
|
for (const pathElement of keyPropertyRef.getName().split('/')) {
|
|
85
85
|
value = value[pathElement]
|
|
86
|
-
if (value === undefined) {
|
|
86
|
+
if (value === undefined && name !== 'IsActiveEntity') {
|
|
87
87
|
throw new IllegalArgumentError(`The key '${pathElement}' does not exist in the given entity`)
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
if (value === undefined && name === 'IsActiveEntity') continue
|
|
90
91
|
value = primitiveValueEncoder.encodeText(value, property)
|
|
91
92
|
keys.push({ type: property.getType(), name, value })
|
|
92
93
|
}
|
|
@@ -306,9 +306,10 @@ const _getPathInfo = (query, model) => {
|
|
|
306
306
|
} else {
|
|
307
307
|
returnType = target
|
|
308
308
|
isCollection = Array.isArray(query) || (query.SELECT && !query.SELECT.one)
|
|
309
|
-
propertyName =
|
|
309
|
+
propertyName = query._propertyAccess && query.SELECT.columns[0].ref[query.SELECT.columns[0].ref.length - 1]
|
|
310
|
+
if (propertyName) returnType = query.SELECT.columns[0].ref.reduce((r, c) => r.elements[c], target)
|
|
310
311
|
}
|
|
311
|
-
const isStream =
|
|
312
|
+
const isStream = propertyName && target.elements[propertyName]?.['@Core.MediaType']
|
|
312
313
|
return {
|
|
313
314
|
path,
|
|
314
315
|
target,
|
|
@@ -38,6 +38,12 @@ const _getOperationQueryColumns = urlQueryOptions => {
|
|
|
38
38
|
const _isDraftAction = req => req.event in { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
|
|
39
39
|
const _isActionOrFunction = req => !(req.event in CDS_EVENTS) || _isDraftAction(req)
|
|
40
40
|
const _isWriteWithResponse = req => req.event in WRITE_EVENTS && !(req.event in { CANCEL: 1, DELETE: 1 })
|
|
41
|
+
const _ensureKeysAreSelected = query => {
|
|
42
|
+
if (!query.SELECT.columns || query.SELECT.columns.some(c => c === '*')) return
|
|
43
|
+
for (const key in query._target.keys) {
|
|
44
|
+
if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: false }) => {
|
|
43
49
|
let query
|
|
@@ -55,6 +61,7 @@ const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: fa
|
|
|
55
61
|
query = getDeepSelect(req)
|
|
56
62
|
}
|
|
57
63
|
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
64
|
+
_ensureKeysAreSelected(query)
|
|
58
65
|
// gracefully set location and no body if no read auth or not readable capability
|
|
59
66
|
let result
|
|
60
67
|
try {
|
|
@@ -108,9 +108,6 @@ const getStreamProperties = (req, model) => {
|
|
|
108
108
|
// used cloned path
|
|
109
109
|
let select = SELECT.one.from({ ref: deepCopyArray(req.query.SELECT.from.ref) }).columns(columns)
|
|
110
110
|
|
|
111
|
-
// new parser has media property as last ref element -> remove
|
|
112
|
-
if (req._metaInfo && req._metaInfo.propertyName) select.SELECT.from.ref.pop()
|
|
113
|
-
|
|
114
111
|
const pathToDraft = isPathToDraft(select.SELECT.from.ref, model)
|
|
115
112
|
if (req.target._isDraftEnabled && pathToDraft) {
|
|
116
113
|
select = cqn2cqn4sql(select, model)
|
|
@@ -54,26 +54,14 @@ class ApplicationService extends cds.Service {
|
|
|
54
54
|
|
|
55
55
|
registerFioriHandlers() {
|
|
56
56
|
if (cds.env.features.lean_draft) {
|
|
57
|
-
const {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
onDraftActivate,
|
|
61
|
-
onPatch,
|
|
62
|
-
onDraftEdit,
|
|
63
|
-
onDelete
|
|
64
|
-
} = require('../../fiori/lean-draft')
|
|
65
|
-
const LOG = cds.log('fiori|drafts')
|
|
66
|
-
|
|
67
|
-
for (let each of this.entities)
|
|
57
|
+
const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
|
|
58
|
+
|
|
59
|
+
for (const each of this.entities)
|
|
68
60
|
if (each.drafts) {
|
|
69
|
-
|
|
70
|
-
this.on('
|
|
71
|
-
this.on('
|
|
72
|
-
this.on('
|
|
73
|
-
this.on('draftPrepare', each, onDraftPrepare)
|
|
74
|
-
this.on('draftActivate', each, onDraftActivate)
|
|
75
|
-
this.on('draftActivate', each, onDraftActivate)
|
|
76
|
-
this.on(['CANCEL', 'DELETE'], each, onDelete)
|
|
61
|
+
this.on('NEW', each.drafts, onNew)
|
|
62
|
+
this.on('EDIT', each, onEdit)
|
|
63
|
+
this.on('CANCEL', each.drafts, onCancel)
|
|
64
|
+
this.on('draftPrepare', each.drafts, onPrepare)
|
|
77
65
|
}
|
|
78
66
|
} else return require('../../fiori/generic').impl.call(this)
|
|
79
67
|
}
|
|
@@ -148,4 +136,5 @@ class ApplicationService extends cds.Service {
|
|
|
148
136
|
}
|
|
149
137
|
}
|
|
150
138
|
|
|
139
|
+
ApplicationService.prototype.isAppService = true
|
|
151
140
|
module.exports = ApplicationService
|
|
@@ -4,6 +4,8 @@ const LOG = cds.log()
|
|
|
4
4
|
|
|
5
5
|
const { DRAFT_COLUMNS_UNION_MAP } = require('../../../common/constants/draft')
|
|
6
6
|
|
|
7
|
+
const defaultSearchableType = 'cds.String'
|
|
8
|
+
|
|
7
9
|
// REVISIT: Can we combine that with db/utils/columns.js?
|
|
8
10
|
/**
|
|
9
11
|
* This method gets all columns for an entity.
|
|
@@ -43,7 +45,10 @@ const _getSearchableColumns = entity => {
|
|
|
43
45
|
const columnsOptions = { removeIgnore: true, filterVirtual: true }
|
|
44
46
|
const columns = getColumns(entity, columnsOptions)
|
|
45
47
|
const cdsSearchTerm = '@cds.search'
|
|
46
|
-
const cdsSearchKeys =
|
|
48
|
+
const cdsSearchKeys = []
|
|
49
|
+
for (const key in entity) {
|
|
50
|
+
if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
|
|
51
|
+
}
|
|
47
52
|
const cdsSearchColumnMap = new Map()
|
|
48
53
|
let atLeastOneColumnIsSearchable = false
|
|
49
54
|
|
|
@@ -63,9 +68,6 @@ const _getSearchableColumns = entity => {
|
|
|
63
68
|
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
// For performance reasons, by default, only elements typed as strings are searchable unless
|
|
67
|
-
// the @cds.search annotation is specified.
|
|
68
|
-
const defaultSearchableType = 'cds.String'
|
|
69
71
|
const searchableColumns = columns.filter(column => {
|
|
70
72
|
const annotatedColumnValue = cdsSearchColumnMap.get(column.name)
|
|
71
73
|
|
|
@@ -144,6 +146,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
|
|
|
144
146
|
|
|
145
147
|
const columnRef = column.ref
|
|
146
148
|
if (columnRef) {
|
|
149
|
+
if (entity.elements[columnRef[columnRef.length - 1]]?._type !== defaultSearchableType) return
|
|
147
150
|
column = { ref: [...column.ref] }
|
|
148
151
|
if (alias) column.ref.unshift(alias)
|
|
149
152
|
toBeSearched.push(column)
|
|
@@ -25,7 +25,6 @@ const ASSERT_FORMAT = 'ASSERT_FORMAT'
|
|
|
25
25
|
const ASSERT_DATA_TYPE = 'ASSERT_DATA_TYPE'
|
|
26
26
|
const ASSERT_ENUM = 'ASSERT_ENUM'
|
|
27
27
|
const ASSERT_NOT_NULL = 'ASSERT_NOT_NULL'
|
|
28
|
-
const ASSERT_DEEP_ASSOCIATION = 'ASSERT_DEEP_ASSOCIATION'
|
|
29
28
|
|
|
30
29
|
const _enumValues = element => {
|
|
31
30
|
return Object.keys(element).map(enumKey => {
|
|
@@ -221,11 +220,18 @@ const _isNotFilled = value => {
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
const _checkMandatoryElement = (element, value, errors, key, pathSegments) => {
|
|
223
|
+
if (element.parent?.query?.SELECT?.columns?.find(col => _isNavigationColumn(col, element.name))) return
|
|
224
224
|
if (element._isMandatory && !element.default && _isNotFilled(value)) {
|
|
225
225
|
errors.push(assertError(ASSERT_NOT_NULL, element, value, key, pathSegments))
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
const _isNavigationColumn = (column, searched) => {
|
|
230
|
+
return (
|
|
231
|
+
column.ref && column.ref.length > 1 && (column.as === searched || column.ref[column.ref.length - 1] === searched)
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
229
235
|
const _getEnumElement = element => {
|
|
230
236
|
return (element['@assert.range'] && element.enum) || element['@assert.enum'] ? element.enum : undefined
|
|
231
237
|
}
|
|
@@ -2,7 +2,8 @@ const { parentPort } = require('worker_threads')
|
|
|
2
2
|
const { Responses, Errors } = require('../../../../lib/req/response')
|
|
3
3
|
|
|
4
4
|
class WorkerReq {
|
|
5
|
-
constructor(reqData) {
|
|
5
|
+
constructor(contextId, reqData) {
|
|
6
|
+
this.contextId = contextId
|
|
6
7
|
Object.assign(this, reqData)
|
|
7
8
|
this.postMessages = []
|
|
8
9
|
this.messages = this.messages ?? []
|
|
@@ -64,6 +65,7 @@ class WorkerReq {
|
|
|
64
65
|
|
|
65
66
|
reject(...args) {
|
|
66
67
|
parentPort.postMessage({
|
|
68
|
+
contextId: this.contextId,
|
|
67
69
|
kind: 'run',
|
|
68
70
|
target: 'req',
|
|
69
71
|
prop: 'reject',
|
|
@@ -4,6 +4,7 @@ const path = require('node:path')
|
|
|
4
4
|
const { timeout, resourceLimits } = require('./config')
|
|
5
5
|
const workerPath = path.resolve(__dirname, 'worker.js')
|
|
6
6
|
const { Errors } = require('../../../../lib/req/response')
|
|
7
|
+
const LOG = cds.log()
|
|
7
8
|
|
|
8
9
|
const _getReqData = req => {
|
|
9
10
|
return {
|
|
@@ -17,10 +18,11 @@ const _getReqData = req => {
|
|
|
17
18
|
|
|
18
19
|
module.exports = async function executeCode(code, req) {
|
|
19
20
|
const reqData = _getReqData(req)
|
|
21
|
+
const srv = this
|
|
20
22
|
const _getTarget = target => {
|
|
21
23
|
switch (target) {
|
|
22
24
|
case 'srv':
|
|
23
|
-
return
|
|
25
|
+
return srv
|
|
24
26
|
|
|
25
27
|
case 'req':
|
|
26
28
|
return req
|
|
@@ -29,6 +31,7 @@ module.exports = async function executeCode(code, req) {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
const workerId = cds.utils.uuid()
|
|
34
|
+
const contextId = cds.utils.uuid()
|
|
32
35
|
const worker = new Worker(workerPath, {
|
|
33
36
|
workerData: { id: workerId },
|
|
34
37
|
resourceLimits
|
|
@@ -50,6 +53,8 @@ module.exports = async function executeCode(code, req) {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
function onMessageReceived(message) {
|
|
56
|
+
if (LOG._debug)
|
|
57
|
+
LOG.debug(`Post message received on main thread (code-ext/execute.js) from worker thread`, message)
|
|
53
58
|
switch (message.kind) {
|
|
54
59
|
case 'run':
|
|
55
60
|
run(message)
|
|
@@ -89,6 +94,7 @@ module.exports = async function executeCode(code, req) {
|
|
|
89
94
|
if (typeof result?.then === 'function') result = await result
|
|
90
95
|
if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
|
|
91
96
|
} catch (error) {
|
|
97
|
+
if (LOG._debug) LOG.debug(`Calling ${message.target}.${message.prop}(...) throws an error.`, error)
|
|
92
98
|
worker.postMessage({ id: message.id, kind: 'cleanup' })
|
|
93
99
|
reject(error)
|
|
94
100
|
}
|
|
@@ -101,6 +107,7 @@ module.exports = async function executeCode(code, req) {
|
|
|
101
107
|
})
|
|
102
108
|
|
|
103
109
|
// triggers execution of the code in the worker thread
|
|
104
|
-
|
|
110
|
+
const message = { contextId, workerId, kind: 'start', code, reqData }
|
|
111
|
+
worker.postMessage(message)
|
|
105
112
|
return executePromise
|
|
106
113
|
}
|
|
@@ -35,14 +35,14 @@ module.exports = cds.service.impl(function () {
|
|
|
35
35
|
let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
|
|
36
36
|
if (!fqn) {
|
|
37
37
|
if (req.target) return next()
|
|
38
|
-
fqn = this.model.definitions[`${this.name}.${req.event}`] // check for
|
|
38
|
+
fqn = this.model.definitions[`${this.name}.${req.event}`] // check for bound action/function or event
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
|
|
42
42
|
// REVISIT: check whether action/function or event is part of an extension
|
|
43
43
|
if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
|
|
44
44
|
const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
|
|
45
|
-
if (!code) return
|
|
45
|
+
if (!code) return next()
|
|
46
46
|
return await executeCode.call(this, code, req)
|
|
47
47
|
}
|
|
48
48
|
})
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const LOG = cds.log()
|
|
1
3
|
const { parentPort, workerData } = require('worker_threads')
|
|
2
4
|
const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
|
|
3
5
|
const WorkerReq = require('./WorkerReq')
|
|
4
6
|
const { timeout } = require('./config')
|
|
5
7
|
|
|
6
|
-
parentPort.once('message', function
|
|
7
|
-
|
|
8
|
+
parentPort.once('message', function onWorkerMessageReceived(message) {
|
|
9
|
+
const { contextId, workerId, kind, code, reqData } = message
|
|
10
|
+
if (LOG._debug) LOG.debug(`Post message received on worker thread (worker.js) from main thread`, message)
|
|
11
|
+
if (kind !== 'start' || workerId !== workerData.id) return
|
|
8
12
|
|
|
9
13
|
// eslint-disable-next-line cds/no-missing-dependencies
|
|
10
14
|
const { VM } = require('vm2')
|
|
11
|
-
const workerReq = new WorkerReq(reqData)
|
|
15
|
+
const workerReq = new WorkerReq(contextId, reqData)
|
|
12
16
|
const vm = new VM({
|
|
13
17
|
console: 'inherit',
|
|
14
18
|
timeout, // specifies the number of milliseconds to execute code before terminating execution
|
|
@@ -28,9 +32,9 @@ parentPort.once('message', function onMessageReceived({ id, code, reqData }) {
|
|
|
28
32
|
try {
|
|
29
33
|
;(async function () {
|
|
30
34
|
const result = await vm.run(code)
|
|
31
|
-
parentPort.postMessage({ kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
|
|
35
|
+
parentPort.postMessage({ contextId, kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
|
|
32
36
|
})()
|
|
33
37
|
} catch (error) {
|
|
34
|
-
parentPort.postMessage({ kind: 'error', error })
|
|
38
|
+
parentPort.postMessage({ contextId, kind: 'error', error })
|
|
35
39
|
}
|
|
36
40
|
})
|