|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| import { API_V1, CREDENTIALS } from "../apiContract";
|
|
|
| export type Result<T> =
|
| | { ok: true; value: T }
|
| | { ok: false; status: number; message: string };
|
|
|
|
|
| export interface SavedQuery {
|
| id: string;
|
| scope: string;
|
| viewId: string;
|
| name: string;
|
|
|
| kind: string;
|
| question: string;
|
| explain: string;
|
| createdAt?: string;
|
| }
|
|
|
|
|
| export interface BuildResult {
|
| spec: unknown | null;
|
| explain: string | null;
|
| refused: string | null;
|
|
|
| reason?: string;
|
| id?: string;
|
| }
|
|
|
| |
|
|
| export function errorMessage(status: number, message?: string): string {
|
| if (status >= 500 || !message) {
|
| return status >= 500
|
| ? "Something went wrong on our side. Try again in a moment."
|
| : `The server answered ${status}.`;
|
| }
|
| return message;
|
| }
|
|
|
| async function call<T>(
|
| path: string,
|
| init: RequestInit,
|
| read: (body: unknown) => T
|
| ): Promise<Result<T>> {
|
| let res: Response;
|
| try {
|
| res = await fetch(`${API_V1}${path}`, { credentials: CREDENTIALS, ...init });
|
| } catch {
|
| return { ok: false, status: 0, message: "Cannot reach the server." };
|
| }
|
| const body = (await res.json().catch(() => null)) as unknown;
|
| if (!res.ok) {
|
| const detail = (body as { error?: { message?: string } } | null)?.error?.message;
|
| return { ok: false, status: res.status, message: errorMessage(res.status, detail) };
|
| }
|
| return { ok: true, value: read(body) };
|
| }
|
|
|
| const json = (data: unknown): RequestInit => ({
|
| method: "POST",
|
| headers: { "Content-Type": "application/json" },
|
| body: JSON.stringify(data),
|
| });
|
|
|
| const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
|
|
|
| export function parseSaved(body: unknown): SavedQuery[] {
|
| const rows = (body as { views?: unknown[] } | null)?.views;
|
| if (!Array.isArray(rows)) return [];
|
| return rows
|
| .map((r) => r as Record<string, unknown>)
|
| .filter((r) => r && str(r.id) && str(r.scope))
|
| .map((r) => ({
|
| id: str(r.id),
|
| scope: str(r.scope),
|
| viewId: str(r.viewId) || str(r.id),
|
| name: str(r.name) || "Query",
|
| kind: str(r.kind) || "grid",
|
| question: str(r.question),
|
| explain: str(r.explain),
|
| createdAt: str(r.createdAt) || undefined,
|
| }));
|
| }
|
|
|
|
|
| export interface QueryIndex {
|
| views: SavedQuery[];
|
| |
| |
| |
| |
| |
| |
| |
|
|
| builtins: string[];
|
| }
|
|
|
| export function fetchQueries(): Promise<Result<QueryIndex>> {
|
| return call("/query", {}, (b) => ({
|
| views: parseSaved(b),
|
| builtins: (((b as { builtins?: unknown } | null)?.builtins as unknown[]) ?? [])
|
| .filter((k): k is string => typeof k === "string"),
|
| }));
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| export function buildQuery(question: string, scope: string): Promise<Result<BuildResult>> {
|
| return call("/query/build", json({ question, scope }), (b) => {
|
| const r = (b || {}) as Record<string, unknown>;
|
| return {
|
| spec: r.spec ?? null,
|
| explain: str(r.explain) || null,
|
| refused: str(r.refused) || null,
|
| reason: str(r.reason) || undefined,
|
| id: str(r.id) || undefined,
|
| };
|
| });
|
| }
|
|
|
| export function saveQuery(
|
| question: string,
|
| scope: string,
|
| spec: unknown
|
| ): Promise<Result<SavedQuery>> {
|
| return call("/query/save", json({ question, scope, spec }), (b) => parseSaved({ views: [b] })[0]);
|
| }
|
|
|
| export function deleteQuery(id: string): Promise<Result<true>> {
|
| return call(`/query/${encodeURIComponent(id)}`, { method: "DELETE" }, () => true as const);
|
| }
|
|
|