forge-fsql 1.0.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/LICENSE +21 -0
- package/README.md +60 -0
- package/bin/fsql +2 -0
- package/bin/setup +157 -0
- package/dist/cjs/cli.js +49 -0
- package/dist/cjs/client.js +65 -0
- package/dist/cjs/commands.js +66 -0
- package/dist/cjs/execute-sql.js +56 -0
- package/dist/cjs/formatter.js +64 -0
- package/dist/cjs/history.js +78 -0
- package/dist/cjs/index.js +176 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cli.js +14 -0
- package/dist/client.js +58 -0
- package/dist/commands.js +59 -0
- package/dist/execute-sql.js +53 -0
- package/dist/formatter.js +57 -0
- package/dist/history.js +41 -0
- package/dist/index.js +134 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Christian Hatch
|
|
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,60 @@
|
|
|
1
|
+
# Forge FSQL CLI
|
|
2
|
+
|
|
3
|
+
Interactive command-line interface for querying Atlassian Forge SQL databases via web triggers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎨 Beautiful table formatting with colors
|
|
8
|
+
- 📝 Multi-line SQL support
|
|
9
|
+
- ⌨️ Command history (↑/↓ arrows)
|
|
10
|
+
- ⚡ Special commands (.tables, .describe, .schema)
|
|
11
|
+
- ⏱️ Query timing
|
|
12
|
+
- 💾 Persistent history across sessions
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### In Your Forge Project
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D forge-fsql
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add to your `package.json` scripts:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"scripts": {
|
|
27
|
+
"sql": "fsql"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Global Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add -g forge-fsql
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Create a `.env` file in your project root:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
FORGE_SQL_URL=https://your-trigger-url.forge.atlassian.com/sql
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or pass via command line:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
fsql --url https://your-trigger-url.com
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# If installed in project
|
|
56
|
+
pnpm sql
|
|
57
|
+
|
|
58
|
+
# If installed globally
|
|
59
|
+
fsql
|
|
60
|
+
```
|
package/bin/fsql
ADDED
package/bin/setup
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
|
|
7
|
+
// Support both ESM and CJS imports for js-yaml
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const yaml = require("js-yaml");
|
|
10
|
+
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
|
|
13
|
+
console.log("Setting up Forge SQL CLI in this project...");
|
|
14
|
+
|
|
15
|
+
// Detect consumer project type
|
|
16
|
+
let isEsm = false;
|
|
17
|
+
let isTypeScript = fs.existsSync(path.join(projectRoot, "tsconfig.json"));
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
21
|
+
if (fs.existsSync(pkgPath)) {
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
23
|
+
if (pkg.type === "module") {
|
|
24
|
+
isEsm = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Ignore errors reading package.json
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 1. Create src/fsql file
|
|
32
|
+
const srcDir = path.join(projectRoot, "src");
|
|
33
|
+
if (!fs.existsSync(srcDir)) {
|
|
34
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const extension = isTypeScript ? "ts" : isEsm ? "js" : "js";
|
|
38
|
+
// If it's CJS, we still use .js but different content
|
|
39
|
+
const fsqlPath = path.join(srcDir, `fsql.${extension}`);
|
|
40
|
+
|
|
41
|
+
let fsqlContent = "";
|
|
42
|
+
if (isEsm || isTypeScript) {
|
|
43
|
+
fsqlContent = `import { executeSql } from 'forge-fsql';
|
|
44
|
+
|
|
45
|
+
export { executeSql };
|
|
46
|
+
`;
|
|
47
|
+
} else {
|
|
48
|
+
fsqlContent = `const { executeSql } = require('forge-fsql');
|
|
49
|
+
|
|
50
|
+
module.exports = { executeSql };
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(fsqlPath, fsqlContent);
|
|
55
|
+
console.log(`✓ Created src/fsql.${extension}`);
|
|
56
|
+
|
|
57
|
+
// 2. Update manifest.yaml or manifest.yml
|
|
58
|
+
let manifestPath = path.join(projectRoot, "manifest.yml");
|
|
59
|
+
if (!fs.existsSync(manifestPath)) {
|
|
60
|
+
manifestPath = path.join(projectRoot, "manifest.yaml");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(manifestPath)) {
|
|
64
|
+
console.error(
|
|
65
|
+
"Error: Could not find manifest.yml or manifest.yaml in the current directory.",
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let manifest;
|
|
71
|
+
try {
|
|
72
|
+
const fileContents = fs.readFileSync(manifestPath, "utf8");
|
|
73
|
+
manifest = yaml.load(fileContents);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error("Error reading manifest:", e);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!manifest.modules) {
|
|
80
|
+
manifest.modules = {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ensure function is an array
|
|
84
|
+
const functionKey = "executeSql";
|
|
85
|
+
if (!manifest.modules.function) {
|
|
86
|
+
manifest.modules.function = [];
|
|
87
|
+
} else if (!Array.isArray(manifest.modules.function)) {
|
|
88
|
+
// Handle case where it might be a map (unlikely in Forge but just in case)
|
|
89
|
+
manifest.modules.function = Object.entries(manifest.modules.function).map(
|
|
90
|
+
([key, val]) => ({ key, ...val }),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const functionExists = manifest.modules.function.find(
|
|
95
|
+
(f) => f.key === functionKey,
|
|
96
|
+
);
|
|
97
|
+
const handlerName = "fsql.executeSql";
|
|
98
|
+
|
|
99
|
+
if (!functionExists) {
|
|
100
|
+
manifest.modules.function.push({
|
|
101
|
+
key: functionKey,
|
|
102
|
+
handler: handlerName,
|
|
103
|
+
});
|
|
104
|
+
console.log(
|
|
105
|
+
`✓ Added function:${functionKey} with handler ${handlerName} to manifest`,
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
functionExists.handler = handlerName;
|
|
109
|
+
console.log(
|
|
110
|
+
`✓ Updated function:${functionKey} handler to ${handlerName} in manifest`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Ensure webtrigger is an array
|
|
115
|
+
const webtriggerKey = "execute-sql";
|
|
116
|
+
if (!manifest.modules.webtrigger) {
|
|
117
|
+
manifest.modules.webtrigger = [];
|
|
118
|
+
} else if (!Array.isArray(manifest.modules.webtrigger)) {
|
|
119
|
+
manifest.modules.webtrigger = Object.entries(manifest.modules.webtrigger).map(
|
|
120
|
+
([key, val]) => ({ key, ...val }),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const webtriggerExists = manifest.modules.webtrigger.find(
|
|
125
|
+
(w) => w.key === webtriggerKey,
|
|
126
|
+
);
|
|
127
|
+
if (!webtriggerExists) {
|
|
128
|
+
manifest.modules.webtrigger.push({
|
|
129
|
+
key: webtriggerKey,
|
|
130
|
+
function: functionKey,
|
|
131
|
+
});
|
|
132
|
+
console.log(`✓ Added webtrigger:${webtriggerKey} to manifest`);
|
|
133
|
+
} else {
|
|
134
|
+
webtriggerExists.function = functionKey;
|
|
135
|
+
console.log(
|
|
136
|
+
`✓ Ensured webtrigger:${webtriggerKey} points to function ${functionKey}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Use a large enough lineWidth to prevent wrapping which can break Forge parsing sometimes
|
|
142
|
+
const newYaml = yaml.dump(manifest, {
|
|
143
|
+
indent: 2,
|
|
144
|
+
lineWidth: -1,
|
|
145
|
+
noRefs: true,
|
|
146
|
+
});
|
|
147
|
+
fs.writeFileSync(manifestPath, newYaml);
|
|
148
|
+
console.log("✓ Updated manifest file successfully");
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error("Error writing manifest:", e);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log("\nSetup completed successfully!");
|
|
155
|
+
console.log(
|
|
156
|
+
"You can now use the forge-fsql CLI to interact with your Forge SQL database.",
|
|
157
|
+
);
|
package/dist/cjs/cli.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const dotenv = __importStar(require("dotenv"));
|
|
37
|
+
const index_js_1 = require("./index.js");
|
|
38
|
+
dotenv.config();
|
|
39
|
+
// Parse command line arguments
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const config = {};
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
if (args[i] === "--url" && args[i + 1]) {
|
|
44
|
+
config.url = args[i + 1];
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const cli = new index_js_1.ForgeSqlCli(config);
|
|
49
|
+
cli.start();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ForgeClient = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
class ForgeClient {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = {
|
|
11
|
+
timeout: 30000,
|
|
12
|
+
...config,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async execute(sql) {
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
// Ensure SQL ends with a semicolon
|
|
18
|
+
const finalSql = sql.trim().endsWith(";") ? sql : `${sql};`;
|
|
19
|
+
try {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
22
|
+
const headers = {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
};
|
|
25
|
+
const response = await (0, node_fetch_1.default)(this.config.url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers,
|
|
28
|
+
body: JSON.stringify({ query: finalSql }),
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
clearTimeout(timeoutId);
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorText = await response.text();
|
|
34
|
+
return {
|
|
35
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const elapsed = Date.now() - startTime;
|
|
40
|
+
return {
|
|
41
|
+
...data,
|
|
42
|
+
metadata: {
|
|
43
|
+
...data.metadata,
|
|
44
|
+
queryTime: elapsed,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.name === "AbortError") {
|
|
50
|
+
return { error: "Query timeout exceeded" };
|
|
51
|
+
}
|
|
52
|
+
return { error: error.message || "Unknown error" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async testConnection() {
|
|
56
|
+
try {
|
|
57
|
+
const result = await this.execute("SELECT 1 as test");
|
|
58
|
+
return !result.error;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.ForgeClient = ForgeClient;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.specialCommands = void 0;
|
|
7
|
+
exports.parseCommand = parseCommand;
|
|
8
|
+
const formatter_js_1 = require("./formatter.js");
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
exports.specialCommands = [
|
|
11
|
+
{
|
|
12
|
+
name: ".tables",
|
|
13
|
+
description: "List all tables",
|
|
14
|
+
execute: async (client) => {
|
|
15
|
+
const result = await client.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()");
|
|
16
|
+
return formatter_js_1.ResultFormatter.formatResult(result);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: ".describe",
|
|
21
|
+
description: "Describe a table (.describe table_name)",
|
|
22
|
+
execute: async (client, args) => {
|
|
23
|
+
if (!args) {
|
|
24
|
+
return chalk_1.default.yellow("Usage: .describe <table_name>");
|
|
25
|
+
}
|
|
26
|
+
const result = await client.execute(`DESCRIBE ${args}`);
|
|
27
|
+
return formatter_js_1.ResultFormatter.formatResult(result);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: ".schema",
|
|
32
|
+
description: "Show database schema",
|
|
33
|
+
execute: async (client) => {
|
|
34
|
+
const result = await client.execute("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = DATABASE() ORDER BY table_name, ordinal_position");
|
|
35
|
+
return formatter_js_1.ResultFormatter.formatResult(result);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: ".help",
|
|
40
|
+
description: "Show available commands",
|
|
41
|
+
execute: async () => {
|
|
42
|
+
const helpText = [
|
|
43
|
+
chalk_1.default.bold("Special Commands:"),
|
|
44
|
+
...exports.specialCommands.map((cmd) => ` ${chalk_1.default.cyan(cmd.name.padEnd(15))} ${cmd.description}`),
|
|
45
|
+
"",
|
|
46
|
+
chalk_1.default.bold("Other:"),
|
|
47
|
+
` ${chalk_1.default.cyan("exit, quit".padEnd(15))} Exit the CLI`,
|
|
48
|
+
` ${chalk_1.default.cyan("Ctrl+C".padEnd(15))} Cancel current query`,
|
|
49
|
+
` ${chalk_1.default.cyan("Ctrl+D".padEnd(15))} Exit the CLI`,
|
|
50
|
+
` ${chalk_1.default.cyan("↑/↓".padEnd(15))} Navigate command history`,
|
|
51
|
+
];
|
|
52
|
+
return helpText.join("\n");
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
function parseCommand(input) {
|
|
57
|
+
const trimmed = input.trim();
|
|
58
|
+
if (trimmed.startsWith(".")) {
|
|
59
|
+
const parts = trimmed.split(/\s+/);
|
|
60
|
+
const cmdName = parts[0];
|
|
61
|
+
const args = parts.slice(1).join(" ");
|
|
62
|
+
const command = exports.specialCommands.find((c) => c.name === cmdName);
|
|
63
|
+
return { command, args, isSpecial: true };
|
|
64
|
+
}
|
|
65
|
+
return { isSpecial: false };
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeSql = void 0;
|
|
4
|
+
const sql_1 = require("@forge/sql");
|
|
5
|
+
const executeSql = async (req) => {
|
|
6
|
+
console.log("\n=== Executing Custom SQL Query ===");
|
|
7
|
+
const payload = req.body;
|
|
8
|
+
let sqlRequest = null;
|
|
9
|
+
let query;
|
|
10
|
+
try {
|
|
11
|
+
sqlRequest = JSON.parse(payload);
|
|
12
|
+
query = sqlRequest?.query;
|
|
13
|
+
if (!query) {
|
|
14
|
+
return getHttpResponse(400, {
|
|
15
|
+
success: false,
|
|
16
|
+
error: "No SQL query provided",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
console.log("Executing query:", query);
|
|
20
|
+
// Import sql directly for custom queries
|
|
21
|
+
const result = await sql_1.sql.executeRaw(query);
|
|
22
|
+
console.log("Query result:", result);
|
|
23
|
+
return getHttpResponse(200, {
|
|
24
|
+
success: true,
|
|
25
|
+
rows: result.rows || [],
|
|
26
|
+
rowCount: result.rows?.length || 0,
|
|
27
|
+
query,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(error);
|
|
32
|
+
console.error("Error while executing sql", { error });
|
|
33
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return getHttpResponse(500, {
|
|
35
|
+
success: false,
|
|
36
|
+
error: errorMessage,
|
|
37
|
+
...(query && { query }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
exports.executeSql = executeSql;
|
|
42
|
+
function getHttpResponse(statusCode, body) {
|
|
43
|
+
const statusTexts = {
|
|
44
|
+
200: "OK",
|
|
45
|
+
400: "Bad Request",
|
|
46
|
+
404: "Not Found",
|
|
47
|
+
500: "Internal Server Error",
|
|
48
|
+
};
|
|
49
|
+
const statusText = statusTexts[statusCode] || "Bad Request";
|
|
50
|
+
return {
|
|
51
|
+
headers: { "Content-Type": ["application/json"] },
|
|
52
|
+
statusCode,
|
|
53
|
+
statusText,
|
|
54
|
+
body: JSON.stringify(body),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ResultFormatter = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
9
|
+
class ResultFormatter {
|
|
10
|
+
static formatTable(rows) {
|
|
11
|
+
if (!rows || rows.length === 0) {
|
|
12
|
+
return chalk_1.default.yellow("(0 rows)");
|
|
13
|
+
}
|
|
14
|
+
const headers = Object.keys(rows[0]);
|
|
15
|
+
const table = new cli_table3_1.default({
|
|
16
|
+
head: headers.map((h) => chalk_1.default.cyan(h)),
|
|
17
|
+
style: {
|
|
18
|
+
head: [],
|
|
19
|
+
border: ["grey"],
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
rows.forEach((row) => {
|
|
23
|
+
table.push(headers.map((h) => this.formatValue(row[h])));
|
|
24
|
+
});
|
|
25
|
+
return (table.toString() +
|
|
26
|
+
"\n" +
|
|
27
|
+
chalk_1.default.gray(`(${rows.length} row${rows.length !== 1 ? "s" : ""})`));
|
|
28
|
+
}
|
|
29
|
+
static formatValue(value) {
|
|
30
|
+
if (value === null || value === undefined) {
|
|
31
|
+
return chalk_1.default.gray("NULL");
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "boolean") {
|
|
34
|
+
return value ? chalk_1.default.green("true") : chalk_1.default.red("false");
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "number") {
|
|
37
|
+
return chalk_1.default.yellow(value.toString());
|
|
38
|
+
}
|
|
39
|
+
return value.toString();
|
|
40
|
+
}
|
|
41
|
+
static formatError(error) {
|
|
42
|
+
return chalk_1.default.red("✗ Error: ") + error;
|
|
43
|
+
}
|
|
44
|
+
static formatSuccess(message) {
|
|
45
|
+
return chalk_1.default.green("✓ ") + message;
|
|
46
|
+
}
|
|
47
|
+
static formatResult(result) {
|
|
48
|
+
if (result.error) {
|
|
49
|
+
return this.formatError(result.error);
|
|
50
|
+
}
|
|
51
|
+
if (result.rows && result.rows.length > 0) {
|
|
52
|
+
return this.formatTable(result.rows);
|
|
53
|
+
}
|
|
54
|
+
if (result.affectedRows !== undefined) {
|
|
55
|
+
return this.formatSuccess(`${result.affectedRows} row(s) affected`);
|
|
56
|
+
}
|
|
57
|
+
return chalk_1.default.gray("Query executed successfully");
|
|
58
|
+
}
|
|
59
|
+
static formatQueryTime(ms) {
|
|
60
|
+
const seconds = (ms / 1000).toFixed(3);
|
|
61
|
+
return chalk_1.default.gray(`⏱ ${seconds}s`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.ResultFormatter = ResultFormatter;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.History = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
class History {
|
|
41
|
+
constructor(maxSize = 1000) {
|
|
42
|
+
this.history = [];
|
|
43
|
+
this.historyFile = path.join(os.homedir(), ".forge_sql_history");
|
|
44
|
+
this.maxSize = maxSize;
|
|
45
|
+
this.load();
|
|
46
|
+
}
|
|
47
|
+
load() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(this.historyFile)) {
|
|
50
|
+
const content = fs.readFileSync(this.historyFile, "utf-8");
|
|
51
|
+
this.history = content.split("\n").filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error("Failed to load history:", error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
save() {
|
|
59
|
+
try {
|
|
60
|
+
// Keep only last maxSize entries
|
|
61
|
+
const toSave = this.history.slice(-this.maxSize);
|
|
62
|
+
fs.writeFileSync(this.historyFile, toSave.join("\n"));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error("Failed to save history:", error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
add(command) {
|
|
69
|
+
const trimmed = command.trim();
|
|
70
|
+
if (trimmed && trimmed !== this.history[this.history.length - 1]) {
|
|
71
|
+
this.history.push(trimmed);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
getAll() {
|
|
75
|
+
return [...this.history];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.History = History;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ForgeSqlCli = exports.executeSql = exports.ForgeClient = void 0;
|
|
40
|
+
const readline = __importStar(require("readline"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const client_js_1 = require("./client.js");
|
|
43
|
+
const formatter_js_1 = require("./formatter.js");
|
|
44
|
+
const history_js_1 = require("./history.js");
|
|
45
|
+
const commands_js_1 = require("./commands.js");
|
|
46
|
+
var client_js_2 = require("./client.js");
|
|
47
|
+
Object.defineProperty(exports, "ForgeClient", { enumerable: true, get: function () { return client_js_2.ForgeClient; } });
|
|
48
|
+
var execute_sql_js_1 = require("./execute-sql.js");
|
|
49
|
+
Object.defineProperty(exports, "executeSql", { enumerable: true, get: function () { return execute_sql_js_1.executeSql; } });
|
|
50
|
+
const getPrimaryPrompt = () => chalk_1.default.green("fsql> ");
|
|
51
|
+
const getMultilinePrompt = () => chalk_1.default.green(" ...> ");
|
|
52
|
+
class ForgeSqlCli {
|
|
53
|
+
constructor(config) {
|
|
54
|
+
this.multilineBuffer = "";
|
|
55
|
+
this.isMultiline = false;
|
|
56
|
+
const url = config.url || process.env.FORGE_SQL_URL;
|
|
57
|
+
if (!url) {
|
|
58
|
+
console.error(chalk_1.default.red("Error: FORGE_SQL_URL not configured"));
|
|
59
|
+
console.error(chalk_1.default.yellow("Set it via environment variable or .env file"));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
this.client = new client_js_1.ForgeClient({
|
|
63
|
+
url,
|
|
64
|
+
});
|
|
65
|
+
this.history = new history_js_1.History();
|
|
66
|
+
this.rl = readline.createInterface({
|
|
67
|
+
input: process.stdin,
|
|
68
|
+
output: process.stdout,
|
|
69
|
+
prompt: getPrimaryPrompt(),
|
|
70
|
+
history: this.history.getAll().reverse(),
|
|
71
|
+
historySize: 1000,
|
|
72
|
+
});
|
|
73
|
+
this.setupHandlers();
|
|
74
|
+
}
|
|
75
|
+
setupHandlers() {
|
|
76
|
+
this.rl.on("line", (line) => this.handleLine(line));
|
|
77
|
+
this.rl.on("close", () => this.handleClose());
|
|
78
|
+
// Handle Ctrl+C
|
|
79
|
+
this.rl.on("SIGINT", () => {
|
|
80
|
+
if (this.isMultiline) {
|
|
81
|
+
console.log("\n" + chalk_1.default.yellow("Multi-line input cancelled"));
|
|
82
|
+
this.multilineBuffer = "";
|
|
83
|
+
this.isMultiline = false;
|
|
84
|
+
this.rl.setPrompt(getPrimaryPrompt());
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log("\n" + chalk_1.default.gray("(Use .exit, exit, or Ctrl+D to quit)"));
|
|
88
|
+
}
|
|
89
|
+
this.rl.prompt();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async handleLine(line) {
|
|
93
|
+
const input = line.trim();
|
|
94
|
+
// Handle exit commands
|
|
95
|
+
if (input === "exit" || input === "quit" || input === ".exit") {
|
|
96
|
+
this.rl.close();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Handle empty input
|
|
100
|
+
if (!input) {
|
|
101
|
+
this.rl.prompt();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Check for multi-line SQL (doesn't end with semicolon)
|
|
105
|
+
if (!this.isMultiline && !input.endsWith(";") && !input.startsWith(".")) {
|
|
106
|
+
this.isMultiline = true;
|
|
107
|
+
this.multilineBuffer = input;
|
|
108
|
+
this.rl.setPrompt(getMultilinePrompt());
|
|
109
|
+
this.rl.prompt();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Continue multi-line
|
|
113
|
+
if (this.isMultiline) {
|
|
114
|
+
this.multilineBuffer += "\n" + input;
|
|
115
|
+
if (input.endsWith(";")) {
|
|
116
|
+
const fullSql = this.multilineBuffer;
|
|
117
|
+
this.multilineBuffer = "";
|
|
118
|
+
this.isMultiline = false;
|
|
119
|
+
this.rl.setPrompt(getPrimaryPrompt());
|
|
120
|
+
await this.executeCommand(fullSql);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.rl.prompt();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
await this.executeCommand(input);
|
|
129
|
+
}
|
|
130
|
+
this.rl.prompt();
|
|
131
|
+
}
|
|
132
|
+
async executeCommand(input) {
|
|
133
|
+
this.history.add(input);
|
|
134
|
+
// Check for special commands
|
|
135
|
+
const { command, args, isSpecial } = (0, commands_js_1.parseCommand)(input);
|
|
136
|
+
if (isSpecial) {
|
|
137
|
+
if (command) {
|
|
138
|
+
const result = await command.execute(this.client, args);
|
|
139
|
+
console.log(result);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(chalk_1.default.red("Unknown command. Type .help for available commands"));
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Execute SQL
|
|
147
|
+
const result = await this.client.execute(input);
|
|
148
|
+
console.log(formatter_js_1.ResultFormatter.formatResult(result));
|
|
149
|
+
if (result.metadata?.queryTime) {
|
|
150
|
+
console.log(formatter_js_1.ResultFormatter.formatQueryTime(result.metadata.queryTime));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
handleClose() {
|
|
154
|
+
this.history.save();
|
|
155
|
+
console.log(chalk_1.default.gray("\nGoodbye!"));
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
async start() {
|
|
159
|
+
console.log(chalk_1.default.bold.blue("Forge FSQL CLI"));
|
|
160
|
+
console.log(chalk_1.default.gray("Type .help for commands, exit to quit"));
|
|
161
|
+
console.log(chalk_1.default.gray("=".repeat(50)));
|
|
162
|
+
// Test connection
|
|
163
|
+
process.stdout.write("Testing connection... ");
|
|
164
|
+
const connected = await this.client.testConnection();
|
|
165
|
+
if (connected) {
|
|
166
|
+
console.log(chalk_1.default.green("✓ Connected"));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log(chalk_1.default.red("✗ Connection failed"));
|
|
170
|
+
console.log(chalk_1.default.yellow("Check your FORGE_SQL_URL configuration"));
|
|
171
|
+
}
|
|
172
|
+
console.log("");
|
|
173
|
+
this.rl.prompt();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.ForgeSqlCli = ForgeSqlCli;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type": "commonjs"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as dotenv from "dotenv";
|
|
2
|
+
import { ForgeSqlCli } from "./index.js";
|
|
3
|
+
dotenv.config();
|
|
4
|
+
// Parse command line arguments
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const config = {};
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
if (args[i] === "--url" && args[i + 1]) {
|
|
9
|
+
config.url = args[i + 1];
|
|
10
|
+
i++;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const cli = new ForgeSqlCli(config);
|
|
14
|
+
cli.start();
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
export class ForgeClient {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = {
|
|
5
|
+
timeout: 30000,
|
|
6
|
+
...config,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
async execute(sql) {
|
|
10
|
+
const startTime = Date.now();
|
|
11
|
+
// Ensure SQL ends with a semicolon
|
|
12
|
+
const finalSql = sql.trim().endsWith(";") ? sql : `${sql};`;
|
|
13
|
+
try {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
16
|
+
const headers = {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
};
|
|
19
|
+
const response = await fetch(this.config.url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers,
|
|
22
|
+
body: JSON.stringify({ query: finalSql }),
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const errorText = await response.text();
|
|
28
|
+
return {
|
|
29
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
const elapsed = Date.now() - startTime;
|
|
34
|
+
return {
|
|
35
|
+
...data,
|
|
36
|
+
metadata: {
|
|
37
|
+
...data.metadata,
|
|
38
|
+
queryTime: elapsed,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.name === "AbortError") {
|
|
44
|
+
return { error: "Query timeout exceeded" };
|
|
45
|
+
}
|
|
46
|
+
return { error: error.message || "Unknown error" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async testConnection() {
|
|
50
|
+
try {
|
|
51
|
+
const result = await this.execute("SELECT 1 as test");
|
|
52
|
+
return !result.error;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ResultFormatter } from "./formatter.js";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
export const specialCommands = [
|
|
4
|
+
{
|
|
5
|
+
name: ".tables",
|
|
6
|
+
description: "List all tables",
|
|
7
|
+
execute: async (client) => {
|
|
8
|
+
const result = await client.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()");
|
|
9
|
+
return ResultFormatter.formatResult(result);
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: ".describe",
|
|
14
|
+
description: "Describe a table (.describe table_name)",
|
|
15
|
+
execute: async (client, args) => {
|
|
16
|
+
if (!args) {
|
|
17
|
+
return chalk.yellow("Usage: .describe <table_name>");
|
|
18
|
+
}
|
|
19
|
+
const result = await client.execute(`DESCRIBE ${args}`);
|
|
20
|
+
return ResultFormatter.formatResult(result);
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: ".schema",
|
|
25
|
+
description: "Show database schema",
|
|
26
|
+
execute: async (client) => {
|
|
27
|
+
const result = await client.execute("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = DATABASE() ORDER BY table_name, ordinal_position");
|
|
28
|
+
return ResultFormatter.formatResult(result);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: ".help",
|
|
33
|
+
description: "Show available commands",
|
|
34
|
+
execute: async () => {
|
|
35
|
+
const helpText = [
|
|
36
|
+
chalk.bold("Special Commands:"),
|
|
37
|
+
...specialCommands.map((cmd) => ` ${chalk.cyan(cmd.name.padEnd(15))} ${cmd.description}`),
|
|
38
|
+
"",
|
|
39
|
+
chalk.bold("Other:"),
|
|
40
|
+
` ${chalk.cyan("exit, quit".padEnd(15))} Exit the CLI`,
|
|
41
|
+
` ${chalk.cyan("Ctrl+C".padEnd(15))} Cancel current query`,
|
|
42
|
+
` ${chalk.cyan("Ctrl+D".padEnd(15))} Exit the CLI`,
|
|
43
|
+
` ${chalk.cyan("↑/↓".padEnd(15))} Navigate command history`,
|
|
44
|
+
];
|
|
45
|
+
return helpText.join("\n");
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
export function parseCommand(input) {
|
|
50
|
+
const trimmed = input.trim();
|
|
51
|
+
if (trimmed.startsWith(".")) {
|
|
52
|
+
const parts = trimmed.split(/\s+/);
|
|
53
|
+
const cmdName = parts[0];
|
|
54
|
+
const args = parts.slice(1).join(" ");
|
|
55
|
+
const command = specialCommands.find((c) => c.name === cmdName);
|
|
56
|
+
return { command, args, isSpecial: true };
|
|
57
|
+
}
|
|
58
|
+
return { isSpecial: false };
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sql } from "@forge/sql";
|
|
2
|
+
const executeSql = async (req) => {
|
|
3
|
+
console.log("\n=== Executing Custom SQL Query ===");
|
|
4
|
+
const payload = req.body;
|
|
5
|
+
let sqlRequest = null;
|
|
6
|
+
let query;
|
|
7
|
+
try {
|
|
8
|
+
sqlRequest = JSON.parse(payload);
|
|
9
|
+
query = sqlRequest?.query;
|
|
10
|
+
if (!query) {
|
|
11
|
+
return getHttpResponse(400, {
|
|
12
|
+
success: false,
|
|
13
|
+
error: "No SQL query provided",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
console.log("Executing query:", query);
|
|
17
|
+
// Import sql directly for custom queries
|
|
18
|
+
const result = await sql.executeRaw(query);
|
|
19
|
+
console.log("Query result:", result);
|
|
20
|
+
return getHttpResponse(200, {
|
|
21
|
+
success: true,
|
|
22
|
+
rows: result.rows || [],
|
|
23
|
+
rowCount: result.rows?.length || 0,
|
|
24
|
+
query,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(error);
|
|
29
|
+
console.error("Error while executing sql", { error });
|
|
30
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
31
|
+
return getHttpResponse(500, {
|
|
32
|
+
success: false,
|
|
33
|
+
error: errorMessage,
|
|
34
|
+
...(query && { query }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function getHttpResponse(statusCode, body) {
|
|
39
|
+
const statusTexts = {
|
|
40
|
+
200: "OK",
|
|
41
|
+
400: "Bad Request",
|
|
42
|
+
404: "Not Found",
|
|
43
|
+
500: "Internal Server Error",
|
|
44
|
+
};
|
|
45
|
+
const statusText = statusTexts[statusCode] || "Bad Request";
|
|
46
|
+
return {
|
|
47
|
+
headers: { "Content-Type": ["application/json"] },
|
|
48
|
+
statusCode,
|
|
49
|
+
statusText,
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export { executeSql };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
export class ResultFormatter {
|
|
4
|
+
static formatTable(rows) {
|
|
5
|
+
if (!rows || rows.length === 0) {
|
|
6
|
+
return chalk.yellow("(0 rows)");
|
|
7
|
+
}
|
|
8
|
+
const headers = Object.keys(rows[0]);
|
|
9
|
+
const table = new Table({
|
|
10
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
11
|
+
style: {
|
|
12
|
+
head: [],
|
|
13
|
+
border: ["grey"],
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
rows.forEach((row) => {
|
|
17
|
+
table.push(headers.map((h) => this.formatValue(row[h])));
|
|
18
|
+
});
|
|
19
|
+
return (table.toString() +
|
|
20
|
+
"\n" +
|
|
21
|
+
chalk.gray(`(${rows.length} row${rows.length !== 1 ? "s" : ""})`));
|
|
22
|
+
}
|
|
23
|
+
static formatValue(value) {
|
|
24
|
+
if (value === null || value === undefined) {
|
|
25
|
+
return chalk.gray("NULL");
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "boolean") {
|
|
28
|
+
return value ? chalk.green("true") : chalk.red("false");
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
return chalk.yellow(value.toString());
|
|
32
|
+
}
|
|
33
|
+
return value.toString();
|
|
34
|
+
}
|
|
35
|
+
static formatError(error) {
|
|
36
|
+
return chalk.red("✗ Error: ") + error;
|
|
37
|
+
}
|
|
38
|
+
static formatSuccess(message) {
|
|
39
|
+
return chalk.green("✓ ") + message;
|
|
40
|
+
}
|
|
41
|
+
static formatResult(result) {
|
|
42
|
+
if (result.error) {
|
|
43
|
+
return this.formatError(result.error);
|
|
44
|
+
}
|
|
45
|
+
if (result.rows && result.rows.length > 0) {
|
|
46
|
+
return this.formatTable(result.rows);
|
|
47
|
+
}
|
|
48
|
+
if (result.affectedRows !== undefined) {
|
|
49
|
+
return this.formatSuccess(`${result.affectedRows} row(s) affected`);
|
|
50
|
+
}
|
|
51
|
+
return chalk.gray("Query executed successfully");
|
|
52
|
+
}
|
|
53
|
+
static formatQueryTime(ms) {
|
|
54
|
+
const seconds = (ms / 1000).toFixed(3);
|
|
55
|
+
return chalk.gray(`⏱ ${seconds}s`);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
export class History {
|
|
5
|
+
constructor(maxSize = 1000) {
|
|
6
|
+
this.history = [];
|
|
7
|
+
this.historyFile = path.join(os.homedir(), ".forge_sql_history");
|
|
8
|
+
this.maxSize = maxSize;
|
|
9
|
+
this.load();
|
|
10
|
+
}
|
|
11
|
+
load() {
|
|
12
|
+
try {
|
|
13
|
+
if (fs.existsSync(this.historyFile)) {
|
|
14
|
+
const content = fs.readFileSync(this.historyFile, "utf-8");
|
|
15
|
+
this.history = content.split("\n").filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error("Failed to load history:", error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
save() {
|
|
23
|
+
try {
|
|
24
|
+
// Keep only last maxSize entries
|
|
25
|
+
const toSave = this.history.slice(-this.maxSize);
|
|
26
|
+
fs.writeFileSync(this.historyFile, toSave.join("\n"));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error("Failed to save history:", error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
add(command) {
|
|
33
|
+
const trimmed = command.trim();
|
|
34
|
+
if (trimmed && trimmed !== this.history[this.history.length - 1]) {
|
|
35
|
+
this.history.push(trimmed);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getAll() {
|
|
39
|
+
return [...this.history];
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { ForgeClient } from "./client.js";
|
|
4
|
+
import { ResultFormatter } from "./formatter.js";
|
|
5
|
+
import { History } from "./history.js";
|
|
6
|
+
import { parseCommand } from "./commands.js";
|
|
7
|
+
export { ForgeClient } from "./client.js";
|
|
8
|
+
export { executeSql } from "./execute-sql.js";
|
|
9
|
+
const getPrimaryPrompt = () => chalk.green("fsql> ");
|
|
10
|
+
const getMultilinePrompt = () => chalk.green(" ...> ");
|
|
11
|
+
export class ForgeSqlCli {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.multilineBuffer = "";
|
|
14
|
+
this.isMultiline = false;
|
|
15
|
+
const url = config.url || process.env.FORGE_SQL_URL;
|
|
16
|
+
if (!url) {
|
|
17
|
+
console.error(chalk.red("Error: FORGE_SQL_URL not configured"));
|
|
18
|
+
console.error(chalk.yellow("Set it via environment variable or .env file"));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
this.client = new ForgeClient({
|
|
22
|
+
url,
|
|
23
|
+
});
|
|
24
|
+
this.history = new History();
|
|
25
|
+
this.rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout,
|
|
28
|
+
prompt: getPrimaryPrompt(),
|
|
29
|
+
history: this.history.getAll().reverse(),
|
|
30
|
+
historySize: 1000,
|
|
31
|
+
});
|
|
32
|
+
this.setupHandlers();
|
|
33
|
+
}
|
|
34
|
+
setupHandlers() {
|
|
35
|
+
this.rl.on("line", (line) => this.handleLine(line));
|
|
36
|
+
this.rl.on("close", () => this.handleClose());
|
|
37
|
+
// Handle Ctrl+C
|
|
38
|
+
this.rl.on("SIGINT", () => {
|
|
39
|
+
if (this.isMultiline) {
|
|
40
|
+
console.log("\n" + chalk.yellow("Multi-line input cancelled"));
|
|
41
|
+
this.multilineBuffer = "";
|
|
42
|
+
this.isMultiline = false;
|
|
43
|
+
this.rl.setPrompt(getPrimaryPrompt());
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log("\n" + chalk.gray("(Use .exit, exit, or Ctrl+D to quit)"));
|
|
47
|
+
}
|
|
48
|
+
this.rl.prompt();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async handleLine(line) {
|
|
52
|
+
const input = line.trim();
|
|
53
|
+
// Handle exit commands
|
|
54
|
+
if (input === "exit" || input === "quit" || input === ".exit") {
|
|
55
|
+
this.rl.close();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Handle empty input
|
|
59
|
+
if (!input) {
|
|
60
|
+
this.rl.prompt();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Check for multi-line SQL (doesn't end with semicolon)
|
|
64
|
+
if (!this.isMultiline && !input.endsWith(";") && !input.startsWith(".")) {
|
|
65
|
+
this.isMultiline = true;
|
|
66
|
+
this.multilineBuffer = input;
|
|
67
|
+
this.rl.setPrompt(getMultilinePrompt());
|
|
68
|
+
this.rl.prompt();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Continue multi-line
|
|
72
|
+
if (this.isMultiline) {
|
|
73
|
+
this.multilineBuffer += "\n" + input;
|
|
74
|
+
if (input.endsWith(";")) {
|
|
75
|
+
const fullSql = this.multilineBuffer;
|
|
76
|
+
this.multilineBuffer = "";
|
|
77
|
+
this.isMultiline = false;
|
|
78
|
+
this.rl.setPrompt(getPrimaryPrompt());
|
|
79
|
+
await this.executeCommand(fullSql);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.rl.prompt();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await this.executeCommand(input);
|
|
88
|
+
}
|
|
89
|
+
this.rl.prompt();
|
|
90
|
+
}
|
|
91
|
+
async executeCommand(input) {
|
|
92
|
+
this.history.add(input);
|
|
93
|
+
// Check for special commands
|
|
94
|
+
const { command, args, isSpecial } = parseCommand(input);
|
|
95
|
+
if (isSpecial) {
|
|
96
|
+
if (command) {
|
|
97
|
+
const result = await command.execute(this.client, args);
|
|
98
|
+
console.log(result);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(chalk.red("Unknown command. Type .help for available commands"));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Execute SQL
|
|
106
|
+
const result = await this.client.execute(input);
|
|
107
|
+
console.log(ResultFormatter.formatResult(result));
|
|
108
|
+
if (result.metadata?.queryTime) {
|
|
109
|
+
console.log(ResultFormatter.formatQueryTime(result.metadata.queryTime));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
handleClose() {
|
|
113
|
+
this.history.save();
|
|
114
|
+
console.log(chalk.gray("\nGoodbye!"));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
async start() {
|
|
118
|
+
console.log(chalk.bold.blue("Forge FSQL CLI"));
|
|
119
|
+
console.log(chalk.gray("Type .help for commands, exit to quit"));
|
|
120
|
+
console.log(chalk.gray("=".repeat(50)));
|
|
121
|
+
// Test connection
|
|
122
|
+
process.stdout.write("Testing connection... ");
|
|
123
|
+
const connected = await this.client.testConnection();
|
|
124
|
+
if (connected) {
|
|
125
|
+
console.log(chalk.green("✓ Connected"));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(chalk.red("✗ Connection failed"));
|
|
129
|
+
console.log(chalk.yellow("Check your FORGE_SQL_URL configuration"));
|
|
130
|
+
}
|
|
131
|
+
console.log("");
|
|
132
|
+
this.rl.prompt();
|
|
133
|
+
}
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forge-fsql",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Interactive SQL CLI for Atlassian Forge SQL via web triggers",
|
|
6
|
+
"main": "dist/cjs/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"fsql": "bin/fsql",
|
|
11
|
+
"fsql-setup": "bin/setup"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/cjs/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
|
27
|
+
"dev": "ts-node src/index.ts",
|
|
28
|
+
"start": "node dist/index.js",
|
|
29
|
+
"fsql": "node ./bin/fsql",
|
|
30
|
+
"lint": "pnpm lint:eslint && pnpm lint:prettier",
|
|
31
|
+
"lint:eslint": "eslint '**/*.{ts,tsx,js,jsx}' --ignore-pattern '**/eslint.config.js'",
|
|
32
|
+
"lint:prettier": "prettier '**/*.{ts,tsx,js,jsx,json,md,yaml,yml}' --check || echo '⚠️ Warning: Prettier formatting issues found. Run \"pnpm fixstyle\" to fix them.'",
|
|
33
|
+
"fixall": "pnpm fixstyle && pnpm fixsrc",
|
|
34
|
+
"fixsrc": "eslint '**/*.{ts,tsx,js,jsx}' --ignore-pattern '**/eslint.config.js' --fix",
|
|
35
|
+
"fixstyle": "prettier '**/*.{ts,tsx,js,jsx,json,md,yaml,yml}' --write",
|
|
36
|
+
"prepare": "husky"
|
|
37
|
+
},
|
|
38
|
+
"lint-staged": {
|
|
39
|
+
"**/*.{ts,tsx,js,jsx,json,md,yaml,yml}": [
|
|
40
|
+
"pnpm fixstyle",
|
|
41
|
+
"pnpm run lint:eslint"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"forge",
|
|
46
|
+
"sql",
|
|
47
|
+
"cli",
|
|
48
|
+
"atlassian"
|
|
49
|
+
],
|
|
50
|
+
"author": "Chris Hatch",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@forge/sql": "^3.0.14",
|
|
54
|
+
"chalk": "^4.1.2",
|
|
55
|
+
"cli-table3": "^0.6.3",
|
|
56
|
+
"dotenv": "^16.0.3",
|
|
57
|
+
"js-yaml": "^4.1.1",
|
|
58
|
+
"node-fetch": "^2.7.0",
|
|
59
|
+
"readline": "^1.3.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
63
|
+
"@eslint/js": "^9.39.1",
|
|
64
|
+
"@types/js-yaml": "^4.0.9",
|
|
65
|
+
"@types/node": "^22.19.1",
|
|
66
|
+
"@types/node-fetch": "^2.6.11",
|
|
67
|
+
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
|
68
|
+
"@typescript-eslint/parser": "^8.48.0",
|
|
69
|
+
"eslint": "9.39.1",
|
|
70
|
+
"eslint-config-prettier": "^9.1.2",
|
|
71
|
+
"eslint-plugin-unused-imports": "^4.3.0",
|
|
72
|
+
"globals": "^15.15.0",
|
|
73
|
+
"husky": "^9.1.7",
|
|
74
|
+
"lint-staged": "^16.2.7",
|
|
75
|
+
"prettier": "^3.7.3",
|
|
76
|
+
"ts-node": "^10.9.1",
|
|
77
|
+
"typescript": "^5.9.3",
|
|
78
|
+
"typescript-eslint": "^8.48.0"
|
|
79
|
+
}
|
|
80
|
+
}
|