@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 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 true
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
 
@@ -1,4 +1,4 @@
1
- const DEBUG = /\b(y|all|serve)\b/.test (process.env.DEBUG) && console.warn
1
+ const DEBUG = /\b(y|all|serve|bindings)\b/.test (process.env.DEBUG) && console.warn
2
2
  // || console.debug
3
3
 
4
4
  const cds = require ('..')
@@ -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]
@@ -1,4 +1,4 @@
1
- const _runtime = '../../libx/_runtime'
1
+ const _runtime = '@sap/cds/libx/_runtime'
2
2
 
3
3
  exports = module.exports = {
4
4
 
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'} */
@@ -1,11 +1,11 @@
1
1
  const lib = require('../../libx/_runtime')
2
2
  const registry = {
3
- rest: lib.to.old_rest,
4
- new_rest: lib.to.new_rest,
5
- odata: lib.to.odata_v4,
6
- odata_v2: lib.to.odata_v4,
7
- odata_v4: lib.to.odata_v4,
8
- fiori: lib.to.odata_v4,
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
 
@@ -1,5 +1,6 @@
1
- const cds = require('..')
2
- const { path, isfile } = cds.utils
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 (_required[kind]) return _required[kind]
32
+ if (_require[kind]) return _require[kind]
31
33
  const {impl} = cds.requires[kind] || cds.error (`No configuration found for 'cds.requires.${kind}'`)
32
- return _required[kind] = _require (impl || cds.error (`No 'impl' configured for 'cds.requires.${kind}'`))
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
- if (it.startsWith('@sap/cds/')) it = cds.home + it.slice(8) //> for local tests in @sap/cds dev
38
- else if (it.startsWith('./')) it = _relative (d, it.slice(2)) //> relative to <service>.cds
39
- else if (it.startsWith('//')) it = path.resolve (cds.root,it.slice(2)) //> relative to cds.root
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
 
@@ -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
- return all.push (_new (Service, d,csn,o))
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) return
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
- const { Service } = require('../../../../../../lib/serve/factory')
21
- async function createNewService(name, csn, defaultOptions) {
22
- const options = Object.assign({}, defaultOptions)
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('./utils/restrictions')
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 && el.SELECT) {
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 and
71
- // the navigation starts with a draft entity (IsActiveEntity=false)
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 target is an association and
75
- // only if it isn't annotated with @odata.draft.enclosed
76
- const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
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 (query.INSERT.entries || query.INSERT.rows) return values.map(v => ({ affectedRows: 1, values: v }))
304
+ if (entriesOrRows) return values.map(v => ({ affectedRows: 1, values: v }))
288
305
  // INSERT into SELECT
289
306
  return [{ affectedRows }]
290
307
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.9.1",
3
+ "version": "5.9.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [