@sap/cds 8.7.0 → 8.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,25 @@
4
4
  - The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## Version 8.7.2 - 2025-02-14
8
+
9
+ ### Fixed
10
+
11
+ - Strip `Z` suffix of values of `cds.Timestamp` with OData type `Edm.DateTime`
12
+ - Skip validation for mandatory fields in update scenarios for entities in draft activation
13
+ - `cds.compile.to.yaml` escapes strings including colons if necessary
14
+
15
+ ## Version 8.7.1 - 2025-02-04
16
+
17
+ ### Fixed
18
+
19
+ - Loading of CAP Plugins implemented in Typescript
20
+ - `Location` header if read after write returns empty result due to missing read authentication
21
+ - Enable accessing `req.params` when handling requests on parameterized views
22
+ - `cds.connect.to(class {...})` did not call the `init` function
23
+ - Generic Paging/Sorting was run twice for non-draft requests
24
+ - Service implementation loaded from `node_modules`
25
+
7
26
  ## Version 8.7.0 - 2025-01-28
8
27
 
9
28
  ### Added
@@ -29,7 +29,7 @@ module.exports = function _2yaml (object, {limit=111}={}) {
29
29
  if (typeof o === 'string') {
30
30
  if (o.indexOf('\n')>=0) return '|'+'\n'+indent+ o.replace(/\n/g,'\n'+indent)
31
31
  let s = o.trim()
32
- return !s || /^[\^@#:,=!<>*|]/.test(s) ? '"'+ o.replace(/\\/g,'\\\\') +'"' : s
32
+ return !s || /^[\^@#:,=!<>*|]/.test(s) || /:\s/.test(o) ? '"'+ o.replace(/\\/g,'\\\\') +'"' : s
33
33
  }
34
34
  if (typeof o === 'function') return
35
35
  else return o
@@ -52,12 +52,12 @@ class Config {
52
52
  if (_context !== 'cds') {
53
53
  this.#import (_home,'package.json', { get: p => p[_context] })
54
54
  } else {
55
- for (let {impl} of Object.values(this.plugins)) {
55
+ for (let {impl, packageJson} of Object.values(this.plugins)) {
56
56
  const _plugin = path.dirname(impl)
57
57
  this.#import (_plugin,'.cdsrc.yaml', { load: _readYaml })
58
58
  this.#import (_plugin,'.cdsrc.json')
59
59
  this.#import (_plugin,'.cdsrc.js')
60
- this.#import (_plugin,'package.json', { get: p => p.cds })
60
+ this.#import (path.dirname(packageJson), 'package.json', { get: p => p.cds })
61
61
  }
62
62
  const user_ = process.env.CDS_USER_HOME || require('os').homedir()
63
63
  this.#import (user_,'.cdsrc.json')
@@ -171,7 +171,8 @@ const $any = class any {
171
171
  // different keys and thus different insert checks.
172
172
  const _is_insert = this.own('__is_insert', () => {
173
173
  const entity = this._target || this
174
- const keys = Object.keys (entity.keys||{})
174
+ let keys = Object.keys (entity.keys||{})
175
+ keys = keys.filter(k => !entity.elements[k].virtual)
175
176
  if (!keys.length) return ()=> true
176
177
  else return data => typeof data === 'object' && !keys.every(k => k in data)
177
178
  })
@@ -43,7 +43,7 @@ connect.to = (datasource, options) => {
43
43
  throw new Error (`No service definition found for '${required.service || datasource}'`)
44
44
  }
45
45
  // construct new service instance
46
- let srv = await new Service (datasource,m,o); await Service.init?.(srv)
46
+ let srv = await new Service (datasource,m,o); await (Service._is_service_class ? srv.init?.() : Service.init?.(srv))
47
47
  if (o.outbox) srv = cds.outboxed(srv)
48
48
  if (datasource) {
49
49
  if (datasource === 'db') cds.db = srv
@@ -1,4 +1,3 @@
1
- const exts = process.env.CDS_TYPESCRIPT ? ['ts','js','mjs'] : ['js','mjs']
2
1
  const cds = require('..'), { path, isfile } = cds.utils
3
2
  /**
4
3
  * NOTE: Need this typed helper variable to be able to use IntelliSense for calls with new keyword.
@@ -44,13 +43,17 @@ function ServiceFactory (name, model, options) {
44
43
  }
45
44
 
46
45
 
46
+ const exts = process.env.CDS_TYPESCRIPT ? ['.ts','.js','.mjs'] : ['.js','.mjs']
47
47
  const _source4 = d => d['@source'] || d.$location?.file
48
48
  const _sibling = d => {
49
49
  let file = _source4(d); if (!file) return
50
- let { dir, name } = path.parse (file)
51
- for (let subdir of ['', './lib', './handlers']) {
52
- for (let ext of exts) {
53
- if (file = isfile (dir, subdir, name + '.' + ext)) return file // eslint-disable-line no-cond-assign
50
+ let { dir, name } = path.parse (file); if (!dir) dir = '.'
51
+ for (let subdir of ['/', '/lib/', '/handlers/']) {
52
+ for (let ext of exts) try {
53
+ const impl = dir + subdir + name + ext
54
+ return isfile(impl) || require.resolve (impl)
55
+ } catch(e) {
56
+ if (e.code !== 'MODULE_NOT_FOUND') throw e
54
57
  }
55
58
  }
56
59
  }
@@ -560,8 +560,7 @@ const read = service => {
560
560
  odataReq.getBatchApplicationData().results[changeset].push({ result, req })
561
561
  } else if (result.value && isStreaming(odataReq.getUriInfo().getPathSegments())) {
562
562
  if (odataRes._response.destroyed) {
563
- err = new Error('Response is closed while streaming')
564
- err.code = 'ERR_STREAM_PREMATURE_CLOSE'
563
+ err = new cds.error({ code: 'ERR_STREAM_PREMATURE_CLOSE', message: 'Response was closed while streaming' })
565
564
  tx.rollback(err).catch(() => {})
566
565
  } else {
567
566
  // REVISIT: temp workaround for url streaming
@@ -574,7 +573,10 @@ const read = service => {
574
573
  })
575
574
  odataRes._response.on('close', () => {
576
575
  if (!finished) {
577
- err = new Error('Response is closed while streaming')
576
+ err = new cds.error({
577
+ code: 'ERR_STREAM_PREMATURE_CLOSE',
578
+ message: 'Response was closed while streaming'
579
+ })
578
580
  tx.rollback(err).catch(() => {})
579
581
  }
580
582
  })
@@ -407,10 +407,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
407
407
  }
408
408
 
409
409
  if (req.event === 'READ') {
410
- // apply paging and sorting on original query for protocol adapters relying on it
411
- commonGenericPaging(req)
412
- commonGenericSorting(req)
413
-
414
410
  if (
415
411
  !Object.keys(draftParams).length &&
416
412
  !req.query._target.name?.endsWith('DraftAdministrativeData') &&
@@ -419,6 +415,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
419
415
  req.query = query
420
416
  return handle(req)
421
417
  }
418
+
419
+ // apply paging and sorting on original query for protocol adapters relying on it
420
+ commonGenericPaging(req)
421
+ commonGenericSorting(req)
422
+
422
423
  const read =
423
424
  draftParams.IsActiveEntity === false &&
424
425
  _hasStreaming(query.SELECT.columns, query._target) &&
@@ -25,6 +25,8 @@ const getKeysAndParamsFromPath = (from, { model }) => {
25
25
  const seg_keys = where2obj(ref.where)
26
26
  Object.assign(keys, seg_keys)
27
27
  params[i] = seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys
28
+ } else if (ref.args) {
29
+ params[i] = Object.fromEntries(Object.entries(ref.args).map(([k, v]) => [k, 'val' in v ? v.val : v]))
28
30
  }
29
31
  if (lastElement.isAssociation && from.ref.length > 1) {
30
32
  // add keys for navigation from path
@@ -72,14 +72,14 @@ module.exports = (adapter, isUpsert) => {
72
72
 
73
73
  handleSapMessages(cdsReq, req, res)
74
74
 
75
- // case: read after write returns no results, e.g., due to auth (academic but possible)
76
- if (result == null) return res.sendStatus(204)
77
-
78
75
  if (!target._isSingleton) {
79
76
  // determine calculation based on result with req.data as fallback
80
77
  res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
81
78
  }
82
79
 
80
+ // case: read after write returns no results, e.g., due to auth (academic but possible)
81
+ if (result == null) return res.sendStatus(204)
82
+
83
83
  const preference = getPreferReturnHeader(req)
84
84
  postProcess(cdsReq.target, model, result, preference === 'minimal')
85
85
  if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
@@ -232,13 +232,23 @@ module.exports = adapter => {
232
232
  _setStreamingHeaders(result, res)
233
233
 
234
234
  return new Promise((resolve, reject) => {
235
- if (res.destroyed) return reject(new Error('Response was closed while streaming'))
235
+ if (res.destroyed)
236
+ return reject(
237
+ new cds.error({ code: 'ERR_STREAM_PREMATURE_CLOSE', message: 'Response was closed while streaming' })
238
+ )
236
239
  stream.pipe(res)
237
240
  stream.on('end', () => resolve(result))
238
241
  stream.once('error', reject)
239
242
  let finished = false
240
243
  res.on('finish', () => (finished = true))
241
- res.on('close', () => !finished && reject(new Error('Response was closed while streaming')))
244
+ res.on(
245
+ 'close',
246
+ () =>
247
+ !finished &&
248
+ reject(
249
+ new cds.error({ code: 'ERR_STREAM_PREMATURE_CLOSE', message: 'Response was closed while streaming' })
250
+ )
251
+ )
242
252
  })
243
253
  })
244
254
  })
@@ -145,7 +145,11 @@ const _v2 = (val, element) => {
145
145
  case 'cds.Time':
146
146
  return `time'${_PT(val.split(':'))}'`
147
147
  case 'cds.Timestamp':
148
- return element['@odata.Type'] === 'Edm.DateTime' ? `datetime'${val}'` : `datetimeoffset'${val}'`
148
+ return element['@odata.Type'] === 'Edm.DateTime'
149
+ ? val.endsWith('Z')
150
+ ? `datetime'${val.slice(0, -1)}'`
151
+ : `datetime'${val}'`
152
+ : `datetimeoffset'${val}'`
149
153
  // bool
150
154
  case 'cds.Boolean':
151
155
  return val
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.7.0",
3
+ "version": "8.7.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [