apiConnectionRunner.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /**
  2. * API Connection Runner — fetches live data from external APIs defined in the
  3. * apiConnections table and stores the result as a knowledge entry.
  4. */
  5. import { getDb } from "./db";
  6. import { apiConnections, knowledgeEntries } from "../drizzle/schema";
  7. import { eq, and } from "drizzle-orm";
  8. export interface RunResult {
  9. success: boolean;
  10. connectionId: number;
  11. rowsStored?: number;
  12. error?: string;
  13. }
  14. /**
  15. * Execute a single API connection, store the response in knowledgeEntries,
  16. * and update lastExecutedAt + executionCount on the connection record.
  17. */
  18. export async function runApiConnection(connectionId: number): Promise<RunResult> {
  19. const db = await getDb();
  20. if (!db) return { success: false, connectionId, error: "Database not available" };
  21. const [conn] = await db.select().from(apiConnections)
  22. .where(eq(apiConnections.id, connectionId));
  23. if (!conn) return { success: false, connectionId, error: "Connection not found" };
  24. if (!conn.isActive) return { success: false, connectionId, error: "Connection is inactive" };
  25. // Build request headers
  26. const headers: Record<string, string> = { "Content-Type": "application/json" };
  27. if (conn.headers && typeof conn.headers === "object") {
  28. Object.assign(headers, conn.headers);
  29. }
  30. // Build request body (POST/PUT only)
  31. const hasBody = conn.method === "POST" || conn.method === "PUT";
  32. const body = hasBody ? JSON.stringify(conn.testPayload ?? {}) : undefined;
  33. let rawData: unknown;
  34. try {
  35. const res = await fetch(conn.endpoint, { method: conn.method, headers, body });
  36. if (!res.ok) {
  37. throw new Error(`HTTP ${res.status} ${res.statusText}`);
  38. }
  39. rawData = await res.json().catch(() => res.text());
  40. } catch (err: unknown) {
  41. const msg = err instanceof Error ? err.message : String(err);
  42. return { success: false, connectionId, error: `Fetch failed: ${msg}` };
  43. }
  44. // Apply output variable mapping to extract relevant fields
  45. const extracted = applyOutputMapping(rawData, conn.outputVariables as Record<string, string> | null);
  46. // Persist as a knowledge entry (upsert by connection id stored in metadata)
  47. const content = JSON.stringify(extracted, null, 2);
  48. const title = `API: ${conn.name}`;
  49. const existing = await db.select({ id: knowledgeEntries.id })
  50. .from(knowledgeEntries)
  51. .where(eq(knowledgeEntries.source, "api"))
  52. .limit(1);
  53. if (existing.length > 0) {
  54. await db.update(knowledgeEntries)
  55. .set({ answer: content, updatedAt: new Date() })
  56. .where(eq(knowledgeEntries.id, existing[0].id));
  57. } else {
  58. await db.insert(knowledgeEntries).values({
  59. question: title,
  60. answer: content,
  61. category: conn.category ?? "api",
  62. source: "api",
  63. });
  64. }
  65. // Update connection metadata
  66. await db.update(apiConnections)
  67. .set({
  68. lastExecutedAt: new Date(),
  69. executionCount: (conn.executionCount ?? 0) + 1,
  70. })
  71. .where(eq(apiConnections.id, connectionId));
  72. return { success: true, connectionId, rowsStored: 1 };
  73. }
  74. /**
  75. * Apply dot-path output variable mappings to extract fields from the response.
  76. * outputVariables format: { "fieldName": "path.to.value" }
  77. * If no mappings defined, returns the raw data as-is.
  78. */
  79. function applyOutputMapping(
  80. data: unknown,
  81. mappings: Record<string, string> | null
  82. ): unknown {
  83. if (!mappings || Object.keys(mappings).length === 0) return data;
  84. const result: Record<string, unknown> = {};
  85. for (const [key, path] of Object.entries(mappings)) {
  86. result[key] = resolvePath(data, path);
  87. }
  88. return result;
  89. }
  90. function resolvePath(obj: unknown, path: string): unknown {
  91. return path.split(".").reduce((cur: unknown, key) => {
  92. if (cur && typeof cur === "object") return (cur as Record<string, unknown>)[key];
  93. return undefined;
  94. }, obj);
  95. }