@xpert-ai/plugin-sdk 3.18.6 → 3.19.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 +27 -0
- package/MIDDLEWARE_APPS.md +71 -0
- package/README.md +2 -0
- package/data-workbench.cjs.js +217 -66
- package/data-workbench.esm.js +217 -66
- package/index.cjs.js +162 -46
- package/index.esm.js +148 -47
- package/package.json +2 -2
- package/runtime-capabilities.md +35 -0
- package/src/index.d.ts +3 -0
- package/src/lib/agent/index.d.ts +1 -0
- package/src/lib/agent/middleware/capabilities/assistant-task.d.ts +55 -20
- package/src/lib/agent/middleware/types.d.ts +13 -3
- package/src/lib/agent/runtime/capability.d.ts +29 -0
- package/src/lib/agent/runtime/index.d.ts +3 -0
- package/src/lib/agent/runtime/strategy.d.ts +50 -0
- package/src/lib/agent/runtime/types.d.ts +132 -0
- package/src/lib/assistant-capability/index.d.ts +42 -0
- package/src/lib/connector/runtime-capability.d.ts +2 -0
- package/src/lib/connector/strategy.interface.d.ts +8 -0
- package/src/lib/core/context/request-context.d.ts +6 -0
- package/src/lib/data/datasource/workbench-pagination.d.ts +6 -0
- package/src/lib/data/datasource/workbench-sql-lexer.d.ts +11 -0
- package/src/lib/data/datasource/workbench-sql-syntax.d.ts +3 -0
- package/src/lib/evolution/runtime-capability.d.ts +2 -1
- package/src/lib/integration/strategy.interface.d.ts +22 -0
- package/src/lib/mcp/app.d.ts +10 -0
- package/src/lib/project-task/index.d.ts +59 -0
- package/src/lib/project-type/index.d.ts +64 -0
- package/src/lib/rag/analyzer/index.d.ts +3 -0
- package/src/lib/rag/analyzer/strategy.decorator.d.ts +2 -0
- package/src/lib/rag/analyzer/strategy.interface.d.ts +11 -0
- package/src/lib/rag/analyzer/strategy.registry.d.ts +8 -0
- package/src/lib/rag/index.d.ts +1 -0
- package/src/lib/runtime/capabilities/knowledge-graph.d.ts +59 -0
- package/src/lib/runtime/capabilities/knowledgebase-documents.d.ts +21 -0
- package/src/lib/runtime/capabilities/knowledgebase.d.ts +8 -3
- package/src/lib/runtime/capabilities/project-access.d.ts +2 -1
- package/src/lib/runtime/capabilities/project-provisioning.d.ts +3 -0
- package/src/lib/runtime/runtime-scope.d.ts +7 -1
- package/src/lib/sandbox/execution-environment.d.ts +10 -0
- package/src/lib/sandbox/execution.d.ts +3 -0
- package/src/lib/sandbox/index.d.ts +1 -0
- package/src/lib/sandbox/protocol.d.ts +2 -0
- package/src/lib/tool-provider/types.d.ts +6 -0
- package/src/lib/types.d.ts +2 -1
- package/src/lib/workflow/trigger/strategy.interface.d.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @xpert-ai/plugin-sdk
|
|
2
2
|
|
|
3
|
+
## 3.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 99d09ec: Release a coordinated minor update across all Xpert workspace packages, including the API, Web and Bosi applications, shared SDK and contracts, backend and UI libraries, bundled plugins, sandbox runtimes and documentation.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 8a78318: Add explicit application Project types, provider-backed domain bindings, and typed Project provisioning/access contracts. App plugins using ProjectTypeProvider require these releases (3.19.0 or newer) and the matching host Project type endpoints/schema migration.
|
|
12
|
+
- Updated dependencies [8a78318]
|
|
13
|
+
- Updated dependencies [1e44173]
|
|
14
|
+
- Updated dependencies [c953822]
|
|
15
|
+
- Updated dependencies [b7983c8]
|
|
16
|
+
- Updated dependencies [08f66e7]
|
|
17
|
+
- Updated dependencies [b62543f]
|
|
18
|
+
- Updated dependencies [5f26e05]
|
|
19
|
+
- Updated dependencies [99d09ec]
|
|
20
|
+
- @xpert-ai/contracts@3.19.0
|
|
21
|
+
|
|
22
|
+
## 3.18.7
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- 5213185: Expose keyword analyzer strategy interfaces, discovery metadata, and a scoped registry with exact source lookup. Add optional plugin removal preflight guards so hosts can protect active analyzer dependencies.
|
|
27
|
+
|
|
28
|
+
Correct read-only SQL classification for derived tables, CTE column declarations and type modifiers, and preserve duplicate MySQL output names with direct bounded pagination.
|
|
29
|
+
|
|
3
30
|
## 3.18.6
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# MCP Apps in Agent middleware
|
|
2
|
+
|
|
3
|
+
An `AgentMiddleware` can return `apps` alongside `tools`. It does not need a remote
|
|
4
|
+
MCP server or a published Toolset. The host renders the bundled HTML using its
|
|
5
|
+
existing MCP App sandbox and RPC bridge.
|
|
6
|
+
|
|
7
|
+
ChatKit accesses resources, RPC, approvals and teardown through `/api/ai/mcp-apps`,
|
|
8
|
+
using the AI module's `ApiKeyOrClientSecretAuthGuard`. The ordinary management
|
|
9
|
+
routes keep the platform's standard authorization. Use an SDK version whose
|
|
10
|
+
`client.mcp.apps` targets this AI endpoint.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
return {
|
|
14
|
+
name: 'ProjectSettings',
|
|
15
|
+
tools: [configure, readSettings, saveSettings],
|
|
16
|
+
apps: {
|
|
17
|
+
definitions: [{ key: 'settings', entry: 'dist/mcp-apps/settings.html' }],
|
|
18
|
+
tools: {
|
|
19
|
+
configure: { resourceKey: 'settings', visibility: ['model', 'app'] },
|
|
20
|
+
read_settings: { visibility: ['app'], approval: 'none' },
|
|
21
|
+
save_settings: { visibility: ['app'], approval: 'none' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Keys in `apps.tools` must match actual tool names. App-only tools are excluded
|
|
28
|
+
from the model's tool list. `approval: 'none'` is a trusted plugin declaration for
|
|
29
|
+
operations whose own services enforce scope, revision and validation. Omit it
|
|
30
|
+
to retain host approval. App access never grants access to another middleware.
|
|
31
|
+
For decorated providers, use `@XpertToolProvider({ apps: [...] })` and
|
|
32
|
+
`@XpertTool({ middleware: true, app: { ... } })` with the same binding contract.
|
|
33
|
+
|
|
34
|
+
To pause after a successful tool, configure the Assistant:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
team:
|
|
38
|
+
agentConfig:
|
|
39
|
+
interruptAfter: [configure]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The completed tool result is checkpointed before a separate gate calls
|
|
43
|
+
LangGraph `interrupt`. Resume targets the gate, so tool side effects are not
|
|
44
|
+
replayed. Ordinary tools display a Continue control; an App sends a standard
|
|
45
|
+
`ui/message` after saving. The host supplies the original tool-call and execution
|
|
46
|
+
identity, and ChatKit uses the existing `action: resume` protocol. No `ui/resume`
|
|
47
|
+
extension is required. The App's message is added once at user priority on resume,
|
|
48
|
+
so the Agent can distinguish the confirmed input from the pre-pause tool result.
|
|
49
|
+
Inline images, audio and binary resources are retained in this message (25 MiB
|
|
50
|
+
combined); arbitrary file paths and remote attachment URLs are rejected. ChatKit
|
|
51
|
+
acknowledges continuation only after the run response supplies `Content-Location`,
|
|
52
|
+
not when the local thread is resolved. Cross-origin hosts must expose that header.
|
|
53
|
+
Error ToolMessages skip this gate so the Agent can repair
|
|
54
|
+
the failure. Prefer calling interactive tools alone rather than mixing them with
|
|
55
|
+
unrelated parallel work.
|
|
56
|
+
|
|
57
|
+
The runtime re-resolves middleware bindings and enabled tools for each App RPC.
|
|
58
|
+
UI teardown retains paused native form snapshots for 7 days; it does not cancel
|
|
59
|
+
the Agent. Business settings must still be persisted by domain services before
|
|
60
|
+
continuation. This requires the host capability `platform.middleware.mcp-apps`
|
|
61
|
+
and the corresponding ChatKit continuation support; it adds no ORM dependency
|
|
62
|
+
to the SDK.
|
|
63
|
+
|
|
64
|
+
On hosts with schema synchronization disabled, apply
|
|
65
|
+
`packages/server-ai/src/mcp-app-runtime/migrations/20260928-middleware-app-audit.sql`
|
|
66
|
+
before enabling native middleware Apps. Their audit rows retain provider identity
|
|
67
|
+
in `source` and leave `toolsetId` null. Interactive ChatKit user sessions are
|
|
68
|
+
accepted only for their bound Assistant; existing user JWT access remains valid.
|
|
69
|
+
An App produced by an external Assistant is also accessible when its persisted
|
|
70
|
+
execution parent chain terminates at the session's entry Assistant in the same
|
|
71
|
+
thread. Each execution remains subject to the normal access check.
|
package/README.md
CHANGED
|
@@ -115,6 +115,8 @@ For completed operations, `prepareToolResult(() => project(result), () => minima
|
|
|
115
115
|
|
|
116
116
|
For a host-native MCP App, declare each static HTML bundle once in the Provider's `apps` array, then bind a Tool with `mcp.app.resourceKey` and include `app` visibility. The App key must exist in the same Provider and its entry must be a relative `.html` path inside the packaged plugin. The host publishes the bundle as a `ui://` resource; plugins must not create a stdio server or put CSP/credentials in Tool results.
|
|
117
117
|
|
|
118
|
+
Agent middleware can also expose these Apps without a published Toolset. See [middleware Apps and post-tool continuation](./MIDDLEWARE_APPS.md) for `app` bindings and `interruptAfter` checkpoints.
|
|
119
|
+
|
|
118
120
|
The host owns the public MCP endpoint identity. It combines the plugin's stable `artifactNamespace`, the Provider key, and an opaque scope hash; client names and product-specific identifiers do not belong in the slug. A tenant- or system-level plugin has one tenant-scoped Publication and capability snapshot, while each organization receives an independent access grant and organization-bound credential. An organization-level plugin receives a dedicated organization-scoped Publication.
|
|
119
121
|
|
|
120
122
|
## View extensions
|
package/data-workbench.cjs.js
CHANGED
|
@@ -8,6 +8,159 @@ const DataSourceRuntimeCapability = Object.freeze({
|
|
|
8
8
|
__type: undefined
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
/** Mask comments and quoted content without changing source offsets. Shared by guards and pagination. */ function lexWorkbenchSql(source) {
|
|
12
|
+
if (!source.trim() || source.length > 100000 || source.includes('\0')) throw new Error('invalid_sql');
|
|
13
|
+
const masked = source.split('');
|
|
14
|
+
const quoted = new Map();
|
|
15
|
+
const hide = (start, end, kind)=>{
|
|
16
|
+
for(let i = start; i < end; i++)if (masked[i] !== '\n') masked[i] = ' ';
|
|
17
|
+
if (kind) {
|
|
18
|
+
const marker = start + 1;
|
|
19
|
+
quoted.set(marker, kind);
|
|
20
|
+
masked[marker] = kind === 'literal' ? '?' : 'i';
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
let index = 0;
|
|
24
|
+
while(index < source.length){
|
|
25
|
+
const start = index, c = source[index], next = source[index + 1];
|
|
26
|
+
if (c === '-' && next === '-' || c === '#') {
|
|
27
|
+
while(index < source.length && source[index] !== '\n')index++;
|
|
28
|
+
hide(start, index);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === '/' && next === '*') {
|
|
32
|
+
if (source[index + 2] === '!') throw new Error('executable_comment_not_supported');
|
|
33
|
+
const end = source.indexOf('*/', index + 2);
|
|
34
|
+
if (end < 0) throw new Error('unterminated_comment');
|
|
35
|
+
index = end + 2;
|
|
36
|
+
hide(start, index);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
40
|
+
let closed = false;
|
|
41
|
+
index++;
|
|
42
|
+
while(index < source.length){
|
|
43
|
+
if (source[index] === '\\') throw new Error('use_parameters_for_backslash_literals');
|
|
44
|
+
if (source[index] === c) {
|
|
45
|
+
if (source[index + 1] === c) {
|
|
46
|
+
index += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
index++;
|
|
50
|
+
closed = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
index++;
|
|
54
|
+
}
|
|
55
|
+
if (!closed) throw new Error('unterminated_literal');
|
|
56
|
+
hide(start, index, c === "'" ? 'literal' : 'identifier');
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (c === '$') {
|
|
60
|
+
var _source_slice_match;
|
|
61
|
+
const tag = (_source_slice_match = source.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/)) == null ? void 0 : _source_slice_match[0];
|
|
62
|
+
if (tag) {
|
|
63
|
+
const end = source.indexOf(tag, index + tag.length);
|
|
64
|
+
if (end < 0) throw new Error('unterminated_literal');
|
|
65
|
+
index = end + tag.length;
|
|
66
|
+
hide(start, index, 'literal');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
index++;
|
|
71
|
+
}
|
|
72
|
+
const text = masked.join('');
|
|
73
|
+
const tokens = [
|
|
74
|
+
...text.matchAll(/[A-Z_][A-Z_0-9]*|[0-9]+|[^\s]/gi)
|
|
75
|
+
].map((match)=>{
|
|
76
|
+
var _quoted_get;
|
|
77
|
+
return {
|
|
78
|
+
text: match[0].toUpperCase(),
|
|
79
|
+
start: match.index,
|
|
80
|
+
end: match.index + match[0].length,
|
|
81
|
+
kind: (_quoted_get = quoted.get(match.index)) != null ? _quoted_get : /^[A-Z_]/i.test(match[0]) ? 'word' : /^[0-9]/.test(match[0]) ? 'number' : 'symbol'
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
masked: text,
|
|
86
|
+
tokens
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const MODIFIED_TYPES = new Set('DECIMAL NUMERIC CHAR VARCHAR NCHAR NVARCHAR BINARY VARBINARY DATETIME TIME TIMESTAMP'.split(' '));
|
|
91
|
+
/** Positions where name(...) declares columns or a built-in type, rather than invoking a function. */ function sqlDeclarationPositions(tokens) {
|
|
92
|
+
const declarations = new Set();
|
|
93
|
+
const closing = new Map(), parents = new Map();
|
|
94
|
+
const stack = [];
|
|
95
|
+
tokens.forEach((token, index)=>{
|
|
96
|
+
if (stack.length) parents.set(index, stack[stack.length - 1]);
|
|
97
|
+
if (token.text === '(') stack.push(index);
|
|
98
|
+
if (token.text === ')') {
|
|
99
|
+
const open = stack.pop();
|
|
100
|
+
if (open !== undefined) closing.set(open, index);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const name = (index)=>{
|
|
104
|
+
var _tokens_index;
|
|
105
|
+
return [
|
|
106
|
+
'word',
|
|
107
|
+
'identifier'
|
|
108
|
+
].includes((_tokens_index = tokens[index]) == null ? void 0 : _tokens_index.kind);
|
|
109
|
+
};
|
|
110
|
+
const is = (index, word)=>{
|
|
111
|
+
var _tokens_index;
|
|
112
|
+
return ((_tokens_index = tokens[index]) == null ? void 0 : _tokens_index.text) === word;
|
|
113
|
+
};
|
|
114
|
+
for(let i = 0; i < tokens.length; i++){
|
|
115
|
+
if (tokens[i].kind !== 'word' || !is(i, 'WITH')) continue;
|
|
116
|
+
let cursor = i + 1;
|
|
117
|
+
if (is(cursor, 'RECURSIVE')) cursor++;
|
|
118
|
+
while(name(cursor)){
|
|
119
|
+
const declaration = cursor++;
|
|
120
|
+
let columns = false;
|
|
121
|
+
if (is(cursor, '(')) {
|
|
122
|
+
const end = closing.get(cursor);
|
|
123
|
+
if (end === undefined || end === cursor + 1) break;
|
|
124
|
+
const columnTokens = tokens.slice(cursor + 1, end);
|
|
125
|
+
if (columnTokens.length % 2 === 0 || !columnTokens.every((token, n)=>n % 2 === 0 ? [
|
|
126
|
+
'word',
|
|
127
|
+
'identifier'
|
|
128
|
+
].includes(token.kind) : token.text === ',')) break;
|
|
129
|
+
columns = true;
|
|
130
|
+
cursor = end + 1;
|
|
131
|
+
}
|
|
132
|
+
if (!is(cursor++, 'AS')) break;
|
|
133
|
+
if (is(cursor, 'NOT')) cursor++;
|
|
134
|
+
if (is(cursor, 'MATERIALIZED')) cursor++;
|
|
135
|
+
if (!is(cursor, '(')) break;
|
|
136
|
+
const end = closing.get(cursor);
|
|
137
|
+
if (end === undefined) break;
|
|
138
|
+
if (columns) declarations.add(tokens[declaration].start);
|
|
139
|
+
cursor = end + 1;
|
|
140
|
+
if (!is(cursor, ',')) break;
|
|
141
|
+
cursor++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
tokens.forEach((token, i)=>{
|
|
145
|
+
var _tokens_;
|
|
146
|
+
if (token.kind !== 'word' || !MODIFIED_TYPES.has(token.text) || !is(i + 1, '(')) return;
|
|
147
|
+
const end = closing.get(i + 1);
|
|
148
|
+
if (end === undefined) return;
|
|
149
|
+
const args = tokens.slice(i + 2, end);
|
|
150
|
+
if (![
|
|
151
|
+
1,
|
|
152
|
+
3
|
|
153
|
+
].includes(args.length) || !args.every((arg, n)=>n % 2 ? arg.text === ',' : arg.kind === 'number')) return;
|
|
154
|
+
const parent = parents.get(i);
|
|
155
|
+
const owner = parent === undefined ? undefined : (_tokens_ = tokens[parent - 1]) == null ? void 0 : _tokens_.text;
|
|
156
|
+
const cast = owner === 'CAST' && is(i - 1, 'AS');
|
|
157
|
+
const convert = owner === 'CONVERT' && is(i - 1, ',');
|
|
158
|
+
const postgresCast = is(i - 1, ':') && is(i - 2, ':');
|
|
159
|
+
if (cast || convert || postgresCast) declarations.add(token.start);
|
|
160
|
+
});
|
|
161
|
+
return declarations;
|
|
162
|
+
}
|
|
163
|
+
|
|
11
164
|
const WRITE = new Set([
|
|
12
165
|
'INSERT',
|
|
13
166
|
'UPDATE',
|
|
@@ -42,15 +195,16 @@ const FORBIDDEN = new Set([
|
|
|
42
195
|
'DETACH'
|
|
43
196
|
]);
|
|
44
197
|
const SAFE_FUNCTIONS = new Set('ABS ACOS ASIN ATAN ATAN2 AVG CAST CEIL CEILING COALESCE CONCAT CONCAT_WS CONVERT COS COUNT CURDATE CURRENT_DATE CURRENT_TIMESTAMP DATE DATE_ADD DATE_SUB DATE_FORMAT DATEDIFF DAY DAYOFMONTH DAYOFWEEK EXTRACT FLOOR FORMAT GREATEST GROUP_CONCAT IF IFNULL JSON_EXTRACT JSON_LENGTH JSON_UNQUOTE LEAST LEFT LENGTH LN LOG LOWER LPAD LTRIM MAX MD5 MIN MOD MONTH NOW NULLIF POWER RAND REPLACE REVERSE RIGHT ROUND ROW_NUMBER RANK DENSE_RANK LAG LEAD FIRST_VALUE LAST_VALUE LPAD RPAD RTRIM SIGN SIN SQRT STDDEV STDDEV_POP STDDEV_SAMP SUBSTR SUBSTRING SUM TAN TO_CHAR TO_DATE TO_TIMESTAMP TRIM TRUNC UPPER VAR_POP VAR_SAMP VERSION WEEK YEAR DATABASE CURRENT_DATABASE CURRENT_SCHEMA ARRAY_AGG STRING_AGG JSON_AGG JSON_BUILD_OBJECT PG_GET_VIEWDEF'.split(' '));
|
|
45
|
-
|
|
198
|
+
// FROM/JOIN can introduce a parenthesized subquery, not a function call.
|
|
199
|
+
const SQL_PARENS = new Set('IN AS EXISTS OVER PARTITION VALUES SELECT WITH FROM JOIN AND OR NOT ON USING FILTER GROUPING ROLLUP CUBE DISTINCT EXPLAIN'.split(' '));
|
|
46
200
|
const UNSAFE_FUNCTIONS = /\b(pg_sleep|sleep|benchmark|dblink\w*|lo_\w+|pg_(?:read|write|ls|stat|terminate|cancel|reload|rotate|log|create|drop|promote|switch|backup|replication|advisory)\w*|get_lock|release_lock|load_file|nextval|setval|set_config|http\w*|s3|hdfs|jdbc|mysql|postgresql|file|url)\s*\(/i;
|
|
47
201
|
function splitWorkbenchSql(source) {
|
|
48
|
-
|
|
202
|
+
const { masked, tokens } = lexWorkbenchSql(source);
|
|
49
203
|
const statements = [];
|
|
50
|
-
let start = 0
|
|
204
|
+
let start = 0;
|
|
51
205
|
const finish = (end)=>{
|
|
52
|
-
const text =
|
|
53
|
-
if (text) {
|
|
206
|
+
const text = masked.slice(start, end);
|
|
207
|
+
if (text.trim()) {
|
|
54
208
|
var _text_toUpperCase_match;
|
|
55
209
|
const words = (_text_toUpperCase_match = text.toUpperCase().match(/[A-Z_][A-Z_0-9]*/g)) != null ? _text_toUpperCase_match : [];
|
|
56
210
|
const first = words[0];
|
|
@@ -61,10 +215,11 @@ function splitWorkbenchSql(source) {
|
|
|
61
215
|
if (words.some((word, index)=>WRITE.has(word) && !(first === 'SHOW' && index === 1 && word === 'CREATE') || FORBIDDEN.has(word)) || UNSAFE_FUNCTIONS.test(text) || /\bFOR\s+(UPDATE|SHARE)\b/i.test(text) || /:=|\bEXPLAIN\s+ANALYZE\b|@/i.test(text)) effect = 'unsupported';
|
|
62
216
|
// eslint-disable-next-line no-control-regex -- intentional: reject any non-ASCII/control input
|
|
63
217
|
if (/[^\x00-\x7f]/.test(text) || /\.\s*[A-Z_][A-Z_0-9]*\s*\(/i.test(text)) effect = 'unsupported';
|
|
218
|
+
const declarations = sqlDeclarationPositions(tokens.filter((token)=>token.start >= start && token.end <= end));
|
|
64
219
|
if ([
|
|
65
220
|
...text.matchAll(/([A-Z_][A-Z_0-9]*)\s*\(/gi)
|
|
66
|
-
].some((match)=>!SAFE_FUNCTIONS.has(match[1].toUpperCase()) && !SQL_PARENS.has(match[1].toUpperCase()))) effect = 'unsupported';
|
|
67
|
-
if (first === 'SHOW' && !/^(SHOW\s+(DATABASES|SCHEMAS|CATALOGS|TABLES|FULL\s+TABLES|COLUMNS|FULL\s+COLUMNS|INDEX|INDEXES|KEYS|CREATE\s+TABLE|CREATE\s+VIEW|PARTITIONS|VARIABLES|STATUS|GRANTS))\b/i.test(text)) effect = 'unsupported';
|
|
221
|
+
].some((match)=>!declarations.has(start + match.index) && !SAFE_FUNCTIONS.has(match[1].toUpperCase()) && !SQL_PARENS.has(match[1].toUpperCase()))) effect = 'unsupported';
|
|
222
|
+
if (first === 'SHOW' && !/^(SHOW\s+(DATABASES|SCHEMAS|CATALOGS|TABLES|FULL\s+TABLES|COLUMNS|FULL\s+COLUMNS|INDEX|INDEXES|KEYS|CREATE\s+TABLE|CREATE\s+VIEW|PARTITIONS|VARIABLES|STATUS|GRANTS))\b/i.test(text.trim())) effect = 'unsupported';
|
|
68
223
|
}
|
|
69
224
|
// CREATE/ALTER routines, external resources and privileged SQL are outside this app.
|
|
70
225
|
if (effect === 'write' && /\b(FUNCTION|PROCEDURE|TRIGGER|USER|ROLE|CATALOG|RESOURCE|SYSTEM|SERVER|EXTENSION|FILE|OUTFILE|DUMPFILE|EXECUTE)\b/i.test(text)) effect = 'unsupported';
|
|
@@ -74,63 +229,10 @@ function splitWorkbenchSql(source) {
|
|
|
74
229
|
effect
|
|
75
230
|
});
|
|
76
231
|
}
|
|
77
|
-
clean = '';
|
|
78
232
|
start = end + 1;
|
|
79
233
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (c === '-' && next === '-' || c === '#') {
|
|
83
|
-
while(index < source.length && source[index] !== '\n')index++;
|
|
84
|
-
clean += ' ';
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (c === '/' && next === '*') {
|
|
88
|
-
if (source[index + 2] === '!') throw new Error('executable_comment_not_supported');
|
|
89
|
-
const end = source.indexOf('*/', index + 2);
|
|
90
|
-
if (end < 0) throw new Error('unterminated_comment');
|
|
91
|
-
index = end + 2;
|
|
92
|
-
clean += ' ';
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (c === "'" || c === '"' || c === '`') {
|
|
96
|
-
const quote = c;
|
|
97
|
-
let closed = false;
|
|
98
|
-
index++;
|
|
99
|
-
while(index < source.length){
|
|
100
|
-
if (source[index] === '\\') throw new Error('use_parameters_for_backslash_literals');
|
|
101
|
-
if (source[index] === quote) {
|
|
102
|
-
if (source[index + 1] === quote) {
|
|
103
|
-
index += 2;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
index++;
|
|
107
|
-
closed = true;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
index++;
|
|
111
|
-
}
|
|
112
|
-
if (!closed) throw new Error('unterminated_literal');
|
|
113
|
-
clean += quote === "'" ? ' ? ' : ' identifier ';
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (c === '$') {
|
|
117
|
-
var _source_slice_match;
|
|
118
|
-
const tag = (_source_slice_match = source.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/)) == null ? void 0 : _source_slice_match[0];
|
|
119
|
-
if (tag) {
|
|
120
|
-
const end = source.indexOf(tag, index + tag.length);
|
|
121
|
-
if (end < 0) throw new Error('unterminated_literal');
|
|
122
|
-
index = end + tag.length;
|
|
123
|
-
clean += ' ? ';
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (c === ';') {
|
|
128
|
-
finish(index);
|
|
129
|
-
index++;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
clean += c;
|
|
133
|
-
index++;
|
|
234
|
+
for (const token of tokens){
|
|
235
|
+
if (token.kind === 'symbol' && token.text === ';') finish(token.start);
|
|
134
236
|
}
|
|
135
237
|
finish(source.length);
|
|
136
238
|
if (!statements.length || statements.length > 50) throw new Error('statement_count_exceeded');
|
|
@@ -148,6 +250,53 @@ function quoteDatabaseIdentifier(name, engine) {
|
|
|
148
250
|
return quote + name.split(quote).join(quote + quote) + quote;
|
|
149
251
|
}
|
|
150
252
|
|
|
253
|
+
/** Apply a page to a MySQL result without turning its (possibly duplicate) output names into table columns. */ function paginateMysqlQuery(sql, parameters = [], limit, offset) {
|
|
254
|
+
const { tokens } = lexWorkbenchSql(sql);
|
|
255
|
+
const top = [];
|
|
256
|
+
let depth = 0;
|
|
257
|
+
for (const token of tokens){
|
|
258
|
+
if (token.text === '(') depth++;
|
|
259
|
+
if (!depth) top.push(token);
|
|
260
|
+
if (token.text === ')') depth--;
|
|
261
|
+
if (depth < 0) throw new Error('unsupported_pagination');
|
|
262
|
+
}
|
|
263
|
+
if (depth) throw new Error('unsupported_pagination');
|
|
264
|
+
const start = top.findIndex((token)=>token.kind === 'word' && token.text === 'LIMIT');
|
|
265
|
+
if (start < 0) return {
|
|
266
|
+
sql: `${sql}\nLIMIT ${limit + 1} OFFSET ${offset}`,
|
|
267
|
+
parameters
|
|
268
|
+
};
|
|
269
|
+
const clause = top.slice(start + 1);
|
|
270
|
+
const bindings = tokens.filter((token)=>token.text === '?' && token.kind === 'symbol');
|
|
271
|
+
const consumed = new Set();
|
|
272
|
+
const integer = (token)=>{
|
|
273
|
+
if ((token == null ? void 0 : token.kind) === 'number') return BigInt(token.text);
|
|
274
|
+
if ((token == null ? void 0 : token.kind) === 'symbol' && token.text === '?') {
|
|
275
|
+
const index = bindings.indexOf(token), value = parameters[index];
|
|
276
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
|
|
277
|
+
consumed.add(index);
|
|
278
|
+
return BigInt(value);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw new Error('unsupported_pagination');
|
|
282
|
+
};
|
|
283
|
+
let count, skip = 0n;
|
|
284
|
+
if (clause.length === 1) count = integer(clause[0]);
|
|
285
|
+
else if (clause.length === 3 && clause[1].text === ',') {
|
|
286
|
+
skip = integer(clause[0]);
|
|
287
|
+
count = integer(clause[2]);
|
|
288
|
+
} else if (clause.length === 3 && clause[1].kind === 'word' && clause[1].text === 'OFFSET') {
|
|
289
|
+
count = integer(clause[0]);
|
|
290
|
+
skip = integer(clause[2]);
|
|
291
|
+
} else throw new Error('unsupported_pagination');
|
|
292
|
+
const remaining = count > BigInt(offset) ? count - BigInt(offset) : 0n;
|
|
293
|
+
const size = remaining < BigInt(limit + 1) ? remaining : BigInt(limit + 1);
|
|
294
|
+
return {
|
|
295
|
+
sql: `${sql.slice(0, top[start].start)}LIMIT ${size} OFFSET ${skip + BigInt(offset)}${sql.slice(clause[clause.length - 1].end)}`,
|
|
296
|
+
parameters: parameters.filter((_, index)=>!consumed.has(index))
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
151
300
|
const number = (value)=>Number(value != null ? value : 0);
|
|
152
301
|
const string = (value)=>value == null ? '' : String(value);
|
|
153
302
|
const cell = (result, row, name)=>row[result.columns.findIndex((column)=>column.name.toLowerCase() === name.toLowerCase())];
|
|
@@ -389,12 +538,14 @@ const cell = (result, row, name)=>row[result.columns.findIndex((column)=>column.
|
|
|
389
538
|
if (input.mode === 'write' && !(await this.capabilities()).writes) throw new Error('engine_version_writes_unavailable');
|
|
390
539
|
if (input.mode === 'write' && offset) throw new Error('mutation_cannot_be_paginated');
|
|
391
540
|
let sql = statements[0].sql;
|
|
392
|
-
|
|
541
|
+
let parameters = input.parameters;
|
|
393
542
|
if (input.mode === 'read' && [
|
|
394
543
|
'SELECT',
|
|
395
544
|
'WITH'
|
|
396
|
-
].includes(statements[0].words[0]))
|
|
397
|
-
|
|
545
|
+
].includes(statements[0].words[0])) {
|
|
546
|
+
if (this.engine === 'mysql') ({ sql, parameters } = paginateMysqlQuery(sql, parameters, limit, offset));
|
|
547
|
+
else sql = `SELECT * FROM (\n${sql}\n) AS ${this.quote('_db_studio_page')} LIMIT ${limit + 1} OFFSET ${offset}`;
|
|
548
|
+
} else if (offset) throw new Error('statement_cannot_be_paginated');
|
|
398
549
|
this.busy = true;
|
|
399
550
|
let readTransaction = false;
|
|
400
551
|
try {
|
|
@@ -403,7 +554,7 @@ const cell = (result, row, name)=>row[result.columns.findIndex((column)=>column.
|
|
|
403
554
|
readTransaction = true;
|
|
404
555
|
}
|
|
405
556
|
var _input_timeoutMs;
|
|
406
|
-
const result = await this.raw(sql,
|
|
557
|
+
const result = await this.raw(sql, parameters, limit + 1, signal, Math.max(1, Math.min((_input_timeoutMs = input.timeoutMs) != null ? _input_timeoutMs : 30000, 120000)));
|
|
407
558
|
const hasMore = result.rows.length > limit || result.truncated;
|
|
408
559
|
return _extends._extends({}, result, {
|
|
409
560
|
rows: result.rows.slice(0, limit),
|