@sap/cds 6.5.0 → 6.6.1
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 +53 -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/deploy/to-hana/hdiDeployUtil.js +24 -12
- 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/env/schemas/cds-rc.json +4 -0
- 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 +4 -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 +1 -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 +7 -8
- 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 +22 -10
- 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/structured.js +1 -0
- 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 +6 -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
|
@@ -5,8 +5,9 @@ const util = require('util');
|
|
|
5
5
|
const execAsync = util.promisify(cp.exec);
|
|
6
6
|
|
|
7
7
|
const cds = require('../../../lib');
|
|
8
|
-
const { SILENT } = cds.log.levels;
|
|
9
8
|
const LOG = cds.log ? cds.log('deploy') : console;
|
|
9
|
+
const DEBUG = cds.debug('deploy');
|
|
10
|
+
const { SILENT } = cds.log.levels;
|
|
10
11
|
|
|
11
12
|
class HdiDeployUtil {
|
|
12
13
|
|
|
@@ -17,7 +18,7 @@ class HdiDeployUtil {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async deployTenant(dbDir, env) {
|
|
20
|
-
await this._executeDeploy(dbDir, env);
|
|
21
|
+
await this._executeDeploy(dbDir, env, true);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async deploy(dbDir, vcapEnv, options) {
|
|
@@ -89,7 +90,7 @@ class HdiDeployUtil {
|
|
|
89
90
|
Add it either as a devDependency using 'npm install -D ${this.deployerName}' or install it globally using 'npm install -g ${this.deployerName}'.`);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
LOG.info(`Using HDI deployer from ${libPath}`)
|
|
93
94
|
|
|
94
95
|
// let any error go through and abort deploy
|
|
95
96
|
return require(libPath);
|
|
@@ -109,29 +110,40 @@ Add it either as a devDependency using 'npm install -D ${this.deployerName}' or
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
|
|
112
|
-
async _executeDeploy(dbDir, env) {
|
|
113
|
+
async _executeDeploy(dbDir, env, fromMtx) {
|
|
113
114
|
const hdiDeployLib = await this._getHdiDeployLib(dbDir);
|
|
115
|
+
let writeStream
|
|
116
|
+
if (fromMtx) {
|
|
117
|
+
await cds.utils.mkdirp('logs')
|
|
118
|
+
writeStream = require('fs').createWriteStream(path.join(cds.root, 'logs', `${cds.context.tenant}.log`))
|
|
119
|
+
}
|
|
114
120
|
return new Promise((resolve, reject) => {
|
|
115
121
|
const callbacks = {
|
|
116
|
-
stderrCB:
|
|
122
|
+
stderrCB: buffer => {
|
|
123
|
+
LOG.error(buffer.toString())
|
|
124
|
+
writeStream?.write(buffer)
|
|
125
|
+
},
|
|
117
126
|
}
|
|
118
|
-
if (
|
|
119
|
-
callbacks.stdoutCB =
|
|
127
|
+
if (fromMtx) {
|
|
128
|
+
callbacks.stdoutCB = buffer => {
|
|
129
|
+
DEBUG?.(buffer.toString())
|
|
130
|
+
writeStream.write(buffer)
|
|
131
|
+
}
|
|
132
|
+
} else if (LOG.level !== SILENT) {
|
|
133
|
+
callbacks.stdoutCB = buffer => LOG.info(buffer.toString())
|
|
120
134
|
}
|
|
121
|
-
|
|
122
135
|
hdiDeployLib.deploy(dbDir, env, (error, response) => {
|
|
123
136
|
if (error) {
|
|
124
137
|
return reject(error);
|
|
125
138
|
}
|
|
126
|
-
if (response
|
|
139
|
+
if (response?.exitCode) {
|
|
127
140
|
let message = `HDI deployment failed with exit code ${response.exitCode}`
|
|
128
141
|
if (response.signal) message += `. ${response.signal}`
|
|
129
142
|
return reject(new Error(message));
|
|
130
143
|
}
|
|
131
144
|
return resolve();
|
|
132
|
-
}, callbacks
|
|
133
|
-
|
|
134
|
-
});
|
|
145
|
+
}, callbacks);
|
|
146
|
+
}).finally(() => writeStream.end());
|
|
135
147
|
}
|
|
136
148
|
}
|
|
137
149
|
|
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
|
|
@@ -342,6 +342,10 @@
|
|
|
342
342
|
"description": "HDI deployment parameters as defined on https://www.npmjs.com/package/@sap/hdi-deploy#supported-features"
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
+
},
|
|
346
|
+
"lazyT0": {
|
|
347
|
+
"type": "boolean",
|
|
348
|
+
"description": "Onboard bookkeeping t0 container at the first subscription."
|
|
345
349
|
}
|
|
346
350
|
}
|
|
347
351
|
}
|
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)
|
|
@@ -114,6 +115,7 @@ class ExtendedModels {
|
|
|
114
115
|
})
|
|
115
116
|
if (has_new_extensions) { // new extensions arrived -> refresh model in cache
|
|
116
117
|
let [ tenant = undefined, toggles ] = key.split(':')
|
|
118
|
+
cds.emit('cds.xt.TENANT_UPDATED', { tenant })
|
|
117
119
|
return _get_model4 (tenant, toggles.split(','))
|
|
118
120
|
} else { // no new extensions...
|
|
119
121
|
_cached.touched = Date.now() // check again in 1 min or so
|
|
@@ -151,8 +153,7 @@ class ExtendedModels {
|
|
|
151
153
|
if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
|
|
152
154
|
delete this [key]
|
|
153
155
|
}
|
|
154
|
-
}}, ExtendedModels.sentinelInterval)
|
|
155
|
-
cds.on('shutdown', ()=> clearInterval(this.sentinel))
|
|
156
|
+
}}, ExtendedModels.sentinelInterval).unref()
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
|
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
|
}
|