@sap/cds 5.7.5 → 5.8.2
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 +97 -0
- package/app/fiori/routes.js +1 -1
- package/bin/deploy/to-hana/cfUtil.js +251 -138
- package/bin/deploy/to-hana/gitUtil.js +55 -0
- package/bin/deploy/to-hana/hana.js +92 -93
- package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
- package/bin/deploy/to-hana/index.js +14 -13
- package/bin/mtx/in-cds.js +1 -0
- package/bin/serve.js +1 -1
- package/bin/version.js +1 -0
- package/lib/compile/cdsc.js +0 -6
- package/lib/compile/resolve.js +1 -1
- package/lib/compile/to/srvinfo.js +1 -1
- package/lib/core/classes.js +21 -1
- package/lib/env/index.js +3 -2
- package/lib/env/requires.js +4 -0
- package/lib/i18n/localize.js +5 -8
- package/lib/index.js +1 -0
- package/lib/log/errors.js +1 -1
- package/lib/log/format/kibana.js +3 -3
- package/lib/ql/SELECT.js +2 -2
- package/lib/req/cds-context.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/serve/Transaction.js +9 -5
- package/lib/serve/index.js +13 -21
- package/lib/utils/tests.js +90 -66
- package/libx/_runtime/audit/generic/personal/modification.js +0 -8
- package/libx/_runtime/auth/index.js +7 -6
- package/libx/_runtime/auth/strategies/dwc.js +43 -0
- package/libx/_runtime/auth/utils.js +24 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
- package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
- package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
- package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
- package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
- package/libx/_runtime/common/aspects/Association.js +16 -0
- package/libx/_runtime/common/composition/data.js +28 -37
- package/libx/_runtime/common/composition/delete.js +107 -58
- package/libx/_runtime/common/composition/index.js +3 -3
- package/libx/_runtime/common/composition/insert.js +14 -27
- package/libx/_runtime/common/composition/tree.js +1 -1
- package/libx/_runtime/common/composition/update.js +39 -34
- package/libx/_runtime/common/error/frontend.js +19 -5
- package/libx/_runtime/common/generic/auth.js +20 -85
- package/libx/_runtime/common/generic/crud.js +22 -1
- package/libx/_runtime/common/i18n/messages.properties +2 -1
- package/libx/_runtime/common/utils/cqn.js +2 -6
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
- package/libx/_runtime/common/utils/csn.js +29 -6
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -1
- package/libx/_runtime/common/utils/keys.js +2 -1
- package/libx/_runtime/common/utils/path.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +12 -4
- package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
- package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
- package/libx/_runtime/common/utils/structured.js +10 -4
- package/libx/_runtime/common/utils/vcap.js +27 -10
- package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
- package/libx/_runtime/db/expand/expand-v2.js +21 -12
- package/libx/_runtime/db/expand/expandCQNToJoin.js +67 -26
- package/libx/_runtime/db/expand/index.js +3 -0
- package/libx/_runtime/db/generic/create.js +0 -10
- package/libx/_runtime/db/generic/index.js +3 -0
- package/libx/_runtime/db/generic/read.js +2 -24
- package/libx/_runtime/db/generic/rewrite.js +1 -3
- package/libx/_runtime/db/generic/update.js +1 -1
- package/libx/_runtime/db/query/delete.js +10 -4
- package/libx/_runtime/db/query/insert.js +3 -4
- package/libx/_runtime/db/query/read.js +4 -1
- package/libx/_runtime/db/query/update.js +5 -5
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
- package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
- package/libx/_runtime/db/sql-builder/index.js +3 -0
- package/libx/_runtime/db/utils/columns.js +5 -2
- package/libx/_runtime/db/utils/deep.js +16 -14
- package/libx/_runtime/db/utils/generateAliases.js +56 -6
- package/libx/_runtime/fiori/generic/before.js +73 -49
- package/libx/_runtime/fiori/generic/edit.js +14 -18
- package/libx/_runtime/fiori/generic/patch.js +8 -11
- package/libx/_runtime/fiori/generic/read.js +20 -19
- package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
- package/libx/_runtime/fiori/utils/handler.js +1 -11
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/conversion.js +12 -1
- package/libx/_runtime/hana/dynatrace.js +11 -5
- package/libx/_runtime/hana/execute.js +132 -19
- package/libx/_runtime/hana/search.js +3 -3
- package/libx/_runtime/hana/search2cqn4sql.js +23 -25
- package/libx/_runtime/hana/searchToContains.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
- package/libx/_runtime/messaging/file-based.js +3 -1
- package/libx/_runtime/messaging/service.js +4 -1
- package/libx/_runtime/remote/utils/client.js +41 -24
- package/libx/_runtime/remote/utils/data.js +54 -12
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/conversion.js +10 -0
- package/libx/_runtime/types/api.js +2 -2
- package/libx/gql/resolvers/crud/update.js +8 -5
- package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
- package/libx/odata/afterburner.js +29 -6
- package/libx/odata/cqn2odata.js +9 -0
- package/libx/odata/grammar.pegjs +49 -21
- package/libx/odata/index.js +2 -2
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +2 -2
- package/libx/rest/RestAdapter.js +29 -1
- package/libx/rest/middleware/auth.js +1 -3
- package/libx/rest/middleware/parse.js +1 -0
- package/package.json +1 -1
- package/server.js +1 -1
- package/bin/deploy/to-hana/logger.js +0 -27
- package/bin/deploy/to-hana/runCommand.js +0 -113
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
- package/libx/_runtime/common/utils/auth.js +0 -16
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
|
|
2
|
-
const generateAliases = require('../utils/generateAliases')
|
|
2
|
+
const { generateAliases } = require('../utils/generateAliases')
|
|
3
3
|
const { restoreLink } = require('../../common/utils/resolveView')
|
|
4
4
|
|
|
5
5
|
const _isLinked = req => {
|
|
@@ -18,7 +18,6 @@ function handler(req) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const streaming = req.query._streaming
|
|
21
|
-
const validationQuery = req.query._validationQuery
|
|
22
21
|
|
|
23
22
|
// for restore link to req.data
|
|
24
23
|
const linked = _isLinked(req)
|
|
@@ -31,7 +30,6 @@ function handler(req) {
|
|
|
31
30
|
if (linked) restoreLink(req)
|
|
32
31
|
|
|
33
32
|
if (streaming) req.query._streaming = streaming
|
|
34
|
-
if (validationQuery) req.query._validationQuery = validationQuery
|
|
35
33
|
|
|
36
34
|
generateAliases(req.query)
|
|
37
35
|
}
|
|
@@ -70,7 +70,7 @@ module.exports = async function (req) {
|
|
|
70
70
|
if (onlyKeysRemain(req)) return
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
|
-
const result = await this._update(this.model, this.dbc, req
|
|
73
|
+
const result = await this._update(this.model, this.dbc, req)
|
|
74
74
|
return result
|
|
75
75
|
} catch (err) {
|
|
76
76
|
// REVISIT: db specifics
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
const { getFlatArray, processCQNs } = require('../utils/deep')
|
|
2
2
|
const { timestampToISO } = require('../data-conversion/timestamp')
|
|
3
|
-
const { hasDeepDelete, getDeepDeleteCQNs } = require('../../common/composition')
|
|
3
|
+
const { hasDeepDelete, getDeepDeleteCQNs, getSetNullParentForeignKeyCQNs } = require('../../common/composition')
|
|
4
4
|
|
|
5
|
-
const deleteFn = executeDeleteCQN => async (model, dbc, query, req) => {
|
|
5
|
+
const deleteFn = (executeDeleteCQN, executeUpdateCQN) => async (model, dbc, query, req) => {
|
|
6
6
|
const { user, locale, timestamp } = req
|
|
7
7
|
const isoTs = timestampToISO(timestamp)
|
|
8
8
|
|
|
9
9
|
let result
|
|
10
|
-
if (hasDeepDelete(model
|
|
11
|
-
let cqns = getDeepDeleteCQNs(model
|
|
10
|
+
if (hasDeepDelete(model, query)) {
|
|
11
|
+
let cqns = await getDeepDeleteCQNs(model, req)
|
|
12
12
|
|
|
13
13
|
// the delete chunks, i.e., how many deletes can be processed in parallel
|
|
14
14
|
const chunks = []
|
|
@@ -25,6 +25,12 @@ const deleteFn = executeDeleteCQN => async (model, dbc, query, req) => {
|
|
|
25
25
|
result = results[results.length - 1]
|
|
26
26
|
} else {
|
|
27
27
|
result = await executeDeleteCQN(model, dbc, query, user, locale, isoTs)
|
|
28
|
+
if (result) {
|
|
29
|
+
const updateCQNs = await getSetNullParentForeignKeyCQNs(model, req)
|
|
30
|
+
for (const cqn of updateCQNs) {
|
|
31
|
+
await executeUpdateCQN(model, dbc, cqn, user, locale, isoTs)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
return result
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { hasDeepInsert, getDeepInsertCQNs
|
|
1
|
+
const { hasDeepInsert, getDeepInsertCQNs } = require('../../common/composition')
|
|
2
2
|
const { getFlatArray, processCQNs } = require('../utils/deep')
|
|
3
3
|
const { timestampToISO } = require('../data-conversion/timestamp')
|
|
4
4
|
|
|
@@ -6,8 +6,8 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
|
|
|
6
6
|
const { user, locale, timestamp } = req
|
|
7
7
|
const isoTs = timestampToISO(timestamp)
|
|
8
8
|
|
|
9
|
-
if (hasDeepInsert(model
|
|
10
|
-
const cqns = getFlatArray(getDeepInsertCQNs(model
|
|
9
|
+
if (hasDeepInsert(model, query)) {
|
|
10
|
+
const cqns = getFlatArray(getDeepInsertCQNs(model, query))
|
|
11
11
|
|
|
12
12
|
// return array of individual results
|
|
13
13
|
if (cqns.length === 0) return []
|
|
@@ -15,7 +15,6 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
|
|
|
15
15
|
return getFlatArray(results)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
cleanEmptyCompositionsOfMany(model && model.definitions, query)
|
|
19
18
|
return executeInsertCQN(model, dbc, query, user, locale, isoTs)
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const { timestampToISO } = require('../data-conversion/timestamp')
|
|
2
2
|
|
|
3
|
+
const { deepCopyObject } = require('../../common/utils/copy')
|
|
4
|
+
|
|
3
5
|
function _arrayWithCount(a, count) {
|
|
4
6
|
const _map = a.map
|
|
5
7
|
const map = (..._) => _arrayWithCount(_map.call(a, ..._), count)
|
|
@@ -10,7 +12,8 @@ function _arrayWithCount(a, count) {
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
function _createCountQuery(query) {
|
|
13
|
-
|
|
15
|
+
// REVISIT: Use query.clone() instead
|
|
16
|
+
let _query = { SELECT: deepCopyObject(query.SELECT) }
|
|
14
17
|
delete _query.SELECT.orderBy // not necessary to keep that
|
|
15
18
|
delete _query.SELECT.limit
|
|
16
19
|
// Also change columns in sub queries
|
|
@@ -50,11 +50,11 @@ const _getFilteredCqns = (cqns, model) => {
|
|
|
50
50
|
return cqns
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const update =
|
|
54
|
-
const { user, locale, timestamp } = req
|
|
53
|
+
const update = executeUpdateCQN => async (model, dbc, req) => {
|
|
54
|
+
const { query, user, locale, timestamp } = req
|
|
55
55
|
const isoTs = timestampToISO(timestamp)
|
|
56
56
|
|
|
57
|
-
if (hasDeepUpdate(model
|
|
57
|
+
if (hasDeepUpdate(model, query)) {
|
|
58
58
|
// REVISIT: _activeData gets set in case of draftActivate for performance, but this is a layer violation
|
|
59
59
|
let selectData = req._ && req._.query && req._.query._activeData
|
|
60
60
|
|
|
@@ -62,10 +62,10 @@ const update = (executeUpdateCQN, executeSelectCQN) => async (model, dbc, query,
|
|
|
62
62
|
selectData = [selectData]
|
|
63
63
|
} else {
|
|
64
64
|
// REVISIT: avoid additional read
|
|
65
|
-
selectData = await selectDeepUpdateData(
|
|
65
|
+
selectData = await selectDeepUpdateData(cds.db, model, req)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
let cqns = getDeepUpdateCQNs(model
|
|
68
|
+
let cqns = await getDeepUpdateCQNs(model, req, selectData)
|
|
69
69
|
|
|
70
70
|
// the delete chunks, i.e., how many deletes can be processed in parallel
|
|
71
71
|
const chunks = []
|
|
@@ -11,6 +11,10 @@ function _fillAfterDot(val) {
|
|
|
11
11
|
return `${beforeDot}.${afterDot.padEnd(3, '0')}`
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function _valButNoBuffer(arg) {
|
|
15
|
+
return arg && arg.val && typeof arg.val === 'object' && !(arg.val instanceof Buffer)
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
* ExpressionBuilder is used to take a part of a CQN object as an input and to build an object representing an expression
|
|
16
20
|
* with SQL string and values to be used with a prepared statement.
|
|
@@ -87,9 +91,9 @@ class ExpressionBuilder extends BaseBuilder {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
_isStructured(op1, comp, op2) {
|
|
90
|
-
if (op1.ref && comp === '=' &&
|
|
94
|
+
if (op1.ref && comp === '=' && _valButNoBuffer(op2)) return true
|
|
91
95
|
// also check reverse
|
|
92
|
-
if (op1
|
|
96
|
+
if (_valButNoBuffer(op1) && comp === '=' && op2.ref) return true
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
_expressionObjectsToSQL(objects) {
|
|
@@ -379,4 +383,7 @@ class ExpressionBuilder extends BaseBuilder {
|
|
|
379
383
|
}
|
|
380
384
|
}
|
|
381
385
|
|
|
386
|
+
/*
|
|
387
|
+
* this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
|
|
388
|
+
*/
|
|
382
389
|
module.exports = ExpressionBuilder
|
|
@@ -96,7 +96,7 @@ class SelectBuilder extends BaseBuilder {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (this._obj.SELECT.orderBy && this._obj.SELECT.orderBy.length) {
|
|
99
|
-
this._orderBy()
|
|
99
|
+
this._orderBy(noQuoting)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (this._obj.SELECT.limit || this._obj.SELECT.one) {
|
|
@@ -373,7 +373,11 @@ class SelectBuilder extends BaseBuilder {
|
|
|
373
373
|
this._outputObj.values.push(...values)
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
_getOrderByElement(noQuoting, name, element) {
|
|
377
|
+
return (noQuoting ? name : this._quote(name)) + ' ' + (element.sort || 'asc').toUpperCase()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_orderBy(noQuoting) {
|
|
377
381
|
const sqls = []
|
|
378
382
|
this._outputObj.sql.push('ORDER BY')
|
|
379
383
|
for (const element of this._obj.SELECT.orderBy) {
|
|
@@ -385,7 +389,7 @@ class SelectBuilder extends BaseBuilder {
|
|
|
385
389
|
if (!columns.find(c => JSON.stringify(c.ref) === serialized)) {
|
|
386
390
|
const toMatch = element.as || (element.ref && element.ref.length === 1 && element.ref[0])
|
|
387
391
|
if (toMatch && columns.find(c => c.as === toMatch)) {
|
|
388
|
-
sqls.push(this.
|
|
392
|
+
sqls.push(this._getOrderByElement(noQuoting, toMatch, element))
|
|
389
393
|
continue
|
|
390
394
|
}
|
|
391
395
|
}
|
|
@@ -9,6 +9,9 @@ const ReferenceBuilder = require('./ReferenceBuilder')
|
|
|
9
9
|
const FunctionBuilder = require('./FunctionBuilder')
|
|
10
10
|
const sqlFactory = require('./sqlFactory')
|
|
11
11
|
|
|
12
|
+
/*
|
|
13
|
+
* this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
|
|
14
|
+
*/
|
|
12
15
|
module.exports = {
|
|
13
16
|
CreateBuilder,
|
|
14
17
|
DropBuilder,
|
|
@@ -11,7 +11,7 @@ const { DRAFT_COLUMNS } = require('../../common/constants/draft')
|
|
|
11
11
|
* @param entity - the csn entity
|
|
12
12
|
* @returns {Array} - array of columns
|
|
13
13
|
*/
|
|
14
|
-
const getColumns = (entity, {
|
|
14
|
+
const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }) => {
|
|
15
15
|
// REVISIT is this correct or just a problem that occurs because of new structure we do not deal with yet?
|
|
16
16
|
if (!(entity && entity.elements)) return []
|
|
17
17
|
const columnNames = []
|
|
@@ -21,7 +21,7 @@ const getColumns = (entity, { db, onlyKeys } = { db: true, onlyKeys: false }) =>
|
|
|
21
21
|
const element = elements[elementName]
|
|
22
22
|
if (onlyKeys && !element.key) continue
|
|
23
23
|
if (element.isAssociation) continue
|
|
24
|
-
if (
|
|
24
|
+
if (_4db && entity._isDraftEnabled && DRAFT_COLUMNS.includes(elementName)) continue
|
|
25
25
|
if (cds.env.effective.odata.structs && element.elements) {
|
|
26
26
|
columnNames.push(...resolveStructured({ structName: elementName, structProperties: [] }, element.elements, false))
|
|
27
27
|
continue
|
|
@@ -31,4 +31,7 @@ const getColumns = (entity, { db, onlyKeys } = { db: true, onlyKeys: false }) =>
|
|
|
31
31
|
return columnNames.map(name => elements[name] || { name })
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/*
|
|
35
|
+
* this module is required by cds-pg. -> in case of incompatible changes, we should let them know.
|
|
36
|
+
*/
|
|
34
37
|
module.exports = getColumns
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const _flattenDeep = (arr, res) => {
|
|
2
|
+
if (!Array.isArray(arr)) {
|
|
3
|
+
res.push(arr)
|
|
4
|
+
return res
|
|
5
|
+
}
|
|
6
|
+
for (const a of arr) {
|
|
7
|
+
_flattenDeep(a, res)
|
|
8
|
+
}
|
|
9
|
+
return res
|
|
3
10
|
}
|
|
4
11
|
|
|
5
12
|
/*
|
|
6
13
|
* flatten with a dfs approach. this is important!!!
|
|
7
14
|
*/
|
|
8
|
-
|
|
9
|
-
if (!Array.isArray(arg)) return [arg]
|
|
10
|
-
return _flattenDeep(arg)
|
|
11
|
-
}
|
|
15
|
+
const getFlatArray = arg => _flattenDeep(arg, [])
|
|
12
16
|
|
|
13
17
|
async function _processChunk(processFn, model, dbc, cqns, user, locale, ts, indexes, results) {
|
|
14
18
|
const promises = []
|
|
@@ -28,20 +32,14 @@ async function processCQNs(processFn, cqns, model, dbc, user, locale, ts, chunks
|
|
|
28
32
|
const results = new Array(cqns.length)
|
|
29
33
|
|
|
30
34
|
const deletes = []
|
|
31
|
-
const
|
|
35
|
+
const updatesForDeletes = []
|
|
32
36
|
const others = []
|
|
33
37
|
for (let i = 0; i < cqns.length; i++) {
|
|
34
38
|
if (cqns[i].DELETE) deletes.push(i)
|
|
35
|
-
else if (cqns[i].
|
|
39
|
+
else if (cqns[i].__4delete) updatesForDeletes.push(i)
|
|
36
40
|
else others.push(i)
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
// UPDATEs to SET null parent's foreign keys of one compositions
|
|
40
|
-
// which are otherwise violate foreign key constraints
|
|
41
|
-
if (updatesBeforeDelete.length) {
|
|
42
|
-
await _processChunk(processFn, model, dbc, cqns, user, locale, ts, updatesBeforeDelete, results)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
43
|
if (deletes.length > 0) {
|
|
46
44
|
if (chunks) {
|
|
47
45
|
let offset = 0
|
|
@@ -55,6 +53,10 @@ async function processCQNs(processFn, cqns, model, dbc, user, locale, ts, chunks
|
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
55
|
|
|
56
|
+
if (updatesForDeletes.length > 0) {
|
|
57
|
+
await _processChunk(processFn, model, dbc, cqns, user, locale, ts, updatesForDeletes, results)
|
|
58
|
+
}
|
|
59
|
+
|
|
58
60
|
if (others.length > 0) {
|
|
59
61
|
await _processChunk(processFn, model, dbc, cqns, user, locale, ts, others, results)
|
|
60
62
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
const { ensureUnlocalized } = require('../../common/utils/draft')
|
|
2
|
-
|
|
3
2
|
const ALIAS_PREFIX = 'ALIAS_'
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const PARENT_ALIAS = '_parent_'
|
|
4
|
+
const FOREIGN_ALIAS = '_foreign_'
|
|
5
|
+
const PARENT_ALIAS_REGEX = new RegExp('^' + PARENT_ALIAS + '\\d*$')
|
|
6
|
+
const cleanUpName = name => ensureUnlocalized(name).replace(/\./g, '_')
|
|
8
7
|
|
|
9
8
|
const _redirectXpr = (xpr, aliasMap) => {
|
|
10
9
|
if (!xpr) return
|
|
@@ -97,4 +96,55 @@ const generateAliases = query => {
|
|
|
97
96
|
_generateAliases(query)
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
|
|
99
|
+
const _addParentAlias = (where, alias) => {
|
|
100
|
+
where.forEach(e => {
|
|
101
|
+
if (e.ref && e.ref[0].match(PARENT_ALIAS_REGEX)) {
|
|
102
|
+
e.ref[0] = alias
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const _addAliasToElement = (expr, alias) => {
|
|
108
|
+
if (expr.ref) {
|
|
109
|
+
if (typeof alias === 'function') {
|
|
110
|
+
alias = alias(expr.ref)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { ref: [alias, ...expr.ref] }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (expr.list) {
|
|
117
|
+
return { list: expr.list.map(arg => _addAliasToElement(arg, alias)) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (expr.func) {
|
|
121
|
+
const args = expr.args.map(arg => _addAliasToElement(arg, alias))
|
|
122
|
+
return { ...expr, args }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (expr.SELECT && expr.SELECT.where) {
|
|
126
|
+
// special case in lambda functions
|
|
127
|
+
_addParentAlias(expr.SELECT.where, alias)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (expr.xpr) {
|
|
131
|
+
return { xpr: expr.xpr.map(xpr => _addAliasToElement(xpr, alias)) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return expr
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const addAliasToExpression = (expression, alias) => {
|
|
138
|
+
if (expression && alias) {
|
|
139
|
+
return expression.map(expr => _addAliasToElement(expr, alias))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return expression
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
generateAliases,
|
|
147
|
+
addAliasToExpression,
|
|
148
|
+
PARENT_ALIAS,
|
|
149
|
+
FOREIGN_ALIAS
|
|
150
|
+
}
|
|
@@ -12,9 +12,7 @@ const { DRAFT_COLUMNS_ADMIN } = require('../../common/constants/draft')
|
|
|
12
12
|
|
|
13
13
|
// copied from adapter/odata-v4/utils/context-object
|
|
14
14
|
const _getTargetEntityName = (service, pathSegments) => {
|
|
15
|
-
if (isCustomOperation(pathSegments, false))
|
|
16
|
-
return undefined
|
|
17
|
-
}
|
|
15
|
+
if (isCustomOperation(pathSegments, false)) return
|
|
18
16
|
|
|
19
17
|
let navSegmentName
|
|
20
18
|
let entityName = `${service.name}.${pathSegments[0].getEntitySet().getName()}`
|
|
@@ -36,12 +34,11 @@ const _getTargetEntityName = (service, pathSegments) => {
|
|
|
36
34
|
* @returns {object}
|
|
37
35
|
* @private
|
|
38
36
|
*/
|
|
39
|
-
const _getParent = (
|
|
37
|
+
const _getParent = (req, service) => {
|
|
40
38
|
// REVISIT: get rid of getUriInfo
|
|
41
39
|
if (!req.getUriInfo) return
|
|
42
40
|
|
|
43
41
|
const segments = req.getUriInfo().getPathSegments()
|
|
44
|
-
|
|
45
42
|
if (segments.length === 1) return
|
|
46
43
|
|
|
47
44
|
const parent = {
|
|
@@ -50,6 +47,7 @@ const _getParent = (service, req) => {
|
|
|
50
47
|
|
|
51
48
|
const parentKeyPredicates = segments[segments.length - 2].getKeyPredicates()
|
|
52
49
|
let keyPredicateName, keyPredicateText
|
|
50
|
+
|
|
53
51
|
for (const keyPredicate of parentKeyPredicates) {
|
|
54
52
|
keyPredicateName = keyPredicate.getEdmRef().getName()
|
|
55
53
|
keyPredicateText = keyPredicate.getText()
|
|
@@ -65,36 +63,28 @@ const _getParent = (service, req) => {
|
|
|
65
63
|
return parent
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
const _validateDraft = (draftResult,
|
|
69
|
-
if (!draftResult || draftResult.length === 0)
|
|
70
|
-
req.reject(404)
|
|
71
|
-
}
|
|
72
|
-
|
|
66
|
+
const _validateDraft = (req, draftResult, isBoundAction) => {
|
|
67
|
+
if (!draftResult || draftResult.length === 0) req.reject(404)
|
|
73
68
|
const draftAdminData = draftResult[0]
|
|
74
69
|
|
|
75
|
-
// the same user that locked the entity can always delete it
|
|
76
|
-
if (draftAdminData.InProcessByUser === req.user.id)
|
|
77
|
-
return
|
|
78
|
-
}
|
|
70
|
+
// the same user that locked the entity can always delete/update it
|
|
71
|
+
if (draftAdminData.InProcessByUser === req.user.id) return
|
|
79
72
|
|
|
80
|
-
// proceed with the delete action only if it was initiated by a different
|
|
81
|
-
// than the one who locked the entity and the configured drafts cancellation
|
|
73
|
+
// proceed with the delete/update action only if it was initiated by a different
|
|
74
|
+
// user than the one who locked the entity and the configured drafts cancellation
|
|
82
75
|
// timeout timer has expired
|
|
83
76
|
if (draftIsLocked(draftAdminData.LastChangeDateTime)) {
|
|
84
77
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
85
78
|
}
|
|
79
|
+
|
|
80
|
+
// At this point, the request user ID isn't the owner of the draft.
|
|
81
|
+
if (isBoundAction) req.reject(403)
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
const _addDraftDataToContext = (req, result) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (req.
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!req._draftMetadata) {
|
|
96
|
-
req._draftMetadata = {}
|
|
97
|
-
}
|
|
85
|
+
if (!result || result.length === 0) return
|
|
86
|
+
if (req.rejected) return
|
|
87
|
+
if (!req._draftMetadata) req._draftMetadata = {}
|
|
98
88
|
|
|
99
89
|
DRAFT_COLUMNS_ADMIN.forEach(column => {
|
|
100
90
|
if (column in result[0]) req._draftMetadata[column] = result[0][column]
|
|
@@ -103,11 +93,7 @@ const _addDraftDataToContext = (req, result) => {
|
|
|
103
93
|
req.data.DraftAdministrativeData_DraftUUID = result[0].DraftUUID
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
const _prefixDraftColumns = () => {
|
|
107
|
-
return DRAFT_COLUMNS_ADMIN.map(col => {
|
|
108
|
-
return { ref: ['DRAFT_DraftAdministrativeData', col] }
|
|
109
|
-
})
|
|
110
|
-
}
|
|
96
|
+
const _prefixDraftColumns = () => DRAFT_COLUMNS_ADMIN.map(col => ({ ref: ['DRAFT_DraftAdministrativeData', col] }))
|
|
111
97
|
|
|
112
98
|
const _getSelectDraftDataCqn = (entityName, where) => {
|
|
113
99
|
return SELECT.from(ensureDraftsSuffix(entityName), _prefixDraftColumns())
|
|
@@ -116,27 +102,41 @@ const _getSelectDraftDataCqn = (entityName, where) => {
|
|
|
116
102
|
.where(where)
|
|
117
103
|
}
|
|
118
104
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
105
|
+
const _getDraftDataFromExistingDraft = async (req, service, parent = _getParent(req, service)) => {
|
|
106
|
+
if (parent) {
|
|
107
|
+
if (parent.IsActiveEntity === 'false') {
|
|
108
|
+
const parentWhere = [{ ref: [parent.keyName] }, '=', { val: parent.keyValue }]
|
|
109
|
+
const query = _getSelectDraftDataCqn(parent.entityName, parentWhere)
|
|
110
|
+
const result = await cds.tx(req).run(query)
|
|
111
|
+
return result
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return []
|
|
128
115
|
}
|
|
129
116
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
const rootWhere = getKeysCondition(req.target, req.data)
|
|
118
|
+
const query = _getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere)
|
|
119
|
+
const result = await cds.tx(req).run(query)
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const _addDraftDataFromExistingDraft = async (req, service) => {
|
|
124
|
+
const parent = _getParent(req, service)
|
|
125
|
+
const result = await _getDraftDataFromExistingDraft(req, service, parent)
|
|
126
|
+
|
|
127
|
+
if (parent) {
|
|
128
|
+
if (parent.IsActiveEntity === 'false') {
|
|
129
|
+
_validateDraft(req, result)
|
|
134
130
|
_addDraftDataToContext(req, result)
|
|
131
|
+
return result
|
|
135
132
|
}
|
|
136
|
-
|
|
133
|
+
|
|
134
|
+
return []
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
if (result && result.length > 0) _validateDraft(req, result)
|
|
138
|
+
_addDraftDataToContext(req, result)
|
|
139
|
+
return result
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const _addGeneratedDraftUUID = async req => {
|
|
@@ -156,11 +156,12 @@ const _new = async function (req) {
|
|
|
156
156
|
if (isNavigationToMany(req)) {
|
|
157
157
|
const result = await _addDraftDataFromExistingDraft(req, this)
|
|
158
158
|
|
|
159
|
-
// in order to fix
|
|
159
|
+
// in order to fix corner case where active subitems are created in draft case
|
|
160
160
|
if (result.length === 0) req.reject(404)
|
|
161
|
-
|
|
162
|
-
_addGeneratedDraftUUID(req)
|
|
161
|
+
return
|
|
163
162
|
}
|
|
163
|
+
|
|
164
|
+
_addGeneratedDraftUUID(req)
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
/**
|
|
@@ -173,7 +174,7 @@ const _patchUpdate = async function (req) {
|
|
|
173
174
|
|
|
174
175
|
const result = await _addDraftDataFromExistingDraft(req, this)
|
|
175
176
|
|
|
176
|
-
// means that draft not exists
|
|
177
|
+
// means that the draft does not exists
|
|
177
178
|
if (result.length === 0) req.reject(404)
|
|
178
179
|
}
|
|
179
180
|
|
|
@@ -186,6 +187,28 @@ const _deleteCancel = async function (req) {
|
|
|
186
187
|
await _addDraftDataFromExistingDraft(req, this)
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
const _validateDraftBoundAction = async function (req, srv) {
|
|
191
|
+
const result = await _getDraftDataFromExistingDraft(req, srv)
|
|
192
|
+
const isBoundAction = true
|
|
193
|
+
if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const _registerBoundActionHandlers = function (entityName, actions) {
|
|
197
|
+
if (!actions) return
|
|
198
|
+
|
|
199
|
+
const boundActions = Object.values(actions).filter(
|
|
200
|
+
action =>
|
|
201
|
+
action.kind === 'action' &&
|
|
202
|
+
action.name !== 'draftPrepare' &&
|
|
203
|
+
action.name !== 'draftEdit' &&
|
|
204
|
+
action.name !== 'draftActivate'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
for (const action of boundActions) {
|
|
208
|
+
this.before(action.name, entityName, req => _validateDraftBoundAction(req, this))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
189
212
|
module.exports = cds.service.impl(function () {
|
|
190
213
|
_new._initial = true
|
|
191
214
|
_patchUpdate._initial = true
|
|
@@ -196,5 +219,6 @@ module.exports = cds.service.impl(function () {
|
|
|
196
219
|
this.before('NEW', entity, _new)
|
|
197
220
|
this.before(['PATCH', 'UPDATE'], entity, _patchUpdate)
|
|
198
221
|
this.before(['DELETE', 'CANCEL'], entity, _deleteCancel)
|
|
222
|
+
_registerBoundActionHandlers.call(this, entity.name, entity.actions)
|
|
199
223
|
}
|
|
200
224
|
})
|
|
@@ -56,21 +56,18 @@ const _getLockWhere = (where, columnsMap) => {
|
|
|
56
56
|
return lockWhere
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const _select = async (
|
|
60
|
-
let allResults
|
|
61
|
-
|
|
59
|
+
const _select = async (lockRecordCQN, draftExistsCQN, selectCQNs, req, dbtx) => {
|
|
62
60
|
try {
|
|
63
|
-
|
|
64
|
-
} catch (
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
req.reject(err)
|
|
61
|
+
await dbtx.run(lockRecordCQN)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
const drafts = await dbtx.run(draftExistsCQN)
|
|
64
|
+
if (drafts.length) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
65
|
+
req.reject(409, 'ENTITY_LOCKED')
|
|
71
66
|
}
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
const promisesResults = await Promise.allSettled([dbtx.run(draftExistsCQN), ...selectCQNs.map(cqn => dbtx.run(cqn))])
|
|
68
|
+
const firstRejected = promisesResults.find(r => r.status === 'rejected')
|
|
69
|
+
if (firstRejected) req.reject(firstRejected.reason)
|
|
70
|
+
return promisesResults.map(r => r.value)
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
/**
|
|
@@ -127,11 +124,9 @@ const _handler = async function (req) {
|
|
|
127
124
|
}
|
|
128
125
|
}
|
|
129
126
|
|
|
130
|
-
const lockAndSelectCQNs = [lockRecordCQN, draftExistsCQN, ...selectCQNs]
|
|
131
|
-
|
|
132
127
|
const dbtx = cds.tx(req)
|
|
133
128
|
// REVISIT: Use service.read with expand **
|
|
134
|
-
const [
|
|
129
|
+
const [draftExists, ...results] = await _select(lockRecordCQN, draftExistsCQN, [...selectCQNs], req, dbtx)
|
|
135
130
|
|
|
136
131
|
if (!results[0].length) {
|
|
137
132
|
req.reject(404)
|
|
@@ -169,12 +164,13 @@ const _handler = async function (req) {
|
|
|
169
164
|
|
|
170
165
|
await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
|
|
171
166
|
setStatusCodeAndHeader(req._.odataRes, rootWhere, req.target.name.replace(`${this.name}.`, ''), false)
|
|
172
|
-
|
|
173
167
|
return Object.assign({}, results[0][0], { HasDraftEntity: false, HasActiveEntity: true, IsActiveEntity: false })
|
|
174
168
|
}
|
|
175
169
|
|
|
176
170
|
module.exports = cds.service.impl(function () {
|
|
177
|
-
|
|
171
|
+
const entities = Object.values(this.entities).filter(e => e._isDraftEnabled)
|
|
172
|
+
|
|
173
|
+
for (const entity of entities) {
|
|
178
174
|
this.on('EDIT', entity, _handler)
|
|
179
175
|
}
|
|
180
176
|
})
|