@sap/cds 6.4.1 → 6.6.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 +79 -6
- package/README.md +5 -0
- package/apis/cqn.d.ts +14 -3
- package/apis/ql.d.ts +8 -8
- package/apis/services.d.ts +37 -65
- package/apis/test.d.ts +7 -0
- package/bin/build/buildTaskEngine.js +9 -14
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +3 -14
- package/bin/build/index.js +8 -2
- package/bin/build/provider/buildTaskProviderInternal.js +18 -13
- 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/hana/template/package.json +3 -0
- package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
- package/bin/build/provider/mtx-extension/index.js +57 -37
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +18 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/serve.js +36 -20
- package/common.cds +7 -0
- package/lib/auth/jwt-auth.js +8 -7
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/compile/minify.js +3 -3
- package/lib/dbs/cds-deploy.js +18 -17
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +5 -1
- package/lib/env/schemas/cds-rc.json +74 -3
- package/lib/index.js +4 -2
- package/lib/lazy.js +6 -8
- package/lib/log/cds-error.js +2 -2
- package/lib/ql/Whereable.js +22 -11
- package/lib/ql/cds-ql.js +1 -1
- package/lib/req/cds-context.js +3 -3
- package/lib/req/response.js +8 -3
- package/lib/req/user.js +12 -2
- 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 +8 -3
- package/lib/utils/cds-test.js +7 -5
- package/lib/utils/cds-utils.js +3 -1
- package/lib/utils/tar.js +6 -3
- package/libx/_runtime/auth/strategies/JWT.js +1 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
- package/libx/_runtime/auth/strategies/mock.js +12 -1
- package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
- package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -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 +8 -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 +1 -0
- 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 +11 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
- package/libx/_runtime/common/code-ext/config.js +13 -0
- package/libx/_runtime/common/code-ext/execute.js +113 -0
- package/libx/_runtime/common/code-ext/handlers.js +49 -0
- package/libx/_runtime/common/code-ext/worker.js +40 -0
- package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
- 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/crud.js +4 -0
- package/libx/_runtime/common/generic/etag.js +3 -1
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
- package/libx/_runtime/common/utils/path.js +5 -26
- package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
- 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 +7 -4
- 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/db/utils/localized.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +7 -1
- package/libx/_runtime/fiori/generic/before.js +9 -1
- package/libx/_runtime/fiori/generic/edit.js +8 -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 +16 -5
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +505 -241
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +51 -51
- 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 +23 -8
- package/libx/odata/cqn2odata.js +1 -1
- 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/libx/rest/middleware/parse.js +26 -4
- 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
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 + '"' : '') +
|
|
@@ -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)
|
|
@@ -22,6 +22,9 @@ class ApplicationService extends cds.Service {
|
|
|
22
22
|
require('../../common/generic/temporal').call(this, this)
|
|
23
23
|
require('../../common/generic/paging').call(this, this) // > paging must be executed before sorting
|
|
24
24
|
require('../../common/generic/sorting').call(this, this)
|
|
25
|
+
|
|
26
|
+
if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this, this)
|
|
27
|
+
|
|
25
28
|
this.registerFioriHandlers(this)
|
|
26
29
|
this.registerPersonalDataHandlers(this)
|
|
27
30
|
this.registerCrudHandlers(this) // default .on handlers, have to go last
|
|
@@ -51,26 +54,14 @@ class ApplicationService extends cds.Service {
|
|
|
51
54
|
|
|
52
55
|
registerFioriHandlers() {
|
|
53
56
|
if (cds.env.features.lean_draft) {
|
|
54
|
-
const {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
onDraftActivate,
|
|
58
|
-
onPatch,
|
|
59
|
-
onDraftEdit,
|
|
60
|
-
onDelete
|
|
61
|
-
} = require('../../fiori/lean-draft')
|
|
62
|
-
const LOG = cds.log('fiori|drafts')
|
|
63
|
-
|
|
64
|
-
for (let each of this.entities)
|
|
57
|
+
const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
|
|
58
|
+
|
|
59
|
+
for (const each of this.entities)
|
|
65
60
|
if (each.drafts) {
|
|
66
|
-
|
|
67
|
-
this.on('
|
|
68
|
-
this.on('
|
|
69
|
-
this.on('
|
|
70
|
-
this.on('draftPrepare', each, onDraftPrepare)
|
|
71
|
-
this.on('draftActivate', each, onDraftActivate)
|
|
72
|
-
this.on('draftActivate', each, onDraftActivate)
|
|
73
|
-
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)
|
|
74
65
|
}
|
|
75
66
|
} else return require('../../fiori/generic').impl.call(this)
|
|
76
67
|
}
|
|
@@ -145,4 +136,5 @@ class ApplicationService extends cds.Service {
|
|
|
145
136
|
}
|
|
146
137
|
}
|
|
147
138
|
|
|
139
|
+
ApplicationService.prototype.isAppService = true
|
|
148
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
|
|
|
@@ -122,44 +124,44 @@ const _getSearchableColumns = entity => {
|
|
|
122
124
|
* @returns {import('../../../types/api').ColumnRefs}
|
|
123
125
|
*/
|
|
124
126
|
const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, alias) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (cqn.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
cqn.SELECT.columns.length === 1 &&
|
|
143
|
-
column.func === 'count' &&
|
|
144
|
-
(column.as === '_counted_' || column.as === '$count')
|
|
145
|
-
)
|
|
127
|
+
let toBeSearched = []
|
|
128
|
+
|
|
129
|
+
// aggregations case
|
|
130
|
+
// in the new parser groupBy is moved to sub select.
|
|
131
|
+
if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
|
|
132
|
+
cqn.SELECT.columns &&
|
|
133
|
+
cqn.SELECT.columns.forEach(column => {
|
|
134
|
+
if (column.func) {
|
|
135
|
+
// exclude $count by SELECT of number of Items in a Collection
|
|
136
|
+
if (
|
|
137
|
+
cqn.SELECT.columns.length === 1 &&
|
|
138
|
+
column.func === 'count' &&
|
|
139
|
+
(column.as === '_counted_' || column.as === '$count')
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
toBeSearched.push(column)
|
|
146
144
|
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const columnRef = column.ref
|
|
148
|
+
if (columnRef) {
|
|
149
|
+
if (entity.elements[columnRef[columnRef.length - 1]]?._type !== defaultSearchableType) return
|
|
150
|
+
column = { ref: [...column.ref] }
|
|
151
|
+
if (alias) column.ref.unshift(alias)
|
|
152
|
+
toBeSearched.push(column)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
} else {
|
|
156
|
+
toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
157
|
+
|
|
158
|
+
if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
|
|
159
|
+
toBeSearched = toBeSearched.map(c => {
|
|
160
|
+
const col = { ref: [c] }
|
|
161
|
+
if (alias) col.ref.unshift(alias)
|
|
162
|
+
return col
|
|
161
163
|
})
|
|
162
|
-
|
|
164
|
+
}
|
|
163
165
|
return toBeSearched
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { parentPort } = require('worker_threads')
|
|
2
|
+
const { Responses, Errors } = require('../../../../lib/req/response')
|
|
3
|
+
|
|
4
|
+
class WorkerReq {
|
|
5
|
+
constructor(contextId, reqData) {
|
|
6
|
+
this.contextId = contextId
|
|
7
|
+
Object.assign(this, reqData)
|
|
8
|
+
this.postMessages = []
|
|
9
|
+
this.messages = this.messages ?? []
|
|
10
|
+
this.errors = this.errors ?? new Errors()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#push(args) {
|
|
14
|
+
this.postMessages.push({
|
|
15
|
+
kind: 'run',
|
|
16
|
+
target: 'req',
|
|
17
|
+
...args
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
notify(...args) {
|
|
22
|
+
this.#push({
|
|
23
|
+
prop: 'notify',
|
|
24
|
+
args
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const notify = Responses.get(1, ...args)
|
|
28
|
+
this.messages.push(notify)
|
|
29
|
+
return notify
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
info(...args) {
|
|
33
|
+
this.#push({
|
|
34
|
+
prop: 'info',
|
|
35
|
+
args
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const info = Responses.get(2, ...args)
|
|
39
|
+
this.messages.push(info)
|
|
40
|
+
return info
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warn(...args) {
|
|
44
|
+
this.#push({
|
|
45
|
+
prop: 'warn',
|
|
46
|
+
args
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const warn = Responses.get(3, ...args)
|
|
50
|
+
this.messages.push(warn)
|
|
51
|
+
return warn
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
error(...args) {
|
|
55
|
+
this.#push({
|
|
56
|
+
prop: 'error',
|
|
57
|
+
args
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
let error = Responses.get(4, ...args)
|
|
61
|
+
if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.error)
|
|
62
|
+
this.errors.push(error)
|
|
63
|
+
return error
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
reject(...args) {
|
|
67
|
+
parentPort.postMessage({
|
|
68
|
+
contextId: this.contextId,
|
|
69
|
+
kind: 'run',
|
|
70
|
+
target: 'req',
|
|
71
|
+
prop: 'reject',
|
|
72
|
+
args
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
let error = Responses.get(4, ...args)
|
|
76
|
+
if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.reject)
|
|
77
|
+
throw error
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = WorkerReq
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const totalMemory = os.totalmem() // total amount of system memory in bytes
|
|
3
|
+
const maxOldGenerationSizeMb = Math.floor(totalMemory / 1024 ** 2 / 8) // max size of the main heap in MB
|
|
4
|
+
const maxYoungGenerationSizeMb = Math.floor(totalMemory / 1024 ** 2 / 8) // max size of a heap space for recently created objects
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
timeout: 10000,
|
|
8
|
+
resourceLimits: {
|
|
9
|
+
maxOldGenerationSizeMb,
|
|
10
|
+
maxYoungGenerationSizeMb,
|
|
11
|
+
stackSizeMb: 4 // default
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const { Worker } = require('worker_threads')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const { timeout, resourceLimits } = require('./config')
|
|
5
|
+
const workerPath = path.resolve(__dirname, 'worker.js')
|
|
6
|
+
const { Errors } = require('../../../../lib/req/response')
|
|
7
|
+
const LOG = cds.log()
|
|
8
|
+
|
|
9
|
+
const _getReqData = req => {
|
|
10
|
+
return {
|
|
11
|
+
data: req.data,
|
|
12
|
+
params: req.params,
|
|
13
|
+
results: req.results,
|
|
14
|
+
messages: req.messages,
|
|
15
|
+
errors: req.errors ?? new Errors()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = async function executeCode(code, req) {
|
|
20
|
+
const reqData = _getReqData(req)
|
|
21
|
+
const srv = this
|
|
22
|
+
const _getTarget = target => {
|
|
23
|
+
switch (target) {
|
|
24
|
+
case 'srv':
|
|
25
|
+
return srv
|
|
26
|
+
|
|
27
|
+
case 'req':
|
|
28
|
+
return req
|
|
29
|
+
|
|
30
|
+
// no default
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const workerId = cds.utils.uuid()
|
|
34
|
+
const contextId = cds.utils.uuid()
|
|
35
|
+
const worker = new Worker(workerPath, {
|
|
36
|
+
workerData: { id: workerId },
|
|
37
|
+
resourceLimits
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const executePromise = new Promise(function executeCodePromiseExecutor(resolve, reject) {
|
|
41
|
+
worker.on('online', onStarted)
|
|
42
|
+
worker.on('message', onMessageReceived)
|
|
43
|
+
worker.on('error', onError)
|
|
44
|
+
worker.on('exit', onExit)
|
|
45
|
+
|
|
46
|
+
let onStartTimeoutID
|
|
47
|
+
|
|
48
|
+
function onStarted() {
|
|
49
|
+
onStartTimeoutID = setTimeout(() => {
|
|
50
|
+
worker.terminate()
|
|
51
|
+
reject(new Error(`Script execution timed out after ${timeout}ms`))
|
|
52
|
+
}, timeout)
|
|
53
|
+
}
|
|
54
|
+
|
|
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)
|
|
58
|
+
switch (message.kind) {
|
|
59
|
+
case 'run':
|
|
60
|
+
run(message)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
case 'success':
|
|
64
|
+
onSuccess(message)
|
|
65
|
+
cleanup()
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
case 'error':
|
|
69
|
+
onError(message.error)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
// no default
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function onSuccess(message) {
|
|
77
|
+
for (const m of message.postMessages) await run(m)
|
|
78
|
+
req.data && Object.assign(req.data, message.req.data) // REVISIT: Why Object.assign(...) is a required?
|
|
79
|
+
req.results = message.req.results
|
|
80
|
+
resolve(req.results ?? message.result)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onError(error) {
|
|
84
|
+
reject(error)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onExit(exitCode) {
|
|
88
|
+
if (exitCode !== 0) reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function run(message) {
|
|
92
|
+
try {
|
|
93
|
+
let result = _getTarget(message.target)[message.prop](...message.args)
|
|
94
|
+
if (typeof result?.then === 'function') result = await result
|
|
95
|
+
if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (LOG._debug) LOG.debug(`Calling ${message.target}.${message.prop}(...) throws an error.`, error)
|
|
98
|
+
worker.postMessage({ id: message.id, kind: 'cleanup' })
|
|
99
|
+
reject(error)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cleanup() {
|
|
104
|
+
clearTimeout(onStartTimeoutID)
|
|
105
|
+
worker.terminate()
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// triggers execution of the code in the worker thread
|
|
110
|
+
const message = { contextId, workerId, kind: 'start', code, reqData }
|
|
111
|
+
worker.postMessage(message)
|
|
112
|
+
return executePromise
|
|
113
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const executeCode = require('./execute')
|
|
3
|
+
|
|
4
|
+
const CODE_ANNOTATION = '@extension.code'
|
|
5
|
+
|
|
6
|
+
module.exports = cds.service.impl(function () {
|
|
7
|
+
const getCodeFromAnnotation = async (defName, operation, registration) => {
|
|
8
|
+
// REVISIT: tenant info in not in this.model and cds.context.model is undefined for single tenancy
|
|
9
|
+
const model = cds.context.model || this.model
|
|
10
|
+
const el = model.definitions[defName]
|
|
11
|
+
const boundEl = el.actions?.[operation]
|
|
12
|
+
const extensionCode = boundEl?.[CODE_ANNOTATION] ?? el[CODE_ANNOTATION]
|
|
13
|
+
if (extensionCode) {
|
|
14
|
+
const annotation = extensionCode.filter(element => element[registration] === operation)
|
|
15
|
+
return annotation.length && annotation[0].code
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.after('READ', async function (result, req) {
|
|
20
|
+
if (result == null) return // whether result is null or undefined
|
|
21
|
+
const code = await getCodeFromAnnotation(req.target.name, req.event, 'after')
|
|
22
|
+
if (!code) return
|
|
23
|
+
await executeCode.call(this, code, req)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
this.before(['CREATE', 'UPDATE', 'DELETE'], async function (req) {
|
|
27
|
+
const code = await getCodeFromAnnotation(req.target.name, req.event, 'before')
|
|
28
|
+
if (!code) return
|
|
29
|
+
await executeCode.call(this, code, req)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
this.on('*', async function (req, next) {
|
|
33
|
+
if (this.name.startsWith('cds.xt')) return next()
|
|
34
|
+
// REVISIT: req.target -> wait until implementation task finished
|
|
35
|
+
let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
|
|
36
|
+
if (!fqn) {
|
|
37
|
+
if (req.target) return next()
|
|
38
|
+
fqn = this.model.definitions[`${this.name}.${req.event}`] // check for bound action/function or event
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
|
|
42
|
+
// REVISIT: check whether action/function or event is part of an extension
|
|
43
|
+
if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
|
|
44
|
+
const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
|
|
45
|
+
if (!code) return next()
|
|
46
|
+
return await executeCode.call(this, code, req)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|