From 799423b59af47e8995117e9cb70d446190b961a9 Mon Sep 17 00:00:00 2001 From: vimtor Date: Fri, 25 Oct 2024 15:55:37 +0200 Subject: [PATCH 1/7] Add shortcuts to focus inputs --- extensions/things/CHANGELOG.md | 4 +++ extensions/things/src/add-new-project.tsx | 44 +++++++++++++++++++++++ extensions/things/src/add-new-todo.tsx | 44 +++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/extensions/things/CHANGELOG.md b/extensions/things/CHANGELOG.md index ac1919d9cdc..29baf99150b 100644 --- a/extensions/things/CHANGELOG.md +++ b/extensions/things/CHANGELOG.md @@ -1,5 +1,9 @@ # Things Changelog +## [Focus Input Fields] - 2024-10-25 + +- Add shortcuts to focus the input fields in both the `Add New To-Do` and `Add New Project` commands. + ## [Quick ToDo Fixes] - 2024-08-19 - Quick ToDo Command: Disable Automatic Date (when & deadline), List parsing when AI is not enabled in preferences/is not available via environment. diff --git a/extensions/things/src/add-new-project.tsx b/extensions/things/src/add-new-project.tsx index 53d7bfc291f..5a6f554a7d4 100644 --- a/extensions/things/src/add-new-project.tsx +++ b/extensions/things/src/add-new-project.tsx @@ -99,6 +99,50 @@ Tasks:`); + + focus('title')} + shortcut={{ modifiers: ['cmd'], key: '1' }} + /> + focus('notes')} + shortcut={{ modifiers: ['cmd'], key: '2' }} + /> + focus('when')} + shortcut={{ modifiers: ['cmd'], key: 's' }} + /> + focus('areaId')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 'm' }} + /> + focus('tags')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 't' }} + /> + focus('toDos')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 'c' }} + /> + focus('deadline')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 'd' }} + /> + } > diff --git a/extensions/things/src/add-new-todo.tsx b/extensions/things/src/add-new-todo.tsx index 5aacec0a97f..d119fe126f0 100644 --- a/extensions/things/src/add-new-todo.tsx +++ b/extensions/things/src/add-new-todo.tsx @@ -138,6 +138,50 @@ export function AddNewTodo({ title, commandListName, draftValues }: AddNewTodoPr {environment.canAccess(AI) && ( )} + + focus('title')} + shortcut={{ modifiers: ['cmd'], key: '1' }} + /> + focus('notes')} + shortcut={{ modifiers: ['cmd'], key: '2' }} + /> + focus('when')} + shortcut={{ modifiers: ['cmd'], key: 's' }} + /> + focus('listId')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 'm' }} + /> + focus('tags')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 't' }} + /> + focus('checklist-items')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 'c' }} + /> + focus('deadline')} + shortcut={{ modifiers: ['cmd', 'shift'], key: 'd' }} + /> + } // Don't enable drafts if coming from another list or an empty view From 69d6ba45f37a87c22107741dd8daa08c375a8c2b Mon Sep 17 00:00:00 2001 From: vimtor Date: Fri, 25 Oct 2024 16:02:37 +0200 Subject: [PATCH 2/7] Add @vimtor as a contributor of the Things 3 extension --- extensions/things/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/things/package.json b/extensions/things/package.json index eae624f1ef4..f13fcd45c97 100644 --- a/extensions/things/package.json +++ b/extensions/things/package.json @@ -10,7 +10,8 @@ "andreaselia", "stelo", "thomaslombart", - "srikirank" + "srikirank", + "vimtor" ], "license": "MIT", "commands": [ From dcfaaee9851ca862ce636fdcfcfc4b42b91d7a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Nielsen=20Tik=C3=A6r?= Date: Tue, 29 Oct 2024 10:34:01 +0100 Subject: [PATCH 3/7] Update add-new-project.tsx --- extensions/things/src/add-new-project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/things/src/add-new-project.tsx b/extensions/things/src/add-new-project.tsx index 5a6f554a7d4..10b002849e7 100644 --- a/extensions/things/src/add-new-project.tsx +++ b/extensions/things/src/add-new-project.tsx @@ -107,7 +107,7 @@ Tasks:`); shortcut={{ modifiers: ['cmd'], key: '1' }} /> focus('notes')} shortcut={{ modifiers: ['cmd'], key: '2' }} From 32dc85b35f668af85a87dc92b87adbe5b04964c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Nielsen=20Tik=C3=A6r?= Date: Tue, 29 Oct 2024 10:34:32 +0100 Subject: [PATCH 4/7] Update add-new-todo.tsx --- extensions/things/src/add-new-todo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/things/src/add-new-todo.tsx b/extensions/things/src/add-new-todo.tsx index d119fe126f0..8eb5ce84502 100644 --- a/extensions/things/src/add-new-todo.tsx +++ b/extensions/things/src/add-new-todo.tsx @@ -146,7 +146,7 @@ export function AddNewTodo({ title, commandListName, draftValues }: AddNewTodoPr shortcut={{ modifiers: ['cmd'], key: '1' }} /> focus('notes')} shortcut={{ modifiers: ['cmd'], key: '2' }} From 3465023c4af36cb662b4c0912ea2f53c253dc593 Mon Sep 17 00:00:00 2001 From: vimtor Date: Sat, 8 Mar 2025 12:10:49 +0100 Subject: [PATCH 5/7] Initial version --- extensions/metabase/package-lock.json | 117 ++++++++++++++++-- extensions/metabase/package.json | 16 ++- .../metabase/src/tools/get-databases.ts | 22 ++++ extensions/metabase/src/tools/get-schemas.ts | 31 +++++ extensions/metabase/src/tools/run-query.ts | 69 +++++++++++ 5 files changed, 241 insertions(+), 14 deletions(-) create mode 100644 extensions/metabase/src/tools/get-databases.ts create mode 100644 extensions/metabase/src/tools/get-schemas.ts create mode 100644 extensions/metabase/src/tools/run-query.ts diff --git a/extensions/metabase/package-lock.json b/extensions/metabase/package-lock.json index 89776f5c8a6..ba848919092 100644 --- a/extensions/metabase/package-lock.json +++ b/extensions/metabase/package-lock.json @@ -8,7 +8,8 @@ "license": "MIT", "dependencies": { "@raycast/api": "^1.90.0", - "@raycast/utils": "^1.17.0" + "@raycast/utils": "^1.17.0", + "node-fetch": "^3.3.2" }, "devDependencies": { "@raycast/eslint-config": "^1.0.11", @@ -1659,6 +1660,26 @@ "node-fetch": "^2.7.0" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1680,6 +1701,15 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2126,6 +2156,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2211,6 +2264,18 @@ "dev": true, "license": "ISC" }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2720,24 +2785,41 @@ "dev": true, "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/object-hash": { @@ -3276,6 +3358,15 @@ "punycode": "^2.1.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/extensions/metabase/package.json b/extensions/metabase/package.json index b99f59d8dcb..381c06d3cc1 100644 --- a/extensions/metabase/package.json +++ b/extensions/metabase/package.json @@ -21,6 +21,19 @@ "mode": "view" } ], + "tools": [ + { + "name": "get-databases", + "title": "Get Databases", + "description": "Search the saved databases in Metabase" + } + , + { + "name": "run-query", + "title": "Run Query", + "description": "Run query against a database" + } + ], "preferences": [ { "name": "instanceUrl", @@ -41,7 +54,8 @@ ], "dependencies": { "@raycast/api": "^1.90.0", - "@raycast/utils": "^1.17.0" + "@raycast/utils": "^1.17.0", + "node-fetch": "^3.3.2" }, "devDependencies": { "@raycast/eslint-config": "^1.0.11", diff --git a/extensions/metabase/src/tools/get-databases.ts b/extensions/metabase/src/tools/get-databases.ts new file mode 100644 index 00000000000..35f4894be78 --- /dev/null +++ b/extensions/metabase/src/tools/get-databases.ts @@ -0,0 +1,22 @@ +import fetch from "node-fetch"; +import { getPreferenceValues } from "@raycast/api"; + +export default async function () { + const preferences = getPreferenceValues(); + + const endpoint = new URL("/api/database", preferences.instanceUrl); + + endpoint.searchParams.set("include", "tables"); + + const response = await fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }).then((res) => res.json() as Promise<{ data: unknown[] }>); + + return { + databases: response.data, + }; +} diff --git a/extensions/metabase/src/tools/get-schemas.ts b/extensions/metabase/src/tools/get-schemas.ts new file mode 100644 index 00000000000..3175d0d0a79 --- /dev/null +++ b/extensions/metabase/src/tools/get-schemas.ts @@ -0,0 +1,31 @@ +import fetch from "node-fetch"; +import { getPreferenceValues } from "@raycast/api"; + +type Input = { + /** The database ID to get the schema for */ + databaseId: number; +}; + +export default async function (input: Input) { + const preferences = getPreferenceValues(); + + const endpoint = new URL(`/api/database/${input.databaseId}/schema/`, preferences.instanceUrl); + + console.log(endpoint.toString()); + + console.log(preferences.apiToken); + + const response = await fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }); + + console.log(response.status); + console.log(await response.text()); + console.log(await response.json()); + + return []; +} diff --git a/extensions/metabase/src/tools/run-query.ts b/extensions/metabase/src/tools/run-query.ts new file mode 100644 index 00000000000..e8da623a2ee --- /dev/null +++ b/extensions/metabase/src/tools/run-query.ts @@ -0,0 +1,69 @@ +import { Tool, getPreferenceValues } from "@raycast/api"; +import fetch from "node-fetch"; + +type Input = { + /** The SQL query to run */ + query: string; + /** The database ID to run the query against */ + databaseId: number; +}; + +export default async function (input: Input) { + const preferences = getPreferenceValues(); + + const endpoint = new URL(`/api/dataset`, preferences.instanceUrl); + + const response = await fetch(endpoint.toString(), { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + "x-api-key": preferences.apiToken, + }, + body: JSON.stringify({ + database: input.databaseId, + type: "native", + native: { + query: input.query, + }, + }), + }); + + if (!response.status.toString().startsWith("2")) { + const error = await response.text(); + throw new Error(`Error: ${error}`); + } + + return response.json(); +} + +export const confirmation: Tool.Confirmation = async (input) => { + const preferences = getPreferenceValues(); + + const endpoint = new URL(`/api/database/${input.databaseId}`, preferences.instanceUrl); + + const response = await fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }); + + const database = (await response.json()) as { name: string }; + + return { + title: "Run Tool", + message: "Are you sure you want to run this query?", + info: [ + { + name: "Database", + value: database.name, + }, + { + name: "Query", + value: input.query, + }, + ], + }; +}; From 5d2a7933880a874041eb2bce9d0f7688ef3304a8 Mon Sep 17 00:00:00 2001 From: vimtor Date: Sat, 8 Mar 2025 14:14:02 +0100 Subject: [PATCH 6/7] AI tools for Metabase extension --- extensions/metabase/CHANGELOG.md | 2 + extensions/metabase/package.json | 30 +++- extensions/metabase/src/lib/api.ts | 137 ++++++++++++++++++ extensions/metabase/src/search-questions.tsx | 38 +---- .../metabase/src/tools/get-databases.ts | 62 ++++++-- .../metabase/src/tools/get-questions.ts | 23 +++ extensions/metabase/src/tools/get-schemas.ts | 31 ---- extensions/metabase/src/tools/run-query.ts | 46 +----- extensions/metabase/src/tools/run-question.ts | 52 +++++++ 9 files changed, 300 insertions(+), 121 deletions(-) create mode 100644 extensions/metabase/src/lib/api.ts create mode 100644 extensions/metabase/src/tools/get-questions.ts delete mode 100644 extensions/metabase/src/tools/get-schemas.ts create mode 100644 extensions/metabase/src/tools/run-question.ts diff --git a/extensions/metabase/CHANGELOG.md b/extensions/metabase/CHANGELOG.md index f28f2d614cb..4ee138aa327 100644 --- a/extensions/metabase/CHANGELOG.md +++ b/extensions/metabase/CHANGELOG.md @@ -1,3 +1,5 @@ # Metabase Changelog +## [AI Tools] - {PR_MERGE_DATE} + ## [Initial Version] - 2025-02-01 diff --git a/extensions/metabase/package.json b/extensions/metabase/package.json index 381c06d3cc1..523dd6a9702 100644 --- a/extensions/metabase/package.json +++ b/extensions/metabase/package.json @@ -26,14 +26,36 @@ "name": "get-databases", "title": "Get Databases", "description": "Search the saved databases in Metabase" - } - , + }, + { + "name": "get-questions", + "title": "Get Questions", + "description": "Get the saved Metabase questions" + }, { "name": "run-query", "title": "Run Query", - "description": "Run query against a database" + "description": "Run a native SQL query against a database" + }, + { + "name": "run-question", + "title": "Run Question", + "description": "Get latest saved question results" } - ], + ], + "ai": { + "instructions": "When fetching data from Metabase, always retrieve the saved questions and execute them if they match the requested information. If they do not match, get the databases and run a native SQL query based on their metadata. You can retrieve both the saved questions and the databases in parallel so you can make the best decision.", + "evals": [ + { + "input": "@metabase How many users do I have?", + "expected": [] + }, + { + "input": "@metabase What databases do I have connected?", + "expected": [] + } + ] + }, "preferences": [ { "name": "instanceUrl", diff --git a/extensions/metabase/src/lib/api.ts b/extensions/metabase/src/lib/api.ts new file mode 100644 index 00000000000..fe761e5ea8a --- /dev/null +++ b/extensions/metabase/src/lib/api.ts @@ -0,0 +1,137 @@ +import { getPreferenceValues } from "@raycast/api"; +import fetch from "node-fetch"; + +const preferences = getPreferenceValues(); + +export interface Database { + id: number; + name: string; + engine: string; + tables: { + name: string; + schema: string; + // and other fields + }[]; +} + +export interface Card { + id: number; + name: string; + description: string | null; + created_at: string; + updated_at: string; + archived: boolean; + view_count: number; + display: "bar" | "table" | "scalar" | "line" | "pie" | (string & Record); + dataset_query: { + database: number; + native: { + query: string; + template_tags: Record; + }; + type: "native"; + }; + creator: { + email: string; + first_name: string; + last_login: string; + is_qbnewb: boolean; + is_superuser: boolean; + id: number; + last_name: string; + date_joined: string; + common_name: string; + }; +} + +export interface Query { + rows: string[][]; +} + +export async function runQuery(input: { query: string; databaseId: number }) { + const endpoint = new URL(`/api/dataset`, preferences.instanceUrl); + + return fetch(endpoint.toString(), { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + "x-api-key": preferences.apiToken, + }, + body: JSON.stringify({ + database: input.databaseId, + type: "native", + native: { + query: input.query, + }, + }), + }) + .then((res) => res.json() as Promise<{ data: Query }>) + .then((res) => res.data); +} + +export async function getQuestion(input: { questionId: number }) { + const endpoint = new URL(`/api/card/${input.questionId}`, preferences.instanceUrl); + + return fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }).then((res) => res.json() as Promise); +} + +export async function runQuestion(input: { questionId: number }) { + const endpoint = new URL(`/api/card/${input.questionId}/query`, preferences.instanceUrl); + + return fetch(endpoint.toString(), { + method: "POST", + headers: { + accept: "application/json", + contentType: "application/json", + "x-api-key": preferences.apiToken, + }, + body: JSON.stringify({}), + }).then((res) => res.json() as Promise); +} + +export async function getDatabases() { + const endpoint = new URL("/api/database", preferences.instanceUrl); + + endpoint.searchParams.set("include", "tables"); + + return fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }) + .then((res) => res.json() as Promise<{ data: Database[] }>) + .then((res) => res.data); +} + +export async function getDatabase(databaseId: number) { + const endpoint = new URL(`/api/database/${databaseId}`, preferences.instanceUrl); + + return fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }).then((res) => res.json() as Promise); +} + +export async function getQuestions() { + const endpoint = new URL("/api/card", preferences.instanceUrl); + + return fetch(endpoint.toString(), { + method: "GET", + headers: { + accept: "application/json", + "x-api-key": preferences.apiToken, + }, + }).then((res) => res.json() as Promise); +} diff --git a/extensions/metabase/src/search-questions.tsx b/extensions/metabase/src/search-questions.tsx index 0513a8be705..7d817876f0b 100644 --- a/extensions/metabase/src/search-questions.tsx +++ b/extensions/metabase/src/search-questions.tsx @@ -1,32 +1,9 @@ import { Action, ActionPanel, getPreferenceValues, Icon, List } from "@raycast/api"; -import { getAvatarIcon, useFetch } from "@raycast/utils"; +import { getAvatarIcon, useCachedPromise } from "@raycast/utils"; +import { type Card, getQuestions } from "./lib/api"; const preferences = getPreferenceValues(); -interface Card { - id: number; - name: string; - description: string | null; - created_at: string; - updated_at: string; - archived: boolean; - view_count: number; - display: "bar" | "table" | "scalar" | "line" | "pie" | (string & Record); - creator: { - email: string; - first_name: string; - last_login: string; - is_qbnewb: boolean; - is_superuser: boolean; - id: number; - last_name: string; - date_joined: string; - common_name: string; - }; -} - -const endpoint = new URL("/api/card", preferences.instanceUrl); - function getQuestionIcon(card: Card): Icon { switch (card.display) { case "bar": @@ -45,13 +22,7 @@ function getQuestionIcon(card: Card): Icon { } export default function SearchQuestions() { - const { data: cards, isLoading } = useFetch(endpoint.toString(), { - method: "GET", - headers: { - accept: "application/json", - "x-api-key": preferences.apiToken, - }, - }); + const { data: cards, isLoading } = useCachedPromise(getQuestions); return ( @@ -75,6 +46,9 @@ export default function SearchQuestions() { icon: getQuestionIcon(card), }} /> + + + } /> diff --git a/extensions/metabase/src/tools/get-databases.ts b/extensions/metabase/src/tools/get-databases.ts index 35f4894be78..9cd9b916fa6 100644 --- a/extensions/metabase/src/tools/get-databases.ts +++ b/extensions/metabase/src/tools/get-databases.ts @@ -1,22 +1,58 @@ -import fetch from "node-fetch"; -import { getPreferenceValues } from "@raycast/api"; +import { getDatabases, runQuery } from "../lib/api"; + +const postgresDllQuery = ` + WITH t AS (SELECT table_name as name, jsonb_object_agg(column_name, data_type) as columns + FROM information_schema.columns + WHERE table_schema not in ('pg_catalog', 'information_schema') + group by table_name), + e AS (SELECT t.typname AS name, (SELECT jsonb_agg(enumlabel ORDER BY enumsortorder) FROM pg_enum WHERE enumtypid = t.oid) AS values + FROM pg_type t + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') + AND t.typtype = 'e' + AND t.typarray != 0) + SELECT (SELECT jsonb_agg(t.*) FROM t) as tables, + (SELECT jsonb_agg(e.*) FROM e) as enums; +`; + +const ddlQueries: Record = { + postgres: postgresDllQuery, +}; export default async function () { - const preferences = getPreferenceValues(); + const databases = await getDatabases(); + + const promises = databases.map(async (database) => { + const schema: { tables: { name: string; columns?: string[] }[]; enums?: { name: string; values: string[] }[] } = { + tables: database.tables.map((table) => ({ name: table.name })), + }; - const endpoint = new URL("/api/database", preferences.instanceUrl); + // Get table schemas using DDL queries to improve query accuracy + if (database.engine in ddlQueries) { + try { + const ddl = await runQuery({ + query: ddlQueries[database.engine], + databaseId: database.id, + }); - endpoint.searchParams.set("include", "tables"); + schema.tables = JSON.parse(ddl.rows[0][0]); + if (ddl.rows[0][1]) { + schema.enums = JSON.parse(ddl.rows[0][1]); + } + } catch { + // Use the original tables if the DDL query fails + } + } - const response = await fetch(endpoint.toString(), { - method: "GET", - headers: { - accept: "application/json", - "x-api-key": preferences.apiToken, - }, - }).then((res) => res.json() as Promise<{ data: unknown[] }>); + return { + id: database.id, + name: database.name, + engine: database.engine, + schema, + }; + }); return { - databases: response.data, + databases: await Promise.all(promises), }; } diff --git a/extensions/metabase/src/tools/get-questions.ts b/extensions/metabase/src/tools/get-questions.ts new file mode 100644 index 00000000000..46e25b6bc87 --- /dev/null +++ b/extensions/metabase/src/tools/get-questions.ts @@ -0,0 +1,23 @@ +import { getQuestions } from "../lib/api"; + +export default async function () { + const questions = await getQuestions(); + + return { + questions: questions.map((question) => ({ + id: question.id, + name: question.name, + description: question.description, + createdAt: new Date(question.created_at), + updatedAt: new Date(question.updated_at), + archived: question.archived, + databaseId: question?.dataset_query?.database, + databaseQuery: question?.dataset_query?.native?.query, + creator: { + email: question.creator.email, + firstName: question.creator.first_name, + lastLogin: new Date(question.creator.last_login), + }, + })), + }; +} diff --git a/extensions/metabase/src/tools/get-schemas.ts b/extensions/metabase/src/tools/get-schemas.ts deleted file mode 100644 index 3175d0d0a79..00000000000 --- a/extensions/metabase/src/tools/get-schemas.ts +++ /dev/null @@ -1,31 +0,0 @@ -import fetch from "node-fetch"; -import { getPreferenceValues } from "@raycast/api"; - -type Input = { - /** The database ID to get the schema for */ - databaseId: number; -}; - -export default async function (input: Input) { - const preferences = getPreferenceValues(); - - const endpoint = new URL(`/api/database/${input.databaseId}/schema/`, preferences.instanceUrl); - - console.log(endpoint.toString()); - - console.log(preferences.apiToken); - - const response = await fetch(endpoint.toString(), { - method: "GET", - headers: { - accept: "application/json", - "x-api-key": preferences.apiToken, - }, - }); - - console.log(response.status); - console.log(await response.text()); - console.log(await response.json()); - - return []; -} diff --git a/extensions/metabase/src/tools/run-query.ts b/extensions/metabase/src/tools/run-query.ts index e8da623a2ee..3b5dc7702c8 100644 --- a/extensions/metabase/src/tools/run-query.ts +++ b/extensions/metabase/src/tools/run-query.ts @@ -1,5 +1,5 @@ -import { Tool, getPreferenceValues } from "@raycast/api"; -import fetch from "node-fetch"; +import { Tool } from "@raycast/api"; +import { getDatabase, runQuery } from "../lib/api"; type Input = { /** The SQL query to run */ @@ -9,51 +9,15 @@ type Input = { }; export default async function (input: Input) { - const preferences = getPreferenceValues(); + const result = await runQuery(input); - const endpoint = new URL(`/api/dataset`, preferences.instanceUrl); - - const response = await fetch(endpoint.toString(), { - method: "POST", - headers: { - accept: "application/json", - "content-type": "application/json", - "x-api-key": preferences.apiToken, - }, - body: JSON.stringify({ - database: input.databaseId, - type: "native", - native: { - query: input.query, - }, - }), - }); - - if (!response.status.toString().startsWith("2")) { - const error = await response.text(); - throw new Error(`Error: ${error}`); - } - - return response.json(); + return { result }; } export const confirmation: Tool.Confirmation = async (input) => { - const preferences = getPreferenceValues(); - - const endpoint = new URL(`/api/database/${input.databaseId}`, preferences.instanceUrl); - - const response = await fetch(endpoint.toString(), { - method: "GET", - headers: { - accept: "application/json", - "x-api-key": preferences.apiToken, - }, - }); - - const database = (await response.json()) as { name: string }; + const database = await getDatabase(input.databaseId); return { - title: "Run Tool", message: "Are you sure you want to run this query?", info: [ { diff --git a/extensions/metabase/src/tools/run-question.ts b/extensions/metabase/src/tools/run-question.ts new file mode 100644 index 00000000000..ca729bfb879 --- /dev/null +++ b/extensions/metabase/src/tools/run-question.ts @@ -0,0 +1,52 @@ +import { Tool } from "@raycast/api"; +import { getDatabase, getQuestion, runQuestion } from "../lib/api"; + +type Input = { + /** The question ID to run */ + questionId: number; +}; + +export default async function (input: Input) { + const result = await runQuestion(input); + + return { result }; +} + +export const confirmation: Tool.Confirmation = async (input) => { + const question = await getQuestion(input); + + let database; + try { + database = await getDatabase(question.dataset_query.database); + } catch { + // Failed to get database to display in the confirmation + } + + return { + message: "Are you sure you want to run this question?", + info: [ + database + ? { + name: "Database", + value: database.name, + } + : null, + { + name: "Name", + value: question.name, + }, + question.description + ? { + name: "Description", + value: question.description, + } + : null, + question.dataset_query?.native?.query + ? { + name: "Query", + value: question.dataset_query?.native?.query, + } + : null, + ].filter((x): x is { name: string; value: string } => !!x), + }; +}; From 268a6717ff91dc0ab5e8565c76d4dd83009b7ba2 Mon Sep 17 00:00:00 2001 From: vimtor Date: Mon, 10 Mar 2025 13:34:40 +0100 Subject: [PATCH 7/7] Add `requiresConfirmation` option to run question AI tool --- extensions/metabase/src/tools/run-question.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/metabase/src/tools/run-question.ts b/extensions/metabase/src/tools/run-question.ts index ca729bfb879..e4ab14178a7 100644 --- a/extensions/metabase/src/tools/run-question.ts +++ b/extensions/metabase/src/tools/run-question.ts @@ -4,6 +4,8 @@ import { getDatabase, getQuestion, runQuestion } from "../lib/api"; type Input = { /** The question ID to run */ questionId: number; + /** Defaults to `true` unless the user specifies otherwise */ + requiresConfirmation: boolean; }; export default async function (input: Input) { @@ -13,6 +15,10 @@ export default async function (input: Input) { } export const confirmation: Tool.Confirmation = async (input) => { + if (!input.requiresConfirmation) { + return true; + } + const question = await getQuestion(input); let database;