@sap/cds 5.9.1 → 5.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/compile/etc/_localized.js +3 -2
- package/lib/connect/bindings.js +1 -1
- package/lib/connect/index.js +2 -3
- package/lib/env/requires.js +1 -1
- package/lib/index.js +1 -0
- package/lib/serve/adapters.js +6 -6
- package/lib/serve/factory.js +14 -9
- package/lib/serve/index.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +11 -6
- package/libx/_runtime/cds-services/services/Service.js +1 -1
- package/libx/_runtime/common/generic/auth/restrict.js +3 -1
- package/libx/_runtime/{cds-services/services/utils → common/generic/auth}/restrictions.js +8 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
- package/libx/_runtime/fiori/generic/readOverDraft.js +9 -6
- package/libx/_runtime/hana/execute.js +18 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 5.9.2 - 2022-04-07
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- i18n translation for errors did not work correctly in some cases
|
|
12
|
+
- Normalization in custom `getRestrictions`
|
|
13
|
+
- Throw exception by `INSERT` into HANA queries if number of provided rows deviates from number of affected rows returned by hdb to prevent data losses
|
|
14
|
+
- Handler detection for extended services
|
|
15
|
+
- Speed-up in localization handling
|
|
16
|
+
- Draft: navigation via an association to many from a non-draft enabled entity to a draft-enabled entity
|
|
17
|
+
- Limited support of `SELECT` queries with operator expressions (`xpr`)
|
|
18
|
+
|
|
7
19
|
## Version 5.9.1 - 2022-03-31
|
|
8
20
|
|
|
9
21
|
### Fixed
|
|
@@ -97,8 +97,8 @@ function unfold_csn (m) { // NOSONAR
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
const $localized = '$$localized', _is_localized = (d,_path={}) => {
|
|
100
|
-
if (d.own($localized)) return
|
|
101
|
-
if (!d.elements || d.name.endsWith('.texts')) return false
|
|
100
|
+
if (typeof d.own($localized) === 'boolean') return d.own($localized)
|
|
101
|
+
if (!d.elements || d.name.endsWith('.texts')) return d.set($localized,false)
|
|
102
102
|
// if (d.elements.texts && d.elements.texts.target === `${d.name}.texts`) return d.set($localized,true)
|
|
103
103
|
for (let each in d.elements) {
|
|
104
104
|
const e = d.elements [each]
|
|
@@ -106,6 +106,7 @@ const $localized = '$$localized', _is_localized = (d,_path={}) => {
|
|
|
106
106
|
return d.set($localized,true)
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
return d.set($localized,false)
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
|
package/lib/connect/bindings.js
CHANGED
package/lib/connect/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const cds = require('..'), {one_model} = cds.env.features, LOG = cds.log('cds.connect')
|
|
2
|
-
const factory = require('../serve/factory')
|
|
3
2
|
const _pending = cds.services._pending || {} // used below to chain parallel connect.to(<same>)
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -22,7 +21,7 @@ const connect = module.exports = async function cds_connect (options) {
|
|
|
22
21
|
* @returns { Promise<import('../serve/Service-api')> }
|
|
23
22
|
*/
|
|
24
23
|
connect.to = async (datasource, options) => {
|
|
25
|
-
let Service = factory, _done = x=>x
|
|
24
|
+
let Service = cds.service.factory, _done = x=>x
|
|
26
25
|
if (typeof datasource === 'object') [options,datasource] = [datasource]
|
|
27
26
|
else if (datasource) {
|
|
28
27
|
if (datasource._is_service_class) [ Service, datasource ] = [ datasource, datasource.name ]
|
|
@@ -33,7 +32,7 @@ connect.to = async (datasource, options) => {
|
|
|
33
32
|
// queue parallel requests to a single promise, to avoid creating multiple services
|
|
34
33
|
_pending[datasource] = new Promise (r=>_done=r).finally(()=>{ delete _pending[datasource] })
|
|
35
34
|
}
|
|
36
|
-
const o = Service === factory ? options4 (datasource, options) : {}
|
|
35
|
+
const o = Service === cds.service.factory ? options4 (datasource, options) : {}
|
|
37
36
|
const m = await model4 (o)
|
|
38
37
|
// check if required service definition exists
|
|
39
38
|
const required = cds.requires[datasource]
|
package/lib/env/requires.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ if (global.cds) Object.assign(module,{exports:global.cds}) ; else {
|
|
|
12
12
|
/** @type {{ [path:string] : Service }} */ paths: {},
|
|
13
13
|
/** @type Service[] */ providers: [],
|
|
14
14
|
factory: require ('./serve/factory'),
|
|
15
|
+
adapters: require ('./serve/adapters'),
|
|
15
16
|
bindings: require ('./connect/bindings'),
|
|
16
17
|
})}
|
|
17
18
|
/** @type {import './req/context'} */
|
package/lib/serve/adapters.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const lib = require('../../libx/_runtime')
|
|
2
2
|
const registry = {
|
|
3
|
-
rest
|
|
4
|
-
new_rest
|
|
5
|
-
odata
|
|
6
|
-
odata_v2
|
|
7
|
-
odata_v4
|
|
8
|
-
fiori
|
|
3
|
+
get rest() { return lib.to.old_rest },
|
|
4
|
+
get new_rest() { return lib.to.new_rest },
|
|
5
|
+
get odata() { return lib.to.odata_v4 },
|
|
6
|
+
get odata_v2() { return lib.to.odata_v4 },
|
|
7
|
+
get odata_v4() { return lib.to.odata_v4 },
|
|
8
|
+
get fiori() { return lib.to.odata_v4 },
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
|
package/lib/serve/factory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const cds = require('..')
|
|
2
|
-
const
|
|
1
|
+
const cds = require('..'), { path, isfile } = cds.utils
|
|
2
|
+
const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
|
|
3
|
+
const DEBUG = cds.debug('srv.factory'); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
|
|
3
4
|
|
|
4
5
|
/** @typedef {import('./Service-api')} Service @type { (()=>Service) & (new()=>Service) } */
|
|
5
6
|
const ServiceFactory = function (name, model, options) { //NOSONAR
|
|
@@ -8,6 +9,7 @@ const ServiceFactory = function (name, model, options) { //NOSONAR
|
|
|
8
9
|
const serve = !cds.requires[name] || o.mocked
|
|
9
10
|
const defs = !model ? {[name]:{}} : model.definitions || cds.error (`Invalid argument for 'model': ${model}`)
|
|
10
11
|
const def = !name || name === 'db' ? {} : defs[name] || {}
|
|
12
|
+
DEBUG && DEBUG ({ name, definition:def, options:o })
|
|
11
13
|
|
|
12
14
|
let it /* eslint-disable no-cond-assign */
|
|
13
15
|
if (it = o.with) return _use (it) // from cds.serve (<options>)
|
|
@@ -27,21 +29,24 @@ const ServiceFactory = function (name, model, options) { //NOSONAR
|
|
|
27
29
|
|
|
28
30
|
function _required() {
|
|
29
31
|
const kind = o.kind = serve && def['@kind'] || o.kind || 'app-service'
|
|
30
|
-
if (
|
|
32
|
+
if (_require[kind]) return _require[kind]
|
|
31
33
|
const {impl} = cds.requires[kind] || cds.error (`No configuration found for 'cds.requires.${kind}'`)
|
|
32
|
-
|
|
34
|
+
DEBUG && DEBUG ('requires',{kind,impl})
|
|
35
|
+
return _require[kind] = _require (impl || cds.error (`No 'impl' configured for 'cds.requires.${kind}'`))
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const _require = (it,d) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
try { var resolved = require.resolve(it) } catch(e) {
|
|
40
|
+
DEBUG && d && DEBUG ('requires',{ service: d.name, source:_source(d), impl:it })
|
|
41
|
+
if (it.startsWith('@sap/cds/')) it = cds.home + it.slice(8) //> for local tests in @sap/cds dev
|
|
42
|
+
if (it.startsWith('./')) it = _relative (d,it.slice(2)) //> relative to <service>.cds
|
|
43
|
+
try { var resolved = require.resolve(it,{paths}) } catch(e) {
|
|
41
44
|
try { resolved = require.resolve(it = path.resolve(cds.root,it)) } catch(e) { // for compatibility
|
|
45
|
+
DEBUG && DEBUG (`Failed loading service implementation from '${it}'`, { 'cds.root':cds.root, paths })
|
|
42
46
|
throw cds.error(`Failed loading service implementation from '${it}'`)
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
DEBUG && DEBUG({resolved})
|
|
45
50
|
return require(resolved)
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -59,7 +64,7 @@ const sibling = (d) => {
|
|
|
59
64
|
let found
|
|
60
65
|
if (process.env.CDS_TYPESCRIPT === 'true') found = isfile(path.join(home, each, file + '.ts'))
|
|
61
66
|
if (!found) found = isfile(path.join(home, each, file + '.js'))
|
|
62
|
-
if (found) return found
|
|
67
|
+
if (found) return found //> equiv to '.'+found.slice(home.length)
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
package/lib/serve/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const { ProtocolAdapter } = require('./adapters')
|
|
2
|
-
const { Service } = require('./factory')
|
|
3
1
|
const cds = require ('..')
|
|
2
|
+
const { ProtocolAdapter } = cds.service.adapters
|
|
3
|
+
const { Service } = cds.service.factory
|
|
4
4
|
const _ready = Symbol(), _pending = cds.services._pending || {}
|
|
5
5
|
|
|
6
6
|
/** @param som - a service name or a model (name or csn) */
|
|
@@ -54,7 +54,8 @@ function cds_serve (som, _options) { // NOSONAR
|
|
|
54
54
|
// Shortcut for directly passed service classes
|
|
55
55
|
if (o.service && o.service._is_service_class) {
|
|
56
56
|
const Service = o.service, d = { name: o.service.name }
|
|
57
|
-
|
|
57
|
+
const srv = _new (Service, d,csn,o)
|
|
58
|
+
return all.push (srv)
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Get relevant service definitions from model...
|
|
@@ -66,7 +66,12 @@ function _log(level, arg) {
|
|
|
66
66
|
|
|
67
67
|
// reduce 4xx to warning
|
|
68
68
|
if (isClientError(obj)) {
|
|
69
|
-
if (!LOG._warn)
|
|
69
|
+
if (!LOG._warn) {
|
|
70
|
+
// restore
|
|
71
|
+
obj.message = _message
|
|
72
|
+
if (_details) obj.details = _details
|
|
73
|
+
return
|
|
74
|
+
}
|
|
70
75
|
level = 'warn'
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../../../../cds')
|
|
2
2
|
const OData = require('../OData')
|
|
3
|
+
const DEBUG = cds.debug('extensibility')
|
|
3
4
|
|
|
4
5
|
const { alias2ref } = require('../../../../common/utils/csn')
|
|
5
6
|
const { BASE_TENANT } = require('../../../../common/utils/extensibilityUtils')
|
|
@@ -17,14 +18,18 @@ function createOdataService(service) {
|
|
|
17
18
|
return odataService
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const service = new Service(name, csn, options)
|
|
24
|
-
if (!service.path) service.path = cds.service.path4(service)
|
|
21
|
+
async function createNewService(name, model, options) {
|
|
22
|
+
const { constructor: Service, path } = cds.services[name]
|
|
23
|
+
const service = new Service(name, model, { ...options }) // cloning options to be safe
|
|
25
24
|
if (service.init) await service.prepend(service.init)
|
|
26
25
|
if (options.impl) await service.prepend(options.impl)
|
|
27
|
-
|
|
26
|
+
if (path) service.path = path
|
|
27
|
+
DEBUG &&
|
|
28
|
+
DEBUG('Created tenant-specific service:', service.name, '= new', Service.name, {
|
|
29
|
+
_handlers: {
|
|
30
|
+
on: service._handlers.on.map(h => ({ on: h.on, handler: () => {} }))
|
|
31
|
+
}
|
|
32
|
+
})
|
|
28
33
|
return createOdataService(service)
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -8,7 +8,7 @@ const { postProcess } = require('../../common/utils/postProcessing')
|
|
|
8
8
|
const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
|
|
9
9
|
|
|
10
10
|
// for getRestrictions()
|
|
11
|
-
const { getNormalizedRestrictions, getApplicableRestrictions } = require('
|
|
11
|
+
const { getNormalizedRestrictions, getApplicableRestrictions } = require('../../common/generic/auth/restrictions.js')
|
|
12
12
|
const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
|
|
13
13
|
const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
14
14
|
|
|
@@ -2,6 +2,7 @@ const cds = require('../../../cds')
|
|
|
2
2
|
|
|
3
3
|
const { reject, getRejectReason, resolveUserAttrs, getAuthRelevantEntity } = require('./utils')
|
|
4
4
|
const { DRAFT_EVENTS, MOD_EVENTS } = require('./constants')
|
|
5
|
+
const { getNormalizedPlainRestrictions } = require('./restrictions')
|
|
5
6
|
|
|
6
7
|
const { cqn2cqn4sql } = require('../../utils/cqn2cqn4sql')
|
|
7
8
|
|
|
@@ -250,7 +251,8 @@ async function handler(req) {
|
|
|
250
251
|
// > no applicable restrictions -> 403
|
|
251
252
|
reject(req, getRejectReason(req, '@restrict', definition))
|
|
252
253
|
}
|
|
253
|
-
|
|
254
|
+
// normalize
|
|
255
|
+
restrictions = getNormalizedPlainRestrictions(restrictions, definition)
|
|
254
256
|
// at least one if the user's roles grants unrestricted access => done
|
|
255
257
|
if (restrictions.some(restrict => !restrict.where)) return
|
|
256
258
|
|
|
@@ -72,7 +72,14 @@ const getApplicableRestrictions = (restrictions, event, user) => {
|
|
|
72
72
|
})
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const getNormalizedPlainRestrictions = (restrictions, definition) => {
|
|
76
|
+
const result = []
|
|
77
|
+
for (const restriction of restrictions) _addNormalizedRestrict(restriction, result, definition)
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
module.exports = {
|
|
76
82
|
getNormalizedRestrictions,
|
|
77
|
-
getApplicableRestrictions
|
|
83
|
+
getApplicableRestrictions,
|
|
84
|
+
getNormalizedPlainRestrictions
|
|
78
85
|
}
|
|
@@ -501,8 +501,9 @@ const _convertNotEqual = (container, partName = 'where') => {
|
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
if (el
|
|
505
|
-
_convertNotEqual(el.SELECT, partName)
|
|
504
|
+
if (el) {
|
|
505
|
+
if (el.SELECT) _convertNotEqual(el.SELECT, partName)
|
|
506
|
+
if (el.xpr) _convertNotEqual(el, 'xpr')
|
|
506
507
|
}
|
|
507
508
|
})
|
|
508
509
|
|
|
@@ -715,6 +716,10 @@ const _convertToOneEqNullInFilter = (query, target) => {
|
|
|
715
716
|
|
|
716
717
|
// eslint-disable-next-line complexity
|
|
717
718
|
const _convertSelect = (query, model, _options) => {
|
|
719
|
+
const { _initial } = _options
|
|
720
|
+
if (_initial) {
|
|
721
|
+
delete _options._initial
|
|
722
|
+
}
|
|
718
723
|
const options = Object.assign(
|
|
719
724
|
{
|
|
720
725
|
_4db: _options.service instanceof cds.DatabaseService,
|
|
@@ -730,9 +735,6 @@ const _convertSelect = (query, model, _options) => {
|
|
|
730
735
|
query.SELECT.from = _convertSelect(query.SELECT.from, model, options)
|
|
731
736
|
}
|
|
732
737
|
|
|
733
|
-
// REVISIT: a temporary workaround for xpr from new parser
|
|
734
|
-
if (cds.env.features.odata_new_parser) _flattenCQN(query)
|
|
735
|
-
|
|
736
738
|
// lambda functions
|
|
737
739
|
convertWhereExists(query.SELECT, model, options)
|
|
738
740
|
|
|
@@ -793,6 +795,9 @@ const _convertSelect = (query, model, _options) => {
|
|
|
793
795
|
}
|
|
794
796
|
}
|
|
795
797
|
|
|
798
|
+
// temporary workaround for xpr - cds v5.9.2 only
|
|
799
|
+
if (_initial) _flattenCQN(query)
|
|
800
|
+
|
|
796
801
|
return query
|
|
797
802
|
}
|
|
798
803
|
|
|
@@ -932,7 +937,7 @@ const _convertUpdate = (query, model, options) => {
|
|
|
932
937
|
*/
|
|
933
938
|
const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
|
|
934
939
|
if (query.SELECT) {
|
|
935
|
-
return _convertSelect(query, model, options)
|
|
940
|
+
return _convertSelect(query, model, Object.assign(options, { _initial: true }))
|
|
936
941
|
}
|
|
937
942
|
|
|
938
943
|
if (query.UPDATE) {
|
|
@@ -67,19 +67,22 @@ const _shouldReadOverDraft = (req, definitions) => {
|
|
|
67
67
|
const rootEntityName = typeof firstFromRef === 'string' ? firstFromRef : firstFromRef.id
|
|
68
68
|
const rootEntity = definitions[rootEntityName]
|
|
69
69
|
|
|
70
|
-
// read over the draft only if the root entity is draft-enabled
|
|
71
|
-
|
|
72
|
-
if (!!rootEntity._isDraftEnabled && isActiveEntityRequested(firstFromRef.where)) return false
|
|
70
|
+
// read over the draft only if the root entity is draft-enabled
|
|
71
|
+
if (!rootEntity._isDraftEnabled) return false
|
|
73
72
|
|
|
74
|
-
// read over the draft if the navigation
|
|
75
|
-
//
|
|
76
|
-
|
|
73
|
+
// read over the draft only if the navigation starts from a draft entity, e.g.,
|
|
74
|
+
// /Books(ID=1, IsActiveEntity=false)
|
|
75
|
+
if (isActiveEntityRequested(firstFromRef.where)) return false
|
|
77
76
|
|
|
77
|
+
const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
|
|
78
78
|
const excludeAssoc = assoc => {
|
|
79
79
|
if (assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity') return true
|
|
80
80
|
return false
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Read over the draft only if:
|
|
84
|
+
// - the navigation target is an association and
|
|
85
|
+
// - isn't annotated with the @odata.draft.enclosed annotation
|
|
83
86
|
return _hasNavToNonDraftEnclosedAssoc(pathSegments, definitions, excludeAssoc)
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -280,11 +280,28 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
return _executeSimpleSQL(dbc, sql, values).then(affectedRows => {
|
|
283
|
+
const entriesOrRows = query.INSERT.entries || query.INSERT.rows
|
|
284
|
+
const affectedRowsCount = Array.isArray(affectedRows)
|
|
285
|
+
? affectedRows.reduce((sum, rows) => sum + rows, 0)
|
|
286
|
+
: affectedRows
|
|
287
|
+
if (entriesOrRows && entriesOrRows.length !== affectedRowsCount) {
|
|
288
|
+
LOG._warn &&
|
|
289
|
+
LOG.warn(
|
|
290
|
+
`INSERT input deviates from affected rows (input: ${entriesOrRows.length}, affectedRows: ${affectedRowsCount})`,
|
|
291
|
+
{
|
|
292
|
+
sql,
|
|
293
|
+
args: values && values.length,
|
|
294
|
+
values,
|
|
295
|
+
query
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
throw new Error('Possible data loss by INSERT into HANA db. Please, update a corresponding HANA driver.')
|
|
299
|
+
}
|
|
283
300
|
// InsertResult needs an object per row with its values
|
|
284
301
|
// query.INSERT.values -> one row
|
|
285
302
|
if (query.INSERT.values) return [{ affectedRows: 1, values: [values] }]
|
|
286
303
|
// query.INSERT.entries or .rows -> multiple rows
|
|
287
|
-
if (
|
|
304
|
+
if (entriesOrRows) return values.map(v => ({ affectedRows: 1, values: v }))
|
|
288
305
|
// INSERT into SELECT
|
|
289
306
|
return [{ affectedRows }]
|
|
290
307
|
})
|