@relatalabs/n8n-nodes-relatasql 0.1.7
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 +72 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/credentials/RelataSqlApi.credentials.d.ts +10 -0
- package/dist/credentials/RelataSqlApi.credentials.js +48 -0
- package/dist/credentials/relatasql.png +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/nodes/RelataSql/GenericFunctions.d.ts +49 -0
- package/dist/nodes/RelataSql/GenericFunctions.js +90 -0
- package/dist/nodes/RelataSql/RelataSql.node.d.ts +12 -0
- package/dist/nodes/RelataSql/RelataSql.node.js +679 -0
- package/dist/nodes/RelataSql/relatasql.png +0 -0
- package/dist/nodes/RelataSql/relatasql.svg +12 -0
- package/package.json +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `n8n-nodes-relatasql`.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
Dates are npm publish dates.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- The community node is now published under the canonical RelataLabs npm scope as `@relatalabs/n8n-nodes-relatasql`.
|
|
13
|
+
- Wording and formatting of the node description in the package metadata.
|
|
14
|
+
|
|
15
|
+
## [0.1.7] — 2026-07-07
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Create Dump**: backend errors now reach the node with their real reason (the underlying
|
|
19
|
+
`pg_dump` error) and a support reference. The node requested the response as binary and
|
|
20
|
+
discarded the JSON error body, so every failure surfaced as the same generic message.
|
|
21
|
+
|
|
22
|
+
## [0.1.6] — 2026-07-07
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Create Dump**: 2-hour HTTP timeout for large databases, and an explicit message when a
|
|
26
|
+
dump exceeds the limit instead of a silent cut-off.
|
|
27
|
+
|
|
28
|
+
## [0.1.5] — 2026-07-03
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- The credential now defaults to the official cloud API (`https://api.relatasql.com`).
|
|
32
|
+
Self-hosting became the case you override explicitly instead of the default.
|
|
33
|
+
|
|
34
|
+
## [0.1.4] — 2026-06-18
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **Create Dump**: failure messages rewritten for the person reading them in n8n, rather than
|
|
38
|
+
passing through internal wording.
|
|
39
|
+
|
|
40
|
+
## [0.1.3] — 2026-06-18
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Create Dump**: clearer error messages when a dump cannot be produced.
|
|
44
|
+
|
|
45
|
+
## [0.1.2] — 2026-06-15
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **Database → Create Dump** operation: produces a gzip-compressed `.sql.gz` as binary data,
|
|
49
|
+
ready to hand to Google Drive, Dropbox, S3 or any other node in the workflow.
|
|
50
|
+
|
|
51
|
+
## [0.1.1] — 2026-06-15
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- RelataSQL brand icons for the node and its credential.
|
|
55
|
+
|
|
56
|
+
## [0.1.0] — 2026-06-15
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- First public release of the community node: run SQL and inspect schemas across the database
|
|
60
|
+
connections of your RelataSQL workspace, authenticated with your API key.
|
|
61
|
+
- This is a database-infrastructure integration only: it deliberately ships **no AI/LLM
|
|
62
|
+
features**.
|
|
63
|
+
|
|
64
|
+
[Unreleased]: https://github.com/RelataSQL/n8n-nodes-relatasql/compare/v0.1.7...HEAD
|
|
65
|
+
[0.1.7]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.7
|
|
66
|
+
[0.1.6]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.6
|
|
67
|
+
[0.1.5]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.5
|
|
68
|
+
[0.1.4]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.4
|
|
69
|
+
[0.1.3]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.3
|
|
70
|
+
[0.1.2]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.2
|
|
71
|
+
[0.1.1]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.1
|
|
72
|
+
[0.1.0]: https://www.npmjs.com/package/n8n-nodes-relatasql/v/0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RelataSQL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# n8n-nodes-relatasql
|
|
2
|
+
|
|
3
|
+
Official [n8n](https://n8n.io) community node for **[RelataSQL](https://relatasql.com)** — run SQL, inspect
|
|
4
|
+
schemas, and govern writes across your RelataSQL database connections, straight from your automations.
|
|
5
|
+
|
|
6
|
+
This node is a pure **database-infrastructure** integration. It deliberately does **not** include any AI/LLM
|
|
7
|
+
operation: use n8n's native AI nodes (with your own key) to generate SQL or logic, then hand it to this node to
|
|
8
|
+
execute safely against your databases. That keeps token costs on your side and avoids runaway-loop billing.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
In n8n: **Settings → Community Nodes → Install** and enter `@relatalabs/n8n-nodes-relatasql`.
|
|
13
|
+
|
|
14
|
+
Self-hosted/manual: build this package (`npm install && npm run build`) and place/link it under your n8n custom
|
|
15
|
+
extensions folder (`~/.n8n/custom`) or set `N8N_CUSTOM_EXTENSIONS` to its path.
|
|
16
|
+
|
|
17
|
+
## Credentials
|
|
18
|
+
|
|
19
|
+
Create a **RelataSQL API** credential:
|
|
20
|
+
|
|
21
|
+
- **Base URL** — RelataSQL API base URL. Defaults to the official cloud API `https://api.relatasql.com` (no trailing slash).
|
|
22
|
+
- **API Key** — generated in RelataSQL → **Settings → API Keys** (starts with `relata_live_`). Sent as
|
|
23
|
+
`Authorization: Bearer <key>`.
|
|
24
|
+
|
|
25
|
+
The credential **Test** lists your connections to verify the key.
|
|
26
|
+
|
|
27
|
+
## Operations
|
|
28
|
+
|
|
29
|
+
**Connection**
|
|
30
|
+
- *List* — connections reachable with this API key (also powers the connection dropdown).
|
|
31
|
+
|
|
32
|
+
**Database** (Postgres + MySQL; queries run read-only)
|
|
33
|
+
- *Execute Query* — run a read-only SQL query and return one item per row.
|
|
34
|
+
- *Sample Rows* — first N rows of a table.
|
|
35
|
+
- *Run in Sandbox* — execute SQL inside a transaction that is always rolled back (safe test).
|
|
36
|
+
- *Create Dump* — run pg_dump/mysqldump and return a compressed `.sql.gz` as **binary**,
|
|
37
|
+
ready to hand to a Google Drive / Dropbox / S3 / FTP upload node. Scope = structure+data /
|
|
38
|
+
structure-only / data-only. Requires a paid plan.
|
|
39
|
+
|
|
40
|
+
**Schema**
|
|
41
|
+
- *Get Tables* / *Get Columns* / *Get Relations* — introspect a connection.
|
|
42
|
+
|
|
43
|
+
**Write (with approval)** — the governed mutation path
|
|
44
|
+
- *Request Approval* → returns an approval ID.
|
|
45
|
+
- *Check Approval* → poll its status (a human approves it in RelataSQL → Settings → MCP → Approvals).
|
|
46
|
+
- *Execute Approved* → run the approved statement.
|
|
47
|
+
|
|
48
|
+
**Backup**
|
|
49
|
+
- *List Schedules* — backup schedules reachable with this API key (also powers the schedule dropdown).
|
|
50
|
+
- *Trigger Run* — run a schedule now (fire-and-forget; poll *List Logs* for the result).
|
|
51
|
+
- *List Logs* — recent backup runs (status, size, duration, error).
|
|
52
|
+
|
|
53
|
+
## Important constraints
|
|
54
|
+
|
|
55
|
+
- **Enable the connection first.** Programmatic access is gated: in RelataSQL go to **Settings → MCP** and enable
|
|
56
|
+
access for each connection (choose an *indefinite* grant for unattended n8n flows). Otherwise calls fail with a
|
|
57
|
+
clear "connection not enabled for API access" error.
|
|
58
|
+
- **Read vs write.** *Execute Query* is read-only by design. Mutations go through the **Write (with approval)**
|
|
59
|
+
flow, which requires a human approval in the RelataSQL UI.
|
|
60
|
+
- **PostgreSQL only** for query/schema in this version. MySQL/MSSQL execution is coming on the backend.
|
|
61
|
+
|
|
62
|
+
## Example: back up a database to Google Drive
|
|
63
|
+
|
|
64
|
+
`RelataSQL: Create Dump` (pick the connection) → `Google Drive: Upload` (binary field `data`). Add a
|
|
65
|
+
Schedule Trigger in front for a nightly backup to your own Drive — no intermediate storage needed.
|
|
66
|
+
|
|
67
|
+
## Example: move data between databases
|
|
68
|
+
|
|
69
|
+
`Execute Query` on connection A (read) → transform in n8n → `Request Write` on connection B → approve in
|
|
70
|
+
RelataSQL → `Execute Approved`.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, Icon, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
export declare class RelataSqlApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
icon: Icon;
|
|
6
|
+
documentationUrl: string;
|
|
7
|
+
properties: INodeProperties[];
|
|
8
|
+
authenticate: IAuthenticateGeneric;
|
|
9
|
+
test: ICredentialTestRequest;
|
|
10
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RelataSqlApi = void 0;
|
|
4
|
+
class RelataSqlApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'relataSqlApi';
|
|
7
|
+
this.displayName = 'RelataSQL API';
|
|
8
|
+
this.icon = 'file:relatasql.png';
|
|
9
|
+
this.documentationUrl = 'https://github.com/RelataSQL/n8n-nodes-relatasql';
|
|
10
|
+
this.properties = [
|
|
11
|
+
{
|
|
12
|
+
displayName: 'Base URL',
|
|
13
|
+
name: 'baseUrl',
|
|
14
|
+
type: 'string',
|
|
15
|
+
default: 'https://api.relatasql.com',
|
|
16
|
+
required: true,
|
|
17
|
+
placeholder: 'https://api.relatasql.com',
|
|
18
|
+
description: 'RelataSQL API base URL (no trailing slash). Keep the default cloud API URL.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
displayName: 'API Key',
|
|
22
|
+
name: 'apiKey',
|
|
23
|
+
type: 'string',
|
|
24
|
+
typeOptions: { password: true },
|
|
25
|
+
default: '',
|
|
26
|
+
required: true,
|
|
27
|
+
description: 'A RelataSQL API key from Settings → API Keys (starts with "relata_live_").',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
// Injected on every request the node makes.
|
|
31
|
+
this.authenticate = {
|
|
32
|
+
type: 'generic',
|
|
33
|
+
properties: {
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: '=Bearer {{$credentials.apiKey}}',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
// Validates the key by listing connections (cheap, API-key-gated).
|
|
40
|
+
this.test = {
|
|
41
|
+
request: {
|
|
42
|
+
baseURL: '={{$credentials.baseUrl}}',
|
|
43
|
+
url: '/mcp/connections',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.RelataSqlApi = RelataSqlApi;
|
|
Binary file
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IDataObject, IExecuteFunctions, IHttpRequestMethods, ILoadOptionsFunctions } from 'n8n-workflow';
|
|
2
|
+
export interface RelataConnection {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
engine: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
databaseName: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
workspaceName?: string;
|
|
11
|
+
mcpAccessStatus: 'ACTIVE' | 'INACTIVE' | 'EXPIRED' | 'INDEFINITE';
|
|
12
|
+
mcpGrantedUntil: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface RelataColumn {
|
|
15
|
+
name: string;
|
|
16
|
+
dataType: string;
|
|
17
|
+
isNullable: boolean;
|
|
18
|
+
isPrimaryKey?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface RelataTable {
|
|
21
|
+
schema: string;
|
|
22
|
+
name: string;
|
|
23
|
+
columns: RelataColumn[];
|
|
24
|
+
}
|
|
25
|
+
export interface RelataSchema {
|
|
26
|
+
connectionId: string;
|
|
27
|
+
tables: RelataTable[];
|
|
28
|
+
}
|
|
29
|
+
export interface RelataQueryResult {
|
|
30
|
+
columns: string[];
|
|
31
|
+
rows: unknown[][];
|
|
32
|
+
rowCount: number;
|
|
33
|
+
truncated?: boolean;
|
|
34
|
+
}
|
|
35
|
+
type RequestContext = IExecuteFunctions | ILoadOptionsFunctions;
|
|
36
|
+
/**
|
|
37
|
+
* Single entry point for every call to the RelataSQL backend. Resolves the
|
|
38
|
+
* credential's base URL, lets n8n inject the Bearer auth header, and maps
|
|
39
|
+
* backend errors (notably the JIT access gate) to actionable messages.
|
|
40
|
+
*/
|
|
41
|
+
export declare function relataApiRequest(this: RequestContext, method: IHttpRequestMethods, path: string, body?: IDataObject): Promise<any>;
|
|
42
|
+
/** Turns the backend's columnar result into row objects n8n can map. */
|
|
43
|
+
export declare function rowsToObjects(columns: string[], rows: unknown[][]): IDataObject[];
|
|
44
|
+
/** Splits a "schema.table" dropdown value into its parts. */
|
|
45
|
+
export declare function splitQualifiedTable(value: string): {
|
|
46
|
+
schema: string;
|
|
47
|
+
table: string;
|
|
48
|
+
};
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.relataApiRequest = relataApiRequest;
|
|
4
|
+
exports.rowsToObjects = rowsToObjects;
|
|
5
|
+
exports.splitQualifiedTable = splitQualifiedTable;
|
|
6
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
7
|
+
/**
|
|
8
|
+
* Single entry point for every call to the RelataSQL backend. Resolves the
|
|
9
|
+
* credential's base URL, lets n8n inject the Bearer auth header, and maps
|
|
10
|
+
* backend errors (notably the JIT access gate) to actionable messages.
|
|
11
|
+
*/
|
|
12
|
+
async function relataApiRequest(method, path, body) {
|
|
13
|
+
var _a;
|
|
14
|
+
const credentials = await this.getCredentials('relataSqlApi');
|
|
15
|
+
const baseUrl = String((_a = credentials.baseUrl) !== null && _a !== void 0 ? _a : '').replace(/\/+$/, '');
|
|
16
|
+
if (!baseUrl) {
|
|
17
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'RelataSQL "Base URL" is not set in the credential.');
|
|
18
|
+
}
|
|
19
|
+
const options = {
|
|
20
|
+
method,
|
|
21
|
+
url: `${baseUrl}${path}`,
|
|
22
|
+
headers: { Accept: 'application/json' },
|
|
23
|
+
json: true,
|
|
24
|
+
};
|
|
25
|
+
if (body !== undefined) {
|
|
26
|
+
options.body = body;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return await this.helpers.httpRequestWithAuthentication.call(this, 'relataSqlApi', options);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw mapRelataError(this, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function extractMessage(error) {
|
|
36
|
+
var _a;
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (typeof (error === null || error === void 0 ? void 0 : error.message) === 'string')
|
|
39
|
+
parts.push(error.message);
|
|
40
|
+
const resp = error === null || error === void 0 ? void 0 : error.response;
|
|
41
|
+
if (resp) {
|
|
42
|
+
const b = (_a = resp.body) !== null && _a !== void 0 ? _a : resp.data;
|
|
43
|
+
if (typeof b === 'string')
|
|
44
|
+
parts.push(b);
|
|
45
|
+
else if (b && typeof b === 'object') {
|
|
46
|
+
if (typeof b.message === 'string')
|
|
47
|
+
parts.push(b.message);
|
|
48
|
+
else
|
|
49
|
+
parts.push(JSON.stringify(b));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (typeof (error === null || error === void 0 ? void 0 : error.description) === 'string')
|
|
53
|
+
parts.push(error.description);
|
|
54
|
+
return parts.join(' | ');
|
|
55
|
+
}
|
|
56
|
+
function mapRelataError(ctx, error) {
|
|
57
|
+
const node = ctx.getNode();
|
|
58
|
+
const text = extractMessage(error);
|
|
59
|
+
if (text.includes('JIT_ACCESS_REQUIRED')) {
|
|
60
|
+
return new n8n_workflow_1.NodeOperationError(node, 'This RelataSQL connection is not enabled for programmatic (API key) access.', {
|
|
61
|
+
description: 'Open RelataSQL → Settings → MCP and enable access for this connection (pick an indefinite grant for unattended n8n flows), then retry.',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (text.includes('only supports Postgres') || text.includes('NotImplemented')) {
|
|
65
|
+
return new n8n_workflow_1.NodeOperationError(node, 'RelataSQL currently supports only PostgreSQL connections for this operation.', {
|
|
66
|
+
description: 'MySQL/MSSQL execution is not yet available on the RelataSQL backend.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (error instanceof n8n_workflow_1.NodeOperationError || error instanceof n8n_workflow_1.NodeApiError) {
|
|
70
|
+
return error;
|
|
71
|
+
}
|
|
72
|
+
return new n8n_workflow_1.NodeApiError(node, error);
|
|
73
|
+
}
|
|
74
|
+
/** Turns the backend's columnar result into row objects n8n can map. */
|
|
75
|
+
function rowsToObjects(columns, rows) {
|
|
76
|
+
return rows.map((row) => {
|
|
77
|
+
const obj = {};
|
|
78
|
+
columns.forEach((col, i) => {
|
|
79
|
+
obj[col] = row[i];
|
|
80
|
+
});
|
|
81
|
+
return obj;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Splits a "schema.table" dropdown value into its parts. */
|
|
85
|
+
function splitQualifiedTable(value) {
|
|
86
|
+
const idx = value.indexOf('.');
|
|
87
|
+
if (idx < 0)
|
|
88
|
+
return { schema: 'public', table: value };
|
|
89
|
+
return { schema: value.slice(0, idx), table: value.slice(idx + 1) };
|
|
90
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class RelataSql implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getConnections(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
|
+
getSchedules(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RelataSql = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const GenericFunctions_1 = require("./GenericFunctions");
|
|
6
|
+
class RelataSql {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: 'RelataSQL',
|
|
10
|
+
name: 'relataSql',
|
|
11
|
+
icon: 'file:relatasql.png',
|
|
12
|
+
group: ['transform'],
|
|
13
|
+
version: 1,
|
|
14
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
15
|
+
description: 'Run SQL, inspect schemas, and govern writes across your RelataSQL database connections',
|
|
16
|
+
defaults: { name: 'RelataSQL' },
|
|
17
|
+
usableAsTool: true,
|
|
18
|
+
inputs: ['main'],
|
|
19
|
+
outputs: ['main'],
|
|
20
|
+
credentials: [{ name: 'relataSqlApi', required: true }],
|
|
21
|
+
properties: [
|
|
22
|
+
// ── Resource ────────────────────────────────────────────────
|
|
23
|
+
{
|
|
24
|
+
displayName: 'Resource',
|
|
25
|
+
name: 'resource',
|
|
26
|
+
type: 'options',
|
|
27
|
+
noDataExpression: true,
|
|
28
|
+
options: [
|
|
29
|
+
{ name: 'Backup', value: 'backups' },
|
|
30
|
+
{ name: 'Connection', value: 'connection' },
|
|
31
|
+
{ name: 'Database', value: 'database' },
|
|
32
|
+
{ name: 'Schema', value: 'schema' },
|
|
33
|
+
{ name: 'Write (With Approval)', value: 'write' },
|
|
34
|
+
],
|
|
35
|
+
default: 'database',
|
|
36
|
+
},
|
|
37
|
+
// ── Operations: Connection ──────────────────────────────────
|
|
38
|
+
{
|
|
39
|
+
displayName: 'Operation',
|
|
40
|
+
name: 'operation',
|
|
41
|
+
type: 'options',
|
|
42
|
+
noDataExpression: true,
|
|
43
|
+
displayOptions: { show: { resource: ['connection'] } },
|
|
44
|
+
options: [
|
|
45
|
+
{
|
|
46
|
+
name: 'List',
|
|
47
|
+
value: 'list',
|
|
48
|
+
action: 'List database connections',
|
|
49
|
+
description: 'List the database connections reachable with this API key',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
default: 'list',
|
|
53
|
+
},
|
|
54
|
+
// ── Operations: Database ────────────────────────────────────
|
|
55
|
+
{
|
|
56
|
+
displayName: 'Operation',
|
|
57
|
+
name: 'operation',
|
|
58
|
+
type: 'options',
|
|
59
|
+
noDataExpression: true,
|
|
60
|
+
displayOptions: { show: { resource: ['database'] } },
|
|
61
|
+
options: [
|
|
62
|
+
{
|
|
63
|
+
name: 'Create Dump',
|
|
64
|
+
value: 'createDump',
|
|
65
|
+
action: 'Create a compressed SQL dump',
|
|
66
|
+
description: 'Run pg_dump/mysqldump and return a .sql.gz binary file (e.g. to upload to Google Drive)',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Execute Query',
|
|
70
|
+
value: 'executeQuery',
|
|
71
|
+
action: 'Execute a read-only SQL query',
|
|
72
|
+
description: 'Run a read-only SQL query (BEGIN READ ONLY) and return rows',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'Sample Rows',
|
|
76
|
+
value: 'sampleRows',
|
|
77
|
+
action: 'Sample rows from a table',
|
|
78
|
+
description: 'Return the first N rows of a table',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Run in Sandbox',
|
|
82
|
+
value: 'sandbox',
|
|
83
|
+
action: 'Run SQL in a rolled-back sandbox',
|
|
84
|
+
description: 'Execute SQL inside a transaction that is always rolled back (safe test)',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
default: 'executeQuery',
|
|
88
|
+
},
|
|
89
|
+
// ── Operations: Schema ──────────────────────────────────────
|
|
90
|
+
{
|
|
91
|
+
displayName: 'Operation',
|
|
92
|
+
name: 'operation',
|
|
93
|
+
type: 'options',
|
|
94
|
+
noDataExpression: true,
|
|
95
|
+
displayOptions: { show: { resource: ['schema'] } },
|
|
96
|
+
options: [
|
|
97
|
+
{
|
|
98
|
+
name: 'Get Tables',
|
|
99
|
+
value: 'getTables',
|
|
100
|
+
action: 'Get tables',
|
|
101
|
+
description: 'List tables (and column counts) for a connection',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'Get Columns',
|
|
105
|
+
value: 'getColumns',
|
|
106
|
+
action: 'Get columns of a table',
|
|
107
|
+
description: 'List the columns of a single table',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Get Relations',
|
|
111
|
+
value: 'getRelations',
|
|
112
|
+
action: 'Get foreign key relations',
|
|
113
|
+
description: 'List foreign-key relationships for a connection',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
default: 'getTables',
|
|
117
|
+
},
|
|
118
|
+
// ── Operations: Write ───────────────────────────────────────
|
|
119
|
+
{
|
|
120
|
+
displayName: 'Operation',
|
|
121
|
+
name: 'operation',
|
|
122
|
+
type: 'options',
|
|
123
|
+
noDataExpression: true,
|
|
124
|
+
displayOptions: { show: { resource: ['write'] } },
|
|
125
|
+
options: [
|
|
126
|
+
{
|
|
127
|
+
name: 'Request Approval',
|
|
128
|
+
value: 'requestApproval',
|
|
129
|
+
action: 'Request approval for a write',
|
|
130
|
+
description: 'Submit a write statement for human approval in RelataSQL',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Check Approval',
|
|
134
|
+
value: 'checkApproval',
|
|
135
|
+
action: 'Check approval status',
|
|
136
|
+
description: 'Get the current status of a write approval',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Execute Approved',
|
|
140
|
+
value: 'executeApproved',
|
|
141
|
+
action: 'Execute an approved write',
|
|
142
|
+
description: 'Execute a write that has been approved in RelataSQL',
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
default: 'requestApproval',
|
|
146
|
+
},
|
|
147
|
+
// ── Operations: Backups ─────────────────────────────────────
|
|
148
|
+
{
|
|
149
|
+
displayName: 'Operation',
|
|
150
|
+
name: 'operation',
|
|
151
|
+
type: 'options',
|
|
152
|
+
noDataExpression: true,
|
|
153
|
+
displayOptions: { show: { resource: ['backups'] } },
|
|
154
|
+
options: [
|
|
155
|
+
{
|
|
156
|
+
name: 'List Schedules',
|
|
157
|
+
value: 'listSchedules',
|
|
158
|
+
action: 'List backup schedules',
|
|
159
|
+
description: 'List the backup schedules reachable with this API key',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'Trigger Run',
|
|
163
|
+
value: 'triggerRun',
|
|
164
|
+
action: 'Trigger a backup run now',
|
|
165
|
+
description: 'Manually run a backup schedule immediately',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'List Logs',
|
|
169
|
+
value: 'listLogs',
|
|
170
|
+
action: 'List recent backup runs',
|
|
171
|
+
description: 'List recent backup run logs',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
default: 'listSchedules',
|
|
175
|
+
},
|
|
176
|
+
// ── scheduleId (backups → triggerRun) ───────────────────────
|
|
177
|
+
{
|
|
178
|
+
displayName: 'Schedule Name or ID',
|
|
179
|
+
name: 'scheduleId',
|
|
180
|
+
type: 'options',
|
|
181
|
+
typeOptions: { loadOptionsMethod: 'getSchedules' },
|
|
182
|
+
required: true,
|
|
183
|
+
default: '',
|
|
184
|
+
description: 'The backup schedule to run. Choose from the list, or specify an ID using an expression.',
|
|
185
|
+
displayOptions: { show: { resource: ['backups'], operation: ['triggerRun'] } },
|
|
186
|
+
},
|
|
187
|
+
// ── connectionId (database / schema) ────────────────────────
|
|
188
|
+
{
|
|
189
|
+
displayName: 'Connection Name or ID',
|
|
190
|
+
name: 'connectionId',
|
|
191
|
+
type: 'options',
|
|
192
|
+
typeOptions: { loadOptionsMethod: 'getConnections' },
|
|
193
|
+
required: true,
|
|
194
|
+
default: '',
|
|
195
|
+
description: 'The RelataSQL connection to target. Choose from the list, or specify an ID using an expression.',
|
|
196
|
+
displayOptions: { show: { resource: ['database', 'schema'] } },
|
|
197
|
+
},
|
|
198
|
+
// ── connectionId (write → requestApproval) ──────────────────
|
|
199
|
+
{
|
|
200
|
+
displayName: 'Connection Name or ID',
|
|
201
|
+
name: 'connectionId',
|
|
202
|
+
type: 'options',
|
|
203
|
+
typeOptions: { loadOptionsMethod: 'getConnections' },
|
|
204
|
+
required: true,
|
|
205
|
+
default: '',
|
|
206
|
+
description: 'The RelataSQL connection to target. Choose from the list, or specify an ID using an expression.',
|
|
207
|
+
displayOptions: { show: { resource: ['write'], operation: ['requestApproval'] } },
|
|
208
|
+
},
|
|
209
|
+
// ── SQL query (executeQuery / sandbox / requestApproval) ────
|
|
210
|
+
{
|
|
211
|
+
displayName: 'SQL Query',
|
|
212
|
+
name: 'sqlQuery',
|
|
213
|
+
type: 'string',
|
|
214
|
+
typeOptions: { rows: 6 },
|
|
215
|
+
default: '',
|
|
216
|
+
required: true,
|
|
217
|
+
placeholder: 'SELECT * FROM users LIMIT 100',
|
|
218
|
+
description: 'The SQL statement to run',
|
|
219
|
+
displayOptions: {
|
|
220
|
+
show: {
|
|
221
|
+
resource: ['database', 'write'],
|
|
222
|
+
operation: ['executeQuery', 'sandbox', 'requestApproval'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
// ── Table (sampleRows / getColumns) ─────────────────────────
|
|
227
|
+
{
|
|
228
|
+
displayName: 'Table Name or ID',
|
|
229
|
+
name: 'table',
|
|
230
|
+
type: 'options',
|
|
231
|
+
typeOptions: {
|
|
232
|
+
loadOptionsMethod: 'getTables',
|
|
233
|
+
loadOptionsDependsOn: ['connectionId'],
|
|
234
|
+
},
|
|
235
|
+
required: true,
|
|
236
|
+
default: '',
|
|
237
|
+
description: 'The table to use (shown as schema.table). Choose from the list, or specify using an expression.',
|
|
238
|
+
displayOptions: {
|
|
239
|
+
show: { resource: ['database', 'schema'], operation: ['sampleRows', 'getColumns'] },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
displayName: 'Limit',
|
|
244
|
+
name: 'limit',
|
|
245
|
+
type: 'number',
|
|
246
|
+
typeOptions: { minValue: 1, maxValue: 50 },
|
|
247
|
+
default: 10,
|
|
248
|
+
description: 'Max number of rows to sample',
|
|
249
|
+
displayOptions: { show: { resource: ['database'], operation: ['sampleRows'] } },
|
|
250
|
+
},
|
|
251
|
+
// ── Create Dump options ─────────────────────────────────────
|
|
252
|
+
{
|
|
253
|
+
displayName: 'Scope',
|
|
254
|
+
name: 'scope',
|
|
255
|
+
type: 'options',
|
|
256
|
+
options: [
|
|
257
|
+
{ name: 'Structure + Data', value: 'structure-and-data' },
|
|
258
|
+
{ name: 'Structure Only', value: 'structure-only' },
|
|
259
|
+
{ name: 'Data Only', value: 'data-only' },
|
|
260
|
+
],
|
|
261
|
+
default: 'structure-and-data',
|
|
262
|
+
description: 'What to include in the dump',
|
|
263
|
+
displayOptions: { show: { resource: ['database'], operation: ['createDump'] } },
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
displayName: 'Put Output File in Field',
|
|
267
|
+
name: 'binaryPropertyName',
|
|
268
|
+
type: 'string',
|
|
269
|
+
default: 'data',
|
|
270
|
+
required: true,
|
|
271
|
+
hint: 'The name of the output binary field to write the .sql.gz file to',
|
|
272
|
+
displayOptions: { show: { resource: ['database'], operation: ['createDump'] } },
|
|
273
|
+
},
|
|
274
|
+
// ── Justification (sandbox / requestApproval) ───────────────
|
|
275
|
+
{
|
|
276
|
+
displayName: 'Justification',
|
|
277
|
+
name: 'justification',
|
|
278
|
+
type: 'string',
|
|
279
|
+
default: '',
|
|
280
|
+
required: true,
|
|
281
|
+
placeholder: 'Why this statement runs',
|
|
282
|
+
description: 'A short reason recorded for audit/approval',
|
|
283
|
+
displayOptions: {
|
|
284
|
+
show: { resource: ['database', 'write'], operation: ['sandbox', 'requestApproval'] },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
displayName: 'Operation Summary',
|
|
289
|
+
name: 'operationSummary',
|
|
290
|
+
type: 'string',
|
|
291
|
+
default: '',
|
|
292
|
+
description: 'Optional human-readable summary of the write',
|
|
293
|
+
displayOptions: { show: { resource: ['write'], operation: ['requestApproval'] } },
|
|
294
|
+
},
|
|
295
|
+
// ── Approval ID (checkApproval / executeApproved) ───────────
|
|
296
|
+
{
|
|
297
|
+
displayName: 'Approval ID',
|
|
298
|
+
name: 'approvalId',
|
|
299
|
+
type: 'string',
|
|
300
|
+
default: '',
|
|
301
|
+
required: true,
|
|
302
|
+
description: 'The ID returned by "Request Approval"',
|
|
303
|
+
displayOptions: {
|
|
304
|
+
show: { resource: ['write'], operation: ['checkApproval', 'executeApproved'] },
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
this.methods = {
|
|
310
|
+
loadOptions: {
|
|
311
|
+
async getConnections() {
|
|
312
|
+
const connections = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', '/mcp/connections'));
|
|
313
|
+
return (connections !== null && connections !== void 0 ? connections : []).map((c) => ({
|
|
314
|
+
name: `${c.name} (${c.engine} · ${c.databaseName})`,
|
|
315
|
+
value: c.id,
|
|
316
|
+
description: `${c.host}:${c.port} — MCP access: ${c.mcpAccessStatus}`,
|
|
317
|
+
}));
|
|
318
|
+
},
|
|
319
|
+
async getTables() {
|
|
320
|
+
var _a;
|
|
321
|
+
const connectionId = this.getCurrentNodeParameter('connectionId');
|
|
322
|
+
if (!connectionId)
|
|
323
|
+
return [];
|
|
324
|
+
const schema = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', `/mcp/connections/${encodeURIComponent(connectionId)}/schema`));
|
|
325
|
+
return ((_a = schema.tables) !== null && _a !== void 0 ? _a : []).map((t) => ({
|
|
326
|
+
name: `${t.schema}.${t.name}`,
|
|
327
|
+
value: `${t.schema}.${t.name}`,
|
|
328
|
+
}));
|
|
329
|
+
},
|
|
330
|
+
async getSchedules() {
|
|
331
|
+
const schedules = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', '/mcp/backups/schedules'));
|
|
332
|
+
return (schedules !== null && schedules !== void 0 ? schedules : []).map((s) => ({
|
|
333
|
+
name: `${s.name} (${s.cron})${s.enabled ? '' : ' — disabled'}`,
|
|
334
|
+
value: s.id,
|
|
335
|
+
}));
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async execute() {
|
|
341
|
+
const items = this.getInputData();
|
|
342
|
+
const returnData = [];
|
|
343
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
344
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
345
|
+
for (let i = 0; i < items.length; i++) {
|
|
346
|
+
try {
|
|
347
|
+
if (resource === 'database' && operation === 'createDump') {
|
|
348
|
+
returnData.push(await createDumpItem.call(this, i));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const out = await runOperation.call(this, resource, operation, i);
|
|
352
|
+
for (const json of out) {
|
|
353
|
+
returnData.push({ json, pairedItem: { item: i } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (this.continueOnFail()) {
|
|
358
|
+
returnData.push({
|
|
359
|
+
json: { error: error.message },
|
|
360
|
+
pairedItem: { item: i },
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return [returnData];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
exports.RelataSql = RelataSql;
|
|
371
|
+
async function runOperation(resource, operation, i) {
|
|
372
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
373
|
+
if (resource === 'connection' && operation === 'list') {
|
|
374
|
+
const connections = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', '/mcp/connections'));
|
|
375
|
+
return (connections !== null && connections !== void 0 ? connections : []);
|
|
376
|
+
}
|
|
377
|
+
if (resource === 'database') {
|
|
378
|
+
const connectionId = this.getNodeParameter('connectionId', i);
|
|
379
|
+
const cid = encodeURIComponent(connectionId);
|
|
380
|
+
if (operation === 'executeQuery') {
|
|
381
|
+
const sql = this.getNodeParameter('sqlQuery', i);
|
|
382
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'POST', `/mcp/connections/${cid}/query`, { sql }));
|
|
383
|
+
return resultToItems(res);
|
|
384
|
+
}
|
|
385
|
+
if (operation === 'sampleRows') {
|
|
386
|
+
const tableValue = this.getNodeParameter('table', i);
|
|
387
|
+
const limit = this.getNodeParameter('limit', i);
|
|
388
|
+
const { schema, table } = (0, GenericFunctions_1.splitQualifiedTable)(tableValue);
|
|
389
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'POST', `/mcp/connections/${cid}/sample-rows`, { schema, table, limit }));
|
|
390
|
+
return resultToItems(res);
|
|
391
|
+
}
|
|
392
|
+
if (operation === 'sandbox') {
|
|
393
|
+
const sql = this.getNodeParameter('sqlQuery', i);
|
|
394
|
+
const justification = this.getNodeParameter('justification', i);
|
|
395
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'POST', `/mcp/connections/${cid}/sandbox`, { sql, justification }));
|
|
396
|
+
return [
|
|
397
|
+
{
|
|
398
|
+
ok: res.ok,
|
|
399
|
+
rolledBack: res.rolledBack,
|
|
400
|
+
rowCount: res.rowCount,
|
|
401
|
+
rows: (0, GenericFunctions_1.rowsToObjects)((_a = res.columns) !== null && _a !== void 0 ? _a : [], (_b = res.rows) !== null && _b !== void 0 ? _b : []),
|
|
402
|
+
error: (_c = res.error) !== null && _c !== void 0 ? _c : null,
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (resource === 'schema') {
|
|
408
|
+
const connectionId = this.getNodeParameter('connectionId', i);
|
|
409
|
+
const cid = encodeURIComponent(connectionId);
|
|
410
|
+
if (operation === 'getTables') {
|
|
411
|
+
const schema = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', `/mcp/connections/${cid}/schema`));
|
|
412
|
+
return ((_d = schema.tables) !== null && _d !== void 0 ? _d : []).map((t) => {
|
|
413
|
+
var _a, _b;
|
|
414
|
+
return ({
|
|
415
|
+
schema: t.schema,
|
|
416
|
+
name: t.name,
|
|
417
|
+
columnCount: (_b = (_a = t.columns) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (operation === 'getColumns') {
|
|
422
|
+
const tableValue = this.getNodeParameter('table', i);
|
|
423
|
+
const { schema: schemaName, table } = (0, GenericFunctions_1.splitQualifiedTable)(tableValue);
|
|
424
|
+
const schema = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', `/mcp/connections/${cid}/schema`));
|
|
425
|
+
const match = ((_e = schema.tables) !== null && _e !== void 0 ? _e : []).find((t) => t.schema === schemaName && t.name === table);
|
|
426
|
+
return ((_f = match === null || match === void 0 ? void 0 : match.columns) !== null && _f !== void 0 ? _f : []).map((c) => ({ ...c }));
|
|
427
|
+
}
|
|
428
|
+
if (operation === 'getRelations') {
|
|
429
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', `/mcp/connections/${cid}/relations`));
|
|
430
|
+
return ((_g = res.relations) !== null && _g !== void 0 ? _g : []);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (resource === 'write') {
|
|
434
|
+
if (operation === 'requestApproval') {
|
|
435
|
+
const connectionId = this.getNodeParameter('connectionId', i);
|
|
436
|
+
const sql = this.getNodeParameter('sqlQuery', i);
|
|
437
|
+
const justification = this.getNodeParameter('justification', i);
|
|
438
|
+
const operationSummary = this.getNodeParameter('operationSummary', i, '');
|
|
439
|
+
const body = { sql, justification };
|
|
440
|
+
if (operationSummary)
|
|
441
|
+
body.operationSummary = operationSummary;
|
|
442
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'POST', `/mcp/connections/${encodeURIComponent(connectionId)}/write-approvals`, body));
|
|
443
|
+
return [res];
|
|
444
|
+
}
|
|
445
|
+
if (operation === 'checkApproval') {
|
|
446
|
+
const approvalId = this.getNodeParameter('approvalId', i);
|
|
447
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', `/mcp/write-approvals/${encodeURIComponent(approvalId)}`));
|
|
448
|
+
return [res];
|
|
449
|
+
}
|
|
450
|
+
if (operation === 'executeApproved') {
|
|
451
|
+
const approvalId = this.getNodeParameter('approvalId', i);
|
|
452
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'POST', `/mcp/write-approvals/${encodeURIComponent(approvalId)}/execute`));
|
|
453
|
+
return resultToItems(res);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (resource === 'backups') {
|
|
457
|
+
if (operation === 'listSchedules') {
|
|
458
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', '/mcp/backups/schedules'));
|
|
459
|
+
return res !== null && res !== void 0 ? res : [];
|
|
460
|
+
}
|
|
461
|
+
if (operation === 'triggerRun') {
|
|
462
|
+
const scheduleId = this.getNodeParameter('scheduleId', i);
|
|
463
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'POST', `/mcp/backups/schedules/${encodeURIComponent(scheduleId)}/run`));
|
|
464
|
+
return [res];
|
|
465
|
+
}
|
|
466
|
+
if (operation === 'listLogs') {
|
|
467
|
+
const res = (await GenericFunctions_1.relataApiRequest.call(this, 'GET', '/mcp/backups/logs'));
|
|
468
|
+
return res !== null && res !== void 0 ? res : [];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
throw new Error(`Unsupported operation "${operation}" for resource "${resource}"`);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Database → Create Dump: POST the dump request, receive the gzipped binary,
|
|
475
|
+
* and emit it as an n8n binary item (ready for a Google Drive / S3 / FTP node).
|
|
476
|
+
*/
|
|
477
|
+
async function createDumpItem(i) {
|
|
478
|
+
var _a, _b, _c, _d;
|
|
479
|
+
const connectionId = this.getNodeParameter('connectionId', i);
|
|
480
|
+
const scope = this.getNodeParameter('scope', i, 'structure-and-data');
|
|
481
|
+
const binaryProperty = this.getNodeParameter('binaryPropertyName', i, 'data');
|
|
482
|
+
const allowedScopes = ['structure-and-data', 'structure-only', 'data-only'];
|
|
483
|
+
if (!allowedScopes.includes(scope)) {
|
|
484
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid scope "${scope}". Use one of: ${allowedScopes.join(', ')}.`);
|
|
485
|
+
}
|
|
486
|
+
const credentials = await this.getCredentials('relataSqlApi');
|
|
487
|
+
const baseUrl = String((_a = credentials.baseUrl) !== null && _a !== void 0 ? _a : '').replace(/\/+$/, '');
|
|
488
|
+
let response;
|
|
489
|
+
try {
|
|
490
|
+
response = (await this.helpers.httpRequestWithAuthentication.call(this, 'relataSqlApi', {
|
|
491
|
+
method: 'POST',
|
|
492
|
+
url: `${baseUrl}/mcp/connections/${encodeURIComponent(connectionId)}/dump`,
|
|
493
|
+
body: JSON.stringify({ scope }),
|
|
494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
495
|
+
json: false,
|
|
496
|
+
encoding: 'arraybuffer',
|
|
497
|
+
returnFullResponse: true,
|
|
498
|
+
// Don't throw on 4xx/5xx: with arraybuffer encoding n8n discards the
|
|
499
|
+
// error body, so we'd lose the backend's real reason (code, detail,
|
|
500
|
+
// support reference). We inspect the status ourselves below.
|
|
501
|
+
ignoreHttpStatusErrors: true,
|
|
502
|
+
// A large database can take many minutes to dump. Wait up to 2h so the
|
|
503
|
+
// node never gives up before the backend's own dump timeout
|
|
504
|
+
// (DUMP_TIMEOUT_MS, default 2h). A reverse proxy in front of RelataSQL
|
|
505
|
+
// must allow an equally long read/idle timeout.
|
|
506
|
+
timeout: 2 * 60 * 60 * 1000,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
// Transport-level failure (timeout, connection reset) — still throws.
|
|
511
|
+
throw mapDumpError(this, error);
|
|
512
|
+
}
|
|
513
|
+
// HTTP error status: the (arraybuffer) body holds the JSON error; hand the
|
|
514
|
+
// whole thing to mapDumpError so it surfaces the backend's real reason.
|
|
515
|
+
const status = Number((_b = response.statusCode) !== null && _b !== void 0 ? _b : 200);
|
|
516
|
+
if (status >= 400) {
|
|
517
|
+
throw mapDumpError(this, {
|
|
518
|
+
statusCode: status,
|
|
519
|
+
response: { statusCode: status, body: response.body },
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const raw = response.body;
|
|
523
|
+
let buffer;
|
|
524
|
+
if (Buffer.isBuffer(raw)) {
|
|
525
|
+
buffer = raw;
|
|
526
|
+
}
|
|
527
|
+
else if (raw instanceof Uint8Array || raw instanceof ArrayBuffer) {
|
|
528
|
+
buffer = Buffer.from(raw);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'RelataSQL returned an empty or unexpected dump response.');
|
|
532
|
+
}
|
|
533
|
+
const disposition = String((_d = (_c = response.headers) === null || _c === void 0 ? void 0 : _c['content-disposition']) !== null && _d !== void 0 ? _d : '');
|
|
534
|
+
const m = /filename="?([^"]+)"?/.exec(disposition);
|
|
535
|
+
const fileName = m ? m[1] : `${connectionId}.sql.gz`;
|
|
536
|
+
const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'application/gzip');
|
|
537
|
+
return {
|
|
538
|
+
json: { fileName, mimeType: 'application/gzip', sizeBytes: buffer.length },
|
|
539
|
+
binary: { [binaryProperty]: binaryData },
|
|
540
|
+
pairedItem: { item: i },
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function extractDumpErrorDetails(error) {
|
|
544
|
+
var _a, _b, _c, _d;
|
|
545
|
+
const parts = [];
|
|
546
|
+
let detail;
|
|
547
|
+
const statusCode = [
|
|
548
|
+
error === null || error === void 0 ? void 0 : error.statusCode,
|
|
549
|
+
error === null || error === void 0 ? void 0 : error.status,
|
|
550
|
+
error === null || error === void 0 ? void 0 : error.httpCode,
|
|
551
|
+
(_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.statusCode,
|
|
552
|
+
(_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status,
|
|
553
|
+
].find((value) => typeof value === 'number');
|
|
554
|
+
const addObject = (value) => {
|
|
555
|
+
for (const key of ['code', 'message', 'error', 'description', 'detail']) {
|
|
556
|
+
const part = value[key];
|
|
557
|
+
if (typeof part === 'string') {
|
|
558
|
+
parts.push(part);
|
|
559
|
+
// The backend's `detail` carries the real dump-tool error.
|
|
560
|
+
if (key === 'detail' && part.trim())
|
|
561
|
+
detail = part.trim();
|
|
562
|
+
}
|
|
563
|
+
else if (Array.isArray(part))
|
|
564
|
+
parts.push(part.join(', '));
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
parts.push(JSON.stringify(value));
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
/* ignore */
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const addBody = (body) => {
|
|
574
|
+
if (!body)
|
|
575
|
+
return;
|
|
576
|
+
try {
|
|
577
|
+
if (Buffer.isBuffer(body)) {
|
|
578
|
+
addBody(body.toString('utf8'));
|
|
579
|
+
}
|
|
580
|
+
else if (body instanceof ArrayBuffer) {
|
|
581
|
+
addBody(Buffer.from(body).toString('utf8'));
|
|
582
|
+
}
|
|
583
|
+
else if (body instanceof Uint8Array) {
|
|
584
|
+
addBody(Buffer.from(body).toString('utf8'));
|
|
585
|
+
}
|
|
586
|
+
else if (typeof body === 'string') {
|
|
587
|
+
parts.push(body);
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(body);
|
|
590
|
+
if (parsed && typeof parsed === 'object') {
|
|
591
|
+
addObject(parsed);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
/* body was plain text */
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else if (typeof body === 'object') {
|
|
599
|
+
addObject(body);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
/* ignore malformed error bodies */
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
addBody(error === null || error === void 0 ? void 0 : error.message);
|
|
607
|
+
addBody(error === null || error === void 0 ? void 0 : error.description);
|
|
608
|
+
addBody((_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.body);
|
|
609
|
+
addBody((_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.data);
|
|
610
|
+
addBody(error === null || error === void 0 ? void 0 : error.error);
|
|
611
|
+
return { text: parts.join(' | '), statusCode, detail };
|
|
612
|
+
}
|
|
613
|
+
/** Pulls the backend's support reference (dump-<uuid>) out of an error body. */
|
|
614
|
+
function extractSupportReference(text) {
|
|
615
|
+
var _a;
|
|
616
|
+
const m = (_a = /"supportReference"\s*:\s*"([^"]+)"/.exec(text)) !== null && _a !== void 0 ? _a : /(dump-[0-9a-fA-F-]{8,})/.exec(text);
|
|
617
|
+
return m === null || m === void 0 ? void 0 : m[1];
|
|
618
|
+
}
|
|
619
|
+
function mapDumpError(ctx, error) {
|
|
620
|
+
const { text, statusCode, detail } = extractDumpErrorDetails(error);
|
|
621
|
+
const lower = text.toLowerCase();
|
|
622
|
+
const ref = extractSupportReference(text);
|
|
623
|
+
const refHint = ref ? ` Support reference: ${ref}.` : '';
|
|
624
|
+
const detailHint = detail ? ` Backend error: ${detail}` : '';
|
|
625
|
+
// Client- or proxy-side timeout: the node/proxy gave up waiting. Distinct
|
|
626
|
+
// from a backend dump failure so the user knows to raise the time limit
|
|
627
|
+
// rather than "check the connection".
|
|
628
|
+
if (statusCode === 504 ||
|
|
629
|
+
statusCode === 408 ||
|
|
630
|
+
lower.includes('econnaborted') ||
|
|
631
|
+
lower.includes('etimedout') ||
|
|
632
|
+
lower.includes('socket hang up') ||
|
|
633
|
+
/timeout of \d+ *ms exceeded/.test(lower) ||
|
|
634
|
+
lower.includes('timed out')) {
|
|
635
|
+
return new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'The database dump timed out before it finished.', {
|
|
636
|
+
description: 'The database is large and the export exceeded the time limit. Raise DUMP_TIMEOUT_MS on the RelataSQL backend (and any reverse-proxy read/idle timeout), then retry. For very large databases, consider dumping "Structure only" or splitting the export by table.',
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (text.includes('JIT_ACCESS_REQUIRED')) {
|
|
640
|
+
return new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'This RelataSQL connection is not enabled for programmatic (API key) access.', {
|
|
641
|
+
description: 'Enable MCP access for this connection in RelataSQL → Settings → MCP (pick an indefinite grant for unattended n8n flows), then retry.',
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (text.includes('DUMP_REQUIRES_PAID_PLAN') ||
|
|
645
|
+
text.includes('Upgrade to Pro to create database backups from the API') ||
|
|
646
|
+
text.toLowerCase().includes('create dump requires a paid relatasql plan')) {
|
|
647
|
+
return new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Create Dump requires a paid RelataSQL plan.', {
|
|
648
|
+
description: 'Your current RelataSQL plan does not include automated database dumps. Upgrade to Pro or Team, then retry this workflow.',
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
if (statusCode === 403 ||
|
|
652
|
+
text.toLowerCase().includes('forbidden - perhaps check your credentials')) {
|
|
653
|
+
return new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'RelataSQL rejected this dump request before the database export started.', {
|
|
654
|
+
description: 'Check that the selected RelataSQL connection is enabled for API access and that this API key belongs to the same workspace.',
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (text.includes('DUMP_PROCESS_FAILED') ||
|
|
658
|
+
statusCode === 500 ||
|
|
659
|
+
lower.includes('the service failed to process your request')) {
|
|
660
|
+
return new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'RelataSQL could not create the database dump.', {
|
|
661
|
+
description: 'Verify the selected connection in RelataSQL and try again. If the database is large, the export may have exceeded the dump time limit — raise DUMP_TIMEOUT_MS on the backend. If it keeps failing, contact RelataSQL support with the support reference and this n8n execution time.' +
|
|
662
|
+
detailHint +
|
|
663
|
+
refHint,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return new n8n_workflow_1.NodeOperationError(ctx.getNode(), (text.trim() || 'Create Dump failed') + detailHint + refHint);
|
|
667
|
+
}
|
|
668
|
+
/** Maps a columnar query result to row objects; never returns zero items. */
|
|
669
|
+
function resultToItems(res) {
|
|
670
|
+
var _a, _b, _c, _d;
|
|
671
|
+
const rows = (0, GenericFunctions_1.rowsToObjects)((_a = res.columns) !== null && _a !== void 0 ? _a : [], (_b = res.rows) !== null && _b !== void 0 ? _b : []);
|
|
672
|
+
if (rows.length === 0) {
|
|
673
|
+
return [{ rowCount: (_c = res.rowCount) !== null && _c !== void 0 ? _c : 0, truncated: (_d = res.truncated) !== null && _d !== void 0 ? _d : false }];
|
|
674
|
+
}
|
|
675
|
+
if (res.truncated) {
|
|
676
|
+
rows[rows.length - 1] = { ...rows[rows.length - 1], _truncated: true };
|
|
677
|
+
}
|
|
678
|
+
return rows;
|
|
679
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="rs" x1="8" y1="6" x2="40" y2="42" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#6366F1"/>
|
|
5
|
+
<stop offset="1" stop-color="#3B82F6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<ellipse cx="24" cy="12" rx="14" ry="6" fill="url(#rs)"/>
|
|
9
|
+
<path d="M10 12v10c0 3.31 6.27 6 14 6s14-2.69 14-6V12" fill="url(#rs)" opacity="0.85"/>
|
|
10
|
+
<path d="M10 22v10c0 3.31 6.27 6 14 6s14-2.69 14-6V22" fill="url(#rs)" opacity="0.7"/>
|
|
11
|
+
<path d="M27 19l-7 9h5l-2 8 9-11h-6l3-6z" fill="#FFFFFF"/>
|
|
12
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@relatalabs/n8n-nodes-relatasql",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "n8n community node for RelataSQL - run SQL, inspect schemas, and govern writes across your database connections using a RelataSQL API key.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n",
|
|
8
|
+
"relatasql",
|
|
9
|
+
"sql",
|
|
10
|
+
"postgres",
|
|
11
|
+
"database"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://github.com/RelataSQL/n8n-nodes-relatasql",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "RelataSQL"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/RelataSQL/n8n-nodes-relatasql.git"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.15"
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/index.js",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc && gulp build:icons",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"dev": "tsc --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint nodes credentials package.json",
|
|
32
|
+
"lintfix": "eslint nodes credentials package.json --fix"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"CHANGELOG.md"
|
|
37
|
+
],
|
|
38
|
+
"n8n": {
|
|
39
|
+
"n8nNodesApiVersion": 1,
|
|
40
|
+
"credentials": [
|
|
41
|
+
"dist/credentials/RelataSqlApi.credentials.js"
|
|
42
|
+
],
|
|
43
|
+
"nodes": [
|
|
44
|
+
"dist/nodes/RelataSql/RelataSql.node.js"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"n8n-workflow": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.12.0",
|
|
52
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
53
|
+
"eslint": "^8.57.0",
|
|
54
|
+
"eslint-plugin-n8n-nodes-base": "^1.16.0",
|
|
55
|
+
"gulp": "^4.0.2",
|
|
56
|
+
"n8n-workflow": "*",
|
|
57
|
+
"typescript": "^5.4.0"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|