@sap/cds-dk 10.2.0 → 10.2.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 +13 -0
- package/bin/bind.js +5 -4
- package/bin/completion/scripts/cds.fish +4 -3
- package/bin/completion/scripts/cds.ps1 +4 -4
- package/bin/completion/scripts/cds.sh +4 -4
- package/bin/completion/scripts/cds.zsh +4 -4
- package/bin/debug.js +3 -3
- package/bin/deploy/index.js +2 -2
- package/bin/deploy/to-hana/hana.js +1 -1
- package/bin/deploy/to-hana/hdiDeployUtil.js +25 -14
- package/bin/up.js +3 -3
- package/bin/version.js +1 -1
- package/bin/watch/vite-watch.js +12 -6
- package/lib/bind/shared.js +1 -1
- package/lib/init/index.js +1 -0
- package/lib/init/projectReader.js +3 -2
- package/lib/init/template/approuter/files/values.yaml.hbs +0 -9
- package/lib/init/template/approuter/files/xs-app.json.hbs +0 -22
- package/lib/init/template/approuter/index.js +8 -9
- package/lib/init/template/html5-repo/index.js +12 -0
- package/lib/init/template/react/files/vite.config.{{extension}}.hbs +2 -3
- package/lib/init/template/react/index.js +1 -3
- package/lib/init/template/sample/files/app/index.html.hbs +18 -0
- package/lib/init/template/sample/index.js +1 -0
- package/lib/init/template/vue/files/vite.config.{{extension}}.hbs +2 -3
- package/lib/init/template/vue/index.js +1 -3
- package/lib/mtx/push.js +7 -1
- package/node_modules/@cap-js/asyncapi/README.md +4 -14
- package/node_modules/@cap-js/asyncapi/cds-plugin.js +12 -0
- package/node_modules/@cap-js/asyncapi/index.js +3 -2
- package/node_modules/@cap-js/asyncapi/lib/compile/components/messages/index.js +4 -2
- package/node_modules/@cap-js/asyncapi/lib/compile/components/schemas/definition.js +11 -1
- package/node_modules/@cap-js/asyncapi/lib/compile/components/schemas/utils.js +16 -0
- package/node_modules/@cap-js/asyncapi/lib/compile/utils.js +4 -3
- package/node_modules/@cap-js/asyncapi/lib/import/importAsyncAPI.js +34 -0
- package/node_modules/@cap-js/asyncapi/lib/import/index.js +18 -0
- package/node_modules/@cap-js/asyncapi/lib/import/message.js +9 -0
- package/node_modules/@cap-js/asyncapi/lib/import/parserContext.js +368 -0
- package/node_modules/@cap-js/asyncapi/lib/import/utils.js +85 -0
- package/node_modules/@cap-js/asyncapi/package.json +89 -1
- package/node_modules/@sap/cds/CHANGELOG.md +10 -0
- package/node_modules/@sap/cds/app/index.js +1 -1
- package/node_modules/@sap/cds/lib/env/schemas/cds-package.js +0 -2
- package/node_modules/@sap/cds/lib/env/schemas/cds-rc.js +3 -3
- package/node_modules/@sap/cds/lib/env/schemas/index.js +4 -4
- package/node_modules/@sap/cds/lib/utils/cds-utils.js +21 -8
- package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/connections.js +45 -39
- package/node_modules/@sap/cds/libx/odata/ODataAdapter.js +1 -1
- package/node_modules/@sap/cds/libx/queue/TaskRunner.js +22 -5
- package/node_modules/@sap/cds/package.json +1 -1
- package/node_modules/@sap/cds-mtxs/CHANGELOG.md +8 -0
- package/node_modules/@sap/cds-mtxs/package.json +1 -1
- package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +57 -41
- package/node_modules/@sap/cds-mtxs/srv/jobs-service.js +27 -12
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +31 -25
- package/node_modules/body-parser/README.md +17 -13
- package/node_modules/body-parser/lib/read.js +74 -57
- package/node_modules/body-parser/lib/types/json.js +11 -11
- package/node_modules/body-parser/lib/types/urlencoded.js +5 -5
- package/node_modules/body-parser/lib/utils.js +0 -15
- package/node_modules/body-parser/package.json +7 -12
- package/node_modules/encodeurl/README.md +14 -0
- package/node_modules/encodeurl/index.js +5 -10
- package/node_modules/encodeurl/package.json +10 -6
- package/node_modules/on-finished/index.js +14 -2
- package/node_modules/on-finished/package.json +11 -7
- package/node_modules/qs/.editorconfig +1 -1
- package/node_modules/qs/CHANGELOG.md +87 -22
- package/node_modules/qs/README.md +63 -10
- package/node_modules/qs/dist/qs.js +17 -17
- package/node_modules/qs/eslint.config.mjs +61 -50
- package/node_modules/qs/lib/formats.js +7 -2
- package/node_modules/qs/lib/parse.js +29 -10
- package/node_modules/qs/lib/stringify.js +5 -3
- package/node_modules/qs/lib/utils.js +28 -9
- package/node_modules/qs/package.json +6 -6
- package/node_modules/qs/test/parse.js +335 -2
- package/node_modules/qs/test/stringify.js +222 -0
- package/node_modules/qs/test/utils.js +61 -3
- package/node_modules/semver/classes/range.js +2 -2
- package/node_modules/semver/internal/constants.js +30 -0
- package/node_modules/semver/internal/trim-comparator-whitespace.js +438 -0
- package/node_modules/semver/package.json +1 -1
- package/node_modules/ws/README.md +21 -29
- package/node_modules/ws/lib/buffer-util.js +13 -2
- package/node_modules/ws/lib/sender.js +4 -1
- package/node_modules/ws/lib/websocket.js +8 -3
- package/node_modules/ws/package.json +1 -1
- package/package.json +1 -1
- package/node_modules/on-finished/HISTORY.md +0 -98
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const messages = require('./message')
|
|
2
|
+
|
|
3
|
+
function validateAsyncAPI(content) {
|
|
4
|
+
if ('channels' in content && 'components' in content && 'messages' in content.components) {
|
|
5
|
+
return
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
throw new Error(messages.INVALID_ASYNCAPI_FILE)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Accepted scenarios:
|
|
13
|
+
* * with version info : <namespace>.<businessObject>.<operation>.<version>
|
|
14
|
+
* * without version info : <namespace>.<businessObject>.<operation>
|
|
15
|
+
*
|
|
16
|
+
* ? Is it mandatory for <namespace> to always follow small case notation?
|
|
17
|
+
* ? Is it mandatory for <businessObject> to follow Camel Case or Upper Came Case notation?
|
|
18
|
+
*
|
|
19
|
+
* ToDo: Can we support `-` in operations?
|
|
20
|
+
*/
|
|
21
|
+
const regExp = new RegExp(
|
|
22
|
+
'^(?<namespace>(?:[a-z][a-z0-9]*)(?:[.][a-z][a-z0-9]*)+)[.](?<businessObject>[a-zA-Z0-9]+)[.](?<operation>[a-zA-Z0-9]+)[.]?(?<version>v(?:[0-9]|[1-9][0-9]*))?$'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates the messages' `key` with the regExp and returns the regEx groups.
|
|
27
|
+
* @returns regexGroups
|
|
28
|
+
*/
|
|
29
|
+
function getRegExpGroups(key) {
|
|
30
|
+
/**
|
|
31
|
+
* Result format:
|
|
32
|
+
* index 0: key
|
|
33
|
+
* index 1: <namespace> value
|
|
34
|
+
* index 2: <businessObject> value
|
|
35
|
+
* index 3: <operation> value
|
|
36
|
+
* index 4: <version> value; undefined if <version> is optional
|
|
37
|
+
*/
|
|
38
|
+
const regExpGroups = regExp.exec(key)
|
|
39
|
+
|
|
40
|
+
if (regExpGroups == null) {
|
|
41
|
+
throw new Error(messages.INVALID_MESSAGE_NAME)
|
|
42
|
+
}
|
|
43
|
+
return regExpGroups
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parses a message name into a [fullName, serviceName] pair.
|
|
48
|
+
*
|
|
49
|
+
* Two formats are supported:
|
|
50
|
+
*
|
|
51
|
+
* 1. SAP event-catalog format: `<namespace>.<BusinessObject>.<Operation>[.v<n>]`
|
|
52
|
+
* where <namespace> is ≥2 lowercase dot-separated segments (e.g. `sap.s4.beh`).
|
|
53
|
+
* Detected by matching the strict regex; service key = namespace + '.' + businessObject.
|
|
54
|
+
*
|
|
55
|
+
* 2. CAP-produced format: `<ServiceName>.<EventName>` (optionally nested), where the
|
|
56
|
+
* first segment is always the PascalCase CDS service name.
|
|
57
|
+
* Detected by the presence of `x-sap-application-namespace` in the AsyncAPI document.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} key - The message name to parse
|
|
60
|
+
* @param {string|undefined} applicationNamespace - Value of `x-sap-application-namespace`
|
|
61
|
+
* from the AsyncAPI document, or undefined if absent
|
|
62
|
+
* @returns {Array} [fullName, serviceName]
|
|
63
|
+
*/
|
|
64
|
+
function parseMessageName(key, applicationNamespace) {
|
|
65
|
+
const regExpGroups = regExp.exec(key)
|
|
66
|
+
if (regExpGroups != null) {
|
|
67
|
+
const serviceKey = regExpGroups[1] + '.' + regExpGroups[2]
|
|
68
|
+
return [regExpGroups[0], serviceKey]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (applicationNamespace) {
|
|
72
|
+
const segments = key.split('.')
|
|
73
|
+
if (segments.length >= 2) {
|
|
74
|
+
return [key, segments[0]]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error(messages.INVALID_MESSAGE_NAME)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
validateAsyncAPI,
|
|
83
|
+
getRegExpGroups,
|
|
84
|
+
parseMessageName
|
|
85
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/asyncapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CAP tool for AsyncAPI",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,9 +16,14 @@
|
|
|
16
16
|
"author": "SAP SE (https://www.sap.com)",
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"main": "index.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./index.js",
|
|
21
|
+
"./cds-plugin": "./cds-plugin.js"
|
|
22
|
+
},
|
|
19
23
|
"files": [
|
|
20
24
|
"lib/",
|
|
21
25
|
"index.js",
|
|
26
|
+
"cds-plugin.js",
|
|
22
27
|
"LICENSE"
|
|
23
28
|
],
|
|
24
29
|
"scripts": {
|
|
@@ -29,6 +34,89 @@
|
|
|
29
34
|
"@sap/cds": ">=7.6"
|
|
30
35
|
},
|
|
31
36
|
"devDependencies": {
|
|
37
|
+
"@sap/cds-dk": "^10",
|
|
32
38
|
"eslint": "^10.0.0"
|
|
39
|
+
},
|
|
40
|
+
"cds": {
|
|
41
|
+
"schema": {
|
|
42
|
+
"export": {
|
|
43
|
+
"asyncapi": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"description": "AsyncAPI settings for cds compile",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"properties": {
|
|
48
|
+
"application_namespace": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "The registered namespace of the application"
|
|
51
|
+
},
|
|
52
|
+
"merged": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"description": "properties that needs to be there when merged option is used",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"properties": {
|
|
57
|
+
"title": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Title to be used for merged doc"
|
|
60
|
+
},
|
|
61
|
+
"version": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Version to be used for merged doc"
|
|
64
|
+
},
|
|
65
|
+
"short_text": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Short description of the AsyncAPIs"
|
|
68
|
+
},
|
|
69
|
+
"description": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Description to be used for the merged doc"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"event_spec_version": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Mentions the version of AsyncAPI spec"
|
|
78
|
+
},
|
|
79
|
+
"event_source": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Event source information",
|
|
82
|
+
"default": "{region}/{applicationNamespace}/{instanceId}"
|
|
83
|
+
},
|
|
84
|
+
"event_source_params": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"description": "Parameters related to the source of event",
|
|
87
|
+
"default": {
|
|
88
|
+
"region": "the region",
|
|
89
|
+
"applicationNamespace": "the namespace",
|
|
90
|
+
"instanceId": "the instance id"
|
|
91
|
+
},
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"region": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "The regional context of the application"
|
|
97
|
+
},
|
|
98
|
+
"applicationNamespace": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "The registered namespace of the application"
|
|
101
|
+
},
|
|
102
|
+
"instanceId": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "The instance id (tenant, installation, ...) of the application."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"event_characteristics": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"description": "The characteristics defined for events",
|
|
111
|
+
"additionalProperties": true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"importFrom": {
|
|
117
|
+
"const": "asyncapi",
|
|
118
|
+
"description": "AsyncAPI specification"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
33
121
|
}
|
|
34
122
|
}
|
|
@@ -4,6 +4,16 @@
|
|
|
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 10.2.2 - 2026-10-28
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- App index page no longer generates links with a double slash (e.g. `//bookshop`) for app links that already start with `/`
|
|
12
|
+
- Prevent more than one concurrent `AMQP` client reconnect loop
|
|
13
|
+
- Translation of messages in `sap-messages` header
|
|
14
|
+
- Legacy event queue: `TimeoutOverflowWarning` no longer emitted when tasks are scheduled with a retention period exceeding 24.8 days (e.g. `cds.env.agents.retention = '31d'`)
|
|
15
|
+
- CDS config JSON schema: corrected invalid/misplaced `additionalProperties` keywords so extra properties are validated as intended
|
|
16
|
+
|
|
7
17
|
## Version 10.2.1 - 2026-10-20
|
|
8
18
|
|
|
9
19
|
### Fixed
|
|
@@ -38,7 +38,7 @@ const app_info = {
|
|
|
38
38
|
return find (folder, ['*.html', '*/*.html', '*/*/*.html'])
|
|
39
39
|
.map (file => path.relative(folder,file).replace(/\\/g,'/'))
|
|
40
40
|
.concat (cds.app._app_links||[]) // concatenate additional app links
|
|
41
|
-
.map (href => ({ href: '/'+href, label: href.replace('/index.html','') }))
|
|
41
|
+
.map (href => ({ href: href[0]==='/' ? href : '/'+href, label: href.replace('/index.html','') }))
|
|
42
42
|
},
|
|
43
43
|
|
|
44
44
|
get agents() {
|
|
@@ -29,7 +29,7 @@ module.exports = {
|
|
|
29
29
|
|
|
30
30
|
$defs: {
|
|
31
31
|
cdsRoot: {
|
|
32
|
-
|
|
32
|
+
additionalProperties: true,
|
|
33
33
|
patternProperties: {
|
|
34
34
|
'\\[.+\\]': {
|
|
35
35
|
default: {},
|
|
@@ -489,12 +489,12 @@ module.exports = {
|
|
|
489
489
|
create: {
|
|
490
490
|
type: 'object',
|
|
491
491
|
description: 'HDI container provisioning parameters.',
|
|
492
|
+
additionalProperties: true,
|
|
492
493
|
properties: {
|
|
493
494
|
database_id: {
|
|
494
495
|
type: 'string',
|
|
495
496
|
description: 'HANA Cloud instance ID.'
|
|
496
|
-
}
|
|
497
|
-
additionalProperties: true
|
|
497
|
+
}
|
|
498
498
|
}
|
|
499
499
|
},
|
|
500
500
|
bind: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
const { join } = require('path')
|
|
1
|
+
const { join, parse } = require('node:path')
|
|
2
2
|
|
|
3
3
|
module.exports = new class {
|
|
4
4
|
async default4(name) {
|
|
5
|
-
const file = join(__dirname, name.
|
|
5
|
+
const file = join(__dirname, parse(name).name)
|
|
6
6
|
try {
|
|
7
7
|
return structuredClone(require(file))
|
|
8
|
-
} catch {
|
|
9
|
-
throw new Error(`ENOENT: Could not load schema '${name}'
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw new Error(`ENOENT: Could not load schema '${name}'`, { cause: e })
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -203,20 +203,33 @@ exports.write = function write (file, data, o) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
const { setTimeout: sleep } = require('node:timers/promises')
|
|
206
|
-
exports.lock = async function (file, retries = 10, delay = 20) {
|
|
206
|
+
exports.lock = async function (file, retries = 10, delay = 20, stale = 5_000) {
|
|
207
207
|
file = resolve (cds.root, file)
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
const lockfile = file + '.lock'
|
|
209
|
+
const { open, unlink, stat } = fs.promises
|
|
210
|
+
// `retries` bounds waits on a live lock and, independently, reclaim attempts:
|
|
211
|
+
// a stale reclaim or a vanished-lock race retries the acquire at once — so the
|
|
212
|
+
// final reclaim still gets a matching attempt — without spinning unbounded.
|
|
213
|
+
for (let waited = 0, reclaims = 0; ;) try {
|
|
214
|
+
const handle = await open (lockfile, 'wx')
|
|
211
215
|
await handle.close()
|
|
212
|
-
return {release:() => unlink (
|
|
216
|
+
return {release:() => unlink (lockfile) .catch (err => {
|
|
213
217
|
if (err.code !== 'ENOENT') throw err // ignore if already deleted
|
|
214
218
|
})}
|
|
215
219
|
} catch (err) {
|
|
216
|
-
if (err.code
|
|
217
|
-
|
|
220
|
+
if (err.code !== 'EEXIST') throw err
|
|
221
|
+
// Config edits normally complete quickly, so a lock older than `stale`
|
|
222
|
+
// likely belongs to a process that crashed mid-edit. A negative age means
|
|
223
|
+
// the lock vanished between the failed open and the stat — retry at once.
|
|
224
|
+
const age = await stat (lockfile) .then (s => Date.now() - s.mtimeMs, () => -1)
|
|
225
|
+
if (age < 0 || age > stale) { // vanished, or stale → reclaim and retry now
|
|
226
|
+
if (age > stale) await unlink (lockfile) .catch (() => {})
|
|
227
|
+
if (reclaims++ < retries) continue
|
|
228
|
+
} else if (waited++ < retries) { // held by a live process → wait and retry
|
|
229
|
+
await sleep (delay); continue
|
|
230
|
+
}
|
|
231
|
+
throw new Error (`Could not acquire lock on ${file}`, { cause: err })
|
|
218
232
|
}
|
|
219
|
-
throw new Error('Could not acquire lock: connection timed out')
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
|
|
@@ -10,45 +10,24 @@ const connect = (client, LOG, keepAlive) => {
|
|
|
10
10
|
return new Promise((resolve, reject) => {
|
|
11
11
|
_rmHandlers(client)
|
|
12
12
|
|
|
13
|
-
client
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
LOG('Reconnecting')
|
|
32
|
-
try {
|
|
33
|
-
client.connect()
|
|
34
|
-
} catch {
|
|
35
|
-
// we try again...
|
|
36
|
-
}
|
|
37
|
-
setTimeout(_untilConnected, waitingTime(x++)).unref()
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
_untilConnected()
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
resolve(client)
|
|
45
|
-
})
|
|
46
|
-
.once('error', err => {
|
|
47
|
-
_rmHandlers(client)
|
|
48
|
-
const e = new Error('Connection error')
|
|
49
|
-
e.cause = err
|
|
50
|
-
reject(e)
|
|
51
|
-
})
|
|
13
|
+
client.once('connected', function () {
|
|
14
|
+
_rmHandlers(client)
|
|
15
|
+
|
|
16
|
+
// It's important to _always_ keep an error listener,
|
|
17
|
+
// otherwise it will throw and crash the server
|
|
18
|
+
client.on('error', err => LOG(err?.message))
|
|
19
|
+
|
|
20
|
+
if (keepAlive) client.once('disconnected', reconnect(client, LOG))
|
|
21
|
+
|
|
22
|
+
resolve(client)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
client.once('error', err => {
|
|
26
|
+
_rmHandlers(client)
|
|
27
|
+
const e = new Error('Connection error')
|
|
28
|
+
e.cause = err
|
|
29
|
+
reject(e)
|
|
30
|
+
})
|
|
52
31
|
|
|
53
32
|
try {
|
|
54
33
|
client.connect()
|
|
@@ -58,6 +37,33 @@ const connect = (client, LOG, keepAlive) => {
|
|
|
58
37
|
})
|
|
59
38
|
}
|
|
60
39
|
|
|
40
|
+
const reconnect = (client, LOG) => () => {
|
|
41
|
+
let connected = false
|
|
42
|
+
|
|
43
|
+
client.once('connected', () => {
|
|
44
|
+
LOG('Reconnected')
|
|
45
|
+
connected = true
|
|
46
|
+
client.once('disconnected', reconnect(client, LOG))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
let retries = 1
|
|
50
|
+
const _untilConnected = async () => {
|
|
51
|
+
if (!connected) {
|
|
52
|
+
LOG('Reconnecting')
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
client.connect()
|
|
56
|
+
} catch {
|
|
57
|
+
// we try again ...
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setTimeout(_untilConnected, waitingTime(retries++)).unref()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_untilConnected()
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
const disconnect = client => {
|
|
62
68
|
return new Promise((resolve, reject) => {
|
|
63
69
|
_rmHandlers(client)
|
|
@@ -111,7 +111,7 @@ class ODataAdapter extends HttpAdapter {
|
|
|
111
111
|
if (res.headersSent || !cdsReq.messages?.length) return
|
|
112
112
|
const messages = cdsReq.messages.map(err => {
|
|
113
113
|
if (!err.code) err.code = '' // default to empty string if not provided
|
|
114
|
-
const { code, message, numericSeverity, longtextUrl, target, additionalTargets } = this.normalized(err)
|
|
114
|
+
const { code, message, numericSeverity, longtextUrl, target, additionalTargets } = this.localized(this.normalized(err))
|
|
115
115
|
// only return allowed subset of properties
|
|
116
116
|
return { code, message, numericSeverity, longtextUrl, target, additionalTargets }
|
|
117
117
|
})
|
|
@@ -4,6 +4,26 @@ const QUEUED = 'queued'
|
|
|
4
4
|
const PLANNED = 'planned'
|
|
5
5
|
const CALLBACKS = 'callbacks'
|
|
6
6
|
|
|
7
|
+
// Node's setTimeout delay is a 32-bit signed int; delays > MAX_TIMEOUT_MS emit a
|
|
8
|
+
// TimeoutOverflowWarning and fire almost immediately. Chain segments so the callback
|
|
9
|
+
// fires at the correct time. Returns a handle with cancel() that always clears the
|
|
10
|
+
// currently-active segment — not just the first — so re-plans work at any point in the chain.
|
|
11
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1
|
|
12
|
+
const _setLongTimeout = (cb, delay) => {
|
|
13
|
+
const deadline = Date.now() + delay
|
|
14
|
+
const handle = {}
|
|
15
|
+
const schedule = () => {
|
|
16
|
+
// Re-derive the step from the absolute deadline each segment, so lateness in one
|
|
17
|
+
// segment (process suspension, blocked event loop) doesn't compound across the chain.
|
|
18
|
+
const remaining = deadline - Date.now()
|
|
19
|
+
const step = Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS)
|
|
20
|
+
const t = setTimeout(remaining > MAX_TIMEOUT_MS ? schedule : cb, step).unref()
|
|
21
|
+
handle.cancel = () => clearTimeout(t)
|
|
22
|
+
}
|
|
23
|
+
schedule()
|
|
24
|
+
return handle
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
module.exports = class TaskRunner {
|
|
8
28
|
constructor() {
|
|
9
29
|
this.states = new Map()
|
|
@@ -49,11 +69,8 @@ module.exports = class TaskRunner {
|
|
|
49
69
|
const newDate = Date.now() + waitingTime
|
|
50
70
|
const alreadyPlanned = this._getStateProp(PLANNED, { name, tenant })
|
|
51
71
|
if (!alreadyPlanned || alreadyPlanned.date > newDate) {
|
|
52
|
-
if (alreadyPlanned)
|
|
53
|
-
const timer =
|
|
54
|
-
() => (this._setStateProp(PLANNED, undefined, { name, tenant }), cb()),
|
|
55
|
-
waitingTime
|
|
56
|
-
).unref()
|
|
72
|
+
if (alreadyPlanned) alreadyPlanned.timer.cancel()
|
|
73
|
+
const timer = _setLongTimeout(() => (this._setStateProp(PLANNED, undefined, { name, tenant }), cb()), waitingTime)
|
|
57
74
|
this._setStateProp(PLANNED, { date: newDate, timer }, { name, tenant })
|
|
58
75
|
}
|
|
59
76
|
}
|
|
@@ -6,6 +6,14 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## Version 4.2.1 - 2026-10-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Extensibility login now falls back to XSUAA when required IAS login metadata is unavailable, instead of using provider IAS metadata for an unresolved subscriber.
|
|
14
|
+
- `cds.xt.DeploymentService` now reuses extracted HANA deployment resources across tenant subscriptions when extensibility is disabled.
|
|
15
|
+
- Database and CSN compilation errors during HANA tenant upgrades now fail only the affected tenant instead of shutting down the server and stopping the entire upgrade job.
|
|
16
|
+
|
|
9
17
|
## Version 4.2.0 - 2026-10-21
|
|
10
18
|
|
|
11
19
|
### Added
|
|
@@ -443,12 +443,15 @@ module.exports = class ExtensibilityService extends cds.ApplicationService {
|
|
|
443
443
|
cds.on('served', () => {
|
|
444
444
|
cds.app?.post('/-/cds/login/grant', express.urlencoded({ extended: false }), async (req, res) => {
|
|
445
445
|
try {
|
|
446
|
-
const cred = cds.env.requires.auth.credentials || {}
|
|
447
|
-
if (!cred.clientid) throw cds.error('missing authentication service binding', { status: 500 })
|
|
448
|
-
|
|
449
446
|
const query = req.body || {}
|
|
447
|
+
const meta = _isIas() ? await _iasMetadata(query) : undefined
|
|
448
|
+
if (!meta && _isIas() && typeof query.authorization_code === 'string') {
|
|
449
|
+
throw cds.error('IAS login metadata is unavailable', { status: 503 })
|
|
450
|
+
}
|
|
451
|
+
const cred = meta ? cds.env.requires.auth.credentials : _xsuaaCredentials()
|
|
452
|
+
if (!cred.clientid) throw cds.error('missing authentication service binding', { status: 500 })
|
|
450
453
|
|
|
451
|
-
const raw =
|
|
454
|
+
const raw = meta ? await _iasGrant(cred, query, meta) : await _xsuaaGrant(cred, query)
|
|
452
455
|
const data = JSON.parse(raw)
|
|
453
456
|
if (data.error) return res.status(401).json(data)
|
|
454
457
|
res.json(data)
|
|
@@ -542,6 +545,13 @@ function _isIas() {
|
|
|
542
545
|
return cds.env.requires?.auth?.kind === 'ias'
|
|
543
546
|
}
|
|
544
547
|
|
|
548
|
+
function _xsuaaCredentials() {
|
|
549
|
+
const auth = cds.env.requires?.auth
|
|
550
|
+
const cred = _isIas() ? cds.env.requires[auth.xsuaa || 'xsuaa']?.credentials : auth?.credentials
|
|
551
|
+
if (!cred) throw cds.error('missing XSUAA service binding', { status: 500 })
|
|
552
|
+
return cred
|
|
553
|
+
}
|
|
554
|
+
|
|
545
555
|
// XSUAA login: exchange passcode or refresh_token for a token with the
|
|
546
556
|
// `<xsappname>.cds.ExtensionDeveloper` scope.
|
|
547
557
|
async function _xsuaaGrant(cred, query) {
|
|
@@ -556,11 +566,12 @@ async function _xsuaaGrant(cred, query) {
|
|
|
556
566
|
return authToken(cred, { form, subdomain: query.subdomain })
|
|
557
567
|
}
|
|
558
568
|
|
|
559
|
-
//
|
|
560
|
-
//
|
|
569
|
+
// A requested subscriber must have its own IAS tenant; never substitute the provider.
|
|
570
|
+
// Without a subdomain, use the explicit app_tid or provider binding.
|
|
561
571
|
async function _iasLoginTarget(cred, query = {}) {
|
|
562
572
|
const subdomain = typeof query.subdomain === 'string' ? query.subdomain : undefined
|
|
563
573
|
const tenant = subdomain ? await _subscriberTenant(subdomain, cred) : undefined
|
|
574
|
+
if (subdomain && (!tenant?.app_tid || !tenant?.tenantHost)) return {}
|
|
564
575
|
const app_tid =
|
|
565
576
|
(typeof query.app_tid === 'string' && query.app_tid) ||
|
|
566
577
|
tenant?.app_tid ||
|
|
@@ -621,14 +632,13 @@ async function _subscriberTenant(subdomain, cred) {
|
|
|
621
632
|
|
|
622
633
|
// IAS login: exchange the authorization_code (browser PKCE flow) or a refresh_token
|
|
623
634
|
// for a token. Authorization as `cds.ExtensionDeveloper` comes from AMS policies, not scopes.
|
|
624
|
-
async function _iasGrant(cred, query) {
|
|
635
|
+
async function _iasGrant(cred, query, meta) {
|
|
625
636
|
const refresh_token = typeof query.refresh_token === 'string' ? query.refresh_token : undefined
|
|
626
637
|
const code = typeof query.authorization_code === 'string' ? query.authorization_code : undefined
|
|
627
638
|
if (!refresh_token && !code)
|
|
628
639
|
throw cds.error('missing authorization_code or refresh_token', { status: 400 })
|
|
629
640
|
|
|
630
|
-
|
|
631
|
-
const { app_tid, tenantHost } = await _iasLoginTarget(cred, query)
|
|
641
|
+
const { app_tid, token_endpoint } = meta
|
|
632
642
|
|
|
633
643
|
const form = refresh_token
|
|
634
644
|
? { grant_type: 'refresh_token', refresh_token }
|
|
@@ -640,42 +650,49 @@ async function _iasGrant(cred, query) {
|
|
|
640
650
|
}
|
|
641
651
|
if (app_tid) form.app_tid = app_tid
|
|
642
652
|
|
|
643
|
-
|
|
644
|
-
// the fixed provider host (no `subdomain`).
|
|
645
|
-
return authToken(cred, tenantHost
|
|
646
|
-
? { form, endpoint: `https://${tenantHost}${IAS_TOKEN_PATH}` }
|
|
647
|
-
: { form, path: IAS_TOKEN_PATH })
|
|
653
|
+
return authToken(cred, { form, endpoint: token_endpoint })
|
|
648
654
|
}
|
|
649
655
|
|
|
650
|
-
async function
|
|
656
|
+
async function _iasMetadata(query = {}) {
|
|
651
657
|
const cred = cds.env.requires?.auth?.credentials || {}
|
|
652
658
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
DEBUG?.('IAS login metadata:', {
|
|
666
|
-
client_id: meta.client_id,
|
|
667
|
-
requested_subdomain: req.query?.subdomain,
|
|
668
|
-
app_tid: meta.app_tid,
|
|
669
|
-
binding_app_tid: cred.app_tid || cred.appTid || cred.zone_uuid,
|
|
670
|
-
tenant_host: tenantHost,
|
|
671
|
-
authorization_endpoint: meta.authorization_endpoint,
|
|
672
|
-
token_endpoint: meta.token_endpoint,
|
|
673
|
-
binding_url: cred.url,
|
|
674
|
-
binding_certurl: cred.certurl
|
|
675
|
-
})
|
|
676
|
-
return res.send(meta)
|
|
659
|
+
const { app_tid, tenantHost } = await _iasLoginTarget(cred, query)
|
|
660
|
+
if (typeof cred.clientid !== 'string' || !cred.clientid.trim() ||
|
|
661
|
+
typeof app_tid !== 'string' || !app_tid.trim()) return undefined
|
|
662
|
+
const meta = {
|
|
663
|
+
flow: 'authorization_code',
|
|
664
|
+
pkce: true,
|
|
665
|
+
client_id: cred.clientid,
|
|
666
|
+
scope: 'openid',
|
|
667
|
+
authorization_endpoint: tenantHost ? `https://${tenantHost}${IAS_AUTHORIZE_PATH}` : iasAuthorizeUrl(cred),
|
|
668
|
+
token_endpoint: tenantHost ? `https://${tenantHost}${IAS_TOKEN_PATH}` : iasTokenUrl(cred),
|
|
669
|
+
app_tid // sent on both authorize and token
|
|
677
670
|
}
|
|
678
|
-
|
|
671
|
+
for (const endpoint of [meta.authorization_endpoint, meta.token_endpoint]) {
|
|
672
|
+
if (new URL(endpoint).protocol !== 'https:') return undefined
|
|
673
|
+
}
|
|
674
|
+
DEBUG?.('IAS login metadata:', {
|
|
675
|
+
client_id: meta.client_id,
|
|
676
|
+
requested_subdomain: query.subdomain,
|
|
677
|
+
app_tid: meta.app_tid,
|
|
678
|
+
binding_app_tid: cred.app_tid || cred.appTid || cred.zone_uuid,
|
|
679
|
+
tenant_host: tenantHost,
|
|
680
|
+
authorization_endpoint: meta.authorization_endpoint,
|
|
681
|
+
token_endpoint: meta.token_endpoint,
|
|
682
|
+
binding_url: cred.url,
|
|
683
|
+
binding_certurl: cred.certurl
|
|
684
|
+
})
|
|
685
|
+
return meta
|
|
686
|
+
} catch (err) {
|
|
687
|
+
DEBUG?.('Failed to resolve IAS login metadata; falling back to XSUAA:', err.message)
|
|
688
|
+
return undefined
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function authMeta(req, res) {
|
|
693
|
+
try {
|
|
694
|
+
const meta = _isIas() ? await _iasMetadata(req.query || {}) : undefined
|
|
695
|
+
res.send(meta || { passcode_url: _passcodeUrl(_xsuaaCredentials(), req.query?.subdomain) })
|
|
679
696
|
} catch (err) {
|
|
680
697
|
res.status(err.status || err.statusCode || 500).json({ error: err.message })
|
|
681
698
|
}
|
|
@@ -715,4 +732,3 @@ const _decodeError = (activationError) => {
|
|
|
715
732
|
return value
|
|
716
733
|
})
|
|
717
734
|
}
|
|
718
|
-
|