@sap/cds 7.1.2 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +68 -4
  2. package/apis/cds.d.ts +10 -6
  3. package/apis/connect.d.ts +1 -2
  4. package/apis/core.d.ts +54 -5
  5. package/apis/log.d.ts +19 -6
  6. package/apis/models.d.ts +0 -18
  7. package/apis/ql.d.ts +23 -23
  8. package/apis/serve.d.ts +18 -15
  9. package/apis/services.d.ts +67 -56
  10. package/apis/test.d.ts +1 -2
  11. package/bin/serve.js +4 -4
  12. package/common.cds +4 -4
  13. package/lib/auth/basic-auth.js +1 -1
  14. package/lib/auth/dummy-auth.js +2 -1
  15. package/lib/auth/ias-auth.js +68 -2
  16. package/lib/auth/index.js +5 -5
  17. package/lib/auth/jwt-auth.js +40 -24
  18. package/lib/auth/mocked-users.js +0 -13
  19. package/lib/auth/passport-basic.js +2 -0
  20. package/lib/auth/passport-digest.js +2 -0
  21. package/lib/compile/etc/_localized.js +0 -1
  22. package/lib/compile/extend.js +16 -0
  23. package/lib/compile/for/lean_drafts.js +38 -6
  24. package/lib/compile/resolve.js +7 -5
  25. package/lib/compile/to/json.js +6 -2
  26. package/lib/dbs/cds-deploy.js +3 -3
  27. package/lib/env/cds-env.js +3 -3
  28. package/lib/env/cds-requires.js +1 -0
  29. package/lib/env/defaults.js +8 -1
  30. package/lib/env/schemas/cds-rc.json +27 -3
  31. package/lib/i18n/localize.js +3 -3
  32. package/lib/index.js +4 -0
  33. package/lib/log/cds-log.js +10 -1
  34. package/lib/ql/Whereable.js +7 -3
  35. package/lib/req/user.js +18 -16
  36. package/lib/srv/middlewares/sap-statistics.js +3 -3
  37. package/lib/srv/middlewares/trace.js +5 -4
  38. package/lib/srv/srv-dispatch.js +10 -9
  39. package/lib/utils/axios.js +3 -0
  40. package/lib/utils/cds-test.js +3 -0
  41. package/lib/utils/cds-utils.js +2 -0
  42. package/libx/_runtime/auth/index.js +8 -32
  43. package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
  44. package/libx/_runtime/auth/strategies/mock.js +1 -12
  45. package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
  47. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
  48. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
  50. package/libx/_runtime/common/composition/data.js +5 -3
  51. package/libx/_runtime/common/composition/insert.js +6 -3
  52. package/libx/_runtime/common/composition/update.js +12 -8
  53. package/libx/_runtime/common/error/constants.js +6 -1
  54. package/libx/_runtime/common/generic/auth/requires.js +11 -3
  55. package/libx/_runtime/common/generic/auth/restrict.js +21 -15
  56. package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
  57. package/libx/_runtime/common/generic/crud.js +6 -0
  58. package/libx/_runtime/common/generic/paging.js +3 -1
  59. package/libx/_runtime/common/i18n/messages.properties +1 -0
  60. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
  61. package/libx/_runtime/common/utils/resolveView.js +3 -1
  62. package/libx/_runtime/common/utils/restrictions.js +47 -0
  63. package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
  64. package/libx/_runtime/db/generic/input.js +1 -1
  65. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
  66. package/libx/_runtime/fiori/lean-draft.js +27 -24
  67. package/libx/_runtime/hana/driver.js +2 -4
  68. package/libx/_runtime/hana/pool.js +1 -1
  69. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  70. package/libx/_runtime/messaging/outbox/utils.js +1 -2
  71. package/libx/_runtime/remote/Service.js +10 -9
  72. package/libx/_runtime/remote/utils/client.js +4 -3
  73. package/libx/_runtime/sqlite/Service.js +0 -4
  74. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
  75. package/libx/odata/afterburner.js +5 -3
  76. package/libx/odata/cqn2odata.js +7 -7
  77. package/libx/odata/utils.js +4 -1
  78. package/libx/rest/RestAdapter.js +15 -16
  79. package/package.json +1 -1
  80. package/lib/auth/xsuaa-auth.js +0 -2
  81. package/libx/_runtime/auth/utils.js +0 -32
  82. package/libx/audit-log/client.cds +0 -0
  83. package/libx/audit-log/client.js +0 -0
@@ -10,19 +10,6 @@ class MockedUsers {
10
10
  if (typeof v === 'boolean') continue
11
11
  if (typeof v === 'string') v = { password:v }
12
12
  let id = _configured(v).id || k
13
-
14
- // Only for mock users the pseudo roles are kept in the role list.
15
- // In all other cases pseudo roles are filtered out.
16
- if (v.roles) {
17
- if (Array.isArray(v.roles)) {
18
- if (v.roles.includes('system-user')) v._is_system = true
19
- if (v.roles.includes('internal-user')) v._is_internal = true
20
- } else {
21
- if ('system-user' in v.roles) v._is_system = true
22
- if ('internal-user' in v.roles) v._is_internal = true
23
- }
24
- } else v.roles = []
25
-
26
13
  let u = users[id] = new User ({ id, ...v })
27
14
  let fts = tenants[u.tenant]?.features
28
15
  if (fts && !u.features) u.features = fts
@@ -1,3 +1,5 @@
1
+ // REVISIT: either document passport basic auth or remove it
2
+
1
3
  /* eslint-disable cds/no-missing-dependencies */
2
4
  module.exports = function passport_basic_auth (options) {
3
5
  // const session = require('express-session')({ secret:'secret', resave:false, saveUninitialized:true, })
@@ -1,3 +1,5 @@
1
+ // REVISIT: either document passport digest auth or remove it
2
+
1
3
  /* eslint-disable cds/no-missing-dependencies */
2
4
  module.exports = function passport_digest_auth (options) {
3
5
  // const session = require('express-session')({ secret:'secret', resave:false, saveUninitialized:true, })
@@ -2,7 +2,6 @@ const cds = require('../..'), {env} = cds
2
2
  const DEBUG = cds.debug('alpha|_localized')
3
3
  const _locales_4sql = {
4
4
  sqlite : env.i18n.for_sqlite || env.i18n.for_sql || [],
5
- h2 : env.i18n.for_sql || [],
6
5
  plain : env.i18n.for_sql || [],
7
6
  }
8
7
 
@@ -3,15 +3,31 @@ const { extend } = require ('../lazy')
3
3
 
4
4
  module.exports = o => o.definitions ? { with(...csns) {
5
5
 
6
+ // merge all extension csns
6
7
  const csn=o, merged = { definitions: {}, extensions: [] }
7
8
  for (const { definitions, extensions } of csns) {
8
9
  if (definitions) Object.assign(merged.definitions, definitions)
9
10
  if (extensions) merged.extensions.push(...extensions)
10
11
  }
12
+
13
+ // extend given base csn with merged extensions
11
14
  const extended = compile({
12
15
  'base.csn': compile.to.json(csn),
13
16
  'ext.csn': compile.to.json(merged)
14
17
  })
18
+
19
+ // handle localized extension elements
20
+ for (let ext of merged.extensions) {
21
+ for (let name in ext.elements) {
22
+ const e = ext.elements[name]
23
+ if (e.localized) {
24
+ // add localized element also to respective .texts entity
25
+ const texts = extended.definitions[ext.extend+'.texts']
26
+ texts.elements[name] ??= { ...e, localized:null }
27
+ }
28
+ }
29
+ }
30
+
15
31
  extended.$sources = csn.$sources // required to load resources like i18n later on
16
32
  return extended
17
33
 
@@ -72,17 +72,49 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
72
72
  // Positive list would be bigger (search, requires, fiori, ...)
73
73
  if (draft['@readonly']) draft['@readonly'] = undefined
74
74
  if (draft['@insertonly']) draft['@insertonly'] = undefined
75
- if (draft['@restrict']) draft['@restrict'] = undefined
76
- if ('@Capabilities.DeleteRestrictions.Deletable' in draft) draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
77
- if ('@Capabilities.InsertRestrictions.Insertable' in draft) draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
78
- if ('@Capabilities.UpdateRestrictions.Updatable' in draft) draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
79
- if ('@Capabilities.NavigationRestrictions.RestrictedProperties' in draft) draft['@Capabilities.NavigationRestrictions.RestrictedProperties'] = undefined
75
+ if (draft['@restrict']) {
76
+ const restrictions = ['CREATE', 'WRITE', '*']
77
+ draft['@restrict'] = draft['@restrict']
78
+ .map(d => ({
79
+ ...d,
80
+ grant:
81
+ d.grant && Array.isArray(d.grant)
82
+ ? d.grant.filter(g => restrictions.includes(g))
83
+ : typeof d.grant === 'string' && restrictions.includes(d.grant)
84
+ ? [d.grant]
85
+ : []
86
+ }))
87
+ .filter(r => r.grant.length > 0)
88
+ if (draft['@restrict'].length > 0) {
89
+ // Change WRITE & CREATE to NEW
90
+ draft['@restrict'] = draft['@restrict'].map(d => {
91
+ if (d.grant.includes('WRITE') || d.grant.includes('CREATE')) {
92
+ return { ...d, grant: 'NEW' }
93
+ }
94
+ return d
95
+ })
96
+ } else {
97
+ draft['@restrict'] = undefined
98
+ }
99
+ }
100
+ if ('@Capabilities.DeleteRestrictions.Deletable' in draft)
101
+ draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
102
+ if ('@Capabilities.InsertRestrictions.Insertable' in draft)
103
+ draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
104
+ if ('@Capabilities.UpdateRestrictions.Updatable' in draft)
105
+ draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
106
+ if ('@Capabilities.NavigationRestrictions.RestrictedProperties' in draft)
107
+ draft['@Capabilities.NavigationRestrictions.RestrictedProperties'] = undefined
80
108
 
81
109
  // Recursively add drafts for compositions
82
110
  for (const each in draft.elements) {
83
111
  const e = draft.elements[each]
84
112
  const newEl = Object.create(e)
85
- if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || ((!active['@Common.DraftRoot.ActivationAction'] || e._target === active) && _isCompositionBacklink(e))) {
113
+ if (
114
+ e.isComposition ||
115
+ (e.isAssociation && e['@odata.draft.enclosed']) ||
116
+ ((!active['@Common.DraftRoot.ActivationAction'] || e._target === active) && _isCompositionBacklink(e) && _isDraft(e._target))
117
+ ) {
86
118
  if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
87
119
  _redirect(newEl, addDraftEntity(e._target, model))
88
120
  }
@@ -14,7 +14,6 @@ const suffixes = [ '.csn', '.cds', sep+'index.csn', sep+'index.cds', sep+'csn.js
14
14
  * @returns and array of absolute filenames
15
15
  */
16
16
  module.exports = exports = function cds_resolve (model, o={}) { // NOSONAR
17
-
18
17
  if (!model || model === '--') return
19
18
  if (model._resolved) return model
20
19
  if (model === '*') return _resolve_all(o,this)
@@ -25,7 +24,7 @@ module.exports = exports = function cds_resolve (model, o={}) { // NOSONAR
25
24
  if (model.endsWith('/*')) return _resolve_subdirs_in(model,o,this)
26
25
 
27
26
  const cwd = o.root || this.root, local = resolve (cwd,model)
28
- const context = _paths(cwd,o), {cached} = context
27
+ const context = _paths(cwd,o,this), {cached} = context
29
28
  let id = model.startsWith('.') ? local : model
30
29
  if (id in cached) return cached[id]
31
30
 
@@ -93,11 +92,14 @@ function _resolve_subdirs_in (pattern='fts/*',o,cds) {
93
92
  }
94
93
  }
95
94
 
96
- function _paths (dir,o) {
95
+ function _paths (dir,o,cds) {
97
96
  const cache = o.cache || exports.cache
98
97
  const cached = cache[dir]; if (cached) return cached
99
- const a = dir.split(sep), n = a.length, nm = sep+'node_modules'
100
- const paths = [ dir, ...a.map ((_,i,a)=> a.slice(0,n-i).join(sep)+nm) ]
98
+ const a = dir.split(sep), n = a.length, paths = [ dir ]
99
+ const { cdsc: { moduleLookupDirectories }} = o.env ?? cds.env
100
+ for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
101
+ paths.push(...a.map ((_,i,a)=> a.slice(0,n-i).join(sep)+sep+mld))
102
+ }
101
103
  return cache[dir] = { paths, cached:{} }
102
104
  }
103
105
 
@@ -4,6 +4,7 @@ const path = require('path')
4
4
  module.exports = (csn,o={}) => {
5
5
  const relative = filename => (o.src !== o.cwd) ? path.relative(o.src, path.join(o.cwd, filename)) : filename
6
6
  const relative_cds_home = RegExp ('^' + path.relative (o.src || o.cwd || cds.root, cds.home) + '/')
7
+ const { moduleLookupDirectories } = cds.env.cdsc
7
8
 
8
9
  const resolver = (_,v) => {
9
10
 
@@ -20,9 +21,12 @@ module.exports = (csn,o={}) => {
20
21
  // Preserve original sources for services so we can use them for finding
21
22
  // sibling implementation files when reloaded from csn.json.
22
23
  let file = relative(v.$location.file)
23
- .replace(relative_cds_home,'@sap/cds/')
24
- .replace('node_modules/','')
25
24
  .replace(/\\/g,'/')
25
+ .replace(relative_cds_home,'@sap/cds/')
26
+ for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
27
+ file = file.replace(mld, '')
28
+ }
29
+
26
30
  // If there is still a relative path pointing outside of cwd, convert it to a module path
27
31
  // e.g. ../bookshop/srv/cat-service.cds -> @capire/bookshop/srv/cat-service.cds
28
32
  if (file.startsWith('../')) {
@@ -80,7 +80,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
80
80
  let schevo = o.schema_evolution === 'auto' || o['with-auto-schema-evolution'] || o['model-only'] || o['delta-from'] || (o.kind === 'postgres' && o.schema_evolution !== false);
81
81
  if (schevo) {
82
82
  const { prior, table_exists } = await get_prior_model()
83
- const { afterImage, drops: d, createsAndAlters } = cds.compile.to.sql.delta(csn, o, prior && JSON.parse(prior))
83
+ const { afterImage, drops: d, createsAndAlters } = cds.compile.to.sql.delta(csn, o, prior);
84
84
  const after = JSON.stringify(afterImage)
85
85
  if (!o.dry && after != prior) {
86
86
  if (!table_exists) {
@@ -135,7 +135,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
135
135
  let file = o['delta-from']
136
136
  if (file) {
137
137
  let prior = await cds.utils.read(file)
138
- return { prior }
138
+ return { prior: typeof prior === 'string' ? JSON.parse(prior) : prior }
139
139
  }
140
140
  if (o.dry) return {}
141
141
 
@@ -153,7 +153,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
153
153
 
154
154
  if (table_exists) {
155
155
  let [{ csn }] = await db.run('SELECT csn from cds_model')
156
- return { prior: csn, table_exists }
156
+ return { prior: csn && JSON.parse(csn), table_exists }
157
157
  }
158
158
  return { table_exists } // no prior csn
159
159
  }
@@ -33,6 +33,7 @@ class Config {
33
33
  if (NODE_ENV) profiles.push (NODE_ENV)
34
34
  if (CDS_ENV) profiles.push (...CDS_ENV.split(/\s*,\s*/))
35
35
  if (_home) _add_static_profiles (_home, profiles);
36
+ if (_home && this['project-nature'] === 'java') profiles.push('java')
36
37
  if (!profiles.includes('production')) profiles.push('development')
37
38
  this._profiles = new Set (profiles)
38
39
  this._profiles._defined = new Set
@@ -173,8 +174,8 @@ class Config {
173
174
  * For BAS only: to find out whether this is a Java or Node.js project
174
175
  */
175
176
  get "project-nature" () {
176
- const has_pom_xml = [this.folders.srv,'.'] .some (
177
- f => isfile (path.join (this._home, f, 'pom.xml'))
177
+ const has_pom_xml = [this.folders?.srv,'.'] .some (
178
+ f => f && isfile (path.join (this._home, f, 'pom.xml'))
178
179
  )
179
180
  return has_pom_xml ? 'java' : 'nodejs'
180
181
  }
@@ -193,7 +194,6 @@ class Config {
193
194
  // The following are internal APIs which can always change!
194
195
  //
195
196
 
196
-
197
197
  _add_to_process_env (cwd, filename) {
198
198
  const file = path.resolve (cwd,filename)
199
199
  try {
@@ -46,6 +46,7 @@ const _authentication_strategies = {
46
46
  tenants: {}
47
47
  },
48
48
  "mocked-auth": {
49
+ _kind: 'mocked', // REVISIT: workaround to distinguish from 'basic-auth' (cf. restrict_all_services)
49
50
  kind: 'basic-auth',
50
51
  users: {
51
52
  alice: { tenant: 't1', roles: [ ...admin ] },
@@ -149,7 +149,10 @@ const defaults = module.exports = {
149
149
  },
150
150
 
151
151
  build: {
152
- target: 'gen'
152
+ target: 'gen',
153
+ '[java]': {
154
+ target: '.'
155
+ }
153
156
  },
154
157
 
155
158
  mtx: {
@@ -163,6 +166,10 @@ const defaults = module.exports = {
163
166
  },
164
167
 
165
168
  cdsc: {
169
+ moduleLookupDirectories: ['node_modules/'],
170
+ '[java]': {
171
+ moduleLookupDirectories: ['node_modules/', 'target/cds/'],
172
+ }
166
173
  // cv2: {
167
174
  // _localized_entries: true,
168
175
  // _texts_entries: true,
@@ -11,7 +11,7 @@
11
11
  "profile": {
12
12
  "description": "A single static profile",
13
13
  "anyOf": [
14
- { "enum": [ "mtx-sidecar", "with-mtx-sidecar" ] },
14
+ { "enum": [ "mtx-sidecar", "with-mtx-sidecar", "java" ] },
15
15
  { "type": "string" }
16
16
  ]
17
17
  },
@@ -257,8 +257,32 @@
257
257
  "description": "Enables new middlewares support (experimental)."
258
258
  },
259
259
  "multitenancy": {
260
- "type": "boolean",
261
- "description": "Shortcut to enable multitenancy."
260
+ "oneOf": [
261
+ {
262
+ "type": "boolean",
263
+ "description": "Shortcut to enable multitenancy."
264
+ },
265
+ {
266
+ "type": "object",
267
+ "description": "Multitenancy configuration options.",
268
+ "properties": {
269
+ "jobs": {
270
+ "type": "object",
271
+ "description": "Configuration options for the built-in async job executor.",
272
+ "properties": {
273
+ "workerSize": {
274
+ "type": "number",
275
+ "description": "Number of workers running in parallel per database."
276
+ },
277
+ "clusterSize": {
278
+ "type": "number",
279
+ "description": "Number of databases executing parallel tasks."
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ ]
262
286
  },
263
287
  "extensibility": {
264
288
  "oneOf": [
@@ -3,7 +3,7 @@ const {existsSync, readdirSync} = require ('fs')
3
3
  const {join,dirname,resolve,parse,sep} = require ('path')
4
4
 
5
5
  const DEBUG = cds.debug('i18n')
6
- const _node_modules = sep + 'node_modules'
6
+ const _node_modules = cds.env.cdsc.moduleLookupDirectories.map(d => sep+d.slice(0, -1))
7
7
 
8
8
  module.exports = Object.assign (localize, {
9
9
  bundles4, folders4, folder4, bundle4
@@ -182,8 +182,8 @@ function folder4 (loc) {
182
182
  }
183
183
  //> no --> search up the folder hierarchy up to cds.root, cds.home, or some .../node_modules/<package>
184
184
  let next = dirname(loc)
185
- if (next.includes(_node_modules)) {
186
- if (next.endsWith(_node_modules)) return folder4[loc] = null
185
+ if (_node_modules.some(m => next.includes(m))) {
186
+ if (_node_modules.some(m => next.endsWith(m))) return folder4[loc] = null
187
187
  } else {
188
188
  if (!(
189
189
  next.startsWith(cds.root) ||
package/lib/index.js CHANGED
@@ -139,3 +139,7 @@ global.cds = cds // REVISIT: using global.cds seems wrong
139
139
 
140
140
  // install jest util if jest is defined
141
141
  if (process.env.CDS_JEST_MEM_FIX && typeof jest !== 'undefined') require('./utils/jest.js')
142
+
143
+ // Allow for import cds from '@sap/cds' without esModuleInterop
144
+ Object.defineProperty(module.exports, "__esModule", { value: true });
145
+ module.exports.default = module.exports
@@ -1,5 +1,6 @@
1
1
  const cds = require ('../index'), conf = cds.env.log
2
2
  const log = module.exports = exports = cds_log
3
+ const path = require('path')
3
4
 
4
5
  /**
5
6
  * Cache used for all constructed loggers.
@@ -167,7 +168,15 @@ const { ERROR, WARN, INFO, DEBUG, TRACE } = exports.levels = {
167
168
 
168
169
  ;(function _init() {
169
170
  const conf = cds.env.log
170
- if (conf.Logger) exports.Logger = require (conf.Logger) // Use configured logger in case of cds serve
171
+ if (conf.Logger) {
172
+ let resolvedPath
173
+ try { resolvedPath = require.resolve(conf.Logger) } catch {
174
+ try { resolvedPath = require.resolve(path.join(cds.root, conf.Logger)) } catch {
175
+ throw new Error(`Cannot find logger at "${conf.Logger}"`)
176
+ }
177
+ }
178
+ exports.Logger = require (resolvedPath) // Use configured logger in case of cds serve
179
+ }
171
180
  if (conf.service) {
172
181
  const {app} = cds, serveIn = app => require('./service').serveIn(app)
173
182
  app ? setImmediate(() => serveIn(app)) : cds.on('bootstrap', app => serveIn(app))
@@ -78,12 +78,16 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
78
78
  pred.push('or', ...predicate4([x],_clause))
79
79
  continue
80
80
  }
81
+ if (k === 'not') {
82
+ pred.push('not', {xpr:predicate4([x],_clause)})
83
+ continue
84
+ }
81
85
  if (k === 'exists') {
82
- pred.push(pred.length && 'and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
86
+ pred.push('and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
83
87
  continue
84
88
  }
85
89
  if (k === 'not exists') {
86
- pred.push(pred.length && 'and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
90
+ pred.push('and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
87
91
  continue
88
92
  }
89
93
  else pred.push('and', parse.expr(k))
@@ -97,7 +101,7 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
97
101
  else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
98
102
  else pred.push('=', {val:x})
99
103
  }
100
- return pred.slice(1)
104
+ return pred[0] === 'and' ? pred.slice(1) : pred
101
105
  }
102
106
 
103
107
  const _fluid_predicate = (args) => { // e.g. .where ('ID=',4711, 'and stock >=',1)
package/lib/req/user.js CHANGED
@@ -1,5 +1,3 @@
1
- const PSEUDO_ROLES = ['system-user', 'internal-user']
2
-
3
1
  class User {
4
2
 
5
3
  constructor (_) {
@@ -8,27 +6,31 @@ class User {
8
6
  if (new.target === Anonymous) return
9
7
  else return new User.default
10
8
  }
11
- if (typeof _ === 'string') { this.id = _; return }
12
- for (let each in _) super[each === '_roles' ? 'roles' : each] = _[each] // overrides getters
13
- const roles = this.hasOwnProperty('roles') && this.roles // eslint-disable-line no-prototype-builtins
14
- if (Array.isArray(roles)) this.roles = roles.filter(r => !PSEUDO_ROLES.includes(r)).reduce ((p,n)=>{p[n]=1; return p},{})
15
- else PSEUDO_ROLES.forEach(r => delete this.roles[r])
9
+ else if (typeof _ === 'string') this.id = _
10
+ else Object.assign(this,_)
16
11
  }
17
12
 
18
13
  get attr() { return super.attr = {} }
14
+ set attr(a) { super.attr = a }
15
+
19
16
  get roles(){ return super.roles = {} }
20
- get _roles(){ return this.roles } // compatibility
21
-
22
- is (role) {
23
- return role === 'any' ||
24
- role === 'identified-user' ||
25
- role === 'system-user' && this._is_system ||
26
- role === 'internal-user' && this._is_internal ||
27
- role === 'authenticated-user' ||
28
- !!this.roles[role]
17
+ set roles(r) {
18
+ super.roles = !Array.isArray(r) ? r : r.reduce((p,n)=>{ p[n]=1; return p },{})
19
+ }
20
+
21
+ is (role) {
22
+ return (
23
+ role === 'authenticated-user' ||
24
+ role === 'identified-user' ||
25
+ role === 'any' ||
26
+ !!this.roles[role]
27
+ )
29
28
  }
30
29
  valueOf() { return this.id }
31
30
 
31
+ // compatibility
32
+ get _roles(){ return this.roles }
33
+ set _roles(r){ this.roles = r }
32
34
  }
33
35
 
34
36
  /**
@@ -1,10 +1,10 @@
1
- const { performance:{now} } = require ('perf_hooks')
1
+ const { performance } = require ('perf_hooks')
2
2
 
3
3
  module.exports = (prec = 1000) => function sap_statistics (req, res, next) {
4
4
  if (req.query['sap-statistics'] || req.headers['sap-statistics']) {
5
- const { writeHead } = res, t0 = now()
5
+ const { writeHead } = res, t0 = performance.now()
6
6
  res.writeHead = function (...args) {
7
- const total = ((now() - t0) / prec).toFixed(2)
7
+ const total = ((performance.now() - t0) / prec).toFixed(2)
8
8
  if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
9
9
  writeHead.call(this, ...args)
10
10
  }
@@ -24,19 +24,20 @@ if (!LOG._debug) module.exports = ()=>[]; else {
24
24
  }
25
25
  }
26
26
 
27
- const { performance:{now} } = require ('perf_hooks')
27
+ const { performance } = require ('perf_hooks')
28
28
  const { format } = require ('util')
29
29
 
30
30
  class PerfTrace extends Array {
31
31
  log (...details) {
32
- const e = { details, start:now() }
32
+ const e = { details, start:performance.now() }
33
+ console.log(e)
33
34
  return this.push(e), e
34
35
  }
35
36
  done (e) {
36
- return e.stop = now()
37
+ return e.stop = performance.now()
37
38
  }
38
39
  toString ({truncate}) {
39
- const t0 = this[0].start; if (!this[0].stop) this[0].stop = now()
40
+ const t0 = this[0].start; if (!this[0].stop) this[0].stop = performance.now()
40
41
  return '\n'+ this.map (e => truncate (format (
41
42
  (e.start - t0).toFixed(2).padStart(6), '→',
42
43
  (e.stop - t0).toFixed(2).padEnd(6), '= ',
@@ -114,16 +114,17 @@ const _ensure_target = (srv,req) => {
114
114
  req.target = typeof q === 'object' ? cds.infer(q,defs) : defs[req.path]
115
115
  }
116
116
 
117
- const _ensure_fqn = (x,p,srv, name = x[p]) => {
118
- if (typeof name === 'string') {
117
+ const _ensure_fqn = (x,p,srv, from = x[p]) => {
118
+ if (!from) return
119
+ if (typeof from === 'string') {
119
120
  if (srv.isDatabaseService) return
120
- if (srv.model && name in srv.model.definitions) return
121
- if (name.startsWith(srv.namespace)) return
122
- if (name.endsWith('_drafts')) return // REVISIT: rather fix test/fiori/localized-draft.test.js ?
123
- else x[p] = `${srv.namespace}.${name}`
124
- } else if (name.ref) {
125
- const [head] = name.ref
126
- head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(name.ref,0,srv)
121
+ if (srv.model && from in srv.model.definitions) return
122
+ if (from.startsWith(srv.namespace)) return
123
+ if (from.endsWith('_drafts')) return // REVISIT: rather fix test/fiori/localized-draft.test.js ?
124
+ else x[p] = `${srv.namespace}.${from}`
125
+ } else if (from.ref) {
126
+ const [head] = from.ref
127
+ head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(from.ref,0,srv)
127
128
  if (x.where) for (let y of x.where) if (y.SELECT) _ensure_fqn(y.SELECT,'from',srv)
128
129
  }
129
130
  }
@@ -44,6 +44,9 @@ const _error = (e) => {
44
44
  })
45
45
  const { code, message } = e.response && e.response.data && e.response.data.error || {}
46
46
  if (message) e.message = code && code !== 'null' ? `${code} - ${message}` : message
47
+ // Promote toJSON from prototype to own property to make it iterable
48
+ // eslint-disable-next-line no-self-assign
49
+ if (typeof jest !== 'undefined') e.toJSON = e.toJSON
47
50
  throw e
48
51
  }
49
52
 
@@ -23,6 +23,9 @@ class Test extends require('./axios') {
23
23
 
24
24
  // launch cds server...
25
25
  before (`launching ${cmd} ${args.join(' ')}...`, async () => {
26
+ // cds.plugins is filling cds.env before cds serve -> profile must be parsed here
27
+ const profile = args.indexOf('--profile')
28
+ if (profile !== -1) process.env.CDS_ENV = [...process.env.CDS_ENV?.split(',') ?? [], ...args[profile+1].split(',')]
26
29
  await cds.plugins
27
30
  cds.once ('listening', ({server,url}) => {
28
31
  const axp = Reflect.getOwnPropertyDescriptor(this,'axios')
@@ -5,6 +5,8 @@ const ux = module.exports = exports = new class {
5
5
  get inflect() { return super.inflect = require('./inflect') }
6
6
  get inspect() { return super.inspect = require('util').inspect }
7
7
  get uuid() { return super.uuid = require('@sap/cds-foss').uuid }
8
+ get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
9
+ get pool() { return super.pool = require('@sap/cds-foss').pool }
8
10
  get tar() { return super.tar = require('./tar') }
9
11
  }
10
12
 
@@ -2,7 +2,8 @@ const cds = require('../cds')
2
2
  const LOG = cds.log()
3
3
 
4
4
  const _require = require('../common/utils/require')
5
- const { UNAUTHORIZED, FORBIDDEN, isRestricted } = require('./utils')
5
+ const { containsAnyRestrictions } = require('../common/utils/restrictions')
6
+ const { ODATA_UNAUTHORIZED } = require('../common/error/constants')
6
7
 
7
8
  let passport, logged
8
9
 
@@ -60,7 +61,7 @@ const _log = (req, challenges) => {
60
61
  const cap_auth_callback = (req, res, next, internalError, user, arg) => {
61
62
  // An internal error occurs during the authentication process
62
63
  if (internalError) {
63
- return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
64
+ return res.status(401).json({ error: ODATA_UNAUTHORIZED }) // no details to client
64
65
  }
65
66
 
66
67
  let challenges
@@ -106,10 +107,9 @@ const _mountMockAuth = (srv, app, strategy, config) => {
106
107
 
107
108
  const _mountPassportAuth = (srv, app, strategy, config) => {
108
109
  if (strategy in { jwt: 1, xsuaa: 1 } && !config.credentials) {
109
- LOG._warn &&
110
- LOG.warn(`Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.
111
- This is NOT recommended in production!`)
112
- return
110
+ let msg = `Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.`
111
+ msg += ' Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
112
+ throw new Error(msg)
113
113
  }
114
114
 
115
115
  if (!passport) passport = _require('passport')
@@ -140,11 +140,12 @@ module.exports = (srv, options = srv.options) => {
140
140
  // NOTE: options.auth is not an official API
141
141
  let config = 'auth' in options ? options.auth : cds.env.requires.auth
142
142
  if (!config) {
143
+ // REVISIT: can config be falsy? req.user would be undefined!
143
144
  if (cds.requires.db && cds.requires.multitenancy) {
144
145
  process.exitCode = 1 // REVISIT: why exitCode needed?
145
146
  throw new Error('Authentication required for multitenancy')
146
147
  }
147
- if (isRestricted(srv)) {
148
+ if (containsAnyRestrictions(srv)) {
148
149
  process.exitCode = 1 // REVISIT: why exitCode needed?
149
150
  throw new Error('Authentication required for authorization checks')
150
151
  }
@@ -161,12 +162,6 @@ module.exports = (srv, options = srv.options) => {
161
162
  // mount authentication middleware or strategy
162
163
  if (!logged) LOG._debug && LOG.debug(`Using authentication`, { kind: config.kind })
163
164
 
164
- // Security by default: set restrict_all_services if not disabled
165
- // this is done dynamically to also cover custom auth impl
166
- if (process.env.NODE_ENV === 'production' && config.restrict_all_services !== false) {
167
- config.restrict_all_services = true
168
- }
169
-
170
165
  if (config.impl) {
171
166
  // mount custom authentication middleware
172
167
  _mountCustomAuth(srv, app, config)
@@ -184,15 +179,6 @@ module.exports = (srv, options = srv.options) => {
184
179
  }
185
180
  }
186
181
 
187
- // Security by default: enforce authenticated users in production if auth service bound
188
- if (
189
- cds.requires.multitenancy ||
190
- (process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
191
- ) {
192
- if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
193
- app.use(cap_enforce_login)
194
- }
195
-
196
182
  // so we don't log the same stuff multiple times
197
183
  logged = true
198
184
 
@@ -206,13 +192,3 @@ const _strategy4 = config => {
206
192
  process.exitCode = 1 // REVISIT: why exitCode needed?
207
193
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
208
194
  }
209
-
210
- const cap_enforce_login = (req, res, next) => {
211
- if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
212
- if (!req.user || req.user._is_anonymous) {
213
- if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
214
- return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
215
- } else {
216
- return res.status(403).json({ error: FORBIDDEN }) // no details to client
217
- }
218
- }