@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 +19 -0
- package/lib/compile/to/yaml.js +1 -1
- package/lib/env/cds-env.js +2 -2
- package/lib/req/validate.js +2 -1
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/factory.js +8 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +5 -3
- package/libx/_runtime/fiori/lean-draft.js +5 -4
- package/libx/common/utils/path.js +2 -0
- package/libx/odata/middleware/create.js +3 -3
- package/libx/odata/middleware/stream.js +12 -2
- package/libx/odata/utils/index.js +5 -1
- package/package.json +1 -1
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
|
package/lib/compile/to/yaml.js
CHANGED
|
@@ -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)
|
|
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
|
package/lib/env/cds-env.js
CHANGED
|
@@ -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 (
|
|
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')
|
package/lib/req/validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
})
|
package/lib/srv/cds-connect.js
CHANGED
|
@@ -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
|
package/lib/srv/factory.js
CHANGED
|
@@ -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 ['', '
|
|
52
|
-
for (let ext of exts) {
|
|
53
|
-
|
|
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
|
|
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
|
|
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)
|
|
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(
|
|
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'
|
|
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
|