@sap/cds 6.8.4 → 7.0.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 +66 -4
- package/README.md +0 -1
- package/bin/cds-serve.js +50 -3
- package/bin/deploy/to-hana.js +1 -0
- package/bin/serve.js +16 -20
- package/lib/auth/basic-auth.js +6 -4
- package/lib/auth/index.js +4 -3
- package/lib/auth/jwt-auth.js +2 -5
- package/lib/compile/cds-compile.js +34 -89
- package/lib/compile/cdsc.js +11 -0
- package/lib/compile/etc/properties.js +2 -2
- package/lib/compile/for/lean_drafts.js +36 -69
- package/lib/compile/for/nodejs.js +2 -1
- package/lib/compile/load.js +3 -3
- package/lib/compile/minify.js +2 -0
- package/lib/compile/to/csn.js +74 -0
- package/{bin/build/provider/hana/2tabledata.js → lib/compile/to/hdbtabledata.js} +4 -6
- package/lib/compile/to/json.js +1 -1
- package/lib/compile/to/sql.js +8 -6
- package/lib/dbs/cds-deploy.js +174 -114
- package/lib/env/cds-env.js +64 -79
- package/lib/env/cds-requires.js +11 -28
- package/lib/env/defaults.js +13 -3
- package/lib/env/plugins.js +1 -12
- package/lib/env/presets.js +25 -21
- package/lib/index.js +121 -147
- package/lib/{core/reflect.js → linked/models.js} +2 -2
- package/lib/{core/infer.js → linked/queries.js} +2 -0
- package/lib/{core/index.js → linked/types.js} +2 -1
- package/lib/log/cds-error.js +13 -7
- package/lib/log/format/cf.js +1 -1
- package/lib/plugins.js +49 -0
- package/lib/ql/Query.js +0 -9
- package/lib/ql/STREAM.js +0 -1
- package/lib/req/context.js +2 -7
- package/lib/req/request.js +6 -2
- package/lib/req/response.js +23 -10
- package/lib/srv/middlewares/ctx-model.js +2 -2
- package/lib/srv/middlewares/errors.js +1 -1
- package/lib/srv/protocols/_legacy.js +1 -0
- package/lib/srv/protocols/graphql.js +7 -16
- package/lib/srv/protocols/index.js +59 -45
- package/lib/srv/protocols/odata-v2-proxy.js +2 -70
- package/lib/srv/protocols/odata-v4.js +9 -4
- package/lib/srv/srv-api.js +9 -3
- package/lib/srv/srv-dispatch.js +12 -9
- package/lib/srv/srv-models.js +4 -21
- package/lib/srv/srv-tx.js +15 -12
- package/lib/utils/cds-test.js +14 -9
- package/lib/utils/cds-utils.js +2 -12
- package/lib/utils/check-version.js +17 -0
- package/{bin/build → lib/utils}/csv-reader.js +23 -24
- package/libx/_runtime/auth/index.js +27 -23
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +15 -72
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +33 -63
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +14 -18
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +15 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +37 -40
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +7 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +101 -38
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/AbstractError.js +5 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +9 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +15 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +4 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -123
- package/libx/_runtime/cds-services/services/Service.js +79 -107
- package/libx/_runtime/cds-services/services/utils/columns.js +23 -19
- package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -1
- package/libx/_runtime/cds-services/services/utils/differ.js +7 -2
- package/libx/_runtime/cds-services/util/assert.js +65 -2
- package/libx/_runtime/common/composition/data.js +1 -0
- package/libx/_runtime/common/generic/auth/expand.js +1 -1
- package/libx/_runtime/common/generic/auth/restrict.js +5 -10
- package/libx/_runtime/common/generic/auth/restrictions.js +40 -0
- package/libx/_runtime/common/generic/auth/utils.js +1 -2
- package/libx/_runtime/common/generic/crud.js +32 -16
- package/libx/_runtime/common/generic/etag.js +133 -104
- package/libx/_runtime/common/generic/input.js +6 -21
- package/libx/_runtime/common/generic/put.js +1 -1
- package/libx/_runtime/common/generic/stream.js +52 -0
- package/libx/_runtime/common/generic/temporal.js +25 -8
- package/libx/_runtime/common/i18n/messages.properties +0 -2
- package/libx/_runtime/common/utils/cqn.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
- package/libx/_runtime/common/utils/csn.js +0 -51
- package/libx/_runtime/common/utils/etag.js +30 -0
- package/libx/_runtime/common/utils/keys.js +1 -1
- package/libx/_runtime/common/utils/normalizeTimestamp.js +25 -0
- package/libx/_runtime/common/utils/path.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +6 -4
- package/libx/_runtime/common/utils/search2cqn4sql.js +12 -16
- package/libx/_runtime/common/utils/stream.js +140 -0
- package/libx/_runtime/common/utils/streamProp.js +29 -12
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +0 -2
- package/libx/_runtime/db/generic/index.js +0 -2
- package/libx/_runtime/db/query/delete.js +2 -2
- package/libx/_runtime/db/query/insert.js +2 -2
- package/libx/_runtime/db/query/read.js +2 -2
- package/libx/_runtime/db/query/run.js +2 -2
- package/libx/_runtime/db/query/update.js +2 -2
- package/libx/_runtime/db/sql-builder/BaseBuilder.js +0 -6
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +23 -12
- package/libx/_runtime/db/sql-builder/FunctionBuilder.js +18 -6
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -0
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -7
- package/libx/_runtime/db/sql-builder/UpsertBuilder.js +1 -0
- package/libx/_runtime/db/utils/normalizeTimeData.js +7 -3
- package/libx/_runtime/fiori/draft.js +2 -0
- package/libx/_runtime/fiori/generic/activate.js +8 -9
- package/libx/_runtime/fiori/generic/before.js +30 -20
- package/libx/_runtime/fiori/generic/cancel.js +5 -3
- package/libx/_runtime/fiori/generic/delete.js +5 -3
- package/libx/_runtime/fiori/generic/edit.js +7 -7
- package/libx/_runtime/fiori/generic/index.js +10 -16
- package/libx/_runtime/fiori/generic/new.js +5 -3
- package/libx/_runtime/fiori/generic/patch.js +11 -8
- package/libx/_runtime/fiori/generic/prepare.js +13 -6
- package/libx/_runtime/fiori/generic/read.js +12 -6
- package/libx/_runtime/fiori/lean-draft.js +207 -152
- package/libx/_runtime/fiori/utils/delete.js +10 -5
- package/libx/_runtime/fiori/utils/req.js +17 -5
- package/libx/_runtime/fiori/utils/stream.js +36 -0
- package/libx/_runtime/hana/Service.js +12 -9
- package/libx/_runtime/hana/conversion.js +10 -15
- package/libx/_runtime/hana/driver.js +2 -0
- package/libx/_runtime/hana/execute.js +28 -6
- package/libx/_runtime/hana/pool.js +36 -122
- package/libx/_runtime/hana/search2cqn4sql.js +34 -36
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -6
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +10 -58
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/remote/Service.js +20 -1
- package/libx/_runtime/remote/utils/client.js +3 -5
- package/libx/_runtime/sqlite/Service.js +4 -6
- package/libx/_runtime/sqlite/conversion.js +3 -13
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +9 -6
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +6 -1
- package/libx/_runtime/sqlite/execute.js +5 -16
- package/libx/odata/afterburner.js +22 -6
- package/libx/odata/grammar.pegjs +6 -1
- package/libx/odata/parser.js +1 -1
- package/libx/rest/RestAdapter.js +16 -9
- package/libx/rest/RestRequest.js +1 -1
- package/libx/rest/middleware/input.js +2 -1
- package/libx/rest/middleware/operation.js +1 -0
- package/libx/rest/middleware/parse.js +3 -2
- package/libx/rest/middleware/payload.js +9 -8
- package/libx/rest/middleware/read.js +1 -0
- package/package.json +9 -16
- package/server.js +1 -1
- package/app/fiori/preview.js +0 -270
- package/app/fiori/routes.js +0 -59
- package/bin/build/buildTaskEngine.js +0 -360
- package/bin/build/buildTaskFactory.js +0 -283
- package/bin/build/buildTaskHandler.js +0 -241
- package/bin/build/buildTaskProvider.js +0 -22
- package/bin/build/buildTaskProviderFactory.js +0 -175
- package/bin/build/cds.js +0 -5
- package/bin/build/constants.js +0 -66
- package/bin/build/index.js +0 -58
- package/bin/build/provider/buildTaskHandlerEdmx.js +0 -82
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +0 -131
- package/bin/build/provider/buildTaskHandlerInternal.js +0 -254
- package/bin/build/provider/buildTaskProviderInternal.js +0 -383
- package/bin/build/provider/fiori/index.js +0 -171
- package/bin/build/provider/hana/2migration.js +0 -179
- package/bin/build/provider/hana/index.js +0 -505
- package/bin/build/provider/hana/migrationtable.js +0 -472
- package/bin/build/provider/hana/template/.hdiconfig-haas +0 -163
- package/bin/build/provider/hana/template/.hdiconfig-hanacloud +0 -137
- package/bin/build/provider/hana/template/.hdinamespace +0 -4
- package/bin/build/provider/hana/template/package.json +0 -12
- package/bin/build/provider/hana/template/undeploy.json +0 -5
- package/bin/build/provider/java/index.js +0 -111
- package/bin/build/provider/java-cf/index.js +0 -1
- package/bin/build/provider/mtx/index.js +0 -268
- package/bin/build/provider/mtx/resourcesTarBuilder.js +0 -95
- package/bin/build/provider/mtx-extension/index.js +0 -131
- package/bin/build/provider/mtx-sidecar/index.js +0 -137
- package/bin/build/provider/node-cf/index.js +0 -1
- package/bin/build/provider/nodejs/index.js +0 -192
- package/bin/build/util.js +0 -299
- package/bin/cds.js +0 -125
- package/bin/deploy/to-hana/cfUtil.js +0 -355
- package/bin/deploy/to-hana/gitUtil.js +0 -57
- package/bin/deploy/to-hana/hana.js +0 -306
- package/bin/deploy/to-hana/hdiDeployUtil.js +0 -153
- package/bin/deploy/to-hana/index.js +0 -16
- package/bin/deploy/to-hana/mtaUtil.js +0 -170
- package/bin/mtx/in-cds.js +0 -17
- package/bin/plugins.js +0 -32
- package/bin/run.js +0 -24
- package/bin/utils/log.js +0 -24
- package/bin/version.js +0 -178
- package/libx/_runtime/audit/Service.js +0 -222
- package/libx/_runtime/audit/generic/personal/access.js +0 -61
- package/libx/_runtime/audit/generic/personal/index.js +0 -56
- package/libx/_runtime/audit/generic/personal/modification.js +0 -132
- package/libx/_runtime/audit/generic/personal/utils.js +0 -186
- package/libx/_runtime/audit/utils/log.js +0 -23
- package/libx/_runtime/audit/utils/v2.js +0 -176
- package/libx/_runtime/db/data-conversion/timestamp.js +0 -9
- package/libx/_runtime/db/generic/integrity.js +0 -455
- package/srv/audit-log.cds +0 -87
- package/srv/mtx.cds +0 -2
- package/srv/mtx.js +0 -8
- /package/lib/{core → linked}/classes.js +0 -0
- /package/lib/{core → linked}/entities.js +0 -0
|
@@ -146,20 +146,15 @@ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
|
|
|
146
146
|
req.query._draftRestrictions = resolvedApplicables
|
|
147
147
|
return
|
|
148
148
|
}
|
|
149
|
+
// in case of $apply take a query from sub SELECT//
|
|
150
|
+
const query = req.query.SELECT.from.SELECT?.from?.ref ? req.query.SELECT.from : req.query
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
// in case of $apply take a ref from sub SELECT//
|
|
152
|
-
req.query.SELECT.from.ref = _addWheresToRef(
|
|
153
|
-
req.query.SELECT.from.ref || req.query.SELECT.from.SELECT?.from?.ref,
|
|
154
|
-
model,
|
|
155
|
-
resolvedApplicables
|
|
156
|
-
)
|
|
152
|
+
query.SELECT.from.ref = _addWheresToRef(query.SELECT.from.ref, model, resolvedApplicables)
|
|
157
153
|
|
|
158
154
|
const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
|
|
159
155
|
if (!restrictionForTarget) return
|
|
160
156
|
|
|
161
|
-
|
|
162
|
-
req.query.where(restrictionForTarget)
|
|
157
|
+
query.where(restrictionForTarget)
|
|
163
158
|
}
|
|
164
159
|
|
|
165
160
|
const _getFromWithIsActiveEntityRemoved = from => {
|
|
@@ -229,7 +224,7 @@ async function handler(req) {
|
|
|
229
224
|
return
|
|
230
225
|
}
|
|
231
226
|
|
|
232
|
-
let restrictions = this.getRestrictions(definition, req.event, req.user)
|
|
227
|
+
let restrictions = this.getRestrictions.call(this, definition, req.event, req.user)
|
|
233
228
|
if (restrictions instanceof Promise) restrictions = await restrictions
|
|
234
229
|
if (!restrictions) {
|
|
235
230
|
// > unrestricted
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
|
|
2
|
+
const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the applicable restrictions for the current request as follows:
|
|
6
|
+
* - null: unrestricted access
|
|
7
|
+
* - []: no access
|
|
8
|
+
* - [{ grant: '...', to: ['...'], where: '...' }, ...]: applicable restrictions with grant normalized to strings,
|
|
9
|
+
* i.e., grant: ['CREATE', 'UPDATE'] in model becomes [{ grant: 'CREATE' }, { grant: 'UPDATE' }]
|
|
10
|
+
* - Promise resovling to any of the above (needed for CAS overrides)
|
|
11
|
+
*
|
|
12
|
+
* @param {object} definition - then csn definition of an entity or an (un)bound action or function
|
|
13
|
+
* @param {string} event - the event name
|
|
14
|
+
* @param {import('../../../../lib/req/user')} user - the current user
|
|
15
|
+
* @returns {Promise | Array | null}
|
|
16
|
+
*/
|
|
17
|
+
function getRestrictions(definition, event, user) {
|
|
18
|
+
const { model } = this
|
|
19
|
+
let restrictions = getNormalizedRestrictions(definition, model.definitions, event)
|
|
20
|
+
if (!restrictions && (event in CRUD || !definition.parent)) {
|
|
21
|
+
// > unrestricted entity or unbound
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
if (event in CRUD && restrictions.length && restrictions.every(r => r.grant !== '*' && !(r.grant in CRUD))) {
|
|
25
|
+
// > only bounds are restricted
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
if (!(event in CRUD) && !restrictions && definition.parent) {
|
|
29
|
+
// > bound without own restrictions -> get from parent
|
|
30
|
+
restrictions = getNormalizedRestrictions(definition.parent, model.definitions, event)
|
|
31
|
+
if (!restrictions) {
|
|
32
|
+
// > unrestricted bound
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// return the applicable restrictions (grant and to fit to request and user)
|
|
37
|
+
return getApplicableRestrictions(restrictions, event, user)
|
|
38
|
+
}
|
|
39
|
+
|
|
1
40
|
const _getLocalName = definition => {
|
|
2
41
|
return definition._service ? definition.name.replace(`${definition._service.name}.`, '') : definition.name
|
|
3
42
|
}
|
|
@@ -87,6 +126,7 @@ const getNormalizedPlainRestrictions = (restrictions, definition) => {
|
|
|
87
126
|
}
|
|
88
127
|
|
|
89
128
|
module.exports = {
|
|
129
|
+
getRestrictions,
|
|
90
130
|
getNormalizedRestrictions,
|
|
91
131
|
getApplicableRestrictions,
|
|
92
132
|
getNormalizedPlainRestrictions
|
|
@@ -10,11 +10,10 @@ const reject = (req, reason = null) => {
|
|
|
10
10
|
if (req.user._is_anonymous) {
|
|
11
11
|
// REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
|
|
12
12
|
// REVISIT: improve `req.http.req` check if this is an HTTP request
|
|
13
|
-
if (req.http?.
|
|
13
|
+
if (req.http?.res && req.user._challenges && req.user._challenges.length > 0) {
|
|
14
14
|
req.http.res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// REVISIT: security log in else case?
|
|
18
17
|
return req.reject(401)
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -2,25 +2,12 @@ const cds = require('../../cds')
|
|
|
2
2
|
const { SELECT } = cds.ql
|
|
3
3
|
|
|
4
4
|
const { deepCopyArray } = require('../utils/copy')
|
|
5
|
-
|
|
6
5
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
7
|
-
|
|
6
|
+
const { enhanceStreamResult } = require('../utils/stream')
|
|
8
7
|
const getError = require('../error')
|
|
9
8
|
|
|
10
9
|
const _targetEntityDoesNotExist = async req => {
|
|
11
|
-
const
|
|
12
|
-
const cqn = SELECT.from(query.UPDATE.entity, [1])
|
|
13
|
-
|
|
14
|
-
if (query.UPDATE.entity.as) {
|
|
15
|
-
cqn.SELECT.from.as = query.UPDATE.entity.as
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// REVISIT: compat mode for service functions .update
|
|
19
|
-
if (query.UPDATE && query.UPDATE.where) {
|
|
20
|
-
cqn.where(query.UPDATE.where)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const exists = await cds.tx(req).run(cqn)
|
|
10
|
+
const exists = await cds.tx(req).run(SELECT.from(req.subject, [1]))
|
|
24
11
|
return exists.length === 0
|
|
25
12
|
}
|
|
26
13
|
|
|
@@ -80,22 +67,26 @@ exports.impl = cds.service.impl(function () {
|
|
|
80
67
|
result = await cds.tx(req).run(req.query, req.data)
|
|
81
68
|
}
|
|
82
69
|
|
|
70
|
+
// regarding etag validation: we do not want to execute an additional select to distinguish between 412 and 404
|
|
71
|
+
|
|
83
72
|
if (req.event === 'READ') {
|
|
84
73
|
if ((result == null || result.length === 0) && pathExistsQuery) {
|
|
85
74
|
const res = await pathExistsQuery
|
|
86
75
|
if (res.length === 0) req.reject(404)
|
|
87
76
|
}
|
|
77
|
+
if (result == null && req._etagValidationType === 'if-match') req.reject(412)
|
|
88
78
|
return result
|
|
89
79
|
}
|
|
90
80
|
|
|
91
81
|
if (req.event === 'DELETE') {
|
|
92
|
-
if (result === 0) req.reject(404)
|
|
82
|
+
if (result === 0) req.reject(req._etagValidationType ? 412 : 404)
|
|
93
83
|
return result
|
|
94
84
|
}
|
|
95
85
|
|
|
96
86
|
// case: no authorization check and payload more than just keys but no changes
|
|
97
87
|
// -> affected rows === 0 -> no change or not exists?
|
|
98
88
|
if (req.event === 'UPDATE' && result === 0 && !req._authChecked) {
|
|
89
|
+
if (req._etagValidationType) req.reject(412)
|
|
99
90
|
if (await _targetEntityDoesNotExist(req)) req.reject(404)
|
|
100
91
|
}
|
|
101
92
|
|
|
@@ -104,4 +95,29 @@ exports.impl = cds.service.impl(function () {
|
|
|
104
95
|
|
|
105
96
|
return req.data
|
|
106
97
|
})
|
|
98
|
+
|
|
99
|
+
this.after('READ', '*', async function ([result], req) {
|
|
100
|
+
if (req.query?._streaming) {
|
|
101
|
+
await enhanceStreamResult(req, req.query, result, this.model)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
this.on('STREAM', '*', async function (req) {
|
|
106
|
+
if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
|
|
107
|
+
throw getError({
|
|
108
|
+
code: 501,
|
|
109
|
+
message: `Entity "${req.target.name}" is annotated with "@cds.persistence.skip" and cannot be served generically.`
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (req.query.STREAM.from) {
|
|
114
|
+
return { value: await cds.tx(req).run(req.query, req.data) }
|
|
115
|
+
} else {
|
|
116
|
+
return await cds.tx(req).run(req.query, req.data)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
this.after(['STREAM'], '*', async function ([result], req) {
|
|
121
|
+
if (req.query.STREAM.from) await enhanceStreamResult(req, req.query, result, this.model)
|
|
122
|
+
})
|
|
107
123
|
})
|
|
@@ -1,76 +1,66 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { SELECT } = cds.ql
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
const { ensureDraftsSuffix } = require('../../fiori/utils/handler')
|
|
7
|
-
const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
|
|
8
|
-
const { isAsteriskColumn } = require('../../common/utils/rewriteAsterisks')
|
|
9
|
-
const { resolveView } = require('../utils/resolveView')
|
|
10
|
-
|
|
11
|
-
const C_U_ = {
|
|
12
|
-
CREATE: 1,
|
|
13
|
-
UPDATE: 1
|
|
14
|
-
}
|
|
4
|
+
const { convertPathExpressionToWhere } = require('../utils/cqn2cqn4sql')
|
|
5
|
+
const { addEtagColumns } = require('../utils/etag')
|
|
15
6
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return query.UPDATE.entity
|
|
21
|
-
} else {
|
|
22
|
-
return query.DELETE.from
|
|
7
|
+
const _getMatchHeaders = req => {
|
|
8
|
+
return {
|
|
9
|
+
ifMatch: req.headers['if-match']?.replace(/^"\*"$/, '*'),
|
|
10
|
+
ifNoneMatch: req.headers['if-none-match']?.replace(/^"\*"$/, '*')
|
|
23
11
|
}
|
|
24
12
|
}
|
|
25
13
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
const _parseHeaderEtagValue = value => {
|
|
15
|
+
return value.split(',').map(str => {
|
|
16
|
+
let result = str.trim()
|
|
17
|
+
if (result === '*') return result
|
|
18
|
+
if (result.startsWith('W/')) result = result.substring(2)
|
|
19
|
+
return result.startsWith('"') && result.endsWith('"') ? result.substring(1, result.length - 1) : null
|
|
20
|
+
})
|
|
33
21
|
}
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (targetEntity) {
|
|
55
|
-
_addEtagColumns(expand.expand, targetEntity)
|
|
23
|
+
const _getValidationStmt = (ifMatchEtags, ifNoneMatchEtags, req, model) => {
|
|
24
|
+
const etagElement = req.target.elements[req.target._etag.name]
|
|
25
|
+
|
|
26
|
+
const { target, alias, where } = convertPathExpressionToWhere(req.subject, model, { tableAliasPrefix: 'ETAG_' })
|
|
27
|
+
const select = SELECT.from(target).columns(1).where(where)
|
|
28
|
+
if (alias) select.SELECT.from.as = alias
|
|
29
|
+
// tell resolveView to leave this select alone
|
|
30
|
+
select._doNotResolve = true
|
|
31
|
+
// tell db layer that this is a subselect for etag validation
|
|
32
|
+
// hacky solution for gap in @sap/hana-client
|
|
33
|
+
select._etagValidation = true
|
|
34
|
+
|
|
35
|
+
const cond = []
|
|
36
|
+
if (ifMatchEtags) {
|
|
37
|
+
if (ifMatchEtags.includes('*')) return true
|
|
38
|
+
// HANA does not allow malformed time values in prepared statements
|
|
39
|
+
if (etagElement.type === 'cds.Timestamp' || etagElement.type === 'cds.DateTime') {
|
|
40
|
+
ifMatchEtags = ifMatchEtags.filter(val => new Date(val).toString() !== 'Invalid Date')
|
|
41
|
+
if (!ifMatchEtags.length) return false
|
|
56
42
|
}
|
|
43
|
+
|
|
44
|
+
cond.push({ ref: alias ? [alias, etagElement.name] : [etagElement.name] })
|
|
45
|
+
if (ifMatchEtags.length === 1) cond.push('=', { val: ifMatchEtags[0] })
|
|
46
|
+
else cond.push('in', { list: ifMatchEtags.map(val => ({ val })) })
|
|
47
|
+
} else {
|
|
48
|
+
if (ifNoneMatchEtags.includes('*')) return false
|
|
49
|
+
// if a malformed time value is present, it cannot match -> precondition true
|
|
50
|
+
if (
|
|
51
|
+
(etagElement.type === 'cds.Timestamp' || etagElement.type === 'cds.DateTime') &&
|
|
52
|
+
ifNoneMatchEtags.some(val => new Date(val).toString() === 'Invalid Date')
|
|
53
|
+
) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cond.push({ ref: alias ? [alias, etagElement.name] : [etagElement.name] })
|
|
58
|
+
if (ifNoneMatchEtags.length === 1) cond.push('!=', { val: ifNoneMatchEtags[0] })
|
|
59
|
+
else cond.push('not', 'in', { list: ifNoneMatchEtags.map(val => ({ val })) })
|
|
57
60
|
}
|
|
58
|
-
|
|
61
|
+
if (!cond.length) return
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
const isReadAfterDraftAction =
|
|
62
|
-
req.event === 'READ' && req.target._isDraftEnabled && req.context.event in { draftActivate: 1, EDIT: 1 }
|
|
63
|
-
// It's allowed to also delete drafts when actives are deleted
|
|
64
|
-
if (
|
|
65
|
-
cds.env.fiori?.lean_draft &&
|
|
66
|
-
req.event === 'READ' &&
|
|
67
|
-
req.context.event === 'DELETE' &&
|
|
68
|
-
req.target?.name.endsWith('.drafts') &&
|
|
69
|
-
!req.context?.target?.name.endsWith('.drafts')
|
|
70
|
-
)
|
|
71
|
-
return
|
|
72
|
-
const _req = isReadAfterDraftAction ? req.context : req
|
|
73
|
-
return _req._isOData && _req.isConcurrentResource
|
|
63
|
+
return select.where(cond)
|
|
74
64
|
}
|
|
75
65
|
|
|
76
66
|
/**
|
|
@@ -78,71 +68,110 @@ const _isConcurrentODataReq = req => {
|
|
|
78
68
|
*
|
|
79
69
|
* @param req
|
|
80
70
|
*/
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cqn = getSelectCQN(req.query, req.target, this.model, isActive)
|
|
102
|
-
}
|
|
71
|
+
const commonGenericValidateETag = async function (req) {
|
|
72
|
+
/*
|
|
73
|
+
* currently, etag is only supported for OData requests
|
|
74
|
+
* REST requests should be added later
|
|
75
|
+
* other protocols, such as graphql, need to be checked
|
|
76
|
+
*/
|
|
77
|
+
if (req.protocol !== 'odata-v4') return
|
|
78
|
+
|
|
79
|
+
// automatically add etag columns if not already there
|
|
80
|
+
if (req.query.SELECT) addEtagColumns(req.query.SELECT.columns, req.target)
|
|
81
|
+
|
|
82
|
+
// querying a collection?
|
|
83
|
+
if (req.event === 'READ' && !req.query.SELECT.one) return
|
|
84
|
+
|
|
85
|
+
// etag provided?
|
|
86
|
+
const { ifMatch, ifNoneMatch } = _getMatchHeaders(req)
|
|
87
|
+
if (!ifMatch && !ifNoneMatch) {
|
|
88
|
+
if (req.event === 'READ') return // > ok and nothing more to do
|
|
89
|
+
req.reject(428) // > on writes, an etag must be provided
|
|
90
|
+
}
|
|
103
91
|
|
|
104
|
-
|
|
92
|
+
// normalize
|
|
93
|
+
const ifMatchEtags = ifMatch && _parseHeaderEtagValue(ifMatch)
|
|
94
|
+
const ifNoneMatchEtags = ifNoneMatch && _parseHeaderEtagValue(ifNoneMatch)
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
req.validateEtag(etag == null ? 'null' : etag)
|
|
109
|
-
} else {
|
|
110
|
-
req.validateEtag('*')
|
|
111
|
-
}
|
|
112
|
-
}
|
|
96
|
+
// get select for validation
|
|
97
|
+
const validationStmt = _getValidationStmt(ifMatchEtags, ifNoneMatchEtags, req, this.model)
|
|
113
98
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
// shortcuts
|
|
100
|
+
if (validationStmt === true) {
|
|
101
|
+
// wildcard -> nothing to do
|
|
102
|
+
return
|
|
103
|
+
} else if (validationStmt === false) {
|
|
104
|
+
// never true -> reject
|
|
105
|
+
req.reject(412)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// bound action or CRUD?
|
|
109
|
+
if (req.event === 'EDIT' || (req.target.actions && req.event in req.target.actions)) {
|
|
110
|
+
// REVISIT: how to not directly access db?
|
|
111
|
+
if (cds.db) {
|
|
112
|
+
const result = await cds.tx(req).run(validationStmt)
|
|
113
|
+
if (!result.length) req.reject(412)
|
|
117
114
|
}
|
|
115
|
+
} else if (validationStmt) {
|
|
116
|
+
// add where clause for validation
|
|
117
|
+
const validationClause = ['exists', validationStmt]
|
|
118
|
+
req.query.where(validationClause)
|
|
119
|
+
// HACK for current draft impl
|
|
120
|
+
req._etagValidationClause = validationClause
|
|
121
|
+
req._etagValidationType = ifMatchEtags ? 'if-match' : 'if-none-match'
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/**
|
|
122
|
-
*
|
|
126
|
+
* adds a new uuid for the etag element to the request payload
|
|
123
127
|
*
|
|
128
|
+
* @param req
|
|
129
|
+
*/
|
|
130
|
+
const commonGenericGenerateETag = function (req) {
|
|
131
|
+
const etagElement = req.target.elements[req.target._etag.name]
|
|
132
|
+
req.data[etagElement.name] = cds.utils.uuid()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* handler registration
|
|
124
137
|
*/
|
|
125
138
|
/* istanbul ignore next */
|
|
126
139
|
module.exports = cds.service.impl(function () {
|
|
127
|
-
|
|
140
|
+
commonGenericValidateETag._initial = true
|
|
141
|
+
commonGenericGenerateETag._initial = true
|
|
128
142
|
|
|
129
143
|
for (const k in this.entities) {
|
|
130
144
|
const entity = this.entities[k]
|
|
131
145
|
|
|
132
146
|
if (!entity._etag) continue
|
|
133
147
|
|
|
134
|
-
// handler for CREATE is registered for backwards compatibility w.r.t. ETag generation
|
|
135
|
-
let events = ['CREATE', 'READ', 'UPDATE', 'DELETE']
|
|
136
|
-
|
|
137
|
-
// if odata and fiori is separated, this will not be needed in the odata version
|
|
138
148
|
if (entity._isDraftEnabled) {
|
|
139
|
-
|
|
149
|
+
if (cds.env.fiori?.lean_draft) {
|
|
150
|
+
this.before(['READ', 'DELETE'], entity, commonGenericValidateETag)
|
|
151
|
+
// if draft compat is on, the read handler is automatically registered for <entity> and <entity>.drafts
|
|
152
|
+
const events = cds.env.fiori.draft_compat ? ['UPDATE', 'CANCEL'] : ['READ', 'UPDATE', 'CANCEL']
|
|
153
|
+
this.before(events, entity.drafts, commonGenericValidateETag)
|
|
154
|
+
} else {
|
|
155
|
+
this.before(['READ', 'PATCH', 'CANCEL', 'DELETE'], entity, commonGenericValidateETag)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
this.before(['READ', 'UPDATE', 'DELETE'], entity, commonGenericValidateETag)
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
this.before(events, entity, commonGenericEtag)
|
|
143
|
-
|
|
144
161
|
for (const action in entity.actions) {
|
|
145
|
-
|
|
162
|
+
// etag not applicable to functions and unbound actions
|
|
163
|
+
if (entity.actions[action].kind !== 'action') continue
|
|
164
|
+
if (entity._isDraftEnabled) {
|
|
165
|
+
let _entity = entity
|
|
166
|
+
if (cds.env.fiori?.lean_draft) _entity = action === 'draftEdit' ? entity : entity.drafts
|
|
167
|
+
this.before(action === 'draftEdit' ? 'EDIT' : action, _entity, commonGenericValidateETag)
|
|
168
|
+
} else {
|
|
169
|
+
this.before(action, entity, commonGenericValidateETag)
|
|
170
|
+
}
|
|
146
171
|
}
|
|
172
|
+
|
|
173
|
+
// for backwards compatibility w.r.t. ETag generation if type UUID
|
|
174
|
+
const etagElement = entity.elements[entity._etag.name]
|
|
175
|
+
if (etagElement.isUUID) this.before(['CREATE', 'UPDATE', 'NEW'], entity, commonGenericGenerateETag)
|
|
147
176
|
}
|
|
148
177
|
})
|
|
@@ -42,18 +42,6 @@ const _isDraftCoreComputed = (req, element, event) =>
|
|
|
42
42
|
req._.event === 'draftActivate' &&
|
|
43
43
|
!((event === 'CREATE' && element['@cds.on.insert']) || element['@cds.on.update'])
|
|
44
44
|
|
|
45
|
-
const _isStreamingProperty = (elements, row, property) =>
|
|
46
|
-
Object.values(elements).some(
|
|
47
|
-
element => element['@Core.MediaType'] && element['@Core.MediaType']['='] === property && row[element.name]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const _getMediaTypeValue = () => {
|
|
51
|
-
const ctx = cds.context
|
|
52
|
-
return (
|
|
53
|
-
!ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
45
|
const _preProcessAssertTarget = (assocInfo, assertMap) => {
|
|
58
46
|
const { element: assoc, row } = assocInfo
|
|
59
47
|
const assocTarget = assoc._target
|
|
@@ -147,14 +135,6 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
|
147
135
|
if ((event === 'UPDATE' || event === 'CREATE') && category === '@assert.target') {
|
|
148
136
|
_preProcessAssertTarget(elementInfo, assertMap)
|
|
149
137
|
}
|
|
150
|
-
|
|
151
|
-
// set media type from content-type header if streaming
|
|
152
|
-
if (category === 'stream') {
|
|
153
|
-
if (_isStreamingProperty(element.parent.elements, row, key)) {
|
|
154
|
-
const mtValue = _getMediaTypeValue()
|
|
155
|
-
if (mtValue) row[key] = mtValue
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
138
|
}
|
|
159
139
|
|
|
160
140
|
const _getProcessorFn = (req, errors, assertMap) => {
|
|
@@ -186,7 +166,12 @@ const _pick = element => {
|
|
|
186
166
|
categories.push({ category: 'propagateForeignKeys' })
|
|
187
167
|
}
|
|
188
168
|
|
|
189
|
-
if (
|
|
169
|
+
if (
|
|
170
|
+
element['@assert.range'] ||
|
|
171
|
+
element['@assert.enum'] ||
|
|
172
|
+
element['@assert.format'] ||
|
|
173
|
+
element.type === 'cds.Decimal'
|
|
174
|
+
) {
|
|
190
175
|
categories.push('assert')
|
|
191
176
|
}
|
|
192
177
|
|
|
@@ -24,7 +24,7 @@ const _fillStructure = (row, parts, element, category, args) => {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const _getProcessorFn = req => {
|
|
27
|
-
const REST = req.
|
|
27
|
+
const REST = req.protocol === 'rest'
|
|
28
28
|
|
|
29
29
|
return ({ row, key, element, plain }) => {
|
|
30
30
|
if (!row || row[key] !== undefined) return
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const { isNewStream } = require('../utils/stream')
|
|
2
|
+
|
|
3
|
+
const cds = require('../../cds')
|
|
4
|
+
|
|
5
|
+
const _getStreamingProperties = elements => {
|
|
6
|
+
const result = []
|
|
7
|
+
for (const key in elements) {
|
|
8
|
+
const element = elements[key]
|
|
9
|
+
if (typeof element['@Core.MediaType'] === 'object') {
|
|
10
|
+
result.push({ stream: key, type: element['@Core.MediaType']['='] })
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const _getMediaTypeValue = () => {
|
|
18
|
+
const ctx = cds.context
|
|
19
|
+
return (
|
|
20
|
+
!ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _addContentType(req, mtValue) {
|
|
25
|
+
if (!req.data) return
|
|
26
|
+
const streamProp = _getStreamingProperties(req.target.elements).find(prop => req.data[prop.stream])
|
|
27
|
+
if (streamProp) req.data[streamProp.type] = mtValue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function _addContentTypeStream(req, mtValue) {
|
|
31
|
+
if (req.query.UPDATE || req.query.STREAM?.from) return
|
|
32
|
+
if (req.target._hasPersistenceSkip) return
|
|
33
|
+
const streamProp = _getStreamingProperties(req.target.elements).find(prop => req.query.STREAM.column === prop.stream)
|
|
34
|
+
// REVISIT: move this update to after handler and add etag
|
|
35
|
+
if (streamProp) await UPDATE.entity(req.query.STREAM.into).data({ [streamProp.type]: mtValue })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function addContentType(req) {
|
|
39
|
+
if (!req.query || !req.target) return
|
|
40
|
+
const mtValue = _getMediaTypeValue()
|
|
41
|
+
if (!mtValue) return
|
|
42
|
+
|
|
43
|
+
if (isNewStream()) {
|
|
44
|
+
_addContentTypeStream(req, mtValue)
|
|
45
|
+
} else {
|
|
46
|
+
_addContentType(req, mtValue)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = cds.service.impl(function () {
|
|
51
|
+
this.before(['PATCH', 'UPDATE', 'STREAM'], '*', addContentType)
|
|
52
|
+
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
+
const normalizeTimestamp = require('../utils/normalizeTimestamp')
|
|
2
3
|
|
|
3
4
|
const _getDateFromQueryOptions = str => {
|
|
4
5
|
if (str) {
|
|
5
6
|
const match = str.match(/^date'(.+)'$/)
|
|
6
7
|
// REVISIT: What happens with invalid date values in query parameter? if match.length > 1
|
|
7
|
-
return
|
|
8
|
+
return normalizeTimestamp(match ? match[1] : str)
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -51,15 +52,31 @@ const commonGenericTemporal = function (req) {
|
|
|
51
52
|
|
|
52
53
|
if (_isAsOfNow(_queryOptions)) {
|
|
53
54
|
const date = new Date()
|
|
54
|
-
_['VALID-FROM'] = date
|
|
55
|
-
_['VALID-TO'] =
|
|
55
|
+
_['VALID-FROM'] = normalizeTimestamp(date)
|
|
56
|
+
_['VALID-TO'] = normalizeTimestamp(date.getTime() + _getTimeDelta(req.target))
|
|
56
57
|
} else if (_queryOptions['sap-valid-at']) {
|
|
57
|
-
const
|
|
58
|
-
_['VALID-FROM'] =
|
|
59
|
-
|
|
58
|
+
const dateAsIsoString = _getDateFromQueryOptions(_queryOptions['sap-valid-at'])
|
|
59
|
+
_['VALID-FROM'] = dateAsIsoString
|
|
60
|
+
|
|
61
|
+
if (cds.env.features.precise_timestamps) {
|
|
62
|
+
const nanos = dateAsIsoString.slice(-5)
|
|
63
|
+
// we would lose the nano precision here, so we just cut it off before and attach it again here
|
|
64
|
+
_['VALID-TO'] =
|
|
65
|
+
normalizeTimestamp(
|
|
66
|
+
new Date(dateAsIsoString).getTime() + _getTimeDelta(req.target, _queryOptions['sap-valid-at'])
|
|
67
|
+
).slice(0, -5) + nanos
|
|
68
|
+
} else {
|
|
69
|
+
_['VALID-TO'] = normalizeTimestamp(
|
|
70
|
+
new Date(dateAsIsoString).getTime() + _getTimeDelta(req.target, _queryOptions['sap-valid-at'])
|
|
71
|
+
)
|
|
72
|
+
}
|
|
60
73
|
} else if (_queryOptions['sap-valid-from'] || _queryOptions['sap-valid-to']) {
|
|
61
|
-
_['VALID-FROM'] =
|
|
62
|
-
|
|
74
|
+
_['VALID-FROM'] = normalizeTimestamp(
|
|
75
|
+
_getDateFromQueryOptions(_queryOptions['sap-valid-from'] ?? normalizeTimestamp('0001-01-01T00:00:00.0000000Z'))
|
|
76
|
+
)
|
|
77
|
+
_['VALID-TO'] = normalizeTimestamp(
|
|
78
|
+
_getDateFromQueryOptions(_queryOptions['sap-valid-to'] ?? normalizeTimestamp('9999-12-31T23:59:59.9999999Z'))
|
|
79
|
+
)
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
@@ -45,9 +45,7 @@ ASSERT_FORMAT=Value "{0}" is not in specified format "{1}"
|
|
|
45
45
|
ASSERT_DATA_TYPE=Value {0} is invalid according to type definition "{1}"
|
|
46
46
|
ASSERT_ENUM=Value {0} is invalid according to enum declaration {{1}}
|
|
47
47
|
ASSERT_NOT_NULL=Value is required
|
|
48
|
-
ASSERT_REFERENCE_INTEGRITY=Reference integrity is violated for association "{0}"
|
|
49
48
|
ASSERT_TARGET="Value doesn't exist"
|
|
50
|
-
ASSERT_DEEP_ASSOCIATION=It is not allowed to modify sub documents in {0} Association "{1}"
|
|
51
49
|
|
|
52
50
|
# db
|
|
53
51
|
NO_DATABASE_CONNECTION=No database connection
|
|
@@ -83,7 +83,7 @@ function isPathToDraft(path, model) {
|
|
|
83
83
|
for (const r of path) {
|
|
84
84
|
if (r.id) {
|
|
85
85
|
const isaIndex = r.where.findIndex(ele => ele.ref && ele.ref[0] === 'IsActiveEntity')
|
|
86
|
-
if (isaIndex) draft = !r.where[isaIndex + 2].val
|
|
86
|
+
if (isaIndex !== -1) draft = !r.where[isaIndex + 2].val
|
|
87
87
|
current = current ? definitions[current.elements[r.id].target] : definitions[r.id]
|
|
88
88
|
} else {
|
|
89
89
|
if (r === 'SiblingEntity') draft = !!draft
|