@sap/cds 7.0.1 → 7.0.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 CHANGED
@@ -4,6 +4,29 @@
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.0.3 - 2023-07-19
8
+
9
+ ### Fixed
10
+
11
+ - Compile for lean draft: do not add draft entity for external entities
12
+ - Rollback awaited in REST adapter
13
+ - `service.on('error')` handler invoked only once
14
+ - `SELECT.one.localized`
15
+ - `COPYFILE_DISABLE=1` is now set for building `tar` archives by default
16
+ - Actions of projection target are no longer accessible in linked models
17
+ - Batch execute model-less mass inputs when on `@sap/hana-client`
18
+ - Requests to `/<path>/webapp` return 404 for absolute `@path` specifications
19
+ - `cds compile --to serviceinfo` no longer returns paths w/ Windows `\` path characters
20
+
21
+ ## Version 7.0.2 - 2023-07-06
22
+
23
+ ### Fixed
24
+
25
+ - Glitch in `cds.deploy` if no change was applied
26
+ - Detection of `.cdsrc-private.json` during startup
27
+ - Respect capabilities annotation for draft events
28
+ - `cds compile --to serviceinfo` returns correct service paths again
29
+
7
30
  ## Version 7.0.1 - 2023-07-03
8
31
 
9
32
  ### Fixed
@@ -72,6 +72,9 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
72
72
  if (draft['@readonly']) draft['@readonly'] = undefined
73
73
  if (draft['@insertonly']) draft['@insertonly'] = undefined
74
74
  if (draft['@restrict']) draft['@restrict'] = undefined
75
+ if ('@Capabilities.DeleteRestrictions.Deletable' in draft) draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
76
+ if ('@Capabilities.InsertRestrictions.Insertable' in draft) draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
77
+ if ('@Capabilities.UpdateRestrictions.Updatable' in draft) draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
75
78
 
76
79
  // Recursively add drafts for compositions
77
80
  for (const each in draft.elements) {
@@ -103,7 +106,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
103
106
  }
104
107
  for (const name in csn.definitions) {
105
108
  const def = csn.definitions[name]
106
- if (!_isDraft(def)) continue
109
+ if (!_isDraft(def) || def['@cds.external']) continue
107
110
  def.elements.IsActiveEntity.virtual = true
108
111
  def.elements.HasDraftEntity.virtual = true
109
112
  def.elements.HasActiveEntity.virtual = true
@@ -35,24 +35,44 @@ module.exports = (model, options={}) => {
35
35
  }
36
36
  }
37
37
  function _makeNode(service) {
38
+ const path = _effectiveNodePath(service)
38
39
  return {
39
40
  name: service.name,
40
- urlPath: _url4 (cds.service.path4(service)),
41
+ urlPath: _url4 (path),
41
42
  destination: 'srv-api', // the name to register in xs-app.json
42
43
  runtime: 'Node.js',
43
44
  location: service.$location
44
45
  }
45
46
  }
46
47
 
48
+ // TODO use a function from cds.service... instead
49
+ function _effectiveNodePath(service) {
50
+ if (service['@path']?.[0] === '/') { // absolute path given
51
+ return service['@path']
52
+ }
53
+ const { ProtocolAdapter } = cds.service.protocols
54
+ const prots = ProtocolAdapter.protocols4(service)
55
+ const prot = prots.find(p => p.kind.startsWith('odata')) || prots[0] // prefer odata for compat. reasons
56
+ if (prot.path && prot.path.startsWith('/')) {
57
+ return prot.path
58
+ }
59
+ const rootPath = cds.env.requires.middlewares ? ProtocolAdapter.protocols[prot.kind]?.path || '' : ''
60
+ return join(rootPath, prot.path || cds.service.path4(service))
61
+ }
62
+
47
63
  // the URL path that is *likely* effective at runtime
48
64
  function _url4 (p) {
49
- return normalize (p.replace(/^\/+/, '') + '/') //> /foo/bar -> foo/bar/
65
+ p = p.replace(/\\/g, '/') // handle Windows
66
+ .replace(/^\/+/, '') // strip leading
67
+ .replace(/\/+$/, '') // strip trailing
68
+ p += '/' // end with /
69
+ return p
50
70
  }
51
71
 
52
72
  function _javaPath (service) {
53
73
  const d = model.definitions[service.name]
54
74
  const path = d && d['@path'] ? d['@path'].replace(/^[^/]/, c => '/'+c) : service.name
55
- return join(javaPrefix, path).replace(/\\/g, '/')
75
+ return join(javaPrefix, path)
56
76
  }
57
77
 
58
78
  function _isNodeProject(root) {
@@ -15,7 +15,7 @@ module.exports = exports = function cds_deploy (model,options,csvs) {
15
15
  DEBUG = cds.debug('deploy')
16
16
  return {
17
17
  /** @param {import('@sap/cds/lib/srv/srv-api')} db */
18
- async to(db, o = options || cds.options || {}) {
18
+ async to(db, o = options || {}) {
19
19
 
20
20
  const TRACE = cds.debug('trace')
21
21
  TRACE?.time('cds.deploy db ')
@@ -90,8 +90,8 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
90
90
  } else {
91
91
  await db.run(`UPDATE cds_model SET csn = ?`, after)
92
92
  }
93
- db.options.schema_evolution = 'auto' // for updating package.json
94
93
  }
94
+ o.schema_evolution = 'auto' // for INSERT_from4 below
95
95
  // cds deploy --model-only > fills in table cds_model above
96
96
  if (o['model-only']) return o.dry && console.log(after)
97
97
  // cds deploy -- with auto schema evolution > upgrade by applying delta to former model
@@ -253,9 +253,6 @@ exports.resources = async function cds_deploy_resources (csn, opts) {
253
253
  const folders = await cds_deploy_resources.folders(csn, opts)
254
254
  const found={}, ts = process.env.CDS_TYPESCRIPT
255
255
  for (let folder of folders) {
256
- // fetching init.js files
257
- const init_js = ts && isfile(folder,'init.ts') || isfile(folder,'init.js')
258
- if (init_js) found[init_js] = '*'
259
256
  // fetching .csv and .json files
260
257
  for (let each of ['data','csv']) {
261
258
  const subdir = isdir(folder,each); if (!subdir) continue
@@ -281,6 +278,9 @@ exports.resources = async function cds_deploy_resources (csn, opts) {
281
278
  }
282
279
  }
283
280
  }
281
+ // fetching init.js files -> Note: after .csv files to have that on top, when processing in .reverse order
282
+ const init_js = ts && isfile(folder,'init.ts') || isfile(folder,'init.js')
283
+ if (init_js) found[init_js] = '*'
284
284
  }
285
285
  return found
286
286
  }
@@ -341,7 +341,7 @@ const _queries4 = (db,csn) => !db.cqn2sql ? q => q : q => {
341
341
 
342
342
 
343
343
  const INSERT_from4 = (db,o) => {
344
- const schevo = db.options?.schema_evolution === 'auto' || cds.env.requires.db?.schema_evolution === 'auto' || o?.schema_evolution === 'auto'
344
+ const schevo = o?.schema_evolution === 'auto' || db.options.schema_evolution === 'auto'
345
345
  const INSERT_into = (schevo ? UPSERT : INSERT).into
346
346
  return (file) => ({
347
347
  '.json': { into (entity, json) {
@@ -113,14 +113,14 @@ class Config {
113
113
  ...( global._plugins||[] ).map (root => ({
114
114
  path: root, file: 'package.json', mapper: x => x.cds
115
115
  })),
116
- { path: user_home, file: '.cdsrc.json' },
117
- { path: home, file: '.cdsrc-private.json' },
118
- { path: home, file: '.cdsrc.json' },
116
+ { path: user_home, file: '.cdsrc.json', mapper: x => x.cds||x },
117
+ { path: home, file: '.cdsrc.json', mapper: x => x.cds||x },
119
118
  { path: home, file: 'package.json', mapper: _package_json },
119
+ { path: home, file: '.cdsrc-private.json', mapper: x => x.cds||x },
120
120
  ]
121
121
  function _package_json (pkg) { // fill cds.extends from .extends
122
122
  let cds = pkg.cds
123
- if (pkg.extends) (cds||(cds={})).extends = pkg.extends
123
+ if (pkg.extends) (cds??={}).extends = pkg.extends
124
124
  return cds
125
125
  }
126
126
  }
@@ -434,7 +434,7 @@ class Config {
434
434
  function _add_static_profiles (_home, profiles) {
435
435
  for (let src of ['package.json', '.cdsrc.json']) try {
436
436
  const conf = require(path.join(_home,src))
437
- const cds = src === '.cdsrc.json' ? conf : conf.cds
437
+ const cds = src === 'package.json' ? conf.cds : conf.cds||conf
438
438
  if (cds?.profiles) return profiles.push(...cds.profiles)
439
439
  if (cds?.profile) return profiles.push(cds.profile)
440
440
  } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
@@ -25,7 +25,8 @@ class LinkedCSN extends any {
25
25
  /* else: */ any.prototype
26
26
  )
27
27
  if (p.key && !d.key && d.kind === 'element') Object.defineProperty (d,'key',{value:undefined}) //> don't propagate .key
28
- if (p.params && !d.params && d.kind === 'entity') Object.defineProperty (d,'params',{value:undefined}) //> don't propagate .key
28
+ if (p.params && !d.params && d.kind === 'entity') Object.defineProperty (d,'params',{value:undefined}) //> don't propagate .params
29
+ if (p.actions && !d.actions && d.kind === 'entity') Object.defineProperty (d,'actions',{value:undefined}) //> don't propagate .actions
29
30
  if (d.elements && d.elements.localized) Object.defineProperty (d,'texts',{value: defs [d.elements.localized.target] })
30
31
  try { return Object.setPrototypeOf(d,p) } //> link d to resolved proto
31
32
  catch(e) { //> cyclic proto error
package/lib/ql/SELECT.js CHANGED
@@ -17,6 +17,7 @@ module.exports = class Query extends Whereable {
17
17
  one: $((...x) => new this({one:true})._select_or_from(...x),{
18
18
  columns: (..._) => new this({one:true}).columns(..._),
19
19
  from: (..._) => new this({one:true}).from(..._),
20
+ localized: (..._) => new this({one:true,localized:true}).from(..._)
20
21
  }),
21
22
  from: $((..._) => new this().from(..._), {
22
23
  localized: (..._) => new this({localized:true}).from(..._)
@@ -83,11 +83,28 @@ class ProtocolAdapter {
83
83
  app.use (`/${path}`, before, adapter, after)
84
84
  }
85
85
 
86
+ if (!cds.env.features.serve_on_root) {
87
+ // log warning for changed path if $metadata is accessed
88
+ let logged = false
89
+ app.use(`*/${path}/\\$metadata`, (req,res,next) => {
90
+ if (!logged) {
91
+ const logger = cds.log('adapters')
92
+ logger._warn && logger.warn(`With @sap/cds version 7, the service path has changed to '${srv.path}'.
93
+ If you use SAP Fiori Elements, make sure to adapt the 'dataSources.uri' paths
94
+ in 'manifest.json' files accordingly. For more information, see the release notes at
95
+ https://cap.cloud.sap/docs/releases/jun23.`)
96
+ logged = true
97
+ }
98
+ next()
99
+ })
100
+ }
101
+
86
102
  prefix = this.protocols[p.kind].path
87
103
  prefix = prefix ? (prefix.endsWith('/') ? prefix : prefix + '/') : '/'
88
104
  path = prefix + path
89
105
  }
90
106
 
107
+ app.use (`${path}/webapp/`, (_,res) => res.sendStatus(404))
91
108
  DEBUG?.('app.use(', path, ', ... )')
92
109
  app.use (path, before, adapter, after)
93
110
  // REVISIT this doesn't handle multiple protocols correctly
package/lib/utils/tar.js CHANGED
@@ -102,7 +102,7 @@ exports.create = async (dir='.', ...args) => {
102
102
  args.push('.')
103
103
  }
104
104
 
105
- c = spawn ('tar', ['c', '-C', dir, ...args])
105
+ c = spawn ('tar', ['c', '-C', dir, ...args], { env: { COPYFILE_DISABLE: 1 }})
106
106
  }
107
107
 
108
108
  return {__proto__:c, // returning a thenable + fluent ChildProcess...
@@ -169,7 +169,7 @@ exports.extract = (archive, ...args) => ({
169
169
  to (...dest) {
170
170
  if (typeof dest === 'string') dest = _resolve(...dest)
171
171
  const input = typeof archive !== 'string' || archive == '-' ? '-' : _resolve(archive)
172
- const x = spawn('tar', ['xf', win(input), '-C', win(dest), ...args], { env: { COPYFILE_DISABLE: 1 }})
172
+ const x = spawn('tar', ['xf', win(input), '-C', win(dest), ...args])
173
173
  if (archive === '-') return x.stdin
174
174
  if (Buffer.isBuffer(archive)) archive = require('stream').Readable.from (archive)
175
175
  if (typeof archive !== 'string') archive.pipe (x.stdin)
@@ -112,6 +112,7 @@ const getErrorHandler = (crashOnError = true, srv) => {
112
112
  if ('_data' in err && !('data' in req)) req.data = err._data
113
113
  }
114
114
 
115
+ // REVISIT: invoking service.on('error') handlers needs a cleanup with new protocol adapters!!!
115
116
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
116
117
  // REVISIT: if for compat reasons, remove once cds^5.1
117
118
  if (srv._handlers._error.length) {
@@ -124,7 +125,7 @@ const getErrorHandler = (crashOnError = true, srv) => {
124
125
  // > error before req was dispatched
125
126
  const creq = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
126
127
  for (const each of srv._handlers._error) each.handler.call(srv, err, creq)
127
- } else {
128
+ } else if (ctx._tx?._done !== 'rolled back') {
128
129
  // > error after req was dispatched, e.g., serialization error in okra
129
130
  const creq = /* odataReq.req || */ new cds.Request({ req, res: req.res, user: ctx.user, tenant: ctx.tenant })
130
131
  for (const each of srv._handlers._error) each.handler.call(srv, err, creq)
@@ -856,10 +856,13 @@ function expandStarStar(target, recursion = new Map()) {
856
856
 
857
857
  async function onNew(req) {
858
858
  LOG.debug('new draft')
859
+ if (req.target.actives['@Capabilities.InsertRestrictions.Insertable'] === false || req.target.actives['@readonly'])
860
+ req.reject(405)
859
861
  const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
860
862
  // Only allowed for pseudo draft roots (entities with this action)
861
863
  if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
862
864
  req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
865
+
863
866
  let DraftUUID
864
867
  if (isDirectAccess) DraftUUID = cds.utils.uuid()
865
868
  else {
@@ -929,6 +932,12 @@ async function onEdit(req) {
929
932
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
930
933
  req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
931
934
  }
935
+ if (
936
+ req.target['@Capabilities.UpdateRestrictions.Updatable'] === false ||
937
+ req.target['@insertonly'] ||
938
+ req.target['@readonly']
939
+ )
940
+ req.reject(405)
932
941
  const targetWhere = req.query.SELECT.from.ref[0].where
933
942
 
934
943
  if (draftParams.IsActiveEntity !== true) req.reject(400)
@@ -18,7 +18,8 @@ const _loadStreamExtensionIfNeeded = () => {
18
18
  const streamExtension = _loadStreamExtensionIfNeeded()
19
19
 
20
20
  function hasStreamInsert(insert, model) {
21
- if (!model) return true
21
+ if (!model) return false
22
+
22
23
  const name = insert.into.ref ? insert.into.ref[0] : insert.into
23
24
  const into = model.definitions[ensureNoDraftsSuffix(name)]
24
25
  if (!into) return false
@@ -38,9 +39,7 @@ function hasStreamInsert(insert, model) {
38
39
  }
39
40
 
40
41
  function hasStreamUpdate(update, model) {
41
- if (!model) {
42
- return true
43
- }
42
+ if (!model) return false
44
43
 
45
44
  const entity = model.definitions[ensureNoDraftsSuffix((update.entity.ref && update.entity.ref[0]) || update.entity)]
46
45
  if (!entity) return false
@@ -194,12 +194,12 @@ const RestAdapter = function (srv) {
194
194
 
195
195
  // -----------------------------------------------------------------------------------------
196
196
  // error handling
197
- router.use((err, req, res, next) => {
197
+ router.use(async (err, req, res, next) => {
198
198
  // REVISIT: should not be neccessary!
199
199
  // request may fail during processing or during commit -> both caught here
200
200
 
201
201
  // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
202
- cds.context?.tx?.rollback(err).catch(() => {}) // REVISIT: silently ?!?
202
+ await cds.context?.tx?.rollback(err).catch(() => {}) // REVISIT: silently ?!?
203
203
 
204
204
  next(err)
205
205
  })
@@ -39,13 +39,16 @@ const _log = err => {
39
39
  module.exports = (err, req, res, next) => {
40
40
  const { _srv: srv } = req
41
41
 
42
+ // REVISIT: invoking service.on('error') handlers needs a cleanup with new protocol adapters!!!
42
43
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
43
44
  let ctx = cds.context
44
45
  if (!ctx) {
45
46
  // > error before req was dispatched
46
47
  ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
48
+ for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
49
+ } else if (ctx._tx?._done !== 'rolled back') {
50
+ for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
47
51
  }
48
- for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
49
52
 
50
53
  // log the error (4xx -> warn)
51
54
  _log(err)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [