forge-fsql 1.1.0 → 1.2.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
@@ -1,6 +1,10 @@
1
1
  # Forge FSQL CLI
2
2
 
3
- Interactive command-line interface for querying Atlassian Forge SQL databases via web triggers.
3
+ Interactive CLI for querying Atlassian Forge SQL databases via web triggers.
4
+
5
+ ## Demo
6
+
7
+ ![demo](demo-usage.gif)
4
8
 
5
9
  ## Features
6
10
 
@@ -11,6 +15,10 @@ Interactive command-line interface for querying Atlassian Forge SQL databases vi
11
15
  - ⏱️ Query timing
12
16
  - 📝 Multi-line SQL support
13
17
 
18
+ ## Security
19
+
20
+ - Disabled in Production - returns a 403 error if you attempt to call it
21
+
14
22
  ## Installation
15
23
 
16
24
  ### In Your Forge Project
@@ -26,7 +34,7 @@ Notes:
26
34
  - creates a webtrigger in your manifest.yml
27
35
  - creates a module at src/fsql.ts for the webtrigger function
28
36
  - deploys the project with the new manifest
29
- - creates the webtrigger with `forge webtrigger create`
37
+ - creates the webtrigger with `forge webtrigger create` (default environment which is `DEVELOPMENT` in a standard setup)
30
38
  - adds the webtrigger URL to a FORGE_SQL_WEBTRIGGER environment variable in .env
31
39
 
32
40
  ## Run
@@ -34,3 +42,14 @@ Notes:
34
42
  ```sh
35
43
  fsql
36
44
  ```
45
+
46
+ ## Upgrade
47
+
48
+ ```sh
49
+ # upgrade the CLI
50
+ > npm install -g forge-fsql@latest
51
+
52
+ # run the setup from the root of your project to pick up the new version
53
+ # it will install fsql.ts again and redeploy again
54
+ myforgeproject> fsql-setup
55
+ ```
package/bin/build ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ # Build templates from TypeScript source
5
+ ts-node scripts/build-templates.ts
6
+
7
+ # Format generated files
8
+ pnpm fixstyle
9
+
10
+ # Compile TypeScript (ESM)
11
+ tsc
12
+
13
+ # Compile TypeScript (CommonJS)
14
+ tsc -p tsconfig.cjs.json
15
+ echo '{"type": "commonjs"}' > dist/cjs/package.json
package/bin/setup.js CHANGED
@@ -23,7 +23,26 @@ const projectRoot = process.cwd();
23
23
  async function main() {
24
24
  console.log(chalk.bold.blue("\n🚀 Forge SQL CLI Setup\n"));
25
25
 
26
- // Detect consumer project type
26
+ const { isEsm, isTypeScript } = checkDependencies();
27
+ const manifestPath = getManifestPath();
28
+
29
+ const fsqlRelPath = await createExecutionFunction(isTypeScript, isEsm);
30
+ if (!fsqlRelPath) return;
31
+
32
+ updateManifestFile(manifestPath, fsqlRelPath);
33
+
34
+ const deployed = await deployForgeApp();
35
+ if (!deployed) return;
36
+
37
+ const url = await setupWebTrigger();
38
+ if (url) {
39
+ updateEnv(url);
40
+ }
41
+
42
+ finish();
43
+ }
44
+
45
+ function checkDependencies() {
27
46
  let isEsm = false;
28
47
  let isTypeScript = fs.existsSync(path.join(projectRoot, "tsconfig.json"));
29
48
 
@@ -65,8 +84,10 @@ async function main() {
65
84
  } catch (error) {
66
85
  console.error(chalk.yellow("Warning: Could not read package.json:"), error);
67
86
  }
87
+ return { isEsm, isTypeScript };
88
+ }
68
89
 
69
- // 1. Detect manifest.yaml or manifest.yml
90
+ function getManifestPath() {
70
91
  let manifestPath = path.join(projectRoot, "manifest.yml");
71
92
  if (!fs.existsSync(manifestPath)) {
72
93
  manifestPath = path.join(projectRoot, "manifest.yaml");
@@ -80,8 +101,10 @@ async function main() {
80
101
  );
81
102
  process.exit(1);
82
103
  }
104
+ return manifestPath;
105
+ }
83
106
 
84
- // 2. Prompt for fsql execution function path
107
+ async function createExecutionFunction(isTypeScript, isEsm) {
85
108
  const extension = isTypeScript ? "ts" : isEsm ? "js" : "js";
86
109
  const defaultPath = `src/fsql.${extension}`;
87
110
 
@@ -94,14 +117,13 @@ async function main() {
94
117
 
95
118
  if (!response.fsqlPath) {
96
119
  console.log(chalk.yellow("Setup cancelled."));
97
- process.exit(0);
120
+ return null;
98
121
  }
99
122
 
100
123
  const fsqlRelPath = response.fsqlPath;
101
124
  const fsqlAbsPath = path.resolve(projectRoot, fsqlRelPath);
102
125
  const srcSameDir = path.dirname(fsqlAbsPath);
103
126
 
104
- // Spinner for file creation
105
127
  const spinner = ora("Creating function file...").start();
106
128
 
107
129
  if (!fs.existsSync(srcSameDir)) {
@@ -122,72 +144,89 @@ async function main() {
122
144
  "utf8",
123
145
  );
124
146
 
147
+ const fileExists = fs.existsSync(fsqlAbsPath);
125
148
  fs.writeFileSync(fsqlAbsPath, fsqlContent);
126
- spinner.succeed(`Created ${fsqlRelPath}`);
127
149
 
128
- // 3. Update manifest
129
- spinner.start("Updating manifest.yml...");
150
+ if (fileExists) {
151
+ spinner.succeed(`Updated ${fsqlRelPath}`);
152
+ } else {
153
+ spinner.succeed(`Created ${fsqlRelPath}`);
154
+ }
155
+
156
+ return fsqlRelPath;
157
+ }
158
+
159
+ function updateManifestFile(manifestPath, fsqlRelPath) {
160
+ const spinner = ora("Updating manifest.yml...").start();
130
161
 
131
- let doc;
132
162
  try {
133
- const fileContents = fs.readFileSync(manifestPath, "utf8");
134
- doc = YAML.parseDocument(fileContents);
163
+ const doc = readManifest(manifestPath);
164
+ ensureModulesStructure(doc);
165
+
166
+ const modules = doc.get("modules");
167
+
168
+ // 1. Determine the handler name (e.g. index.executeSql)
169
+ const handlerName = resolveHandlerName(doc, fsqlRelPath);
170
+
171
+ // 2. Add or update the function definition
172
+ const functionKey = "executeSql";
173
+ upsertFunction(doc, modules, functionKey, handlerName);
174
+
175
+ // 3. Add or update the webtrigger definition
176
+ const webtriggerKey = "execute-sql";
177
+ upsertWebTrigger(doc, modules, webtriggerKey, functionKey);
178
+
179
+ // 4. Save
180
+ writeManifest(manifestPath, doc);
181
+ spinner.succeed("Updated manifest file");
135
182
  } catch (e) {
136
- spinner.fail("Error reading manifest");
183
+ spinner.fail("Error updating manifest");
137
184
  console.error(e);
138
185
  process.exit(1);
139
186
  }
187
+ }
188
+
189
+ function readManifest(manifestPath) {
190
+ try {
191
+ const fileContents = fs.readFileSync(manifestPath, "utf8");
192
+ return YAML.parseDocument(fileContents);
193
+ } catch (e) {
194
+ throw new Error(`Error reading manifest: ${e.message}`);
195
+ }
196
+ }
140
197
 
198
+ function ensureModulesStructure(doc) {
141
199
  if (!doc.contents) {
142
200
  doc.contents = doc.createNode({});
143
201
  }
144
-
145
202
  if (!doc.has("modules")) {
146
203
  doc.set("modules", doc.createNode({}));
147
204
  }
205
+ }
148
206
 
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
207
+ function resolveHandlerName(doc, fsqlRelPath) {
173
208
  let handlerPath = fsqlRelPath.replace(/\.(ts|js|cjs|mjs)$/, "");
174
- // Ensure we use forward slashes for handler paths
175
209
  handlerPath = handlerPath.split(path.sep).join("/");
176
210
 
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.
211
+ const modules = doc.get("modules");
212
+ let functions = modules.get("function"); // might be null/undefined
181
213
 
182
- // Heuristic: Check existing function handlers
214
+ // Logic to detect if we should strip 'src/' from the handler path
183
215
  let srcIsRoot = false;
216
+
217
+ // normalize functions list to check existing handlers
218
+ let items = [];
219
+ if (functions && YAML.isSeq(functions)) {
220
+ items = functions.items;
221
+ }
222
+
184
223
  try {
185
- if (functions && functions.items && functions.items.length > 0) {
186
- for (const f of functions.items) {
224
+ if (items.length > 0) {
225
+ for (const f of items) {
187
226
  const fJson = f.toJSON();
188
227
  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
228
+ const h = fJson.handler.split(".")[0];
229
+ const possibleSrcPath = path.join(projectRoot, "src", h + ".ts");
191
230
  const possibleRootPath = path.join(projectRoot, h + ".ts");
192
231
 
193
232
  if (
@@ -200,7 +239,6 @@ async function main() {
200
239
  }
201
240
  }
202
241
  } else {
203
- // Fallback: if index.ts is in src but not root
204
242
  if (
205
243
  fs.existsSync(path.join(projectRoot, "src", "index.ts")) &&
206
244
  !fs.existsSync(path.join(projectRoot, "index.ts"))
@@ -213,20 +251,37 @@ async function main() {
213
251
  }
214
252
 
215
253
  if (srcIsRoot && handlerPath.startsWith("src/")) {
216
- handlerPath = handlerPath.substring(4); // remove "src/"
254
+ handlerPath = handlerPath.substring(4);
217
255
  }
218
256
 
219
- const handlerName = `${handlerPath}.executeSql`;
257
+ return `${handlerPath}.executeSql`;
258
+ }
259
+
260
+ function upsertFunction(doc, modules, key, handlerName) {
261
+ if (!modules.has("function")) {
262
+ modules.set("function", doc.createNode([]));
263
+ }
264
+
265
+ let functions = modules.get("function");
266
+
267
+ // Fix if it's not a sequence (e.g. some malformed yaml or object)
268
+ if (!YAML.isSeq(functions)) {
269
+ const obj = functions.toJSON();
270
+ functions = doc.createNode(
271
+ Object.entries(obj).map(([k, v]) => ({ key: k, ...v })),
272
+ );
273
+ modules.set("function", functions);
274
+ }
220
275
 
221
276
  let functionExists = functions.items.find((f) => {
222
277
  const js = f.toJSON();
223
- return js && js.key === functionKey;
278
+ return js && js.key === key;
224
279
  });
225
280
 
226
281
  if (!functionExists) {
227
282
  functions.add(
228
283
  doc.createNode({
229
- key: functionKey,
284
+ key: key,
230
285
  handler: handlerName,
231
286
  }),
232
287
  );
@@ -235,36 +290,35 @@ async function main() {
235
290
  functionExists.set("handler", handlerName);
236
291
  } else {
237
292
  const idx = functions.items.indexOf(functionExists);
238
- functions.set(
239
- idx,
240
- doc.createNode({ key: functionKey, handler: handlerName }),
241
- );
293
+ functions.set(idx, doc.createNode({ key: key, handler: handlerName }));
242
294
  }
243
295
  }
296
+ }
244
297
 
245
- const webtriggerKey = "execute-sql";
298
+ function upsertWebTrigger(doc, modules, triggerKey, functionKey) {
246
299
  if (!modules.has("webtrigger")) {
247
300
  modules.set("webtrigger", doc.createNode([]));
248
301
  }
249
302
 
250
303
  let webtriggers = modules.get("webtrigger");
304
+
251
305
  if (!YAML.isSeq(webtriggers)) {
252
306
  const obj = webtriggers.toJSON();
253
307
  webtriggers = doc.createNode(
254
- Object.entries(obj).map(([key, val]) => ({ key, ...val })),
308
+ Object.entries(obj).map(([k, v]) => ({ key: k, ...v })),
255
309
  );
256
310
  modules.set("webtrigger", webtriggers);
257
311
  }
258
312
 
259
313
  let webtriggerExists = webtriggers.items.find((w) => {
260
314
  const js = w.toJSON();
261
- return js && js.key === webtriggerKey;
315
+ return js && js.key === triggerKey;
262
316
  });
263
317
 
264
318
  if (!webtriggerExists) {
265
319
  webtriggers.add(
266
320
  doc.createNode({
267
- key: webtriggerKey,
321
+ key: triggerKey,
268
322
  function: functionKey,
269
323
  }),
270
324
  );
@@ -275,22 +329,18 @@ async function main() {
275
329
  const idx = webtriggers.items.indexOf(webtriggerExists);
276
330
  webtriggers.set(
277
331
  idx,
278
- doc.createNode({ key: webtriggerKey, function: functionKey }),
332
+ doc.createNode({ key: triggerKey, function: functionKey }),
279
333
  );
280
334
  }
281
335
  }
336
+ }
282
337
 
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
- }
338
+ function writeManifest(manifestPath, doc) {
339
+ fs.writeFileSync(manifestPath, doc.toString());
340
+ }
291
341
 
292
- // 4. Prompt for deployment
293
- console.log(); // Add newline for better UX
342
+ async function deployForgeApp() {
343
+ console.log();
294
344
  const deployResponse = await prompts({
295
345
  type: "text",
296
346
  name: "cmd",
@@ -301,7 +351,7 @@ async function main() {
301
351
 
302
352
  if (!deployResponse.cmd) {
303
353
  console.log(chalk.yellow("Deployment cancelled."));
304
- process.exit(0);
354
+ return false;
305
355
  }
306
356
 
307
357
  const deployCmd = deployResponse.cmd;
@@ -322,11 +372,12 @@ async function main() {
322
372
  resolve();
323
373
  });
324
374
  });
375
+ return true;
376
+ }
325
377
 
326
- // 5. Run forge webtrigger create
378
+ async function setupWebTrigger() {
327
379
  let webtriggerArgs = ["webtrigger", "create", "--functionKey", "execute-sql"];
328
380
 
329
- // Try to detect existing installation
330
381
  try {
331
382
  const listProc = spawn("forge", ["install", "list", "--json"], {
332
383
  shell: true,
@@ -337,7 +388,6 @@ async function main() {
337
388
  listProc.on("close", resolve);
338
389
  });
339
390
 
340
- // Attempt to extract JSON
341
391
  const jsonStart = listOutput.indexOf("[");
342
392
  const jsonEnd = listOutput.lastIndexOf("]");
343
393
  if (jsonStart !== -1 && jsonEnd !== -1) {
@@ -364,7 +414,7 @@ async function main() {
364
414
  }
365
415
  }
366
416
  } catch {
367
- // Ignore errors in auto-detection
417
+ // Ignore
368
418
  }
369
419
 
370
420
  console.log(
@@ -378,39 +428,47 @@ async function main() {
378
428
  );
379
429
  console.log(separator);
380
430
 
381
- const forgeCmd = spawn("forge", webtriggerArgs, {
382
- stdio: ["inherit", "pipe", "inherit"],
383
- shell: true,
384
- });
431
+ return new Promise((resolve) => {
432
+ const forgeCmd = spawn("forge", webtriggerArgs, {
433
+ stdio: [
434
+ "inherit", // stdin
435
+ "pipe", // stdout
436
+ "pipe", // stderr
437
+ ],
438
+ shell: true,
439
+ });
385
440
 
386
- let stdoutData = "";
441
+ let outputData = "";
387
442
 
388
- forgeCmd.stdout.on("data", (data) => {
389
- process.stdout.write(data);
390
- stdoutData += data.toString();
391
- });
443
+ const onData = (data, stream) => {
444
+ stream.write(data);
445
+ outputData += data.toString();
446
+ };
392
447
 
393
- forgeCmd.on("close", (code) => {
394
- console.log(separator);
448
+ forgeCmd.stdout.on("data", (d) => onData(d, process.stdout));
449
+ forgeCmd.stderr.on("data", (d) => onData(d, process.stderr));
395
450
 
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
- }
451
+ forgeCmd.on("close", (code) => {
452
+ console.log(separator);
403
453
 
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);
454
+ const urlRegex =
455
+ /https:\/\/[a-zA-Z0-9-.]+\.atlassian(-dev)?\.net\/[a-zA-Z0-9/_.~%-]+/;
456
+ const matches = outputData.match(urlRegex);
457
+
458
+ if (matches) {
459
+ const url = matches[0];
460
+ console.log(chalk.green(`\nFound Webtrigger URL: ${url}`));
461
+ resolve(url);
462
+ return;
463
+ }
464
+
465
+ if (code !== 0) {
466
+ console.error(
467
+ chalk.red(`\n❌ Error: Forge command failed with exit code ${code}.`),
468
+ );
469
+ process.exit(code);
470
+ }
408
471
 
409
- if (matches) {
410
- const url = matches[0];
411
- console.log(chalk.green(`\nFound Webtrigger URL: ${url}`));
412
- updateEnv(url);
413
- } else {
414
472
  console.log(
415
473
  chalk.yellow(
416
474
  "\nCould not automatically find Webtrigger URL in output.",
@@ -419,8 +477,8 @@ async function main() {
419
477
  console.log(
420
478
  "If created, please add the URL to your .env file as FORGE_SQL_WEBTRIGGER=<url>",
421
479
  );
422
- finish();
423
- }
480
+ resolve(null);
481
+ });
424
482
  });
425
483
  }
426
484
 
@@ -438,19 +496,15 @@ function updateEnv(url) {
438
496
  }
439
497
  }
440
498
 
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
- );
499
+ const declRegex = /^FORGE_SQL_WEBTRIGGER=.*$/m;
500
+ if (declRegex.test(envContent)) {
501
+ envContent = envContent.replace(declRegex, envVar);
447
502
  } else {
448
503
  envContent += `${envVar}\n`;
449
504
  }
450
505
 
451
506
  fs.writeFileSync(envPath, envContent);
452
507
  spinner.succeed("Updated .env with Webtrigger URL");
453
- finish();
454
508
  }
455
509
 
456
510
  function finish() {
@@ -51,6 +51,9 @@ class ForgeClient {
51
51
  async testConnection() {
52
52
  try {
53
53
  const result = await this.execute("SELECT 1 as test");
54
+ if (result.error) {
55
+ console.error(`Connection test failed: ${result.error}`);
56
+ }
54
57
  return !result.error;
55
58
  }
56
59
  catch {
@@ -1,9 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeSql = void 0;
4
+ const api_1 = require("@forge/api");
4
5
  const sql_1 = require("@forge/sql");
5
6
  const executeSql = async (req) => {
6
7
  console.log("\n=== Executing Custom SQL Query ===");
8
+ if ((0, api_1.getAppContext)()?.environmentType === `PRODUCTION`) {
9
+ const errorMsg = `executeSql is disabled in PRODUCTION for security.`;
10
+ console.log(errorMsg);
11
+ return getHttpResponse(403, {
12
+ success: false,
13
+ error: errorMsg,
14
+ });
15
+ }
7
16
  const payload = req.body;
8
17
  let sqlRequest = null;
9
18
  let query;
@@ -43,6 +52,7 @@ function getHttpResponse(statusCode, body) {
43
52
  const statusTexts = {
44
53
  200: "OK",
45
54
  400: "Bad Request",
55
+ 403: "Forbidden",
46
56
  404: "Not Found",
47
57
  500: "Internal Server Error",
48
58
  };
package/dist/cjs/index.js CHANGED
@@ -159,8 +159,7 @@ class ForgeSqlCli {
159
159
  console.log(chalk_1.default.bold.blue("Forge FSQL CLI"));
160
160
  console.log(chalk_1.default.gray("Type .help for commands, exit to quit"));
161
161
  console.log(chalk_1.default.gray("=".repeat(50)));
162
- // Test connection
163
- process.stdout.write("Testing connection... ");
162
+ process.stdout.write("Connecting ... ");
164
163
  const connected = await this.client.testConnection();
165
164
  if (connected) {
166
165
  console.log(chalk_1.default.green("✓ Connected"));
package/dist/client.js CHANGED
@@ -48,6 +48,9 @@ export class ForgeClient {
48
48
  async testConnection() {
49
49
  try {
50
50
  const result = await this.execute("SELECT 1 as test");
51
+ if (result.error) {
52
+ console.error(`Connection test failed: ${result.error}`);
53
+ }
51
54
  return !result.error;
52
55
  }
53
56
  catch {
@@ -1,6 +1,15 @@
1
+ import { getAppContext } from "@forge/api";
1
2
  import { sql } from "@forge/sql";
2
3
  const executeSql = async (req) => {
3
4
  console.log("\n=== Executing Custom SQL Query ===");
5
+ if (getAppContext()?.environmentType === `PRODUCTION`) {
6
+ const errorMsg = `executeSql is disabled in PRODUCTION for security.`;
7
+ console.log(errorMsg);
8
+ return getHttpResponse(403, {
9
+ success: false,
10
+ error: errorMsg,
11
+ });
12
+ }
4
13
  const payload = req.body;
5
14
  let sqlRequest = null;
6
15
  let query;
@@ -39,6 +48,7 @@ function getHttpResponse(statusCode, body) {
39
48
  const statusTexts = {
40
49
  200: "OK",
41
50
  400: "Bad Request",
51
+ 403: "Forbidden",
42
52
  404: "Not Found",
43
53
  500: "Internal Server Error",
44
54
  };
package/dist/index.js CHANGED
@@ -118,8 +118,7 @@ export class ForgeSqlCli {
118
118
  console.log(chalk.bold.blue("Forge FSQL CLI"));
119
119
  console.log(chalk.gray("Type .help for commands, exit to quit"));
120
120
  console.log(chalk.gray("=".repeat(50)));
121
- // Test connection
122
- process.stdout.write("Testing connection... ");
121
+ process.stdout.write("Connecting ... ");
123
122
  const connected = await this.client.testConnection();
124
123
  if (connected) {
125
124
  console.log(chalk.green("✓ Connected"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-fsql",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -43,6 +43,7 @@
43
43
  "author": "Chris Hatch",
44
44
  "license": "MIT",
45
45
  "dependencies": {
46
+ "@forge/api": "^6.4.2",
46
47
  "@forge/sql": "^3.0.14",
47
48
  "chalk": "^5.4.1",
48
49
  "cli-table3": "^0.6.3",
@@ -69,7 +70,7 @@
69
70
  "typescript-eslint": "^8.48.0"
70
71
  },
71
72
  "scripts": {
72
- "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
73
+ "build": "sh bin/build",
73
74
  "dev": "ts-node src/index.ts",
74
75
  "start": "node dist/index.js",
75
76
  "fsql": "node ./bin/fsql.js",
@@ -1,64 +1,66 @@
1
- const { sql } = require("@forge/sql");
2
-
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeSql = void 0;
4
+ const api_1 = require("@forge/api");
5
+ const sql_1 = require("@forge/sql");
3
6
  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
- });
7
+ console.log("\n=== Executing Custom SQL Query ===");
8
+ if ((0, api_1.getAppContext)()?.environmentType === `PRODUCTION`) {
9
+ const errorMsg = `executeSql is disabled in PRODUCTION for security.`;
10
+ console.log(errorMsg);
11
+ return getHttpResponse(403, {
12
+ success: false,
13
+ error: errorMsg,
14
+ });
15
+ }
16
+ const payload = req.body;
17
+ let sqlRequest = null;
18
+ let query;
19
+ try {
20
+ sqlRequest = JSON.parse(payload);
21
+ query = sqlRequest?.query;
22
+ if (!query) {
23
+ return getHttpResponse(400, {
24
+ success: false,
25
+ error: "No SQL query provided",
26
+ });
27
+ }
28
+ console.log("Executing query:", query);
29
+ // Import sql directly for custom queries
30
+ const result = await sql_1.sql.executeRaw(query);
31
+ console.log("Query result:", result);
32
+ return getHttpResponse(200, {
33
+ success: true,
34
+ rows: result.rows || [],
35
+ rowCount: result.rows?.length || 0,
36
+ query,
37
+ });
38
+ }
39
+ catch (error) {
40
+ console.error(error);
41
+ console.error("Error while executing sql", { error });
42
+ const errorMessage = error instanceof Error ? error.message : String(error);
43
+ return getHttpResponse(500, {
44
+ success: false,
45
+ error: errorMessage,
46
+ ...(query && { query }),
47
+ });
19
48
  }
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
49
  };
46
-
50
+ exports.executeSql = executeSql;
47
51
  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
- };
52
+ const statusTexts = {
53
+ 200: "OK",
54
+ 400: "Bad Request",
55
+ 403: "Forbidden",
56
+ 404: "Not Found",
57
+ 500: "Internal Server Error",
58
+ };
59
+ const statusText = statusTexts[statusCode] || "Bad Request";
60
+ return {
61
+ headers: { "Content-Type": ["application/json"] },
62
+ statusCode,
63
+ statusText,
64
+ body: JSON.stringify(body),
65
+ };
62
66
  }
63
-
64
- module.exports = { executeSql };
@@ -1,29 +1,31 @@
1
+ import { getAppContext } from "@forge/api";
1
2
  import { sql } from "@forge/sql";
2
-
3
3
  const executeSql = async (req) => {
4
4
  console.log("\n=== Executing Custom SQL Query ===");
5
-
5
+ if (getAppContext()?.environmentType === `PRODUCTION`) {
6
+ const errorMsg = `executeSql is disabled in PRODUCTION for security.`;
7
+ console.log(errorMsg);
8
+ return getHttpResponse(403, {
9
+ success: false,
10
+ error: errorMsg,
11
+ });
12
+ }
6
13
  const payload = req.body;
7
14
  let sqlRequest = null;
8
15
  let query;
9
-
10
16
  try {
11
17
  sqlRequest = JSON.parse(payload);
12
18
  query = sqlRequest?.query;
13
-
14
19
  if (!query) {
15
20
  return getHttpResponse(400, {
16
21
  success: false,
17
22
  error: "No SQL query provided",
18
23
  });
19
24
  }
20
-
21
25
  console.log("Executing query:", query);
22
-
26
+ // Import sql directly for custom queries
23
27
  const result = await sql.executeRaw(query);
24
-
25
28
  console.log("Query result:", result);
26
-
27
29
  return getHttpResponse(200, {
28
30
  success: true,
29
31
  rows: result.rows || [],
@@ -33,9 +35,7 @@ const executeSql = async (req) => {
33
35
  } catch (error) {
34
36
  console.error(error);
35
37
  console.error("Error while executing sql", { error });
36
-
37
38
  const errorMessage = error instanceof Error ? error.message : String(error);
38
-
39
39
  return getHttpResponse(500, {
40
40
  success: false,
41
41
  error: errorMessage,
@@ -43,16 +43,15 @@ const executeSql = async (req) => {
43
43
  });
44
44
  }
45
45
  };
46
-
47
46
  function getHttpResponse(statusCode, body) {
48
47
  const statusTexts = {
49
48
  200: "OK",
50
49
  400: "Bad Request",
50
+ 403: "Forbidden",
51
51
  404: "Not Found",
52
52
  500: "Internal Server Error",
53
53
  };
54
54
  const statusText = statusTexts[statusCode] || "Bad Request";
55
-
56
55
  return {
57
56
  headers: { "Content-Type": ["application/json"] },
58
57
  statusCode,
@@ -60,5 +59,4 @@ function getHttpResponse(statusCode, body) {
60
59
  body: JSON.stringify(body),
61
60
  };
62
61
  }
63
-
64
62
  export { executeSql };
@@ -1,3 +1,4 @@
1
+ import { getAppContext } from "@forge/api";
1
2
  import { sql } from "@forge/sql";
2
3
 
3
4
  const executeSql = async (req: {
@@ -5,6 +6,15 @@ const executeSql = async (req: {
5
6
  }): Promise<ReturnType<typeof getHttpResponse>> => {
6
7
  console.log("\n=== Executing Custom SQL Query ===");
7
8
 
9
+ if (getAppContext()?.environmentType === `PRODUCTION`) {
10
+ const errorMsg = `executeSql is disabled in PRODUCTION for security.`;
11
+ console.log(errorMsg);
12
+ return getHttpResponse(403, {
13
+ success: false,
14
+ error: errorMsg,
15
+ });
16
+ }
17
+
8
18
  const payload = req.body;
9
19
  let sqlRequest: { query?: string } | null = null;
10
20
  let query: string | undefined;
@@ -59,6 +69,7 @@ function getHttpResponse(
59
69
  const statusTexts: Record<number, string> = {
60
70
  200: "OK",
61
71
  400: "Bad Request",
72
+ 403: "Forbidden",
62
73
  404: "Not Found",
63
74
  500: "Internal Server Error",
64
75
  };