@sap/cds 6.1.0 → 6.1.3
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 +67 -0
- package/apis/log.d.ts +112 -36
- package/apis/services.d.ts +13 -2
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -3
- package/bin/build/provider/hana/index.js +4 -2
- package/bin/build/provider/mtx/resourcesTarBuilder.js +4 -8
- package/bin/deploy/to-hana/hana.js +20 -25
- package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/schemas/cds-rc.json +10 -1
- package/lib/index.js +1 -1
- package/lib/log/format/kibana.js +19 -1
- package/lib/ql/Query.js +9 -3
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/UPDATE.js +2 -2
- package/lib/ql/cds-ql.js +4 -10
- package/lib/req/context.js +15 -11
- package/lib/srv/srv-api.js +8 -0
- package/lib/srv/srv-dispatch.js +11 -7
- package/lib/srv/srv-models.js +4 -3
- package/lib/srv/srv-tx.js +52 -40
- package/lib/utils/cds-utils.js +3 -3
- package/lib/utils/resources/index.js +5 -5
- package/lib/utils/resources/tar.js +1 -1
- package/libx/_runtime/auth/index.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -2
- package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -1
- package/libx/_runtime/cds-services/util/assert.js +3 -0
- package/libx/_runtime/common/generic/input.js +17 -2
- package/libx/_runtime/common/generic/put.js +4 -1
- package/libx/_runtime/common/generic/temporal.js +0 -3
- package/libx/_runtime/common/utils/binary.js +3 -4
- package/libx/_runtime/common/utils/keys.js +14 -6
- package/libx/_runtime/common/utils/propagateForeignKeys.js +122 -0
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/common/utils/template.js +2 -3
- package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
- package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
- package/libx/_runtime/db/generic/input.js +7 -4
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
- package/libx/_runtime/extensibility/add.js +3 -0
- package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
- package/libx/_runtime/extensibility/push.js +11 -11
- package/libx/_runtime/extensibility/token.js +2 -1
- package/libx/_runtime/extensibility/utils.js +8 -6
- package/libx/_runtime/fiori/generic/new.js +1 -3
- package/libx/_runtime/fiori/generic/patch.js +1 -7
- package/libx/_runtime/fiori/utils/where.js +1 -1
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +1 -2
- package/libx/_runtime/remote/utils/client.js +29 -10
- package/libx/_runtime/sqlite/Service.js +7 -5
- package/libx/_runtime/sqlite/execute.js +41 -28
- package/libx/odata/cqn2odata.js +6 -2
- package/libx/rest/RestAdapter.js +3 -6
- package/libx/rest/middleware/input.js +2 -3
- package/package.json +1 -1
- package/srv/extensibility-service.cds +4 -3
- package/srv/model-provider.js +1 -1
- package/srv/mtx.js +18 -9
- package/libx/_runtime/db/utils/propagateForeignKeys.js +0 -93
package/lib/req/context.js
CHANGED
|
@@ -12,11 +12,15 @@ const { EventEmitter } = require('events')
|
|
|
12
12
|
class EventContext {
|
|
13
13
|
|
|
14
14
|
/** Creates a new instance that inherits from cds.context */
|
|
15
|
-
static for (_) {
|
|
15
|
+
static for (_,_as_root) {
|
|
16
16
|
const ctx = new this (_)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const base = cds.context
|
|
18
|
+
if (base) {
|
|
19
|
+
ctx._set('_propagated', base) // we inherit from former cds.currents
|
|
20
|
+
if (!_as_root) {
|
|
21
|
+
if (!ctx.context) ctx._set('context', base.context) // all transaction handling works with root contexts
|
|
22
|
+
if (!ctx.tx && base.tx) ctx.tx = base.tx
|
|
23
|
+
}
|
|
20
24
|
}
|
|
21
25
|
return ctx
|
|
22
26
|
}
|
|
@@ -38,7 +42,7 @@ class EventContext {
|
|
|
38
42
|
//
|
|
39
43
|
|
|
40
44
|
get emitter() {
|
|
41
|
-
return this.context._emitter ||
|
|
45
|
+
return this.context._emitter || this.context._set('_emitter', new EventEmitter)
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
async emit (event,...args) {
|
|
@@ -115,7 +119,8 @@ class EventContext {
|
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
get model() {
|
|
118
|
-
|
|
122
|
+
const m = this._propagated.model || this.http?.req.__model // IMPORTANT: Never use that anywhere else
|
|
123
|
+
return this._set('model',m)
|
|
119
124
|
}
|
|
120
125
|
set model(m) {
|
|
121
126
|
super.model = m
|
|
@@ -156,12 +161,11 @@ class EventContext {
|
|
|
156
161
|
*/
|
|
157
162
|
set tx (tx) {
|
|
158
163
|
Object.defineProperty (this,'tx',{value:tx}) //> allowed only once!
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
if (!this.hasOwnProperty('context')) this.context =
|
|
162
|
-
|
|
164
|
+
const root = tx.context?.context
|
|
165
|
+
if (root && root !== this) {
|
|
166
|
+
if (!this.hasOwnProperty('context')) this.context = root // eslint-disable-line no-prototype-builtins
|
|
163
167
|
if (features.assert_integrity && features.assert_integrity_type == 'RT') {
|
|
164
|
-
const reqs =
|
|
168
|
+
const reqs = root._children || root._set('_children', {})
|
|
165
169
|
const all = reqs[tx.name] || (reqs[tx.name] = [])
|
|
166
170
|
all.push(this)
|
|
167
171
|
}
|
package/lib/srv/srv-api.js
CHANGED
|
@@ -56,6 +56,14 @@ class Service extends require('./srv-handlers') {
|
|
|
56
56
|
* Querying API to send synchronous requests...
|
|
57
57
|
*/
|
|
58
58
|
run (query, data) {
|
|
59
|
+
if (typeof query === 'function') {
|
|
60
|
+
const ctx = cds.context, fn = query
|
|
61
|
+
if (ctx?.tx && !ctx.tx._done) {
|
|
62
|
+
return fn (this.tx(ctx)) // with nested tx
|
|
63
|
+
} else {
|
|
64
|
+
return this.tx (fn) // with root tx
|
|
65
|
+
}
|
|
66
|
+
}
|
|
59
67
|
const req = new Request ({ query, data })
|
|
60
68
|
return this.dispatch (req)
|
|
61
69
|
}
|
package/lib/srv/srv-dispatch.js
CHANGED
|
@@ -13,18 +13,23 @@ exports.dispatch = async function dispatch (req) { //NOSONAR
|
|
|
13
13
|
|
|
14
14
|
// Ensure we are in a proper transaction
|
|
15
15
|
if (!this.context) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
|
|
16
|
+
const ctx = cds.context
|
|
17
|
+
if (ctx?.tx && !ctx.tx._done) {
|
|
18
|
+
return this.tx (ctx) .dispatch(req) // with nested tx
|
|
19
|
+
} else {
|
|
20
|
+
return this.tx (tx => tx.dispatch(req)) // with root tx
|
|
21
|
+
}
|
|
19
22
|
}
|
|
20
23
|
if (!req.tx) req.tx = this // `this` is a tx from now on...
|
|
21
24
|
|
|
22
25
|
// Inform potential listeners // REVISIT: -> this should move into protocol adapters
|
|
23
|
-
|
|
26
|
+
let _is_root = req.constructor.name in { ODataRequest:1, RestRequest:2 }
|
|
27
|
+
if (_is_root) req._.req.emit ('dispatch',req)
|
|
24
28
|
|
|
25
29
|
// Handle batches of queries
|
|
26
|
-
if (_is_array(req.query))
|
|
27
|
-
|
|
30
|
+
if (_is_array(req.query)) return Promise.all (req.query.map (
|
|
31
|
+
q => this.dispatch ({ query:q, context: req.context, __proto__:req })
|
|
32
|
+
))
|
|
28
33
|
|
|
29
34
|
// Ensure target and fqns
|
|
30
35
|
if (!req.target) _ensure_target (this,req)
|
|
@@ -88,7 +93,6 @@ exports.handle = async function handle (req) {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
|
|
91
|
-
const _is_root = (req) => /OData|REST/i.test(req.constructor.name)
|
|
92
96
|
const _is_array = Array.isArray
|
|
93
97
|
const _dummy = ()=>{} // REVISIT: required for some messaging tests which obviously still expect and call next()
|
|
94
98
|
|
package/lib/srv/srv-models.js
CHANGED
|
@@ -22,13 +22,13 @@ class ExtendedModels {
|
|
|
22
22
|
*/
|
|
23
23
|
static middleware4 (srv) {
|
|
24
24
|
if (!(srv instanceof cds.ApplicationService)) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
|
|
25
|
-
if (srv.
|
|
26
|
-
else return async (req,res,next)
|
|
25
|
+
if (!srv.isExtensible) return [] //> no middleware to add
|
|
26
|
+
else return async function cds_context_model (req,res,next) {
|
|
27
27
|
if (!req.user?.tenant) return next()
|
|
28
28
|
const ctx = cds.context = cds.EventContext.for({ http: { req, res } })
|
|
29
29
|
ctx.user = req.user // REVISIT: should move to auth middleware?
|
|
30
30
|
try {
|
|
31
|
-
ctx.model = req.__model = await
|
|
31
|
+
ctx.model = req.__model = await ExtendedModels.model4 (ctx.tenant, ctx.features)
|
|
32
32
|
} catch (e) {
|
|
33
33
|
LOG.error (Object.assign(e, { message: 'Unable to get service from service map due to error: ' + e.message }))
|
|
34
34
|
|
|
@@ -99,6 +99,7 @@ class ExtendedModels {
|
|
|
99
99
|
if (model.then) return model //> promised model to avoid race conditions
|
|
100
100
|
|
|
101
101
|
const {_cached} = model, interval = ExtendedModels.checkInterval
|
|
102
|
+
if (!_cached.touched) return model
|
|
102
103
|
if (Date.now() - _cached.touched < interval) return model //> checked recently
|
|
103
104
|
|
|
104
105
|
else return this[key] = (async()=>{ // temporarily replace cache entry by promise to avoid race conditions...
|
package/lib/srv/srv-tx.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
const cds = require('../index'), { cds_tx_protection } = cds.env.features
|
|
2
2
|
const EventContext = require('../req/context')
|
|
3
|
-
class RootContext extends EventContext {
|
|
3
|
+
class RootContext extends EventContext {
|
|
4
|
+
static for(_) {
|
|
5
|
+
if (_ instanceof EventContext) return _
|
|
6
|
+
else return super.for(_,'as root')
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
class NestedContext extends EventContext {
|
|
10
|
+
static for(_) {
|
|
11
|
+
if (_ instanceof EventContext) return _
|
|
12
|
+
else return super.for(_)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
|
|
5
17
|
/**
|
|
6
18
|
* This is the implementation of the `srv.tx(req)` method. It constructs
|
|
@@ -8,35 +20,28 @@ class RootContext extends EventContext {}
|
|
|
8
20
|
* @returns { Transaction & import('./srv-api') }
|
|
9
21
|
* @param { EventContext } ctx
|
|
10
22
|
*/
|
|
11
|
-
|
|
23
|
+
function srv_tx (ctx,fn) { const srv = this
|
|
12
24
|
|
|
13
25
|
if (srv.context) return srv // srv.tx().tx() -> idempotent
|
|
26
|
+
if (!ctx) return RootTransaction.for (srv)
|
|
27
|
+
|
|
28
|
+
// Creating root or nested txes for existing contexts
|
|
29
|
+
if (ctx instanceof EventContext) {
|
|
30
|
+
if (ctx.tx) return NestedTransaction.for (srv, ctx)
|
|
31
|
+
else return RootTransaction.for (srv, ctx)
|
|
32
|
+
}
|
|
14
33
|
|
|
15
34
|
// Last arg may be a function -> srv.tx (tx => { ... })
|
|
16
35
|
if (typeof ctx === 'function') [ ctx, fn ] = [ undefined, ctx ]
|
|
17
36
|
if (typeof fn === 'function') {
|
|
18
|
-
const tx =
|
|
19
|
-
|
|
20
|
-
return _has_tx ? fx() : cds._context.run(tx,fx)
|
|
37
|
+
const tx = RootTransaction.for (srv, ctx)
|
|
38
|
+
return cds._context.run (tx, ()=> Promise.resolve(fn(tx)) .then (tx.commit, tx.rollback))
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (ctx instanceof EventContext) {
|
|
28
|
-
if (ctx.tx) return NestedTransaction.for (srv, ctx)
|
|
29
|
-
else return RootTransaction.for (srv, ctx)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// REVISIT: This is for compatibility with AFC only
|
|
33
|
-
if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
|
|
34
|
-
else Object.defineProperty(ctx, '_txed_before', { value: ctx = RootContext.for(ctx) })
|
|
35
|
-
return RootTransaction.for (srv, ctx)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// `ctx` is a plain context object or undefined
|
|
39
|
-
return RootTransaction.for (srv, RootContext.for(ctx))
|
|
41
|
+
// REVISIT: following is for compatibility with AFC only -> we should get rid of that
|
|
42
|
+
if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
|
|
43
|
+
else Object.defineProperty (ctx, '_txed_before', { value: ctx = RootContext.for(ctx) })
|
|
44
|
+
return RootTransaction.for (srv, ctx)
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
|
|
@@ -45,17 +50,16 @@ class Transaction {
|
|
|
45
50
|
/**
|
|
46
51
|
* Returns an already started tx for given srv, or creates a new instance
|
|
47
52
|
*/
|
|
48
|
-
static for (srv,
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a qirty hack for current Okra Adapters
|
|
53
|
+
static for (srv, ctx) {
|
|
54
|
+
const txs = ctx.context.transactions || ctx.context._set ('transactions', new Map)
|
|
55
|
+
if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a dirty hack for current Okra Adapters
|
|
52
56
|
let tx = txs.get (srv)
|
|
53
|
-
if (!tx) txs.set (srv, tx = new this (srv,
|
|
57
|
+
if (!tx) txs.set (srv, tx = new this (srv,ctx))
|
|
54
58
|
return tx
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
constructor (srv,
|
|
58
|
-
const tx = { __proto__:srv, context:
|
|
61
|
+
constructor (srv, ctx) {
|
|
62
|
+
const tx = { __proto__:srv, _kind: new.target.name, context: ctx }
|
|
59
63
|
const proto = new.target.prototype
|
|
60
64
|
tx.commit = proto.commit.bind(tx)
|
|
61
65
|
tx.rollback = proto.rollback.bind(tx)
|
|
@@ -107,11 +111,12 @@ class Transaction {
|
|
|
107
111
|
class RootTransaction extends Transaction {
|
|
108
112
|
|
|
109
113
|
/**
|
|
110
|
-
* Register the new transaction with the
|
|
111
|
-
* @param {EventContext}
|
|
114
|
+
* Register the new transaction with the given context.
|
|
115
|
+
* @param {EventContext} ctx
|
|
112
116
|
*/
|
|
113
|
-
static for (srv,
|
|
114
|
-
|
|
117
|
+
static for (srv, ctx) {
|
|
118
|
+
ctx = RootContext.for (ctx?.tx?._done ? {} : ctx)
|
|
119
|
+
return ctx.tx = super.for (srv, ctx)
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
@@ -153,16 +158,21 @@ class RootTransaction extends Transaction {
|
|
|
153
158
|
|
|
154
159
|
class NestedTransaction extends Transaction {
|
|
155
160
|
|
|
161
|
+
static for (srv,ctx) {
|
|
162
|
+
ctx = NestedContext.for (ctx)
|
|
163
|
+
return super.for (srv, ctx)
|
|
164
|
+
}
|
|
165
|
+
|
|
156
166
|
/**
|
|
157
|
-
* Registers event listeners with the
|
|
167
|
+
* Registers event listeners with the given context, to commit or rollback
|
|
158
168
|
* when the root tx is about to commit or rollback.
|
|
159
|
-
* @param {
|
|
169
|
+
* @param {EventContext} ctx
|
|
160
170
|
*/
|
|
161
|
-
constructor (srv,
|
|
162
|
-
super (srv,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if ('end' in srv)
|
|
171
|
+
constructor (srv,ctx) {
|
|
172
|
+
super (srv,ctx)
|
|
173
|
+
ctx.before ('succeeded', ()=> this.commit())
|
|
174
|
+
ctx.before ('failed', ()=> this.rollback())
|
|
175
|
+
if ('end' in srv) ctx.once ('done', ()=> srv.end())
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
}
|
|
@@ -193,3 +203,5 @@ const _begin = async function (req) {
|
|
|
193
203
|
delete this.dispatch
|
|
194
204
|
return this.dispatch (req)
|
|
195
205
|
}
|
|
206
|
+
|
|
207
|
+
module.exports = srv_tx
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -70,17 +70,17 @@ exports.mkdirp = async function (...path) {
|
|
|
70
70
|
|
|
71
71
|
exports.rmdir = (...path) => {
|
|
72
72
|
const d = resolve (cds.root,...path)
|
|
73
|
-
return
|
|
73
|
+
return fs.promises.rm (d, {recursive:true})
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
exports.rimraf = (...path) => {
|
|
77
77
|
const d = resolve (cds.root,...path)
|
|
78
|
-
return
|
|
78
|
+
return fs.promises.rm (d, {recursive:true,force:true})
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
exports.rm = async function rm (x) {
|
|
82
82
|
const y = resolve (cds.root,x)
|
|
83
|
-
return
|
|
83
|
+
return fs.promises.rm(y)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
exports.copy = async function copy (x,y) {
|
|
@@ -8,22 +8,22 @@ const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
|
|
|
8
8
|
|
|
9
9
|
const packTarArchive = async (files, root, flat = false) => {
|
|
10
10
|
if (typeof files === 'string') return await packArchiveCLI(files)
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
let tgzBuffer, temp
|
|
13
13
|
try {
|
|
14
|
-
temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
|
|
14
|
+
temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
|
|
15
15
|
for (const file of files) {
|
|
16
16
|
const fname = flat ? path.basename(file) : path.relative(root, file)
|
|
17
17
|
const destination = path.join(temp, fname)
|
|
18
18
|
const dirname = path.dirname(destination)
|
|
19
19
|
if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
|
|
20
|
-
await fs.promises.copyFile(file, destination)
|
|
20
|
+
await fs.promises.copyFile(file, destination)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
tgzBuffer = await packArchiveCLI(temp)
|
|
24
24
|
} finally {
|
|
25
25
|
if (await exists(temp)) {
|
|
26
|
-
await
|
|
26
|
+
await fs.promises.rm(temp, { recursive: true, force: true })
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -38,7 +38,7 @@ const unpackTarArchive = async (buffer, folder) => {
|
|
|
38
38
|
try {
|
|
39
39
|
await unpackArchiveCLI(tgz, folder)
|
|
40
40
|
} finally {
|
|
41
|
-
if (await exists(temp)) await
|
|
41
|
+
if (await exists(temp)) await fs.promises.rm(temp, { recursive: true, force: true })
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -183,7 +183,7 @@ module.exports = (srv, options = srv.options) => {
|
|
|
183
183
|
(process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
|
|
184
184
|
) {
|
|
185
185
|
if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
|
|
186
|
-
app.use(
|
|
186
|
+
app.use(cap_enforce_login)
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// so we don't log the same stuff multiple times
|
|
@@ -199,7 +199,7 @@ const _strategy4 = config => {
|
|
|
199
199
|
throw new Error(`Authentication kind "${config.kind}" is not supported`)
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
const
|
|
202
|
+
const cap_enforce_login = (req, res, next) => {
|
|
203
203
|
if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
|
|
204
204
|
if (!req.user || req.user._is_anonymous) {
|
|
205
205
|
if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
@@ -81,7 +81,8 @@ const _getCount = async (tx, readReq) => {
|
|
|
81
81
|
// REVISIT: this process appears to be rather clumsy
|
|
82
82
|
// Copy CQN including from, where and search + changing columns
|
|
83
83
|
const select = SELECT.from(readReq.query.SELECT.from)
|
|
84
|
-
|
|
84
|
+
// { val: 1 } is used on purpose, as "numbers" are not used as param in prepared stmt
|
|
85
|
+
select.SELECT.columns = [{ func: 'count', args: [{ val: 1 }], as: '$count' }]
|
|
85
86
|
|
|
86
87
|
if (readReq.query.SELECT.where) select.SELECT.where = readReq.query.SELECT.where
|
|
87
88
|
if (readReq.query.SELECT.search) select.SELECT.search = readReq.query.SELECT.search
|
|
@@ -68,8 +68,6 @@ const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\(
|
|
|
68
68
|
const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i')
|
|
69
69
|
const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i')
|
|
70
70
|
|
|
71
|
-
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
|
|
72
|
-
|
|
73
71
|
function _getBase64(val) {
|
|
74
72
|
if (isInvalidBase64string(val)) return
|
|
75
73
|
// convert url-safe to standard base64
|
|
@@ -40,8 +40,10 @@ class DeserializerFactory {
|
|
|
40
40
|
let additionalInformation = { hasDelta: false }
|
|
41
41
|
const deserializer = new ResourceJsonDeserializer(edm, jsonContentTypeInfo)
|
|
42
42
|
return (edmObject, value) => {
|
|
43
|
+
const body = deserializer[name](edmObject, value, expand, additionalInformation)
|
|
44
|
+
|
|
43
45
|
return {
|
|
44
|
-
body
|
|
46
|
+
body,
|
|
45
47
|
expand,
|
|
46
48
|
additionalInformation
|
|
47
49
|
}
|
|
@@ -4,8 +4,6 @@ const {
|
|
|
4
4
|
Request,
|
|
5
5
|
ql: { SELECT }
|
|
6
6
|
} = cds
|
|
7
|
-
const LOG = cds.log()
|
|
8
|
-
const { normalizeError } = require('../../../../common/error/frontend')
|
|
9
7
|
|
|
10
8
|
const { getDeepSelect, getSimpleSelectCQN } = require('./handlerUtils')
|
|
11
9
|
const { hasDeepUpdate } = require('../../../../common/composition/update')
|
|
@@ -155,7 +155,9 @@ const _skipToMany = (entity, prop) => {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
|
|
158
|
-
|
|
158
|
+
// On app-service layer, generated foreign keys are not enumerable,
|
|
159
|
+
// include them here too.
|
|
160
|
+
for (const prop of Object.getOwnPropertyNames(newEntry)) {
|
|
159
161
|
if (keys.includes(prop)) {
|
|
160
162
|
_addKeysToResult(result, prop, newEntry, oldEntry)
|
|
161
163
|
continue
|
|
@@ -277,6 +277,9 @@ const checkIfAssocDeep = (element, value, req) => {
|
|
|
277
277
|
// managed to one
|
|
278
278
|
Object.keys(value).forEach(prop => {
|
|
279
279
|
if (typeof value[prop] !== 'object') {
|
|
280
|
+
const foreignKey = element._foreignKeys.find(fk => fk.childElement.name === prop)
|
|
281
|
+
if (foreignKey) return
|
|
282
|
+
|
|
280
283
|
const key = element.keys.find(element => element.ref[0] === prop)
|
|
281
284
|
if (key) return
|
|
282
285
|
|
|
@@ -11,6 +11,7 @@ const cds = require('../../cds')
|
|
|
11
11
|
const LOG = cds.log('app')
|
|
12
12
|
const { enrichDataWithKeysFromWhere } = require('../utils/keys')
|
|
13
13
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
14
|
+
const propagateForeignKeys = require('../utils/propagateForeignKeys')
|
|
14
15
|
const { checkInputConstraints, assertTargets } = require('../../cds-services/util/assert')
|
|
15
16
|
const getTemplate = require('../utils/template')
|
|
16
17
|
const templateProcessor = require('../utils/templateProcessor')
|
|
@@ -126,6 +127,11 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
|
126
127
|
const { row, key, element, isRoot } = elementInfo
|
|
127
128
|
category = _getSimpleCategory(category)
|
|
128
129
|
|
|
130
|
+
if (category === 'propagateForeignKeys') {
|
|
131
|
+
propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { enumerable: false })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
// remember mandatory
|
|
130
136
|
if (category === 'mandatory') {
|
|
131
137
|
value.mandatory = true
|
|
@@ -186,6 +192,11 @@ const _pick = element => {
|
|
|
186
192
|
// collect actions to apply
|
|
187
193
|
const categories = []
|
|
188
194
|
|
|
195
|
+
// REVISIT: element._foreignKeys.length seems to be a very broad check
|
|
196
|
+
if (element.isAssociation && element._foreignKeys.length) {
|
|
197
|
+
categories.push({ category: 'propagateForeignKeys' })
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
if (element['@assert.range'] || element['@assert.enum'] || element['@assert.format']) {
|
|
190
201
|
categories.push('assert')
|
|
191
202
|
}
|
|
@@ -241,7 +252,10 @@ async function _handler(req) {
|
|
|
241
252
|
if (!req.query) return // FIXME: the code below expects req.query to be defined
|
|
242
253
|
if (!req.target) return
|
|
243
254
|
|
|
244
|
-
const template = getTemplate('app-input', this, req.target, {
|
|
255
|
+
const template = getTemplate('app-input', this, req.target, {
|
|
256
|
+
pick: _pick,
|
|
257
|
+
ignore: element => element._isAssociationStrict
|
|
258
|
+
})
|
|
245
259
|
if (template.elements.size === 0) return
|
|
246
260
|
|
|
247
261
|
const errors = []
|
|
@@ -301,7 +315,8 @@ const _processActionFunctionRow = (row, param, key, errors, event, service) => {
|
|
|
301
315
|
|
|
302
316
|
// structured
|
|
303
317
|
const template = getTemplate('app-input-operation', service, param, {
|
|
304
|
-
pick: _pick
|
|
318
|
+
pick: _pick,
|
|
319
|
+
ignore: element => element._isAssociationStrict
|
|
305
320
|
})
|
|
306
321
|
|
|
307
322
|
if (template && template.elements.size) {
|
|
@@ -64,7 +64,10 @@ function _handler(req) {
|
|
|
64
64
|
const { elements } = req.target
|
|
65
65
|
for (const k in req.data) if (k in elements && elements[k]['@Core.MediaType']) return
|
|
66
66
|
|
|
67
|
-
const template = getTemplate('app-put', this, req.target, {
|
|
67
|
+
const template = getTemplate('app-put', this, req.target, {
|
|
68
|
+
pick: _pick,
|
|
69
|
+
ignore: element => element._isAssociationStrict
|
|
70
|
+
})
|
|
68
71
|
if (template.elements.size === 0) return
|
|
69
72
|
|
|
70
73
|
// REVISIT: req.data should point into req.query
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const LOG = cds.log('app')
|
|
3
2
|
|
|
4
3
|
const _getDateFromQueryOptions = str => {
|
|
5
4
|
if (str) {
|
|
@@ -11,8 +10,6 @@ const _getDateFromQueryOptions = str => {
|
|
|
11
10
|
|
|
12
11
|
const _isDate = dateStr => !dateStr.includes(':')
|
|
13
12
|
const _isTimestamp = dateStr => dateStr.includes('.')
|
|
14
|
-
const _isWarningRequired = (warning, queryOptions) =>
|
|
15
|
-
!warning && queryOptions && (queryOptions['sap-valid-from'] || queryOptions['sap-valid-to'])
|
|
16
13
|
const _isAsOfNow = queryOptions =>
|
|
17
14
|
!queryOptions || (!queryOptions['sap-valid-at'] && !queryOptions['sap-valid-to'] && !queryOptions['sap-valid-from'])
|
|
18
15
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const getTemplate = require('./template')
|
|
2
2
|
const templateProcessor = require('./templateProcessor')
|
|
3
3
|
|
|
4
|
-
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}={0,1}|[A-Za-z0-9+/]{2}={0,2})$/
|
|
5
|
-
|
|
6
4
|
// convert the standard base64 encoding to the URL-safe variant
|
|
7
5
|
const toBase64url = value =>
|
|
8
6
|
(Buffer.isBuffer(value) ? value.toString('base64') : value).replace(/\//g, '_').replace(/\+/g, '-')
|
|
@@ -17,12 +15,13 @@ const isInvalidBase64string = value => {
|
|
|
17
15
|
if (Buffer.isBuffer(value)) return // ok
|
|
18
16
|
|
|
19
17
|
// convert to standard base64 string; let it crash if typeof value !== 'string'
|
|
20
|
-
const
|
|
18
|
+
const base64value = value.replace(/_/g, '/').replace(/-/g, '+')
|
|
21
19
|
const normalized = normalizeBase64string(value)
|
|
22
20
|
|
|
23
21
|
// example of invalid base64 string --> 'WTGTdDsD/k21LnFRb+uNcAi=' <-- '...i=' must be '...g='
|
|
24
22
|
// see https://datatracker.ietf.org/doc/html/rfc4648#section-4
|
|
25
|
-
|
|
23
|
+
if (base64value.replace(/=/g, '') !== normalized.replace(/=/g, '')) return true
|
|
24
|
+
return base64value.length > normalized.length
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
const _picker = element => {
|
|
@@ -27,13 +27,18 @@ function _getOnCondElements(onCond, onCondElements = []) {
|
|
|
27
27
|
return onCondElements
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
30
|
+
function _mergeWhere(base, additional) {
|
|
31
|
+
if (additional?.length) {
|
|
32
32
|
// copy where else query will be modified
|
|
33
|
-
const whereCopy = deepCopyArray(
|
|
34
|
-
if (
|
|
35
|
-
|
|
33
|
+
const whereCopy = deepCopyArray(additional)
|
|
34
|
+
if (base.length > 0) base.push('and')
|
|
35
|
+
base.push(...whereCopy)
|
|
36
36
|
}
|
|
37
|
+
return base
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _modifyWhereWithNavigations(where, newWhere, entityKey, targetKey) {
|
|
41
|
+
_mergeWhere(newWhere, where)
|
|
37
42
|
|
|
38
43
|
newWhere.forEach(element => {
|
|
39
44
|
if (element.ref && element.ref[0] === targetKey) {
|
|
@@ -85,7 +90,10 @@ function _getWhereFromUpdate(query, target, model) {
|
|
|
85
90
|
return where
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
const where = query.UPDATE.where || []
|
|
94
|
+
if (query.UPDATE.entity.ref?.length === 1 && query.UPDATE.entity.ref[0].where)
|
|
95
|
+
return _mergeWhere(query.UPDATE.entity.ref[0].where, where)
|
|
96
|
+
return where
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
// params: data, req, service/tx
|