@sap/cds 7.7.0 → 7.7.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,23 @@
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 7.7.2 - 2024-03-11
8
+
9
+ ### Fixed
10
+
11
+ - Requests to actions/functions on entities in draft state via navigation.
12
+ - PUT/PATCH with if-none-match: * forces insert
13
+
14
+ ## Version 7.7.1 - 2024-03-06
15
+
16
+ ### Fixed
17
+
18
+ - JWT authentication for Event Mesh endpoints
19
+ - `cds.log`'s json formatter: ensure `type` is set (required on kubernetes until CLS defaults this)
20
+ - Erroneously generated foreign keys in `req.data` for UPDATE using path expressions
21
+ - `INSERT.columns.rows` for multiple nested composition of aspects
22
+ - Paths passed to `tar` on Windows are now normalized to use forward slashes.
23
+
7
24
  ## Version 7.7.0 - 2024-02-26
8
25
 
9
26
  ### Added
@@ -6,7 +6,6 @@ const compile = module.exports = Object.assign (cds_compile, {
6
6
  for: new class {
7
7
  get java(){ return super.java = require('./for/java') }
8
8
  get nodejs() { return super.nodejs = require('./for/nodejs') }
9
- get drafts() { return super.drafts = require('./for/drafts') }
10
9
  get lean_drafts() { return super.lean_drafts = require('./for/lean_drafts') }
11
10
  get odata() { return super.odata = require('./for/odata') }
12
11
  get sql() { return super.sql = require('./for/sql') }
@@ -98,21 +98,7 @@ function unfold_csn (m) { // NOSONAR
98
98
  }
99
99
 
100
100
 
101
- const _is_localized = (
102
- !env.features._ucsn_ ? d => d.own('$localized') // as set by compiler in compile.for.odata
103
- : (d,_path={}) => d.own('$localized', ()=>{ // calculate our own for effective CSNs
104
- // if (d.elements.texts && d.elements.texts.target === `${d.name}.texts`) return d.set($localized,true)
105
- if (!d.elements || d.name.endsWith('.texts')) return false
106
- for (let each in d.elements) {
107
- const e = d.elements [each]
108
- if (e.localized || e._target && !(_path && e._target.name in _path) && _is_localized(e._target,Object.assign(_path, { [d.name]:1 }))) {
109
- return true
110
- }
111
- }
112
- return false
113
- })
114
- )
115
-
101
+ const _is_localized = d => d.own('$localized') // as set by compiler in compile.for.odata
116
102
 
117
103
  // feature-toggled exports
118
104
  module.exports = { unfold_csn, unfold_ddl }
@@ -1,9 +1,23 @@
1
+ // The only entry point to produce CSNs consumed by the Java Runtime
2
+
1
3
  const cds = require ('../../index')
2
4
 
5
+ // TODO: compiler functions to clarify - publish as individual API functions or
6
+ // have a compiler API function for.java?
7
+ function _4java (csn,o) {
8
+ const compile = require ('../cdsc');
9
+ const { addTenantFields } = require ('@sap/cds-compiler/lib/transform/addTenantFields');
10
+ const _4draft = require ('@sap/cds-compiler/lib/transform/draft/odata');
11
+ const dsn = JSON.parse (JSON.stringify (csn)) // REVISIT: workaround for bad test setup
12
+ o = compile._options.for.odata(o); // get compiler options, see compile.for.odata
13
+ if (o.tenantDiscriminator) addTenantFields (dsn, o);
14
+ return _4draft (dsn, o);
15
+ }
16
+
3
17
  module.exports = function cds_compile_for_java (csn,o) {
4
18
  if ('_4java' in csn) return csn._4java
5
- let dsn = csn // cds.minify (csn)
6
- dsn = cds.compile.for.drafts (csn,o)
19
+ // cds.minify (csn) ?
20
+ const dsn = (!cds.env.features._ucsn_) ? cds.compile.for.odata (csn,o||{}) : _4java (csn,o||{});
7
21
  if (dsn.definitions) for (let [name,d] of Object.entries(dsn.definitions)) {
8
22
  // Add @cds.external to external services
9
23
  if (d.kind === 'service' && name in cds.requires) d['@cds.external'] = true
@@ -16,4 +30,4 @@ module.exports = function cds_compile_for_java (csn,o) {
16
30
  Object.defineProperty (csn, '_4java', {value:dsn})
17
31
  Object.defineProperty (dsn, '_4java', {value:dsn})
18
32
  return dsn
19
- }
33
+ }
@@ -6,7 +6,7 @@ module.exports = function cds_compile_for_nodejs (csn,o) {
6
6
  if ('_4nodejs' in csn) return csn._4nodejs
7
7
  TRACE?.time('cds.compile 4nodejs'.padEnd(22))
8
8
  let dsn = csn // cds.minify (csn)
9
- dsn = cds.compile.for.drafts (csn,o) //> creates a partial copy -> avoid any cds.linked() before
9
+ dsn = cds.compile.for.odata (csn,o) //> creates a partial copy -> avoid any cds.linked() before
10
10
  dsn = unfold_csn (dsn)
11
11
  dsn = cds.linked (dsn)
12
12
  if (cds.env.fiori.lean_draft) cds.compile.for.lean_drafts(dsn, o)
@@ -86,6 +86,9 @@ module.exports = function format(module, level, ...args) {
86
86
  // append remaining args via util.format()
87
87
  if (args.length) toLog.msg = toLog.msg ? util.format(toLog.msg, ...args) : util.format(...args)
88
88
 
89
+ // ensure type (required on kubernetes if logs are pulled instead of pushed through binding)
90
+ toLog.type ??= 'log'
91
+
89
92
  // REVISIT: should not be necessary with new protocol adapters
90
93
  // 4xx: lower to warning (if error)
91
94
  if (toLog.level && toLog.level.match(/error/i) && _is4xx(toLog)) toLog.level = 'warn'
package/lib/utils/tar.js CHANGED
@@ -11,7 +11,7 @@ const _resolve = (...x) => path.resolve (cds.root,...x)
11
11
  // tar does not work properly on Windows (by npm/jest tests) w/o this change
12
12
  const win = path => {
13
13
  if (!path) return path
14
- if (typeof path === 'string') return path.replace('C:', '//localhost/c$')
14
+ if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
15
15
  if (Array.isArray(path)) return path.map(el => win(el))
16
16
  }
17
17
 
@@ -101,7 +101,9 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
101
101
  result = await tx.dispatch(req)
102
102
  } catch (e) {
103
103
  const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
104
- if (is404 && _isUpsertAllowed(req)) {
104
+ const isForcedInsert =
105
+ (e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
106
+ if ((is404 || isForcedInsert) && _isUpsertAllowed(req)) {
105
107
  // PUT/ PATCH with if-match header means "only if already exists", i.e., no insert if not
106
108
  if (req.headers['if-match'])
107
109
  throw Object.assign(new Error('412'), { statusCode: 412 })
@@ -37,7 +37,7 @@ function where2obj(where, target = null, data = {}) {
37
37
  // optional validation if target is passed
38
38
  if (target) {
39
39
  const colEl = target.elements[colName]
40
- if (!colEl || !(colEl.key || colEl.__foreignKey4)) continue
40
+ if (!colEl || !colEl.key) continue
41
41
  }
42
42
  const opWhere = where[i + 1]
43
43
  const valWhere = where[i + 2]
@@ -803,7 +803,7 @@ class JoinCQNFromExpanded {
803
803
  return 'DRAFT.DraftAdministrativeData'
804
804
  }
805
805
 
806
- if (isActiveRequired && !defaultLanguage) {
806
+ if (this._SELECT.localized !== false && isActiveRequired && !defaultLanguage) {
807
807
  const locale = this._locale ? `${this._locale}.` : ''
808
808
  const localized = `localized.${locale}${target}`
809
809
  if (this._csn.definitions[localized]) {
@@ -243,7 +243,7 @@ class InsertBuilder extends BaseBuilder {
243
243
  if (this.uuidKeys) {
244
244
  for (const key of this.uuidKeys) {
245
245
  if (!flattenColumnMap.get(key)) {
246
- columns.push(...this.uuidKeys.map(key => this._quoteElement(key)))
246
+ columns.push(this._quoteElement(key))
247
247
  }
248
248
  }
249
249
  }
@@ -469,9 +469,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
469
469
  if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
470
470
  if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
471
471
  const rootQuery = query.clone()
472
- const columns = entity_keys(query._target).map(k => ({ ref: [k] }))
473
- columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
474
- rootQuery.SELECT.columns = columns
472
+ rootQuery.SELECT.columns = [{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }]
475
473
  rootQuery.SELECT.one = true
476
474
  rootQuery.SELECT.from = { ref: [query.SELECT.from.ref[0]] }
477
475
  const root = await cds.run(rootQuery)
@@ -17,15 +17,15 @@ class EndpointRegistry {
17
17
  if (isSecured()) {
18
18
  if (cds.requires.auth.impl) {
19
19
  if (cds.env.requires.middlewares !== false) {
20
- paths.forEach(path => cds.app.use(path, cds.middlewares.before)) // contains auth, trace, context
20
+ cds.app.use(basePath, cds.middlewares.before) // contains auth, trace, context
21
21
  } else {
22
22
  const impl = _require(cds.resolve(cds.requires.auth.impl))
23
- paths.forEach(path => cds.app.use(path, impl))
23
+ cds.app.use(basePath, impl)
24
24
  }
25
25
  } else {
26
26
  if (cds.env.requires.middlewares !== false) {
27
27
  const jwt_auth = require('../../../../lib/auth/jwt-auth.js')
28
- paths.forEach(path => cds.app.use(path, jwt_auth(cds.requires.auth)))
28
+ cds.app.use(basePath, jwt_auth(cds.requires.auth))
29
29
  } else {
30
30
  const JWTStrategy = require('../../auth/strategies/JWT.js')
31
31
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
@@ -33,28 +33,22 @@ class EndpointRegistry {
33
33
  // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
34
34
  // In principle, user-facing endpoints might differ from messaging ones.
35
35
  passport.use(new JWTStrategy(cds.requires.auth.credentials))
36
- paths.forEach(path => {
37
- cds.app.use(path, passport.initialize())
38
- cds.app.use(path, passport.authenticate('JWT', { session: false }))
39
- })
36
+ cds.app.use(basePath, passport.initialize())
37
+ cds.app.use(basePath, passport.authenticate('JWT', { session: false }))
40
38
  }
41
39
  }
42
40
  // unsuccessful auth doesn't automatically reject!
43
- paths.forEach(path => {
44
- cds.app.use(path, (req, res, next) => {
45
- // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
46
- if (!req.user) res.status(401).json({ error: ODATA_UNAUTHORIZED })
47
- next()
48
- })
41
+ cds.app.use(basePath, (req, res, next) => {
42
+ // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
43
+ if (!req.user) res.status(401).json({ error: ODATA_UNAUTHORIZED })
44
+ next()
49
45
  })
50
46
  } else if (process.env.NODE_ENV === 'production') {
51
47
  LOG.warn('Messaging endpoints not secured')
52
48
  }
53
- paths.forEach(path => {
54
- cds.app.use(path, express.json({ type: 'application/*+json' }))
55
- cds.app.use(path, express.json())
56
- cds.app.use(path, express.urlencoded({ extended: true }))
57
- })
49
+ cds.app.use(basePath, express.json({ type: 'application/*+json' }))
50
+ cds.app.use(basePath, express.json())
51
+ cds.app.use(basePath, express.urlencoded({ extended: true }))
58
52
  LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' })
59
53
 
60
54
  // Clear cds.context as it would interfere with subsequent transactions
@@ -62,10 +56,6 @@ class EndpointRegistry {
62
56
  cds.context = undefined
63
57
  next()
64
58
  })
65
- cds.app.use(deployPath, (_req, _res, next) => {
66
- cds.context = undefined
67
- next()
68
- })
69
59
 
70
60
  cds.app.options(basePath, (req, res) => {
71
61
  try {
@@ -295,9 +295,13 @@ const getKeysFromPath = (from, srv) => {
295
295
  const join = [...relation[0].xpr]
296
296
  while (join.length >= 3) {
297
297
  const [left, _, right] = join.splice(0, 4)
298
- if (left.ref?.[0] === 'target') keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
299
- else if (right.ref?.[0] === 'target') keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
300
- else {
298
+ if (left.ref?.[0] === 'target') {
299
+ if (left.ref[1] in keys) break // we already added the foreign key for the last segment
300
+ keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
301
+ } else if (right.ref?.[0] === 'target') {
302
+ if (right.ref[1] in keys) break // we already added the foreign key for the last segment
303
+ keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
304
+ } else {
301
305
  // REVISIT: what to do here?
302
306
  }
303
307
  }
@@ -22,7 +22,10 @@ module.exports = srv => async _req => {
22
22
  result = await srv.dispatch(new RestRequest({ query, _target, method: _req.method, params: _params }))
23
23
  if (_params && result) Object.assign(result, _params[_params.length - 1])
24
24
  } catch (e) {
25
- if ((e.code === 404 || e.status === 404 || e.statusCode === 404) && UPSERT_ALLOWED) {
25
+ const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
26
+ const isForcedInsert =
27
+ (e.code === 412 || e.status === 412 || e.statusCode === 412) && _req.headers['if-none-match'] === '*'
28
+ if ((is404 || isForcedInsert) && UPSERT_ALLOWED) {
26
29
  query = INSERT.into(query.UPDATE.entity).entries(
27
30
  _params ? Object.assign(_data, _params[_params.length - 1]) : _data
28
31
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.7.0",
3
+ "version": "7.7.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -1,8 +0,0 @@
1
- let _unfold = (..._) => (_unfold = require ('@sap/cds-compiler/lib/transform/draft/odata')) (..._)
2
- const cds = require ('../../index')
3
- module.exports = function cds_compile_for_drafts (csn,o) {
4
- if (!cds.env.features._ucsn_) return cds.compile.for.odata (csn,o)
5
- csn = JSON.parse (JSON.stringify (csn)) // REVISIT: workaround for bad test setup
6
- csn = _unfold (csn,o||{})
7
- return csn
8
- }