@sap/cds 6.8.3 → 7.0.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 +61 -2
- 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 +1 -1
- 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 +1 -1
- 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/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/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/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
|
@@ -5,126 +5,75 @@ const { postProcess } = require('../../common/utils/postProcessing')
|
|
|
5
5
|
|
|
6
6
|
const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
|
|
7
7
|
|
|
8
|
-
// for getRestrictions()
|
|
9
|
-
const { getNormalizedRestrictions, getApplicableRestrictions } = require('../../common/generic/auth/restrictions.js')
|
|
10
|
-
const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
|
|
11
|
-
const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
12
|
-
|
|
13
8
|
/**
|
|
14
|
-
* Generic Service
|
|
9
|
+
* Generic Application Service Provider
|
|
15
10
|
*/
|
|
16
11
|
class ApplicationService extends cds.Service {
|
|
17
12
|
init() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
require('../../common/generic/paging').call(this, this) // > paging must be executed before sorting
|
|
24
|
-
require('../../common/generic/sorting').call(this, this)
|
|
13
|
+
const clazz = this.constructor,
|
|
14
|
+
{ generics } = clazz
|
|
15
|
+
for (let each of generics) clazz[each].call(this)
|
|
16
|
+
return super.init()
|
|
17
|
+
}
|
|
25
18
|
|
|
26
|
-
|
|
19
|
+
static get generics() {
|
|
20
|
+
let generics = Reflect.getOwnPropertyDescriptor(this, '_generics')
|
|
21
|
+
if (generics) return generics.value
|
|
22
|
+
else {
|
|
23
|
+
const set = new Set(this.__proto__.generics)
|
|
24
|
+
for (let p of Reflect.ownKeys(this)) if (p.startsWith('handle_')) set.add(p)
|
|
25
|
+
Object.defineProperty(this, '_generics', { value: (generics = [...set]) })
|
|
26
|
+
}
|
|
27
|
+
return generics
|
|
28
|
+
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.registerCrudHandlers(this) // default .on handlers, have to go last
|
|
31
|
-
return this
|
|
30
|
+
static handle_authorization() {
|
|
31
|
+
require('../../common/generic/auth').call(this)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @deprecated since version 1.11.0 - use Service.prepend instead
|
|
37
|
-
*/
|
|
38
|
-
with(serviceImpl) {
|
|
39
|
-
return this.prepend(serviceImpl)
|
|
34
|
+
static handle_etags() {
|
|
35
|
+
require('../../common/generic/etag').call(this)
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* @param {string | object | Function} serviceImpl - init function to register custom handlers.
|
|
45
|
-
*/
|
|
46
|
-
impl(serviceImpl) {
|
|
47
|
-
if (typeof serviceImpl === 'string') serviceImpl = require(serviceImpl)
|
|
48
|
-
return this.prepend(serviceImpl)
|
|
38
|
+
static handle_validations() {
|
|
39
|
+
require('../../common/generic/input').call(this)
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
registerFioriHandlers() {
|
|
56
|
-
if (cds.env.fiori.lean_draft) {
|
|
57
|
-
const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
|
|
58
|
-
|
|
59
|
-
for (const each of this.entities)
|
|
60
|
-
if (each.drafts) {
|
|
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)
|
|
65
|
-
if (cds.env.fiori.draft_compat) {
|
|
66
|
-
// register after read handlers to add `IsActiveEntity`,
|
|
67
|
-
// so stakeholders have access to it when calling next()
|
|
68
|
-
|
|
69
|
-
// to check if data contains a key value
|
|
70
|
-
let _key
|
|
71
|
-
for (const key in each.keys) {
|
|
72
|
-
if (key === 'IsActiveEntity') continue
|
|
73
|
-
_key = key
|
|
74
|
-
break
|
|
75
|
-
}
|
|
76
|
-
const _addIsActiveEntity = (data, IsActiveEntity) => {
|
|
77
|
-
if (!data) return
|
|
78
|
-
if (Array.isArray(data)) return data.map(d => _addIsActiveEntity(d, IsActiveEntity))
|
|
79
|
-
if (_key in data) data.IsActiveEntity = IsActiveEntity
|
|
80
|
-
}
|
|
81
|
-
this.on('READ', each, async (req, next) => {
|
|
82
|
-
const data = await next()
|
|
83
|
-
_addIsActiveEntity(data, !req.target?.isDraft)
|
|
84
|
-
return data
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
} else return require('../../fiori/generic').impl.call(this)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
registerCrudHandlers() {
|
|
92
|
-
return require('../../common/generic/crud').impl.call(this)
|
|
42
|
+
static handle_stream_property() {
|
|
43
|
+
require('../../common/generic/stream').call(this)
|
|
93
44
|
}
|
|
94
45
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// return the applicable restrictions (grant and to fit to request and user)
|
|
127
|
-
return getApplicableRestrictions(restrictions, event, user)
|
|
46
|
+
static handle_puts() {
|
|
47
|
+
require('../../common/generic/put').call(this)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static handle_temporal_data() {
|
|
51
|
+
require('../../common/generic/temporal').call(this)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static handle_localized_data() {
|
|
55
|
+
// TODO: can we move handling of localized data here?
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static handle_managed_data() {
|
|
59
|
+
// TODO: can we move handling of managed data here?
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static handle_paging() {
|
|
63
|
+
require('../../common/generic/paging').call(this) // > paging must be executed before sorting
|
|
64
|
+
require('../../common/generic/sorting').call(this)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static handle_code_ext() {
|
|
68
|
+
if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static handle_fiori() {
|
|
72
|
+
require('../../fiori/draft').impl.call(this)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static handle_crud() {
|
|
76
|
+
require('../../common/generic/crud').impl.call(this)
|
|
128
77
|
}
|
|
129
78
|
|
|
130
79
|
// Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
|
|
@@ -139,7 +88,7 @@ class ApplicationService extends cds.Service {
|
|
|
139
88
|
// - undefined/null in case of plain string queries
|
|
140
89
|
if (this.model && _isSimpleCqnQuery(req.query)) {
|
|
141
90
|
const q = resolveView(req.query, this.model, this)
|
|
142
|
-
const t = findQueryTarget(q) || req.target
|
|
91
|
+
const t = findQueryTarget(q) || req.target // REVISIT: why is req.target not correct?
|
|
143
92
|
|
|
144
93
|
// compat
|
|
145
94
|
restoreLink(req)
|
|
@@ -156,6 +105,29 @@ class ApplicationService extends cds.Service {
|
|
|
156
105
|
|
|
157
106
|
return super.handle(req)
|
|
158
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param serviceImpl
|
|
111
|
+
* @deprecated since version 1.11.0 - use Service.prepend instead
|
|
112
|
+
*/
|
|
113
|
+
with(serviceImpl) {
|
|
114
|
+
return this.prepend(serviceImpl)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Registers custom handlers.
|
|
119
|
+
* @param {string | object | Function} serviceImpl - init function to register custom handlers.
|
|
120
|
+
*/
|
|
121
|
+
impl(serviceImpl) {
|
|
122
|
+
if (typeof serviceImpl === 'string') serviceImpl = require(serviceImpl)
|
|
123
|
+
return this.prepend(serviceImpl)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// NOTE: getRestrictions is VERY INOFFICIAL!!!
|
|
128
|
+
const { getRestrictions } = require('../../common/generic/auth/restrictions')
|
|
129
|
+
ApplicationService.prototype.getRestrictions = function (..._) {
|
|
130
|
+
return getRestrictions.call(this, ..._)
|
|
159
131
|
}
|
|
160
132
|
|
|
161
133
|
ApplicationService.prototype.isAppService = true
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
const cds = require('../../../cds')
|
|
2
2
|
// requesting logger without module on purpose!
|
|
3
3
|
const LOG = cds.log()
|
|
4
|
-
|
|
5
4
|
const { DRAFT_COLUMNS_UNION_MAP } = require('../../../common/constants/draft')
|
|
6
|
-
|
|
7
|
-
const defaultSearchableType = 'cds.String'
|
|
5
|
+
const DEFAULT_SEARCHABLE_TYPE = 'cds.String'
|
|
8
6
|
|
|
9
7
|
// REVISIT: Can we combine that with db/utils/columns.js?
|
|
10
8
|
/**
|
|
@@ -18,6 +16,7 @@ const defaultSearchableType = 'cds.String'
|
|
|
18
16
|
* @param [options.filterDraft=false] - indicates whether the draft columns should be filtered if the entity is draft enabled
|
|
19
17
|
* @param [options.removeIgnore=false]
|
|
20
18
|
* @param [options.filterVirtual=false]
|
|
19
|
+
* @param [options.keysOnly=false]
|
|
21
20
|
* @returns {Array<object>} - array of columns
|
|
22
21
|
*/
|
|
23
22
|
const getColumns = (
|
|
@@ -46,10 +45,12 @@ const _getSearchableColumns = entity => {
|
|
|
46
45
|
const columns = getColumns(entity, columnsOptions)
|
|
47
46
|
const cdsSearchTerm = '@cds.search'
|
|
48
47
|
const cdsSearchKeys = []
|
|
48
|
+
const cdsSearchColumnMap = new Map()
|
|
49
|
+
|
|
49
50
|
for (const key in entity) {
|
|
50
51
|
if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
let atLeastOneColumnIsSearchable = false
|
|
54
55
|
|
|
55
56
|
// build a map of columns annotated with the @cds.search annotation
|
|
@@ -62,7 +63,7 @@ const _getSearchableColumns = entity => {
|
|
|
62
63
|
continue
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
const annotationKey = cdsSearchTerm
|
|
66
|
+
const annotationKey = `${cdsSearchTerm}.${columnName}`
|
|
66
67
|
const annotationValue = entity[annotationKey]
|
|
67
68
|
if (annotationValue) atLeastOneColumnIsSearchable = true
|
|
68
69
|
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
@@ -71,21 +72,22 @@ const _getSearchableColumns = entity => {
|
|
|
71
72
|
const searchableColumns = columns.filter(column => {
|
|
72
73
|
const annotatedColumnValue = cdsSearchColumnMap.get(column.name)
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// The @cds.search annotation is provided, and the column is annotated as searchable, e.g.:
|
|
77
|
-
// @cds.search { column1: true } or just @cds.search { column1 }
|
|
75
|
+
// the element is searchable if it is annotated with the @cds.search, e.g.:
|
|
76
|
+
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
78
77
|
if (annotatedColumnValue) return true
|
|
79
78
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
79
|
+
// if at least one element is explicitly annotated as searchable, e.g.:
|
|
80
|
+
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
82
81
|
// and it is not the current column name, then it must be excluded from the search
|
|
83
82
|
if (atLeastOneColumnIsSearchable) return false
|
|
84
83
|
|
|
85
|
-
//
|
|
86
|
-
// is typed as string
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
// the element is considered searchable if it is explicitly annotated as such or
|
|
85
|
+
// if it is not annotated and the column is typed as a string (excluding elements/elements expressions)
|
|
86
|
+
return (
|
|
87
|
+
annotatedColumnValue === undefined &&
|
|
88
|
+
column._type === DEFAULT_SEARCHABLE_TYPE &&
|
|
89
|
+
!entity?.query?.SELECT?.columns?.find(col => col.xpr && col.as === column.name)
|
|
90
|
+
)
|
|
89
91
|
})
|
|
90
92
|
|
|
91
93
|
// if the @cds.search annotation is provided -->
|
|
@@ -109,7 +111,8 @@ const _getSearchableColumns = entity => {
|
|
|
109
111
|
if (!cds._deprecationWarningForDefaultSearchElement) {
|
|
110
112
|
LOG._warn &&
|
|
111
113
|
LOG.warn(
|
|
112
|
-
'Annotation "@Search.defaultSearchElement" is deprecated and will be removed in an upcoming release.
|
|
114
|
+
'Annotation "@Search.defaultSearchElement" is deprecated and will be removed in an upcoming release. ' +
|
|
115
|
+
'Use "@cds.search" instead.'
|
|
113
116
|
)
|
|
114
117
|
cds._deprecationWarningForDefaultSearchElement = true
|
|
115
118
|
}
|
|
@@ -137,8 +140,9 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
|
|
|
137
140
|
cqn.SELECT.columns.length === 1 &&
|
|
138
141
|
column.func === 'count' &&
|
|
139
142
|
(column.as === '_counted_' || column.as === '$count')
|
|
140
|
-
)
|
|
143
|
+
) {
|
|
141
144
|
return
|
|
145
|
+
}
|
|
142
146
|
|
|
143
147
|
toBeSearched.push(column)
|
|
144
148
|
return
|
|
@@ -146,7 +150,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
|
|
|
146
150
|
|
|
147
151
|
const columnRef = column.ref
|
|
148
152
|
if (columnRef) {
|
|
149
|
-
if (entity.elements[columnRef[columnRef.length - 1]]?._type !==
|
|
153
|
+
if (entity.elements[columnRef[columnRef.length - 1]]?._type !== DEFAULT_SEARCHABLE_TYPE) return
|
|
150
154
|
column = { ref: [...column.ref] }
|
|
151
155
|
if (alias) column.ref.unshift(alias)
|
|
152
156
|
toBeSearched.push(column)
|
|
@@ -154,7 +158,6 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
|
|
|
154
158
|
})
|
|
155
159
|
} else {
|
|
156
160
|
toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
157
|
-
|
|
158
161
|
if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
|
|
159
162
|
toBeSearched = toBeSearched.map(c => {
|
|
160
163
|
const col = { ref: [c] }
|
|
@@ -162,6 +165,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
|
|
|
162
165
|
return col
|
|
163
166
|
})
|
|
164
167
|
}
|
|
168
|
+
|
|
165
169
|
return toBeSearched
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -154,10 +154,20 @@ const _skipToMany = (entity, prop) => {
|
|
|
154
154
|
return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// Returns all property names from the new entry and add missing managed elements
|
|
158
|
+
const _propertiesAndManaged = (newEntry, entity) => {
|
|
159
|
+
return [
|
|
160
|
+
...Object.getOwnPropertyNames(newEntry),
|
|
161
|
+
...Object.keys(entity.elements).filter(
|
|
162
|
+
elementName => newEntry[elementName] === undefined && entity.elements[elementName]['@cds.on.update']
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
|
|
158
168
|
// On app-service layer, generated foreign keys are not enumerable,
|
|
159
169
|
// include them here too.
|
|
160
|
-
for (const prop of
|
|
170
|
+
for (const prop of _propertiesAndManaged(newEntry, entity)) {
|
|
161
171
|
if (keys.includes(prop)) {
|
|
162
172
|
_addKeysToResult(result, prop, newEntry, oldEntry)
|
|
163
173
|
continue
|
|
@@ -9,6 +9,7 @@ const { DRAFT_COLUMNS_MAP } = require('../../../common/constants/draft')
|
|
|
9
9
|
const { cqn2cqn4sql, convertPathExpressionToWhere } = require('../../../common/utils/cqn2cqn4sql')
|
|
10
10
|
const { revertData } = require('../../../common/utils/resolveView')
|
|
11
11
|
const { removeIsActiveEntityRecursively } = require('../../../fiori/utils/where')
|
|
12
|
+
const { enrichDataWithKeysFromWhere } = require('../../../common/utils/keys')
|
|
12
13
|
|
|
13
14
|
module.exports = class Differ {
|
|
14
15
|
constructor(srv) {
|
|
@@ -37,8 +38,9 @@ module.exports = class Differ {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
_diffDelete(req) {
|
|
40
|
-
const { DELETE } = (req._ && req._.query) || req.query
|
|
41
|
-
const
|
|
41
|
+
const { DELETE } = cds.env.fiori.lean_draft ? req.query : (req._ && req._.query) || req.query
|
|
42
|
+
const target = DELETE._transitions?.[DELETE._transitions.length - 1]?.target || req.target
|
|
43
|
+
const query = SELECT.from(DELETE.from).columns(this._createSelectColumnsForDelete(target))
|
|
42
44
|
if (DELETE.where) query.where(DELETE.where)
|
|
43
45
|
|
|
44
46
|
return cds
|
|
@@ -57,6 +59,7 @@ module.exports = class Differ {
|
|
|
57
59
|
if (cds.db) await this._addPartialPersistentState(req)
|
|
58
60
|
const newQuery = cqn2cqn4sql(req.query, this._srv.model)
|
|
59
61
|
const combinedData = providedData || Object.assign({}, req.query.UPDATE.data || {}, req.query.UPDATE.with || {})
|
|
62
|
+
enrichDataWithKeysFromWhere(combinedData, req, this._srv)
|
|
60
63
|
const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
|
|
61
64
|
const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
|
|
62
65
|
return compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
|
|
@@ -85,6 +88,8 @@ module.exports = class Differ {
|
|
|
85
88
|
? req.query.INSERT.entries[0]
|
|
86
89
|
: req.query.INSERT.entries
|
|
87
90
|
|
|
91
|
+
enrichDataWithKeysFromWhere(originalData, req, this._srv)
|
|
92
|
+
|
|
88
93
|
return compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -263,11 +263,70 @@ const _checkFormatElement = (element, value, errors, key, pathSegments) => {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
const getNormalizedDecimal = value => {
|
|
267
|
+
let val = `${value}`
|
|
268
|
+
const cgs = val.match(/^(\d*\.*\d*)e([+|-]*)(\d*)$/)
|
|
269
|
+
if (cgs) {
|
|
270
|
+
let [l, r = ''] = cgs[1].split('.')
|
|
271
|
+
const dir = cgs[2] || '+'
|
|
272
|
+
const exp = Number(cgs[3])
|
|
273
|
+
if (dir === '+') {
|
|
274
|
+
// move decimal point to the right
|
|
275
|
+
r = r.padEnd(exp, '0')
|
|
276
|
+
l += r.substring(0, exp)
|
|
277
|
+
r = r.slice(exp)
|
|
278
|
+
val = `${l}${r ? '.' + r : ''}`
|
|
279
|
+
} else {
|
|
280
|
+
// move decimal point to the left
|
|
281
|
+
l = l.padStart(exp, '0')
|
|
282
|
+
r = l.substring(0, exp) + r
|
|
283
|
+
l = l.slice(exp)
|
|
284
|
+
val = `${l ? l : '0'}.${r}`
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return val
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const _checkDecimalElement = (element, value, errors, key, path) => {
|
|
291
|
+
const { precision, scale } = element
|
|
292
|
+
let val = getNormalizedDecimal(value)
|
|
293
|
+
if (precision != null && scale != null) {
|
|
294
|
+
let isValid = true
|
|
295
|
+
if (!val.match(/\./)) val += '.0'
|
|
296
|
+
|
|
297
|
+
if (precision === scale) {
|
|
298
|
+
if (!val.match(new RegExp(`^-?0\\.\\d{0,${scale}}$`, 'g'))) isValid = false
|
|
299
|
+
} else if (scale === 0) {
|
|
300
|
+
if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.0{0,1}$`, 'g'))) isValid = false
|
|
301
|
+
} else if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.\\d{0,${scale}}$`, 'g'))) {
|
|
302
|
+
isValid = false
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!isValid)
|
|
306
|
+
errors.push(
|
|
307
|
+
assertError(
|
|
308
|
+
{ code: ASSERT_DATA_TYPE, args: [value, `Decimal(${precision},${scale})`] },
|
|
309
|
+
element,
|
|
310
|
+
value,
|
|
311
|
+
key,
|
|
312
|
+
path
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
} else if (precision != null) {
|
|
316
|
+
if (!val.match(new RegExp(`^-?\\d{1,${precision}}$`, 'g'))) {
|
|
317
|
+
errors.push(
|
|
318
|
+
assertError({ code: ASSERT_DATA_TYPE, args: [value, `Decimal(${precision})`] }, element, value, key, path)
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
266
324
|
/**
|
|
267
325
|
* @param {import('../../types/api').InputConstraints} constraints
|
|
268
326
|
*/
|
|
269
327
|
const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }) => {
|
|
270
328
|
if (!element) return errors
|
|
329
|
+
|
|
271
330
|
let path
|
|
272
331
|
|
|
273
332
|
if (pathSegmentsInfo?.length) path = templatePathSerializer(element.name || key, pathSegmentsInfo)
|
|
@@ -278,6 +337,9 @@ const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }
|
|
|
278
337
|
_checkEnumElement(element, value, errors, key, path)
|
|
279
338
|
_checkRangeElement(element, value, errors, key, path)
|
|
280
339
|
_checkFormatElement(element, value, errors, key, path)
|
|
340
|
+
|
|
341
|
+
if (element.type === 'cds.Decimal') _checkDecimalElement(element, value, errors, key, path)
|
|
342
|
+
|
|
281
343
|
return errors
|
|
282
344
|
}
|
|
283
345
|
|
|
@@ -336,7 +398,7 @@ const assertTargets = async (assertMap, errors) => {
|
|
|
336
398
|
|
|
337
399
|
targetsExistsResults.forEach((txPromise, index) => {
|
|
338
400
|
const isPromiseRejected = txPromise.status === 'rejected'
|
|
339
|
-
const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value
|
|
401
|
+
const shouldAssertError = (txPromise.status === 'fulfilled' && txPromise.value == null) || isPromiseRejected
|
|
340
402
|
if (!shouldAssertError) return
|
|
341
403
|
|
|
342
404
|
const target = targets[index]
|
|
@@ -374,5 +436,6 @@ module.exports = {
|
|
|
374
436
|
assertError,
|
|
375
437
|
checkStaticElementByKey,
|
|
376
438
|
assertNotNullError,
|
|
377
|
-
assertTargets
|
|
439
|
+
assertTargets,
|
|
440
|
+
getNormalizedDecimal
|
|
378
441
|
}
|
|
@@ -135,6 +135,7 @@ const _getWhereObj = (row, links) => {
|
|
|
135
135
|
return links.reduce((res, currentLink) => {
|
|
136
136
|
if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey) && row[currentLink.targetKey] !== null)
|
|
137
137
|
res[currentLink.entityKey] = row[currentLink.targetKey]
|
|
138
|
+
else if (currentLink.targetVal !== undefined) res[currentLink.entityKey] = currentLink.targetVal
|
|
138
139
|
return res
|
|
139
140
|
}, {})
|
|
140
141
|
}
|
|
@@ -10,7 +10,7 @@ const _getTarget = (ref, target, definitions) => {
|
|
|
10
10
|
if (ref.length === 1) return definitions[ensureNoDraftsSuffix(target_.target)]
|
|
11
11
|
return _getTarget(ref.slice(1), target_, definitions)
|
|
12
12
|
}
|
|
13
|
-
const target_ = target.elements[ref.join('_')]
|
|
13
|
+
const target_ = target.elements[ref.map(x => x.id || x).join('_')]
|
|
14
14
|
return definitions[ensureNoDraftsSuffix(target_.target)]
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -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
|
|