forge-fsql 1.0.4 → 1.1.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/README.md CHANGED
@@ -4,49 +4,33 @@ Interactive command-line interface for querying Atlassian Forge SQL databases vi
4
4
 
5
5
  ## Features
6
6
 
7
- - šŸŽØ Beautiful table formatting with colors
8
- - šŸ“ Multi-line SQL support
9
- - āŒØļø Command history (↑/↓ arrows)
7
+ - šŸŽØ Table formatting with colors
10
8
  - ⚔ Special commands (.tables, .describe, .schema)
11
- - ā±ļø Query timing
9
+ - āŒØļø Command history (↑/↓ arrows)
12
10
  - šŸ’¾ Persistent history across sessions
11
+ - ā±ļø Query timing
12
+ - šŸ“ Multi-line SQL support
13
13
 
14
14
  ## Installation
15
15
 
16
16
  ### In Your Forge Project
17
17
 
18
18
  ```sh
19
- npm install -D forge-fsql
19
+ npm install -g forge-fsql
20
20
 
21
- # add webtrigger to manifest.yml and a wrapper module for the corresponding function
22
- node_modules/.bin/fsql-setup
23
-
24
- # deploy with the webtrigger
25
- forge deploy
26
-
27
- # get trigger url:
28
- forge webtrigger create --product Confluence --site <site>.atlassian.net --functionKey execute-sql
21
+ fsql-setup
29
22
  ```
30
23
 
31
- Add to your `package.json` scripts:
24
+ Notes:
32
25
 
33
- ```json
34
- {
35
- "scripts": {
36
- "fsql": "fsql"
37
- }
38
- }
39
- ```
26
+ - creates a webtrigger in your manifest.yml
27
+ - creates a module at src/fsql.ts for the webtrigger function
28
+ - deploys the project with the new manifest
29
+ - creates the webtrigger with `forge webtrigger create`
30
+ - adds the webtrigger URL to a FORGE_SQL_WEBTRIGGER environment variable in .env
40
31
 
41
32
  ## Run
42
33
 
43
34
  ```sh
44
- # set URL using value from previous step
45
- export FORGE_SQL_URL=https://your-trigger-url.com
46
-
47
- # run fsql!
48
- npm run fsql
49
-
50
- # or
51
- npm run fsql --url https://your-trigger-url.com
35
+ fsql
52
36
  ```
package/bin/setup.js ADDED
@@ -0,0 +1,468 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { createRequire } from "module";
6
+ import { fileURLToPath } from "url";
7
+ import { spawn } from "child_process";
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ import prompts from "prompts";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const pkgRoot = path.join(__dirname, "..");
15
+ const templatesDir = path.join(pkgRoot, "templates");
16
+
17
+ // Support both ESM and CJS imports for js-yaml
18
+ const require = createRequire(import.meta.url);
19
+ const YAML = require("yaml");
20
+
21
+ const projectRoot = process.cwd();
22
+
23
+ async function main() {
24
+ console.log(chalk.bold.blue("\nšŸš€ Forge SQL CLI Setup\n"));
25
+
26
+ // Detect consumer project type
27
+ let isEsm = false;
28
+ let isTypeScript = fs.existsSync(path.join(projectRoot, "tsconfig.json"));
29
+
30
+ try {
31
+ const pkgPath = path.join(projectRoot, "package.json");
32
+ if (fs.existsSync(pkgPath)) {
33
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
34
+ if (pkg.type === "module") {
35
+ isEsm = true;
36
+ }
37
+
38
+ // Check for @forge/sql dependency
39
+ const hasForgeSql =
40
+ (pkg.dependencies && pkg.dependencies["@forge/sql"]) ||
41
+ (pkg.devDependencies && pkg.devDependencies["@forge/sql"]);
42
+
43
+ if (!hasForgeSql) {
44
+ console.error(chalk.red("\nāŒ Error: @forge/sql is not installed."));
45
+ console.log(
46
+ chalk.yellow(
47
+ "This tool requires @forge/sql to be present in your project.",
48
+ ),
49
+ );
50
+ console.log("Please install it by running:");
51
+ console.log(chalk.blue("\n npm install @forge/sql"));
52
+ console.log(" # or");
53
+ console.log(chalk.blue(" yarn add @forge/sql\n"));
54
+ process.exit(1);
55
+ }
56
+ } else {
57
+ console.error(chalk.red("\nāŒ Error: package.json not found."));
58
+ console.log(
59
+ chalk.yellow(
60
+ "This tool requires a valid Node.js project with @forge/sql installed.",
61
+ ),
62
+ );
63
+ process.exit(1);
64
+ }
65
+ } catch (error) {
66
+ console.error(chalk.yellow("Warning: Could not read package.json:"), error);
67
+ }
68
+
69
+ // 1. Detect manifest.yaml or manifest.yml
70
+ let manifestPath = path.join(projectRoot, "manifest.yml");
71
+ if (!fs.existsSync(manifestPath)) {
72
+ manifestPath = path.join(projectRoot, "manifest.yaml");
73
+ }
74
+
75
+ if (!fs.existsSync(manifestPath)) {
76
+ console.error(
77
+ chalk.red(
78
+ "Error: Could not find manifest.yml or manifest.yaml in the current directory.",
79
+ ),
80
+ );
81
+ process.exit(1);
82
+ }
83
+
84
+ // 2. Prompt for fsql execution function path
85
+ const extension = isTypeScript ? "ts" : isEsm ? "js" : "js";
86
+ const defaultPath = `src/fsql.${extension}`;
87
+
88
+ const response = await prompts({
89
+ type: "text",
90
+ name: "fsqlPath",
91
+ message: "Where should the SQL execution function be created?",
92
+ initial: defaultPath,
93
+ });
94
+
95
+ if (!response.fsqlPath) {
96
+ console.log(chalk.yellow("Setup cancelled."));
97
+ process.exit(0);
98
+ }
99
+
100
+ const fsqlRelPath = response.fsqlPath;
101
+ const fsqlAbsPath = path.resolve(projectRoot, fsqlRelPath);
102
+ const srcSameDir = path.dirname(fsqlAbsPath);
103
+
104
+ // Spinner for file creation
105
+ const spinner = ora("Creating function file...").start();
106
+
107
+ if (!fs.existsSync(srcSameDir)) {
108
+ fs.mkdirSync(srcSameDir, { recursive: true });
109
+ }
110
+
111
+ let templateFile = "";
112
+ if (isTypeScript) {
113
+ templateFile = "execute-sql.ts";
114
+ } else if (isEsm) {
115
+ templateFile = "execute-sql.js";
116
+ } else {
117
+ templateFile = "execute-sql.cjs";
118
+ }
119
+
120
+ const fsqlContent = fs.readFileSync(
121
+ path.join(templatesDir, templateFile),
122
+ "utf8",
123
+ );
124
+
125
+ fs.writeFileSync(fsqlAbsPath, fsqlContent);
126
+ spinner.succeed(`Created ${fsqlRelPath}`);
127
+
128
+ // 3. Update manifest
129
+ spinner.start("Updating manifest.yml...");
130
+
131
+ let doc;
132
+ try {
133
+ const fileContents = fs.readFileSync(manifestPath, "utf8");
134
+ doc = YAML.parseDocument(fileContents);
135
+ } catch (e) {
136
+ spinner.fail("Error reading manifest");
137
+ console.error(e);
138
+ process.exit(1);
139
+ }
140
+
141
+ if (!doc.contents) {
142
+ doc.contents = doc.createNode({});
143
+ }
144
+
145
+ if (!doc.has("modules")) {
146
+ doc.set("modules", doc.createNode({}));
147
+ }
148
+
149
+ const modules = doc.get("modules");
150
+
151
+ const functionKey = "executeSql";
152
+ if (!modules.has("function")) {
153
+ modules.set("function", doc.createNode([]));
154
+ }
155
+
156
+ let functions = modules.get("function");
157
+
158
+ if (!YAML.isSeq(functions)) {
159
+ const obj = functions.toJSON();
160
+ functions = doc.createNode(
161
+ Object.entries(obj).map(([key, val]) => ({ key, ...val })),
162
+ );
163
+ modules.set("function", functions);
164
+ }
165
+
166
+ // Determine handler path
167
+ // Standard Forge pattern: filename without extension + .functionName
168
+ // We need to be careful with paths.
169
+ // If user chooses src/foo/bar.ts, handler is src/foo/bar.executeSql
170
+ // If user chooses modules/bar.js, handler is modules/bar.executeSql
171
+
172
+ // 145. Determine handler path
173
+ let handlerPath = fsqlRelPath.replace(/\.(ts|js|cjs|mjs)$/, "");
174
+ // Ensure we use forward slashes for handler paths
175
+ handlerPath = handlerPath.split(path.sep).join("/");
176
+
177
+ // Logic to detect if we should strip 'src/' from the handler path
178
+ // This happens if the project uses 'src' as the root for handlers (common in Forge)
179
+ // We check if 'src/index.ts' exists but 'index.ts' does not, AND if there is a handler 'index.handler'
180
+ // Or simply, if the user created the file in 'src/' but 'src/' path is redundant for handlers.
181
+
182
+ // Heuristic: Check existing function handlers
183
+ let srcIsRoot = false;
184
+ try {
185
+ if (functions && functions.items && functions.items.length > 0) {
186
+ for (const f of functions.items) {
187
+ const fJson = f.toJSON();
188
+ if (fJson && fJson.handler) {
189
+ const h = fJson.handler.split(".")[0]; // e.g. "index" or "consumers/ingestion-consumer"
190
+ const possibleSrcPath = path.join(projectRoot, "src", h + ".ts"); // simple check for .ts
191
+ const possibleRootPath = path.join(projectRoot, h + ".ts");
192
+
193
+ if (
194
+ fs.existsSync(possibleSrcPath) &&
195
+ !fs.existsSync(possibleRootPath)
196
+ ) {
197
+ srcIsRoot = true;
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ } else {
203
+ // Fallback: if index.ts is in src but not root
204
+ if (
205
+ fs.existsSync(path.join(projectRoot, "src", "index.ts")) &&
206
+ !fs.existsSync(path.join(projectRoot, "index.ts"))
207
+ ) {
208
+ srcIsRoot = true;
209
+ }
210
+ }
211
+ } catch {
212
+ // ignore
213
+ }
214
+
215
+ if (srcIsRoot && handlerPath.startsWith("src/")) {
216
+ handlerPath = handlerPath.substring(4); // remove "src/"
217
+ }
218
+
219
+ const handlerName = `${handlerPath}.executeSql`;
220
+
221
+ let functionExists = functions.items.find((f) => {
222
+ const js = f.toJSON();
223
+ return js && js.key === functionKey;
224
+ });
225
+
226
+ if (!functionExists) {
227
+ functions.add(
228
+ doc.createNode({
229
+ key: functionKey,
230
+ handler: handlerName,
231
+ }),
232
+ );
233
+ } else {
234
+ if (YAML.isMap(functionExists)) {
235
+ functionExists.set("handler", handlerName);
236
+ } else {
237
+ const idx = functions.items.indexOf(functionExists);
238
+ functions.set(
239
+ idx,
240
+ doc.createNode({ key: functionKey, handler: handlerName }),
241
+ );
242
+ }
243
+ }
244
+
245
+ const webtriggerKey = "execute-sql";
246
+ if (!modules.has("webtrigger")) {
247
+ modules.set("webtrigger", doc.createNode([]));
248
+ }
249
+
250
+ let webtriggers = modules.get("webtrigger");
251
+ if (!YAML.isSeq(webtriggers)) {
252
+ const obj = webtriggers.toJSON();
253
+ webtriggers = doc.createNode(
254
+ Object.entries(obj).map(([key, val]) => ({ key, ...val })),
255
+ );
256
+ modules.set("webtrigger", webtriggers);
257
+ }
258
+
259
+ let webtriggerExists = webtriggers.items.find((w) => {
260
+ const js = w.toJSON();
261
+ return js && js.key === webtriggerKey;
262
+ });
263
+
264
+ if (!webtriggerExists) {
265
+ webtriggers.add(
266
+ doc.createNode({
267
+ key: webtriggerKey,
268
+ function: functionKey,
269
+ }),
270
+ );
271
+ } else {
272
+ if (YAML.isMap(webtriggerExists)) {
273
+ webtriggerExists.set("function", functionKey);
274
+ } else {
275
+ const idx = webtriggers.items.indexOf(webtriggerExists);
276
+ webtriggers.set(
277
+ idx,
278
+ doc.createNode({ key: webtriggerKey, function: functionKey }),
279
+ );
280
+ }
281
+ }
282
+
283
+ try {
284
+ fs.writeFileSync(manifestPath, doc.toString());
285
+ spinner.succeed("Updated manifest file");
286
+ } catch (e) {
287
+ spinner.fail("Error writing manifest");
288
+ console.error(e);
289
+ process.exit(1);
290
+ }
291
+
292
+ // 4. Prompt for deployment
293
+ console.log(); // Add newline for better UX
294
+ const deployResponse = await prompts({
295
+ type: "text",
296
+ name: "cmd",
297
+ message:
298
+ "Command to deploy your app (required before creating webtrigger):",
299
+ initial: "forge deploy",
300
+ });
301
+
302
+ if (!deployResponse.cmd) {
303
+ console.log(chalk.yellow("Deployment cancelled."));
304
+ process.exit(0);
305
+ }
306
+
307
+ const deployCmd = deployResponse.cmd;
308
+
309
+ console.log(chalk.bold.blue(`\nRunning ${deployCmd}...\n`));
310
+
311
+ await new Promise((resolve) => {
312
+ const child = spawn(deployCmd, {
313
+ stdio: "inherit",
314
+ shell: true,
315
+ });
316
+
317
+ child.on("close", (code) => {
318
+ if (code !== 0) {
319
+ console.error(chalk.red(`\nDeploy failed with exit code ${code}.`));
320
+ process.exit(code);
321
+ }
322
+ resolve();
323
+ });
324
+ });
325
+
326
+ // 5. Run forge webtrigger create
327
+ let webtriggerArgs = ["webtrigger", "create", "--functionKey", "execute-sql"];
328
+
329
+ // Try to detect existing installation
330
+ try {
331
+ const listProc = spawn("forge", ["install", "list", "--json"], {
332
+ shell: true,
333
+ });
334
+ let listOutput = "";
335
+ await new Promise((resolve) => {
336
+ listProc.stdout.on("data", (d) => (listOutput += d.toString()));
337
+ listProc.on("close", resolve);
338
+ });
339
+
340
+ // Attempt to extract JSON
341
+ const jsonStart = listOutput.indexOf("[");
342
+ const jsonEnd = listOutput.lastIndexOf("]");
343
+ if (jsonStart !== -1 && jsonEnd !== -1) {
344
+ const installations = JSON.parse(
345
+ listOutput.substring(jsonStart, jsonEnd + 1),
346
+ );
347
+ const devInst = installations.find(
348
+ (i) => i.environment === "development",
349
+ );
350
+
351
+ if (devInst) {
352
+ console.log();
353
+ const useInst = await prompts({
354
+ type: "confirm",
355
+ name: "value",
356
+ message: `Found existing installation for ${chalk.cyan(devInst.product)} at ${chalk.cyan(devInst.site)}. Use this?`,
357
+ initial: true,
358
+ });
359
+
360
+ if (useInst.value) {
361
+ webtriggerArgs.push("--site", devInst.site);
362
+ webtriggerArgs.push("--product", devInst.product);
363
+ }
364
+ }
365
+ }
366
+ } catch {
367
+ // Ignore errors in auto-detection
368
+ }
369
+
370
+ console.log(
371
+ chalk.bold.blue(
372
+ "\nCreating webtrigger... (Please follow prompts if asked)\n",
373
+ ),
374
+ );
375
+
376
+ const separator = chalk.gray(
377
+ "==================================================",
378
+ );
379
+ console.log(separator);
380
+
381
+ const forgeCmd = spawn("forge", webtriggerArgs, {
382
+ stdio: ["inherit", "pipe", "inherit"],
383
+ shell: true,
384
+ });
385
+
386
+ let stdoutData = "";
387
+
388
+ forgeCmd.stdout.on("data", (data) => {
389
+ process.stdout.write(data);
390
+ stdoutData += data.toString();
391
+ });
392
+
393
+ forgeCmd.on("close", (code) => {
394
+ console.log(separator);
395
+
396
+ if (code !== 0) {
397
+ console.log(
398
+ chalk.yellow(
399
+ `\nForge command exited with code ${code}. If you cancelled or it failed, you may need to run it manually.`,
400
+ ),
401
+ );
402
+ }
403
+
404
+ // Capture URL
405
+ const urlRegex =
406
+ /https:\/\/[a-zA-Z0-9-.]+\.atlassian-dev\.net\/[a-zA-Z0-9/-]+/;
407
+ const matches = stdoutData.match(urlRegex);
408
+
409
+ if (matches) {
410
+ const url = matches[0];
411
+ console.log(chalk.green(`\nFound Webtrigger URL: ${url}`));
412
+ updateEnv(url);
413
+ } else {
414
+ console.log(
415
+ chalk.yellow(
416
+ "\nCould not automatically find Webtrigger URL in output.",
417
+ ),
418
+ );
419
+ console.log(
420
+ "If created, please add the URL to your .env file as FORGE_SQL_WEBTRIGGER=<url>",
421
+ );
422
+ finish();
423
+ }
424
+ });
425
+ }
426
+
427
+ function updateEnv(url) {
428
+ console.log();
429
+ const spinner = ora("Updating .env file...").start();
430
+ const envPath = path.join(projectRoot, ".env");
431
+ const envVar = `FORGE_SQL_WEBTRIGGER=${url}`;
432
+
433
+ let envContent = "";
434
+ if (fs.existsSync(envPath)) {
435
+ envContent = fs.readFileSync(envPath, "utf8");
436
+ if (!envContent.endsWith("\n") && envContent.length > 0) {
437
+ envContent += "\n";
438
+ }
439
+ }
440
+
441
+ // Check if already exists
442
+ if (envContent.includes("FORGE_SQL_WEBTRIGGER=")) {
443
+ envContent = envContent.replace(
444
+ /FORGE_SQL_WEBTRIGGER=.*(\n|$)/,
445
+ `${envVar}\n`,
446
+ );
447
+ } else {
448
+ envContent += `${envVar}\n`;
449
+ }
450
+
451
+ fs.writeFileSync(envPath, envContent);
452
+ spinner.succeed("Updated .env with Webtrigger URL");
453
+ finish();
454
+ }
455
+
456
+ function finish() {
457
+ const msg = " āœ” Setup completed successfully! ";
458
+ const line = "─".repeat(msg.length + 1);
459
+ console.log(chalk.bold.green(`\nā”Œ${line}┐`));
460
+ console.log(chalk.bold.green(`│${msg}│`));
461
+ console.log(chalk.bold.green(`ā””${line}ā”˜\n`));
462
+ console.log(`Run ${chalk.cyan("'fsql'")} to start.\n`);
463
+ }
464
+
465
+ main().catch((err) => {
466
+ console.error(chalk.red("Unexpected error:"), err);
467
+ process.exit(1);
468
+ });
package/dist/cjs/index.js CHANGED
@@ -53,9 +53,9 @@ class ForgeSqlCli {
53
53
  constructor(config) {
54
54
  this.multilineBuffer = "";
55
55
  this.isMultiline = false;
56
- const url = config.url || process.env.FORGE_SQL_URL;
56
+ const url = config.url || process.env.FORGE_SQL_WEBTRIGGER;
57
57
  if (!url) {
58
- console.error(chalk_1.default.red("Error: FORGE_SQL_URL not configured"));
58
+ console.error(chalk_1.default.red("Error: FORGE_SQL_WEBTRIGGER not configured"));
59
59
  console.error(chalk_1.default.yellow("Set it via environment variable or .env file"));
60
60
  process.exit(1);
61
61
  }
@@ -167,7 +167,7 @@ class ForgeSqlCli {
167
167
  }
168
168
  else {
169
169
  console.log(chalk_1.default.red("āœ— Connection failed"));
170
- console.log(chalk_1.default.yellow("Check your FORGE_SQL_URL configuration"));
170
+ console.log(chalk_1.default.yellow("Check your FORGE_SQL_WEBTRIGGER configuration"));
171
171
  }
172
172
  console.log("");
173
173
  this.rl.prompt();
package/dist/index.js CHANGED
@@ -12,9 +12,9 @@ export class ForgeSqlCli {
12
12
  constructor(config) {
13
13
  this.multilineBuffer = "";
14
14
  this.isMultiline = false;
15
- const url = config.url || process.env.FORGE_SQL_URL;
15
+ const url = config.url || process.env.FORGE_SQL_WEBTRIGGER;
16
16
  if (!url) {
17
- console.error(chalk.red("Error: FORGE_SQL_URL not configured"));
17
+ console.error(chalk.red("Error: FORGE_SQL_WEBTRIGGER not configured"));
18
18
  console.error(chalk.yellow("Set it via environment variable or .env file"));
19
19
  process.exit(1);
20
20
  }
@@ -126,7 +126,7 @@ export class ForgeSqlCli {
126
126
  }
127
127
  else {
128
128
  console.log(chalk.red("āœ— Connection failed"));
129
- console.log(chalk.yellow("Check your FORGE_SQL_URL configuration"));
129
+ console.log(chalk.yellow("Check your FORGE_SQL_WEBTRIGGER configuration"));
130
130
  }
131
131
  console.log("");
132
132
  this.rl.prompt();
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "forge-fsql",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Interactive SQL CLI for Atlassian Forge SQL via web triggers",
6
6
  "main": "dist/cjs/index.js",
7
7
  "module": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "bin": {
10
- "fsql": "bin/fsql",
11
- "fsql-setup": "bin/setup"
10
+ "fsql": "bin/fsql.js",
11
+ "fsql-setup": "bin/setup.js"
12
12
  },
13
13
  "exports": {
14
14
  ".": {
@@ -25,6 +25,7 @@
25
25
  "files": [
26
26
  "dist",
27
27
  "bin",
28
+ "templates",
28
29
  "README.md"
29
30
  ],
30
31
  "lint-staged": {
@@ -46,6 +47,8 @@
46
47
  "chalk": "^5.4.1",
47
48
  "cli-table3": "^0.6.3",
48
49
  "dotenv": "^16.0.3",
50
+ "ora": "^9.0.0",
51
+ "prompts": "^2.4.2",
49
52
  "yaml": "^2.8.2"
50
53
  },
51
54
  "devDependencies": {
@@ -69,7 +72,7 @@
69
72
  "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
70
73
  "dev": "ts-node src/index.ts",
71
74
  "start": "node dist/index.js",
72
- "fsql": "node ./bin/fsql",
75
+ "fsql": "node ./bin/fsql.js",
73
76
  "lint": "pnpm lint:eslint && pnpm lint:prettier",
74
77
  "lint:eslint": "eslint '**/*.{ts,tsx,js,jsx}' --ignore-pattern '**/eslint.config.js'",
75
78
  "lint:prettier": "prettier '**/*.{ts,tsx,js,jsx,json,md,yaml,yml}' --check || echo 'āš ļø Warning: Prettier formatting issues found. Run \"pnpm fixstyle\" to fix them.'",
@@ -0,0 +1,64 @@
1
+ const { sql } = require("@forge/sql");
2
+
3
+ const executeSql = async (req) => {
4
+ console.log("\n=== Executing Custom SQL Query ===");
5
+
6
+ const payload = req.body;
7
+ let sqlRequest = null;
8
+ let query;
9
+
10
+ try {
11
+ sqlRequest = JSON.parse(payload);
12
+ query = sqlRequest?.query;
13
+
14
+ if (!query) {
15
+ return getHttpResponse(400, {
16
+ success: false,
17
+ error: "No SQL query provided",
18
+ });
19
+ }
20
+
21
+ console.log("Executing query:", query);
22
+
23
+ const result = await sql.executeRaw(query);
24
+
25
+ console.log("Query result:", result);
26
+
27
+ return getHttpResponse(200, {
28
+ success: true,
29
+ rows: result.rows || [],
30
+ rowCount: result.rows?.length || 0,
31
+ query,
32
+ });
33
+ } catch (error) {
34
+ console.error(error);
35
+ console.error("Error while executing sql", { error });
36
+
37
+ const errorMessage = error instanceof Error ? error.message : String(error);
38
+
39
+ return getHttpResponse(500, {
40
+ success: false,
41
+ error: errorMessage,
42
+ ...(query && { query }),
43
+ });
44
+ }
45
+ };
46
+
47
+ function getHttpResponse(statusCode, body) {
48
+ const statusTexts = {
49
+ 200: "OK",
50
+ 400: "Bad Request",
51
+ 404: "Not Found",
52
+ 500: "Internal Server Error",
53
+ };
54
+ const statusText = statusTexts[statusCode] || "Bad Request";
55
+
56
+ return {
57
+ headers: { "Content-Type": ["application/json"] },
58
+ statusCode,
59
+ statusText,
60
+ body: JSON.stringify(body),
61
+ };
62
+ }
63
+
64
+ module.exports = { executeSql };
@@ -0,0 +1,64 @@
1
+ import { sql } from "@forge/sql";
2
+
3
+ const executeSql = async (req) => {
4
+ console.log("\n=== Executing Custom SQL Query ===");
5
+
6
+ const payload = req.body;
7
+ let sqlRequest = null;
8
+ let query;
9
+
10
+ try {
11
+ sqlRequest = JSON.parse(payload);
12
+ query = sqlRequest?.query;
13
+
14
+ if (!query) {
15
+ return getHttpResponse(400, {
16
+ success: false,
17
+ error: "No SQL query provided",
18
+ });
19
+ }
20
+
21
+ console.log("Executing query:", query);
22
+
23
+ const result = await sql.executeRaw(query);
24
+
25
+ console.log("Query result:", result);
26
+
27
+ return getHttpResponse(200, {
28
+ success: true,
29
+ rows: result.rows || [],
30
+ rowCount: result.rows?.length || 0,
31
+ query,
32
+ });
33
+ } catch (error) {
34
+ console.error(error);
35
+ console.error("Error while executing sql", { error });
36
+
37
+ const errorMessage = error instanceof Error ? error.message : String(error);
38
+
39
+ return getHttpResponse(500, {
40
+ success: false,
41
+ error: errorMessage,
42
+ ...(query && { query }),
43
+ });
44
+ }
45
+ };
46
+
47
+ function getHttpResponse(statusCode, body) {
48
+ const statusTexts = {
49
+ 200: "OK",
50
+ 400: "Bad Request",
51
+ 404: "Not Found",
52
+ 500: "Internal Server Error",
53
+ };
54
+ const statusText = statusTexts[statusCode] || "Bad Request";
55
+
56
+ return {
57
+ headers: { "Content-Type": ["application/json"] },
58
+ statusCode,
59
+ statusText,
60
+ body: JSON.stringify(body),
61
+ };
62
+ }
63
+
64
+ export { executeSql };
@@ -0,0 +1,75 @@
1
+ import { sql } from "@forge/sql";
2
+
3
+ const executeSql = async (req: {
4
+ body: string;
5
+ }): Promise<ReturnType<typeof getHttpResponse>> => {
6
+ console.log("\n=== Executing Custom SQL Query ===");
7
+
8
+ const payload = req.body;
9
+ let sqlRequest: { query?: string } | null = null;
10
+ let query: string | undefined;
11
+
12
+ try {
13
+ sqlRequest = JSON.parse(payload);
14
+ query = sqlRequest?.query;
15
+
16
+ if (!query) {
17
+ return getHttpResponse(400, {
18
+ success: false,
19
+ error: "No SQL query provided",
20
+ });
21
+ }
22
+
23
+ console.log("Executing query:", query);
24
+
25
+ // Import sql directly for custom queries
26
+ const result = await sql.executeRaw(query);
27
+
28
+ console.log("Query result:", result);
29
+
30
+ return getHttpResponse(200, {
31
+ success: true,
32
+ rows: result.rows || [],
33
+ rowCount: result.rows?.length || 0,
34
+ query,
35
+ });
36
+ } catch (error: unknown) {
37
+ console.error(error);
38
+ console.error("Error while executing sql", { error });
39
+
40
+ const errorMessage = error instanceof Error ? error.message : String(error);
41
+
42
+ return getHttpResponse(500, {
43
+ success: false,
44
+ error: errorMessage,
45
+ ...(query && { query }),
46
+ });
47
+ }
48
+ };
49
+
50
+ function getHttpResponse(
51
+ statusCode: number,
52
+ body: Record<string, unknown>,
53
+ ): {
54
+ headers: { "Content-Type": string[] };
55
+ statusCode: number;
56
+ statusText: string;
57
+ body: string;
58
+ } {
59
+ const statusTexts: Record<number, string> = {
60
+ 200: "OK",
61
+ 400: "Bad Request",
62
+ 404: "Not Found",
63
+ 500: "Internal Server Error",
64
+ };
65
+ const statusText = statusTexts[statusCode] || "Bad Request";
66
+
67
+ return {
68
+ headers: { "Content-Type": ["application/json"] },
69
+ statusCode,
70
+ statusText,
71
+ body: JSON.stringify(body),
72
+ };
73
+ }
74
+
75
+ export { executeSql };
package/bin/setup DELETED
@@ -1,193 +0,0 @@
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("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 doc;
71
- try {
72
- const fileContents = fs.readFileSync(manifestPath, "utf8");
73
- doc = YAML.parseDocument(fileContents);
74
- } catch (e) {
75
- console.error("Error reading manifest:", e);
76
- process.exit(1);
77
- }
78
-
79
- if (!doc.contents) {
80
- doc.contents = doc.createNode({});
81
- }
82
-
83
- if (!doc.has("modules")) {
84
- doc.set("modules", doc.createNode({}));
85
- }
86
-
87
- const modules = doc.get("modules");
88
-
89
- // Ensure function is an array
90
- const functionKey = "executeSql";
91
- if (!modules.has("function")) {
92
- modules.set("function", doc.createNode([]));
93
- }
94
-
95
- let functions = modules.get("function");
96
-
97
- // If it's somehow not a sequence, convert it (though unlikely in Forge)
98
- if (!YAML.isSeq(functions)) {
99
- const obj = functions.toJSON();
100
- functions = doc.createNode(
101
- Object.entries(obj).map(([key, val]) => ({ key, ...val })),
102
- );
103
- modules.set("function", functions);
104
- }
105
-
106
- const handlerName = "fsql.executeSql";
107
- let functionExists = functions.items.find((f) => {
108
- const js = f.toJSON();
109
- return js && js.key === functionKey;
110
- });
111
-
112
- if (!functionExists) {
113
- functions.add(
114
- doc.createNode({
115
- key: functionKey,
116
- handler: handlerName,
117
- }),
118
- );
119
- console.log(
120
- `āœ“ Added function:${functionKey} with handler ${handlerName} to manifest`,
121
- );
122
- } else {
123
- // If it's a Pair (map item) or just a Map in the sequence
124
- if (YAML.isMap(functionExists)) {
125
- functionExists.set("handler", handlerName);
126
- } else {
127
- // Should not happen with doc.createNode above, but for safety:
128
- const idx = functions.items.indexOf(functionExists);
129
- functions.set(
130
- idx,
131
- doc.createNode({ key: functionKey, handler: handlerName }),
132
- );
133
- }
134
- console.log(
135
- `āœ“ Updated function:${functionKey} handler to ${handlerName} in manifest`,
136
- );
137
- }
138
-
139
- // Ensure webtrigger is an array
140
- const webtriggerKey = "execute-sql";
141
- if (!modules.has("webtrigger")) {
142
- modules.set("webtrigger", doc.createNode([]));
143
- }
144
-
145
- let webtriggers = modules.get("webtrigger");
146
- if (!YAML.isSeq(webtriggers)) {
147
- const obj = webtriggers.toJSON();
148
- webtriggers = doc.createNode(
149
- Object.entries(obj).map(([key, val]) => ({ key, ...val })),
150
- );
151
- modules.set("webtrigger", webtriggers);
152
- }
153
-
154
- let webtriggerExists = webtriggers.items.find((w) => {
155
- const js = w.toJSON();
156
- return js && js.key === webtriggerKey;
157
- });
158
-
159
- if (!webtriggerExists) {
160
- webtriggers.add(
161
- doc.createNode({
162
- key: webtriggerKey,
163
- function: functionKey,
164
- }),
165
- );
166
- console.log(`āœ“ Added webtrigger:${webtriggerKey} to manifest`);
167
- } else {
168
- if (YAML.isMap(webtriggerExists)) {
169
- webtriggerExists.set("function", functionKey);
170
- } else {
171
- const idx = webtriggers.items.indexOf(webtriggerExists);
172
- webtriggers.set(
173
- idx,
174
- doc.createNode({ key: webtriggerKey, function: functionKey }),
175
- );
176
- }
177
- console.log(
178
- `āœ“ Ensured webtrigger:${webtriggerKey} points to function ${functionKey}`,
179
- );
180
- }
181
-
182
- try {
183
- fs.writeFileSync(manifestPath, doc.toString());
184
- console.log("āœ“ Updated manifest file successfully");
185
- } catch (e) {
186
- console.error("Error writing manifest:", e);
187
- process.exit(1);
188
- }
189
-
190
- console.log("\nSetup completed successfully!");
191
- console.log(
192
- "You can now use the forge-fsql CLI to interact with your Forge SQL database.",
193
- );
File without changes