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 +21 -2
- package/bin/build +15 -0
- package/bin/setup.js +166 -112
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/execute-sql.js +10 -0
- package/dist/cjs/index.js +1 -2
- package/dist/client.js +3 -0
- package/dist/execute-sql.js +10 -0
- package/dist/index.js +1 -2
- package/package.json +3 -2
- package/templates/execute-sql.cjs +61 -59
- package/templates/execute-sql.js +11 -13
- package/templates/execute-sql.ts +11 -0
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Forge FSQL CLI
|
|
2
2
|
|
|
3
|
-
Interactive
|
|
3
|
+
Interactive CLI for querying Atlassian Forge SQL databases via web triggers.
|
|
4
|
+
|
|
5
|
+
## Demo
|
|
6
|
+
|
|
7
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
134
|
-
doc
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
//
|
|
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 (
|
|
186
|
-
for (const f of
|
|
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];
|
|
190
|
-
const possibleSrcPath = path.join(projectRoot, "src", h + ".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);
|
|
254
|
+
handlerPath = handlerPath.substring(4);
|
|
217
255
|
}
|
|
218
256
|
|
|
219
|
-
|
|
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 ===
|
|
278
|
+
return js && js.key === key;
|
|
224
279
|
});
|
|
225
280
|
|
|
226
281
|
if (!functionExists) {
|
|
227
282
|
functions.add(
|
|
228
283
|
doc.createNode({
|
|
229
|
-
key:
|
|
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
|
-
|
|
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(([
|
|
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 ===
|
|
315
|
+
return js && js.key === triggerKey;
|
|
262
316
|
});
|
|
263
317
|
|
|
264
318
|
if (!webtriggerExists) {
|
|
265
319
|
webtriggers.add(
|
|
266
320
|
doc.createNode({
|
|
267
|
-
key:
|
|
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:
|
|
332
|
+
doc.createNode({ key: triggerKey, function: functionKey }),
|
|
279
333
|
);
|
|
280
334
|
}
|
|
281
335
|
}
|
|
336
|
+
}
|
|
282
337
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
293
|
-
console.log();
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
441
|
+
let outputData = "";
|
|
387
442
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
const onData = (data, stream) => {
|
|
444
|
+
stream.write(data);
|
|
445
|
+
outputData += data.toString();
|
|
446
|
+
};
|
|
392
447
|
|
|
393
|
-
|
|
394
|
-
|
|
448
|
+
forgeCmd.stdout.on("data", (d) => onData(d, process.stdout));
|
|
449
|
+
forgeCmd.stderr.on("data", (d) => onData(d, process.stderr));
|
|
395
450
|
|
|
396
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
if (
|
|
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() {
|
package/dist/cjs/client.js
CHANGED
package/dist/cjs/execute-sql.js
CHANGED
|
@@ -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
|
-
|
|
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
package/dist/execute-sql.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 };
|
package/templates/execute-sql.js
CHANGED
|
@@ -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 };
|
package/templates/execute-sql.ts
CHANGED
|
@@ -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
|
};
|