@sap/cds 6.5.0 → 6.6.0
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 +38 -2
- package/README.md +5 -0
- package/apis/services.d.ts +5 -0
- package/bin/build/buildTaskEngine.js +0 -2
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +10 -6
- package/bin/build/provider/fiori/index.js +5 -10
- package/bin/build/provider/hana/2migration.js +11 -2
- package/bin/build/provider/hana/index.js +17 -14
- package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
- package/bin/build/provider/mtx-extension/index.js +18 -1
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +1 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/serve.js +32 -20
- package/lib/auth/jwt-auth.js +4 -4
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/dbs/cds-deploy.js +6 -8
- package/lib/index.js +4 -2
- package/lib/req/cds-context.js +3 -3
- package/lib/srv/bindings.js +1 -2
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/trace.js +31 -15
- package/lib/srv/protocols/odata-v2-proxy.js +8 -8
- package/lib/srv/srv-handlers.js +26 -7
- package/lib/srv/srv-methods.js +2 -2
- package/lib/srv/srv-models.js +3 -3
- package/lib/utils/cds-test.js +7 -5
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
- package/libx/_runtime/cds-services/services/Service.js +8 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +7 -4
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +3 -1
- package/libx/_runtime/common/code-ext/execute.js +9 -2
- package/libx/_runtime/common/code-ext/handlers.js +2 -2
- package/libx/_runtime/common/code-ext/worker.js +9 -5
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +5 -2
- package/libx/_runtime/common/composition/data.js +5 -2
- package/libx/_runtime/common/composition/tree.js +2 -0
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/etag.js +3 -1
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -11
- package/libx/_runtime/common/utils/path.js +0 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +4 -1
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
- package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
- package/libx/_runtime/db/generic/input.js +2 -2
- package/libx/_runtime/db/generic/integrity.js +1 -0
- package/libx/_runtime/db/generic/virtual.js +1 -0
- package/libx/_runtime/db/query/read.js +3 -2
- package/libx/_runtime/fiori/generic/activate.js +3 -1
- package/libx/_runtime/fiori/generic/before.js +1 -0
- package/libx/_runtime/fiori/generic/edit.js +3 -1
- package/libx/_runtime/fiori/generic/new.js +2 -0
- package/libx/_runtime/fiori/generic/patch.js +2 -0
- package/libx/_runtime/fiori/generic/prepare.js +2 -0
- package/libx/_runtime/fiori/generic/read.js +8 -2
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +498 -245
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- package/libx/_runtime/messaging/Outbox.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
- package/libx/_runtime/messaging/file-based.js +1 -2
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +0 -1
- package/libx/_runtime/remote/Service.js +1 -0
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
- package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
- package/libx/odata/afterburner.js +17 -5
- package/libx/odata/grammar.pegjs +3 -4
- package/libx/odata/index.js +5 -1
- package/libx/odata/parseToCqn.js +3 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +58 -1
- package/package.json +1 -1
- package/server.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
- package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
- package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
- /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
package/bin/serve.js
CHANGED
|
@@ -131,14 +131,14 @@ const cds = require('../lib'), { exists, isfile, local, path } = cds.utils
|
|
|
131
131
|
|
|
132
132
|
// provisional loggers, see _prepare_logging
|
|
133
133
|
let log = console.log
|
|
134
|
-
let debug = false
|
|
135
134
|
|
|
136
135
|
|
|
137
136
|
/**
|
|
138
137
|
* The main function which dispatches into the respective usage variants.
|
|
139
138
|
* @param {string[]} all - project folder, model filenames, or service name
|
|
140
139
|
*/
|
|
141
|
-
async function serve (all=[], o={}) {
|
|
140
|
+
async function serve (all=[], o={}) {
|
|
141
|
+
|
|
142
142
|
// canonicalize options to ease subsequent tasks...
|
|
143
143
|
cds.options = o
|
|
144
144
|
const [pms] = all // project folder, model filenames, or service name
|
|
@@ -200,17 +200,8 @@ async function serve (all=[], o={}) { // NOSONAR
|
|
|
200
200
|
|
|
201
201
|
server.listening ? _started(server) : server.once('listening',_started)
|
|
202
202
|
server.on ('error',_reject) // startup errors like EADDRINUSE
|
|
203
|
-
server.on ('close',
|
|
204
|
-
|
|
205
|
-
process.once('SIGTERM', shutdown)
|
|
206
|
-
process.once('SIGINT', shutdown)
|
|
207
|
-
process.once('SIGHUP', shutdown)
|
|
208
|
-
process.once('SIGUSR2', shutdown) // by nodemon
|
|
209
|
-
process.on('beforeExit', shutdown) //> when event loop empties
|
|
210
|
-
process.on('message', (msg) => { if (msg.close||msg.exit) shutdown() }) // by `cds watch` on Windows
|
|
211
|
-
|
|
212
|
-
return server
|
|
213
|
-
|
|
203
|
+
// server.on ('close', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
|
|
204
|
+
// process.on ('exit', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
|
|
214
205
|
async function _started() {
|
|
215
206
|
_warn_if_cds_was_loaded_from_different_locations()
|
|
216
207
|
const url = cds.server.url = `http://localhost:${server.address().port}`
|
|
@@ -218,13 +209,34 @@ async function serve (all=[], o={}) { // NOSONAR
|
|
|
218
209
|
_resolve (server)
|
|
219
210
|
}
|
|
220
211
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
cds.shutdown = _shutdown //> for programmatic invocation
|
|
213
|
+
process.on('unhandledRejection', (_,p) => _shutdown (console.error('❗️Uncaught',p)))
|
|
214
|
+
process.on('uncaughtException', (e) => _shutdown (console.error('❗️Uncaught',e)))
|
|
215
|
+
process.on('SIGINT', cds.watched ? _shutdown : (s,n)=>_shutdown(s,n,console.log())) //> newline after ^C
|
|
216
|
+
process.on('SIGHUP', _shutdown)
|
|
217
|
+
process.on('SIGHUP2', _shutdown)
|
|
218
|
+
process.on('SIGTERM', _shutdown)
|
|
219
|
+
|
|
220
|
+
async function _shutdown (signal,n) {
|
|
221
|
+
if (signal) DEBUG?.('⚡️',signal,n, 'received by cds serve')
|
|
222
|
+
await Promise.all(cds.listeners('shutdown').map(fn => fn()))
|
|
223
|
+
server.close(()=>{/* it's ok if closed already */}) // first, we try stopping server and process the nice way
|
|
224
|
+
if (!global.it) setTimeout(process.exit,1111).unref() // after ~1 sec, we force-exit it, unless in test mode
|
|
227
225
|
}
|
|
226
|
+
|
|
227
|
+
const DEBUG = cds.debug('cli')
|
|
228
|
+
if (DEBUG) {
|
|
229
|
+
cds.on('shutdown', () => DEBUG ('⚡️','cds serve - cds.shutdown'))
|
|
230
|
+
server.on('close', () => DEBUG ('⚡️','cds serve - server.close(d)'))
|
|
231
|
+
process.on('exit', () => DEBUG ('⚡️','cds serve - process.exit'))
|
|
232
|
+
process.on('beforeExit', ()=> DEBUG ('⚡️','cds serve - process.beforeExit'))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (process.platform === 'win32') {
|
|
236
|
+
process.on('message', msg => msg.close && _shutdown()) // by `cds watch` on Windows
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return server
|
|
228
240
|
})
|
|
229
241
|
}
|
|
230
242
|
|
|
@@ -269,7 +281,7 @@ function _prepare_logging () { // NOSONAR
|
|
|
269
281
|
|
|
270
282
|
// print info when we are finally on air
|
|
271
283
|
cds.once ('listening', ({url})=>{
|
|
272
|
-
|
|
284
|
+
console.log()
|
|
273
285
|
LOG.info ('server listening on',{url})
|
|
274
286
|
_timer && console.timeEnd (_timer)
|
|
275
287
|
if (process.stdin.isTTY) LOG.info (`[ terminate with ^C ]\n`)
|
package/lib/auth/jwt-auth.js
CHANGED
|
@@ -24,16 +24,16 @@ module.exports = function jwt_auth(config) {
|
|
|
24
24
|
.use(passport.authenticate(config.kind, { session: false }))
|
|
25
25
|
.use((req, res, next) => {
|
|
26
26
|
const payload = req.tokenInfo.getPayload()
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
let id = req.user.id
|
|
29
29
|
let _is_system, _is_internal
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
let roles = payload.scope.map(s => s.replace(new RegExp(`^(${config.credentials.xsappname + '.'})`), ''))
|
|
32
32
|
roles.push('identified-user')
|
|
33
33
|
if (payload.grant_type) {
|
|
34
34
|
// > not "weak"
|
|
35
35
|
roles.push('authenticated-user')
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
const CLIENT = { client_credentials: 1, client_x509: 1 }
|
|
38
38
|
if (payload.grant_type in CLIENT) {
|
|
39
39
|
id = 'system'
|
|
@@ -54,7 +54,7 @@ module.exports = function jwt_auth(config) {
|
|
|
54
54
|
req.tenant = req.tokenInfo.getZoneId?.()
|
|
55
55
|
next()
|
|
56
56
|
})
|
|
57
|
-
.use((err, req, res,
|
|
57
|
+
.use((err, req, res, _next) => {
|
|
58
58
|
if (req.tokenInfo) {
|
|
59
59
|
LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
|
|
60
60
|
}
|
|
@@ -1,5 +1,43 @@
|
|
|
1
|
-
const cds = require
|
|
2
|
-
|
|
1
|
+
const cds = require('../../index')
|
|
2
|
+
|
|
3
|
+
function _getBacklinkName(on) {
|
|
4
|
+
const i = on.findIndex(e => e.ref && e.ref[0] === '$self')
|
|
5
|
+
if (i === -1) return
|
|
6
|
+
let ref
|
|
7
|
+
if (on[i + 1] && on[i + 1] === '=') ref = on[i + 2].ref
|
|
8
|
+
if (on[i - 1] && on[i - 1] === '=') ref = on[i - 2].ref
|
|
9
|
+
return ref && ref[ref.length - 1]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function _isCompositionBacklink(e) {
|
|
13
|
+
if (!e.isAssociation) return
|
|
14
|
+
if (!e._target?.associations) return
|
|
15
|
+
if (!(!e.isComposition && (e.keys || e.on))) return
|
|
16
|
+
for (const anchor of Object.values(e._target.associations)) {
|
|
17
|
+
if (!(anchor.isComposition && anchor.on?.length > 2)) continue
|
|
18
|
+
if (_getBacklinkName(anchor.on) === e.name && anchor.target === e.parent.name) return anchor
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const IGNORED_ANNOTATIONS = [
|
|
23
|
+
'@assert.range',
|
|
24
|
+
'@assert.enum',
|
|
25
|
+
'@assert.format',
|
|
26
|
+
'@assert.target',
|
|
27
|
+
'@mandatory',
|
|
28
|
+
'@Core.Immutable',
|
|
29
|
+
'@readonly',
|
|
30
|
+
'@cds.on.update',
|
|
31
|
+
'@cds.on.insert',
|
|
32
|
+
'@Core.Computed',
|
|
33
|
+
'@Common.FieldControl.Readonly',
|
|
34
|
+
'@Common.FieldControl.Mandatory',
|
|
35
|
+
'@FieldControl.Mandatory',
|
|
36
|
+
'@FieldControl.ReadOnly',
|
|
37
|
+
'@Common.FieldControl'
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
3
41
|
const DRAFT_ELEMENTS = new Set([
|
|
4
42
|
'IsActiveEntity',
|
|
5
43
|
'HasDraftEntity',
|
|
@@ -22,6 +60,13 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
|
|
|
22
60
|
return on
|
|
23
61
|
}
|
|
24
62
|
|
|
63
|
+
function _isDraft(def) {
|
|
64
|
+
return (
|
|
65
|
+
def.associations?.DraftAdministrativeData ||
|
|
66
|
+
(def.own('@odata.draft.enabled') && def.own('@Common.DraftRoot.ActivationAction'))
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
25
70
|
const { Draft } = cds.linked(`
|
|
26
71
|
entity ActiveEntity { key ID: UUID; }
|
|
27
72
|
entity Draft {
|
|
@@ -49,15 +94,21 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
|
|
|
49
94
|
const draft = { __proto__: active, name: _draftEntity, elements: { ...active.elements, ...Draft.elements } }
|
|
50
95
|
Object.defineProperty(model.definitions, _draftEntity, { value: draft })
|
|
51
96
|
Object.defineProperty(active, 'drafts', { value: draft })
|
|
97
|
+
Object.defineProperty(draft, 'actives', { value: active })
|
|
52
98
|
draft['@cds.persistence.table'] = _draftEntity
|
|
99
|
+
if (draft['@restrict']) draft['@restrict'] = undefined
|
|
53
100
|
// Recursively add drafts for compositions
|
|
54
101
|
for (const each in draft.elements) {
|
|
55
102
|
const e = draft.elements[each]
|
|
56
103
|
const newEl = Object.create(e)
|
|
57
|
-
if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || e
|
|
104
|
+
if (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || _isCompositionBacklink(e)) {
|
|
105
|
+
if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
|
|
58
106
|
_redirect(newEl, draftEntity(e._target, model))
|
|
59
107
|
}
|
|
60
108
|
newEl.parent = draft
|
|
109
|
+
for (const ignoredAnno of IGNORED_ANNOTATIONS) {
|
|
110
|
+
if (newEl[ignoredAnno]) newEl[ignoredAnno] = undefined
|
|
111
|
+
}
|
|
61
112
|
draft.elements[each] = newEl
|
|
62
113
|
}
|
|
63
114
|
// TODO: Redirect associations to localized
|
|
@@ -65,9 +116,7 @@ module.exports = function cds_compile_for_lean_drafts(csn, o) {
|
|
|
65
116
|
}
|
|
66
117
|
for (const name in csn.definitions) {
|
|
67
118
|
const def = csn.definitions[name]
|
|
68
|
-
if (!def
|
|
69
|
-
continue
|
|
70
|
-
// so that database ignores them
|
|
119
|
+
if (!_isDraft(def)) continue
|
|
71
120
|
;[
|
|
72
121
|
'IsActiveEntity',
|
|
73
122
|
'HasDraftEntity',
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -107,9 +107,7 @@ exports.exclude_external_entities_in = function (csn) { // NOSONAR
|
|
|
107
107
|
|
|
108
108
|
function getSqls(db, csn, o, beforeCsn) {
|
|
109
109
|
const schemaEvo = (db.options?.schema_evolution === 'auto' || o.schema_evolution === 'auto')
|
|
110
|
-
|
|
111
|
-
const in_memory = (creds?.url || creds?.database) === ':memory:';
|
|
112
|
-
if (!in_memory && schemaEvo) {
|
|
110
|
+
if (schemaEvo) {
|
|
113
111
|
const { afterImage: afterCsn, drops, createsAndAlters: creas } = cds.compile.to.sql.delta (csn, o, beforeCsn);
|
|
114
112
|
if(beforeCsn === undefined) {
|
|
115
113
|
// If this is the first deployment done with automatic schema evolution, generate everything as if it was a drop create
|
|
@@ -138,9 +136,8 @@ exports.create = async function (db, csn=db.model, o) {
|
|
|
138
136
|
return db.deploy(csn, o);
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
const in_memory = db.options?.credentials?.url === ':memory:';
|
|
142
139
|
let beforeCsn
|
|
143
|
-
if (
|
|
140
|
+
if (schemaEvo) try {
|
|
144
141
|
const [{ csn }] = await db.read('cds.Model')
|
|
145
142
|
beforeCsn = JSON.parse(csn);
|
|
146
143
|
} catch(e) {
|
|
@@ -160,7 +157,7 @@ exports.create = async function (db, csn=db.model, o) {
|
|
|
160
157
|
} else return db.run (async tx => {
|
|
161
158
|
await tx.run(drops)
|
|
162
159
|
await tx.run(creas)
|
|
163
|
-
if (
|
|
160
|
+
if (schemaEvo) {
|
|
164
161
|
await tx.update('cds.Model').with({ csn: JSON.stringify(afterCsn) })
|
|
165
162
|
}
|
|
166
163
|
return true
|
|
@@ -169,10 +166,11 @@ exports.create = async function (db, csn=db.model, o) {
|
|
|
169
166
|
|
|
170
167
|
|
|
171
168
|
exports.init = (db, csn=db.model, o, csvs, log=()=>{}) => db.run (async tx => {
|
|
169
|
+
|
|
170
|
+
const {tenant} = cds.context; if (tenant && tenant === cds.requires.multitenancy?.t0) return
|
|
171
|
+
const schemaEvo = db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto'
|
|
172
172
|
const resources = await exports.resources(csn, {testdata: cds.env.features.test_data})
|
|
173
173
|
const inits=[]
|
|
174
|
-
const schemaEvo = (db.options?.schema_evolution === 'auto' || o?.schema_evolution === 'auto')
|
|
175
|
-
|
|
176
174
|
|
|
177
175
|
if (csvs) {
|
|
178
176
|
const ccsn = cds.compile.for['nodejs'](csn) // compile to calculate keys for newly added entities
|
package/lib/index.js
CHANGED
|
@@ -12,8 +12,9 @@ const facade = class cds extends require('events') {
|
|
|
12
12
|
set context(_){ this._context._for(this,_) }
|
|
13
13
|
get spawn() { return super.spawn = this._context.spawn }
|
|
14
14
|
|
|
15
|
-
emit (eve, ...args) {
|
|
16
|
-
if (eve === 'served') return Promise.all (this.listeners(eve).map (l => l.call(this,...args)))
|
|
15
|
+
async emit (eve, ...args) {
|
|
16
|
+
// if (eve === 'served') return Promise.all (this.listeners(eve).map (l => l.call(this,...args)))
|
|
17
|
+
if (eve === 'served') for (let l of this.listeners(eve)) await l.call(this,...args)
|
|
17
18
|
else return super.emit (eve, ...args)
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -93,6 +94,7 @@ const cds = module.exports = extend (new facade) .with ({
|
|
|
93
94
|
test: require ('./utils/cds-test'),
|
|
94
95
|
log: require ('./log/cds-log'), debug: lazy => cds.log.debug,
|
|
95
96
|
exec: require ('../bin/cds'),
|
|
97
|
+
exit: (code) => cds.shutdown ? cds.shutdown() : process.exit(code),
|
|
96
98
|
clone: m => JSON.parse (JSON.stringify(m)),
|
|
97
99
|
lazified, lazify,
|
|
98
100
|
|
package/lib/req/cds-context.js
CHANGED
|
@@ -47,9 +47,9 @@ module.exports = new class extends AsyncLocalStorage {
|
|
|
47
47
|
})
|
|
48
48
|
}
|
|
49
49
|
const em = new EventEmitter; em.timer = (
|
|
50
|
-
(o && o.after) ? setTimeout(fx, o.after) :
|
|
51
|
-
(o && o.every) ? setInterval(fx, o.every) :
|
|
52
|
-
setImmediate(fx)
|
|
50
|
+
(o && o.after) ? setTimeout(fx, o.after).unref() :
|
|
51
|
+
(o && o.every) ? setInterval(fx, o.every).unref() :
|
|
52
|
+
setImmediate(fx).unref()
|
|
53
53
|
)
|
|
54
54
|
return em
|
|
55
55
|
}
|
package/lib/srv/bindings.js
CHANGED
package/lib/srv/cds-serve.js
CHANGED
|
@@ -56,7 +56,8 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
|
|
|
56
56
|
if (o.service && o.service !== 'all') {
|
|
57
57
|
// skip services not chosen by o.service, if specified
|
|
58
58
|
const specified = o.service.split(/\s*,\s*/).map (s => required[s] && required[s].service || s )
|
|
59
|
-
|
|
59
|
+
// matching exact or unqualified name
|
|
60
|
+
services = services.filter (s => specified.some (n => s.name === n || s.name.endsWith('.'+n)))
|
|
60
61
|
if (!services.length) throw cds.error (`No such service: '${o.service}'`)
|
|
61
62
|
}
|
|
62
63
|
services = services.filter (d => !(
|
|
@@ -66,15 +66,22 @@ function _instrument_sqlite (_get_perf) {
|
|
|
66
66
|
try { require.resolve('sqlite3') } catch { return }
|
|
67
67
|
// eslint-disable-next-line cds/no-missing-dependencies
|
|
68
68
|
const sqlite = require('sqlite3').Database.prototype
|
|
69
|
-
for (let each of ['all', 'get', 'run', 'prepare'])
|
|
70
|
-
|
|
71
|
-
sqlite[
|
|
72
|
-
|
|
69
|
+
for (let each of ['all', 'get', 'run', 'prepare']) _wrap(each,sqlite)
|
|
70
|
+
function _wrap (op,sqlite) {
|
|
71
|
+
const _super = sqlite[op]
|
|
72
|
+
sqlite[op] = function (q, ..._) {
|
|
73
|
+
const perf = _get_perf(q) //> q is a SQL command like BEGIN, COMMIT, ROLLBACK, SELECT ...
|
|
73
74
|
if (perf) {
|
|
74
|
-
const pe = perf.log ('
|
|
75
|
-
const callback = _[_.length-1]; _[_.length-1] = function(){
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const pe = perf.log ('sqlite3', '-', q)
|
|
76
|
+
const callback = _[_.length-1]; _[_.length-1] = function(ps){
|
|
77
|
+
if (op === 'prepare') callback.apply (this, {
|
|
78
|
+
all: _wrap('all',ps),
|
|
79
|
+
get: _wrap('get',ps),
|
|
80
|
+
run: _wrap('run',ps),
|
|
81
|
+
}); else {
|
|
82
|
+
perf.done(pe)
|
|
83
|
+
callback.apply (this, arguments)
|
|
84
|
+
}
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
return _super.call (this, q, ..._)
|
|
@@ -87,14 +94,23 @@ function _instrument_better_sqlite (_get_perf) {
|
|
|
87
94
|
try { require.resolve('better-sqlite3') } catch { return }
|
|
88
95
|
// eslint-disable-next-line cds/no-missing-dependencies
|
|
89
96
|
const sqlite = require('better-sqlite3').prototype
|
|
90
|
-
for (let each of ['exec', 'prepare'])
|
|
91
|
-
|
|
92
|
-
sqlite[
|
|
93
|
-
|
|
97
|
+
for (let each of ['exec', 'prepare']) _wrap(each,sqlite)
|
|
98
|
+
function _wrap (op,sqlite) {
|
|
99
|
+
const _super = sqlite[op]
|
|
100
|
+
sqlite[op] = function (q, ..._) {
|
|
101
|
+
const perf = _get_perf(q) //> q is a SQL command like BEGIN, COMMIT, ROLLBACK, SELECT ...
|
|
94
102
|
if (perf) {
|
|
95
|
-
const pe = perf.log ('
|
|
96
|
-
try {
|
|
97
|
-
|
|
103
|
+
const pe = perf.log ('better-sqlite3', '-', q)
|
|
104
|
+
try {
|
|
105
|
+
const x = _super.call (this, q, ..._)
|
|
106
|
+
if (op === 'prepare') return {
|
|
107
|
+
all(..._){ try { return x.all(..._) } finally { perf.done(pe) }},
|
|
108
|
+
get(..._){ try { return x.get(..._) } finally { perf.done(pe) }},
|
|
109
|
+
run(..._){ try { return x.run(..._) } finally { perf.done(pe) }},
|
|
110
|
+
}
|
|
111
|
+
else return x
|
|
112
|
+
}
|
|
113
|
+
catch(e) { perf.done(pe); throw e }
|
|
98
114
|
}
|
|
99
115
|
else return _super.call (this, q, ..._)
|
|
100
116
|
}
|
|
@@ -938,7 +938,7 @@ function cov2ap(options = {}) {
|
|
|
938
938
|
}
|
|
939
939
|
}
|
|
940
940
|
|
|
941
|
-
Object.
|
|
941
|
+
Object.keys(headers).forEach(name => {
|
|
942
942
|
if (
|
|
943
943
|
name === "dataserviceversion" ||
|
|
944
944
|
name === "DataServiceVersion" ||
|
|
@@ -1171,7 +1171,7 @@ function cov2ap(options = {}) {
|
|
|
1171
1171
|
return req.context;
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
-
function convertUrlLinks(url
|
|
1174
|
+
function convertUrlLinks(url) {
|
|
1175
1175
|
url.contextPath = url.contextPath.replace(/\/\$links\//gi, "/");
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
@@ -1630,7 +1630,7 @@ function cov2ap(options = {}) {
|
|
|
1630
1630
|
}
|
|
1631
1631
|
}
|
|
1632
1632
|
|
|
1633
|
-
function convertFilter(url
|
|
1633
|
+
function convertFilter(url) {
|
|
1634
1634
|
const _ = "§§";
|
|
1635
1635
|
|
|
1636
1636
|
let filter = url.query["$filter"];
|
|
@@ -1702,7 +1702,7 @@ function cov2ap(options = {}) {
|
|
|
1702
1702
|
}
|
|
1703
1703
|
}
|
|
1704
1704
|
|
|
1705
|
-
function convertSearch(url
|
|
1705
|
+
function convertSearch(url) {
|
|
1706
1706
|
if (url.query.search) {
|
|
1707
1707
|
let search = url.query.search;
|
|
1708
1708
|
if (quoteSearch) {
|
|
@@ -2789,7 +2789,7 @@ function cov2ap(options = {}) {
|
|
|
2789
2789
|
}
|
|
2790
2790
|
}
|
|
2791
2791
|
|
|
2792
|
-
function removeMetadata(data
|
|
2792
|
+
function removeMetadata(data) {
|
|
2793
2793
|
Object.keys(data).forEach((key) => {
|
|
2794
2794
|
if (key.startsWith("@") || key.startsWith("odata.") || key.includes("@odata.")) {
|
|
2795
2795
|
delete data[key];
|
|
@@ -2797,7 +2797,7 @@ function cov2ap(options = {}) {
|
|
|
2797
2797
|
});
|
|
2798
2798
|
}
|
|
2799
2799
|
|
|
2800
|
-
function convertMedia(data
|
|
2800
|
+
function convertMedia(data) {
|
|
2801
2801
|
Object.keys(data).forEach((key) => {
|
|
2802
2802
|
if (key.endsWith("@odata.mediaReadLink")) {
|
|
2803
2803
|
data[key.split("@odata.mediaReadLink")[0]] = data[key];
|
|
@@ -2807,7 +2807,7 @@ function cov2ap(options = {}) {
|
|
|
2807
2807
|
});
|
|
2808
2808
|
}
|
|
2809
2809
|
|
|
2810
|
-
function removeAnnotations(data
|
|
2810
|
+
function removeAnnotations(data) {
|
|
2811
2811
|
Object.keys(data).forEach((key) => {
|
|
2812
2812
|
if (key.startsWith("@")) {
|
|
2813
2813
|
delete data[key];
|
|
@@ -2999,7 +2999,7 @@ function cov2ap(options = {}) {
|
|
|
2999
2999
|
return `PT${timeParts[0] || "00"}H${timeParts[1] || "00"}M${timeParts[2] || "00"}S`;
|
|
3000
3000
|
}
|
|
3001
3001
|
|
|
3002
|
-
function addResultsNesting(data, headers, definition, elements
|
|
3002
|
+
function addResultsNesting(data, headers, definition, elements) {
|
|
3003
3003
|
if (!returnCollectionNested) {
|
|
3004
3004
|
return;
|
|
3005
3005
|
}
|
package/lib/srv/srv-handlers.js
CHANGED
|
@@ -16,13 +16,19 @@ class EventHandlers {
|
|
|
16
16
|
)}
|
|
17
17
|
|
|
18
18
|
async prepend (...impl_functions) {
|
|
19
|
-
// IMPORTANT: We might be called in parallel -> the ._handlers.
|
|
20
|
-
// game below avoids loosing registrations due to race conditions
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
// IMPORTANT: We might be called in parallel -> the ._handlers._real
|
|
20
|
+
// game below avoids loosing registrations due to race conditions.
|
|
21
|
+
// Note also that {__proto__:this, _handlers:_new} doesn't work as
|
|
22
|
+
// usages frequently look like that: srv.prepend(()=>srv.on(...)),
|
|
23
|
+
// which means the derived srv instance would be bypassed.
|
|
24
|
+
const _real = this._handlers._real || this._handlers
|
|
25
|
+
const _new = { on:[], before:[], after:[], _initial:[], _error:[], _real }
|
|
26
|
+
await Promise.all (impl_functions.map (fn => { if (is_impl(fn)) {
|
|
27
|
+
this._handlers = _new
|
|
28
|
+
return fn.call (this,this)
|
|
29
|
+
}}))
|
|
30
|
+
for (let handlers in _new) if (_new[handlers].length) _real[handlers] = [ ..._new[handlers], ..._real[handlers] ]
|
|
31
|
+
this._handlers = _real
|
|
26
32
|
return this
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -85,6 +91,19 @@ const _register = function (srv, phase, event, path, handler) { //NOSONAR
|
|
|
85
91
|
if (!path.startsWith(srv.name+'.')) path = `${srv.name}.${path}`
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
if (cds.env.features.lean_draft && cds.env.features.lean_draft_compatibility) {
|
|
95
|
+
const entity = path && srv.model?.definitions[path.name || path]
|
|
96
|
+
if (['PATCH', 'CANCEL', 'NEW'].includes(event)) {
|
|
97
|
+
// delegate to drafts
|
|
98
|
+
path = typeof path === 'string' && path !== '*' && !path.endsWith('.drafts') ? path + '.drafts' : typeof path === 'object' && path.drafts || path
|
|
99
|
+
if (event === 'PATCH') event = 'UPDATE'
|
|
100
|
+
}
|
|
101
|
+
else if (entity && (event === 'READ' || entity.actions?.[event]) && (entity.drafts && !entity.name.endsWith('.drafts'))) {
|
|
102
|
+
// additionally add drafts for READ and bound actions/functions
|
|
103
|
+
_register(srv, phase, event, entity.name + '.drafts', handler)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
// Finally register with a filter function to match requests to be handled
|
|
89
108
|
const _handlers = srv._handlers [event === 'error' ? '_error' : (handler._initial ? '_initial' : phase)] // REVISIT: remove _initial handlers
|
|
90
109
|
_handlers.push (new EventHandler (phase, event, path, handler))
|
package/lib/srv/srv-methods.js
CHANGED
|
@@ -4,8 +4,8 @@ const LOG = cds.log('cds.serve',{label:'cds'})
|
|
|
4
4
|
|
|
5
5
|
module.exports = (srv) => {
|
|
6
6
|
if (srv.model && ( //> we only support that for app services
|
|
7
|
-
srv
|
|
8
|
-
srv
|
|
7
|
+
srv.isAppService ||
|
|
8
|
+
srv.isExternal ||
|
|
9
9
|
srv._add_stub_methods
|
|
10
10
|
)) {
|
|
11
11
|
for (const each of srv.operations) {
|
package/lib/srv/srv-models.js
CHANGED
|
@@ -61,7 +61,8 @@ class ExtendedModels {
|
|
|
61
61
|
try {
|
|
62
62
|
_has_extensions = tenant && extensibility && await _is_extended(tenant)
|
|
63
63
|
} catch (error) {
|
|
64
|
-
|
|
64
|
+
// Better error message for client
|
|
65
|
+
cds.error('`extensibility: true` is configured but table "cds.xt.Extensions" does not exist. Please redeploy.', error)
|
|
65
66
|
}
|
|
66
67
|
if (!_has_extensions) {
|
|
67
68
|
let k = cache.key4 (tenant = undefined, features)
|
|
@@ -151,8 +152,7 @@ class ExtendedModels {
|
|
|
151
152
|
if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
|
|
152
153
|
delete this [key]
|
|
153
154
|
}
|
|
154
|
-
}}, ExtendedModels.sentinelInterval)
|
|
155
|
-
cds.on('shutdown', ()=> clearInterval(this.sentinel))
|
|
155
|
+
}}, ExtendedModels.sentinelInterval).unref()
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -29,10 +29,8 @@ class Test extends require('./axios') {
|
|
|
29
29
|
catch (e) { if (is_mocha) console.error(e) } // eslint-disable-line no-console
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
// shutdown cds server...
|
|
33
|
-
after (
|
|
34
|
-
this.server ? this.server.close (done) : done && done()
|
|
35
|
-
})
|
|
32
|
+
// gracefully shutdown cds server...
|
|
33
|
+
after (() => this.server && cds.shutdown())
|
|
36
34
|
|
|
37
35
|
beforeEach (async () => {
|
|
38
36
|
if (this.data._autoReset) await this.data.reset()
|
|
@@ -80,7 +78,11 @@ class Test extends require('./axios') {
|
|
|
80
78
|
|
|
81
79
|
then(r) {
|
|
82
80
|
const {cds} = this
|
|
83
|
-
|
|
81
|
+
if (this.server) {
|
|
82
|
+
r({ server: this.server, url: this.url })
|
|
83
|
+
} else {
|
|
84
|
+
cds.once('listening', r)
|
|
85
|
+
}
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -66,7 +66,7 @@ module.exports = function ias_auth(config) {
|
|
|
66
66
|
req.tenant = req.tokenInfo.getZoneId()
|
|
67
67
|
next()
|
|
68
68
|
})
|
|
69
|
-
.use((err, req, res,
|
|
69
|
+
.use((err, req, res, _next) => {
|
|
70
70
|
if (req.tokenInfo) {
|
|
71
71
|
LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
|
|
72
72
|
}
|
|
@@ -253,8 +253,12 @@ class OData {
|
|
|
253
253
|
this._odataService.process(req, res).catch(err => {
|
|
254
254
|
LOG.warn(err)
|
|
255
255
|
// REVISIT: use i18n
|
|
256
|
-
//do not reply with error, if response already processed (streaming)
|
|
257
|
-
|
|
256
|
+
// do not reply with error, if response already processed (streaming)
|
|
257
|
+
// destroy response socket instead
|
|
258
|
+
if (res.headersSent) {
|
|
259
|
+
// REVISIT: temp solution until streaming is switched to express middlewares
|
|
260
|
+
res.socket.destroy()
|
|
261
|
+
} else {
|
|
258
262
|
const { error, statusCode } = normalizeError(err, req)
|
|
259
263
|
res.status(statusCode).send({ error })
|
|
260
264
|
}
|
|
@@ -251,6 +251,14 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
251
251
|
return odataResult
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
const _reliablePagingPossible = req => {
|
|
255
|
+
if (req.target._isDraftEnabled) return false
|
|
256
|
+
if (cds.context?.http.req.query.$apply) return false
|
|
257
|
+
if (req.query.SELECT.limit.offset?.val ?? req.query.SELECT.limit.offset > 0) return false
|
|
258
|
+
if (req.query.SELECT.orderBy.some(o => !o.ref)) return false
|
|
259
|
+
return req.query.SELECT.orderBy.every(o => req.query.SELECT.columns.some(c => o.ref[0] === c.ref[0]))
|
|
260
|
+
}
|
|
261
|
+
|
|
254
262
|
/**
|
|
255
263
|
* Read an entity collection without including the count of the total amount of entities.
|
|
256
264
|
*
|
|
@@ -260,6 +268,7 @@ const _readEntityOrProperty = async (tx, req, segments) => {
|
|
|
260
268
|
* @returns {Promise}
|
|
261
269
|
* @private
|
|
262
270
|
*/
|
|
271
|
+
// eslint-disable-next-line complexity
|
|
263
272
|
const _readCollection = async (tx, req, odataReq) => {
|
|
264
273
|
const result = (await tx.dispatch(req)) || []
|
|
265
274
|
if (Array.isArray(req.query)) {
|
|
@@ -284,7 +293,23 @@ const _readCollection = async (tx, req, odataReq) => {
|
|
|
284
293
|
const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)
|
|
285
294
|
if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
|
|
286
295
|
const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN)
|
|
287
|
-
|
|
296
|
+
if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
|
|
297
|
+
const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
|
|
298
|
+
const skipToken = {
|
|
299
|
+
r: (decoded?.r || 0) + limit,
|
|
300
|
+
c: req.query.SELECT.orderBy.map(o => ({
|
|
301
|
+
a: o.sort ? o.sort === 'asc' : true,
|
|
302
|
+
k: o.ref[0],
|
|
303
|
+
v: result[result.length - 1][o.ref[0]]
|
|
304
|
+
}))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (limit + (decoded?.r || 0) !== top) {
|
|
308
|
+
result.$nextLink = Buffer.from(JSON.stringify(skipToken)).toString('base64')
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
result.$nextLink = (token ? parseInt(token) : 0) + limit
|
|
312
|
+
}
|
|
288
313
|
}
|
|
289
314
|
|
|
290
315
|
const odataResult = toODataResult(result, req)
|
|
@@ -11,6 +11,7 @@ const { isReturnMinimal } = require('../utils/handlerUtils')
|
|
|
11
11
|
const { readAfterWrite } = require('../utils/readAfterWrite')
|
|
12
12
|
const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
|
|
13
13
|
const { hasOmitValuesPreference } = require('../utils/omitValues')
|
|
14
|
+
const { isStreaming } = require('../utils/stream')
|
|
14
15
|
|
|
15
16
|
const { getSapMessages } = require('../../../../common/error/frontend')
|
|
16
17
|
|
|
@@ -146,6 +147,13 @@ const update = service => {
|
|
|
146
147
|
previousResult = await readAfterWrite(req, service, { isBefore: true })
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// in case of express errors in streaming do rollback
|
|
151
|
+
const segments = odataReq.getUriInfo().getPathSegments()
|
|
152
|
+
if (isStreaming(segments)) {
|
|
153
|
+
odataReq.getIncomingRequest().on('error', async err => {
|
|
154
|
+
await tx.rollback(err).catch(() => {})
|
|
155
|
+
})
|
|
156
|
+
}
|
|
149
157
|
// try UPDATE and, on 404 error, try CREATE
|
|
150
158
|
;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
|
|
151
159
|
|