diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce3d4571..cb4614d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,14 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + env: + CXXFLAGS: -std=c++20 + + - name: Patch tree-sitter Bun native binding + run: | + mkdir -p node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64 + cp node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/build/Release/tree_sitter_runtime_binding.node node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64/tree-sitter.node + cp node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/@tree-sitter-grammars+tree-sitter-zig.node node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/tree-sitter-zig.node - name: Prepare environment run: cp .env.sample .env @@ -103,6 +111,14 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + env: + CXXFLAGS: -std=c++20 + + - name: Patch tree-sitter Bun native binding + run: | + mkdir -p node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64 + cp node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/build/Release/tree_sitter_runtime_binding.node node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64/tree-sitter.node + cp node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/@tree-sitter-grammars+tree-sitter-zig.node node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/tree-sitter-zig.node - name: Prepare environment run: cp .env.sample .env diff --git a/.gitignore b/.gitignore index 99db796a..73d2b595 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ backend/server.exe # turbo .turbo + +# AI +./plans diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 333d7f5c..1ca9b105 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -2,6 +2,13 @@ FROM oven/bun:1 AS build WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + g++ \ + make \ + python3 \ + && rm -rf /var/lib/apt/lists/* + # Cache workspace dependency installation COPY package.json bun.lock ./ COPY patches patches @@ -59,6 +66,7 @@ RUN apt-get update \ brotli \ bzip2 \ ca-certificates \ + git \ ghostscript \ gzip \ libarchive-tools \ diff --git a/apps/api/package.json b/apps/api/package.json index c25a5fcc..fc19f06e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -16,6 +16,7 @@ "@kiwi/ai": "workspace:*", "@kiwi/auth": "workspace:*", "@kiwi/contracts": "workspace:*", + "@kiwi/connectors": "workspace:*", "@kiwi/db": "workspace:*", "@kiwi/files": "workspace:*", "@kiwi/graph": "workspace:*", diff --git a/apps/api/src/lib/__tests__/connectors.test.ts b/apps/api/src/lib/__tests__/connectors.test.ts new file mode 100644 index 00000000..f46a3fbc --- /dev/null +++ b/apps/api/src/lib/__tests__/connectors.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +type TestRepository = { + provider: "github"; + id: string; + fullName: string; + name: string; + htmlUrl: string; + defaultBranch: string; + private: boolean; +}; + +const branchRepositoryCalls: TestRepository[] = []; +let listRepositoriesCalls = 0; + +mock.module("@kiwi/connectors", () => ({ + createGitHubClient: () => ({ + provider: "github", + getRepository: async (repositoryId: string) => ({ + provider: "github", + id: "1", + fullName: repositoryId, + name: "app", + htmlUrl: `https://github.com/${repositoryId}`, + defaultBranch: "main", + private: true, + }), + listRepositories: async () => { + listRepositoriesCalls += 1; + return []; + }, + listBranches: async (repository: TestRepository) => { + branchRepositoryCalls.push(repository); + return [{ name: "main", commitSha: "commit-sha" }]; + }, + }), + createGitHubInstallationToken: async () => ({ token: "installation-token", expiresAt: "2026-01-01T01:00:00Z" }), + createGitLabClient: () => { + throw new Error("GitLab client was not expected"); + }, + getGitHubInstallationAccount: async () => ({ + login: "acme", + type: "organization", + repositorySelection: "selected", + }), +})); + +mock.module("@kiwi/connectors/credentials", () => ({ + decryptConnectorCredentials: () => ({ provider: "github", appId: "app-1", privateKeyPem: "pem" }), + decryptConnectorSecret: () => "secret", + encryptConnectorCredentials: () => "encrypted", + encryptConnectorSecret: () => "encrypted-secret", +})); + +mock.module("../env", () => ({ + env: { + AUTH_SECRET: "test-secret", + API_URL: "http://localhost:4321", + TRUSTED_ORIGINS: "http://localhost:3000", + }, +})); + +const { listProviderBranches } = await import("../connectors"); + +describe("connector library helpers", () => { + beforeEach(() => { + branchRepositoryCalls.length = 0; + listRepositoriesCalls = 0; + }); + + test("loads branches through direct repository lookup", async () => { + await expect( + listProviderBranches( + { + id: "connector-1", + provider: "github", + encryptedCredentials: "encrypted", + } as never, + { + id: "installation-1", + providerInstallationId: "99", + } as never, + "acme/app" + ) + ).resolves.toEqual([{ name: "main", commitSha: "commit-sha" }]); + + expect(listRepositoriesCalls).toBe(0); + expect(branchRepositoryCalls[0]).toMatchObject({ fullName: "acme/app" }); + }); +}); diff --git a/apps/api/src/lib/__tests__/graph-file-proxy.test.ts b/apps/api/src/lib/__tests__/graph-file-proxy.test.ts new file mode 100644 index 00000000..665f01a3 --- /dev/null +++ b/apps/api/src/lib/__tests__/graph-file-proxy.test.ts @@ -0,0 +1,177 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +let selectedFile: unknown; +let bindingRow: unknown = null; +let metadataCalls = 0; +let streamCalls = 0; +let selectCallCount = 0; +const providerReadCalls: Array<{ path: string; commitSha: string }> = []; + +function createSelectQuery() { + const chain = { + from: () => chain, + innerJoin: () => chain, + where: () => chain, + limit: () => { + selectCallCount += 1; + if (selectCallCount === 1) { + return selectedFile ? [selectedFile] : []; + } + return bindingRow ? [bindingRow] : []; + }, + }; + return chain; +} + +mock.module("@kiwi/db", () => ({ + db: { + select: () => createSelectQuery(), + }, +})); + +mock.module("@kiwi/files", () => ({ + deleteFile: async () => undefined, + getFileMetadata: async () => { + metadataCalls += 1; + return { size: 12, type: "text/plain", lastModified: null }; + }, + getFileStream: async () => { + streamCalls += 1; + return { content: new Blob(["hello world\n"]).stream(), size: 12, type: "text/plain" }; + }, + listFiles: async () => [], + putGraphFile: async () => ({ key: "key", type: "text/plain" }), +})); + +mock.module("../connectors", () => ({ + createProviderClient: async () => ({ + readFile: async (_repository: unknown, path: string, commitSha: string) => { + providerReadCalls.push({ path, commitSha }); + return "export const older = true;\n"; + }, + }), +})); + +// Dynamic import is required so module mocks are installed before graph-file-proxy is evaluated. +const { getGraphFileProxyResponse } = await import("../graph-file-proxy"); + +describe("graph file proxy", () => { + beforeEach(() => { + metadataCalls = 0; + streamCalls = 0; + selectCallCount = 0; + providerReadCalls.length = 0; + bindingRow = null; + selectedFile = { + key: "graphs/graph-1/file-1.txt", + name: "file.txt", + mimeType: "text/plain", + storageKind: "internal", + externalProvider: null, + externalUrl: null, + repositoryBindingId: null, + metadata: null, + }; + }); + + test("streams internal files from S3", async () => { + const result = await getGraphFileProxyResponse({ + graphId: "graph-1", + fileId: "file-1", + request: new Request("http://localhost/file"), + bucket: "bucket", + }); + + expect(result.status).toBe("ok"); + expect(metadataCalls).toBe(1); + expect(streamCalls).toBe(1); + if (result.status === "ok") { + expect(result.response.status).toBe(200); + } + }); + + test("redirects external GitHub files to immutable raw URLs without S3 calls", async () => { + selectedFile = { + key: "external:github:acme/widgets@commit-1:src/index.ts", + name: "widgets/src/index.ts", + mimeType: "text/plain", + storageKind: "external", + externalProvider: "github", + externalUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts", + repositoryBindingId: null, + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/index.ts", + external: { + provider: "github", + rawUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts", + htmlUrl: "https://github.com/acme/widgets/blob/commit-1/src/index.ts", + }, + }), + }; + + const result = await getGraphFileProxyResponse({ + graphId: "graph-1", + fileId: "file-1", + request: new Request("http://localhost/file"), + bucket: "bucket", + }); + + expect(result.status).toBe("ok"); + expect(metadataCalls).toBe(0); + expect(streamCalls).toBe(0); + expect(providerReadCalls).toEqual([]); + if (result.status === "ok") { + expect(result.response.status).toBe(307); + expect(result.response.headers.get("location")).toBe( + "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts" + ); + } + }); + + test("reads connector-backed files at the row commit without S3 access", async () => { + selectedFile = { + key: "connector:binding-1:commit-2:src/index.ts", + name: "widgets/src/index.ts", + mimeType: "text/plain", + storageKind: "external", + externalProvider: "github", + externalUrl: "https://raw.githubusercontent.com/acme/widgets/commit-2/src/index.ts", + repositoryBindingId: "binding-1", + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/index.ts", + }), + }; + bindingRow = { + binding: { + providerRepositoryId: "1", + repositoryFullName: "acme/widgets", + repositoryHtmlUrl: "https://github.com/acme/widgets", + branch: "main", + }, + installation: { status: "active" }, + connector: { provider: "github", status: "active" }, + }; + + const result = await getGraphFileProxyResponse({ + graphId: "graph-1", + fileId: "file-1", + request: new Request("http://localhost/file"), + bucket: "bucket", + }); + + expect(result.status).toBe("ok"); + expect(metadataCalls).toBe(0); + expect(streamCalls).toBe(0); + expect(providerReadCalls).toEqual([{ path: "src/index.ts", commitSha: "commit-1" }]); + if (result.status === "ok") { + expect(result.response.status).toBe(200); + await expect(result.response.text()).resolves.toBe("export const older = true;\n"); + } + }); +}); diff --git a/apps/api/src/lib/__tests__/repository-url-git.test.ts b/apps/api/src/lib/__tests__/repository-url-git.test.ts new file mode 100644 index 00000000..5790bd4e --- /dev/null +++ b/apps/api/src/lib/__tests__/repository-url-git.test.ts @@ -0,0 +1,103 @@ +import { EventEmitter } from "node:events"; +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +const MAX_GIT_OUTPUT_BYTES = 1 * 1024 * 1024; +let scenario: "broken-symlink" | "limit" | "symlink" = "limit"; +let spawnCall = 0; + +mock.module("node:child_process", () => ({ + spawn: () => { + spawnCall += 1; + const child = Object.assign(new EventEmitter(), { + stdout: new EventEmitter(), + stderr: new EventEmitter(), + kill: () => undefined, + }); + + queueMicrotask(() => { + if (spawnCall === 2) { + child.stdout.emit("data", Buffer.from("commit-sha\n")); + } + if (spawnCall === 3) { + child.stdout.emit( + "data", + Buffer.from( + scenario === "symlink" || scenario === "broken-symlink" + ? "config.ts\0src/index.ts\0" + : Buffer.alloc(MAX_GIT_OUTPUT_BYTES + 1) + ) + ); + } + child.emit("close", spawnCall === 3 && scenario === "limit" ? null : 0); + }); + + return child; + }, +})); + +mock.module("node:fs/promises", () => ({ + mkdtemp: async () => "/tmp/kiwi-repository-test", + readFile: async (filePath: string) => { + if (filePath.endsWith("config.ts")) { + return "server-secret"; + } + return "export const safe = true;\n"; + }, + realpath: async (filePath: string) => { + if (scenario === "broken-symlink" && filePath.endsWith("config.ts")) { + throw Object.assign(new Error("ENOENT: no such file or directory"), { code: "ENOENT" }); + } + if (filePath.endsWith("config.ts")) { + return "/etc/passwd"; + } + return filePath; + }, + rm: async () => undefined, + stat: async (filePath: string) => ({ + isFile: () => true, + size: filePath.endsWith("config.ts") ? "server-secret".length : "export const safe = true;\n".length, + }), +})); + +// Dynamic import is required so Bun module mocks replace child_process before evaluation. +const { loadRepositoryFromUrl } = await import("../repository-url"); + +describe("repository URL git loading", () => { + beforeEach(() => { + spawnCall = 0; + scenario = "limit"; + }); + + test("rejects truncated git stdout as a repository limit", async () => { + await expect(loadRepositoryFromUrl("https://github.com/acme/app")).rejects.toMatchObject({ + name: "RepositoryUrlError", + kind: "limit", + }); + }); + + test("skips repository files whose real path escapes the clone root", async () => { + scenario = "symlink"; + + await expect(loadRepositoryFromUrl("https://github.com/acme/app")).resolves.toMatchObject({ + files: [ + { + path: "src/index.ts", + content: "export const safe = true;\n", + }, + ], + }); + }); + + test("skips repository files whose real path cannot be resolved", async () => { + scenario = "broken-symlink"; + + await expect(loadRepositoryFromUrl("https://github.com/acme/app")).resolves.toMatchObject({ + files: [ + { + path: "src/index.ts", + content: "export const safe = true;\n", + }, + ], + }); + }); +}); diff --git a/apps/api/src/lib/__tests__/repository-url.test.ts b/apps/api/src/lib/__tests__/repository-url.test.ts new file mode 100644 index 00000000..e8c5a4d5 --- /dev/null +++ b/apps/api/src/lib/__tests__/repository-url.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test"; +import { isSupportedCodePath } from "@kiwi/graph/code/file-path"; +import { buildGitHubExternalCodeFile, normalizeRepositoryUrl } from "../repository-url"; + +describe("repository URL helpers", () => { + test("normalizes supported repository roots", () => { + expect(normalizeRepositoryUrl(" https://github.com/owner/repo ")).toEqual({ + url: "https://github.com/owner/repo.git", + name: "repo", + }); + expect(normalizeRepositoryUrl("https://gitlab.com/group/subgroup/repo.git?tab=readme#main")).toEqual({ + url: "https://gitlab.com/group/subgroup/repo.git", + name: "repo", + }); + expect(normalizeRepositoryUrl("https://bitbucket.org/team/repo.git")).toEqual({ + url: "https://bitbucket.org/team/repo.git", + name: "repo", + }); + }); + + test("rejects unsafe or non-root repository URLs", () => { + expect(() => normalizeRepositoryUrl("http://github.com/owner/repo")).toThrow("HTTPS"); + expect(() => normalizeRepositoryUrl("https://token@github.com/owner/repo")).toThrow("credentials"); + expect(() => normalizeRepositoryUrl("https://example.com/owner/repo")).toThrow("host is not supported"); + expect(() => normalizeRepositoryUrl("https://github.com/owner/repo/tree/main")).toThrow("repository root"); + expect(() => normalizeRepositoryUrl("https://github.com/owner")).toThrow("owner and repository"); + expect(() => normalizeRepositoryUrl("https://gitlab.com/group/repo/-/tree/main")).toThrow("repository root"); + }); + + test("recognizes supported code file paths without matching generated directories", () => { + expect(isSupportedCodePath("src/index.ts")).toBe(true); + expect(isSupportedCodePath("src/component.tsx")).toBe(true); + expect(isSupportedCodePath("src/script.js")).toBe(true); + expect(isSupportedCodePath("src/README.md")).toBe(false); + }); + + test("builds immutable GitHub external code links", () => { + expect( + buildGitHubExternalCodeFile({ + repositoryUrl: "https://github.com/owner/repo.git", + commitSha: "abc123", + path: "src/nested/my file.ts", + }) + ).toEqual({ + provider: "github", + rawUrl: "https://raw.githubusercontent.com/owner/repo/abc123/src/nested/my%20file.ts", + htmlUrl: "https://github.com/owner/repo/blob/abc123/src/nested/my%20file.ts", + key: "external:github:owner/repo@abc123:src/nested/my file.ts", + }); + expect( + buildGitHubExternalCodeFile({ + repositoryUrl: "https://gitlab.com/group/repo.git", + commitSha: "abc123", + path: "src/index.ts", + }) + ).toBeNull(); + }); +}); diff --git a/apps/api/src/lib/chat.ts b/apps/api/src/lib/chat.ts index a3d4caf4..b365aff3 100644 --- a/apps/api/src/lib/chat.ts +++ b/apps/api/src/lib/chat.ts @@ -28,6 +28,7 @@ import { chatTable, messageTable, type ChatMessage } from "@kiwi/db/tables/chats import { getDefaultOrganizationId } from "@kiwi/auth/server"; import { organizationPromptsTable, teamPromptsTable, userPromptsTable } from "@kiwi/db/tables/auth"; import { filesTable, graphPromptsTable, processRunsTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourcePredicate, visibleFilePredicate } from "@kiwi/db/source-validity"; import { error as logError, warn as logWarn } from "@kiwi/logger"; import { Result } from "better-result"; import { and, asc, desc, eq, inArray, isNotNull, isNull, sql } from "drizzle-orm"; @@ -707,7 +708,14 @@ export async function enrichCitation(graphId: string, sourceId: string): Promise .from(sourcesTable) .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) - .where(and(eq(sourcesTable.id, sourceId), eq(filesTable.graphId, graphId))) + .where( + and( + eq(sourcesTable.id, sourceId), + eq(filesTable.graphId, graphId), + currentSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) + ) + ) .limit(1); if (!row) { diff --git a/apps/api/src/lib/connector-access.ts b/apps/api/src/lib/connector-access.ts new file mode 100644 index 00000000..ef8e10ee --- /dev/null +++ b/apps/api/src/lib/connector-access.ts @@ -0,0 +1,120 @@ +import { db } from "@kiwi/db"; +import { + connectorInstallationsTable, + connectorsTable, + repositoryGraphBindingsTable, + type ConnectorProvider, +} from "@kiwi/db/tables/connectors"; +import { graphTable } from "@kiwi/db/tables/graph"; +import { and, eq } from "drizzle-orm"; +import type { AuthUser } from "../middleware/auth"; +import { API_ERROR_CODES } from "../types"; +import { assertCanCreateTopLevelGraph, assertCanViewGraphWithRootOwner } from "./graph-access"; +import { requireOrganizationAdmin, requireTeamGraphCreateAccess } from "./team-access"; + +export type ConnectorRow = typeof connectorsTable.$inferSelect; +export type ConnectorInstallationRow = typeof connectorInstallationsTable.$inferSelect; +export type RepositoryGraphBindingRow = typeof repositoryGraphBindingsTable.$inferSelect; + +export async function requireConnector(id: string): Promise { + const [connector] = await db.select().from(connectorsTable).where(eq(connectorsTable.id, id)).limit(1); + if (!connector) { + throw new Error(API_ERROR_CODES.GRAPH_NOT_FOUND); + } + return connector; +} + +export async function requireActiveConnector(id: string, provider?: ConnectorProvider): Promise { + const connector = await requireConnector(id); + if (connector.status !== "active" || (provider && connector.provider !== provider)) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + return connector; +} + +export async function assertCanManageConnectorOwner(user: AuthUser, input: { organizationId?: string; teamId?: string }) { + if (input.teamId) { + return requireTeamGraphCreateAccess(user, input.teamId); + } + + if (input.organizationId) { + return requireOrganizationAdmin(user, input.organizationId); + } + + return assertCanCreateTopLevelGraph(user); +} + +export async function assertCanUseInstallation(user: AuthUser, installationId: string): Promise { + const [installation] = await db + .select() + .from(connectorInstallationsTable) + .where(eq(connectorInstallationsTable.id, installationId)) + .limit(1); + + if (!installation || installation.status !== "active") { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + + await assertCanManageConnectorOwner(user, { + organizationId: installation.organizationId ?? undefined, + teamId: installation.teamId ?? undefined, + }); + return installation; +} + +export async function assertCanViewBinding(user: AuthUser, bindingId: string) { + const [row] = await db + .select({ binding: repositoryGraphBindingsTable, graph: graphTable }) + .from(repositoryGraphBindingsTable) + .innerJoin(graphTable, eq(graphTable.id, repositoryGraphBindingsTable.graphId)) + .where(eq(repositoryGraphBindingsTable.id, bindingId)) + .limit(1); + + if (!row) { + throw new Error(API_ERROR_CODES.GRAPH_NOT_FOUND); + } + + await assertCanViewGraphWithRootOwner(user, row.binding.graphId); + return row; +} + +export async function assertCanSyncBinding(user: AuthUser, bindingId: string) { + const row = await assertCanViewBinding(user, bindingId); + await assertCanManageConnectorOwner(user, { + organizationId: row.graph.organizationId ?? undefined, + teamId: row.graph.teamId ?? undefined, + }); + if (!row.binding.webhookEnabled) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + return row; +} + +export async function loadConnectorBindingGraph(bindingId: string) { + const [row] = await db + .select({ + binding: repositoryGraphBindingsTable, + installation: connectorInstallationsTable, + connector: connectorsTable, + graph: graphTable, + }) + .from(repositoryGraphBindingsTable) + .innerJoin( + connectorInstallationsTable, + eq(connectorInstallationsTable.id, repositoryGraphBindingsTable.connectorInstallationId) + ) + .innerJoin(connectorsTable, eq(connectorsTable.id, connectorInstallationsTable.connectorId)) + .innerJoin(graphTable, eq(graphTable.id, repositoryGraphBindingsTable.graphId)) + .where(eq(repositoryGraphBindingsTable.id, bindingId)) + .limit(1); + + return row ?? null; +} + +export function visibleInstallationWhere(user: AuthUser, connectorId: string) { + if (user.isSystemAdmin) { + return eq(connectorInstallationsTable.connectorId, connectorId); + } + + return and(eq(connectorInstallationsTable.connectorId, connectorId), eq(connectorInstallationsTable.status, "active")); +} diff --git a/apps/api/src/lib/connectors.ts b/apps/api/src/lib/connectors.ts new file mode 100644 index 00000000..96c32eeb --- /dev/null +++ b/apps/api/src/lib/connectors.ts @@ -0,0 +1,256 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; +import { + createGitHubClient, + createGitHubInstallationToken, + createGitLabClient, + getGitHubInstallationAccount, + type ConnectorProvider, + type GitHubConnectorCredentials, + type GitLabConnectorCredentials, + type GitLabInstallationCredentials, + type ProviderInstallationAccount, + type ProviderBranch, + type ProviderRepository, + type ProviderRepositoryClient, +} from "@kiwi/connectors"; +import { + decryptConnectorCredentials, + decryptConnectorSecret, + encryptConnectorCredentials, + encryptConnectorSecret, + type ConnectorSecretPayload, +} from "@kiwi/connectors/credentials"; +import type { connectorsTable, connectorInstallationsTable } from "@kiwi/db/tables/connectors"; +import { env } from "../env"; + +const frontendUrl = + env.TRUSTED_ORIGINS?.split(",") + .map((origin) => origin.trim()) + .find(Boolean) ?? "http://localhost:3000"; + +const STATE_VERSION = "v1"; +const STATE_TTL_MS = 10 * 60 * 1000; + +type ConnectorRow = typeof connectorsTable.$inferSelect; +type InstallationRow = typeof connectorInstallationsTable.$inferSelect; + +export type ConnectorState = { + purpose: "github-manifest" | "github-installation" | "gitlab-oauth"; + userId: string; + connectorId?: string; + organizationId?: string; + teamId?: string; + createdAt: number; +}; + +export type PublicConnector = { + id: string; + provider: ConnectorProvider; + name: string; + slug: string; + status: string; + appId: string | null; + clientId: string | null; + createdAt: string | null; + updatedAt: string | null; +}; + +export function toPublicConnector(row: ConnectorRow): PublicConnector { + return { + id: row.id, + provider: row.provider as ConnectorProvider, + name: row.name, + slug: row.slug, + status: row.status, + appId: row.appId, + clientId: row.clientId, + createdAt: row.createdAt?.toISOString() ?? null, + updatedAt: row.updatedAt?.toISOString() ?? null, + }; +} + +export function toPublicInstallation(row: InstallationRow) { + return { + id: row.id, + connectorId: row.connectorId, + provider: row.provider as ConnectorProvider, + providerInstallationId: row.providerInstallationId, + providerAccountLogin: row.providerAccountLogin, + providerAccountType: row.providerAccountType, + organizationId: row.organizationId, + teamId: row.teamId, + repositorySelection: row.repositorySelection, + status: row.status, + createdAt: row.createdAt?.toISOString() ?? null, + updatedAt: row.updatedAt?.toISOString() ?? null, + }; +} + +export function encryptCredentials(value: ConnectorSecretPayload): string { + return encryptConnectorCredentials(value, env.AUTH_SECRET); +} + +export function encryptSecret(value: string): string { + return encryptConnectorSecret(value, env.AUTH_SECRET); +} + +export function decryptSecret(value: string): string { + return decryptConnectorSecret(value, env.AUTH_SECRET); +} + +function isGitHubConnectorCredentials(value: ConnectorSecretPayload): value is GitHubConnectorCredentials { + return "provider" in value && value.provider === "github"; +} + +function isGitLabConnectorCredentials(value: ConnectorSecretPayload): value is GitLabConnectorCredentials { + return "provider" in value && value.provider === "gitlab" && "baseUrl" in value; +} + +function isGitLabInstallationCredentials(value: ConnectorSecretPayload): value is GitLabInstallationCredentials { + return "provider" in value && value.provider === "gitlab" && "accessToken" in value; +} + +export function signConnectorState(state: Omit): string { + const payload = Buffer.from(JSON.stringify({ ...state, createdAt: Date.now() })).toString("base64url"); + const signature = createHmac("sha256", env.AUTH_SECRET).update(payload).digest("base64url"); + return `${STATE_VERSION}.${payload}.${signature}`; +} + +export function verifyConnectorState( + value: string, + purpose: ConnectorState["purpose"], + userId?: string +): ConnectorState | null { + const [version, payload, signature] = value.split("."); + if (version !== STATE_VERSION || !payload || !signature) { + return null; + } + + const expected = createHmac("sha256", env.AUTH_SECRET).update(payload).digest("base64url"); + const actualBuffer = Buffer.from(signature); + const expectedBuffer = Buffer.from(expected); + if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) { + return null; + } + + const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as ConnectorState; + if (parsed.purpose !== purpose || Date.now() - parsed.createdAt > STATE_TTL_MS) { + return null; + } + + if (userId && parsed.userId !== userId) { + return null; + } + + return parsed; +} + +export function createManifestUrl(state: string, name: string): string { + const manifest = { + name, + url: frontendUrl, + hook_attributes: { + url: `${env.API_URL ?? "http://localhost:4321"}/connectors/webhooks/github`, + }, + redirect_url: `${frontendUrl}/connectors/github/callback`, + setup_url: `${frontendUrl}/connectors/github/install/callback`, + public: false, + default_permissions: { + contents: "read", + metadata: "read", + }, + default_events: ["push", "installation", "installation_repositories"], + }; + const url = new URL("https://github.com/settings/apps/new"); + url.searchParams.set("state", state); + url.searchParams.set("manifest", JSON.stringify(manifest)); + return url.href; +} + +export async function exchangeGitHubManifestCode(code: string) { + const response = await fetch(`https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, { + method: "POST", + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "kiwi-connectors", + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + + if (!response.ok) { + throw new Error("Invalid connector manifest code"); + } + + return (await response.json()) as { + id: number | string; + slug?: string; + name: string; + client_id?: string; + client_secret?: string; + pem: string; + webhook_secret?: string; + }; +} + +export async function createProviderClient( + connector: ConnectorRow, + installation: InstallationRow +): Promise { + const credentials = decryptConnectorCredentials(connector.encryptedCredentials, env.AUTH_SECRET); + if (connector.provider === "github") { + if (!isGitHubConnectorCredentials(credentials)) { + throw new Error("Invalid connector credentials"); + } + const token = await createGitHubInstallationToken({ + credentials, + installationId: installation.providerInstallationId, + }); + return createGitHubClient({ installationToken: token.token }); + } + + if (!isGitLabConnectorCredentials(credentials)) { + throw new Error("Invalid connector credentials"); + } + const installationCredentials = installation.encryptedCredentials + ? decryptConnectorCredentials(installation.encryptedCredentials, env.AUTH_SECRET) + : null; + if (!installationCredentials || !isGitLabInstallationCredentials(installationCredentials)) { + throw new Error("Invalid connector installation credentials"); + } + return createGitLabClient({ + baseUrl: credentials.baseUrl, + accessToken: installationCredentials.accessToken, + }); +} + +export async function getGitHubConnectorInstallationAccount( + connector: ConnectorRow, + installationId: string +): Promise { + const credentials = decryptConnectorCredentials(connector.encryptedCredentials, env.AUTH_SECRET); + if (connector.provider !== "github" || !isGitHubConnectorCredentials(credentials)) { + throw new Error("Invalid connector credentials"); + } + + return getGitHubInstallationAccount({ + credentials, + installationId, + }); +} + +export async function listProviderRepositories( + connector: ConnectorRow, + installation: InstallationRow +): Promise { + return (await createProviderClient(connector, installation)).listRepositories(); +} + +export async function listProviderBranches( + connector: ConnectorRow, + installation: InstallationRow, + repositoryId: string +): Promise { + const client = await createProviderClient(connector, installation); + const repository = await client.getRepository(repositoryId); + return client.listBranches(repository); +} diff --git a/apps/api/src/lib/graph-file-proxy.ts b/apps/api/src/lib/graph-file-proxy.ts index 3c15315e..3c7ea614 100644 --- a/apps/api/src/lib/graph-file-proxy.ts +++ b/apps/api/src/lib/graph-file-proxy.ts @@ -1,13 +1,22 @@ +import type { ConnectorProvider, ProviderRepository } from "@kiwi/connectors"; import { and, eq } from "drizzle-orm"; import { db } from "@kiwi/db"; +import { connectorInstallationsTable, connectorsTable, repositoryGraphBindingsTable } from "@kiwi/db/tables/connectors"; import { filesTable } from "@kiwi/db/tables/graph"; import { getFileMetadata, getFileStream } from "@kiwi/files"; +import { parseCodeFileMetadata } from "@kiwi/graph/code/metadata"; +import { createProviderClient } from "./connectors"; import { contentDispositionForFile, contentDispositionHeader, parseByteRange } from "./file-proxy"; export type GraphFileProxyRecord = { key: string; name: string; mimeType: string; + storageKind: string; + externalProvider: string | null; + externalUrl: string | null; + repositoryBindingId: string | null; + metadata: string | null; }; export type GraphFileProxyResult = @@ -42,6 +51,11 @@ export async function loadGraphFileForProxy(graphId: string, fileId: string): Pr key: filesTable.key, name: filesTable.name, mimeType: filesTable.mimeType, + storageKind: filesTable.storageKind, + externalProvider: filesTable.externalProvider, + externalUrl: filesTable.externalUrl, + repositoryBindingId: filesTable.repositoryBindingId, + metadata: filesTable.metadata, }) .from(filesTable) .where(and(eq(filesTable.graphId, graphId), eq(filesTable.id, fileId), eq(filesTable.deleted, false))) @@ -62,6 +76,56 @@ export async function getGraphFileProxyResponse(options: { return { status: "not_found" }; } + if (file.storageKind === "external") { + if (file.repositoryBindingId) { + const content = await readConnectorFile(file.repositoryBindingId, file.metadata); + if (content === null) { + return { status: "not_found" }; + } + const bytes = new TextEncoder().encode(content); + const range = parseByteRange(options.request.headers.get("range"), bytes.byteLength); + if (range === "invalid") { + return { status: "invalid_range", size: bytes.byteLength }; + } + const body = range ? bytes.slice(range.start, range.end + 1) : bytes; + const headers = new Headers({ + "Accept-Ranges": "bytes", + "Cache-Control": "private, no-cache", + "Content-Length": String(body.byteLength), + "Content-Type": file.mimeType || "text/plain; charset=utf-8", + "X-Content-Type-Options": "nosniff", + }); + if (range) { + headers.set("Content-Range", `bytes ${range.start}-${range.end}/${bytes.byteLength}`); + } + return { + status: "ok", + response: new Response(options.head ? null : body, { status: range ? 206 : 200, headers }), + }; + } + + if (file.externalProvider !== "github" || !file.externalUrl) { + return { status: "not_found" }; + } + + const metadata = parseCodeFileMetadata(file.metadata); + if (!metadata?.external || metadata.external.provider !== "github") { + return { status: "not_found" }; + } + + return { + status: "ok", + response: new Response(null, { + status: 307, + headers: { + "Cache-Control": "private, no-cache", + Location: file.externalUrl, + "X-Content-Type-Options": "nosniff", + }, + }), + }; + } + const metadata = await getFileMetadata(file.key, options.bucket); if (!metadata) { return { status: "not_found" }; @@ -117,3 +181,41 @@ export async function getGraphFileProxyResponse(options: { }), }; } + +async function readConnectorFile(bindingId: string, metadataValue: string | null): Promise { + const metadata = parseCodeFileMetadata(metadataValue); + if (!metadata) { + return null; + } + + const [row] = await db + .select({ + binding: repositoryGraphBindingsTable, + installation: connectorInstallationsTable, + connector: connectorsTable, + }) + .from(repositoryGraphBindingsTable) + .innerJoin( + connectorInstallationsTable, + eq(connectorInstallationsTable.id, repositoryGraphBindingsTable.connectorInstallationId) + ) + .innerJoin(connectorsTable, eq(connectorsTable.id, connectorInstallationsTable.connectorId)) + .where(eq(repositoryGraphBindingsTable.id, bindingId)) + .limit(1); + + if (!row || row.connector.status !== "active" || row.installation.status !== "active") { + return null; + } + + const client = await createProviderClient(row.connector, row.installation); + const repository: ProviderRepository = { + provider: row.connector.provider as ConnectorProvider, + id: row.binding.providerRepositoryId, + fullName: row.binding.repositoryFullName, + name: row.binding.repositoryFullName.split("/").at(-1) ?? row.binding.repositoryFullName, + htmlUrl: row.binding.repositoryHtmlUrl, + defaultBranch: row.binding.branch, + private: true, + }; + return client.readFile(repository, metadata.path, metadata.commitSha); +} diff --git a/apps/api/src/lib/graph-route.ts b/apps/api/src/lib/graph-route.ts index f710b787..aaa37d77 100644 --- a/apps/api/src/lib/graph-route.ts +++ b/apps/api/src/lib/graph-route.ts @@ -15,7 +15,7 @@ import { and, asc, eq, inArray, sql } from "drizzle-orm"; import { env } from "../env"; import { API_ERROR_CODES, errorResponse } from "../types"; import type { GraphDetailFileRecord, GraphFileRecord, GraphListItem, GraphRecentChatItem } from "../types/routes"; -import { type GraphRecord } from "./graph-access"; +import { selectGraphFields, type GraphRecord } from "./graph-access"; import { buildDeleteStepProgress, buildProcessPercentage, @@ -43,6 +43,11 @@ export type UploadedFile = { mimeType: string; key: string; checksum?: string; + metadata?: string; + storageKind?: "internal" | "external"; + externalUrl?: string; + externalProvider?: string; + repositoryBindingId?: string; }; export type CreatedFileRecord = GraphFileRecord; export type GraphFileRow = Omit & { @@ -199,6 +204,17 @@ function buildTimeEstimate( return buildProcessTimeEstimate(estimatedFiles, descriptionProgress); } +function textArray(values: readonly string[]) { + if (values.length === 0) { + return sql`ARRAY[]::text[]`; + } + + return sql`ARRAY[${sql.join( + values.map((value) => sql`${value}`), + sql`, ` + )}]::text[]`; +} + function textList(values: readonly string[]) { return sql.join( values.map((value) => sql`${value}`), @@ -271,7 +287,8 @@ export const restoreGraphFileChangeFailure = async ( previousGraph: GraphRecord, addedFileIds: string[], uploadedKeys: string[], - processRunId?: string + processRunId?: string, + supersededFileIds: string[] = [] ) => { const failedDeletes = await cleanupUploadedKeys(uploadedKeys); @@ -284,6 +301,9 @@ export const restoreGraphFileChangeFailure = async ( if (addedFileIds.length > 0) { await tx.delete(filesTable).where(inArray(filesTable.id, addedFileIds)); } + if (supersededFileIds.length > 0) { + await tx.update(filesTable).set({ deleted: false }).where(inArray(filesTable.id, supersededFileIds)); + } await tx .update(graphTable) @@ -315,6 +335,108 @@ export const restoreGraphFileChangeFailure = async ( } }; +export async function commitGraphFileUploads(options: { + graph: GraphRecord; + uploadedFiles: UploadedFile[]; + supersedeRepositoryUrls?: string[]; +}): Promise<{ graph: GraphRecord; addedFiles: CreatedFileRecord[]; processRunId?: string; supersededFileIds: string[] }> { + const result = await db.transaction(async (tx) => { + const uploadedFileIds = options.uploadedFiles.map((file) => file.id); + const supersededFiles = + options.supersedeRepositoryUrls && options.supersedeRepositoryUrls.length > 0 + ? await tx + .update(filesTable) + .set({ deleted: true }) + .where( + and( + eq(filesTable.graphId, options.graph.id), + eq(filesTable.type, "code"), + eq(filesTable.deleted, false), + sql`${filesTable.id} <> ALL(${textArray(uploadedFileIds)})`, + sql`(${filesTable.metadata}::jsonb ->> 'repositoryUrl') = ANY(${textArray( + options.supersedeRepositoryUrls + )})` + ) + ) + .returning({ id: filesTable.id }) + : []; + + const insertedFiles = await tx + .insert(filesTable) + .values( + options.uploadedFiles.map((file) => ({ + id: file.id, + graphId: options.graph.id, + name: file.name, + size: file.size, + type: file.type, + mimeType: file.mimeType, + key: file.key, + storageKind: file.storageKind, + externalUrl: file.externalUrl, + externalProvider: file.externalProvider, + repositoryBindingId: file.repositoryBindingId, + checksum: file.checksum, + metadata: file.metadata, + })) + ) + .onConflictDoNothing() + .returning(selectFileFields); + + if (insertedFiles.length === 0) { + if (supersededFiles.length > 0) { + throw new Error("Repository snapshot did not insert replacement files"); + } + + return { + graph: options.graph, + addedFiles: insertedFiles, + processRunId: undefined, + supersededFileIds: [], + }; + } + + const [updatedGraph] = await tx + .update(graphTable) + .set({ state: "updating" }) + .where(eq(graphTable.id, options.graph.id)) + .returning(selectGraphFields); + + const [processRun] = await tx + .insert(processRunsTable) + .values({ + graphId: options.graph.id, + status: "pending", + }) + .returning({ id: processRunsTable.id }); + if (!processRun) { + throw new Error("Failed to create process run"); + } + + await tx.insert(processRunFilesTable).values( + insertedFiles.map((file) => ({ + processRunId: processRun.id, + fileId: file.id, + })) + ); + + return { + graph: updatedGraph ?? options.graph, + addedFiles: insertedFiles, + processRunId: processRun.id, + supersededFileIds: supersededFiles.map((file) => file.id), + }; + }); + + const addedKeys = new Set(result.addedFiles.map((file) => file.key)); + const skippedKeys = options.uploadedFiles.map((file) => file.key).filter((key) => !addedKeys.has(key)); + if (skippedKeys.length > 0) { + await cleanupUploadedKeys(skippedKeys); + } + + return result; +} + export function mapGraphError(statusFn: StatusFn, error: unknown) { if (!(error instanceof Error)) { return statusFn(500, errorResponse("Internal server error", API_ERROR_CODES.INTERNAL_SERVER_ERROR)); diff --git a/apps/api/src/lib/graph-upload-file-type.ts b/apps/api/src/lib/graph-upload-file-type.ts index c38ce20a..55829028 100644 --- a/apps/api/src/lib/graph-upload-file-type.ts +++ b/apps/api/src/lib/graph-upload-file-type.ts @@ -8,6 +8,9 @@ export type FileWithChecksum = { export type SupportedFileWithChecksum = FileWithChecksum & { type: GraphFileType; }; +type UploadModelFile = { + type: GraphFileType; +}; export type UploadFileTypeCheck = | { ok: true; files: SupportedFileWithChecksum[] } | { ok: false; fileName: string; message: string }; @@ -37,7 +40,7 @@ export function unsupportedUploadResponse(statusFn: StatusFn, check: Extract segment.trim()) + .filter(Boolean); + if (segments.length < 2) { + throw new RepositoryUrlError("validation", "Repository URL must include an owner and repository name"); + } + + if ((host === "github.com" || host === "bitbucket.org") && segments.length !== 2) { + throw new RepositoryUrlError("validation", "Repository URL must point to a repository root"); + } + + if (host === "gitlab.com" && segments.includes("-")) { + throw new RepositoryUrlError("validation", "Repository URL must point to a repository root"); + } + + const repositoryName = segments[segments.length - 1]!.replace(/\.git$/u, "") || segments[segments.length - 1]!; + parsed.hash = ""; + parsed.search = ""; + parsed.username = ""; + parsed.password = ""; + + const normalizedPath = `/${segments.join("/").replace(/\.git$/u, "")}.git`; + return { + url: `https://${host}${normalizedPath}`, + name: repositoryName, + }; +} + +type GitHubRepositoryParts = { + owner: string; + repo: string; +}; + +export type GitHubExternalCodeFile = { + provider: "github"; + rawUrl: string; + htmlUrl: string; + key: string; +}; + +function githubRepositoryParts(repositoryUrl: string): GitHubRepositoryParts | null { + const parsed = new URL(repositoryUrl); + if (parsed.protocol !== "https:" || parsed.hostname !== "github.com") { + return null; + } + + const segments = parsed.pathname + .split("/") + .map((segment) => segment.trim()) + .filter(Boolean); + if (segments.length !== 2) { + return null; + } + + return { + owner: segments[0]!, + repo: segments[1]!.replace(/\.git$/u, ""), + }; +} + +function encodePathSegments(value: string): string { + return value.split("/").map(encodeURIComponent).join("/"); +} + +export function buildGitHubExternalCodeFile(options: { + repositoryUrl: string; + commitSha: string; + path: string; +}): GitHubExternalCodeFile | null { + const parts = githubRepositoryParts(options.repositoryUrl); + if (!parts) { + return null; + } + + const owner = encodeURIComponent(parts.owner); + const repo = encodeURIComponent(parts.repo); + const commitSha = encodeURIComponent(options.commitSha); + const filePath = encodePathSegments(options.path); + const keyPath = options.path.replaceAll("\\", "/"); + + return { + provider: "github", + rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${commitSha}/${filePath}`, + htmlUrl: `https://github.com/${owner}/${repo}/blob/${commitSha}/${filePath}`, + key: `external:github:${parts.owner}/${parts.repo}@${options.commitSha}:${keyPath}`, + }; +} + +export async function loadRepositoryFromUrl(input: string): Promise { + const repository = normalizeRepositoryUrl(input); + const tempDir = await mkdtemp(path.join(tmpdir(), "kiwi-repository-")); + const repoPath = path.join(tempDir, "repo"); + + try { + await runGit(["clone", "--depth", "1", "--", repository.url, repoPath], tempDir); + const commitSha = (await runGit(["rev-parse", "HEAD"], repoPath)).trim(); + const listedFiles = (await runGit(["ls-files", "-z"], repoPath)).split("\0").filter(Boolean); + const files: RepositorySourceFile[] = []; + const repoRoot = await realpath(repoPath); + let totalBytes = 0; + + for (const filePath of listedFiles) { + if (!shouldLoadCodePath(filePath)) { + continue; + } + + const absolutePath = path.resolve(repoPath, filePath); + if (!isPathInsideRoot(repoRoot, absolutePath)) { + continue; + } + + let realFilePath: string; + try { + realFilePath = await realpath(absolutePath); + } catch { + continue; + } + if (!isPathInsideRoot(repoRoot, realFilePath)) { + continue; + } + + const info = await stat(realFilePath); + if (!info.isFile() || info.size > MAX_REPOSITORY_CODE_FILE_BYTES) { + continue; + } + + if (files.length + 1 > MAX_REPOSITORY_CODE_FILES) { + throw new RepositoryUrlError("limit", "Repository contains too many supported code files"); + } + + if (totalBytes + info.size > MAX_REPOSITORY_CODE_BYTES) { + throw new RepositoryUrlError("limit", "Repository contains too much supported code"); + } + + const content = await readFile(realFilePath, "utf8"); + files.push({ path: filePath, content, size: info.size }); + totalBytes += info.size; + } + + return { + url: repository.url, + name: repository.name, + commitSha, + files, + }; + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + +function isPathInsideRoot(root: string, target: string): boolean { + const relative = path.relative(root, target); + return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative); +} + +function shouldLoadCodePath(filePath: string): boolean { + const normalized = filePath.replaceAll("\\", "/"); + if (!isSupportedCodePath(normalized)) { + return false; + } + + return normalized.split("/").every((segment) => !SKIPPED_PATH_SEGMENTS.has(segment)); +} + +function runGit(args: string[], cwd: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn("git", args, { + cwd, + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + GIT_TERMINAL_PROMPT: "0", + }, + }); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + let stdoutBytes = 0; + let stderrBytes = 0; + let stdoutLimitExceeded = false; + let settled = false; + + const finish = (callback: () => void) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(timeout); + callback(); + }; + + const timeout = setTimeout(() => { + child.kill("SIGKILL"); + finish(() => + reject( + new RepositoryUrlError("load", "Repository could not be loaded", { + cause: new Error("Repository git command timed out"), + }) + ) + ); + }, GIT_COMMAND_TIMEOUT_MS); + + child.stdout.on("data", (chunk: Buffer) => { + stdoutBytes += chunk.byteLength; + if (stdoutBytes > MAX_GIT_OUTPUT_BYTES) { + stdoutLimitExceeded = true; + child.kill("SIGKILL"); + return; + } + stdout.push(chunk); + }); + child.stderr.on("data", (chunk: Buffer) => { + stderrBytes += chunk.byteLength; + if (stderrBytes <= MAX_GIT_OUTPUT_BYTES) { + stderr.push(chunk); + } + }); + child.on("error", (error) => + finish(() => + reject( + stdoutLimitExceeded + ? new RepositoryUrlError("limit", "Repository git output exceeded the supported size") + : new RepositoryUrlError("load", "Repository could not be loaded", { cause: error }) + ) + ) + ); + child.on("close", (code) => { + if (stdoutLimitExceeded) { + finish(() => + reject(new RepositoryUrlError("limit", "Repository git output exceeded the supported size")) + ); + return; + } + + if (code === 0) { + finish(() => resolve(Buffer.concat(stdout).toString("utf8"))); + return; + } + + finish(() => { + const stderrText = Buffer.concat(stderr).toString("utf8").trim(); + reject( + new RepositoryUrlError("load", "Repository could not be loaded", { + cause: new Error(stderrText || "Repository git command failed"), + }) + ); + }); + }); + }); +} diff --git a/apps/api/src/lib/source-reference.ts b/apps/api/src/lib/source-reference.ts index 4723eb00..607f1841 100644 --- a/apps/api/src/lib/source-reference.ts +++ b/apps/api/src/lib/source-reference.ts @@ -1,6 +1,7 @@ import { and, eq, inArray } from "drizzle-orm"; import { db } from "@kiwi/db"; import { filesTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourcePredicate, visibleFilePredicate } from "@kiwi/db/source-validity"; import { getFile } from "@kiwi/files"; import type { SourceReferenceBatchSuccessData } from "@kiwi/contracts"; import { env } from "../env"; @@ -103,7 +104,14 @@ async function loadSourceReferenceRow(graphId: string, sourceId: string): Promis .from(sourcesTable) .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) - .where(and(eq(sourcesTable.id, sourceId), eq(filesTable.graphId, graphId), eq(filesTable.deleted, false))) + .where( + and( + eq(sourcesTable.id, sourceId), + eq(filesTable.graphId, graphId), + currentSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) + ) + ) .limit(1); return row ? normalizeSourceReferenceRow(row) : null; @@ -116,7 +124,12 @@ async function loadSourceReferenceRows(graphId: string, sourceIds: string[]): Pr .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) .where( - and(inArray(sourcesTable.id, sourceIds), eq(filesTable.graphId, graphId), eq(filesTable.deleted, false)) + and( + inArray(sourcesTable.id, sourceIds), + eq(filesTable.graphId, graphId), + currentSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) + ) ); return rows.map(normalizeSourceReferenceRow); diff --git a/apps/api/src/lib/team-chat.ts b/apps/api/src/lib/team-chat.ts index ea02f43a..f2468b54 100644 --- a/apps/api/src/lib/team-chat.ts +++ b/apps/api/src/lib/team-chat.ts @@ -14,6 +14,7 @@ import { createRequestInformationSection } from "@kiwi/ai/prompts/request-info.p import { db } from "@kiwi/db"; import type { ChatMessage } from "@kiwi/db/tables/chats"; import { filesTable, graphTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourcePredicate, visibleFilePredicate } from "@kiwi/db/source-validity"; import { generateText, stepCountIs, tool, type ModelMessage, type ToolSet } from "ai"; import { and, asc, eq, inArray, isNull } from "drizzle-orm"; import { z } from "zod"; @@ -511,7 +512,7 @@ export async function enrichTeamCitation( .from(sourcesTable) .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) - .where(eq(sourcesTable.id, sourceId)) + .where(and(eq(sourcesTable.id, sourceId), currentSourcePredicate(sourcesTable), visibleFilePredicate(filesTable))) .limit(1); if (!row) { diff --git a/apps/api/src/routes/__tests__/connector-webhooks.test.ts b/apps/api/src/routes/__tests__/connector-webhooks.test.ts new file mode 100644 index 00000000..8b6c4c1d --- /dev/null +++ b/apps/api/src/routes/__tests__/connector-webhooks.test.ts @@ -0,0 +1,163 @@ +import { createHmac } from "node:crypto"; +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +type SelectResult = { kind: "where"; value: unknown[] } | { kind: "limit"; value: unknown[] }; + +const connector = { + id: "connector-1", + provider: "github", + status: "active", + webhookSecretEncrypted: "encrypted-secret", +}; + +const bindingOne = { id: "binding-1" }; +const bindingTwo = { id: "binding-2" }; +const ledger = { id: "event-1", status: "enqueued" }; +const failedLedger = { id: "event-1", status: "failed" }; + +const selectResults: SelectResult[] = []; +const insertResults: unknown[][] = []; +const updates: Array> = []; +const joins: unknown[] = []; +const workflowInputs: Array> = []; +let workflowFailureIndex: number | null = null; + +function selectQuery() { + const result = selectResults.shift(); + if (!result) { + throw new Error("Unexpected select call"); + } + + const chain = { + from: () => chain, + innerJoin: (...args: unknown[]) => { + joins.push(args); + return chain; + }, + where: () => (result.kind === "where" ? result.value : chain), + limit: () => result.value, + }; + return chain; +} + +mock.module("@kiwi/db", () => ({ + db: { + select: () => selectQuery(), + insert: () => ({ + values: () => ({ + onConflictDoNothing: () => ({ + returning: () => insertResults.shift() ?? [], + }), + }), + }), + update: () => ({ + set: (values: Record) => ({ + where: async () => { + updates.push(values); + return undefined; + }, + }), + }), + }, +})); + +mock.module("../../lib/connectors", () => ({ + decryptSecret: () => "secret", +})); + +mock.module("../../openworkflow", () => ({ + ow: { + runWorkflow: async (_spec: unknown, input: Record) => { + workflowInputs.push(input); + if (workflowFailureIndex === workflowInputs.length) { + throw new Error("enqueue failed"); + } + return { workflowRun: { id: `run-${workflowInputs.length}` } }; + }, + }, +})); + +function signedGitHubRequest(body: string) { + const signature = `sha256=${createHmac("sha256", "secret").update(body).digest("hex")}`; + return new Request("http://localhost/connectors/webhooks/github", { + method: "POST", + headers: { + "x-hub-signature-256": signature, + "x-github-event": "push", + "x-github-delivery": "delivery-1", + }, + body, + }); +} + +function pushBody() { + return JSON.stringify({ + ref: "refs/heads/main", + after: "commit-new", + repository: { id: 7, full_name: "acme/app" }, + }); +} + +// Dynamic import is required so Bun module mocks are installed before the route module is evaluated. +const { connectorWebhookRoute } = await import("../connector-webhooks"); + +describe("connector webhook route", () => { + beforeEach(() => { + selectResults.length = 0; + insertResults.length = 0; + updates.length = 0; + joins.length = 0; + workflowInputs.length = 0; + workflowFailureIndex = null; + }); + + test("returns a structured 400 for signed invalid JSON", async () => { + selectResults.push({ kind: "where", value: [connector] }); + + const response = await connectorWebhookRoute.handle(signedGitHubRequest("not-json")); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + status: "error", + code: "INVALID_CHAT_REQUEST", + }); + expect(insertResults).toEqual([]); + }); + + test("marks failed workflow enqueues so delivery retries can recover", async () => { + selectResults.push( + { kind: "where", value: [connector] }, + { kind: "where", value: [{ binding: bindingOne }, { binding: bindingTwo }] } + ); + insertResults.push([ledger]); + workflowFailureIndex = 2; + + const response = await connectorWebhookRoute.handle(signedGitHubRequest(pushBody())); + + expect(response.status).toBe(500); + expect(workflowInputs.map((input) => input.bindingId)).toEqual(["binding-1", "binding-2"]); + expect(updates).toContainEqual({ lastSeenCommitSha: "commit-new", syncStatus: "pending", syncErrorCode: null }); + expect(updates).toContainEqual({ syncStatus: "failed", syncErrorCode: "enqueue_failed" }); + expect(updates).toContainEqual({ status: "failed", errorCode: "enqueue_failed" }); + expect(joins).toHaveLength(1); + }); + + test("re-enqueues failed duplicate webhook deliveries", async () => { + selectResults.push( + { kind: "where", value: [connector] }, + { kind: "where", value: [{ binding: bindingOne }, { binding: bindingTwo }] }, + { kind: "limit", value: [failedLedger] } + ); + insertResults.push([]); + + const response = await connectorWebhookRoute.handle(signedGitHubRequest(pushBody())); + + expect(response.status).toBe(202); + await expect(response.json()).resolves.toMatchObject({ + status: "success", + data: { status: "enqueued", enqueued: 2 }, + }); + expect(workflowInputs.map((input) => input.bindingId)).toEqual(["binding-1", "binding-2"]); + expect(updates).toContainEqual({ status: "enqueued", errorCode: null }); + }); +}); diff --git a/apps/api/src/routes/__tests__/connectors.test.ts b/apps/api/src/routes/__tests__/connectors.test.ts new file mode 100644 index 00000000..e3c63fdb --- /dev/null +++ b/apps/api/src/routes/__tests__/connectors.test.ts @@ -0,0 +1,335 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import { Elysia } from "elysia"; + +const authUser = { + id: "user-1", + activeOrganizationId: "org-1", + activeTeamId: null, + isSystemAdmin: false, +}; + +const connector = { + id: "connector-1", + provider: "github", + status: "active", + encryptedCredentials: "encrypted", + appSlug: "kiwi-app", +}; + +const insertValues: Array> = []; +const conflictConfigs: Array> = []; +const installationAccountCalls: string[] = []; +const listBranchesCalls: Array> = []; +const listRepositoriesCalls: Array> = []; +const updateValues: Array> = []; +const signedStates: Array> = []; +const organizationAdminChecks: Array = []; +let installationConnectorId = "connector-1"; +let teamAccessOrganizationId = "org-1"; +let verifiedState: Record = { + purpose: "github-installation", + userId: "user-1", + connectorId: "connector-1", + organizationId: "org-1", + createdAt: Date.now(), +}; +let workflowError: Error | null = null; + +type MockDb = { + insert: () => { + values: (values: Record) => { + onConflictDoUpdate: (config: Record) => { + returning: () => Array>; + }; + returning: () => Array>; + }; + }; + update: () => { + set: (values: Record) => { + where: () => { + returning: () => never[]; + }; + }; + }; + select: () => { + from: () => { + orderBy: () => never[]; + }; + }; + transaction: (callback: (tx: MockDb) => unknown) => Promise; +}; + +mock.module("../../middleware/auth", () => ({ + authMiddleware: new Elysia().derive({ as: "scoped" }, () => ({ + user: authUser, + })), +})); + +mock.module("../../lib/graph-access", () => ({ + assertCanCreateTeamGraph: async () => ({ team: { id: "team-1", organizationId: teamAccessOrganizationId } }), +})); + +mock.module("../../lib/team-access", () => ({ + requireOrganizationAdmin: async (_user: unknown, organizationId?: string) => { + organizationAdminChecks.push(organizationId); + return { organizationId: organizationId ?? "org-1" }; + }, +})); + +mock.module("../../lib/connector-access", () => ({ + assertCanSyncBinding: async () => ({ binding: { id: "binding-1" } }), + assertCanUseInstallation: async () => ({ + id: "installation-1", + connectorId: installationConnectorId, + provider: "github", + organizationId: "org-1", + teamId: null, + }), + assertCanViewBinding: async () => ({ binding: { id: "binding-1" } }), + requireActiveConnector: async (id: string) => ({ ...connector, id }), +})); + +mock.module("../../lib/connectors", () => ({ + createManifestUrl: () => "https://github.com/settings/apps/new", + encryptCredentials: () => "encrypted", + encryptSecret: () => "encrypted-secret", + exchangeGitHubManifestCode: async () => { + throw new Error("manifest exchange not expected"); + }, + getGitHubConnectorInstallationAccount: async (_connector: unknown, installationId: string) => { + installationAccountCalls.push(installationId); + return { + login: "acme", + type: "organization", + repositorySelection: "selected", + }; + }, + listProviderBranches: async (_connector: unknown, _installation: unknown, repositoryId: string) => { + listBranchesCalls.push({ repositoryId }); + return [{ name: "main", commitSha: "commit-sha" }]; + }, + listProviderRepositories: async (connector: { id: string }, installation: { id: string }) => { + listRepositoriesCalls.push({ connectorId: connector.id, installationId: installation.id }); + return [ + { + id: "repo-1", + provider: "github", + fullName: "acme/app", + name: "app", + htmlUrl: "https://github.com/acme/app", + defaultBranch: "main", + private: true, + }, + ]; + }, + signConnectorState: (state: Record) => { + signedStates.push(state); + return "state"; + }, + toPublicConnector: (row: unknown) => row, + toPublicInstallation: (row: unknown) => row, + verifyConnectorState: () => verifiedState, +})); + +mock.module("../../openworkflow", () => ({ + ow: { + runWorkflow: async () => { + if (workflowError) { + throw workflowError; + } + return { workflowRun: { id: "run-1" } }; + }, + }, +})); + +const mockDb: MockDb = { + insert: () => ({ + values: (values: Record) => { + insertValues.push(values); + return { + onConflictDoUpdate: (config: Record) => { + conflictConfigs.push(config); + return { + returning: () => [{ id: "installation-1", ...values, status: "active" }], + }; + }, + returning: () => [{ id: "row-1", ...values }], + }; + }, + }), + update: () => ({ + set: (values: Record) => { + updateValues.push(values); + return { + where: () => ({ + returning: () => [], + }), + }; + }, + }), + select: () => ({ + from: () => ({ + orderBy: () => [], + }), + }), + transaction: async (callback: (tx: typeof mockDb) => unknown) => callback(mockDb), +}; + +mock.module("@kiwi/db", () => ({ db: mockDb })); + +// Dynamic import is required so module mocks are installed before the route module is evaluated. +const { connectorRoute, repositoryGraphBindingRoute } = await import("../connectors"); + +describe("connector route", () => { + beforeEach(() => { + insertValues.length = 0; + conflictConfigs.length = 0; + installationAccountCalls.length = 0; + listBranchesCalls.length = 0; + listRepositoriesCalls.length = 0; + updateValues.length = 0; + signedStates.length = 0; + organizationAdminChecks.length = 0; + installationConnectorId = "connector-1"; + teamAccessOrganizationId = "org-1"; + verifiedState = { + purpose: "github-installation", + userId: "user-1", + connectorId: "connector-1", + organizationId: "org-1", + createdAt: Date.now(), + }; + workflowError = null; + }); + + test("stores GitHub installation account metadata and targets org-scope upserts", async () => { + const response = await connectorRoute.handle( + new Request("http://localhost/connectors/github/install/callback?state=state&installation_id=99") + ); + + expect(response.status).toBe(200); + expect(installationAccountCalls).toEqual(["99"]); + expect(insertValues[0]).toMatchObject({ + connectorId: "connector-1", + provider: "github", + providerInstallationId: "99", + providerAccountLogin: "acme", + providerAccountType: "organization", + organizationId: "org-1", + teamId: null, + repositorySelection: "selected", + }); + expect(conflictConfigs[0]?.target).toHaveLength(3); + expect(conflictConfigs[0]).toHaveProperty("targetWhere"); + expect(conflictConfigs[0]?.set).toMatchObject({ + providerAccountLogin: "acme", + providerAccountType: "organization", + repositorySelection: "selected", + status: "active", + installedByUserId: "user-1", + }); + }); + + test("connect state uses requested organization after admin check", async () => { + const response = await connectorRoute.handle( + new Request("http://localhost/connectors/connector-1/connect?organizationId=org-2") + ); + + expect(response.status).toBe(200); + expect(organizationAdminChecks).toEqual(["org-2"]); + expect(signedStates[0]).toMatchObject({ + purpose: "github-installation", + userId: "user-1", + connectorId: "connector-1", + organizationId: "org-2", + }); + expect(signedStates[0]).not.toHaveProperty("teamId"); + }); + + test("connect state derives organization from team access", async () => { + const response = await connectorRoute.handle( + new Request("http://localhost/connectors/connector-1/connect?teamId=team-1&organizationId=org-2") + ); + + expect(response.status).toBe(200); + expect(organizationAdminChecks).toEqual([]); + expect(signedStates[0]).toMatchObject({ + purpose: "github-installation", + userId: "user-1", + connectorId: "connector-1", + organizationId: "org-1", + teamId: "team-1", + }); + }); + + test("rejects install callback when team state crosses organization", async () => { + verifiedState = { + purpose: "github-installation", + userId: "user-1", + connectorId: "connector-1", + organizationId: "org-2", + teamId: "team-1", + createdAt: Date.now(), + }; + + const response = await connectorRoute.handle( + new Request("http://localhost/connectors/github/install/callback?state=state&installation_id=99") + ); + + expect(response.status).toBe(403); + expect(insertValues).toEqual([]); + }); + + test("marks manual repository graph sync failed when workflow enqueue fails", async () => { + workflowError = new Error("enqueue failed"); + + const response = await repositoryGraphBindingRoute.handle( + new Request("http://localhost/repository-graph-bindings/binding-1/sync", { method: "POST" }) + ); + + expect(response.status).toBe(400); + expect(updateValues).toContainEqual({ syncStatus: "pending", syncErrorCode: null }); + expect(updateValues).toContainEqual({ syncStatus: "failed", syncErrorCode: "enqueue_failed" }); + }); + + test("rejects repository listing when installation belongs to another connector", async () => { + const response = await connectorRoute.handle( + new Request("http://localhost/connectors/connector-2/repositories?installationId=installation-1") + ); + + expect(response.status).toBe(403); + expect(listRepositoriesCalls).toEqual([]); + }); + + test("rejects branch listing when installation belongs to another connector", async () => { + const response = await connectorRoute.handle( + new Request( + "http://localhost/connectors/connector-2/repositories/repo-1/branches?installationId=installation-1" + ) + ); + + expect(response.status).toBe(403); + expect(listBranchesCalls).toEqual([]); + }); + + test("rejects repository graph creation when installation belongs to another connector", async () => { + const response = await connectorRoute.handle( + new Request("http://localhost/connectors/connector-2/repository-graphs", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + connectorInstallationId: "installation-1", + repositoryId: "repo-1", + repositoryFullName: "acme/app", + repositoryHtmlUrl: "https://github.com/acme/app", + branch: "main", + name: "App", + owner: { kind: "organization" }, + }), + }) + ); + + expect(response.status).toBe(403); + expect(listRepositoriesCalls).toEqual([]); + }); +}); diff --git a/apps/api/src/routes/__tests__/graph-archive-upload-route.test.ts b/apps/api/src/routes/__tests__/graph-archive-upload-route.test.ts index a74112dc..ed51ad5b 100644 --- a/apps/api/src/routes/__tests__/graph-archive-upload-route.test.ts +++ b/apps/api/src/routes/__tests__/graph-archive-upload-route.test.ts @@ -3,7 +3,46 @@ import { Elysia } from "elysia"; const uploadedFiles: Array<{ graphId: string; fileId: string; name: string }> = []; let archiveExpansionMode: "success" | "limit" = "success"; -const workflowInputs: Array<{ graphId: string; fileIds: string[]; processRunId: string }> = []; +const workflowInputs: Array<{ + graphId: string; + fileIds: string[]; + processRunId: string; + code?: { kind: "repository"; retiredFileIds?: string[] }; +}> = []; +const loadedUrls: string[] = []; +const insertedFileValues: Array<{ + name: string; + type: string; + mimeType: string; + key: string; + storageKind?: string; + externalProvider?: string; + externalUrl?: string; + metadata?: string; + checksum?: string; +}> = []; +const existingChecksumRows: Array<{ checksum: string }> = []; +const retryFileRows: Array<{ + id: string; + type: string; + status: string; + processStep: string; + processErrorCode: string | null; +}> = []; +const supersededFileIds: string[] = []; +let repositoryLoadMode: "success" | "limit-error" | "git-error" = "success"; + +class RepositoryUrlError extends Error { + constructor( + public readonly kind: "validation" | "limit" | "load", + message: string, + options?: { cause?: unknown } + ) { + super(message, options); + this.name = "RepositoryUrlError"; + } +} +let uploadModelMode: "success" | "missing" = "success"; const graphRow = { id: "graph-1", @@ -32,6 +71,7 @@ function graphFileRows( mimeType: string; key: string; checksum: string; + metadata?: string; }> ) { return values.map((file) => ({ @@ -43,6 +83,7 @@ function graphFileRows( mimeType: file.mimeType, key: file.key, checksum: file.checksum, + metadata: file.metadata, deleted: false, })); } @@ -63,6 +104,7 @@ function insertReturning(values: unknown) { mimeType: string; key: string; checksum: string; + metadata?: string; }> ); } @@ -78,6 +120,16 @@ function insertReturning(values: unknown) { return undefined; } +function selectableRows(rows: TRows[], limitedRows: TLimitedRows[]) { + return { + limit: async (count: number) => limitedRows.slice(0, count), + then: ( + onfulfilled?: ((value: TRows[]) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ) => Promise.resolve(rows).then(onfulfilled, onrejected), + }; +} + const db = { insert: () => ({ values: (values: unknown) => ({ @@ -86,7 +138,7 @@ const db = { }), select: () => ({ from: () => ({ - where: async () => [], + where: () => selectableRows(existingChecksumRows, retryFileRows), }), }), transaction: async (callback: (tx: typeof transactionDb) => Promise) => callback(transactionDb), @@ -96,15 +148,33 @@ const transactionDb = { insert: () => ({ values: (values: unknown) => ({ onConflictDoNothing: () => ({ - returning: () => insertReturning(values), + returning: () => { + if (Array.isArray(values)) { + insertedFileValues.push( + ...(values as Array<{ + name: string; + type: string; + mimeType: string; + key: string; + storageKind?: string; + externalProvider?: string; + externalUrl?: string; + metadata?: string; + checksum?: string; + }>) + ); + } + + return insertReturning(values); + }, }), returning: () => insertReturning(values), }), }), update: () => ({ - set: () => ({ + set: (values: Record) => ({ where: () => ({ - returning: () => [existingGraph], + returning: () => (values.deleted === true ? supersededFileIds.map((id) => ({ id })) : [existingGraph]), }), }), }), @@ -182,6 +252,96 @@ mock.module("../../lib/archive-upload", () => ({ }, })); +mock.module("../../lib/graph-upload-file-type", () => ({ + inferSupportedUploadedFiles: ( + files: Array<{ + file: File; + checksum: string; + }> + ) => ({ + ok: true, + files: files.map((file) => ({ + ...file, + type: file.file.name.endsWith(".ts") ? "code" : "text", + })), + }), + unsupportedUploadResponse: (statusFn: (code: number, body: unknown) => unknown) => + statusFn(415, { status: "error", code: "UNSUPPORTED_FILE_TYPE" }), + assertConfiguredUploadModels: async () => { + if (uploadModelMode === "missing") { + throw new Error("MODEL_NOT_CONFIGURED"); + } + }, +})); + +mock.module("../../lib/repository-url", () => ({ + MAX_REPOSITORY_URLS: 5, + RepositoryUrlError, + buildGitHubExternalCodeFile: ({ + repositoryUrl, + commitSha, + path, + }: { + repositoryUrl: string; + commitSha: string; + path: string; + }) => { + const match = repositoryUrl.match(/^https:\/\/github\.com\/acme\/([^/]+)\.git$/); + if (!match) { + return null; + } + + const repositoryName = match[1]; + return { + provider: "github", + rawUrl: `https://raw.githubusercontent.com/acme/${repositoryName}/${commitSha}/${path}`, + htmlUrl: `https://github.com/acme/${repositoryName}/blob/${commitSha}/${path}`, + key: `external:github:acme/${repositoryName}@${commitSha}:${path}`, + }; + }, + loadRepositoryFromUrl: async (url: string) => { + loadedUrls.push(url); + if (repositoryLoadMode === "limit-error") { + throw new RepositoryUrlError("limit", "Repository contains too many supported code files"); + } + if (repositoryLoadMode === "git-error") { + throw new RepositoryUrlError("load", "Repository could not be loaded", { + cause: new Error("fatal: could not read Username for 'https://github.com': terminal prompts disabled"), + }); + } + + const repositoryName = url.includes("tools") ? "tools" : "widgets"; + return { + url: `https://github.com/acme/${repositoryName}.git`, + name: repositoryName, + commitSha: "commit-1", + files: + repositoryName === "tools" + ? [ + { + path: "src/index.ts", + content: + "import { helper } from './helper';\nexport function main() { return helper(); }\n", + size: 75, + }, + ] + : [ + { + path: "src/index.ts", + content: + "import { helper } from './helper';\nexport function main() { return helper(); }\n", + size: 75, + }, + { + path: "src/helper.ts", + content: "export function helper() { return 1; }\n", + size: 38, + }, + ], + }; + }, +})); + mock.module("../../lib/graph", () => ({ collectGraphClosure: async () => [], })); @@ -237,7 +397,14 @@ describe("graph route archive uploads", () => { beforeEach(() => { uploadedFiles.length = 0; workflowInputs.length = 0; + loadedUrls.length = 0; + insertedFileValues.length = 0; + existingChecksumRows.length = 0; + supersededFileIds.length = 0; + retryFileRows.length = 0; archiveExpansionMode = "success"; + repositoryLoadMode = "success"; + uploadModelMode = "success"; }); test("creates one graph file and workflow input per extracted archive file", async () => { @@ -281,6 +448,32 @@ describe("graph route archive uploads", () => { ]); }); + test("stores direct source uploads as code files", async () => { + const form = new FormData(); + form.append("files", new File(["export const value = 1;\n"], "src/index.ts", { type: "text/plain" })); + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/files", { + method: "POST", + body: form, + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.status).toBe("success"); + expect(insertedFileValues).toHaveLength(1); + expect(insertedFileValues[0].name).toBe("src/index.ts"); + expect(insertedFileValues[0].type).toBe("code"); + expect(workflowInputs).toEqual([ + { + graphId: "graph-1", + fileIds: body.data.addedFiles.map((file: { id: string }) => file.id), + processRunId: "process-run-1", + }, + ]); + }); + test("returns upload limit response when create archive expansion exceeds limits", async () => { archiveExpansionMode = "limit"; @@ -318,4 +511,250 @@ describe("graph route archive uploads", () => { expect(uploadedFiles).toEqual([]); expect(workflowInputs).toEqual([]); }); + + test("adds repository URL code files with metadata and code workflow input", async () => { + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + urls: [" https://github.com/acme/widgets ", "https://github.com/acme/widgets"], + }), + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.status).toBe("success"); + expect(loadedUrls).toEqual(["https://github.com/acme/widgets"]); + expect(uploadedFiles).toEqual([]); + expect(insertedFileValues.map((file) => file.type)).toEqual(["code", "code"]); + expect(insertedFileValues.map((file) => file.mimeType)).toEqual(["text/plain", "text/plain"]); + expect(insertedFileValues.map((file) => file.storageKind)).toEqual(["external", "external"]); + expect(insertedFileValues.map((file) => file.externalProvider)).toEqual(["github", "github"]); + expect(insertedFileValues.map((file) => file.externalUrl)).toEqual([ + "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts", + "https://raw.githubusercontent.com/acme/widgets/commit-1/src/helper.ts", + ]); + expect(insertedFileValues.map((file) => file.key)).toEqual([ + "external:github:acme/widgets@commit-1:src/index.ts", + "external:github:acme/widgets@commit-1:src/helper.ts", + ]); + expect(insertedFileValues.map((file) => JSON.parse(file.metadata ?? "{}"))).toEqual([ + { + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/index.ts", + external: { + provider: "github", + rawUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts", + htmlUrl: "https://github.com/acme/widgets/blob/commit-1/src/index.ts", + }, + }, + { + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/helper.ts", + external: { + provider: "github", + rawUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/helper.ts", + htmlUrl: "https://github.com/acme/widgets/blob/commit-1/src/helper.ts", + }, + }, + ]); + expect(workflowInputs).toEqual([ + { + graphId: "graph-1", + fileIds: body.data.addedFiles.map((file: { id: string }) => file.id), + processRunId: "process-run-1", + code: { kind: "repository", retiredFileIds: [] }, + }, + ]); + }); + + test("passes superseded repository file ids to the processing workflow", async () => { + supersededFileIds.push("old-file-1", "old-file-2"); + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ urls: ["https://github.com/acme/widgets"] }), + }) + ); + + expect(response.status).toBe(200); + expect(workflowInputs).toHaveLength(1); + expect(workflowInputs[0]?.code).toEqual({ + kind: "repository", + retiredFileIds: ["old-file-1", "old-file-2"], + }); + }); + + test("keeps identical repository snapshot content once per repository", async () => { + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + urls: ["https://github.com/acme/widgets", "https://github.com/acme/tools"], + }), + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.status).toBe("success"); + expect(insertedFileValues.map((file) => file.name)).toEqual([ + "widgets/src/index.ts", + "widgets/src/helper.ts", + "tools/src/index.ts", + ]); + }); + + test("validates repository URL models before upload or workflow side effects", async () => { + uploadModelMode = "missing"; + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ urls: ["https://github.com/acme/widgets"] }), + }) + ); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.status).toBe("error"); + expect(body.code).toBe("MODEL_NOT_CONFIGURED"); + expect(uploadedFiles).toEqual([]); + expect(insertedFileValues).toEqual([]); + expect(workflowInputs).toEqual([]); + }); + + test("creates latest repository snapshot files even when earlier checksums already exist", async () => { + const duplicateContent = "import { helper } from './helper';\nexport function main() { return helper(); }\n"; + const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(duplicateContent)); + existingChecksumRows.push({ + checksum: [...new Uint8Array(hashBuffer)].map((byte) => byte.toString(16).padStart(2, "0")).join(""), + }); + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ urls: ["https://github.com/acme/widgets"] }), + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.status).toBe("success"); + expect(body.data.addedFiles.map((file: { name: string }) => file.name)).toEqual([ + "widgets/src/index.ts", + "widgets/src/helper.ts", + ]); + expect(uploadedFiles).toEqual([]); + expect(workflowInputs[0]?.code).toEqual({ kind: "repository", retiredFileIds: [] }); + }); + + test("still enqueues repository workflow when every URL file matches an older checksum", async () => { + for (const content of [ + "import { helper } from './helper';\nexport function main() { return helper(); }\n", + "export function helper() { return 1; }\n", + ]) { + const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)); + existingChecksumRows.push({ + checksum: [...new Uint8Array(hashBuffer)].map((byte) => byte.toString(16).padStart(2, "0")).join(""), + }); + } + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ urls: ["https://github.com/acme/widgets"] }), + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.data.addedFiles.map((file: { name: string }) => file.name)).toEqual([ + "widgets/src/index.ts", + "widgets/src/helper.ts", + ]); + expect(body.data.workflowRunId).toBe("workflow-1"); + expect(uploadedFiles).toEqual([]); + expect(workflowInputs).toHaveLength(1); + }); + + test("retries code files without retiring repository siblings", async () => { + retryFileRows.push({ + id: "file-code", + type: "code", + status: "failed", + processStep: "failed", + processErrorCode: "loader_failed", + }); + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/files/file-code/retry", { + method: "POST", + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.status).toBe("success"); + expect(workflowInputs).toEqual([ + { + graphId: "graph-1", + fileIds: ["file-code"], + processRunId: "process-run-1", + code: { kind: "repository", retiredFileIds: [] }, + }, + ]); + }); + + test("maps repository loader limits to upload limit responses", async () => { + repositoryLoadMode = "limit-error"; + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ urls: ["https://github.com/acme/widgets"] }), + }) + ); + const body = await response.json(); + + expect(response.status).toBe(413); + expect(body.code).toBe("UPLOAD_LIMIT_EXCEEDED"); + expect(uploadedFiles).toEqual([]); + expect(workflowInputs).toEqual([]); + }); + + test("sanitizes repository git failures in client responses", async () => { + repositoryLoadMode = "git-error"; + + const response = await app().handle( + new Request("http://localhost/graphs/graph-1/urls", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ urls: ["https://github.com/acme/widgets"] }), + }) + ); + const body = await response.json(); + const responseText = JSON.stringify(body); + + expect(response.status).toBe(400); + expect(body.code).toBe("UNSUPPORTED_FILE_TYPE"); + expect(body.message).toBe("Repository could not be loaded"); + expect(responseText).not.toContain("fatal:"); + expect(responseText).not.toContain("terminal prompts disabled"); + expect(uploadedFiles).toEqual([]); + expect(workflowInputs).toEqual([]); + }); }); diff --git a/apps/api/src/routes/connector-webhooks.ts b/apps/api/src/routes/connector-webhooks.ts new file mode 100644 index 00000000..0b840dbb --- /dev/null +++ b/apps/api/src/routes/connector-webhooks.ts @@ -0,0 +1,308 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; +import { db } from "@kiwi/db"; +import { + connectorWebhookEventsTable, + connectorInstallationsTable, + connectorsTable, + repositoryGraphBindingsTable, + type ConnectorProvider, +} from "@kiwi/db/tables/connectors"; +import { syncRepositoryGraphSpec } from "@kiwi/worker/sync-repository-graph-spec"; +import { and, eq, or } from "drizzle-orm"; +import Elysia from "elysia"; +import { decryptSecret } from "../lib/connectors"; +import { ow } from "../openworkflow"; +import { API_ERROR_CODES, errorResponse, successResponse } from "../types"; +import type { ApiErrorCode } from "../types"; + +type NormalizedPush = { + eventName: string; + deliveryId: string; + providerRepositoryId: string | null; + repositoryFullName: string | null; + branch: string | null; + commitSha: string | null; + deleted: boolean; +}; + +const ZERO_SHA = /^0+$/; + +function equalSecret(left: string, right: string) { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); +} +function jsonError(message: string, code: ApiErrorCode, status: number) { + return new Response(JSON.stringify(errorResponse(message, code)), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function jsonSuccess(data: TData, status: number) { + return new Response(JSON.stringify(successResponse(data)), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function verifyGitHubSignature(rawBody: string, signatureHeader: string | null, secret: string) { + if (!signatureHeader?.startsWith("sha256=")) { + return false; + } + const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`; + return equalSecret(signatureHeader, expected); +} + +function normalizeBranch(ref: unknown) { + if (typeof ref !== "string" || !ref.startsWith("refs/heads/")) { + return null; + } + return ref.slice("refs/heads/".length); +} + +function normalizeGitHub(payload: Record, eventName: string, deliveryId: string): NormalizedPush { + const repository = payload.repository && typeof payload.repository === "object" ? payload.repository : null; + const repo = repository as { id?: unknown; full_name?: unknown } | null; + const commitSha = typeof payload.after === "string" ? payload.after : null; + return { + eventName, + deliveryId, + providerRepositoryId: repo?.id === undefined ? null : String(repo.id), + repositoryFullName: typeof repo?.full_name === "string" ? repo.full_name : null, + branch: normalizeBranch(payload.ref), + commitSha, + deleted: !commitSha || ZERO_SHA.test(commitSha), + }; +} + +function normalizeGitLab(payload: Record, eventName: string, deliveryId: string): NormalizedPush { + const project = payload.project && typeof payload.project === "object" ? payload.project : null; + const repo = project as { id?: unknown; path_with_namespace?: unknown } | null; + const commitSha = typeof payload.after === "string" ? payload.after : null; + return { + eventName, + deliveryId, + providerRepositoryId: repo?.id === undefined ? null : String(repo.id), + repositoryFullName: typeof repo?.path_with_namespace === "string" ? repo.path_with_namespace : null, + branch: normalizeBranch(payload.ref), + commitSha, + deleted: !commitSha || ZERO_SHA.test(commitSha), + }; +} +function parseWebhookPayload(rawBody: string): Record | null { + try { + const payload = JSON.parse(rawBody); + return payload && typeof payload === "object" && !Array.isArray(payload) + ? (payload as Record) + : null; + } catch { + return null; + } +} + +async function loadWebhookEvent(connectorId: string, provider: ConnectorProvider, deliveryId: string) { + const [event] = await db + .select() + .from(connectorWebhookEventsTable) + .where( + and( + eq(connectorWebhookEventsTable.connectorId, connectorId), + eq(connectorWebhookEventsTable.provider, provider), + eq(connectorWebhookEventsTable.deliveryId, deliveryId) + ) + ) + .limit(1); + return event ?? null; +} + +async function markWebhookEvent(id: string, values: { status: "enqueued" | "failed"; errorCode: string | null }) { + await db.update(connectorWebhookEventsTable).set(values).where(eq(connectorWebhookEventsTable.id, id)); +} + +async function enqueueBindingWorkflows( + bindings: Array, + commitSha: string, + deliveryId: string +) { + const enqueuedBindingIds = new Set(); + try { + for (const binding of bindings) { + await ow.runWorkflow(syncRepositoryGraphSpec, { + bindingId: binding.id, + reason: "webhook", + commitSha, + deliveryId, + }); + enqueuedBindingIds.add(binding.id); + } + } catch (error) { + await Promise.all( + bindings.map((binding) => + db + .update(repositoryGraphBindingsTable) + .set( + enqueuedBindingIds.has(binding.id) + ? { lastSeenCommitSha: commitSha, syncStatus: "pending", syncErrorCode: null } + : { syncStatus: "failed", syncErrorCode: "enqueue_failed" } + ) + .where(eq(repositoryGraphBindingsTable.id, binding.id)) + ) + ); + throw error; + } + + await Promise.all( + bindings.map((binding) => + db + .update(repositoryGraphBindingsTable) + .set({ lastSeenCommitSha: commitSha, syncStatus: "pending", syncErrorCode: null }) + .where(eq(repositoryGraphBindingsTable.id, binding.id)) + ) + ); +} + +async function findVerifiedConnector(provider: ConnectorProvider, request: Request, rawBody: string) { + const connectors = await db + .select() + .from(connectorsTable) + .where(and(eq(connectorsTable.provider, provider), eq(connectorsTable.status, "active"))); + + for (const connector of connectors) { + const secret = decryptSecret(connector.webhookSecretEncrypted); + if (provider === "github") { + if (verifyGitHubSignature(rawBody, request.headers.get("x-hub-signature-256"), secret)) { + return connector; + } + } else if (equalSecret(request.headers.get("x-gitlab-token") ?? "", secret)) { + return connector; + } + } + + return null; +} + +async function handleWebhook(provider: ConnectorProvider, request: Request) { + const rawBody = await request.text(); + const connector = await findVerifiedConnector(provider, request, rawBody); + if (!connector) { + return jsonError("Invalid webhook signature", API_ERROR_CODES.FORBIDDEN, 403); + } + + const payload = parseWebhookPayload(rawBody); + if (!payload) { + return jsonError("Invalid webhook payload", API_ERROR_CODES.INVALID_CHAT_REQUEST, 400); + } + const eventName = + provider === "github" + ? request.headers.get("x-github-event") || "unknown" + : request.headers.get("x-gitlab-event") || String(payload.event_name ?? "unknown"); + const deliveryId = + provider === "github" + ? request.headers.get("x-github-delivery") || "missing" + : request.headers.get("x-gitlab-webhook-uuid") || + `${String(payload.event_name ?? "event")}:${String(payload.after ?? Date.now())}`; + const normalized = + provider === "github" + ? normalizeGitHub(payload, eventName, deliveryId) + : normalizeGitLab(payload, eventName, deliveryId); + const isPush = + provider === "github" ? eventName === "push" : eventName === "Push Hook" || normalized.eventName === "push"; + + let bindings: Array = []; + if ( + isPush && + normalized.branch && + normalized.commitSha && + !normalized.deleted && + (normalized.providerRepositoryId || normalized.repositoryFullName) + ) { + const repositoryWhere = + normalized.providerRepositoryId && normalized.repositoryFullName + ? or( + eq(repositoryGraphBindingsTable.providerRepositoryId, normalized.providerRepositoryId), + eq(repositoryGraphBindingsTable.repositoryFullName, normalized.repositoryFullName) + ) + : normalized.providerRepositoryId + ? eq(repositoryGraphBindingsTable.providerRepositoryId, normalized.providerRepositoryId) + : eq(repositoryGraphBindingsTable.repositoryFullName, normalized.repositoryFullName!); + const bindingRows = await db + .select({ binding: repositoryGraphBindingsTable }) + .from(repositoryGraphBindingsTable) + .innerJoin( + connectorInstallationsTable, + eq(repositoryGraphBindingsTable.connectorInstallationId, connectorInstallationsTable.id) + ) + .where( + and( + eq(connectorInstallationsTable.connectorId, connector.id), + eq(repositoryGraphBindingsTable.provider, provider), + eq(repositoryGraphBindingsTable.branch, normalized.branch), + eq(repositoryGraphBindingsTable.webhookEnabled, true), + repositoryWhere + ) + ); + bindings = bindingRows.map((row) => row.binding); + } + + const status = !isPush || normalized.deleted || bindings.length === 0 ? "ignored" : "enqueued"; + const [ledger] = await db + .insert(connectorWebhookEventsTable) + .values({ + connectorId: connector.id, + provider, + deliveryId, + eventName, + providerRepositoryId: normalized.providerRepositoryId, + branch: normalized.branch, + commitSha: normalized.commitSha, + status, + }) + .onConflictDoNothing({ + target: [ + connectorWebhookEventsTable.connectorId, + connectorWebhookEventsTable.provider, + connectorWebhookEventsTable.deliveryId, + ], + }) + .returning(); + + let activeLedger = ledger ?? null; + if (!activeLedger) { + const existingLedger = await loadWebhookEvent(connector.id, provider, deliveryId); + if (existingLedger?.status !== "failed" || status !== "enqueued" || !normalized.commitSha) { + return jsonSuccess({ status: "duplicate" }, 202); + } + activeLedger = existingLedger; + } + + if (status === "enqueued" && normalized.commitSha) { + try { + await enqueueBindingWorkflows(bindings, normalized.commitSha, deliveryId); + if (activeLedger.status === "failed") { + await markWebhookEvent(activeLedger.id, { status: "enqueued", errorCode: null }); + } + } catch (error) { + await markWebhookEvent(activeLedger.id, { status: "failed", errorCode: "enqueue_failed" }); + throw error; + } + } + + return jsonSuccess({ status, enqueued: status === "enqueued" ? bindings.length : 0 }, 202); +} + +export const connectorWebhookRoute = new Elysia().post( + "/connectors/webhooks/:provider", + async ({ params, request }) => { + if (params.provider !== "github" && params.provider !== "gitlab") { + return new Response( + JSON.stringify(errorResponse("Unsupported provider", API_ERROR_CODES.INVALID_CHAT_REQUEST)), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + return handleWebhook(params.provider, request); + } +); diff --git a/apps/api/src/routes/connectors.ts b/apps/api/src/routes/connectors.ts new file mode 100644 index 00000000..61b2518b --- /dev/null +++ b/apps/api/src/routes/connectors.ts @@ -0,0 +1,606 @@ +import { ulid } from "ulid"; +import { db } from "@kiwi/db"; +import { + connectorInstallationsTable, + connectorsTable, + repositoryGraphBindingsTable, + type ConnectorProvider, +} from "@kiwi/db/tables/connectors"; +import { graphTable } from "@kiwi/db/tables/graph"; +import { syncRepositoryGraphSpec } from "@kiwi/worker/sync-repository-graph-spec"; +import { Result } from "better-result"; +import { and, asc, eq, sql } from "drizzle-orm"; +import Elysia from "elysia"; +import z from "zod"; +import { assertCanCreateTeamGraph } from "../lib/graph-access"; +import { + assertCanSyncBinding, + assertCanUseInstallation, + assertCanViewBinding, + requireActiveConnector, +} from "../lib/connector-access"; +import { requireOrganizationAdmin } from "../lib/team-access"; +import { + createManifestUrl, + encryptCredentials, + encryptSecret, + exchangeGitHubManifestCode, + getGitHubConnectorInstallationAccount, + listProviderBranches, + listProviderRepositories, + signConnectorState, + toPublicConnector, + toPublicInstallation, + verifyConnectorState, +} from "../lib/connectors"; +import { ow } from "../openworkflow"; +import { authMiddleware, type AuthUser } from "../middleware/auth"; +import { API_ERROR_CODES, errorResponse, successResponse } from "../types"; + +const githubManifestStartSchema = z.object({ + name: z.string().trim().min(1), +}); + +const gitLabCreateSchema = z.object({ + name: z.string().trim().min(1), + slug: z.string().trim().min(1), + baseUrl: z.string().trim().url(), + clientId: z.string().trim().min(1), + clientSecret: z.string().trim().min(1), + webhookSecret: z.string().trim().min(1), +}); + +const patchConnectorSchema = z.object({ + name: z.string().trim().min(1).optional(), + status: z.enum(["active", "disabled"]).optional(), + webhookSecret: z.string().trim().min(1).optional(), +}); + +const connectQuerySchema = z.object({ + organizationId: z.string().trim().min(1).optional(), + teamId: z.string().trim().min(1).optional(), +}); + +const installCallbackQuerySchema = z.object({ + state: z.string().trim().min(1), + installation_id: z.string().trim().min(1), + setup_action: z.string().trim().optional(), +}); + +const repositoryQuerySchema = z.object({ + installationId: z.string().trim().min(1), +}); + +const connectorOwnerSchema = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("organization") }), + z.object({ kind: z.literal("team"), teamId: z.string().trim().min(1) }), +]); + +const createRepositoryGraphSchema = z.object({ + connectorInstallationId: z.string().trim().min(1), + repositoryId: z.string().trim().min(1), + repositoryFullName: z.string().trim().min(1), + repositoryHtmlUrl: z.string().trim().url(), + branch: z.string().trim().min(1), + name: z.string().trim().min(1), + owner: connectorOwnerSchema, +}); + +type RouteStatus = (code: number, body: unknown) => unknown; + +function mapConnectorError(status: RouteStatus, error: unknown) { + if (!(error instanceof Error)) { + return status(500, errorResponse("Internal server error", API_ERROR_CODES.INTERNAL_SERVER_ERROR)); + } + const message = error.message.replace(/^Unhandled exception: /u, ""); + if (message === API_ERROR_CODES.UNAUTHORIZED) { + return status(401, errorResponse("Unauthorized", API_ERROR_CODES.UNAUTHORIZED)); + } + if (message === API_ERROR_CODES.FORBIDDEN) { + return status(403, errorResponse("Forbidden", API_ERROR_CODES.FORBIDDEN)); + } + if (message === API_ERROR_CODES.GRAPH_NOT_FOUND) { + return status(404, errorResponse("Not found", API_ERROR_CODES.GRAPH_NOT_FOUND)); + } + return status(400, errorResponse(message || "Invalid connector request", API_ERROR_CODES.INVALID_CHAT_REQUEST)); +} + +async function runConnectorAction(options: { + user: AuthUser | null | undefined; + status: RouteStatus; + action: (user: AuthUser) => Promise; +}) { + if (!options.user) { + return options.status(401, errorResponse("Unauthorized", API_ERROR_CODES.UNAUTHORIZED)); + } + const result = await Result.tryPromise(async () => options.action(options.user!)); + if (result.isErr()) { + return mapConnectorError(options.status, result.error); + } + return options.status(200, successResponse(result.value)); +} + +function assertSystemAdmin(user: AuthUser) { + if (!user.isSystemAdmin) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } +} + +function repositoryMatches(repository: { id: string; fullName: string }, requestedId: string) { + return repository.id === requestedId || repository.fullName === requestedId; +} + +function assertInstallationBelongsToConnector(installation: { connectorId: string }, connectorId: string) { + if (installation.connectorId !== connectorId) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } +} + +function toBindingResponse(binding: typeof repositoryGraphBindingsTable.$inferSelect) { + return { + id: binding.id, + graphId: binding.graphId, + connectorInstallationId: binding.connectorInstallationId, + provider: binding.provider as ConnectorProvider, + providerRepositoryId: binding.providerRepositoryId, + repositoryFullName: binding.repositoryFullName, + repositoryHtmlUrl: binding.repositoryHtmlUrl, + branch: binding.branch, + lastSeenCommitSha: binding.lastSeenCommitSha, + lastSyncedCommitSha: binding.lastSyncedCommitSha, + syncStatus: binding.syncStatus, + syncErrorCode: binding.syncErrorCode, + webhookEnabled: binding.webhookEnabled, + createdAt: binding.createdAt?.toISOString() ?? null, + updatedAt: binding.updatedAt?.toISOString() ?? null, + }; +} + +export const connectorRoute = new Elysia({ prefix: "/connectors" }) + .use(authMiddleware) + .get("/", async ({ status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const rows = await db.select().from(connectorsTable).orderBy(asc(connectorsTable.name)); + return rows + .filter((row) => currentUser.isSystemAdmin || row.status === "active") + .map(toPublicConnector); + }, + }) + ) + .post( + "/github/manifest/start", + async ({ body, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + assertSystemAdmin(currentUser); + const state = signConnectorState({ purpose: "github-manifest", userId: currentUser.id }); + return { state, manifestUrl: createManifestUrl(state, body.name.trim()) }; + }, + }), + { body: githubManifestStartSchema } + ) + .get( + "/github/manifest/callback", + async ({ query, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + assertSystemAdmin(currentUser); + if (!verifyConnectorState(query.state, "github-manifest", currentUser.id)) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + const app = await exchangeGitHubManifestCode(query.code); + const slug = app.slug ?? `github-${String(app.id)}`; + const [connector] = await db + .insert(connectorsTable) + .values({ + provider: "github", + name: app.name, + slug, + appId: String(app.id), + appSlug: slug, + clientId: app.client_id ?? null, + encryptedCredentials: encryptCredentials({ + provider: "github", + appId: String(app.id), + privateKeyPem: app.pem, + clientId: app.client_id, + clientSecret: app.client_secret, + }), + webhookSecretEncrypted: encryptSecret(app.webhook_secret ?? ulid()), + createdByUserId: currentUser.id, + }) + .returning(); + return toPublicConnector(connector); + }, + }), + { query: z.object({ code: z.string().trim().min(1), state: z.string().trim().min(1) }) } + ) + .post( + "/gitlab", + async ({ body, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + assertSystemAdmin(currentUser); + const [connector] = await db + .insert(connectorsTable) + .values({ + provider: "gitlab", + name: body.name, + slug: body.slug, + status: "disabled", + appId: body.clientId, + clientId: body.clientId, + encryptedCredentials: encryptCredentials({ + provider: "gitlab", + baseUrl: body.baseUrl, + clientId: body.clientId, + clientSecret: body.clientSecret, + }), + webhookSecretEncrypted: encryptSecret(body.webhookSecret), + createdByUserId: currentUser.id, + }) + .returning(); + return toPublicConnector(connector); + }, + }), + { body: gitLabCreateSchema } + ) + .patch( + "/:id", + async ({ params, body, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + assertSystemAdmin(currentUser); + const [connector] = await db + .update(connectorsTable) + .set({ + ...(body.name ? { name: body.name } : {}), + ...(body.status ? { status: body.status } : {}), + ...(body.webhookSecret + ? { webhookSecretEncrypted: encryptSecret(body.webhookSecret) } + : {}), + }) + .where(eq(connectorsTable.id, params.id)) + .returning(); + if (!connector) { + throw new Error(API_ERROR_CODES.GRAPH_NOT_FOUND); + } + return toPublicConnector(connector); + }, + }), + { body: patchConnectorSchema } + ) + .get( + "/:id/connect", + async ({ params, query, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const connector = await requireActiveConnector(params.id); + let owner: { organizationId: string; teamId?: string }; + if (query.teamId) { + const access = await assertCanCreateTeamGraph(currentUser, query.teamId); + owner = { organizationId: access.team.organizationId, teamId: query.teamId }; + } else { + const membership = await requireOrganizationAdmin(currentUser, query.organizationId); + owner = { organizationId: membership.organizationId }; + } + const state = signConnectorState({ + purpose: connector.provider === "github" ? "github-installation" : "gitlab-oauth", + userId: currentUser.id, + connectorId: connector.id, + organizationId: owner.organizationId, + ...(owner.teamId ? { teamId: owner.teamId } : {}), + }); + if (connector.provider === "github") { + if (!connector.appSlug) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + return { + redirectUrl: `https://github.com/apps/${connector.appSlug}/installations/new?state=${encodeURIComponent(state)}`, + }; + } + throw new Error("GitLab connector installations are disabled until OAuth flow support lands."); + }, + }), + { query: connectQuerySchema } + ) + .get( + "/github/install/callback", + async ({ query, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const state = verifyConnectorState(query.state, "github-installation", currentUser.id); + if (!state?.connectorId) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + const connector = await requireActiveConnector(state.connectorId, "github"); + let ownerOrganizationId: string; + let ownerTeamId: string | null = null; + if (state.teamId) { + const access = await assertCanCreateTeamGraph(currentUser, state.teamId); + ownerOrganizationId = access.team.organizationId; + ownerTeamId = state.teamId; + if (state.organizationId && state.organizationId !== ownerOrganizationId) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + } else { + const membership = await requireOrganizationAdmin(currentUser, state.organizationId); + ownerOrganizationId = membership.organizationId; + } + const conflictTarget = ownerTeamId + ? { + target: [ + connectorInstallationsTable.connectorId, + connectorInstallationsTable.providerInstallationId, + connectorInstallationsTable.organizationId, + connectorInstallationsTable.teamId, + ], + targetWhere: sql`${connectorInstallationsTable.teamId} is not null`, + } + : { + target: [ + connectorInstallationsTable.connectorId, + connectorInstallationsTable.providerInstallationId, + connectorInstallationsTable.organizationId, + ], + targetWhere: sql`${connectorInstallationsTable.teamId} is null`, + }; + const account = await getGitHubConnectorInstallationAccount(connector, query.installation_id); + const [installation] = await db + .insert(connectorInstallationsTable) + .values({ + connectorId: connector.id, + provider: "github", + providerInstallationId: query.installation_id, + providerAccountLogin: account.login, + providerAccountType: account.type, + organizationId: ownerOrganizationId, + teamId: ownerTeamId, + installedByUserId: currentUser.id, + repositorySelection: account.repositorySelection, + }) + .onConflictDoUpdate({ + ...conflictTarget, + set: { + providerAccountLogin: account.login, + providerAccountType: account.type, + repositorySelection: account.repositorySelection, + status: "active", + installedByUserId: currentUser.id, + }, + }) + .returning(); + return toPublicInstallation(installation); + }, + }), + { query: installCallbackQuerySchema } + ) + .get("/:id/installations", async ({ params, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + await requireActiveConnector(params.id); + const rows = await db + .select() + .from(connectorInstallationsTable) + .where( + and( + eq(connectorInstallationsTable.connectorId, params.id), + eq(connectorInstallationsTable.status, "active") + ) + ) + .orderBy(asc(connectorInstallationsTable.providerAccountLogin)); + const visible = []; + for (const row of rows) { + const allowed = await Result.tryPromise(async () => assertCanUseInstallation(currentUser, row.id)); + if (!allowed.isErr()) { + visible.push(toPublicInstallation(row)); + } + } + return visible; + }, + }) + ) + .get( + "/:id/repositories", + async ({ params, query, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const installation = await assertCanUseInstallation(currentUser, query.installationId); + assertInstallationBelongsToConnector(installation, params.id); + const connector = await requireActiveConnector( + params.id, + installation.provider as ConnectorProvider + ); + const repositories = await listProviderRepositories(connector, installation); + return repositories.map((repository) => ({ + id: repository.id, + provider: repository.provider, + fullName: repository.fullName, + name: repository.name, + htmlUrl: repository.htmlUrl, + defaultBranch: repository.defaultBranch, + private: repository.private, + })); + }, + }), + { query: repositoryQuerySchema } + ) + .get( + "/:id/repositories/:repositoryId/branches", + async ({ params, query, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const installation = await assertCanUseInstallation(currentUser, query.installationId); + assertInstallationBelongsToConnector(installation, params.id); + const connector = await requireActiveConnector( + params.id, + installation.provider as ConnectorProvider + ); + const branches = await listProviderBranches(connector, installation, params.repositoryId); + return branches.map((branch) => ({ name: branch.name, commitSha: branch.commitSha })); + }, + }), + { query: repositoryQuerySchema } + ) + .post( + "/:id/repository-graphs", + async ({ params, body, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const installation = await assertCanUseInstallation(currentUser, body.connectorInstallationId); + assertInstallationBelongsToConnector(installation, params.id); + if (body.owner.kind === "team") { + if (installation.teamId !== body.owner.teamId) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + const access = await assertCanCreateTeamGraph(currentUser, body.owner.teamId); + if (installation.organizationId !== access.team.organizationId) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + } else { + if (installation.teamId !== null || !installation.organizationId) { + throw new Error(API_ERROR_CODES.FORBIDDEN); + } + await requireOrganizationAdmin(currentUser, installation.organizationId); + } + + const connector = await requireActiveConnector( + params.id, + installation.provider as ConnectorProvider + ); + const repositories = await listProviderRepositories(connector, installation); + const repository = repositories.find((candidate) => + repositoryMatches(candidate, body.repositoryId) + ); + if (!repository) { + throw new Error(API_ERROR_CODES.GRAPH_NOT_FOUND); + } + + const branches = await listProviderBranches(connector, installation, repository.id); + const branch = branches.find((candidate) => candidate.name === body.branch); + if (!branch) { + throw new Error(API_ERROR_CODES.GRAPH_NOT_FOUND); + } + + const created = await db.transaction(async (tx) => { + const [graph] = await tx + .insert(graphTable) + .values({ + organizationId: installation.organizationId, + teamId: body.owner.kind === "team" ? body.owner.teamId : null, + name: body.name, + description: null, + state: "updating", + type: "code", + }) + .returning(); + + const [binding] = await tx + .insert(repositoryGraphBindingsTable) + .values({ + graphId: graph.id, + connectorInstallationId: installation.id, + provider: connector.provider, + providerRepositoryId: repository.id, + repositoryFullName: repository.fullName, + repositoryHtmlUrl: repository.htmlUrl, + branch: branch.name, + lastSeenCommitSha: branch.commitSha, + syncStatus: "pending", + }) + .returning(); + + return { graph, binding }; + }); + + try { + const handle = await ow.runWorkflow(syncRepositoryGraphSpec, { + bindingId: created.binding.id, + reason: "initial", + commitSha: branch.commitSha, + }); + return { + graph: created.graph, + binding: toBindingResponse(created.binding), + workflowRunId: handle.workflowRun.id, + }; + } catch (error) { + await Promise.all([ + db + .update(repositoryGraphBindingsTable) + .set({ syncStatus: "failed", syncErrorCode: "enqueue_failed" }) + .where(eq(repositoryGraphBindingsTable.id, created.binding.id)), + db.update(graphTable).set({ state: "ready" }).where(eq(graphTable.id, created.graph.id)), + ]); + throw error; + } + }, + }), + { body: createRepositoryGraphSchema } + ); + +export const repositoryGraphBindingRoute = new Elysia({ prefix: "/repository-graph-bindings" }) + .use(authMiddleware) + .get("/:id", async ({ params, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const { binding } = await assertCanViewBinding(currentUser, params.id); + return toBindingResponse(binding); + }, + }) + ) + .post("/:id/sync", async ({ params, status, user }) => + runConnectorAction({ + user, + status, + action: async (currentUser) => { + const { binding } = await assertCanSyncBinding(currentUser, params.id); + const [updatedBinding] = await db + .update(repositoryGraphBindingsTable) + .set({ syncStatus: "pending", syncErrorCode: null }) + .where(eq(repositoryGraphBindingsTable.id, binding.id)) + .returning(); + try { + const handle = await ow.runWorkflow(syncRepositoryGraphSpec, { + bindingId: binding.id, + reason: "manual", + }); + return { + binding: toBindingResponse(updatedBinding ?? binding), + workflowRunId: handle.workflowRun.id, + }; + } catch (error) { + await db + .update(repositoryGraphBindingsTable) + .set({ syncStatus: "failed", syncErrorCode: "enqueue_failed" }) + .where(eq(repositoryGraphBindingsTable.id, binding.id)); + throw error; + } + }, + }) + ); diff --git a/apps/api/src/routes/graph.ts b/apps/api/src/routes/graph.ts index dae2e89a..0fdeeb1f 100644 --- a/apps/api/src/routes/graph.ts +++ b/apps/api/src/routes/graph.ts @@ -7,6 +7,7 @@ import { db } from "@kiwi/db"; import { filesTable, graphTable, processRunFilesTable, processRunsTable } from "@kiwi/db/tables/graph"; import { teamTable } from "@kiwi/db/tables/auth"; import { deleteFile, listFiles, putGraphFile } from "@kiwi/files"; +import { serializeCodeFileMetadata } from "@kiwi/graph/code/metadata"; import { error as logError } from "@kiwi/logger"; import { deleteGraphFilesSpec } from "@kiwi/worker/delete-graph-files-spec"; import { processFilesSpec } from "@kiwi/worker/process-files-spec"; @@ -15,6 +16,13 @@ import { chunk } from "../lib/array"; import { expandArchiveUploadFiles } from "../lib/archive-upload"; import { collectGraphClosure } from "../lib/graph"; import { listAccessibleGraphs } from "../lib/graph-list"; +import { + buildGitHubExternalCodeFile, + loadRepositoryFromUrl, + MAX_REPOSITORY_URLS, + RepositoryUrlError, + type LoadedRepository, +} from "../lib/repository-url"; import { cancelActiveFileProcessingWorkflowRuns, cancelActiveGraphWorkflowRuns } from "../lib/workflow-cancellation"; import { assertCanCreateTopLevelGraph, @@ -30,6 +38,7 @@ import { import { cleanupUploadedKeys, cleanupFailedGraphCreation, + commitGraphFileUploads, mapGraphError, toGraphFileRecord, assertConfiguredUploadModels, @@ -64,6 +73,12 @@ type NewGraphOwner = graphId: string; }; +type RepositoryUploadSource = { + repository: LoadedRepository; + file: LoadedRepository["files"][number]; + checksum: string; +}; + function archiveUploadResponse( statusFn: (code: number, body: unknown) => unknown, expanded: { ok: false; kind: "unsupported" | "limit"; fileName: string; message: string } @@ -91,6 +106,38 @@ async function getGraphOwnerModelOrganizationId(owner: NewGraphOwner) { return rootOwner.organizationId; } +async function contentChecksum(content: string): Promise { + const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)); + + return [...new Uint8Array(hashBuffer)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function getRepositoryUrlError(error: unknown): RepositoryUrlError | undefined { + if (error instanceof RepositoryUrlError) { + return error; + } + + if (error instanceof Error && error.cause instanceof RepositoryUrlError) { + return error.cause; + } + + return undefined; +} + +function repositoryUrlErrorResponse(statusFn: (code: number, body: unknown) => unknown, error: unknown) { + const repositoryError = getRepositoryUrlError(error); + if (!repositoryError) { + return statusFn(400, errorResponse("Repository could not be loaded", API_ERROR_CODES.UNSUPPORTED_FILE_TYPE)); + } + + if (repositoryError.kind === "limit") { + return statusFn(413, errorResponse(repositoryError.message, API_ERROR_CODES.UPLOAD_LIMIT_EXCEEDED)); + } + + const message = repositoryError.kind === "validation" ? repositoryError.message : "Repository could not be loaded"; + return statusFn(400, errorResponse(message, API_ERROR_CODES.UNSUPPORTED_FILE_TYPE)); +} + export const graphRoute = new Elysia({ prefix: "/graphs" }) .use(authMiddleware) .get( @@ -528,6 +575,273 @@ export const graphRoute = new Elysia({ prefix: "/graphs" }) }), } ) + .post( + "/:id/urls", + async ({ body, params, user, status }) => { + if (!user) { + return status(401, { + status: "error", + message: "Unauthorized", + code: "UNAUTHORIZED", + }); + } + + const accessResult = await Result.tryPromise(async () => assertCanManageGraphFiles(user, params.id)); + if (accessResult.isErr()) { + return mapGraphError(status, accessResult.error); + } + const existingGraph = accessResult.value; + + const urls = [...new Set(body.urls.map((url) => url.trim()).filter(Boolean))]; + if (urls.length === 0) { + return status(400, { + status: "error", + message: "No changes requested", + code: API_ERROR_CODES.NO_CHANGES, + }); + } + + if (urls.length > MAX_REPOSITORY_URLS) { + return status( + 413, + errorResponse( + `At most ${MAX_REPOSITORY_URLS} repository URLs can be processed at once`, + API_ERROR_CODES.UPLOAD_LIMIT_EXCEEDED + ) + ); + } + + const repositoriesResult = await Result.tryPromise(async () => { + const repositories: LoadedRepository[] = []; + for (const url of urls) { + repositories.push(await loadRepositoryFromUrl(url)); + } + + return repositories; + }); + if (repositoriesResult.isErr()) { + return repositoryUrlErrorResponse(status, repositoriesResult.error); + } + const repositories = repositoriesResult.value; + + const seenSnapshotFiles = new Set(); + const repositorySources: RepositoryUploadSource[] = []; + + for (const repository of repositories) { + for (const file of repository.files) { + const checksum = await contentChecksum(file.content); + const snapshotFileKey = `${repository.url}:${checksum}`; + if (seenSnapshotFiles.has(snapshotFileKey)) { + continue; + } + + seenSnapshotFiles.add(snapshotFileKey); + repositorySources.push({ repository, file, checksum }); + } + } + + if (repositorySources.length === 0) { + return status(200, { + status: "success", + data: { + graph: existingGraph, + addedFiles: [], + workflowRunId: null, + }, + }); + } + + const repositoryModelResult = await Result.tryPromise(async () => { + await assertConfiguredUploadModels({ + organizationId: await getGraphOwnerModelOrganizationId({ + ownerMode: "graph", + graphId: existingGraph.id, + }), + files: repositorySources.map(() => ({ type: "code" as const })), + secret: env.AUTH_SECRET, + }); + }); + if (repositoryModelResult.isErr()) { + const error = + repositoryModelResult.error instanceof Error && repositoryModelResult.error.cause instanceof Error + ? repositoryModelResult.error.cause + : repositoryModelResult.error; + return mapGraphError(status, error); + } + + const uploadedFiles: UploadedFile[] = []; + try { + for (const source of repositorySources) { + const fileId = ulid(); + const name = `${source.repository.name}/${source.file.path}`; + const external = buildGitHubExternalCodeFile({ + repositoryUrl: source.repository.url, + commitSha: source.repository.commitSha, + path: source.file.path, + }); + + if (external) { + uploadedFiles.push({ + id: fileId, + name, + size: source.file.size, + type: "code", + mimeType: "text/plain", + key: external.key, + storageKind: "external", + externalProvider: external.provider, + externalUrl: external.rawUrl, + checksum: source.checksum, + metadata: serializeCodeFileMetadata({ + repositoryUrl: source.repository.url, + repositoryName: source.repository.name, + commitSha: source.repository.commitSha, + path: source.file.path, + external: { + provider: external.provider, + rawUrl: external.rawUrl, + htmlUrl: external.htmlUrl, + }, + }), + }); + continue; + } + + const upload = await putGraphFile( + existingGraph.id, + fileId, + name, + source.file.content, + env.S3_BUCKET + ); + + uploadedFiles.push({ + id: fileId, + name, + size: source.file.size, + type: "code", + mimeType: "text/plain", + key: upload.key, + storageKind: "internal", + checksum: source.checksum, + metadata: serializeCodeFileMetadata({ + repositoryUrl: source.repository.url, + repositoryName: source.repository.name, + commitSha: source.repository.commitSha, + path: source.file.path, + }), + }); + } + } catch (uploadError) { + const failedDeletes = await cleanupUploadedKeys( + uploadedFiles.filter((file) => file.storageKind !== "external").map((file) => file.key) + ); + + logError("graph repository URL add failed during file upload", { + graphId: existingGraph.id, + uploadedKeyCount: uploadedFiles.length, + failedS3CleanupCount: failedDeletes, + error: uploadError, + }); + + return status(500, { + status: "error", + message: "Internal server error", + code: "INTERNAL_SERVER_ERROR", + }); + } + + let result: Awaited>; + + try { + result = await commitGraphFileUploads({ + graph: existingGraph, + uploadedFiles, + supersedeRepositoryUrls: repositories.map((repository) => repository.url), + }); + } catch (dbPatchError) { + const failedDeletes = await cleanupUploadedKeys( + uploadedFiles.filter((file) => file.storageKind !== "external").map((file) => file.key) + ); + + logError("graph repository URL add failed during database update", { + graphId: existingGraph.id, + uploadedKeyCount: uploadedFiles.length, + failedS3CleanupCount: failedDeletes, + error: dbPatchError, + }); + + return status(500, { + status: "error", + message: "Internal server error", + code: "INTERNAL_SERVER_ERROR", + }); + } + + if (result.addedFiles.length === 0) { + return status(200, { + status: "success", + data: { + graph: result.graph, + addedFiles: result.addedFiles, + workflowRunId: null, + }, + }); + } + + try { + if (!result.processRunId) { + throw new Error("Missing process run id"); + } + + const handle = await ow.runWorkflow(processFilesSpec, { + graphId: existingGraph.id, + fileIds: result.addedFiles.map((file) => file.id), + processRunId: result.processRunId, + code: { kind: "repository", retiredFileIds: result.supersededFileIds }, + }); + + return status(200, { + status: "success", + data: { + graph: result.graph, + addedFiles: result.addedFiles, + workflowRunId: handle.workflowRun.id, + }, + }); + } catch (enqueueError) { + await restoreGraphFileChangeFailure( + existingGraph.id, + existingGraph, + result.addedFiles.map((file) => file.id), + uploadedFiles.filter((file) => file.storageKind !== "external").map((file) => file.key), + result.processRunId, + result.supersededFileIds + ); + + logError("graph repository URL add failed during workflow enqueue", { + graphId: existingGraph.id, + uploadedKeyCount: uploadedFiles.length, + addedFileCount: result.addedFiles.length, + error: enqueueError, + }); + + return status(500, { + status: "error", + message: "Internal server error", + code: "INTERNAL_SERVER_ERROR", + }); + } + }, + { + params: t.Object({ + id: t.String(), + }), + body: t.Object({ + urls: t.Array(t.String()), + }), + } + ) .post( "/:id/files", async ({ body, params, user, status }) => { @@ -649,77 +963,10 @@ export const graphRoute = new Elysia({ prefix: "/graphs" }) }); } - let graph = existingGraph; - let addedFiles: CreatedFileRecord[] = []; - let processRunId: string | undefined; + let result: Awaited>; try { - const result = await db.transaction(async (tx) => { - const insertedFiles = await tx - .insert(filesTable) - .values( - uploadedFiles.map((file) => ({ - id: file.id, - graphId: existingGraph.id, - name: file.name, - size: file.size, - type: file.type, - mimeType: file.mimeType, - key: file.key, - checksum: file.checksum, - })) - ) - .onConflictDoNothing() - .returning(selectFileFields); - - if (insertedFiles.length === 0) { - return { - graph: existingGraph, - addedFiles: insertedFiles, - processRunId: undefined, - }; - } - - const [updatedGraph] = await tx - .update(graphTable) - .set({ state: "updating" }) - .where(eq(graphTable.id, existingGraph.id)) - .returning(selectGraphFields); - - const [processRun] = await tx - .insert(processRunsTable) - .values({ - graphId: existingGraph.id, - status: "pending", - }) - .returning({ id: processRunsTable.id }); - if (!processRun) { - throw new Error("Failed to create process run"); - } - - await tx.insert(processRunFilesTable).values( - insertedFiles.map((file) => ({ - processRunId: processRun.id, - fileId: file.id, - })) - ); - - return { - graph: updatedGraph ?? existingGraph, - addedFiles: insertedFiles, - processRunId: processRun.id, - }; - }); - - graph = result.graph; - addedFiles = result.addedFiles; - processRunId = result.processRunId; - - const addedKeys = new Set(addedFiles.map((file) => file.key)); - const skippedKeys = uploadedFiles.map((file) => file.key).filter((key) => !addedKeys.has(key)); - if (skippedKeys.length > 0) { - await cleanupUploadedKeys(skippedKeys); - } + result = await commitGraphFileUploads({ graph: existingGraph, uploadedFiles }); } catch (dbPatchError) { const failedDeletes = await cleanupUploadedKeys(uploadedFiles.map((file) => file.key)); @@ -737,33 +984,33 @@ export const graphRoute = new Elysia({ prefix: "/graphs" }) }); } - if (addedFiles.length === 0) { + if (result.addedFiles.length === 0) { return status(200, { status: "success", data: { - graph, - addedFiles, + graph: result.graph, + addedFiles: result.addedFiles, workflowRunId: null, }, }); } try { - if (!processRunId) { + if (!result.processRunId) { throw new Error("Missing process run id"); } const handle = await ow.runWorkflow(processFilesSpec, { graphId: existingGraph.id, - fileIds: addedFiles.map((file) => file.id), - processRunId, + fileIds: result.addedFiles.map((file) => file.id), + processRunId: result.processRunId, }); return status(200, { status: "success", data: { - graph, - addedFiles, + graph: result.graph, + addedFiles: result.addedFiles, workflowRunId: handle.workflowRun.id, }, }); @@ -771,15 +1018,15 @@ export const graphRoute = new Elysia({ prefix: "/graphs" }) await restoreGraphFileChangeFailure( existingGraph.id, existingGraph, - addedFiles.map((file) => file.id), + result.addedFiles.map((file) => file.id), uploadedFiles.map((file) => file.key), - processRunId + result.processRunId ); logError("graph file add failed during workflow enqueue", { graphId: existingGraph.id, uploadedKeyCount: uploadedFiles.length, - addedFileCount: addedFiles.length, + addedFileCount: result.addedFiles.length, error: enqueueError, }); @@ -819,6 +1066,7 @@ export const graphRoute = new Elysia({ prefix: "/graphs" }) const [file] = await db .select({ id: filesTable.id, + type: filesTable.type, status: filesTable.status, processStep: filesTable.processStep, processErrorCode: filesTable.processErrorCode, @@ -906,6 +1154,7 @@ export const graphRoute = new Elysia({ prefix: "/graphs" }) graphId: existingGraph.id, fileIds: [file.id], processRunId: retry.runId, + ...(file.type === "code" ? { code: { kind: "repository" as const, retiredFileIds: [] } } : {}), }); return status(200, { diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index ca7eec95..6f17bb39 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -11,6 +11,8 @@ import { chatRoute } from "./routes/chat"; import { chatLibraryRoute } from "./routes/chat-library"; import { fileTypesRoute } from "./routes/file-types"; import { graphFilesRoute } from "./routes/graph-files"; +import { connectorRoute, repositoryGraphBindingRoute } from "./routes/connectors"; +import { connectorWebhookRoute } from "./routes/connector-webhooks"; import { graphRoute } from "./routes/graph"; import { graphSuggestionsRoute } from "./routes/graph-suggestions"; import { mcpRoute } from "./routes/mcp"; @@ -51,6 +53,7 @@ const app = new Elysia({ ) ) .use(mcpRoute) + .use(connectorWebhookRoute) .use(authMiddleware) .use(authRoute) .use(chatRoute) @@ -58,6 +61,8 @@ const app = new Elysia({ .use(fileTypesRoute) .use(graphFilesRoute) .use(graphSuggestionsRoute) + .use(connectorRoute) + .use(repositoryGraphBindingRoute) .use(graphRoute) .use(modelsRoute) .use(promptsRoute) diff --git a/apps/frontend/components/settings/sections/SystemConfigurationSection.test.tsx b/apps/frontend/components/settings/sections/SystemConfigurationSection.test.tsx index c6f46912..b9d7eaed 100644 --- a/apps/frontend/components/settings/sections/SystemConfigurationSection.test.tsx +++ b/apps/frontend/components/settings/sections/SystemConfigurationSection.test.tsx @@ -56,6 +56,15 @@ const records: FileTypeConfigRecord[] = [ chunk_size_editable: true, document_mode_editable: false, }, + { + file_type: "code", + loader: "text", + chunker: "semantic", + chunk_size: 2000, + document_mode: null, + chunk_size_editable: true, + document_mode_editable: false, + }, ]; const fetchFileTypeConfigsMock = vi.mocked(fetchFileTypeConfigs); @@ -76,11 +85,15 @@ describe("SystemConfigurationSection", () => { renderSection(); expect(await screen.findByText("Dateiverarbeitung")).toBeInTheDocument(); - expect(await screen.findByText("3 Dateitypen geladen")).toBeInTheDocument(); + expect(await screen.findByText("4 Dateitypen geladen")).toBeInTheDocument(); expect(screen.getByText("Dokumente")).toBeInTheDocument(); expect(screen.getByText("Medien")).toBeInTheDocument(); - expect(screen.getByText("pdf")).toBeInTheDocument(); - expect(screen.getByText("semantic")).toBeInTheDocument(); + const pdfRow = screen.getByText("PDF").closest("[data-file-type='pdf']"); + expect(pdfRow).not.toBeNull(); + expect(within(pdfRow!).getByText("pdf")).toBeInTheDocument(); + expect(within(pdfRow!).getByText("semantic")).toBeInTheDocument(); + expect(screen.getByText("Code")).toBeInTheDocument(); + expect(screen.getByText(".js, .ts, .tsx, .rs, .zig, .c, .h")).toBeInTheDocument(); }); test("keeps save disabled until an editable field changes", async () => { @@ -190,7 +203,7 @@ describe("SystemConfigurationSection", () => { await user.click(screen.getByRole("button", { name: "Erneut versuchen" })); - expect(await screen.findByText("3 Dateitypen geladen")).toBeInTheDocument(); + expect(await screen.findByText("4 Dateitypen geladen")).toBeInTheDocument(); expect(fetchFileTypeConfigsMock).toHaveBeenCalledTimes(2); }); diff --git a/apps/frontend/components/settings/sections/SystemConfigurationSection.tsx b/apps/frontend/components/settings/sections/SystemConfigurationSection.tsx index 130c4ae3..16ee78a5 100644 --- a/apps/frontend/components/settings/sections/SystemConfigurationSection.tsx +++ b/apps/frontend/components/settings/sections/SystemConfigurationSection.tsx @@ -103,6 +103,10 @@ const FILE_TYPE_META: Record; diff --git a/apps/frontend/lib/api/connectors.test.ts b/apps/frontend/lib/api/connectors.test.ts new file mode 100644 index 00000000..be5c0e27 --- /dev/null +++ b/apps/frontend/lib/api/connectors.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, test, vi } from "vitest"; +import type { KiwiApiClient } from "./client"; +import { + completeGitHubConnectorInstallation, + completeGitHubConnectorManifest, + createGitLabConnector, + createRepositoryGraph, + fetchConnectorBranches, + fetchConnectorInstallations, + fetchConnectorRepositories, + fetchConnectors, + startConnectorConnect, + startGitHubConnectorManifest, + syncRepositoryGraphBinding, +} from "./connectors"; + +const connector = { + id: "connector-1", + provider: "github" as const, + name: "GitHub", + slug: "github", + status: "active" as const, + appId: "123", + clientId: null, + createdAt: "2026-06-14T00:00:00.000Z", + updatedAt: "2026-06-14T00:00:00.000Z", +}; + +const installation = { + id: "installation-1", + connectorId: connector.id, + provider: "github" as const, + providerInstallationId: "987", + providerAccountLogin: "kiwi-org", + providerAccountType: "organization" as const, + organizationId: "org-1", + teamId: null, + repositorySelection: "selected" as const, + status: "active" as const, + createdAt: connector.createdAt, + updatedAt: connector.updatedAt, +}; + +const repository = { + provider: "github" as const, + id: "repo/1", + fullName: "kiwi/app", + name: "app", + htmlUrl: "https://github.com/kiwi/app", + defaultBranch: "main", + private: true, +}; + +const branch = { name: "main", commitSha: "abc123" }; + +const binding = { + id: "binding-1", + graphId: "graph-1", + connectorInstallationId: installation.id, + provider: "github" as const, + providerRepositoryId: repository.id, + repositoryFullName: repository.fullName, + repositoryHtmlUrl: repository.htmlUrl, + branch: branch.name, + lastSeenCommitSha: branch.commitSha, + lastSyncedCommitSha: null, + syncStatus: "pending" as const, + syncErrorCode: null, + webhookEnabled: true, + createdAt: connector.createdAt, + updatedAt: connector.updatedAt, +}; + +describe("connector API helpers", () => { + test("fetches connectors", async () => { + const get = vi.fn(async () => ({ status: "success" as const, data: [connector] })); + const client = { baseURL: "/api", get } as unknown as KiwiApiClient; + + await expect(fetchConnectors(client)).resolves.toEqual([connector]); + expect(get).toHaveBeenCalledWith("/connectors"); + }); + + test("starts the GitHub manifest flow", async () => { + const input = { name: "GitHub" }; + const data = { manifestUrl: "https://github.com/settings/apps/new", state: "state-1" }; + const post = vi.fn(async () => ({ status: "success" as const, data })); + const client = { baseURL: "/api", post } as unknown as KiwiApiClient; + + await expect(startGitHubConnectorManifest(client, input)).resolves.toEqual(data); + expect(post).toHaveBeenCalledWith("/connectors/github/manifest/start", input); + }); + + test("creates a GitLab connector without rendering returned secrets", async () => { + const input = { + name: "GitLab", + baseUrl: "https://gitlab.com", + clientId: "client-id", + clientSecret: "client-secret", + webhookSecret: "webhook-secret", + }; + const post = vi.fn(async () => ({ status: "success" as const, data: { ...connector, provider: "gitlab" as const } })); + const client = { baseURL: "/api", post } as unknown as KiwiApiClient; + + await expect(createGitLabConnector(client, input)).resolves.toMatchObject({ provider: "gitlab" }); + expect(post).toHaveBeenCalledWith("/connectors/gitlab", input); + }); + + test("completes GitHub connector callbacks through API routes", async () => { + const get = vi + .fn() + .mockResolvedValueOnce({ status: "success" as const, data: connector }) + .mockResolvedValueOnce({ status: "success" as const, data: installation }); + const client = { baseURL: "/api", get } as unknown as KiwiApiClient; + + await expect(completeGitHubConnectorManifest(client, { code: "code-1", state: "state-1" })).resolves.toEqual( + connector + ); + await expect( + completeGitHubConnectorInstallation(client, { + state: "state-2", + installation_id: "installation-1", + setup_action: "install", + }) + ).resolves.toEqual(installation); + expect(get).toHaveBeenNthCalledWith(1, "/connectors/github/manifest/callback?code=code-1&state=state-1"); + expect(get).toHaveBeenNthCalledWith( + 2, + "/connectors/github/install/callback?state=state-2&installation_id=installation-1&setup_action=install" + ); + }); + + test("starts connector installation flow for a managed owner", async () => { + const get = vi.fn(async () => ({ status: "success" as const, data: { redirectUrl: "https://github.com/apps/kiwi/install" } })); + const client = { baseURL: "/api", get } as unknown as KiwiApiClient; + + await expect(startConnectorConnect(client, connector.id, { teamId: "team-1" })).resolves.toEqual({ + redirectUrl: "https://github.com/apps/kiwi/install", + }); + expect(get).toHaveBeenCalledWith("/connectors/connector-1/connect?teamId=team-1"); + }); + + test("fetches installation repositories and encoded branches", async () => { + const get = vi + .fn() + .mockResolvedValueOnce({ status: "success" as const, data: [installation] }) + .mockResolvedValueOnce({ status: "success" as const, data: [repository] }) + .mockResolvedValueOnce({ status: "success" as const, data: [branch] }); + const client = { baseURL: "/api", get } as unknown as KiwiApiClient; + + await expect(fetchConnectorInstallations(client, connector.id)).resolves.toEqual([installation]); + await expect(fetchConnectorRepositories(client, connector.id, installation.id)).resolves.toEqual([repository]); + await expect(fetchConnectorBranches(client, connector.id, installation.id, repository.id)).resolves.toEqual([branch]); + expect(get).toHaveBeenNthCalledWith(1, "/connectors/connector-1/installations"); + expect(get).toHaveBeenNthCalledWith(2, "/connectors/connector-1/repositories?installationId=installation-1"); + expect(get).toHaveBeenNthCalledWith( + 3, + "/connectors/connector-1/repositories/repo%2F1/branches?installationId=installation-1" + ); + }); + + test("creates and syncs repository graphs", async () => { + const graph = { + id: "row-1", + name: "app", + description: null, + organizationId: "org-1", + teamId: null, + userId: null, + graphId: "graph-1", + hidden: false, + state: "updating" as const, + }; + const input = { + connectorInstallationId: installation.id, + repositoryId: repository.id, + repositoryFullName: repository.fullName, + repositoryHtmlUrl: repository.htmlUrl, + branch: branch.name, + name: "app", + owner: { kind: "organization" as const }, + }; + const post = vi + .fn() + .mockResolvedValueOnce({ status: "success" as const, data: { graph, binding, workflowRunId: "run-1" } }) + .mockResolvedValueOnce({ status: "success" as const, data: { binding, workflowRunId: "run-2" } }); + const client = { baseURL: "/api", post } as unknown as KiwiApiClient; + + await expect(createRepositoryGraph(client, connector.id, input)).resolves.toEqual({ graph, binding, workflowRunId: "run-1" }); + await expect(syncRepositoryGraphBinding(client, binding.id)).resolves.toEqual({ binding, workflowRunId: "run-2" }); + expect(post).toHaveBeenNthCalledWith(1, "/connectors/connector-1/repository-graphs", input); + expect(post).toHaveBeenNthCalledWith(2, "/repository-graph-bindings/binding-1/sync"); + }); +}); diff --git a/apps/frontend/lib/api/connectors.ts b/apps/frontend/lib/api/connectors.ts new file mode 100644 index 00000000..cbe2cfb1 --- /dev/null +++ b/apps/frontend/lib/api/connectors.ts @@ -0,0 +1,169 @@ +import type { + ApiResponse, + ConnectorBranchListResponse, + ConnectorBranchRecord, + ConnectorConnectStartResponse, + ConnectorInstallationListResponse, + ConnectorInstallationRecord, + ConnectorListResponse, + ConnectorRecord, + ConnectorRepositoryListResponse, + ConnectorRepositoryRecord, + GitHubConnectorManifestStartInput, + GitHubConnectorManifestStartResponse, + GitHubConnectorManifestStartSuccessData, + GitLabConnectorCreateInput, + GitLabConnectorCreateResponse, + RepositoryGraphBindingResponse, + RepositoryGraphBindingRecord, + RepositoryGraphBindingSyncResponse, + RepositoryGraphBindingSyncSuccessData, + RepositoryGraphCreateInput, + RepositoryGraphCreateResponse, + RepositoryGraphCreateSuccessData, +} from "@kiwi/contracts"; + +import { unwrapApiResponse, type KiwiApiClient } from "./client"; + +export async function fetchConnectors(client: KiwiApiClient): Promise { + const response = await client.get("/connectors"); + return unwrapApiResponse(response); +} + +export async function startGitHubConnectorManifest( + client: KiwiApiClient, + input: GitHubConnectorManifestStartInput +): Promise { + const response = await client.post("/connectors/github/manifest/start", input); + return unwrapApiResponse(response); +} + +export async function completeGitHubConnectorManifest( + client: KiwiApiClient, + input: { code: string; state: string } +): Promise { + const params = new URLSearchParams(input); + const response = await client.get< + ApiResponse + >(`/connectors/github/manifest/callback?${params.toString()}`); + return unwrapApiResponse(response); +} + +export async function completeGitHubConnectorInstallation( + client: KiwiApiClient, + input: { state: string; installation_id: string; setup_action?: string } +): Promise { + const params = new URLSearchParams(); + params.set("state", input.state); + params.set("installation_id", input.installation_id); + if (input.setup_action) { + params.set("setup_action", input.setup_action); + } + const response = await client.get< + ApiResponse + >(`/connectors/github/install/callback?${params.toString()}`); + return unwrapApiResponse(response); +} + +export async function createGitLabConnector( + client: KiwiApiClient, + input: GitLabConnectorCreateInput +): Promise { + const response = await client.post("/connectors/gitlab", input); + return unwrapApiResponse(response); +} +export async function startConnectorConnect( + client: KiwiApiClient, + connectorId: string, + input: { organizationId?: string; teamId?: string } +): Promise<{ redirectUrl: string }> { + const params = new URLSearchParams(); + if (input.organizationId) { + params.set("organizationId", input.organizationId); + } + if (input.teamId) { + params.set("teamId", input.teamId); + } + const response = await client.get( + `/connectors/${encodeURIComponent(connectorId)}/connect?${params.toString()}` + ); + return unwrapApiResponse(response); +} + +export async function fetchConnectorInstallations( + client: KiwiApiClient, + connectorId: string +): Promise { + const response = await client.get( + `/connectors/${encodeURIComponent(connectorId)}/installations` + ); + return unwrapApiResponse(response); +} + +export async function fetchConnectorRepositories( + client: KiwiApiClient, + connectorId: string, + installationId: string +): Promise { + const params = new URLSearchParams({ installationId }); + const response = await client.get( + `/connectors/${encodeURIComponent(connectorId)}/repositories?${params.toString()}` + ); + return unwrapApiResponse(response); +} + +export async function fetchConnectorBranches( + client: KiwiApiClient, + connectorId: string, + installationId: string, + repositoryId: string +): Promise { + const params = new URLSearchParams({ installationId }); + const response = await client.get( + `/connectors/${encodeURIComponent(connectorId)}/repositories/${encodeURIComponent(repositoryId)}/branches?${params.toString()}` + ); + return unwrapApiResponse(response); +} + +export async function createRepositoryGraph( + client: KiwiApiClient, + connectorId: string, + input: RepositoryGraphCreateInput +): Promise { + const response = await client.post( + `/connectors/${encodeURIComponent(connectorId)}/repository-graphs`, + input + ); + return unwrapApiResponse(response); +} + +export async function fetchRepositoryGraphBinding( + client: KiwiApiClient, + bindingId: string +): Promise { + const response = await client.get( + `/repository-graph-bindings/${encodeURIComponent(bindingId)}` + ); + return unwrapApiResponse(response); +} + +export async function syncRepositoryGraphBinding( + client: KiwiApiClient, + bindingId: string +): Promise { + const response = await client.post( + `/repository-graph-bindings/${encodeURIComponent(bindingId)}/sync` + ); + return unwrapApiResponse(response); +} + +export type { + ConnectorBranchRecord, + ConnectorInstallationRecord, + ConnectorRecord, + ConnectorRepositoryRecord, + GitHubConnectorManifestStartInput, + GitLabConnectorCreateInput, + RepositoryGraphBindingRecord, + RepositoryGraphCreateInput, +}; diff --git a/apps/frontend/lib/api/index.ts b/apps/frontend/lib/api/index.ts index 59775757..bbaaf6b9 100644 --- a/apps/frontend/lib/api/index.ts +++ b/apps/frontend/lib/api/index.ts @@ -43,6 +43,30 @@ export type { SourceReferenceResponse, TextUnitResponse, } from "@kiwi/contracts"; +export type { + ConnectorBranchListResponse, + ConnectorBranchRecord, + ConnectorInstallationListResponse, + ConnectorInstallationRecord, + ConnectorListResponse, + ConnectorProvider, + ConnectorRecord, + ConnectorRepositoryListResponse, + ConnectorRepositoryRecord, + GitHubConnectorManifestStartInput, + GitHubConnectorManifestStartResponse, + GitHubConnectorManifestStartSuccessData, + GitLabConnectorCreateInput, + GitLabConnectorCreateResponse, + RepositoryGraphBindingRecord, + RepositoryGraphBindingResponse, + RepositoryGraphBindingSyncResponse, + RepositoryGraphBindingSyncSuccessData, + RepositoryGraphCreateInput, + RepositoryGraphCreateResponse, + RepositoryGraphCreateSuccessData, +} from "@kiwi/contracts"; + // Re-export team API helpers under the existing frontend group names. export { @@ -57,6 +81,21 @@ export { updateGroup, updateGroupUsers, } from "./groups"; +export { + completeGitHubConnectorInstallation, + completeGitHubConnectorManifest, + createGitLabConnector, + createRepositoryGraph, + fetchConnectorBranches, + fetchConnectorInstallations, + fetchConnectorRepositories, + fetchConnectors, + fetchRepositoryGraphBinding, + startConnectorConnect, + startGitHubConnectorManifest, + syncRepositoryGraphBinding, +} from "./connectors"; + // Re-export projects API export { diff --git a/apps/frontend/messages/de.json b/apps/frontend/messages/de.json index 24ea0c60..6a6ad695 100644 --- a/apps/frontend/messages/de.json +++ b/apps/frontend/messages/de.json @@ -473,7 +473,7 @@ "settings.systemConfig.fileProcessing.group.media": "Medien", "settings.systemConfig.fileProcessing.group.structured": "Strukturierte Daten", "settings.systemConfig.fileProcessing.group.communication": "Kommunikation & Web", - "settings.systemConfig.fileProcessing.group.text": "Text", + "settings.systemConfig.fileProcessing.group.text": "Text & Code", "settings.systemConfig.fileProcessing.fileType.pdf": "PDF", "settings.systemConfig.fileProcessing.fileType.doc": "DOC/DOCX", "settings.systemConfig.fileProcessing.fileType.sheet": "Tabellen", @@ -492,6 +492,7 @@ "settings.systemConfig.fileProcessing.fileType.xml": "XML", "settings.systemConfig.fileProcessing.fileType.yaml": "YAML", "settings.systemConfig.fileProcessing.fileType.toml": "TOML", + "settings.systemConfig.fileProcessing.fileType.code": "Code", "settings.systemConfig.fileProcessing.fileType.text": "Text", "settings.systemConfig.fileProcessing.extensions.pdf": ".pdf", "settings.systemConfig.fileProcessing.extensions.doc": ".doc, .docx", @@ -511,6 +512,7 @@ "settings.systemConfig.fileProcessing.extensions.xml": ".xml, .xsd", "settings.systemConfig.fileProcessing.extensions.yaml": ".yaml, .yml", "settings.systemConfig.fileProcessing.extensions.toml": ".toml", + "settings.systemConfig.fileProcessing.extensions.code": ".js, .ts, .tsx, .rs, .zig, .c, .h", "settings.systemConfig.fileProcessing.extensions.text": ".txt und Fallback", "settings.systemConfig.fileProcessing.notApplicable": "Nicht anwendbar", "settings.systemConfig.fileProcessing.chunkSize.aria": "Chunk-Größe für {type}", diff --git a/apps/frontend/messages/en.json b/apps/frontend/messages/en.json index 35e2f248..7b8cc4aa 100644 --- a/apps/frontend/messages/en.json +++ b/apps/frontend/messages/en.json @@ -473,7 +473,7 @@ "settings.systemConfig.fileProcessing.group.media": "Media", "settings.systemConfig.fileProcessing.group.structured": "Structured data", "settings.systemConfig.fileProcessing.group.communication": "Communication & web", - "settings.systemConfig.fileProcessing.group.text": "Text", + "settings.systemConfig.fileProcessing.group.text": "Text & code", "settings.systemConfig.fileProcessing.fileType.pdf": "PDF", "settings.systemConfig.fileProcessing.fileType.doc": "DOC/DOCX", "settings.systemConfig.fileProcessing.fileType.sheet": "Spreadsheets", @@ -492,6 +492,7 @@ "settings.systemConfig.fileProcessing.fileType.xml": "XML", "settings.systemConfig.fileProcessing.fileType.yaml": "YAML", "settings.systemConfig.fileProcessing.fileType.toml": "TOML", + "settings.systemConfig.fileProcessing.fileType.code": "Code", "settings.systemConfig.fileProcessing.fileType.text": "Text", "settings.systemConfig.fileProcessing.extensions.pdf": ".pdf", "settings.systemConfig.fileProcessing.extensions.doc": ".doc, .docx", @@ -511,6 +512,7 @@ "settings.systemConfig.fileProcessing.extensions.xml": ".xml, .xsd", "settings.systemConfig.fileProcessing.extensions.yaml": ".yaml, .yml", "settings.systemConfig.fileProcessing.extensions.toml": ".toml", + "settings.systemConfig.fileProcessing.extensions.code": ".js, .ts, .tsx, .rs, .zig, .c, .h", "settings.systemConfig.fileProcessing.extensions.text": ".txt and fallback", "settings.systemConfig.fileProcessing.notApplicable": "Not applicable", "settings.systemConfig.fileProcessing.chunkSize.aria": "Chunk size for {type}", diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile index 1658a25e..eb0ee011 100644 --- a/apps/worker/Dockerfile +++ b/apps/worker/Dockerfile @@ -1,7 +1,13 @@ FROM oven/bun:1 RUN apt-get update \ - && apt-get install -y --no-install-recommends ghostscript \ + && apt-get install -y --no-install-recommends \ + g++ \ + ghostscript \ + make \ + node-gyp \ + nodejs \ + python3 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -12,6 +18,7 @@ COPY apps/api/package.json apps/api/package.json COPY apps/frontend/package.json apps/frontend/package.json COPY apps/worker/package.json apps/worker/package.json COPY packages/ai/package.json packages/ai/package.json +COPY packages/connectors/package.json packages/connectors/package.json COPY packages/auth/package.json packages/auth/package.json COPY packages/contracts/package.json packages/contracts/package.json COPY packages/db/package.json packages/db/package.json @@ -19,7 +26,15 @@ COPY packages/files/package.json packages/files/package.json COPY packages/graph/package.json packages/graph/package.json COPY packages/logger/package.json packages/logger/package.json -RUN bun install --frozen-lockfile +RUN CXXFLAGS="-std=c++20" bun install --frozen-lockfile + +RUN arch="$(node -p 'process.arch')" \ + && cd packages/graph/node_modules/tree-sitter \ + && CXXFLAGS="-std=c++20" node-gyp rebuild \ + && mkdir -p "prebuilds/linux-${arch}" \ + && cp build/Release/tree_sitter_runtime_binding.node "prebuilds/linux-${arch}/tree-sitter.node" \ + && cd ../@tree-sitter-grammars/tree-sitter-zig \ + && cp "prebuilds/linux-${arch}/@tree-sitter-grammars+tree-sitter-zig.node" "prebuilds/linux-${arch}/tree-sitter-zig.node" COPY apps/worker/index.ts apps/worker/index.ts COPY apps/worker/env.ts apps/worker/env.ts @@ -30,6 +45,8 @@ COPY apps/worker/lib apps/worker/lib COPY apps/worker/workflows apps/worker/workflows COPY packages/ai/src packages/ai/src COPY packages/ai/tsconfig.json packages/ai/tsconfig.json +COPY packages/connectors/src packages/connectors/src +COPY packages/connectors/tsconfig.json packages/connectors/tsconfig.json COPY packages/contracts/src packages/contracts/src COPY packages/contracts/tsconfig.json packages/contracts/tsconfig.json COPY packages/db/src packages/db/src diff --git a/apps/worker/lib/__tests__/code-manifest.test.ts b/apps/worker/lib/__tests__/code-manifest.test.ts new file mode 100644 index 00000000..5fed9cc6 --- /dev/null +++ b/apps/worker/lib/__tests__/code-manifest.test.ts @@ -0,0 +1,280 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +type MockFileRow = { + id: string; + name: string; + key: string; + storageKind: string; + externalUrl: string | null; + externalProvider: string | null; + repositoryBindingId: string | null; + metadata: string; +}; + +const baseRows: MockFileRow[] = [ + { + id: "file-new", + name: "widgets/src/index.ts", + key: "key-new", + storageKind: "internal", + externalUrl: null, + externalProvider: null, + repositoryBindingId: null, + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/index.ts", + }), + }, + { + id: "file-existing", + name: "widgets/src/helper.ts", + key: "key-existing", + storageKind: "internal", + externalUrl: null, + externalProvider: null, + repositoryBindingId: null, + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/helper.ts", + }), + }, + { + id: "file-other", + name: "other/src/helper.ts", + key: "key-other", + storageKind: "internal", + externalUrl: null, + externalProvider: null, + repositoryBindingId: null, + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/other.git", + repositoryName: "other", + commitSha: "commit-1", + path: "src/helper.ts", + }), + }, +]; + +const fileContents: Record = { + "key-new": "import { helper } from './helper';\nexport function main() { return helper(); }\n", + "key-existing": "export function helper() { return 1; }\n", + "key-other": "export function helper() { return 2; }\n", + "key-connector-new": "import { shared } from './shared';\nexport const main = () => shared;\n", + "key-connector-existing": "export const shared = 1;\n", + "key-connector-other-binding": "export const shared = 2;\n", +}; + +let rows = [...baseRows]; +let selectedFileIds = ["file-new"]; +let selectCallCount = 0; +let uploadedManifest: unknown; + +mock.module("@kiwi/db", () => ({ + db: { + select: () => ({ + from: () => ({ + where: async () => { + selectCallCount += 1; + return selectCallCount === 1 ? rows.filter((row) => selectedFileIds.includes(row.id)) : rows; + }, + }), + }), + }, +})); + +mock.module("@kiwi/files", () => ({ + getFile: async (key: string) => { + const content = fileContents[key]; + return content ? { content } : null; + }, + putNamedFile: async (_name: string, content: string) => { + uploadedManifest = JSON.parse(content); + return { key: "manifest-key" }; + }, +})); + +mock.module("../../env", () => ({ + env: { S3_BUCKET: "test-bucket" }, +})); + +// Dynamic import is required so module mocks and env are installed before worker modules are evaluated. +const { prepareCodeManifest } = await import("../code-manifest"); +const { readFileContentSource } = await import("../file-content-source"); + +describe("prepareCodeManifest", () => { + beforeEach(() => { + selectCallCount = 0; + uploadedManifest = undefined; + selectedFileIds = ["file-new"]; + rows = [...baseRows]; + }); + + test("includes active files from the same repository commit as selected files", async () => { + const key = await prepareCodeManifest({ graphId: "graph-1", fileIds: ["file-new"], processRunId: "run-1" }); + + expect(key).toBe("manifest-key"); + expect((uploadedManifest as { files: Array<{ path: string }> }).files.map((file) => file.path).sort()).toEqual([ + "src/helper.ts", + "src/index.ts", + ]); + }); + + test("includes unchanged connector siblings from the same binding across mixed commits", async () => { + rows = [ + { + id: "file-connector-new", + name: "widgets/src/index.ts", + key: "key-connector-new", + storageKind: "internal", + externalUrl: null, + externalProvider: null, + repositoryBindingId: "binding-1", + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-2", + path: "src/index.ts", + }), + }, + { + id: "file-connector-existing", + name: "widgets/src/shared.ts", + key: "key-connector-existing", + storageKind: "internal", + externalUrl: null, + externalProvider: null, + repositoryBindingId: "binding-1", + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/shared.ts", + }), + }, + { + id: "file-connector-other-binding", + name: "widgets/src/shared.ts", + key: "key-connector-other-binding", + storageKind: "internal", + externalUrl: null, + externalProvider: null, + repositoryBindingId: "binding-2", + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/shared.ts", + }), + }, + ]; + selectedFileIds = ["file-connector-new"]; + + const key = await prepareCodeManifest({ graphId: "graph-1", fileIds: selectedFileIds, processRunId: "run-1" }); + + expect(key).toBe("manifest-key"); + expect((uploadedManifest as { files: Array<{ path: string }> }).files.map((file) => file.path).sort()).toEqual([ + "src/index.ts", + "src/shared.ts", + ]); + expect((uploadedManifest as { files: Array<{ fileId: string }> }).files.map((file) => file.fileId).sort()).toEqual([ + "file-connector-existing", + "file-connector-new", + ]); + }); + + test("includes external GitHub files from the same repository commit", async () => { + rows = [ + ...baseRows, + { + id: "file-external", + name: "widgets/src/external.ts", + key: "external:github:acme/widgets@commit-1:src/external.ts", + storageKind: "external", + externalUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/external.ts", + externalProvider: "github", + repositoryBindingId: null, + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/external.ts", + }), + }, + ]; + globalThis.fetch = (async () => + new Response("export const external = true;\n", { + headers: { "content-type": "text/plain" }, + })) as unknown as typeof fetch; + + const key = await prepareCodeManifest({ graphId: "graph-1", fileIds: ["file-new"], processRunId: "run-1" }); + + expect(key).toBe("manifest-key"); + expect((uploadedManifest as { files: Array<{ path: string }> }).files.map((file) => file.path).sort()).toEqual([ + "src/external.ts", + "src/helper.ts", + "src/index.ts", + ]); + }); +}); + +describe("readFileContentSource", () => { + test("reads internal S3 content", async () => { + await expect(readFileContentSource({ kind: "internal", key: "key-new" })).resolves.toBe( + "import { helper } from './helper';\nexport function main() { return helper(); }\n" + ); + }); + + test("fetches external GitHub raw content", async () => { + globalThis.fetch = (async () => + new Response("export const value = 1;\n", { + headers: { "content-type": "text/plain" }, + })) as unknown as typeof fetch; + + await expect( + readFileContentSource({ + kind: "external", + provider: "github", + url: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts", + }) + ).resolves.toBe("export const value = 1;\n"); + }); + + test("rejects non-GitHub external URLs before fetching", async () => { + let fetched = false; + globalThis.fetch = (async () => { + fetched = true; + return new Response("nope"); + }) as unknown as typeof fetch; + + await expect( + readFileContentSource({ + kind: "external", + provider: "github", + url: "https://example.com/acme/widgets/commit-1/src/index.ts", + }) + ).rejects.toThrow("Unsupported external file source"); + expect(fetched).toBe(false); + }); + + test("rejects oversized external responses", async () => { + globalThis.fetch = (async () => + new Response("too large", { + headers: { + "content-type": "text/plain", + "content-length": String(2 * 1024 * 1024 + 1), + }, + })) as unknown as typeof fetch; + + await expect( + readFileContentSource({ + kind: "external", + provider: "github", + url: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts", + }) + ).rejects.toThrow("too large"); + }); +}); diff --git a/apps/worker/lib/code-file-metadata.ts b/apps/worker/lib/code-file-metadata.ts new file mode 100644 index 00000000..386820b7 --- /dev/null +++ b/apps/worker/lib/code-file-metadata.ts @@ -0,0 +1 @@ +export { parseCodeFileMetadata, serializeCodeFileMetadata, type CodeFileMetadata } from "@kiwi/graph/code/metadata"; diff --git a/apps/worker/lib/code-manifest.ts b/apps/worker/lib/code-manifest.ts new file mode 100644 index 00000000..ad5d9910 --- /dev/null +++ b/apps/worker/lib/code-manifest.ts @@ -0,0 +1,128 @@ +import { and, eq, inArray } from "drizzle-orm"; +import { db } from "@kiwi/db"; +import { filesTable } from "@kiwi/db/tables/graph"; +import { getFile, putNamedFile } from "@kiwi/files"; +import { buildCodeRepositoryManifest } from "@kiwi/graph/code/repository"; +import type { CodeRepositoryFile, CodeRepositoryManifest } from "@kiwi/graph/code/repository"; +import { env } from "../env"; +import { parseCodeFileMetadata } from "./code-file-metadata"; +import { fileContentSourceFromRow, readFileContentSource } from "./file-content-source"; + +type ManifestFileRow = { + id: string; + name: string; + key: string; + metadata: string | null; + storageKind: string; + externalUrl: string | null; + externalProvider: string | null; + repositoryBindingId: string | null; +}; + +export async function prepareCodeManifest(options: { + graphId: string; + fileIds: string[]; + processRunId?: string; +}): Promise { + if (options.fileIds.length === 0) { + return undefined; + } + + const selectedRows = await db + .select({ + id: filesTable.id, + name: filesTable.name, + key: filesTable.key, + metadata: filesTable.metadata, + storageKind: filesTable.storageKind, + externalUrl: filesTable.externalUrl, + externalProvider: filesTable.externalProvider, + repositoryBindingId: filesTable.repositoryBindingId, + }) + .from(filesTable) + .where( + and( + eq(filesTable.graphId, options.graphId), + eq(filesTable.type, "code"), + eq(filesTable.deleted, false), + inArray(filesTable.id, options.fileIds) + ) + ); + + if (selectedRows.length === 0) { + return undefined; + } + + const selectedScopes = new Set(selectedRows.map(repositoryManifestScopeKey).filter((scope): scope is string => scope !== null)); + const rows = selectedScopes.size + ? await db + .select({ + id: filesTable.id, + name: filesTable.name, + key: filesTable.key, + metadata: filesTable.metadata, + storageKind: filesTable.storageKind, + externalUrl: filesTable.externalUrl, + externalProvider: filesTable.externalProvider, + repositoryBindingId: filesTable.repositoryBindingId, + }) + .from(filesTable) + .where(and(eq(filesTable.graphId, options.graphId), eq(filesTable.type, "code"), eq(filesTable.deleted, false))) + : selectedRows; + + const files: CodeRepositoryFile[] = []; + for (const row of rows) { + const metadata = parseCodeFileMetadata(row.metadata); + const scope = repositoryManifestScopeKey(row); + if (metadata && selectedScopes.size > 0 && (!scope || !selectedScopes.has(scope))) { + continue; + } + + const content = await readFileContentSource(fileContentSourceFromRow(row)); + if (!metadata || content === null) { + continue; + } + + files.push({ + fileId: row.id, + content, + ...metadata, + }); + } + + if (files.length === 0) { + return undefined; + } + + const manifest = buildCodeRepositoryManifest(files); + const uploaded = await putNamedFile( + "code-manifest.json", + JSON.stringify(manifest), + `graphs/${options.graphId}/process-runs/${options.processRunId ?? "adhoc"}`, + env.S3_BUCKET + ); + + return uploaded.key; +} + +function repositoryManifestScopeKey(row: Pick): string | null { + if (row.repositoryBindingId) { + return `binding:${row.repositoryBindingId}`; + } + + const metadata = parseCodeFileMetadata(row.metadata); + if (!metadata) { + return null; + } + + return `repository:${metadata.repositoryUrl}\0${metadata.commitSha}`; +} + +export async function loadCodeManifest(key: string): Promise { + const manifest = await getFile(key, env.S3_BUCKET, "json"); + if (!manifest) { + throw new Error(`Failed to load code manifest from ${key}`); + } + + return manifest.content; +} diff --git a/apps/worker/lib/code-repository-finalizer.ts b/apps/worker/lib/code-repository-finalizer.ts new file mode 100644 index 00000000..f91a7dbe --- /dev/null +++ b/apps/worker/lib/code-repository-finalizer.ts @@ -0,0 +1,143 @@ +import { db } from "@kiwi/db"; +import { currentSourcePredicate, currentSourceSql } from "@kiwi/db/source-validity"; +import { filesTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { and, eq, inArray, isNotNull, sql } from "drizzle-orm"; +import { parseCodeFileMetadata } from "./code-file-metadata"; +import { textArray } from "./sql"; + +export type RepositoryFileMetadataRow = { + id: string; + metadata: string | null; +}; + +export type RepositoryFinalizationTargets = { + repositoryUrls: string[]; + olderFileIds: string[]; +}; + +export type RepositorySourceInvalidationResult = { + entityIds: string[]; + relationshipIds: string[]; +}; + +export function resolveRepositoryFinalizationTargets( + latestRows: RepositoryFileMetadataRow[], + candidateRows: RepositoryFileMetadataRow[] +): RepositoryFinalizationTargets { + const latestFileIds = new Set(latestRows.map((row) => row.id)); + const repositoryUrls = [ + ...new Set( + latestRows + .map((row) => parseCodeFileMetadata(row.metadata)?.repositoryUrl) + .filter((url): url is string => url !== undefined) + ), + ]; + if (repositoryUrls.length === 0) { + return { repositoryUrls: [], olderFileIds: [] }; + } + + const repositoryUrlSet = new Set(repositoryUrls); + const olderFileIds = candidateRows + .filter((row) => !latestFileIds.has(row.id)) + .filter((row) => { + const metadata = parseCodeFileMetadata(row.metadata); + return metadata !== null && repositoryUrlSet.has(metadata.repositoryUrl); + }) + .map((row) => row.id); + + return { repositoryUrls, olderFileIds }; +} + +export async function invalidateSupersededRepositorySources(options: { + graphId: string; + latestFileIds?: string[]; + retiredFileIds?: string[]; +}): Promise { + if (options.retiredFileIds !== undefined) { + return invalidateRepositorySourceTargets({ + graphId: options.graphId, + retiredFileIds: options.retiredFileIds, + markDeleted: true, + }); + } + + const latestFileIds = options.latestFileIds; + if (!latestFileIds || latestFileIds.length === 0) { + return { entityIds: [], relationshipIds: [] }; + } + + return db.transaction(async (tx) => { + const latestRows = await tx + .select({ id: filesTable.id, metadata: filesTable.metadata }) + .from(filesTable) + .where(and(eq(filesTable.graphId, options.graphId), inArray(filesTable.id, latestFileIds))); + const candidateRows = await tx + .select({ id: filesTable.id, metadata: filesTable.metadata }) + .from(filesTable) + .where(and(eq(filesTable.graphId, options.graphId), eq(filesTable.type, "code"))); + const targets = resolveRepositoryFinalizationTargets(latestRows, candidateRows); + return invalidateRepositorySources(tx, options.graphId, targets.olderFileIds, false); + }); +} + +async function invalidateRepositorySourceTargets(options: { + graphId: string; + retiredFileIds: string[]; + markDeleted: boolean; +}): Promise { + if (options.retiredFileIds.length === 0) { + return { entityIds: [], relationshipIds: [] }; + } + + return db.transaction(async (tx) => + invalidateRepositorySources(tx, options.graphId, options.retiredFileIds, options.markDeleted) + ); +} + +async function invalidateRepositorySources( + tx: Parameters[0]>[0], + graphId: string, + retiredFileIds: string[], + markDeleted: boolean +): Promise { + if (retiredFileIds.length === 0) { + return { entityIds: [], relationshipIds: [] }; + } + + if (markDeleted) { + await tx + .update(filesTable) + .set({ deleted: true }) + .where(and(eq(filesTable.graphId, graphId), eq(filesTable.deleted, false), inArray(filesTable.id, retiredFileIds))); + } + + const affectedEntityRows = await tx + .selectDistinct({ id: sourcesTable.entityId }) + .from(sourcesTable) + .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .where(and(inArray(textUnitTable.fileId, retiredFileIds), currentSourcePredicate(), isNotNull(sourcesTable.entityId))); + const affectedRelationshipRows = await tx + .selectDistinct({ id: sourcesTable.relationshipId }) + .from(sourcesTable) + .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .where( + and( + inArray(textUnitTable.fileId, retiredFileIds), + currentSourcePredicate(), + isNotNull(sourcesTable.relationshipId) + ) + ); + + await tx.execute(sql` + UPDATE sources source + SET valid_until = NOW() + FROM text_units text_unit + WHERE source.text_unit_id = text_unit.id + AND text_unit.file_id = ANY(${textArray(retiredFileIds)}) + AND ${currentSourceSql("source")} + `); + return { + entityIds: affectedEntityRows.map((row) => row.id).filter((id): id is string => id !== null), + relationshipIds: affectedRelationshipRows.map((row) => row.id).filter((id): id is string => id !== null), + }; +} diff --git a/apps/worker/lib/file-content-source.ts b/apps/worker/lib/file-content-source.ts new file mode 100644 index 00000000..ee153e8c --- /dev/null +++ b/apps/worker/lib/file-content-source.ts @@ -0,0 +1,204 @@ +import { + createGitHubClient, + createGitHubInstallationToken, + createGitLabClient, + type GitHubConnectorCredentials, + type GitLabConnectorCredentials, + type GitLabInstallationCredentials, + type ProviderRepository, +} from "@kiwi/connectors"; +import { decryptConnectorCredentials, type ConnectorSecretPayload } from "@kiwi/connectors/credentials"; +import { db } from "@kiwi/db"; +import { connectorInstallationsTable, connectorsTable, repositoryGraphBindingsTable } from "@kiwi/db/tables/connectors"; +import { getFile } from "@kiwi/files"; +import { eq } from "drizzle-orm"; +import { env } from "../env"; +import { parseCodeFileMetadata } from "./code-file-metadata"; + +export type FileContentSource = + | { kind: "internal"; key: string } + | { kind: "external"; provider: "github"; url: string; metadata?: string | null } + | { kind: "connector"; bindingId: string; provider: "github" | "gitlab"; metadata?: string | null }; + +const MAX_CODE_FILE_BYTES = 2 * 1024 * 1024; + +export function fileContentSourceFromRow(row: { + key: string; + storageKind?: string | null; + externalProvider?: string | null; + externalUrl?: string | null; + repositoryBindingId?: string | null; + metadata?: string | null; +}): FileContentSource { + if (row.storageKind === "external") { + if (row.repositoryBindingId) { + if (row.externalProvider !== "github" && row.externalProvider !== "gitlab") { + throw new Error("Unsupported connector file source"); + } + return { + kind: "connector", + provider: row.externalProvider, + bindingId: row.repositoryBindingId, + metadata: row.metadata, + }; + } + + if (row.externalProvider !== "github" || !row.externalUrl) { + throw new Error("Unsupported external file source"); + } + + return { kind: "external", provider: "github", url: row.externalUrl, metadata: row.metadata }; + } + + return { kind: "internal", key: row.key }; +} + +export async function readFileContentSource(source: FileContentSource): Promise { + if (source.kind === "internal") { + const file = await getFile(source.key, env.S3_BUCKET, "text"); + return file?.content ?? null; + } + + if (source.kind === "connector") { + return readConnectorFile(source.bindingId, source.metadata); + } + + return readExternalGitHubFile(source.url); +} + +function isGitHubConnectorCredentials(value: ConnectorSecretPayload): value is GitHubConnectorCredentials { + return "provider" in value && value.provider === "github"; +} + +function isGitLabConnectorCredentials(value: ConnectorSecretPayload): value is GitLabConnectorCredentials { + return "provider" in value && value.provider === "gitlab" && "baseUrl" in value; +} + +function isGitLabInstallationCredentials(value: ConnectorSecretPayload): value is GitLabInstallationCredentials { + return "provider" in value && value.provider === "gitlab" && "accessToken" in value; +} + +async function readConnectorFile(bindingId: string, metadataValue?: string | null): Promise { + const metadata = parseCodeFileMetadata(metadataValue); + if (!metadata) { + return null; + } + + const [row] = await db + .select({ + binding: repositoryGraphBindingsTable, + installation: connectorInstallationsTable, + connector: connectorsTable, + }) + .from(repositoryGraphBindingsTable) + .innerJoin( + connectorInstallationsTable, + eq(connectorInstallationsTable.id, repositoryGraphBindingsTable.connectorInstallationId) + ) + .innerJoin(connectorsTable, eq(connectorsTable.id, connectorInstallationsTable.connectorId)) + .where(eq(repositoryGraphBindingsTable.id, bindingId)) + .limit(1); + + if (!row || row.connector.status !== "active" || row.installation.status !== "active") { + return null; + } + + const connectorCredentials = decryptConnectorCredentials(row.connector.encryptedCredentials, env.AUTH_SECRET); + if (row.connector.provider === "github") { + if (!isGitHubConnectorCredentials(connectorCredentials)) { + return null; + } + const installationToken = await createGitHubInstallationToken({ + credentials: connectorCredentials, + installationId: row.installation.providerInstallationId, + }); + const client = createGitHubClient({ installationToken: installationToken.token }); + const repository: ProviderRepository = { + provider: "github", + id: row.binding.providerRepositoryId, + fullName: row.binding.repositoryFullName, + name: row.binding.repositoryFullName.split("/").at(-1) ?? row.binding.repositoryFullName, + htmlUrl: row.binding.repositoryHtmlUrl, + defaultBranch: row.binding.branch, + private: true, + }; + return client.readFile(repository, metadata.path, metadata.commitSha); + } + + if (!isGitLabConnectorCredentials(connectorCredentials)) { + return null; + } + const installationCredentials = row.installation.encryptedCredentials + ? decryptConnectorCredentials(row.installation.encryptedCredentials, env.AUTH_SECRET) + : null; + if (!installationCredentials || !isGitLabInstallationCredentials(installationCredentials)) { + return null; + } + const client = createGitLabClient({ + baseUrl: connectorCredentials.baseUrl, + accessToken: installationCredentials.accessToken, + }); + const repository: ProviderRepository = { + provider: "gitlab", + id: row.binding.providerRepositoryId, + fullName: row.binding.repositoryFullName, + name: row.binding.repositoryFullName.split("/").at(-1) ?? row.binding.repositoryFullName, + htmlUrl: row.binding.repositoryHtmlUrl, + defaultBranch: row.binding.branch, + private: true, + }; + return client.readFile(repository, metadata.path, metadata.commitSha); +} + +async function readExternalGitHubFile(url: string): Promise { + const parsed = new URL(url); + if (parsed.protocol !== "https:" || parsed.hostname !== "raw.githubusercontent.com") { + throw new Error("Unsupported external file source"); + } + + const response = await fetch(parsed, { redirect: "manual" }); + const responseUrl = new URL(response.url || parsed.href); + if (responseUrl.protocol !== "https:" || responseUrl.hostname !== "raw.githubusercontent.com") { + throw new Error("Unsupported external file source"); + } + + if (!response.ok) { + throw new Error("External file content not found"); + } + + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.toLowerCase().startsWith("text/")) { + throw new Error("External file content is not text"); + } + + const contentLength = response.headers.get("content-length"); + if (contentLength && Number(contentLength) > MAX_CODE_FILE_BYTES) { + throw new Error("External file content is too large"); + } + + if (!response.body) { + throw new Error("External file content is empty"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let totalBytes = 0; + let content = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + totalBytes += value.byteLength; + if (totalBytes > MAX_CODE_FILE_BYTES) { + throw new Error("External file content is too large"); + } + + content += decoder.decode(value, { stream: true }); + } + + content += decoder.decode(); + return content; +} diff --git a/apps/worker/lib/file-processing-state.ts b/apps/worker/lib/file-processing-state.ts new file mode 100644 index 00000000..e8ee167b --- /dev/null +++ b/apps/worker/lib/file-processing-state.ts @@ -0,0 +1,39 @@ +import { db } from "@kiwi/db"; +import { filesTable, type FileProcessStatus, type FileProcessStep } from "@kiwi/db/tables/graph"; +import type { FileProcessErrorCode } from "@kiwi/contracts/routes"; +import { eq } from "drizzle-orm"; + +export async function updateFileProcessingState( + fileId: string, + processStep: FileProcessStep, + status: FileProcessStatus, + processErrorCode?: FileProcessErrorCode | null +) { + await db + .update(filesTable) + .set({ + processStep, + status, + ...(processErrorCode !== undefined + ? { processErrorCode } + : status === "failed" + ? {} + : { processErrorCode: null }), + }) + .where(eq(filesTable.id, fileId)); +} + +export async function stopIfFileDeleted(fileId: string) { + const [file] = await db + .select({ deleted: filesTable.deleted }) + .from(filesTable) + .where(eq(filesTable.id, fileId)) + .limit(1); + + if (file?.deleted) { + await updateFileProcessingState(fileId, "completed", "processed"); + return true; + } + + return false; +} diff --git a/apps/worker/lib/regenerate-descriptions.ts b/apps/worker/lib/regenerate-descriptions.ts index f51f6115..3c610220 100644 --- a/apps/worker/lib/regenerate-descriptions.ts +++ b/apps/worker/lib/regenerate-descriptions.ts @@ -1,5 +1,6 @@ import { db } from "@kiwi/db"; -import { entityTable, relationshipTable, sourcesTable } from "@kiwi/db/tables/graph"; +import { entityTable, filesTable, relationshipTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { unexpiredSourcePredicate, visibleFilePredicate } from "@kiwi/db/source-validity"; import { and, eq, inArray, isNotNull, sql } from "drizzle-orm"; import { embed, embedMany } from "ai"; import { withAiSlot } from "@kiwi/ai"; @@ -50,6 +51,7 @@ export async function updateSourceEmbeddingsBatch( )} ) AS batch(id, embedding) WHERE source.id = batch.id + AND source.valid_until IS NULL `); } @@ -85,12 +87,14 @@ async function regenerateDescriptions( description: string; embedding: number[]; sourceEmbeddings: SourceEmbeddingUpdate[]; - }) => Promise + }) => Promise, + deactivate: (id: string) => Promise ) { for (const chunk of chunkItems(items, DESCRIPTION_BATCH_SIZE)) { await Promise.all( chunk.map(async (item) => { if (item.sources.length === 0) { + await deactivate(item.id); return; } @@ -152,13 +156,17 @@ export async function regenerateEntities(graphId: string, entityIds: string[], c description: sourcesTable.description, }) .from(sourcesTable) + .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) .where( and( inArray( sourcesTable.entityId, entities.map((entity) => entity.id) ), - isNotNull(sourcesTable.entityId) + isNotNull(sourcesTable.entityId), + unexpiredSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) ) ); @@ -185,6 +193,9 @@ export async function regenerateEntities(graphId: string, entityIds: string[], c await updateSourceEmbeddingsBatch(tx, sourceEmbeddings); }); + }, + async (id) => { + await db.update(entityTable).set({ active: false }).where(eq(entityTable.id, id)); } ); } @@ -216,13 +227,17 @@ export async function regenerateRelationships(graphId: string, relationshipIds: description: sourcesTable.description, }) .from(sourcesTable) + .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) .where( and( inArray( sourcesTable.relationshipId, relationships.map((relationship) => relationship.id) ), - isNotNull(sourcesTable.relationshipId) + isNotNull(sourcesTable.relationshipId), + unexpiredSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) ) ); @@ -261,6 +276,9 @@ export async function regenerateRelationships(graphId: string, relationshipIds: await updateSourceEmbeddingsBatch(tx, sourceEmbeddings); }); + }, + async (id) => { + await db.update(relationshipTable).set({ active: false }).where(eq(relationshipTable.id, id)); } ); } diff --git a/apps/worker/lib/save-graph.ts b/apps/worker/lib/save-graph.ts new file mode 100644 index 00000000..5b929c2c --- /dev/null +++ b/apps/worker/lib/save-graph.ts @@ -0,0 +1,535 @@ +import { db } from "@kiwi/db"; +import { entityTable, filesTable, relationshipTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourceSql, unexpiredSourcePredicate, visibleFilePredicate, visibleFileSql } from "@kiwi/db/source-validity"; +import type { Graph } from "@kiwi/graph"; +import { eq, sql, and } from "drizzle-orm"; +import { chunkItems } from "./chunk"; +import { EMPTY_VECTOR_SQL, entityCompactNameKey, textArray } from "./sql"; +import { toTextUnitRows } from "./text-unit-rows"; + +const DEFAULT_RELATIONSHIP_KIND = "RELATED"; + +type GraphSaveTransaction = Parameters[0]>[0]; + +export type GraphSaveResult = { + summary: { + units: number; + entities: number; + relationships: number; + }; + duration: number; + metrics: { + insertUnitsDuration: number; + insertEntitiesDuration: number; + insertRelationshipsDuration: number; + dedupeEntitiesDuration: number; + dedupeRelationshipsDuration: number; + invalidateStaleSourcesDuration: number; + }; +}; + +export async function collectPendingDescriptionTargets(graphId: string) { + const newEntities = await db + .select({ id: entityTable.id, name: entityTable.name }) + .from(entityTable) + .where(and(eq(entityTable.graphId, graphId), eq(entityTable.active, false))); + + const updatedEntityRows = await db + .selectDistinct({ + id: entityTable.id, + name: entityTable.name, + description: entityTable.description, + }) + .from(entityTable) + .innerJoin(sourcesTable, eq(sourcesTable.entityId, entityTable.id)) + .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) + .where( + and( + eq(entityTable.graphId, graphId), + eq(entityTable.active, true), + eq(sourcesTable.active, false), + unexpiredSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) + ) + ); + const updatedEntities = Array.from(new Map(updatedEntityRows.map((entity) => [entity.id, entity])).values()); + + const newRelationships = await db + .select({ id: relationshipTable.id, sourceId: relationshipTable.sourceId, targetId: relationshipTable.targetId }) + .from(relationshipTable) + .where(and(eq(relationshipTable.graphId, graphId), eq(relationshipTable.active, false))); + + const updatedRelationshipRows = await db + .selectDistinct({ + id: relationshipTable.id, + sourceId: relationshipTable.sourceId, + targetId: relationshipTable.targetId, + description: relationshipTable.description, + }) + .from(relationshipTable) + .innerJoin(sourcesTable, eq(sourcesTable.relationshipId, relationshipTable.id)) + .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) + .where( + and( + eq(relationshipTable.graphId, graphId), + eq(relationshipTable.active, true), + eq(sourcesTable.active, false), + unexpiredSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) + ) + ); + const updatedRelationships = Array.from( + new Map(updatedRelationshipRows.map((relationship) => [relationship.id, relationship])).values() + ); + + return { + entityIds: [...newEntities.map((entity) => entity.id), ...updatedEntities.map((entity) => entity.id)], + relationshipIds: [ + ...newRelationships.map((relationship) => relationship.id), + ...updatedRelationships.map((relationship) => relationship.id), + ], + }; +} + +export async function saveGraphToDatabase(graphId: string, graph: Graph): Promise { + const start = performance.now(); + const rows = buildGraphRows(graphId, graph); + + const metrics = await db.transaction(async (tx) => { + const insertMetrics = await insertGraphRows(tx, rows); + const dedupeEntitiesDuration = await measureDuration(() => + dedupeEntityRows(tx, graphId, rows.insertedEntityIds) + ); + const dedupeRelationshipsDuration = await measureDuration(() => + dedupeRelationshipRows(tx, graphId, rows.insertedRelationshipIds) + ); + const invalidateStaleSourcesDuration = await measureDuration(() => + invalidateStaleCurrentCodeSources(tx, graphId, rows.insertedSourceIds) + ); + + return { + ...insertMetrics, + dedupeEntitiesDuration, + dedupeRelationshipsDuration, + invalidateStaleSourcesDuration, + }; + }); + + return { + summary: { + units: graph.units.length, + entities: graph.entities.length, + relationships: graph.relationships.length, + }, + duration: performance.now() - start, + metrics, + }; +} + +function buildGraphRows(graphId: string, graph: Graph) { + const unitRows = toTextUnitRows(graph.units); + const entityRows = graph.entities.map((entity) => ({ + id: entity.id, + graphId, + active: false, + name: entity.name, + description: "", + type: entity.type, + embedding: EMPTY_VECTOR_SQL, + })); + const sourceRows = [ + ...graph.entities.flatMap((entity) => + entity.sources.map((source) => ({ + id: source.id, + entityId: entity.id, + relationshipId: null, + textUnitId: source.unitId, + active: false, + description: source.description, + sourceChunkIds: source.sourceChunkIds ?? [], + embedding: EMPTY_VECTOR_SQL, + })) + ), + ...graph.relationships.flatMap((relationship) => + relationship.sources.map((source) => ({ + id: source.id, + entityId: null, + relationshipId: relationship.id, + textUnitId: source.unitId, + active: false, + description: source.description, + sourceChunkIds: source.sourceChunkIds ?? [], + embedding: EMPTY_VECTOR_SQL, + })) + ), + ]; + const relationshipRows = graph.relationships.map((relationship) => ({ + id: relationship.id, + active: false, + sourceId: relationship.sourceId, + targetId: relationship.targetId, + graphId, + kind: relationship.kind ?? DEFAULT_RELATIONSHIP_KIND, + directed: relationship.directed === true, + rank: relationship.strength, + description: "", + embedding: EMPTY_VECTOR_SQL, + })); + + const insertedEntityIds = entityRows.map((entity) => entity.id); + const insertedRelationshipIds = relationshipRows.map((relationship) => relationship.id); + const insertedSourceIds = sourceRows.map((source) => source.id); + + return { unitRows, entityRows, relationshipRows, sourceRows, insertedEntityIds, insertedRelationshipIds, insertedSourceIds }; +} + +async function measureDuration(work: () => Promise): Promise { + const start = performance.now(); + await work(); + return performance.now() - start; +} + +async function insertGraphRows(tx: GraphSaveTransaction, rows: ReturnType) { + const insertUnitsDuration = await measureDuration(async () => { + for (const chunk of chunkItems(rows.unitRows)) { + await tx + .insert(textUnitTable) + .values(chunk) + .onConflictDoUpdate({ + target: textUnitTable.id, + set: { + fileId: sql`excluded.file_id`, + text: sql`excluded.text`, + startPage: sql`excluded.start_page`, + endPage: sql`excluded.end_page`, + chunks: sql`excluded.chunks`, + updatedAt: sql`NOW()`, + }, + }); + } + }); + + const insertEntitiesDuration = await measureDuration(async () => { + for (const chunk of chunkItems(rows.entityRows)) { + await tx.insert(entityTable).values(chunk).onConflictDoNothing(); + } + }); + + const insertRelationshipsDuration = await measureDuration(async () => { + for (const chunk of chunkItems(rows.relationshipRows)) { + await tx.insert(relationshipTable).values(chunk).onConflictDoNothing(); + } + for (const chunk of chunkItems(rows.sourceRows)) { + await tx.insert(sourcesTable).values(chunk).onConflictDoNothing(); + } + }); + + return { insertUnitsDuration, insertEntitiesDuration, insertRelationshipsDuration }; +} + +async function dedupeEntityRows(tx: GraphSaveTransaction, graphId: string, insertedEntityIds: string[]) { + if (insertedEntityIds.length === 0) { + return; + } + + const entityIds = textArray(insertedEntityIds); + const candidateNameKeySql = sql.raw(entityCompactNameKey("candidate.name")); + const seededNameKeySql = sql.raw(entityCompactNameKey("seed.name")); + + await tx.execute(sql` + WITH seeded_keys AS ( + SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name + FROM entities seed + WHERE seed.graph_id = ${graphId} + AND seed.id = ANY(${entityIds}) + ), duplicates AS ( + SELECT + candidate.id, + first_value(candidate.id) OVER ( + PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} + ORDER BY candidate.active DESC, candidate.id ASC + ) AS canonical_id + FROM entities candidate + JOIN seeded_keys seeded + ON seeded.type = candidate.type + AND seeded.normalized_name = ${candidateNameKeySql} + WHERE candidate.graph_id = ${graphId} + ) + UPDATE sources source + SET entity_id = duplicates.canonical_id + FROM duplicates + WHERE source.entity_id = duplicates.id + AND duplicates.id <> duplicates.canonical_id + `); + + await tx.execute(sql` + WITH seeded_keys AS ( + SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name + FROM entities seed + WHERE seed.graph_id = ${graphId} + AND seed.id = ANY(${entityIds}) + ), duplicates AS ( + SELECT + candidate.id, + first_value(candidate.id) OVER ( + PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} + ORDER BY candidate.active DESC, candidate.id ASC + ) AS canonical_id + FROM entities candidate + JOIN seeded_keys seeded + ON seeded.type = candidate.type + AND seeded.normalized_name = ${candidateNameKeySql} + WHERE candidate.graph_id = ${graphId} + ) + UPDATE relationships relationship + SET source_id = duplicates.canonical_id + FROM duplicates + WHERE relationship.source_id = duplicates.id + AND relationship.graph_id = ${graphId} + AND duplicates.id <> duplicates.canonical_id + `); + + await tx.execute(sql` + WITH seeded_keys AS ( + SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name + FROM entities seed + WHERE seed.graph_id = ${graphId} + AND seed.id = ANY(${entityIds}) + ), duplicates AS ( + SELECT + candidate.id, + first_value(candidate.id) OVER ( + PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} + ORDER BY candidate.active DESC, candidate.id ASC + ) AS canonical_id + FROM entities candidate + JOIN seeded_keys seeded + ON seeded.type = candidate.type + AND seeded.normalized_name = ${candidateNameKeySql} + WHERE candidate.graph_id = ${graphId} + ) + UPDATE relationships relationship + SET target_id = duplicates.canonical_id + FROM duplicates + WHERE relationship.target_id = duplicates.id + AND relationship.graph_id = ${graphId} + AND duplicates.id <> duplicates.canonical_id + `); + + await tx.execute(sql` + WITH seeded_keys AS ( + SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name + FROM entities seed + WHERE seed.graph_id = ${graphId} + AND seed.id = ANY(${entityIds}) + ), duplicates AS ( + SELECT + candidate.id, + first_value(candidate.id) OVER ( + PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} + ORDER BY candidate.active DESC, candidate.id ASC + ) AS canonical_id + FROM entities candidate + JOIN seeded_keys seeded + ON seeded.type = candidate.type + AND seeded.normalized_name = ${candidateNameKeySql} + WHERE candidate.graph_id = ${graphId} + ) + DELETE FROM entities entity + USING duplicates + WHERE entity.id = duplicates.id + AND duplicates.id <> duplicates.canonical_id + `); +} + +async function dedupeRelationshipRows(tx: GraphSaveTransaction, graphId: string, insertedRelationshipIds: string[]) { + await tx.execute(sql` + DELETE FROM relationships + WHERE graph_id = ${graphId} + AND directed = false + AND source_id = target_id + `); + + if (insertedRelationshipIds.length === 0) { + return; + } + + const relationshipIds = textArray(insertedRelationshipIds); + + await tx.execute(sql` + WITH seeded_pairs AS ( + SELECT DISTINCT + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END AS pair_source_id, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END AS pair_target_id + FROM relationships relationship + WHERE relationship.graph_id = ${graphId} + AND relationship.id = ANY(${relationshipIds}) + ), duplicates AS ( + SELECT + relationship.id, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END AS canonical_source_id, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END AS canonical_target_id, + first_value(relationship.id) OVER ( + PARTITION BY relationship.graph_id, + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + ORDER BY relationship.active DESC, relationship.id ASC + ) AS canonical_id, + max(relationship.rank) OVER ( + PARTITION BY relationship.graph_id, + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + ) AS canonical_rank + FROM relationships relationship + JOIN seeded_pairs seeded + ON seeded.kind = relationship.kind + AND seeded.directed = relationship.directed + AND seeded.pair_source_id = CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END + AND seeded.pair_target_id = CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + WHERE relationship.graph_id = ${graphId} + ) + UPDATE relationships relationship + SET source_id = duplicates.canonical_source_id, + target_id = duplicates.canonical_target_id, + rank = CASE + WHEN relationship.id = duplicates.canonical_id THEN duplicates.canonical_rank + ELSE relationship.rank + END, + updated_at = NOW() + FROM duplicates + WHERE relationship.id = duplicates.id + AND ( + relationship.source_id <> duplicates.canonical_source_id + OR relationship.target_id <> duplicates.canonical_target_id + OR ( + relationship.id = duplicates.canonical_id + AND relationship.rank <> duplicates.canonical_rank + ) + ) + `); + + await tx.execute(sql` + WITH seeded_pairs AS ( + SELECT DISTINCT + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END AS pair_source_id, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END AS pair_target_id + FROM relationships relationship + WHERE relationship.graph_id = ${graphId} + AND relationship.id = ANY(${relationshipIds}) + ), duplicates AS ( + SELECT + relationship.id, + first_value(relationship.id) OVER ( + PARTITION BY relationship.graph_id, + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + ORDER BY relationship.active DESC, relationship.id ASC + ) AS canonical_id + FROM relationships relationship + JOIN seeded_pairs seeded + ON seeded.kind = relationship.kind + AND seeded.directed = relationship.directed + AND seeded.pair_source_id = CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END + AND seeded.pair_target_id = CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + WHERE relationship.graph_id = ${graphId} + ) + UPDATE sources source + SET relationship_id = duplicates.canonical_id + FROM duplicates + WHERE source.relationship_id = duplicates.id + AND duplicates.id <> duplicates.canonical_id + `); + + await tx.execute(sql` + WITH seeded_pairs AS ( + SELECT DISTINCT + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END AS pair_source_id, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END AS pair_target_id + FROM relationships relationship + WHERE relationship.graph_id = ${graphId} + AND relationship.id = ANY(${relationshipIds}) + ), duplicates AS ( + SELECT + relationship.id, + first_value(relationship.id) OVER ( + PARTITION BY relationship.graph_id, + relationship.kind, + relationship.directed, + CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END, + CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + ORDER BY relationship.active DESC, relationship.id ASC + ) AS canonical_id + FROM relationships relationship + JOIN seeded_pairs seeded + ON seeded.kind = relationship.kind + AND seeded.directed = relationship.directed + AND seeded.pair_source_id = CASE WHEN relationship.directed THEN relationship.source_id ELSE least(relationship.source_id, relationship.target_id) END + AND seeded.pair_target_id = CASE WHEN relationship.directed THEN relationship.target_id ELSE greatest(relationship.source_id, relationship.target_id) END + WHERE relationship.graph_id = ${graphId} + ) + DELETE FROM relationships relationship + USING duplicates + WHERE relationship.id = duplicates.id + AND duplicates.id <> duplicates.canonical_id + `); +} + +async function invalidateStaleCurrentCodeSources(tx: GraphSaveTransaction, graphId: string, insertedSourceIds: string[]) { + if (insertedSourceIds.length === 0) { + return; + } + + const sourceIds = textArray(insertedSourceIds); + + await tx.execute(sql` + WITH new_code_sources AS ( + SELECT DISTINCT source.id, source.entity_id, source.relationship_id + FROM sources source + INNER JOIN text_units text_unit ON text_unit.id = source.text_unit_id + INNER JOIN files file ON file.id = text_unit.file_id + WHERE source.id = ANY(${sourceIds}) + AND source.valid_until IS NULL + AND file.graph_id = ${graphId} + AND ${visibleFileSql("file")} + AND file.file_type = 'code' + ), stale_sources AS ( + SELECT old_source.id + FROM sources old_source + INNER JOIN text_units old_text_unit ON old_text_unit.id = old_source.text_unit_id + INNER JOIN files old_file ON old_file.id = old_text_unit.file_id + INNER JOIN new_code_sources new_source + ON ( + new_source.entity_id IS NOT NULL + AND old_source.entity_id = new_source.entity_id + ) + OR ( + new_source.relationship_id IS NOT NULL + AND old_source.relationship_id = new_source.relationship_id + ) + WHERE ${currentSourceSql("old_source")} + AND ${visibleFileSql("old_file")} + AND old_file.graph_id = ${graphId} + AND old_file.file_type = 'code' + AND old_source.id <> ALL(${sourceIds}) + ) + UPDATE sources source + SET valid_until = NOW(), + updated_at = NOW() + FROM stale_sources + WHERE source.id = stale_sources.id + `); +} diff --git a/apps/worker/package.json b/apps/worker/package.json index fdfb290e..a49dfc0e 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -15,6 +15,7 @@ "dependencies": { "@kiwi/ai": "workspace:*", "@kiwi/contracts": "workspace:*", + "@kiwi/connectors": "workspace:*", "@kiwi/db": "workspace:*", "@kiwi/files": "workspace:*", "@kiwi/graph": "workspace:*", diff --git a/apps/worker/worker.ts b/apps/worker/worker.ts index 43be035e..3fc4ab5c 100644 --- a/apps/worker/worker.ts +++ b/apps/worker/worker.ts @@ -108,7 +108,9 @@ async function startWorkerProcess() { { deleteProjectFile }, { deleteGraphFiles }, { processFile, processFiles }, + { processCodeFile }, { updateDescriptions }, + { syncRepositoryGraph }, { processDescriptionsGroups }, ] = await Promise.all([ import("."), @@ -116,7 +118,9 @@ async function startWorkerProcess() { import("./workflows/delete-file"), import("./workflows/delete-graph-files"), import("./workflows/process-file"), + import("./workflows/process-code-file"), import("./workflows/update-descriptions"), + import("./workflows/sync-repository-graph"), import("./workflows/process-descriptions-group"), ]); @@ -125,9 +129,11 @@ async function startWorkerProcess() { ow.implementWorkflow(processFiles.spec, processFiles.fn); ow.implementWorkflow(processFile.spec, processFile.fn); + ow.implementWorkflow(processCodeFile.spec, processCodeFile.fn); ow.implementWorkflow(deleteProjectFile.spec, deleteProjectFile.fn); ow.implementWorkflow(deleteGraphFiles.spec, deleteGraphFiles.fn); ow.implementWorkflow(updateDescriptions.spec, updateDescriptions.fn); + ow.implementWorkflow(syncRepositoryGraph.spec, syncRepositoryGraph.fn); ow.implementWorkflow(processDescriptionsGroups.spec, processDescriptionsGroups.fn); const worker = ow.newWorker({ concurrency }); diff --git a/apps/worker/workflows/process-code-file.ts b/apps/worker/workflows/process-code-file.ts new file mode 100644 index 00000000..2f264999 --- /dev/null +++ b/apps/worker/workflows/process-code-file.ts @@ -0,0 +1,232 @@ +import { db } from "@kiwi/db"; +import { filesTable, processStatsTable } from "@kiwi/db/tables/graph"; +import { and, eq } from "drizzle-orm"; +import { defineWorkflow } from "openworkflow"; +import z from "zod"; +import { estimateToken } from "@kiwi/ai"; +import type { Graph } from "@kiwi/graph"; +import type { CodeRepositoryFile } from "@kiwi/graph/code/repository"; +import { getFile, putNamedFile } from "@kiwi/files"; +import { error as logError } from "@kiwi/logger"; +import { env } from "../env"; +import { deleteGraphFileProcessingArtifacts, getGraphFileArtifactPaths } from "../lib/derived-files"; +import { classifyFileProcessError } from "../lib/file-process-error"; +import { loadCodeManifest } from "../lib/code-manifest"; +import { fileContentSourceFromRow, readFileContentSource } from "../lib/file-content-source"; +import { parseCodeFileMetadata } from "../lib/code-file-metadata"; +import { updateFileProcessingState, stopIfFileDeleted } from "../lib/file-processing-state"; +import { saveGraphToDatabase } from "../lib/save-graph"; + +const FILE_DELETED = "__file_deleted__" as const; + +type ProcessFileRow = typeof filesTable.$inferSelect; + +function workflowError(error: unknown) { + if (error instanceof Error) { + return new Error(error.message, { cause: error }); + } + + return new Error("Workflow failed", { cause: error }); +} + +function codeRepositoryFileFromRow(file: ProcessFileRow, content: string): CodeRepositoryFile { + const metadata = parseCodeFileMetadata(file.metadata); + + return { + fileId: file.id, + repositoryUrl: metadata?.repositoryUrl ?? `graph:${file.graphId}`, + repositoryName: metadata?.repositoryName ?? "code", + commitSha: metadata?.commitSha ?? "unknown", + path: metadata?.path ?? file.name, + content, + }; +} + +export const processCodeFile = defineWorkflow( + { + name: "process-code-file", + version: "1.0.0", + retryPolicy: { + initialInterval: "1s", + backoffCoefficient: 2, + maximumInterval: "30s", + maximumAttempts: 3, + }, + schema: z.object({ + graphId: z.string(), + fileId: z.string(), + codeManifestKey: z.string().optional(), + }), + }, + async ({ input, step, run }) => { + try { + const [fileData] = await step.run({ name: "get-code-file-data" }, async () => { + return db + .select() + .from(filesTable) + .where(and(eq(filesTable.graphId, input.graphId), eq(filesTable.id, input.fileId))) + .limit(1); + }); + + if (!fileData) { + return; + } + + if (fileData.deleted) { + await updateFileProcessingState(input.fileId, "completed", "processed"); + return; + } + + const paths = getGraphFileArtifactPaths({ + graphId: input.graphId, + fileId: input.fileId, + fileKey: fileData.key, + }); + + const baseFile = await step.run({ name: "preprocess-code-file" }, async () => { + if (await stopIfFileDeleted(input.fileId)) { + return FILE_DELETED; + } + + await updateFileProcessingState(input.fileId, "preprocessing", "processing"); + const start = performance.now(); + const source = await readFileContentSource(fileContentSourceFromRow(fileData)); + if (source === null) { + throw new Error(`Failed to load file ${fileData.key}`); + } + + if (source.trim() === "") { + throw new Error("No readable text found in file"); + } + + const repositoryFile = codeRepositoryFileFromRow(fileData, source); + const tokenCount = estimateToken(source); + + await db + .update(filesTable) + .set({ tokenCount, loader: "repository", chunker: "ast" }) + .where(eq(filesTable.id, input.fileId)); + + return { + repositoryFile, + duration: performance.now() - start, + tokenCount, + }; + }); + if (baseFile === FILE_DELETED) { + return; + } + + const graphResult = await step.run({ name: "build-code-graph" }, async () => { + if (await stopIfFileDeleted(input.fileId)) { + return FILE_DELETED; + } + + await updateFileProcessingState(input.fileId, "extracting", "processing"); + const start = performance.now(); + const { buildCodeFileGraph, buildCodeRepositoryManifest } = await import("@kiwi/graph/code/repository"); + const manifest = input.codeManifestKey + ? await loadCodeManifest(input.codeManifestKey) + : buildCodeRepositoryManifest([baseFile.repositoryFile]); + const graph = buildCodeFileGraph(baseFile.repositoryFile, manifest); + const uploadedGraph = await putNamedFile( + "graph.json", + JSON.stringify(graph), + paths.processingPrefix, + env.S3_BUCKET + ); + + return { + graphKey: uploadedGraph.key, + duration: performance.now() - start, + }; + }); + if (graphResult === FILE_DELETED) { + return; + } + + const saveGraphResult = await step.run({ name: "save-code-graph" }, async () => { + if (await stopIfFileDeleted(input.fileId)) { + return FILE_DELETED; + } + + await updateFileProcessingState(input.fileId, "saving", "processing"); + const loadedGraph = await getFile(graphResult.graphKey, env.S3_BUCKET, "json"); + if (!loadedGraph) { + throw new Error(`Failed to load graph from ${graphResult.graphKey}`); + } + + const saveResult = await saveGraphToDatabase(input.graphId, loadedGraph.content); + + return { + summary: { + fileId: input.fileId, + ...saveResult.summary, + }, + duration: saveResult.duration, + metrics: saveResult.metrics, + }; + }); + if (saveGraphResult === FILE_DELETED) { + return; + } + + const statsResult = await step.run({ name: "store-code-process-stats" }, async () => { + if (await stopIfFileDeleted(input.fileId)) { + return FILE_DELETED; + } + + try { + await db.insert(processStatsTable).values({ + totalTime: baseFile.duration + graphResult.duration + saveGraphResult.duration, + files: 1, + fileSizes: fileData.size, + fileType: "code", + tokenCount: baseFile.tokenCount, + }); + } catch (error) { + logError("failed to store code file process stats", { + graphId: input.graphId, + fileId: input.fileId, + error, + }); + } + }); + if (statsResult === FILE_DELETED) { + return; + } + + await step.run({ name: "mark-code-file-complete" }, async () => { + await updateFileProcessingState(input.fileId, "completed", "processed"); + }); + + await step.run({ name: "cleanup-code-processing-artifacts" }, async () => { + try { + return await deleteGraphFileProcessingArtifacts({ + graphId: input.graphId, + fileId: input.fileId, + fileKey: fileData.key, + bucket: env.S3_BUCKET, + }); + } catch (error) { + logError("failed to cleanup code processing artifacts", { + graphId: input.graphId, + fileId: input.fileId, + error, + }); + return { deletedKeyCount: 0 }; + } + }); + + return saveGraphResult.summary; + } catch (error) { + if (run.retryTerminal) { + await updateFileProcessingState(input.fileId, "failed", "failed", classifyFileProcessError(error)); + } else { + await updateFileProcessingState(input.fileId, "pending", "processing", null); + } + + throw workflowError(error); + } + } +); diff --git a/apps/worker/workflows/process-file.test.ts b/apps/worker/workflows/process-file.test.ts new file mode 100644 index 00000000..7659df15 --- /dev/null +++ b/apps/worker/workflows/process-file.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, test } from "bun:test"; + +process.env.AUTH_SECRET = "test-secret"; +process.env.S3_ACCESS_KEY_ID = "test-access-key"; +process.env.S3_SECRET_ACCESS_KEY = "test-secret-key"; +process.env.S3_ENDPOINT = "http://localhost:9000"; +process.env.S3_REGION = "us-east-1"; +process.env.S3_BUCKET = "test-bucket"; +process.env.DATABASE_URL = "postgres://user:pass@localhost:5432/kiwi"; +process.env.DATABASE_DIRECT_URL = "postgres://user:pass@localhost:5432/kiwi"; + +// Dynamic import is required so this test can seed worker env vars before env.ts is evaluated. +const { fileProcessingWorkflow, shouldAbortRepositoryBatch, shouldFinalizeRepositoryBatch } = await import( + "./process-file" +); +const { resolveRepositoryFinalizationTargets } = await import("../lib/code-repository-finalizer"); + +describe("fileProcessingWorkflow", () => { + test("routes mixed batch children by stored file type", () => { + const codeWorkflow = fileProcessingWorkflow("graph-1", "code-file", "code", "manifest-key"); + const textWorkflow = fileProcessingWorkflow("graph-1", "text-file", "text", "manifest-key"); + + expect(codeWorkflow.spec.name).toBe("process-code-file"); + expect(codeWorkflow.input).toEqual({ + graphId: "graph-1", + fileId: "code-file", + codeManifestKey: "manifest-key", + }); + expect(textWorkflow.spec.name).toBe("process-file"); + expect(textWorkflow.input).toEqual({ + graphId: "graph-1", + fileId: "text-file", + }); + }); +}); + +describe("repository batch guards", () => { + test("finalizes repository batches only after every child workflow succeeds", () => { + expect( + shouldFinalizeRepositoryBatch( + { kind: "repository", retiredFileIds: ["old-file"] }, + [{ status: "fulfilled", value: undefined }] + ) + ).toBe(true); + expect(shouldFinalizeRepositoryBatch({ kind: "repository", retiredFileIds: ["old-file"] }, [])).toBe(true); + expect( + shouldFinalizeRepositoryBatch( + { kind: "repository", retiredFileIds: ["old-file"] }, + [ + { status: "fulfilled", value: undefined }, + { status: "rejected", reason: new Error("failed") }, + ] + ) + ).toBe(false); + expect(shouldFinalizeRepositoryBatch(undefined, [{ status: "fulfilled", value: undefined }])).toBe(false); + }); + + test("aborts incremental repository batches when any child workflow fails", () => { + expect( + shouldAbortRepositoryBatch( + { kind: "repository", retiredFileIds: [] }, + [ + { status: "fulfilled", value: undefined }, + { status: "rejected", reason: new Error("failed") }, + ] + ) + ).toBe(true); + expect( + shouldAbortRepositoryBatch( + { kind: "repository" }, + [ + { status: "fulfilled", value: undefined }, + { status: "rejected", reason: new Error("failed") }, + ] + ) + ).toBe(false); + }); +}); + +describe("resolveRepositoryFinalizationTargets", () => { + test("targets older files for the same repository URL and preserves other repositories", () => { + const latestMetadata = JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-2", + path: "src/index.ts", + }); + const oldMetadata = JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: "src/removed.ts", + }); + const otherMetadata = JSON.stringify({ + repositoryUrl: "https://github.com/acme/other.git", + repositoryName: "other", + commitSha: "commit-1", + path: "src/index.ts", + }); + + expect( + resolveRepositoryFinalizationTargets( + [{ id: "new-file", metadata: latestMetadata }], + [ + { id: "new-file", metadata: latestMetadata }, + { id: "old-file", metadata: oldMetadata }, + { id: "other-file", metadata: otherMetadata }, + ] + ) + ).toEqual({ + repositoryUrls: ["https://github.com/acme/widgets.git"], + olderFileIds: ["old-file"], + }); + }); +}); diff --git a/apps/worker/workflows/process-file.ts b/apps/worker/workflows/process-file.ts index e068aa56..c59fe699 100644 --- a/apps/worker/workflows/process-file.ts +++ b/apps/worker/workflows/process-file.ts @@ -1,26 +1,15 @@ import { db } from "@kiwi/db"; -import { - type FileProcessStatus, - type FileProcessStep, - entityTable, - filesTable, - graphTable, - processRunsTable, - processStatsTable, - relationshipTable, - sourcesTable, - textUnitTable, -} from "@kiwi/db/tables/graph"; +import { filesTable, graphTable, processRunsTable, processStatsTable } from "@kiwi/db/tables/graph"; import { and, eq, inArray, sql } from "drizzle-orm"; import { defineWorkflow } from "openworkflow"; import z from "zod"; import { S3Loader } from "@kiwi/graph/loader/s3"; import { createGraphChunker } from "@kiwi/graph/chunker/factory"; -import type { FileProcessErrorCode } from "@kiwi/contracts/routes"; import { env } from "../env"; -import { type Graph, type GraphFile, type LoadedGraphDocument, type Unit } from "@kiwi/graph"; +import type { Graph, GraphFile, LoadedGraphDocument, Unit } from "@kiwi/graph"; import { dedupe } from "@kiwi/graph/dedupe"; import { coerceGraphFileType } from "@kiwi/graph/file-type"; +import type { GraphFileType } from "@kiwi/graph/file-type"; import { loadGraphDocument } from "@kiwi/graph/loader/document"; import { createDetectedGraphLoader, detectGraphLoaderFileFormat } from "@kiwi/graph/loader/factory"; import { mergeGraphs } from "@kiwi/graph/merge"; @@ -30,17 +19,21 @@ import { resolveGraphModelOrganizationId } from "@kiwi/ai/models"; import { getFile, putNamedFile } from "@kiwi/files"; import { error as logError } from "@kiwi/logger"; import { createWorkerClient } from "../lib/ai"; -import { EMPTY_VECTOR_SQL, entityCompactNameKey, textArray } from "../lib/sql"; import { chunkItems } from "../lib/chunk"; import { processFilesSpec } from "./process-files-spec"; import { deleteGraphFileProcessingArtifacts, getGraphFileArtifactPaths } from "../lib/derived-files"; import { buildMetadata, buildMetadataExcerpt } from "../lib/metadata"; import { processDescriptionsGroupsSpec, DESCRIPTION_BATCHES_PER_GROUP } from "./process-descriptions-group-spec"; +import { updateDescriptionsSpec } from "./update-descriptions-spec"; import { DESCRIPTION_BATCH_SIZE } from "../lib/description-workflow"; import { getFileTypeProcessingConfig } from "../lib/file-type-config"; -import { toTextUnitRows } from "../lib/text-unit-rows"; import { requireReadableContentText } from "../lib/readable-text"; import { classifyFileProcessError } from "../lib/file-process-error"; +import { collectPendingDescriptionTargets, saveGraphToDatabase } from "../lib/save-graph"; +import { prepareCodeManifest } from "../lib/code-manifest"; +import { invalidateSupersededRepositorySources } from "../lib/code-repository-finalizer"; +import { updateFileProcessingState, stopIfFileDeleted } from "../lib/file-processing-state"; +import { processCodeFile } from "./process-code-file"; const FILE_DELETED = "__file_deleted__" as const; const NO_RETRY = { maximumAttempts: 1 } as const; @@ -54,39 +47,46 @@ function workflowError(error: unknown) { return new Error("Workflow failed", { cause: error }); } -async function updateFileProcessingState( +export function fileProcessingWorkflow( + graphId: string, fileId: string, - processStep: FileProcessStep, - status: FileProcessStatus, - processErrorCode?: FileProcessErrorCode | null + fileType: GraphFileType | undefined, + codeManifestKey?: string ) { - await db - .update(filesTable) - .set({ - processStep, - status, - ...(processErrorCode !== undefined - ? { processErrorCode } - : status === "failed" - ? {} - : { processErrorCode: null }), - }) - .where(eq(filesTable.id, fileId)); + return fileType === "code" + ? { + spec: processCodeFile.spec, + input: { + graphId, + fileId, + ...(codeManifestKey ? { codeManifestKey } : {}), + }, + } + : { + spec: processFile.spec, + input: { + graphId, + fileId, + }, + }; } -async function stopIfFileDeleted(fileId: string) { - const [file] = await db - .select({ deleted: filesTable.deleted }) - .from(filesTable) - .where(eq(filesTable.id, fileId)) - .limit(1); - - if (file?.deleted) { - await updateFileProcessingState(fileId, "completed", "processed"); - return true; - } +export function shouldAbortRepositoryBatch( + code: { kind: "repository"; retiredFileIds?: string[] } | undefined, + results: PromiseSettledResult[] +) { + return code?.kind === "repository" && code.retiredFileIds !== undefined && results.some((result) => result.status === "rejected"); +} - return false; +export function shouldFinalizeRepositoryBatch( + code: { kind: "repository"; retiredFileIds?: string[] } | undefined, + results: PromiseSettledResult[] +) { + return ( + code?.kind === "repository" && + results.every((result) => result.status === "fulfilled") && + (results.length > 0 || (code.retiredFileIds?.length ?? 0) > 0) + ); } export const processFiles = defineWorkflow(processFilesSpec, async ({ input, step, run }) => { @@ -118,86 +118,81 @@ export const processFiles = defineWorkflow(processFilesSpec, async ({ input, ste } }); - const fileResults = await Promise.allSettled( - input.fileIds.map((fileId) => - step.runWorkflow(processFile.spec, { - graphId: input.graphId, - fileId, + const fileTypeRows = await step.run({ name: "load-file-types" }, async () => { + if (input.fileIds.length === 0) { + return []; + } + + return db + .select({ + id: filesTable.id, + type: filesTable.type, }) - ) + .from(filesTable) + .where(and(eq(filesTable.graphId, input.graphId), inArray(filesTable.id, input.fileIds))); + }); + const fileTypes = new Map(fileTypeRows.map((file) => [file.id, coerceGraphFileType(file.type)])); + const hasCodeFiles = [...fileTypes.values()].some((type) => type === "code"); + + const codeManifestKey = + input.code && hasCodeFiles + ? await step.run({ name: "prepare-code-manifest" }, async () => + prepareCodeManifest({ + graphId: input.graphId, + fileIds: input.fileIds.filter((fileId) => fileTypes.get(fileId) === "code"), + processRunId: input.processRunId, + }) + ) + : undefined; + + const fileResults = await Promise.allSettled( + input.fileIds.map((fileId) => { + const workflow = fileProcessingWorkflow(input.graphId, fileId, fileTypes.get(fileId), codeManifestKey); + return step.runWorkflow(workflow.spec, workflow.input); + }) ); if (fileResults.length > 0 && fileResults.every((result) => result.status === "rejected")) { throw new Error(`All ${fileResults.length} file processing workflows failed`); } - - const descriptions = await step.run({ name: "generate-descriptions" }, async () => { - const newEntities = await db - .select({ id: entityTable.id, name: entityTable.name }) - .from(entityTable) - .where(and(eq(entityTable.graphId, input.graphId), eq(entityTable.active, false))); - - const updatedEntityRows = await db - .selectDistinct({ - id: entityTable.id, - name: entityTable.name, - description: entityTable.description, - }) - .from(entityTable) - .innerJoin(sourcesTable, eq(sourcesTable.entityId, entityTable.id)) - .where( - and( - eq(entityTable.graphId, input.graphId), - eq(entityTable.active, true), - eq(sourcesTable.active, false) - ) - ); - const updatedEntities = Array.from( - new Map(updatedEntityRows.map((entity) => [entity.id, entity])).values() + if (shouldAbortRepositoryBatch(input.code, fileResults)) { + throw new Error( + `${fileResults.filter((result) => result.status === "rejected").length} repository file processing workflows failed` ); + } - const entityIds = [ - ...newEntities.map((entity) => entity.id), - ...updatedEntities.map((entity) => entity.id), - ]; - - const newRelationships = await db - .select({ - id: relationshipTable.id, - sourceId: relationshipTable.sourceId, - targetId: relationshipTable.targetId, - }) - .from(relationshipTable) - .where(and(eq(relationshipTable.graphId, input.graphId), eq(relationshipTable.active, false))); - - const updatedRelationshipRows = await db - .selectDistinct({ - id: relationshipTable.id, - sourceId: relationshipTable.sourceId, - targetId: relationshipTable.targetId, - description: relationshipTable.description, - }) - .from(relationshipTable) - .innerJoin(sourcesTable, eq(sourcesTable.relationshipId, relationshipTable.id)) - .where( - and( - eq(relationshipTable.graphId, input.graphId), - eq(relationshipTable.active, true), - eq(sourcesTable.active, false) - ) - ); - const updatedRelationships = Array.from( - new Map(updatedRelationshipRows.map((relationship) => [relationship.id, relationship])).values() + if (shouldFinalizeRepositoryBatch(input.code, fileResults)) { + const invalidated = await step.run({ name: "finalize-repository-snapshot" }, async () => + invalidateSupersededRepositorySources( + input.code?.retiredFileIds !== undefined + ? { + graphId: input.graphId, + retiredFileIds: input.code.retiredFileIds, + } + : { + graphId: input.graphId, + latestFileIds: input.fileIds.filter((fileId) => fileTypes.get(fileId) === "code"), + } + ) ); - const relationshipIds = [ - ...newRelationships.map((relationship) => relationship.id), - ...updatedRelationships.map((relationship) => relationship.id), - ]; + await Promise.all([ + ...chunkItems(invalidated.entityIds, DESCRIPTION_BATCH_SIZE).map((entityIds) => + step.runWorkflow(updateDescriptionsSpec, { + graphId: input.graphId, + entityIds, + }) + ), + ...chunkItems(invalidated.relationshipIds, DESCRIPTION_BATCH_SIZE).map((relationshipIds) => + step.runWorkflow(updateDescriptionsSpec, { + graphId: input.graphId, + relationshipIds, + }) + ), + ]); + } - return { - entityIds, - relationshipIds, - }; + const descriptions = await step.run({ name: "generate-descriptions" }, async () => { + return collectPendingDescriptionTargets(input.graphId); }); // Two-level fan-out: spawn groups of description batches to avoid exceeding @@ -526,7 +521,6 @@ export const processFile = defineWorkflow( } await updateFileProcessingState(input.fileId, "deduplicating", "processing"); - const start = performance.now(); await updateFileProcessingState(input.fileId, "saving", "processing"); @@ -536,337 +530,15 @@ export const processFile = defineWorkflow( } const graph = loadedGraph.content; - const unitRows = toTextUnitRows(graph.units); - const entityRows = graph.entities.map((entity) => ({ - id: entity.id, - graphId: input.graphId, - active: false, - name: entity.name, - description: "", - type: entity.type, - embedding: EMPTY_VECTOR_SQL, - })); - const sourceRows = [ - ...graph.entities.flatMap((entity) => - entity.sources.map((source) => ({ - id: source.id, - entityId: entity.id, - relationshipId: null, - textUnitId: source.unitId, - active: false, - description: source.description, - sourceChunkIds: source.sourceChunkIds ?? [], - embedding: EMPTY_VECTOR_SQL, - })) - ), - ...graph.relationships.flatMap((relationship) => - relationship.sources.map((source) => ({ - id: source.id, - entityId: null, - relationshipId: relationship.id, - textUnitId: source.unitId, - active: false, - description: source.description, - sourceChunkIds: source.sourceChunkIds ?? [], - embedding: EMPTY_VECTOR_SQL, - })) - ), - ]; - const relationshipRows = graph.relationships.map((relationship) => ({ - id: relationship.id, - active: false, - sourceId: relationship.sourceId, - targetId: relationship.targetId, - graphId: input.graphId, - rank: relationship.strength, - description: "", - embedding: EMPTY_VECTOR_SQL, - })); - - const insertedEntityIds = entityRows.map((entity) => entity.id); - const insertedRelationshipIds = relationshipRows.map((relationship) => relationship.id); - - const metrics = await db.transaction(async (tx) => { - const insertUnitsStart = performance.now(); - for (const chunk of chunkItems(unitRows)) { - await tx - .insert(textUnitTable) - .values(chunk) - .onConflictDoUpdate({ - target: textUnitTable.id, - set: { - fileId: sql`excluded.file_id`, - text: sql`excluded.text`, - startPage: sql`excluded.start_page`, - endPage: sql`excluded.end_page`, - chunks: sql`excluded.chunks`, - updatedAt: sql`NOW()`, - }, - }); - } - const insertUnitsDuration = performance.now() - insertUnitsStart; - - const insertEntitiesStart = performance.now(); - for (const chunk of chunkItems(entityRows)) { - await tx.insert(entityTable).values(chunk).onConflictDoNothing(); - } - const insertEntitiesDuration = performance.now() - insertEntitiesStart; - - const insertRelationshipsStart = performance.now(); - for (const chunk of chunkItems(relationshipRows)) { - await tx.insert(relationshipTable).values(chunk).onConflictDoNothing(); - } - for (const chunk of chunkItems(sourceRows)) { - await tx.insert(sourcesTable).values(chunk).onConflictDoNothing(); - } - const insertRelationshipsDuration = performance.now() - insertRelationshipsStart; - - const dedupeEntitiesStart = performance.now(); - if (insertedEntityIds.length > 0) { - const entityIds = textArray(insertedEntityIds); - const candidateNameKeySql = sql.raw(entityCompactNameKey("candidate.name")); - const seededNameKeySql = sql.raw(entityCompactNameKey("seed.name")); - - await tx.execute(sql` - WITH seeded_keys AS ( - SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name - FROM entities seed - WHERE seed.graph_id = ${input.graphId} - AND seed.id = ANY(${entityIds}) - ), duplicates AS ( - SELECT - candidate.id, - first_value(candidate.id) OVER ( - PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} - ORDER BY candidate.active DESC, candidate.id ASC - ) AS canonical_id - FROM entities candidate - JOIN seeded_keys seeded - ON seeded.type = candidate.type - AND seeded.normalized_name = ${candidateNameKeySql} - WHERE candidate.graph_id = ${input.graphId} - ) - UPDATE sources source - SET entity_id = duplicates.canonical_id - FROM duplicates - WHERE source.entity_id = duplicates.id - AND duplicates.id <> duplicates.canonical_id - `); - - await tx.execute(sql` - WITH seeded_keys AS ( - SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name - FROM entities seed - WHERE seed.graph_id = ${input.graphId} - AND seed.id = ANY(${entityIds}) - ), duplicates AS ( - SELECT - candidate.id, - first_value(candidate.id) OVER ( - PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} - ORDER BY candidate.active DESC, candidate.id ASC - ) AS canonical_id - FROM entities candidate - JOIN seeded_keys seeded - ON seeded.type = candidate.type - AND seeded.normalized_name = ${candidateNameKeySql} - WHERE candidate.graph_id = ${input.graphId} - ) - UPDATE relationships relationship - SET source_id = duplicates.canonical_id - FROM duplicates - WHERE relationship.source_id = duplicates.id - AND relationship.graph_id = ${input.graphId} - AND duplicates.id <> duplicates.canonical_id - `); - - await tx.execute(sql` - WITH seeded_keys AS ( - SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name - FROM entities seed - WHERE seed.graph_id = ${input.graphId} - AND seed.id = ANY(${entityIds}) - ), duplicates AS ( - SELECT - candidate.id, - first_value(candidate.id) OVER ( - PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} - ORDER BY candidate.active DESC, candidate.id ASC - ) AS canonical_id - FROM entities candidate - JOIN seeded_keys seeded - ON seeded.type = candidate.type - AND seeded.normalized_name = ${candidateNameKeySql} - WHERE candidate.graph_id = ${input.graphId} - ) - UPDATE relationships relationship - SET target_id = duplicates.canonical_id - FROM duplicates - WHERE relationship.target_id = duplicates.id - AND relationship.graph_id = ${input.graphId} - AND duplicates.id <> duplicates.canonical_id - `); - - await tx.execute(sql` - WITH seeded_keys AS ( - SELECT DISTINCT seed.type, ${seededNameKeySql} AS normalized_name - FROM entities seed - WHERE seed.graph_id = ${input.graphId} - AND seed.id = ANY(${entityIds}) - ), duplicates AS ( - SELECT - candidate.id, - first_value(candidate.id) OVER ( - PARTITION BY candidate.graph_id, candidate.type, ${candidateNameKeySql} - ORDER BY candidate.active DESC, candidate.id ASC - ) AS canonical_id - FROM entities candidate - JOIN seeded_keys seeded - ON seeded.type = candidate.type - AND seeded.normalized_name = ${candidateNameKeySql} - WHERE candidate.graph_id = ${input.graphId} - ) - DELETE FROM entities entity - USING duplicates - WHERE entity.id = duplicates.id - AND duplicates.id <> duplicates.canonical_id - `); - } - const dedupeEntitiesDuration = performance.now() - dedupeEntitiesStart; - - const dedupeRelationshipsStart = performance.now(); - await tx.execute(sql` - DELETE FROM relationships - WHERE graph_id = ${input.graphId} - AND source_id = target_id - `); - - if (insertedRelationshipIds.length > 0) { - const relationshipIds = textArray(insertedRelationshipIds); - - await tx.execute(sql` - WITH seeded_pairs AS ( - SELECT DISTINCT - least(relationship.source_id, relationship.target_id) AS pair_source_id, - greatest(relationship.source_id, relationship.target_id) AS pair_target_id - FROM relationships relationship - WHERE relationship.graph_id = ${input.graphId} - AND relationship.id = ANY(${relationshipIds}) - ), duplicates AS ( - SELECT - relationship.id, - least(relationship.source_id, relationship.target_id) AS canonical_source_id, - greatest(relationship.source_id, relationship.target_id) AS canonical_target_id, - first_value(relationship.id) OVER ( - PARTITION BY relationship.graph_id, least(relationship.source_id, relationship.target_id), greatest(relationship.source_id, relationship.target_id) - ORDER BY relationship.active DESC, relationship.id ASC - ) AS canonical_id, - max(relationship.rank) OVER ( - PARTITION BY relationship.graph_id, least(relationship.source_id, relationship.target_id), greatest(relationship.source_id, relationship.target_id) - ) AS canonical_rank - FROM relationships relationship - JOIN seeded_pairs seeded - ON seeded.pair_source_id = least(relationship.source_id, relationship.target_id) - AND seeded.pair_target_id = greatest(relationship.source_id, relationship.target_id) - WHERE relationship.graph_id = ${input.graphId} - ) - UPDATE relationships relationship - SET source_id = duplicates.canonical_source_id, - target_id = duplicates.canonical_target_id, - rank = CASE - WHEN relationship.id = duplicates.canonical_id THEN duplicates.canonical_rank - ELSE relationship.rank - END, - updated_at = NOW() - FROM duplicates - WHERE relationship.id = duplicates.id - AND ( - relationship.source_id <> duplicates.canonical_source_id - OR relationship.target_id <> duplicates.canonical_target_id - OR ( - relationship.id = duplicates.canonical_id - AND relationship.rank <> duplicates.canonical_rank - ) - ) - `); - - await tx.execute(sql` - WITH seeded_pairs AS ( - SELECT DISTINCT - least(relationship.source_id, relationship.target_id) AS pair_source_id, - greatest(relationship.source_id, relationship.target_id) AS pair_target_id - FROM relationships relationship - WHERE relationship.graph_id = ${input.graphId} - AND relationship.id = ANY(${relationshipIds}) - ), duplicates AS ( - SELECT - relationship.id, - first_value(relationship.id) OVER ( - PARTITION BY relationship.graph_id, least(relationship.source_id, relationship.target_id), greatest(relationship.source_id, relationship.target_id) - ORDER BY relationship.active DESC, relationship.id ASC - ) AS canonical_id - FROM relationships relationship - JOIN seeded_pairs seeded - ON seeded.pair_source_id = least(relationship.source_id, relationship.target_id) - AND seeded.pair_target_id = greatest(relationship.source_id, relationship.target_id) - WHERE relationship.graph_id = ${input.graphId} - ) - UPDATE sources source - SET relationship_id = duplicates.canonical_id - FROM duplicates - WHERE source.relationship_id = duplicates.id - AND duplicates.id <> duplicates.canonical_id - `); - - await tx.execute(sql` - WITH seeded_pairs AS ( - SELECT DISTINCT - least(relationship.source_id, relationship.target_id) AS pair_source_id, - greatest(relationship.source_id, relationship.target_id) AS pair_target_id - FROM relationships relationship - WHERE relationship.graph_id = ${input.graphId} - AND relationship.id = ANY(${relationshipIds}) - ), duplicates AS ( - SELECT - relationship.id, - first_value(relationship.id) OVER ( - PARTITION BY relationship.graph_id, least(relationship.source_id, relationship.target_id), greatest(relationship.source_id, relationship.target_id) - ORDER BY relationship.active DESC, relationship.id ASC - ) AS canonical_id - FROM relationships relationship - JOIN seeded_pairs seeded - ON seeded.pair_source_id = least(relationship.source_id, relationship.target_id) - AND seeded.pair_target_id = greatest(relationship.source_id, relationship.target_id) - WHERE relationship.graph_id = ${input.graphId} - ) - DELETE FROM relationships relationship - USING duplicates - WHERE relationship.id = duplicates.id - AND duplicates.id <> duplicates.canonical_id - `); - } - const dedupeRelationshipsDuration = performance.now() - dedupeRelationshipsStart; - - return { - insertUnitsDuration, - insertEntitiesDuration, - insertRelationshipsDuration, - dedupeEntitiesDuration, - dedupeRelationshipsDuration, - }; - }); - - const duration = performance.now() - start; + const saveResult = await saveGraphToDatabase(input.graphId, graph); return { summary: { fileId: input.fileId, - units: graph.units.length, - entities: graph.entities.length, - relationships: graph.relationships.length, + ...saveResult.summary, }, - duration, - metrics, + duration: saveResult.duration, + metrics: saveResult.metrics, }; }); if (saveGraphResult === FILE_DELETED) { diff --git a/apps/worker/workflows/process-files-spec.ts b/apps/worker/workflows/process-files-spec.ts index f3effac4..922987bd 100644 --- a/apps/worker/workflows/process-files-spec.ts +++ b/apps/worker/workflows/process-files-spec.ts @@ -14,5 +14,11 @@ export const processFilesSpec = defineWorkflowSpec({ graphId: z.string(), fileIds: z.array(z.string()), processRunId: z.string().optional(), + code: z + .object({ + kind: z.literal("repository"), + retiredFileIds: z.array(z.string()).optional(), + }) + .optional(), }), }); diff --git a/apps/worker/workflows/sync-repository-graph-spec.ts b/apps/worker/workflows/sync-repository-graph-spec.ts new file mode 100644 index 00000000..48f4b658 --- /dev/null +++ b/apps/worker/workflows/sync-repository-graph-spec.ts @@ -0,0 +1,19 @@ +import { defineWorkflowSpec } from "openworkflow"; +import z from "zod"; + +export const syncRepositoryGraphSpec = defineWorkflowSpec({ + name: "sync-repository-graph", + version: "1.0.0", + retryPolicy: { + initialInterval: "5s", + backoffCoefficient: 2, + maximumInterval: "1m", + maximumAttempts: 3, + }, + schema: z.object({ + bindingId: z.string(), + reason: z.enum(["initial", "webhook", "manual"]), + commitSha: z.string().optional(), + deliveryId: z.string().optional(), + }), +}); diff --git a/apps/worker/workflows/sync-repository-graph.test.ts b/apps/worker/workflows/sync-repository-graph.test.ts new file mode 100644 index 00000000..0e571f7c --- /dev/null +++ b/apps/worker/workflows/sync-repository-graph.test.ts @@ -0,0 +1,673 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import type { ProviderCodeFile, ProviderRepositoryChange } from "@kiwi/connectors"; + +type SelectResult = { kind: "limit"; value: unknown[] } | { kind: "where"; value: unknown[] }; + +const insertedFileValues: Array> = []; +const processWorkflowInputs: Array> = []; +const deleteWorkflowInputs: Array> = []; +const bindingUpdates: Array> = []; +const compareCalls: Array<{ fromCommitSha: string; toCommitSha: string }> = []; +const readFileCalls: Array<{ path: string; commitSha: string }> = []; +const txWhereConditions: unknown[] = []; +const updateWhereConditions: unknown[] = []; +const pendingReadResolutions: Array<() => void> = []; + +let selectResults: SelectResult[] = []; +let txSelectResults: unknown[][] = []; +let insertedFileRowsOverride: Array<{ id: string; key: string }> | null = null; +let processRunInsertCount = 0; +let processFilesError: Error | null = null; +let compareChanges: ProviderRepositoryChange[] = []; +let compareIsIncremental = true; +let snapshotFiles: ProviderCodeFile[] = []; +let readFileContents: Record = {}; +let loadSnapshotCalls = 0; +let activeReadFileCalls = 0; +let holdReadFiles = false; +let maxReadFileConcurrency = 0; + +function createSelectQuery() { + const result = selectResults.shift(); + if (!result) { + throw new Error("Unexpected select call"); + } + + const chain = { + from: () => chain, + innerJoin: () => chain, + where: () => (result.kind === "where" ? result.value : chain), + limit: () => (result.kind === "limit" ? result.value : result.value), + }; + return chain; +} + +function createTxSelectQuery() { + const result = txSelectResults.shift(); + if (!result) { + throw new Error("Unexpected transaction select call"); + } + + const chain = { + from: () => chain, + innerJoin: () => chain, + where: (condition: unknown) => { + txWhereConditions.push(condition); + return result; + }, + limit: () => result, + }; + return chain; +} + +function queryContainsParamValue(value: unknown, expected: unknown, seen = new WeakSet()): boolean { + if (Array.isArray(value)) { + return value.some((item) => queryContainsParamValue(item, expected, seen)); + } + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return false; + } + seen.add(value); + if (value.constructor.name === "Param" && "value" in value) { + return value.value === expected; + } + return Object.values(value).some((item) => queryContainsParamValue(item, expected, seen)); + } + return false; +} + +const transactionDb = { + select: () => createTxSelectQuery(), + update: () => ({ + set: (values: Record) => ({ + where: async () => { + bindingUpdates.push(values); + return undefined; + }, + }), + }), + insert: () => ({ + values: (values: unknown) => { + if ( + Array.isArray(values) && + values.every( + (value) => + typeof value === "object" && value !== null && "processRunId" in value && "fileId" in value + ) + ) { + return undefined; + } + + return { + onConflictDoNothing: () => ({ + returning: () => { + if (!Array.isArray(values)) { + throw new Error("Expected file rows"); + } + insertedFileValues.push(...(values as Array>)); + return ( + insertedFileRowsOverride ?? + (values as Array<{ id: string; key: string }>).map((value) => ({ + id: value.id, + key: value.key, + })) + ); + }, + }), + returning: () => { + processRunInsertCount += 1; + return [{ id: "process-run-1" }]; + }, + }; + }, + }), +}; + +mock.module("@kiwi/db", () => ({ + db: { + select: () => createSelectQuery(), + update: () => ({ + set: (values: Record) => ({ + where: async (condition: unknown) => { + bindingUpdates.push(values); + updateWhereConditions.push(condition); + return undefined; + }, + }), + }), + transaction: async (callback: (tx: typeof transactionDb) => Promise) => callback(transactionDb), + }, +})); + +mock.module("@kiwi/connectors", () => ({ + ConnectorProviderError: class ConnectorProviderError extends Error { + constructor( + public readonly kind: string, + message: string + ) { + super(message); + this.name = "ConnectorProviderError"; + } + }, + MAX_REPOSITORY_CODE_BYTES: 100_000, + MAX_REPOSITORY_CODE_FILES: 100, + createGitHubClient: () => ({ + provider: "github", + listRepositories: async () => [], + listBranches: async () => [{ name: "main", commitSha: "commit-new" }], + loadRepositorySnapshot: async () => { + loadSnapshotCalls += 1; + return { + repository: { + provider: "github", + id: "1", + fullName: "acme/widgets", + name: "widgets", + htmlUrl: "https://github.com/acme/widgets", + defaultBranch: "main", + private: true, + }, + branch: { name: "main", commitSha: "commit-new" }, + commitSha: "commit-new", + files: snapshotFiles, + }; + }, + compareRepository: async (_repository: unknown, fromCommitSha: string, toCommitSha: string) => { + compareCalls.push({ fromCommitSha, toCommitSha }); + return { fromCommitSha, toCommitSha, isIncremental: compareIsIncremental, changes: compareChanges }; + }, + readFile: async (_repository: unknown, path: string, commitSha: string) => { + readFileCalls.push({ path, commitSha }); + activeReadFileCalls += 1; + maxReadFileConcurrency = Math.max(maxReadFileConcurrency, activeReadFileCalls); + if (holdReadFiles) { + await new Promise((resolve) => { + pendingReadResolutions.push(resolve); + }); + } + activeReadFileCalls -= 1; + const content = readFileContents[path]; + if (!content) { + throw new Error(`Missing mocked content for ${path}`); + } + return content; + }, + }), + createGitHubInstallationToken: async () => ({ token: "installation-token", expiresAt: "2026-01-01T01:00:00Z" }), + createGitLabClient: () => { + throw new Error("GitLab client was not expected"); + }, + normalizeGitLabBaseUrl: (value: string) => value.replace(/\/+$/, ""), +})); + +mock.module("@kiwi/connectors/credentials", () => ({ + decryptConnectorCredentials: () => ({ provider: "github", appId: "app-1", privateKeyPem: "pem" }), +})); + +mock.module("../env", () => ({ + env: { + AUTH_SECRET: "test-secret", + }, +})); + +function bindingRow(lastSyncedCommitSha: string | null) { + return { + binding: { + id: "binding-1", + graphId: "graph-1", + connectorInstallationId: "installation-1", + providerRepositoryId: "1", + repositoryFullName: "acme/widgets", + repositoryHtmlUrl: "https://github.com/acme/widgets", + branch: "main", + webhookEnabled: true, + lastSeenCommitSha: lastSyncedCommitSha, + lastSyncedCommitSha, + }, + installation: { + id: "installation-1", + connectorId: "connector-1", + providerInstallationId: "99", + encryptedCredentials: null, + status: "active", + }, + connector: { + id: "connector-1", + provider: "github", + encryptedCredentials: "encrypted", + status: "active", + }, + graph: { + id: "graph-1", + state: "ready", + }, + }; +} + +function activeFile(id: string, commitSha: string, path: string, size: number) { + return { + id, + size, + metadata: JSON.stringify({ + repositoryUrl: "https://github.com/acme/widgets", + repositoryName: "acme/widgets", + commitSha, + path, + }), + }; +} + +async function runWorkflow(input: { + bindingId: string; + reason: "manual" | "webhook" | "initial"; + commitSha?: string; + deliveryId?: string; +}) { + return syncRepositoryGraph.fn({ + input, + step: { + run: async (_config: { name: string }, fn: () => unknown) => { + const result = await fn(); + return result === undefined ? undefined : JSON.parse(JSON.stringify(result)); + }, + runWorkflow: async (spec: { name: string }, workflowInput?: unknown) => { + if (spec.name === "process-files") { + processWorkflowInputs.push((workflowInput ?? {}) as Record); + if (processFilesError) { + throw processFilesError; + } + return undefined; + } + if (spec.name === "delete-file") { + deleteWorkflowInputs.push((workflowInput ?? {}) as Record); + return undefined; + } + throw new Error(`Unexpected workflow ${spec.name}`); + }, + } as never, + version: null, + run: { + id: "workflow-run-1", + workflowName: "sync-repository-graph", + createdAt: new Date("2026-01-01T00:00:00Z"), + startedAt: new Date("2026-01-01T00:00:00Z"), + retryAttempt: 1, + retryMaxAttempts: 1, + retryTerminal: true, + }, + }); +} + +// Dynamic import is required so module mocks are installed before the workflow module is evaluated. +const { syncRepositoryGraph } = await import("./sync-repository-graph"); + +describe("syncRepositoryGraph", () => { + beforeEach(() => { + insertedFileValues.length = 0; + processWorkflowInputs.length = 0; + deleteWorkflowInputs.length = 0; + bindingUpdates.length = 0; + compareCalls.length = 0; + readFileCalls.length = 0; + txWhereConditions.length = 0; + updateWhereConditions.length = 0; + pendingReadResolutions.length = 0; + selectResults = []; + processFilesError = null; + compareChanges = []; + compareIsIncremental = true; + snapshotFiles = []; + readFileContents = {}; + loadSnapshotCalls = 0; + activeReadFileCalls = 0; + holdReadFiles = false; + maxReadFileConcurrency = 0; + txSelectResults = []; + insertedFileRowsOverride = null; + processRunInsertCount = 0; + }); + + test("processes only changed supported connector files", async () => { + compareChanges = [{ status: "modified", newPath: "src/index.ts" }]; + readFileContents = { + "src/index.ts": "export const next = shared;\n", + }; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [ + activeFile("old-file", "commit-old", "src/index.ts", 25), + activeFile("shared-file", "commit-old", "src/shared.ts", 22), + ], + }, + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 1 }); + expect(loadSnapshotCalls).toBe(0); + expect(compareCalls).toEqual([{ fromCommitSha: "commit-old", toCommitSha: "commit-new" }]); + expect(readFileCalls).toEqual([{ path: "src/index.ts", commitSha: "commit-new" }]); + expect(insertedFileValues.map((row) => row.name)).toEqual(["src/index.ts"]); + expect(processWorkflowInputs).toHaveLength(1); + expect(processWorkflowInputs[0]).toMatchObject({ + graphId: "graph-1", + processRunId: "process-run-1", + code: { kind: "repository", retiredFileIds: ["old-file"] }, + }); + expect(String(insertedFileValues[0]?.checksum)).toStartWith("commit-new:src/index.ts:"); + }); + + test("limits concurrent provider reads for incremental changed files", async () => { + const changedPaths = Array.from({ length: 10 }, (_, index) => `src/file-${index}.ts`); + compareChanges = changedPaths.map((newPath) => ({ status: "modified", newPath })); + readFileContents = Object.fromEntries(changedPaths.map((path) => [path, `export const value = "${path}";\n`])); + holdReadFiles = true; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: changedPaths.map((path, index) => activeFile(`old-file-${index}`, "commit-old", path, 25)), + }, + ]; + + let workflowDone = false; + const resultPromise = runWorkflow({ + bindingId: "binding-1", + reason: "manual", + commitSha: "commit-new", + }).finally(() => { + workflowDone = true; + }); + + while (pendingReadResolutions.length < 4) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + expect(maxReadFileConcurrency).toBeLessThanOrEqual(4); + + while (!workflowDone) { + while (pendingReadResolutions.length === 0 && !workflowDone) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + pendingReadResolutions.splice(0).forEach((resolve) => resolve()); + await new Promise((resolve) => setTimeout(resolve, 0)); + } + + const result = await resultPromise; + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 10 }); + expect(maxReadFileConcurrency).toBeLessThanOrEqual(4); + }); + + test("reuses existing repository files and process run when file insert conflicts", async () => { + compareChanges = [{ status: "modified", newPath: "src/index.ts" }]; + readFileContents = { + "src/index.ts": "export const next = shared;\n", + }; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [activeFile("old-file", "commit-old", "src/index.ts", 25)], + }, + ]; + insertedFileRowsOverride = []; + txSelectResults = [ + [{ id: "existing-file", key: "connector:binding-1:commit-new:src/index.ts" }], + [{ id: "existing-process-run", status: "pending", fileId: "existing-file" }], + [{ processRunId: "existing-process-run", fileId: "existing-file" }], + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 1 }); + expect(processRunInsertCount).toBe(0); + expect(processWorkflowInputs).toEqual([ + { + graphId: "graph-1", + fileIds: ["existing-file"], + processRunId: "existing-process-run", + code: { kind: "repository", retiredFileIds: ["old-file"] }, + }, + ]); + }); + + test("skips processing when file insert conflicts with a completed process run", async () => { + compareChanges = [{ status: "modified", newPath: "src/index.ts" }]; + readFileContents = { + "src/index.ts": "export const next = shared;\n", + }; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [activeFile("old-file", "commit-old", "src/index.ts", 25)], + }, + ]; + insertedFileRowsOverride = []; + txSelectResults = [ + [{ id: "existing-file", key: "connector:binding-1:commit-new:src/index.ts" }], + [{ id: "completed-process-run", status: "completed", fileId: "existing-file" }], + [{ processRunId: "completed-process-run", fileId: "existing-file" }], + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 1 }); + expect(processRunInsertCount).toBe(0); + expect(queryContainsParamValue(txWhereConditions[1], "completed")).toBe(true); + expect(processWorkflowInputs).toEqual([]); + expect(bindingUpdates).toContainEqual({ + syncStatus: "synced", + lastSeenCommitSha: "commit-new", + lastSyncedCommitSha: "commit-new", + syncErrorCode: null, + }); + }); + + test("creates a process run when matching run has extra files", async () => { + compareChanges = [{ status: "modified", newPath: "src/index.ts" }]; + readFileContents = { + "src/index.ts": "export const next = shared;\n", + }; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [activeFile("old-file", "commit-old", "src/index.ts", 25)], + }, + ]; + insertedFileRowsOverride = []; + txSelectResults = [ + [{ id: "existing-file", key: "connector:binding-1:commit-new:src/index.ts" }], + [{ id: "larger-process-run", status: "completed", fileId: "existing-file" }], + [ + { processRunId: "larger-process-run", fileId: "existing-file" }, + { processRunId: "larger-process-run", fileId: "other-file" }, + ], + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 1 }); + expect(processRunInsertCount).toBe(1); + expect(processWorkflowInputs).toEqual([ + { + graphId: "graph-1", + fileIds: ["existing-file"], + processRunId: "process-run-1", + code: { kind: "repository", retiredFileIds: ["old-file"] }, + }, + ]); + }); + + test("finalizes delete-only connector deltas without inserting new files", async () => { + compareChanges = [{ status: "deleted", oldPath: "src/removed.ts" }]; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [ + activeFile("removed-file", "commit-old", "src/removed.ts", 20), + activeFile("shared-file", "commit-old", "src/shared.ts", 22), + ], + }, + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 0 }); + expect(readFileCalls).toEqual([]); + expect(insertedFileValues).toEqual([]); + expect(processWorkflowInputs).toEqual([ + { + graphId: "graph-1", + fileIds: [], + code: { kind: "repository", retiredFileIds: ["removed-file"] }, + }, + ]); + }); + + test("skips processing when no supported code paths changed", async () => { + compareChanges = []; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [activeFile("old-file", "commit-old", "src/index.ts", 25)], + }, + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 0 }); + expect(processWorkflowInputs).toEqual([]); + expect(insertedFileValues).toEqual([]); + expect(bindingUpdates).toContainEqual({ + syncStatus: "synced", + lastSeenCommitSha: "commit-new", + lastSyncedCommitSha: "commit-new", + syncErrorCode: null, + }); + }); + + test("scopes duplicate webhook markers to the connector", async () => { + selectResults = [{ kind: "limit", value: [bindingRow("commit-new")] }]; + + const result = await runWorkflow({ + bindingId: "binding-1", + reason: "webhook", + commitSha: "commit-new", + deliveryId: "delivery-1", + }); + + expect(result).toEqual({ skipped: true, commitSha: "commit-new" }); + expect(bindingUpdates).toContainEqual({ status: "duplicate" }); + expect(queryContainsParamValue(updateWhereConditions.at(-1), "connector-1")).toBe(true); + expect(queryContainsParamValue(updateWhereConditions.at(-1), "github")).toBe(true); + expect(queryContainsParamValue(updateWhereConditions.at(-1), "delivery-1")).toBe(true); + }); + + test("falls back to a full snapshot when compare is not incremental", async () => { + compareIsIncremental = false; + snapshotFiles = [ + { + path: "src/reset.ts", + size: 24, + checksum: "reset-sha", + htmlUrl: "https://github.com/acme/widgets/blob/commit-new/src/reset.ts", + rawUrl: "https://raw.githubusercontent.com/acme/widgets/commit-new/src/reset.ts", + content: "export const reset = true;\n", + }, + ]; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [ + activeFile("old-file", "commit-old", "src/index.ts", 25), + activeFile("shared-file", "commit-old", "src/shared.ts", 22), + ], + }, + ]; + + const result = await runWorkflow({ bindingId: "binding-1", reason: "webhook", commitSha: "commit-new" }); + + expect(result).toMatchObject({ commitSha: "commit-new", fileCount: 1 }); + expect(loadSnapshotCalls).toBe(1); + expect(readFileCalls).toEqual([]); + expect(insertedFileValues.map((row) => row.name)).toEqual(["src/reset.ts"]); + expect(processWorkflowInputs[0]).toMatchObject({ + graphId: "graph-1", + processRunId: "process-run-1", + code: { kind: "repository", retiredFileIds: ["old-file", "shared-file"] }, + }); + }); + + test("rolls back inserted files when incremental processing fails", async () => { + compareChanges = [{ status: "modified", newPath: "src/index.ts" }]; + readFileContents = { + "src/index.ts": "export const next = shared;\n", + }; + processFilesError = new Error("child failure"); + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [ + activeFile("old-file", "commit-old", "src/index.ts", 25), + activeFile("shared-file", "commit-old", "src/shared.ts", 22), + ], + }, + ]; + + await expect( + runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }) + ).rejects.toThrow("child failure"); + expect(deleteWorkflowInputs).toEqual([{ graphId: "graph-1", fileId: insertedFileValues[0]?.id }]); + }); + + test("marks binding failed when terminal processing fails", async () => { + compareChanges = [{ status: "modified", newPath: "src/index.ts" }]; + readFileContents = { + "src/index.ts": "export const next = shared;\n", + }; + processFilesError = new Error("child failure"); + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { + kind: "where", + value: [ + activeFile("old-file", "commit-old", "src/index.ts", 25), + activeFile("shared-file", "commit-old", "src/shared.ts", 22), + ], + }, + ]; + + await expect( + runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }) + ).rejects.toThrow("child failure"); + expect(bindingUpdates).toContainEqual({ + syncStatus: "failed", + syncErrorCode: "sync_failed", + }); + }); + + test("rejects duplicate normalized paths from provider deltas", async () => { + compareChanges = [ + { status: "modified", newPath: "src/index.ts" }, + { status: "deleted", oldPath: "src/index.ts" }, + ]; + selectResults = [ + { kind: "limit", value: [bindingRow("commit-old")] }, + { kind: "where", value: [activeFile("old-file", "commit-old", "src/index.ts", 25)] }, + ]; + + await expect( + runWorkflow({ bindingId: "binding-1", reason: "manual", commitSha: "commit-new" }) + ).rejects.toThrow("Provider delta contained duplicate path src/index.ts"); + expect(processWorkflowInputs).toEqual([]); + }); +}); diff --git a/apps/worker/workflows/sync-repository-graph.ts b/apps/worker/workflows/sync-repository-graph.ts new file mode 100644 index 00000000..42c0d657 --- /dev/null +++ b/apps/worker/workflows/sync-repository-graph.ts @@ -0,0 +1,790 @@ +import { createHash } from "node:crypto"; +import { + ConnectorProviderError, + MAX_REPOSITORY_CODE_BYTES, + MAX_REPOSITORY_CODE_FILES, + createGitHubClient, + createGitHubInstallationToken, + createGitLabClient, + normalizeGitLabBaseUrl, +} from "@kiwi/connectors"; +import type { + GitHubConnectorCredentials, + GitLabConnectorCredentials, + GitLabInstallationCredentials, + ProviderCodeFile, + ProviderRepository, + ProviderRepositoryChange, + ProviderRepositoryClient, + ProviderRepositorySnapshot, +} from "@kiwi/connectors"; +import { decryptConnectorCredentials, type ConnectorSecretPayload } from "@kiwi/connectors/credentials"; +import { db } from "@kiwi/db"; +import { + connectorInstallationsTable, + connectorsTable, + connectorWebhookEventsTable, + repositoryGraphBindingsTable, +} from "@kiwi/db/tables/connectors"; +import { + filesTable, + graphTable, + processRunFilesTable, + processRunsTable, + type ProcessRunStatus, +} from "@kiwi/db/tables/graph"; +import { serializeCodeFileMetadata } from "@kiwi/graph/code/metadata"; +import { and, eq, inArray } from "drizzle-orm"; +import { defineWorkflow } from "openworkflow"; +import { parseCodeFileMetadata } from "../lib/code-file-metadata"; +import { env } from "../env"; +import { deleteFileSpec } from "./delete-file-spec"; +import { processFilesSpec } from "./process-files-spec"; +import { syncRepositoryGraphSpec } from "./sync-repository-graph-spec"; + +type BindingGraphRow = { + binding: typeof repositoryGraphBindingsTable.$inferSelect; + installation: typeof connectorInstallationsTable.$inferSelect; + connector: typeof connectorsTable.$inferSelect; + graph: typeof graphTable.$inferSelect; +}; + +type ProviderClientContext = { + client: ProviderRepositoryClient; + gitLabBaseUrl?: string; +}; + +type Snapshot = ProviderRepositorySnapshot & { + files: ProviderCodeFile[]; +}; + +type ActiveBindingFile = { + id: string; + size: number; + path: string; +}; + +type IncrementalSyncPlan = { + newPaths: string[]; + retiredFileIds: string[]; +}; + +type ReusableProcessRunStatus = Exclude; + +type InsertedRepositoryFiles = { + fileIds: string[]; + processRunId: string; + processRunStatus: ReusableProcessRunStatus; +}; + +const NO_RETRY = { maximumAttempts: 1 } as const; +const PROVIDER_FILE_READ_CONCURRENCY = 4; + +async function loadBindingGraph(bindingId: string): Promise { + const [row] = await db + .select({ + binding: repositoryGraphBindingsTable, + installation: connectorInstallationsTable, + connector: connectorsTable, + graph: graphTable, + }) + .from(repositoryGraphBindingsTable) + .innerJoin( + connectorInstallationsTable, + eq(connectorInstallationsTable.id, repositoryGraphBindingsTable.connectorInstallationId) + ) + .innerJoin(connectorsTable, eq(connectorsTable.id, connectorInstallationsTable.connectorId)) + .innerJoin(graphTable, eq(graphTable.id, repositoryGraphBindingsTable.graphId)) + .where(eq(repositoryGraphBindingsTable.id, bindingId)) + .limit(1); + return row ?? null; +} + +function isGitHubConnectorCredentials(value: ConnectorSecretPayload): value is GitHubConnectorCredentials { + return "provider" in value && value.provider === "github"; +} + +function isGitLabConnectorCredentials(value: ConnectorSecretPayload): value is GitLabConnectorCredentials { + return "provider" in value && value.provider === "gitlab" && "baseUrl" in value; +} + +function isGitLabInstallationCredentials(value: ConnectorSecretPayload): value is GitLabInstallationCredentials { + return "provider" in value && value.provider === "gitlab" && "accessToken" in value; +} + +function repositoryFromBinding(row: BindingGraphRow): ProviderRepository { + return { + provider: row.connector.provider as ProviderRepository["provider"], + id: row.binding.providerRepositoryId, + fullName: row.binding.repositoryFullName, + name: row.binding.repositoryFullName.split("/").at(-1) ?? row.binding.repositoryFullName, + htmlUrl: row.binding.repositoryHtmlUrl, + defaultBranch: row.binding.branch, + private: true, + }; +} + +async function createProviderClient(row: BindingGraphRow): Promise { + const connectorCredentials = decryptConnectorCredentials(row.connector.encryptedCredentials, env.AUTH_SECRET); + if (row.connector.provider === "github") { + if (!isGitHubConnectorCredentials(connectorCredentials)) { + throw new Error("Invalid connector credentials"); + } + const token = await createGitHubInstallationToken({ + credentials: connectorCredentials, + installationId: row.installation.providerInstallationId, + }); + return { + client: createGitHubClient({ installationToken: token.token }), + }; + } + + if (!isGitLabConnectorCredentials(connectorCredentials)) { + throw new Error("Invalid connector credentials"); + } + const installationCredentials = row.installation.encryptedCredentials + ? decryptConnectorCredentials(row.installation.encryptedCredentials, env.AUTH_SECRET) + : null; + if (!installationCredentials || !isGitLabInstallationCredentials(installationCredentials)) { + throw new Error("Invalid connector installation credentials"); + } + return { + client: createGitLabClient({ + baseUrl: connectorCredentials.baseUrl, + accessToken: installationCredentials.accessToken, + }), + gitLabBaseUrl: connectorCredentials.baseUrl, + }; +} + +async function resolveTargetCommitSha(row: BindingGraphRow, inputCommitSha?: string): Promise { + if (inputCommitSha) { + return inputCommitSha; + } + + const { client } = await createProviderClient(row); + const branch = (await client.listBranches(repositoryFromBinding(row))).find( + (candidate) => candidate.name === row.binding.branch + ); + if (!branch) { + throw new ConnectorProviderError("not-found", "Repository branch was not found"); + } + + return branch.commitSha; +} + +async function loadSnapshot(row: BindingGraphRow, commitSha: string): Promise { + const { client } = await createProviderClient(row); + return client.loadRepositorySnapshot( + repositoryFromBinding(row), + row.binding.branch, + commitSha + ) as Promise; +} + +async function compareProviderCommits(row: BindingGraphRow, fromCommitSha: string, toCommitSha: string) { + const { client } = await createProviderClient(row); + return client.compareRepository(repositoryFromBinding(row), fromCommitSha, toCommitSha); +} + +async function loadChangedFiles(row: BindingGraphRow, commitSha: string, paths: string[]): Promise { + const context = await createProviderClient(row); + const repository = repositoryFromBinding(row); + const files: ProviderCodeFile[] = []; + for (let index = 0; index < paths.length; index += PROVIDER_FILE_READ_CONCURRENCY) { + const batch = paths.slice(index, index + PROVIDER_FILE_READ_CONCURRENCY); + files.push( + ...(await Promise.all( + batch.map(async (path) => + buildConnectorFile( + row, + context, + commitSha, + path, + await context.client.readFile(repository, path, commitSha) + ) + ) + )) + ); + } + return files; +} + +async function loadActiveBindingFiles(bindingId: string): Promise { + const rows = await db + .select({ + id: filesTable.id, + size: filesTable.size, + metadata: filesTable.metadata, + }) + .from(filesTable) + .where( + and( + eq(filesTable.repositoryBindingId, bindingId), + eq(filesTable.type, "code"), + eq(filesTable.deleted, false) + ) + ); + + const filesByPath = new Map(); + for (const row of rows) { + const metadata = parseCodeFileMetadata(row.metadata); + if (!metadata) { + throw new Error(`Active binding file ${row.id} is missing repository metadata`); + } + if (filesByPath.has(metadata.path)) { + throw new Error(`Repository binding ${bindingId} has multiple active rows for ${metadata.path}`); + } + filesByPath.set(metadata.path, { + id: row.id, + size: row.size, + path: metadata.path, + }); + } + + return [...filesByPath.values()]; +} + +function activeFilesByPath(files: ActiveBindingFile[]): Map { + return new Map(files.map((file) => [file.path, file])); +} + +function planIncrementalChanges( + activeFiles: Map, + changes: ProviderRepositoryChange[] +): IncrementalSyncPlan { + const seenPaths = new Set(); + const newPaths: string[] = []; + const retiredFileIds = new Set(); + + const trackPath = (path: string) => { + if (seenPaths.has(path)) { + throw new Error(`Provider delta contained duplicate path ${path}`); + } + seenPaths.add(path); + }; + + const retirePath = (path: string) => { + const activeFile = activeFiles.get(path); + if (activeFile) { + retiredFileIds.add(activeFile.id); + } + }; + + for (const change of changes) { + switch (change.status) { + case "added": + case "modified": + trackPath(change.newPath); + newPaths.push(change.newPath); + retirePath(change.newPath); + break; + case "deleted": + trackPath(change.oldPath); + retirePath(change.oldPath); + break; + case "renamed": + trackPath(change.oldPath); + trackPath(change.newPath); + retirePath(change.oldPath); + retirePath(change.newPath); + newPaths.push(change.newPath); + break; + } + } + + return { + newPaths, + retiredFileIds: [...retiredFileIds], + }; +} + +function buildConnectorFile( + row: BindingGraphRow, + context: ProviderClientContext, + commitSha: string, + path: string, + content: string +): ProviderCodeFile { + const rawUrl = + row.connector.provider === "github" + ? `https://raw.githubusercontent.com/${row.binding.repositoryFullName}/${commitSha}/${path}` + : context.gitLabBaseUrl + ? `${normalizeGitLabBaseUrl(context.gitLabBaseUrl)}/api/v4/projects/${encodeURIComponent(row.binding.providerRepositoryId)}/repository/files/${encodeURIComponent(path)}/raw?ref=${encodeURIComponent(commitSha)}` + : undefined; + + return { + path, + size: Buffer.byteLength(content, "utf8"), + checksum: createHash("sha256").update(content, "utf8").digest("hex"), + htmlUrl: + row.connector.provider === "github" + ? `${row.binding.repositoryHtmlUrl}/blob/${commitSha}/${path}` + : `${row.binding.repositoryHtmlUrl}/-/blob/${commitSha}/${path}`, + ...(rawUrl ? { rawUrl } : {}), + content, + }; +} + +function assertBindingSnapshotLimits( + activeFiles: Map, + retiredFileIds: string[], + newFiles: ProviderCodeFile[] +) { + const retiredIds = new Set(retiredFileIds); + const totalFileCount = + [...activeFiles.values()].filter((file) => !retiredIds.has(file.id)).length + newFiles.length; + if (totalFileCount > MAX_REPOSITORY_CODE_FILES) { + throw new ConnectorProviderError("limit", "Repository contains too many supported code files"); + } + + const totalBytes = + [...activeFiles.values()].reduce((sum, file) => sum + (retiredIds.has(file.id) ? 0 : file.size), 0) + + newFiles.reduce((sum, file) => sum + file.size, 0); + if (totalBytes > MAX_REPOSITORY_CODE_BYTES) { + throw new ConnectorProviderError("limit", "Repository contains too much supported code"); + } +} + +function fileRows(row: BindingGraphRow, files: ProviderCodeFile[], commitSha: string) { + return files.map((file) => ({ + graphId: row.binding.graphId, + name: file.path, + size: file.size, + type: "code", + mimeType: "text/plain", + key: `connector:${row.binding.id}:${commitSha}:${file.path}`, + storageKind: "external", + externalUrl: file.rawUrl ?? file.htmlUrl, + externalProvider: row.connector.provider, + repositoryBindingId: row.binding.id, + checksum: `${commitSha}:${file.path}:${file.checksum}`, + metadata: serializeCodeFileMetadata({ + repositoryUrl: row.binding.repositoryHtmlUrl, + repositoryName: row.binding.repositoryFullName, + commitSha, + path: file.path, + external: + row.connector.provider === "github" && file.rawUrl + ? { provider: "github", rawUrl: file.rawUrl, htmlUrl: file.htmlUrl } + : undefined, + }), + id: crypto.randomUUID(), + })); +} + +type InsertedFileRow = { + id: string; + key: string; +}; + +function orderedFilesByKey(rows: ReturnType, files: InsertedFileRow[]): InsertedFileRow[] { + const filesByKey = new Map(files.map((file) => [file.key, file])); + const orderedFiles: InsertedFileRow[] = []; + for (const row of rows) { + const file = filesByKey.get(row.key); + if (file) { + orderedFiles.push(file); + } + } + return orderedFiles; +} + +type ReusableProcessRun = { + id: string; + status: ReusableProcessRunStatus; +}; + +function isReusableProcessRunStatus(status: ProcessRunStatus): status is ReusableProcessRunStatus { + return status !== "failed"; +} + +function processRunStatusPriority(status: ReusableProcessRunStatus) { + switch (status) { + case "completed": + return 0; + case "started": + return 1; + case "pending": + return 2; + } +} + +async function findReusableProcessRun( + tx: Parameters[0]>[0], + graphId: string, + fileIds: string[] +): Promise { + if (fileIds.length === 0) { + return null; + } + + const matchingRunRows = await tx + .select({ + id: processRunFilesTable.processRunId, + status: processRunsTable.status, + fileId: processRunFilesTable.fileId, + }) + .from(processRunFilesTable) + .innerJoin(processRunsTable, eq(processRunFilesTable.processRunId, processRunsTable.id)) + .where( + and( + eq(processRunsTable.graphId, graphId), + inArray(processRunsTable.status, ["pending", "started", "completed"]), + inArray(processRunFilesTable.fileId, fileIds) + ) + ); + + const expectedFileIds = new Set(fileIds); + const candidates = new Map }>(); + for (const row of matchingRunRows) { + if (!isReusableProcessRunStatus(row.status)) { + continue; + } + const candidate = candidates.get(row.id); + if (candidate) { + candidate.matchedFileIds.add(row.fileId); + } else { + candidates.set(row.id, { + id: row.id, + status: row.status, + matchedFileIds: new Set([row.fileId]), + }); + } + } + + const completeCandidateIds = [...candidates.values()] + .filter((candidate) => candidate.matchedFileIds.size === expectedFileIds.size) + .map((candidate) => candidate.id); + if (completeCandidateIds.length === 0) { + return null; + } + + const runFileRows = await tx + .select({ processRunId: processRunFilesTable.processRunId, fileId: processRunFilesTable.fileId }) + .from(processRunFilesTable) + .where(inArray(processRunFilesTable.processRunId, completeCandidateIds)); + const fileIdsByRun = new Map>(); + for (const row of runFileRows) { + const runFileIds = fileIdsByRun.get(row.processRunId); + if (runFileIds) { + runFileIds.add(row.fileId); + } else { + fileIdsByRun.set(row.processRunId, new Set([row.fileId])); + } + } + + const exactCandidates: ReusableProcessRun[] = []; + for (const candidateId of completeCandidateIds) { + const runFileIds = fileIdsByRun.get(candidateId); + if (!runFileIds || runFileIds.size !== expectedFileIds.size) { + continue; + } + let exact = true; + for (const fileId of runFileIds) { + if (!expectedFileIds.has(fileId)) { + exact = false; + break; + } + } + if (exact) { + const candidate = candidates.get(candidateId); + if (candidate) { + exactCandidates.push({ id: candidate.id, status: candidate.status }); + } + } + } + + exactCandidates.sort( + (left, right) => processRunStatusPriority(left.status) - processRunStatusPriority(right.status) + ); + return exactCandidates[0] ?? null; +} + +async function insertRepositoryFiles( + row: BindingGraphRow, + files: ProviderCodeFile[], + commitSha: string +): Promise { + return db.transaction(async (tx) => { + await tx + .update(repositoryGraphBindingsTable) + .set({ syncStatus: "syncing", lastSeenCommitSha: commitSha, syncErrorCode: null }) + .where(eq(repositoryGraphBindingsTable.id, row.binding.id)); + + const rows = fileRows(row, files, commitSha); + const insertedFiles = await tx + .insert(filesTable) + .values(rows) + .onConflictDoNothing() + .returning({ id: filesTable.id, key: filesTable.key }); + let committedFiles = orderedFilesByKey(rows, insertedFiles); + if (committedFiles.length !== files.length) { + const existingFiles = await tx + .select({ id: filesTable.id, key: filesTable.key }) + .from(filesTable) + .where( + and( + eq(filesTable.graphId, row.binding.graphId), + eq(filesTable.deleted, false), + inArray( + filesTable.key, + rows.map((file) => file.key) + ) + ) + ); + committedFiles = orderedFilesByKey(rows, existingFiles); + } + if (committedFiles.length !== files.length) { + throw new Error("Failed to insert all repository files"); + } + + const fileIds = committedFiles.map((file) => file.id); + if (insertedFiles.length === 0) { + const reusableProcessRun = await findReusableProcessRun(tx, row.binding.graphId, fileIds); + if (reusableProcessRun) { + return { + fileIds, + processRunId: reusableProcessRun.id, + processRunStatus: reusableProcessRun.status, + }; + } + } + + const [processRun] = await tx + .insert(processRunsTable) + .values({ graphId: row.binding.graphId, status: "pending" }) + .returning({ id: processRunsTable.id }); + if (!processRun) { + throw new Error("Failed to create process run"); + } + + await tx.insert(processRunFilesTable).values( + fileIds.map((fileId) => ({ + processRunId: processRun.id, + fileId, + })) + ); + + return { + fileIds, + processRunId: processRun.id, + processRunStatus: "pending", + }; + }); +} + +async function markWebhookDuplicate( + connectorId: string, + provider: typeof connectorsTable.$inferSelect.provider, + deliveryId: string +) { + await db + .update(connectorWebhookEventsTable) + .set({ status: "duplicate" }) + .where( + and( + eq(connectorWebhookEventsTable.connectorId, connectorId), + eq(connectorWebhookEventsTable.provider, provider), + eq(connectorWebhookEventsTable.deliveryId, deliveryId) + ) + ); +} + +async function markBindingSynced(bindingId: string, commitSha: string) { + await db + .update(repositoryGraphBindingsTable) + .set({ + syncStatus: "synced", + lastSeenCommitSha: commitSha, + lastSyncedCommitSha: commitSha, + syncErrorCode: null, + }) + .where(eq(repositoryGraphBindingsTable.id, bindingId)); +} + +async function markBindingFailed(bindingId: string) { + await db + .update(repositoryGraphBindingsTable) + .set({ syncStatus: "failed", syncErrorCode: "sync_failed" }) + .where(eq(repositoryGraphBindingsTable.id, bindingId)); +} + +export const syncRepositoryGraph = defineWorkflow(syncRepositoryGraphSpec, async ({ input, step, run }) => { + try { + const row = await step.run({ name: "load-binding" }, async () => loadBindingGraph(input.bindingId)); + if ( + !row || + row.connector.status !== "active" || + row.installation.status !== "active" || + !row.binding.webhookEnabled + ) { + return { skipped: true }; + } + + const commitSha = await step.run({ name: "resolve-target-commit" }, async () => + resolveTargetCommitSha(row, input.commitSha) + ); + + if (row.binding.lastSyncedCommitSha === commitSha) { + if (input.deliveryId) { + await step.run({ name: "mark-webhook-duplicate" }, async () => + markWebhookDuplicate(row.connector.id, row.connector.provider, input.deliveryId!) + ); + } + return { skipped: true, commitSha }; + } + + if (!row.binding.lastSyncedCommitSha) { + const snapshot = await step.run({ name: "load-provider-snapshot" }, async () => + loadSnapshot(row, commitSha) + ); + if (snapshot.files.length === 0) { + await step.run({ name: "mark-empty-binding-synced" }, async () => + markBindingSynced(row.binding.id, commitSha) + ); + return { commitSha, fileCount: 0 }; + } + + const created = await step.run({ name: "commit-external-files" }, async () => + insertRepositoryFiles(row, snapshot.files, commitSha) + ); + if (created.processRunStatus !== "completed") { + try { + await step.runWorkflow(processFilesSpec, { + graphId: row.binding.graphId, + fileIds: created.fileIds, + processRunId: created.processRunId, + code: { kind: "repository", retiredFileIds: [] }, + }); + } catch (error) { + await Promise.all( + created.fileIds.map((fileId) => + step.runWorkflow(deleteFileSpec, { + graphId: row.binding.graphId, + fileId, + }) + ) + ); + throw error; + } + } + + await step.run({ name: "mark-binding-synced" }, async () => markBindingSynced(row.binding.id, commitSha)); + return { commitSha, fileCount: created.fileIds.length }; + } + + const activeFileRows = await step.run({ name: "load-active-binding-files" }, async () => + loadActiveBindingFiles(row.binding.id) + ); + const activeFiles = activeFilesByPath(activeFileRows); + const delta = await step.run({ name: "compare-provider-commits" }, async () => + compareProviderCommits(row, row.binding.lastSyncedCommitSha!, commitSha) + ); + if (!delta.isIncremental) { + const snapshot = await step.run({ name: "load-provider-snapshot" }, async () => + loadSnapshot(row, commitSha) + ); + const retiredFileIds = [...activeFiles.values()].map((file) => file.id); + assertBindingSnapshotLimits(activeFiles, retiredFileIds, snapshot.files); + + if (snapshot.files.length > 0) { + const created = await step.run({ name: "commit-external-files" }, async () => + insertRepositoryFiles(row, snapshot.files, commitSha) + ); + if (created.processRunStatus !== "completed") { + try { + await step.runWorkflow(processFilesSpec, { + graphId: row.binding.graphId, + fileIds: created.fileIds, + processRunId: created.processRunId, + code: { kind: "repository", retiredFileIds }, + }); + } catch (error) { + await Promise.all( + created.fileIds.map((fileId) => + step.runWorkflow(deleteFileSpec, { + graphId: row.binding.graphId, + fileId, + }) + ) + ); + throw error; + } + } + + await step.run({ name: "mark-binding-synced" }, async () => + markBindingSynced(row.binding.id, commitSha) + ); + return { commitSha, fileCount: created.fileIds.length }; + } + + if (retiredFileIds.length > 0) { + await step.runWorkflow(processFilesSpec, { + graphId: row.binding.graphId, + fileIds: [], + code: { kind: "repository", retiredFileIds }, + }); + } + await step.run({ name: "mark-binding-synced" }, async () => markBindingSynced(row.binding.id, commitSha)); + return { commitSha, fileCount: 0 }; + } + const plan = planIncrementalChanges(activeFiles, delta.changes); + if (plan.newPaths.length === 0 && plan.retiredFileIds.length === 0) { + await step.run({ name: "mark-binding-synced" }, async () => markBindingSynced(row.binding.id, commitSha)); + return { commitSha, fileCount: 0 }; + } + + const changedFiles = + plan.newPaths.length > 0 + ? await step.run({ name: "load-changed-files" }, async () => + loadChangedFiles(row, commitSha, plan.newPaths) + ) + : []; + assertBindingSnapshotLimits(activeFiles, plan.retiredFileIds, changedFiles); + + if (changedFiles.length > 0) { + const created = await step.run({ name: "commit-external-files" }, async () => + insertRepositoryFiles(row, changedFiles, commitSha) + ); + if (created.processRunStatus !== "completed") { + try { + await step.runWorkflow(processFilesSpec, { + graphId: row.binding.graphId, + fileIds: created.fileIds, + processRunId: created.processRunId, + code: { kind: "repository", retiredFileIds: plan.retiredFileIds }, + }); + } catch (error) { + await Promise.all( + created.fileIds.map((fileId) => + step.runWorkflow(deleteFileSpec, { + graphId: row.binding.graphId, + fileId, + }) + ) + ); + throw error; + } + } + await step.run({ name: "mark-binding-synced" }, async () => markBindingSynced(row.binding.id, commitSha)); + return { commitSha, fileCount: created.fileIds.length }; + } + + await step.runWorkflow(processFilesSpec, { + graphId: row.binding.graphId, + fileIds: [], + code: { kind: "repository", retiredFileIds: plan.retiredFileIds }, + }); + await step.run({ name: "mark-binding-synced" }, async () => markBindingSynced(row.binding.id, commitSha)); + return { commitSha, fileCount: 0 }; + } catch (error) { + if (run.retryTerminal) { + await step.run({ name: "mark-binding-failed", retryPolicy: NO_RETRY }, async () => + markBindingFailed(input.bindingId) + ); + } + + throw error; + } +}); diff --git a/bun.lock b/bun.lock index b1541df6..c17292a8 100644 --- a/bun.lock +++ b/bun.lock @@ -25,6 +25,7 @@ "@elysiajs/cors": "^1.4.2", "@kiwi/ai": "workspace:*", "@kiwi/auth": "workspace:*", + "@kiwi/connectors": "workspace:*", "@kiwi/contracts": "workspace:*", "@kiwi/db": "workspace:*", "@kiwi/files": "workspace:*", @@ -126,6 +127,7 @@ "name": "@kiwi/worker", "dependencies": { "@kiwi/ai": "workspace:*", + "@kiwi/connectors": "workspace:*", "@kiwi/contracts": "workspace:*", "@kiwi/db": "workspace:*", "@kiwi/files": "workspace:*", @@ -191,6 +193,15 @@ "typescript": "catalog:", }, }, + "packages/connectors": { + "name": "@kiwi/connectors", + "devDependencies": { + "@types/bun": "catalog:", + }, + "peerDependencies": { + "typescript": "catalog:", + }, + }, "packages/contracts": { "name": "@kiwi/contracts", "dependencies": { @@ -241,6 +252,7 @@ "@kiwi/files": "workspace:*", "@libpdf/core": "^0.3.6", "@platformatic/vfs": "^0.4.0", + "@tree-sitter-grammars/tree-sitter-zig": "^1.1.2", "@xmldom/xmldom": "^0.9.10", "ai": "catalog:", "dom-serializer": "^3.1.1", @@ -249,6 +261,11 @@ "js-tiktoken": "^1.0.21", "jszip": "^3.10.1", "pdf-to-img": "^6.1.0", + "tree-sitter": "^0.25.0", + "tree-sitter-c": "^0.24.1", + "tree-sitter-javascript": "^0.25.0", + "tree-sitter-rust": "^0.24.0", + "tree-sitter-typescript": "^0.23.2", "ulid": "^3.0.2", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "catalog:", @@ -278,6 +295,9 @@ }, }, }, + "trustedDependencies": [ + "tree-sitter", + ], "patchedDependencies": { "openworkflow@0.9.0": "patches/openworkflow@0.9.0.patch", }, @@ -613,6 +633,8 @@ "@kiwi/auth": ["@kiwi/auth@workspace:packages/auth"], + "@kiwi/connectors": ["@kiwi/connectors@workspace:packages/connectors"], + "@kiwi/contracts": ["@kiwi/contracts@workspace:packages/contracts"], "@kiwi/db": ["@kiwi/db@workspace:packages/db"], @@ -1239,6 +1261,8 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tree-sitter-grammars/tree-sitter-zig": ["@tree-sitter-grammars/tree-sitter-zig@1.1.2", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-J0L31HZ2isy3F5zb2g5QWQOv2r/pbruQNL9ADhuQv2pn5BQOzxt80WcEJaYXBeuJ8GHxVT42slpCna8k1c8LOw=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@turbo/darwin-64": ["@turbo/darwin-64@2.9.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw=="], @@ -2105,12 +2129,14 @@ "nitro": ["nitro@3.0.260311-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.4", "db0": "^0.3.4", "env-runner": "^0.1.6", "h3": "^2.0.1-rc.16", "hookable": "^6.0.1", "nf3": "^0.3.11", "ocache": "^0.1.2", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.0-rc.8", "srvx": "^0.11.9", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.6" }, "peerDependencies": { "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.59.0", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2", "zephyr-agent": "^0.1.15" }, "optionalPeers": ["dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-0o0fJ9LUh4WKUqJNX012jyieUOtMCnadkNDWr0mHzdraoHpJP/1CGNefjRyZyMXSpoJfwoWdNEZu2iGf35TUvQ=="], - "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-readable-to-web-readable-stream": ["node-readable-to-web-readable-stream@0.4.2", "", {}, "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ=="], "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], @@ -2503,6 +2529,16 @@ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "tree-sitter": ["tree-sitter@0.25.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-PGZZzFW63eElZJDe/b/R/LbsjDDYJa5UEjLZJB59RQsMX+fo0j54fqBPn1MGKav/QNa0JR0zBiVaikYDWCj5KQ=="], + + "tree-sitter-c": ["tree-sitter-c@0.24.1", "", { "dependencies": { "node-addon-api": "^8.3.1", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.4" }, "optionalPeers": ["tree-sitter"] }, "sha512-lkYwWN3SRecpvaeqmFKkuPNR3ZbtnvHU+4XAEEkJdrp3JfSp2pBrhXOtvfsENUneye76g889Y0ddF2DM0gEDpA=="], + + "tree-sitter-javascript": ["tree-sitter-javascript@0.25.0", "", { "dependencies": { "node-addon-api": "^8.3.1", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-1fCbmzAskZkxcZzN41sFZ2br2iqTYP3tKls1b/HKGNPQUVOpsUxpmGxdN/wMqAk3jYZnYBR1dd/y/0avMeU7dw=="], + + "tree-sitter-rust": ["tree-sitter-rust@0.24.0", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-NWemUDf629Tfc90Y0Z55zuwPCAHkLxWnMf2RznYu4iBkkrQl2o/CHGB7Cr52TyN5F1DAx8FmUnDtCy9iUkXZEQ=="], + + "tree-sitter-typescript": ["tree-sitter-typescript@0.23.2", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2", "tree-sitter-javascript": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -2693,6 +2729,8 @@ "@openworkflow/dashboard/react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "@reduxjs/toolkit/immer": ["immer@11.1.6", "", {}, "sha512-uwrF08UBQfxk49i9WcUeCx045wjB1zXEHNJmbYHPVVspxmjwSeWCoKbB8DEIvs3XkBJV6lcRAyLaWJ2+u3MMCw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], @@ -2819,6 +2857,8 @@ "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "tree-sitter-typescript/tree-sitter-javascript": ["tree-sitter-javascript@0.23.1", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA=="], + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], diff --git a/migrations/20260613184716_sturdy_triton/migration.sql b/migrations/20260613184716_sturdy_triton/migration.sql new file mode 100644 index 00000000..022e6a8f --- /dev/null +++ b/migrations/20260613184716_sturdy_triton/migration.sql @@ -0,0 +1,14 @@ +ALTER TABLE "relationships" ADD COLUMN IF NOT EXISTS "kind" text DEFAULT 'RELATED' NOT NULL; +ALTER TABLE "relationships" ADD COLUMN IF NOT EXISTS "directed" boolean DEFAULT false NOT NULL; + +INSERT INTO "file_type_configs" ("id", "organization_id", "file_type", "loader", "chunker", "chunk_size", "document_mode") +SELECT + 'ftc_' || "organization"."id" || '_code', + "organization"."id", + 'code', + 'text', + 'semantic', + 2000, + NULL +FROM "organization" +ON CONFLICT ("organization_id", "file_type") DO NOTHING; diff --git a/migrations/20260613184716_sturdy_triton/snapshot.json b/migrations/20260613184716_sturdy_triton/snapshot.json new file mode 100644 index 00000000..6b88b345 --- /dev/null +++ b/migrations/20260613184716_sturdy_triton/snapshot.json @@ -0,0 +1,6823 @@ +{ + "id": "9da60ec1-81fc-46b4-be3e-0b3b2debf5f1", + "prevIds": [ + "b9a9f2fb-03a6-4d2b-a459-a922887c2436" + ], + "version": "8", + "dialect": "postgres", + "ddl": [ + { + "isRlsEnabled": true, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apikey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "team_member_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "teamMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "chats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "messages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "file_type_configs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "entities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graphs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_updates", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_run_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "relationships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "text_units", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "models", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_suggestions", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_interval", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_amount", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_refill_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_time_window", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_max", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "remaining", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviterId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "systemRoleProvisioned", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "impersonatedBy", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeOrganizationId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeTeamId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_member_id", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banned", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banReason", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banExpires", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'graph'", + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pinned_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archived_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parts", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokens_per_second", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(name, '')), 'A') || setweight(to_tsvector('simple', coalesce(description, '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mime_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_key", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checksum", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "deleted", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'processing'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "process_step", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_error_code", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'ready'", + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hidden", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_type", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_message", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_run_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completed_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "total_time", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "files", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "file_sizes", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'unknown'", + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'RELATED'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "directed", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relationship_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text_unit_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "source_chunk_ids", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "end_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "chunks", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_name", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adapter", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_model", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "250000", + "generated": null, + "identity": null, + "name": "context_window", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_default", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggestion", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggested_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_prompts_organization_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_prompts_team_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "team_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_prompts_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "title", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "chats_title_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_id_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_role_status_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "file_type_configs_organization_file_type_unique", + "entityType": "indexes", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "entities_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "entities_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "checksum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deleted\" = false AND \"checksum\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_checksum_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "files_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_created_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_key_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_prompts_graph_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_team_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_user_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_graph_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "graphs_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"team_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_organization_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_team_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_run_files_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_runs_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_source_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "target_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_target_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "relationships_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "relationships_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_entity_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relationship_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_relationship_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "text_unit_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_text_unit_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "sources_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "sources_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "text_units_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_model_id_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"is_default\" = true", + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_default_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_source_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_entity_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "invitation_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "inviterId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviterId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_prompts_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "impersonatedBy" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_impersonatedBy_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeOrganizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeOrganizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeTeamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeTeamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "team_member_id" + ], + "schemaTo": "public", + "tableTo": "teamMember", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_member_roles_team_member_id_teamMember_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_member_roles" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_prompts_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "user_prompts_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "project_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_project_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "messages_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "file_type_configs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "entities_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "files_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_prompts_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + "team_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id", + "organizationId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_organization_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_updates_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_updates" + }, + { + "nameExplicit": false, + "columns": [ + "process_run_id" + ], + "schemaTo": "public", + "tableTo": "process_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_process_run_id_process_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_runs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_source_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "target_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_target_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "relationship_id" + ], + "schemaTo": "public", + "tableTo": "relationships", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_relationship_id_relationships_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "text_unit_id" + ], + "schemaTo": "public", + "tableTo": "text_units", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_text_unit_id_text_units_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "text_units_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "models_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "suggested_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_suggested_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "message_id" + ], + "schemaTo": "public", + "tableTo": "messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_message_id_messages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "columns": [ + "process_run_id", + "file_id" + ], + "nameExplicit": true, + "name": "process_run_files_pk", + "entityType": "pks", + "schema": "public", + "table": "process_run_files" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apikey_pkey", + "schema": "public", + "table": "apikey", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_prompts_pkey", + "schema": "public", + "table": "organization_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "team_member_id" + ], + "nameExplicit": false, + "name": "team_member_roles_pkey", + "schema": "public", + "table": "team_member_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "teamMember_pkey", + "schema": "public", + "table": "teamMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_prompts_pkey", + "schema": "public", + "table": "team_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_pkey", + "schema": "public", + "table": "team", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_prompts_pkey", + "schema": "public", + "table": "user_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "chats_pkey", + "schema": "public", + "table": "chats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "messages_pkey", + "schema": "public", + "table": "messages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "file_type_configs_pkey", + "schema": "public", + "table": "file_type_configs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "entities_pkey", + "schema": "public", + "table": "entities", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "files_pkey", + "schema": "public", + "table": "files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_prompts_pkey", + "schema": "public", + "table": "graph_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graphs_pkey", + "schema": "public", + "table": "graphs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_updates_pkey", + "schema": "public", + "table": "graph_updates", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_runs_pkey", + "schema": "public", + "table": "process_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_stats_pkey", + "schema": "public", + "table": "process_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "relationships_pkey", + "schema": "public", + "table": "relationships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sources_pkey", + "schema": "public", + "table": "sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "text_units_pkey", + "schema": "public", + "table": "text_units", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "models_pkey", + "schema": "public", + "table": "models", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_suggestions_pkey", + "schema": "public", + "table": "graph_suggestions", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "id", + "organizationId" + ], + "nullsNotDistinct": false, + "name": "team_id_organization_unique", + "entityType": "uniques", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "id" + ], + "nullsNotDistinct": false, + "name": "apikey_id_key", + "schema": "public", + "table": "apikey", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\n (\n \"scope\" = 'graph'\n AND \"project_id\" IS NOT NULL\n AND \"team_id\" IS NULL\n )\n OR\n (\n \"scope\" = 'team'\n AND \"project_id\" IS NULL\n AND \"team_id\" IS NOT NULL\n )\n ", + "name": "chats_scope_target_check", + "entityType": "checks", + "schema": "public", + "table": "chats" + }, + { + "value": "jsonb_typeof(\"parts\") = 'array'", + "name": "chat_messages_parts_array_check", + "entityType": "checks", + "schema": "public", + "table": "messages" + }, + { + "value": "(((\"organization_id\" IS NOT NULL)::int + (\"user_id\" IS NOT NULL)::int + (\"graph_id\" IS NOT NULL)::int) = 1)", + "name": "graphs_single_owner_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "\"team_id\" IS NULL OR \"organization_id\" IS NOT NULL", + "name": "graphs_team_requires_organization_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "((\"start_page\" IS NULL AND \"end_page\" IS NULL) OR (\"start_page\" IS NOT NULL AND \"end_page\" IS NOT NULL AND \"start_page\" >= 1 AND \"end_page\" >= \"start_page\"))", + "name": "text_units_page_span_check", + "entityType": "checks", + "schema": "public", + "table": "text_units" + }, + { + "value": "\n (\n \"kind\" = 'source_correction'\n AND \"source_id\" IS NOT NULL\n AND \"entity_id\" IS NULL\n )\n OR\n (\n \"kind\" = 'entity_addition'\n AND \"source_id\" IS NULL\n AND \"entity_id\" IS NOT NULL\n )\n ", + "name": "graph_suggestions_target_check", + "entityType": "checks", + "schema": "public", + "table": "graph_suggestions" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/migrations/20260613201908_mature_emma_frost/migration.sql b/migrations/20260613201908_mature_emma_frost/migration.sql new file mode 100644 index 00000000..bdc2ab4f --- /dev/null +++ b/migrations/20260613201908_mature_emma_frost/migration.sql @@ -0,0 +1,20 @@ +ALTER TABLE "files" ADD COLUMN "storage_kind" text DEFAULT 'internal' NOT NULL; +ALTER TABLE "files" ADD COLUMN "external_url" text; +ALTER TABLE "files" ADD COLUMN "external_provider" text; + +ALTER TABLE "files" ADD CONSTRAINT "files_storage_origin_check" CHECK ( + ( + "storage_kind" = 'internal' + AND "external_url" IS NULL + AND "external_provider" IS NULL + ) + OR ( + "storage_kind" = 'external' + AND "external_url" IS NOT NULL + AND "external_provider" IS NOT NULL + ) +); + +ALTER TABLE "files" ADD CONSTRAINT "files_external_provider_check" CHECK ( + "external_provider" IS NULL OR "external_provider" = 'github' +); \ No newline at end of file diff --git a/migrations/20260613201908_mature_emma_frost/snapshot.json b/migrations/20260613201908_mature_emma_frost/snapshot.json new file mode 100644 index 00000000..bf0f9b39 --- /dev/null +++ b/migrations/20260613201908_mature_emma_frost/snapshot.json @@ -0,0 +1,6876 @@ +{ + "id": "e7424156-436b-42ec-adee-bdb5dd5d397a", + "prevIds": [ + "9da60ec1-81fc-46b4-be3e-0b3b2debf5f1" + ], + "version": "8", + "dialect": "postgres", + "ddl": [ + { + "isRlsEnabled": true, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apikey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "team_member_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "teamMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "chats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "messages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "file_type_configs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "entities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graphs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_updates", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_run_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "relationships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "text_units", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "models", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_suggestions", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_interval", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_amount", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_refill_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_time_window", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_max", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "remaining", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviterId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "systemRoleProvisioned", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "impersonatedBy", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeOrganizationId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeTeamId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_member_id", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banned", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banReason", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banExpires", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'graph'", + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pinned_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archived_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parts", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokens_per_second", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(name, '')), 'A') || setweight(to_tsvector('simple', coalesce(description, '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mime_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_key", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'internal'", + "generated": null, + "identity": null, + "name": "storage_kind", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_url", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_provider", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checksum", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "deleted", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'processing'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "process_step", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_error_code", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'ready'", + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hidden", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_type", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_message", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_run_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completed_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "total_time", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "files", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "file_sizes", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'unknown'", + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'RELATED'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "directed", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relationship_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text_unit_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "source_chunk_ids", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "end_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "chunks", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_name", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adapter", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_model", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "250000", + "generated": null, + "identity": null, + "name": "context_window", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_default", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggestion", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggested_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_prompts_organization_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_prompts_team_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "team_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_prompts_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "title", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "chats_title_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_id_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_role_status_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "file_type_configs_organization_file_type_unique", + "entityType": "indexes", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "entities_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "entities_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "checksum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deleted\" = false AND \"checksum\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_checksum_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "files_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_created_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_key_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_prompts_graph_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_team_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_user_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_graph_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "graphs_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"team_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_organization_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_team_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_run_files_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_runs_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_source_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "target_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_target_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "relationships_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "relationships_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_entity_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relationship_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_relationship_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "text_unit_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_text_unit_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "sources_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "sources_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "text_units_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_model_id_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"is_default\" = true", + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_default_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_source_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_entity_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "invitation_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "inviterId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviterId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_prompts_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "impersonatedBy" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_impersonatedBy_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeOrganizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeOrganizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeTeamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeTeamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "team_member_id" + ], + "schemaTo": "public", + "tableTo": "teamMember", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_member_roles_team_member_id_teamMember_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_member_roles" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_prompts_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "user_prompts_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "project_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_project_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "messages_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "file_type_configs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "entities_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "files_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_prompts_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + "team_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id", + "organizationId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_organization_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_updates_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_updates" + }, + { + "nameExplicit": false, + "columns": [ + "process_run_id" + ], + "schemaTo": "public", + "tableTo": "process_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_process_run_id_process_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_runs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_source_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "target_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_target_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "relationship_id" + ], + "schemaTo": "public", + "tableTo": "relationships", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_relationship_id_relationships_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "text_unit_id" + ], + "schemaTo": "public", + "tableTo": "text_units", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_text_unit_id_text_units_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "text_units_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "models_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "suggested_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_suggested_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "message_id" + ], + "schemaTo": "public", + "tableTo": "messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_message_id_messages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "columns": [ + "process_run_id", + "file_id" + ], + "nameExplicit": true, + "name": "process_run_files_pk", + "entityType": "pks", + "schema": "public", + "table": "process_run_files" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apikey_pkey", + "schema": "public", + "table": "apikey", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_prompts_pkey", + "schema": "public", + "table": "organization_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "team_member_id" + ], + "nameExplicit": false, + "name": "team_member_roles_pkey", + "schema": "public", + "table": "team_member_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "teamMember_pkey", + "schema": "public", + "table": "teamMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_prompts_pkey", + "schema": "public", + "table": "team_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_pkey", + "schema": "public", + "table": "team", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_prompts_pkey", + "schema": "public", + "table": "user_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "chats_pkey", + "schema": "public", + "table": "chats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "messages_pkey", + "schema": "public", + "table": "messages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "file_type_configs_pkey", + "schema": "public", + "table": "file_type_configs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "entities_pkey", + "schema": "public", + "table": "entities", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "files_pkey", + "schema": "public", + "table": "files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_prompts_pkey", + "schema": "public", + "table": "graph_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graphs_pkey", + "schema": "public", + "table": "graphs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_updates_pkey", + "schema": "public", + "table": "graph_updates", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_runs_pkey", + "schema": "public", + "table": "process_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_stats_pkey", + "schema": "public", + "table": "process_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "relationships_pkey", + "schema": "public", + "table": "relationships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sources_pkey", + "schema": "public", + "table": "sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "text_units_pkey", + "schema": "public", + "table": "text_units", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "models_pkey", + "schema": "public", + "table": "models", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_suggestions_pkey", + "schema": "public", + "table": "graph_suggestions", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "id", + "organizationId" + ], + "nullsNotDistinct": false, + "name": "team_id_organization_unique", + "entityType": "uniques", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "id" + ], + "nullsNotDistinct": false, + "name": "apikey_id_key", + "schema": "public", + "table": "apikey", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\n (\n \"scope\" = 'graph'\n AND \"project_id\" IS NOT NULL\n AND \"team_id\" IS NULL\n )\n OR\n (\n \"scope\" = 'team'\n AND \"project_id\" IS NULL\n AND \"team_id\" IS NOT NULL\n )\n ", + "name": "chats_scope_target_check", + "entityType": "checks", + "schema": "public", + "table": "chats" + }, + { + "value": "jsonb_typeof(\"parts\") = 'array'", + "name": "chat_messages_parts_array_check", + "entityType": "checks", + "schema": "public", + "table": "messages" + }, + { + "value": "(((\"organization_id\" IS NOT NULL)::int + (\"user_id\" IS NOT NULL)::int + (\"graph_id\" IS NOT NULL)::int) = 1)", + "name": "graphs_single_owner_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "\"team_id\" IS NULL OR \"organization_id\" IS NOT NULL", + "name": "graphs_team_requires_organization_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "((\"start_page\" IS NULL AND \"end_page\" IS NULL) OR (\"start_page\" IS NOT NULL AND \"end_page\" IS NOT NULL AND \"start_page\" >= 1 AND \"end_page\" >= \"start_page\"))", + "name": "text_units_page_span_check", + "entityType": "checks", + "schema": "public", + "table": "text_units" + }, + { + "value": "\n (\n \"kind\" = 'source_correction'\n AND \"source_id\" IS NOT NULL\n AND \"entity_id\" IS NULL\n )\n OR\n (\n \"kind\" = 'entity_addition'\n AND \"source_id\" IS NULL\n AND \"entity_id\" IS NOT NULL\n )\n ", + "name": "graph_suggestions_target_check", + "entityType": "checks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "value": "\n (\n \"storage_kind\" = 'internal'\n AND \"external_url\" IS NULL\n AND \"external_provider\" IS NULL\n )\n OR (\n \"storage_kind\" = 'external'\n AND \"external_url\" IS NOT NULL\n AND \"external_provider\" IS NOT NULL\n )\n ", + "name": "files_storage_origin_check", + "entityType": "checks", + "schema": "public", + "table": "files" + }, + { + "value": "\"external_provider\" IS NULL OR \"external_provider\" = 'github'", + "name": "files_external_provider_check", + "entityType": "checks", + "schema": "public", + "table": "files" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/migrations/20260614105716_tricky_plazm/migration.sql b/migrations/20260614105716_tricky_plazm/migration.sql new file mode 100644 index 00000000..5e39488b --- /dev/null +++ b/migrations/20260614105716_tricky_plazm/migration.sql @@ -0,0 +1,116 @@ +ALTER TABLE "sources" ADD COLUMN IF NOT EXISTS "valid_until" timestamp with time zone; + +CREATE INDEX IF NOT EXISTS "sources_current_id_idx" + ON "sources" ("id") + WHERE "active" = true AND "valid_until" IS NULL; + +CREATE INDEX IF NOT EXISTS "sources_entity_current_id_idx" + ON "sources" ("entity_id", "id") + WHERE "active" = true AND "valid_until" IS NULL AND "entity_id" IS NOT NULL; + +CREATE INDEX IF NOT EXISTS "sources_relationship_current_id_idx" + ON "sources" ("relationship_id", "id") + WHERE "active" = true AND "valid_until" IS NULL AND "relationship_id" IS NOT NULL; + +CREATE TABLE IF NOT EXISTS "connectors" ( + "id" text PRIMARY KEY NOT NULL, + "provider" text NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "app_id" text, + "app_slug" text, + "client_id" text, + "encrypted_credentials" text NOT NULL, + "webhook_secret_encrypted" text NOT NULL, + "created_by_user_id" text, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "connectors_slug_unique" UNIQUE("slug"), + CONSTRAINT "connectors_created_by_user_id_user_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."user"("id") ON DELETE set null, + CONSTRAINT "connectors_provider_check" CHECK ("connectors"."provider" in ('github', 'gitlab')), + CONSTRAINT "connectors_status_check" CHECK ("connectors"."status" in ('draft', 'active', 'disabled')) +); + +CREATE TABLE IF NOT EXISTS "connector_installations" ( + "id" text PRIMARY KEY NOT NULL, + "connector_id" text NOT NULL, + "provider" text NOT NULL, + "provider_installation_id" text NOT NULL, + "provider_account_login" text NOT NULL, + "provider_account_type" text, + "organization_id" text, + "team_id" text, + "installed_by_user_id" text, + "encrypted_credentials" text, + "repository_selection" text DEFAULT 'unknown' NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "connector_installations_connector_id_connectors_id_fk" FOREIGN KEY ("connector_id") REFERENCES "public"."connectors"("id") ON DELETE cascade, + CONSTRAINT "connector_installations_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade, + CONSTRAINT "connector_installations_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade, + CONSTRAINT "connector_installations_installed_by_user_id_user_id_fk" FOREIGN KEY ("installed_by_user_id") REFERENCES "public"."user"("id") ON DELETE set null, + CONSTRAINT "connector_installations_provider_check" CHECK ("connector_installations"."provider" in ('github', 'gitlab')), + CONSTRAINT "connector_installations_status_check" CHECK ("connector_installations"."status" in ('active', 'disabled')), + CONSTRAINT "connector_installations_owner_scope_check" CHECK (("organization_id" is not null and "team_id" is null) or ("organization_id" is not null and "team_id" is not null)) +); + +CREATE TABLE IF NOT EXISTS "repository_graph_bindings" ( + "id" text PRIMARY KEY NOT NULL, + "graph_id" text NOT NULL, + "connector_installation_id" text NOT NULL, + "provider" text NOT NULL, + "provider_repository_id" text NOT NULL, + "repository_full_name" text NOT NULL, + "repository_html_url" text NOT NULL, + "branch" text NOT NULL, + "last_seen_commit_sha" text, + "last_synced_commit_sha" text, + "sync_status" text DEFAULT 'pending' NOT NULL, + "sync_error_code" text, + "webhook_enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "repository_graph_bindings_graph_id_graphs_id_fk" FOREIGN KEY ("graph_id") REFERENCES "public"."graphs"("id") ON DELETE cascade, + CONSTRAINT "repository_graph_bindings_connector_installation_id_fk" FOREIGN KEY ("connector_installation_id") REFERENCES "public"."connector_installations"("id") ON DELETE restrict, + CONSTRAINT "repository_graph_bindings_provider_check" CHECK ("repository_graph_bindings"."provider" in ('github', 'gitlab')), + CONSTRAINT "repository_graph_bindings_sync_status_check" CHECK ("repository_graph_bindings"."sync_status" in ('pending', 'syncing', 'synced', 'failed')) +); + +CREATE TABLE IF NOT EXISTS "connector_webhook_events" ( + "id" text PRIMARY KEY NOT NULL, + "connector_id" text NOT NULL, + "provider" text NOT NULL, + "delivery_id" text NOT NULL, + "event_name" text NOT NULL, + "provider_repository_id" text, + "branch" text, + "commit_sha" text, + "status" text NOT NULL, + "error_code" text, + "created_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "connector_webhook_events_connector_id_connectors_id_fk" FOREIGN KEY ("connector_id") REFERENCES "public"."connectors"("id") ON DELETE cascade, + CONSTRAINT "connector_webhook_events_provider_check" CHECK ("connector_webhook_events"."provider" in ('github', 'gitlab')), + CONSTRAINT "connector_webhook_events_status_check" CHECK ("connector_webhook_events"."status" in ('ignored', 'enqueued', 'duplicate', 'failed')) +); + +ALTER TABLE "files" ADD COLUMN IF NOT EXISTS "repository_binding_id" text; +ALTER TABLE "files" DROP CONSTRAINT IF EXISTS "files_external_provider_check"; +ALTER TABLE "files" ADD CONSTRAINT "files_external_provider_check" CHECK ("external_provider" IS NULL OR "external_provider" in ('github', 'gitlab')); +ALTER TABLE "files" DROP CONSTRAINT IF EXISTS "files_repository_binding_id_repository_graph_bindings_id_fk"; +ALTER TABLE "files" ADD CONSTRAINT "files_repository_binding_id_repository_graph_bindings_id_fk" FOREIGN KEY ("repository_binding_id") REFERENCES "public"."repository_graph_bindings"("id") ON DELETE set null; + +CREATE INDEX IF NOT EXISTS "connectors_provider_status_idx" ON "connectors" ("provider", "status"); +CREATE UNIQUE INDEX IF NOT EXISTS "connector_installations_org_scope_unique" ON "connector_installations" ("connector_id", "provider_installation_id", "organization_id") WHERE "team_id" IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS "connector_installations_team_scope_unique" ON "connector_installations" ("connector_id", "provider_installation_id", "organization_id", "team_id") WHERE "team_id" IS NOT NULL; +CREATE INDEX IF NOT EXISTS "connector_installations_connector_status_idx" ON "connector_installations" ("connector_id", "status"); +CREATE INDEX IF NOT EXISTS "connector_installations_organization_idx" ON "connector_installations" ("organization_id"); +CREATE INDEX IF NOT EXISTS "connector_installations_team_idx" ON "connector_installations" ("team_id"); +CREATE UNIQUE INDEX IF NOT EXISTS "repository_graph_bindings_graph_unique" ON "repository_graph_bindings" ("graph_id"); +CREATE UNIQUE INDEX IF NOT EXISTS "repository_graph_bindings_repository_branch_unique" ON "repository_graph_bindings" ("connector_installation_id", "provider_repository_id", "branch"); +CREATE INDEX IF NOT EXISTS "repository_graph_bindings_provider_repo_branch_idx" ON "repository_graph_bindings" ("provider", "provider_repository_id", "branch"); +CREATE INDEX IF NOT EXISTS "repository_graph_bindings_installation_status_idx" ON "repository_graph_bindings" ("connector_installation_id", "sync_status"); +CREATE UNIQUE INDEX IF NOT EXISTS "connector_webhook_events_delivery_unique" ON "connector_webhook_events" ("connector_id", "provider", "delivery_id"); +CREATE INDEX IF NOT EXISTS "connector_webhook_events_binding_lookup_idx" ON "connector_webhook_events" ("provider", "provider_repository_id", "branch"); +CREATE INDEX IF NOT EXISTS "files_repository_binding_active_idx" ON "files" ("repository_binding_id", "created_at", "id") WHERE "deleted" = false; diff --git a/migrations/20260614105716_tricky_plazm/snapshot.json b/migrations/20260614105716_tricky_plazm/snapshot.json new file mode 100644 index 00000000..7fdfb6e6 --- /dev/null +++ b/migrations/20260614105716_tricky_plazm/snapshot.json @@ -0,0 +1,8368 @@ +{ + "id": "9b6b85c6-0014-4a15-bb55-fd27e7e3b3d2", + "prevIds": [ + "e7424156-436b-42ec-adee-bdb5dd5d397a" + ], + "version": "8", + "dialect": "postgres", + "ddl": [ + { + "isRlsEnabled": true, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apikey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "team_member_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "teamMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "chats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "messages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "file_type_configs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "entities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graphs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_updates", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_run_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "relationships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "text_units", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "models", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_suggestions", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_interval", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_amount", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_refill_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_time_window", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_max", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "remaining", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviterId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "systemRoleProvisioned", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "impersonatedBy", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeOrganizationId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeTeamId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_member_id", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banned", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banReason", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banExpires", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'graph'", + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pinned_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archived_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parts", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokens_per_second", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(name, '')), 'A') || setweight(to_tsvector('simple', coalesce(description, '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mime_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_key", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'internal'", + "generated": null, + "identity": null, + "name": "storage_kind", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_url", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_provider", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checksum", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "deleted", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'processing'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "process_step", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_error_code", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'ready'", + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hidden", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_type", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_message", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_run_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completed_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "total_time", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "files", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "file_sizes", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'unknown'", + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'RELATED'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "directed", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relationship_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text_unit_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "valid_until", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "source_chunk_ids", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "end_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "chunks", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_name", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adapter", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_model", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "250000", + "generated": null, + "identity": null, + "name": "context_window", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_default", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggestion", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggested_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_prompts_organization_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_prompts_team_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "team_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_prompts_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "title", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "chats_title_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_id_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_role_status_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "file_type_configs_organization_file_type_unique", + "entityType": "indexes", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "entities_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "entities_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "checksum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deleted\" = false AND \"checksum\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_checksum_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "files_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_created_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_key_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_prompts_graph_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_team_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_user_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_graph_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "graphs_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"team_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_organization_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_team_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_run_files_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_runs_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_source_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "target_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_target_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "relationships_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "relationships_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_entity_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relationship_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_relationship_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"active\" = true AND \"valid_until\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_current_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"active\" = true AND \"valid_until\" IS NULL AND \"entity_id\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_entity_current_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relationship_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"active\" = true AND \"valid_until\" IS NULL AND \"relationship_id\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_relationship_current_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "text_unit_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_text_unit_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "sources_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "sources_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "text_units_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_model_id_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"is_default\" = true", + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_default_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_source_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_entity_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "invitation_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "inviterId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviterId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_prompts_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "impersonatedBy" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_impersonatedBy_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeOrganizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeOrganizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeTeamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeTeamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "team_member_id" + ], + "schemaTo": "public", + "tableTo": "teamMember", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_member_roles_team_member_id_teamMember_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_member_roles" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_prompts_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "user_prompts_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "project_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_project_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "messages_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "file_type_configs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "entities_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "files_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_prompts_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + "team_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id", + "organizationId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_organization_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_updates_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_updates" + }, + { + "nameExplicit": false, + "columns": [ + "process_run_id" + ], + "schemaTo": "public", + "tableTo": "process_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_process_run_id_process_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_runs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_source_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "target_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_target_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "relationship_id" + ], + "schemaTo": "public", + "tableTo": "relationships", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_relationship_id_relationships_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "text_unit_id" + ], + "schemaTo": "public", + "tableTo": "text_units", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_text_unit_id_text_units_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "text_units_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "models_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "suggested_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_suggested_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "message_id" + ], + "schemaTo": "public", + "tableTo": "messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_message_id_messages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "columns": [ + "process_run_id", + "file_id" + ], + "nameExplicit": true, + "name": "process_run_files_pk", + "entityType": "pks", + "schema": "public", + "table": "process_run_files" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apikey_pkey", + "schema": "public", + "table": "apikey", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_prompts_pkey", + "schema": "public", + "table": "organization_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "team_member_id" + ], + "nameExplicit": false, + "name": "team_member_roles_pkey", + "schema": "public", + "table": "team_member_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "teamMember_pkey", + "schema": "public", + "table": "teamMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_prompts_pkey", + "schema": "public", + "table": "team_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_pkey", + "schema": "public", + "table": "team", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_prompts_pkey", + "schema": "public", + "table": "user_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "chats_pkey", + "schema": "public", + "table": "chats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "messages_pkey", + "schema": "public", + "table": "messages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "file_type_configs_pkey", + "schema": "public", + "table": "file_type_configs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "entities_pkey", + "schema": "public", + "table": "entities", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "files_pkey", + "schema": "public", + "table": "files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_prompts_pkey", + "schema": "public", + "table": "graph_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graphs_pkey", + "schema": "public", + "table": "graphs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_updates_pkey", + "schema": "public", + "table": "graph_updates", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_runs_pkey", + "schema": "public", + "table": "process_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_stats_pkey", + "schema": "public", + "table": "process_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "relationships_pkey", + "schema": "public", + "table": "relationships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sources_pkey", + "schema": "public", + "table": "sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "text_units_pkey", + "schema": "public", + "table": "text_units", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "models_pkey", + "schema": "public", + "table": "models", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_suggestions_pkey", + "schema": "public", + "table": "graph_suggestions", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "id", + "organizationId" + ], + "nullsNotDistinct": false, + "name": "team_id_organization_unique", + "entityType": "uniques", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "id" + ], + "nullsNotDistinct": false, + "name": "apikey_id_key", + "schema": "public", + "table": "apikey", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\n (\n \"scope\" = 'graph'\n AND \"project_id\" IS NOT NULL\n AND \"team_id\" IS NULL\n )\n OR\n (\n \"scope\" = 'team'\n AND \"project_id\" IS NULL\n AND \"team_id\" IS NOT NULL\n )\n ", + "name": "chats_scope_target_check", + "entityType": "checks", + "schema": "public", + "table": "chats" + }, + { + "value": "jsonb_typeof(\"parts\") = 'array'", + "name": "chat_messages_parts_array_check", + "entityType": "checks", + "schema": "public", + "table": "messages" + }, + { + "value": "(((\"organization_id\" IS NOT NULL)::int + (\"user_id\" IS NOT NULL)::int + (\"graph_id\" IS NOT NULL)::int) = 1)", + "name": "graphs_single_owner_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "\"team_id\" IS NULL OR \"organization_id\" IS NOT NULL", + "name": "graphs_team_requires_organization_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "((\"start_page\" IS NULL AND \"end_page\" IS NULL) OR (\"start_page\" IS NOT NULL AND \"end_page\" IS NOT NULL AND \"start_page\" >= 1 AND \"end_page\" >= \"start_page\"))", + "name": "text_units_page_span_check", + "entityType": "checks", + "schema": "public", + "table": "text_units" + }, + { + "value": "\n (\n \"kind\" = 'source_correction'\n AND \"source_id\" IS NOT NULL\n AND \"entity_id\" IS NULL\n )\n OR\n (\n \"kind\" = 'entity_addition'\n AND \"source_id\" IS NULL\n AND \"entity_id\" IS NOT NULL\n )\n ", + "name": "graph_suggestions_target_check", + "entityType": "checks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "value": "\n (\n \"storage_kind\" = 'internal'\n AND \"external_url\" IS NULL\n AND \"external_provider\" IS NULL\n )\n OR (\n \"storage_kind\" = 'external'\n AND \"external_url\" IS NOT NULL\n AND \"external_provider\" IS NOT NULL\n )\n ", + "name": "files_storage_origin_check", + "entityType": "checks", + "schema": "public", + "table": "files" + }, + { + "isRlsEnabled": true, + "name": "connectors", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "connector_installations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "repository_graph_bindings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "connector_webhook_events", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_slug", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "client_id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webhook_secret_encrypted", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_installation_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_account_login", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_account_type", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installed_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'unknown'", + "generated": null, + "identity": null, + "name": "repository_selection", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_installation_id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_repository_id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "repository_full_name", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "repository_html_url", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "branch", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_seen_commit_sha", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_synced_commit_sha", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "sync_status", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sync_error_code", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "webhook_enabled", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delivery_id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "event_name", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_repository_id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "branch", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commit_sha", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error_code", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "repository_binding_id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "connectors_provider_check", + "entityType": "checks", + "schema": "public", + "table": "connectors" + }, + { + "value": "\"status\" in ('draft', 'active', 'disabled')", + "name": "connectors_status_check", + "entityType": "checks", + "schema": "public", + "table": "connectors" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "connector_installations_provider_check", + "entityType": "checks", + "schema": "public", + "table": "connector_installations" + }, + { + "value": "\"status\" in ('active', 'disabled')", + "name": "connector_installations_status_check", + "entityType": "checks", + "schema": "public", + "table": "connector_installations" + }, + { + "value": "(\"organization_id\" is not null and \"team_id\" is null) or (\"organization_id\" is not null and \"team_id\" is not null)", + "name": "connector_installations_owner_scope_check", + "entityType": "checks", + "schema": "public", + "table": "connector_installations" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "repository_graph_bindings_provider_check", + "entityType": "checks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "value": "\"sync_status\" in ('pending', 'syncing', 'synced', 'failed')", + "name": "repository_graph_bindings_sync_status_check", + "entityType": "checks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "connector_webhook_events_provider_check", + "entityType": "checks", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "value": "\"status\" in ('ignored', 'enqueued', 'duplicate', 'failed')", + "name": "connector_webhook_events_status_check", + "entityType": "checks", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "value": "\"external_provider\" IS NULL OR \"external_provider\" in ('github', 'gitlab')", + "name": "files_external_provider_check", + "entityType": "checks", + "schema": "public", + "table": "files" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "connector_installations_pkey", + "schema": "public", + "table": "connector_installations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "connector_webhook_events_pkey", + "schema": "public", + "table": "connector_webhook_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "connectors_pkey", + "schema": "public", + "table": "connectors", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "repository_graph_bindings_pkey", + "schema": "public", + "table": "repository_graph_bindings", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connectors_slug_unique", + "entityType": "indexes", + "schema": "public", + "table": "connectors" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connectors_provider_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "connectors" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"team_id\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_org_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"team_id\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_team_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_connector_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_graph_unique", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_repository_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "branch", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_repository_branch_unique", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_repository_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "branch", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_provider_repo_branch_idx", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sync_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_installation_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "delivery_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_webhook_events_delivery_unique", + "entityType": "indexes", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_repository_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "branch", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_webhook_events_binding_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "repository_binding_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_repository_binding_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + "created_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "connectors_created_by_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connectors" + }, + { + "nameExplicit": true, + "columns": [ + "connector_id" + ], + "schemaTo": "public", + "tableTo": "connectors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_installations_connector_id_connectors_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_installations_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_installations_team_id_team_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "installed_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "connector_installations_installed_by_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "repository_graph_bindings_graph_id_graphs_id_fk", + "entityType": "fks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + "connector_installation_id" + ], + "schemaTo": "public", + "tableTo": "connector_installations", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "repository_graph_bindings_connector_installation_id_fk", + "entityType": "fks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + "connector_id" + ], + "schemaTo": "public", + "tableTo": "connectors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_webhook_events_connector_id_connectors_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + "repository_binding_id" + ], + "schemaTo": "public", + "tableTo": "repository_graph_bindings", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "files_repository_binding_id_repository_graph_bindings_id_fk", + "entityType": "fks", + "schema": "public", + "table": "files" + } + ], + "renames": [] +} diff --git a/migrations/20260615094125_fluffy_gladiator/migration.sql b/migrations/20260615094125_fluffy_gladiator/migration.sql new file mode 100644 index 00000000..543d512f --- /dev/null +++ b/migrations/20260615094125_fluffy_gladiator/migration.sql @@ -0,0 +1,2 @@ +DROP INDEX "files_graph_active_key_idx";--> statement-breakpoint +CREATE UNIQUE INDEX "files_graph_active_key_idx" ON "files" ("graph_id","file_key") WHERE "deleted" = false; \ No newline at end of file diff --git a/migrations/20260615094125_fluffy_gladiator/snapshot.json b/migrations/20260615094125_fluffy_gladiator/snapshot.json new file mode 100644 index 00000000..7aac8d98 --- /dev/null +++ b/migrations/20260615094125_fluffy_gladiator/snapshot.json @@ -0,0 +1,8368 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "ab5d8cc3-076f-4287-ad54-18a6f3f26fc8", + "prevIds": [ + "9b6b85c6-0014-4a15-bb55-fd27e7e3b3d2" + ], + "ddl": [ + { + "isRlsEnabled": true, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apikey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "team_member_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "teamMember", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "team", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "chats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "messages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "connector_installations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "connector_webhook_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "connectors", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "repository_graph_bindings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "file_type_configs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "entities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_prompts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graphs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_updates", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_run_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "process_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "relationships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "text_units", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "models", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "graph_suggestions", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accessTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refreshTokenExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idToken", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference_id", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_interval", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refill_amount", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_refill_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_enabled", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_time_window", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rate_limit_max", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "remaining", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "timestamp(6) with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "apikey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviterId", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "systemRoleProvisioned", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organization_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "impersonatedBy", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeOrganizationId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activeTeamId", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_member_id", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_member_roles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "teamId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "teamMember" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "team_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organizationId", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "team" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banned", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banReason", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banExpires", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'graph'", + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pinned_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archived_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "chats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parts", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokens_per_second", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "messages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_installation_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_account_login", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_account_type", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "installed_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'unknown'", + "generated": null, + "identity": null, + "name": "repository_selection", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "connector_installations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delivery_id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "event_name", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_repository_id", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "branch", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "commit_sha", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error_code", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_slug", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "client_id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webhook_secret_encrypted", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "connectors" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_installation_id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_repository_id", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "repository_full_name", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "repository_html_url", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "branch", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_seen_commit_sha", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_synced_commit_sha", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "sync_status", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sync_error_code", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "webhook_enabled", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "file_type_configs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(name, '')), 'A') || setweight(to_tsvector('simple', coalesce(description, '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "entities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mime_type", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_key", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'internal'", + "generated": null, + "identity": null, + "name": "storage_kind", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_url", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_provider", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "repository_binding_id", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checksum", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "deleted", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'processing'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "process_step", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_error_code", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "loader", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunker", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chunk_size", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document_mode", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_prompts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "team_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'ready'", + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hidden", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graphs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_type", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "update_message", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_updates" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "process_run_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_run_files" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completed_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "process_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "total_time", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "files", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "file_sizes", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'unknown'", + "generated": null, + "identity": null, + "name": "file_type", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_count", + "entityType": "columns", + "schema": "public", + "table": "process_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'RELATED'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "directed", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "rank", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relationships" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relationship_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text_unit_id", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "active", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "valid_until", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "source_chunk_ids", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "vector(4096)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "embedding", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(description, '')), 'A')", + "type": "stored" + }, + "identity": null, + "name": "search_tsv", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "sources" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "file_id", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "start_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "end_page", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "chunks", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "text_units" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_name", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "adapter", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_model", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "250000", + "generated": null, + "identity": null, + "name": "context_window", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "encrypted_credentials", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_default", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "graph_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggestion", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "suggested_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_by_user_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_source_id", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "applied_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organization_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_prompts_organization_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "teamId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_member_team_user_unique", + "entityType": "indexes", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_prompts_team_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organizationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "team_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "team_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_prompts_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_project_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"pinned_at\" is null)", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "updated_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archived_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_user_chats_user_team_archived_updated_at", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "title", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "chats_title_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_id_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_chat_messages_chat_role_status_id", + "entityType": "indexes", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"team_id\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_org_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"team_id\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_team_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_connector_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_organization_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_installations_team_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "delivery_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_webhook_events_delivery_unique", + "entityType": "indexes", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_repository_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "branch", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connector_webhook_events_binding_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connectors_slug_unique", + "entityType": "indexes", + "schema": "public", + "table": "connectors" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connectors_provider_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "connectors" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_graph_unique", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_repository_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "branch", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_repository_branch_unique", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_repository_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "branch", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_provider_repo_branch_idx", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_installation_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "sync_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "repository_graph_bindings_installation_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "file_type_configs_organization_file_type_unique", + "entityType": "indexes", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "entities_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "entities_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "entities_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "checksum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deleted\" = false AND \"checksum\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_checksum_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "files_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_created_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "file_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_graph_active_key_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "repository_binding_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"deleted\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "files_repository_binding_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_prompts_graph_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_team_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_user_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_graph_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "graphs_name_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"team_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_organization_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "team_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"graph_id\" IS NULL AND \"hidden\" = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "graphs_visible_root_team_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_run_files_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "process_runs_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_source_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "target_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "relationships_graph_active_target_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "relationships_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "relationships_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_entity_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relationship_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "active", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_relationship_active_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"active\" = true AND \"valid_until\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_current_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"active\" = true AND \"valid_until\" IS NULL AND \"entity_id\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_entity_current_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relationship_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"active\" = true AND \"valid_until\" IS NULL AND \"relationship_id\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_relationship_current_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "text_unit_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sources_text_unit_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "description", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "sources_description_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "embedding", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "vector_cosine_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "diskann", + "concurrently": false, + "name": "sources_embedding_diskann_idx", + "entityType": "indexes", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "file_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "text_units_file_idx", + "entityType": "indexes", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_model_id_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"is_default\" = true", + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_default_unique", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "models_organization_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "graph_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_graph_status_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_source_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "graph_suggestions_entity_idx", + "entityType": "indexes", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "invitation_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "inviterId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviterId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_prompts_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organization_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "impersonatedBy" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_impersonatedBy_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeOrganizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeOrganizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "activeTeamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "session_activeTeamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "team_member_id" + ], + "schemaTo": "public", + "tableTo": "teamMember", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_member_roles_team_member_id_teamMember_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_member_roles" + }, + { + "nameExplicit": false, + "columns": [ + "teamId" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_teamId_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "teamMember_userId_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "teamMember" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_prompts_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organizationId" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "team_organizationId_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "user_prompts_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "user_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "project_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_project_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chats_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "chats" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "messages_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "messages" + }, + { + "nameExplicit": true, + "columns": [ + "connector_id" + ], + "schemaTo": "public", + "tableTo": "connectors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_installations_connector_id_connectors_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_installations_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_installations_team_id_team_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "installed_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "connector_installations_installed_by_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_installations" + }, + { + "nameExplicit": true, + "columns": [ + "connector_id" + ], + "schemaTo": "public", + "tableTo": "connectors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_webhook_events_connector_id_connectors_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + "repository_binding_id" + ], + "schemaTo": "public", + "tableTo": "repository_graph_bindings", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "files_repository_binding_id_repository_graph_bindings_id_fk", + "entityType": "fks", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": true, + "columns": [ + "created_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "connectors_created_by_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "connectors" + }, + { + "nameExplicit": true, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "repository_graph_bindings_graph_id_graphs_id_fk", + "entityType": "fks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": true, + "columns": [ + "connector_installation_id" + ], + "schemaTo": "public", + "tableTo": "connector_installations", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "repository_graph_bindings_connector_installation_id_fk", + "entityType": "fks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "file_type_configs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "file_type_configs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "entities_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "entities" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "files_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_prompts_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_prompts" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "team_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_id_team_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": true, + "columns": [ + "team_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "team", + "columnsTo": [ + "id", + "organizationId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graphs_team_organization_fkey", + "entityType": "fks", + "schema": "public", + "table": "graphs" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_updates_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_updates" + }, + { + "nameExplicit": false, + "columns": [ + "process_run_id" + ], + "schemaTo": "public", + "tableTo": "process_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_process_run_id_process_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_run_files_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_run_files" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "process_runs_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "process_runs" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_source_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "target_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_target_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "relationships_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "relationships" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "relationship_id" + ], + "schemaTo": "public", + "tableTo": "relationships", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_relationship_id_relationships_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "text_unit_id" + ], + "schemaTo": "public", + "tableTo": "text_units", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sources_text_unit_id_text_units_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sources" + }, + { + "nameExplicit": false, + "columns": [ + "file_id" + ], + "schemaTo": "public", + "tableTo": "files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "text_units_file_id_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "text_units" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "models_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "models" + }, + { + "nameExplicit": false, + "columns": [ + "graph_id" + ], + "schemaTo": "public", + "tableTo": "graphs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_graph_id_graphs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "entity_id" + ], + "schemaTo": "public", + "tableTo": "entities", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_entity_id_entities_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "suggested_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "graph_suggestions_suggested_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_chat_id_chats_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "message_id" + ], + "schemaTo": "public", + "tableTo": "messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_message_id_messages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_by_user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_by_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "nameExplicit": false, + "columns": [ + "applied_source_id" + ], + "schemaTo": "public", + "tableTo": "sources", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "graph_suggestions_applied_source_id_sources_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "graph_suggestions" + }, + { + "columns": [ + "process_run_id", + "file_id" + ], + "nameExplicit": true, + "name": "process_run_files_pk", + "entityType": "pks", + "schema": "public", + "table": "process_run_files" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apikey_pkey", + "schema": "public", + "table": "apikey", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_prompts_pkey", + "schema": "public", + "table": "organization_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "team_member_id" + ], + "nameExplicit": false, + "name": "team_member_roles_pkey", + "schema": "public", + "table": "team_member_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "teamMember_pkey", + "schema": "public", + "table": "teamMember", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_prompts_pkey", + "schema": "public", + "table": "team_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "team_pkey", + "schema": "public", + "table": "team", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_prompts_pkey", + "schema": "public", + "table": "user_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "chats_pkey", + "schema": "public", + "table": "chats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "messages_pkey", + "schema": "public", + "table": "messages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "connector_installations_pkey", + "schema": "public", + "table": "connector_installations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "connector_webhook_events_pkey", + "schema": "public", + "table": "connector_webhook_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "connectors_pkey", + "schema": "public", + "table": "connectors", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "repository_graph_bindings_pkey", + "schema": "public", + "table": "repository_graph_bindings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "file_type_configs_pkey", + "schema": "public", + "table": "file_type_configs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "entities_pkey", + "schema": "public", + "table": "entities", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "files_pkey", + "schema": "public", + "table": "files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_prompts_pkey", + "schema": "public", + "table": "graph_prompts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graphs_pkey", + "schema": "public", + "table": "graphs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_updates_pkey", + "schema": "public", + "table": "graph_updates", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_runs_pkey", + "schema": "public", + "table": "process_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "process_stats_pkey", + "schema": "public", + "table": "process_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "relationships_pkey", + "schema": "public", + "table": "relationships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sources_pkey", + "schema": "public", + "table": "sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "text_units_pkey", + "schema": "public", + "table": "text_units", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "models_pkey", + "schema": "public", + "table": "models", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "graph_suggestions_pkey", + "schema": "public", + "table": "graph_suggestions", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "id", + "organizationId" + ], + "nullsNotDistinct": false, + "name": "team_id_organization_unique", + "entityType": "uniques", + "schema": "public", + "table": "team" + }, + { + "nameExplicit": false, + "columns": [ + "id" + ], + "nullsNotDistinct": false, + "name": "apikey_id_key", + "schema": "public", + "table": "apikey", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\n (\n \"scope\" = 'graph'\n AND \"project_id\" IS NOT NULL\n AND \"team_id\" IS NULL\n )\n OR\n (\n \"scope\" = 'team'\n AND \"project_id\" IS NULL\n AND \"team_id\" IS NOT NULL\n )\n ", + "name": "chats_scope_target_check", + "entityType": "checks", + "schema": "public", + "table": "chats" + }, + { + "value": "jsonb_typeof(\"parts\") = 'array'", + "name": "chat_messages_parts_array_check", + "entityType": "checks", + "schema": "public", + "table": "messages" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "connector_installations_provider_check", + "entityType": "checks", + "schema": "public", + "table": "connector_installations" + }, + { + "value": "\"status\" in ('active', 'disabled')", + "name": "connector_installations_status_check", + "entityType": "checks", + "schema": "public", + "table": "connector_installations" + }, + { + "value": "(\"organization_id\" is not null and \"team_id\" is null) or (\"organization_id\" is not null and \"team_id\" is not null)", + "name": "connector_installations_owner_scope_check", + "entityType": "checks", + "schema": "public", + "table": "connector_installations" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "connector_webhook_events_provider_check", + "entityType": "checks", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "value": "\"status\" in ('ignored', 'enqueued', 'duplicate', 'failed')", + "name": "connector_webhook_events_status_check", + "entityType": "checks", + "schema": "public", + "table": "connector_webhook_events" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "connectors_provider_check", + "entityType": "checks", + "schema": "public", + "table": "connectors" + }, + { + "value": "\"status\" in ('draft', 'active', 'disabled')", + "name": "connectors_status_check", + "entityType": "checks", + "schema": "public", + "table": "connectors" + }, + { + "value": "\"provider\" in ('github', 'gitlab')", + "name": "repository_graph_bindings_provider_check", + "entityType": "checks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "value": "\"sync_status\" in ('pending', 'syncing', 'synced', 'failed')", + "name": "repository_graph_bindings_sync_status_check", + "entityType": "checks", + "schema": "public", + "table": "repository_graph_bindings" + }, + { + "value": "\n (\n \"storage_kind\" = 'internal'\n AND \"external_url\" IS NULL\n AND \"external_provider\" IS NULL\n )\n OR (\n \"storage_kind\" = 'external'\n AND \"external_url\" IS NOT NULL\n AND \"external_provider\" IS NOT NULL\n )\n ", + "name": "files_storage_origin_check", + "entityType": "checks", + "schema": "public", + "table": "files" + }, + { + "value": "\"external_provider\" IS NULL OR \"external_provider\" in ('github', 'gitlab')", + "name": "files_external_provider_check", + "entityType": "checks", + "schema": "public", + "table": "files" + }, + { + "value": "(((\"organization_id\" IS NOT NULL)::int + (\"user_id\" IS NOT NULL)::int + (\"graph_id\" IS NOT NULL)::int) = 1)", + "name": "graphs_single_owner_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "\"team_id\" IS NULL OR \"organization_id\" IS NOT NULL", + "name": "graphs_team_requires_organization_check", + "entityType": "checks", + "schema": "public", + "table": "graphs" + }, + { + "value": "((\"start_page\" IS NULL AND \"end_page\" IS NULL) OR (\"start_page\" IS NOT NULL AND \"end_page\" IS NOT NULL AND \"start_page\" >= 1 AND \"end_page\" >= \"start_page\"))", + "name": "text_units_page_span_check", + "entityType": "checks", + "schema": "public", + "table": "text_units" + }, + { + "value": "\n (\n \"kind\" = 'source_correction'\n AND \"source_id\" IS NOT NULL\n AND \"entity_id\" IS NULL\n )\n OR\n (\n \"kind\" = 'entity_addition'\n AND \"source_id\" IS NULL\n AND \"entity_id\" IS NOT NULL\n )\n ", + "name": "graph_suggestions_target_check", + "entityType": "checks", + "schema": "public", + "table": "graph_suggestions" + } + ], + "renames": [] +} diff --git a/package.json b/package.json index 5cd4e6ea..9c278d99 100644 --- a/package.json +++ b/package.json @@ -66,5 +66,8 @@ }, "patchedDependencies": { "openworkflow@0.9.0": "patches/openworkflow@0.9.0.patch" - } + }, + "trustedDependencies": [ + "tree-sitter" + ] } diff --git a/packages/ai/src/__tests__/relationship-tools.test.ts b/packages/ai/src/__tests__/relationship-tools.test.ts new file mode 100644 index 00000000..a3f27102 --- /dev/null +++ b/packages/ai/src/__tests__/relationship-tools.test.ts @@ -0,0 +1,181 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +type Row = Record; + +const selectResults: Row[][] = []; + +function nextSelectResult() { + const result = selectResults.shift(); + if (!result) { + throw new Error("Unexpected select query"); + } + + return result; +} + +function queryResult() { + return { + then: (resolve: (value: Row[]) => unknown, reject: (error: unknown) => unknown) => + Promise.resolve().then(nextSelectResult).then(resolve, reject), + limit: () => nextSelectResult(), + orderBy: () => ({ + limit: () => nextSelectResult(), + }), + }; +} + +mock.module("@kiwi/db", () => ({ + db: { + select: () => ({ + from: () => ({ + where: () => queryResult(), + }), + }), + }, +})); + +mock.module("@kiwi/logger", () => ({ + debug: () => undefined, + error: () => undefined, + info: () => undefined, + warn: () => undefined, +})); + +const { getNeighboursTool, getPathBetweenTool } = await import("../tools/relationship"); + +async function executeTool(tool: { execute?: (input: unknown) => Promise }, input: unknown) { + if (!tool.execute) { + throw new Error("Tool has no execute function"); + } + + return tool.execute(input); +} + +describe("relationship graph tools", () => { + beforeEach(() => { + selectResults.length = 0; + }); + + test("get_path_between_entities does not traverse directed relationships backwards", async () => { + selectResults.push([{ id: "relationship-ba", sourceId: "entity-b", targetId: "entity-a", directed: true }]); + + const output = await executeTool(getPathBetweenTool("graph-1"), { + sourceEntityId: "entity-a", + targetEntityId: "entity-b", + }); + + expect(output).toContain("none found"); + }); + + test("get_path_between_entities returns no path for missing same-entity requests", async () => { + selectResults.push([]); + + const output = await executeTool(getPathBetweenTool("graph-1"), { + sourceEntityId: "missing-entity", + targetEntityId: "missing-entity", + }); + + expect(output).toBe("## Path\n- none found within 5 hops"); + expect(output).not.toContain("- missing-entity,"); + }); + + test("get_path_between_entities returns zero-hop path for existing same-entity requests", async () => { + selectResults.push([{ id: "entity-a", name: "A", type: "CODE_FUNCTION" }]); + + const output = await executeTool(getPathBetweenTool("graph-1"), { + sourceEntityId: "entity-a", + targetEntityId: "entity-a", + }); + + expect(output).toBe("## Path\n- entity-a, A, CODE_FUNCTION"); + }); + + test("get_path_between_entities traverses directed relationships forward", async () => { + selectResults.push( + [{ id: "relationship-ab", sourceId: "entity-a", targetId: "entity-b", directed: true }], + [{ id: "relationship-bc", sourceId: "entity-b", targetId: "entity-c", directed: true }], + [ + { id: "entity-a", name: "A", type: "CODE_FUNCTION" }, + { id: "entity-b", name: "B", type: "CODE_FUNCTION" }, + { id: "entity-c", name: "C", type: "CODE_FUNCTION" }, + ], + [ + { + id: "relationship-ab", + sourceId: "entity-a", + targetId: "entity-b", + kind: "CALLS", + directed: true, + description: "A calls B.", + }, + { + id: "relationship-bc", + sourceId: "entity-b", + targetId: "entity-c", + kind: "CALLS", + directed: true, + description: "B calls C.", + }, + ] + ); + + const output = await executeTool(getPathBetweenTool("graph-1"), { + sourceEntityId: "entity-a", + targetEntityId: "entity-c", + }); + + expect(output).toContain("relationship-ab, entity-a -> entity-b, CALLS"); + expect(output).toContain("relationship-bc, entity-b -> entity-c, CALLS"); + }); + + test("get_path_between_entities still traverses undirected relationships both ways", async () => { + selectResults.push( + [{ id: "relationship-ba", sourceId: "entity-b", targetId: "entity-a", directed: false }], + [ + { id: "entity-a", name: "A", type: "CODE_FUNCTION" }, + { id: "entity-b", name: "B", type: "CODE_FUNCTION" }, + ], + [ + { + id: "relationship-ba", + sourceId: "entity-b", + targetId: "entity-a", + kind: "RELATED", + directed: false, + description: "A and B are related.", + }, + ] + ); + + const output = await executeTool(getPathBetweenTool("graph-1"), { + sourceEntityId: "entity-a", + targetEntityId: "entity-b", + }); + + expect(output).toContain("relationship-ba, entity-b -- entity-a, RELATED"); + }); + + test("get_entity_neighbours reports relationship kind and direction", async () => { + selectResults.push( + [ + { + id: "relationship-ab", + sourceId: "entity-a", + targetId: "entity-b", + kind: "CALLS", + directed: true, + description: "A calls B.", + rank: 0.8, + }, + ], + [{ id: "entity-b", name: "B", type: "CODE_FUNCTION", description: "Function B." }] + ); + + const output = await executeTool(getNeighboursTool("graph-1"), { + entityId: "entity-a", + limit: 10, + }); + + expect(output).toContain("relationship-ab, CALLS, outgoing, entity-a -> entity-b"); + }); +}); diff --git a/packages/ai/src/tools/correction.ts b/packages/ai/src/tools/correction.ts index 57deaaf9..70d4f66d 100644 --- a/packages/ai/src/tools/correction.ts +++ b/packages/ai/src/tools/correction.ts @@ -1,6 +1,7 @@ import { db } from "@kiwi/db"; import { graphSuggestionsTable } from "@kiwi/db/tables/suggestions"; import { entityTable, filesTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourcePredicate, visibleFilePredicate } from "@kiwi/db/source-validity"; import { and, eq } from "drizzle-orm"; import { tool } from "ai"; import { z } from "zod"; @@ -44,7 +45,14 @@ async function assertSourceInGraph(graphId: string, sourceId: string) { .from(sourcesTable) .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) - .where(and(eq(sourcesTable.id, sourceId), eq(filesTable.graphId, graphId))) + .where( + and( + eq(sourcesTable.id, sourceId), + eq(filesTable.graphId, graphId), + currentSourcePredicate(sourcesTable), + visibleFilePredicate(filesTable) + ) + ) .limit(1); if (!source) { diff --git a/packages/ai/src/tools/entity.ts b/packages/ai/src/tools/entity.ts index af968034..d0089330 100644 --- a/packages/ai/src/tools/entity.ts +++ b/packages/ai/src/tools/entity.ts @@ -1,6 +1,7 @@ import { db } from "@kiwi/db"; import type { EmbeddingModelV3 } from "@ai-sdk/provider"; -import { entityTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { entityTable, filesTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourcePredicate, currentSourceSql, visibleFilePredicate, visibleFileSql } from "@kiwi/db/source-validity"; import { and, asc, cosineDistance, eq, exists, gt, inArray, sql } from "drizzle-orm"; import { embed, tool } from "ai"; import { withAiSlot } from "../concurrency"; @@ -66,7 +67,10 @@ function buildFileScopeExpression(fileIds: string[]) { select 1 from sources source inner join text_units text_unit on text_unit.id = source.text_unit_id + inner join files file on file.id = text_unit.file_id where source.entity_id = e.id + and ${currentSourceSql("source")} + and ${visibleFileSql("file")} and text_unit.file_id in (${sql.join( fileIds.map((fileId) => sql`${fileId}`), sql`, ` @@ -238,10 +242,13 @@ export const listEntitiesTool = (graphId: string, options: EntityToolOptions = { .select({ id: sourcesTable.id }) .from(sourcesTable) .innerJoin(textUnitTable, eq(textUnitTable.id, sourcesTable.textUnitId)) + .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) .where( and( eq(sourcesTable.entityId, entityTable.id), - inArray(textUnitTable.fileId, fileIds) + currentSourcePredicate(sourcesTable), + inArray(textUnitTable.fileId, fileIds), + visibleFilePredicate(filesTable) ) ) ) diff --git a/packages/ai/src/tools/relationship.ts b/packages/ai/src/tools/relationship.ts index 23e149bc..d191f639 100644 --- a/packages/ai/src/tools/relationship.ts +++ b/packages/ai/src/tools/relationship.ts @@ -1,6 +1,7 @@ import { db } from "@kiwi/db"; import type { EmbeddingModelV3 } from "@ai-sdk/provider"; import { entityTable, relationshipTable } from "@kiwi/db/tables/graph"; +import { currentSourceSql, visibleFileSql } from "@kiwi/db/source-validity"; import { and, asc, cosineDistance, eq, gt, inArray, or, sql } from "drizzle-orm"; import { embed, tool } from "ai"; import { withAiSlot } from "../concurrency"; @@ -27,11 +28,23 @@ type SearchRelationshipRow = { targetId: string; sourceName: string; targetName: string; + kind: string; + directed: boolean; description: string; rank: number; score: number; }; +type RelationshipEdgeRow = { + id: string; + sourceId: string; + targetId: string; + description: string; + kind: string; + directed: boolean; + rank: number; +}; + function buildRelationshipKeywordBoostExpression(terms: string[]) { return greatest( terms.flatMap((term) => [ @@ -67,7 +80,10 @@ function buildRelationshipFileScopeExpression(fileIds: string[]) { select 1 from sources source inner join text_units text_unit on text_unit.id = source.text_unit_id + inner join files file on file.id = text_unit.file_id where source.relationship_id = r.id + and ${currentSourceSql("source")} + and ${visibleFileSql("file")} and text_unit.file_id in (${sql.join( fileIds.map((fileId) => sql`${fileId}`), sql`, ` @@ -83,23 +99,29 @@ function toSearchRelationshipRows(rows: Record[]): SearchRelati targetId: String(row.targetId ?? ""), sourceName: String(row.sourceName ?? "Unknown"), targetName: String(row.targetName ?? "Unknown"), + kind: String(row.kind ?? "RELATED"), + directed: row.directed === true, description: String(row.description ?? ""), rank: Number(row.rank ?? 0), score: Number(row.score ?? 0), })); } +function formatRelationshipDirection(row: Pick) { + return row.directed ? `${row.sourceId} -> ${row.targetId}` : `${row.sourceId} -- ${row.targetId}`; +} + function formatRelationshipList( rows: Array< Pick< SearchRelationshipRow, - "id" | "sourceId" | "targetId" | "sourceName" | "targetName" | "description" | "rank" + "id" | "sourceId" | "targetId" | "sourceName" | "targetName" | "kind" | "directed" | "description" | "rank" > > ) { return rows.map( (row) => - `- ${row.id}, ${row.sourceId} ${row.sourceName} -> ${row.targetId} ${row.targetName}, ${truncateWords(row.description) || "No description"}, rank ${row.rank}` + `- ${row.id}, ${formatRelationshipDirection(row)}, ${row.sourceName} to ${row.targetName}, ${row.kind}, ${truncateWords(row.description) || "No description"}, rank ${row.rank}` ); } @@ -178,6 +200,8 @@ export const searchRelationshipsTool = ( r.target_id as "targetId", coalesce(source_entity.name, 'Unknown') as "sourceName", coalesce(target_entity.name, 'Unknown') as "targetName", + r.kind, + r.directed, r.description, r.rank, ${semanticScore} as semantic_score, @@ -197,6 +221,8 @@ export const searchRelationshipsTool = ( ranked."targetId", ranked."sourceName", ranked."targetName", + ranked.kind, + ranked.directed, ranked.description, ranked.rank, ranked.score @@ -278,6 +304,8 @@ export const getRelationshipsTool = (graphId: string) => id: relationshipTable.id, sourceId: relationshipTable.sourceId, targetId: relationshipTable.targetId, + kind: relationshipTable.kind, + directed: relationshipTable.directed, description: relationshipTable.description, rank: relationshipTable.rank, }) @@ -309,7 +337,7 @@ export const getRelationshipsTool = (graphId: string) => const source = entityMap.get(row.sourceId); const target = entityMap.get(row.targetId); - return `- ${row.id}, ${row.sourceId} ${source?.name ?? "Unknown"} -> ${row.targetId} ${target?.name ?? "Unknown"}, ${description || "No description"}, rank ${row.rank}`; + return `- ${row.id}, ${formatRelationshipDirection(row)}, ${source?.name ?? "Unknown"} to ${target?.name ?? "Unknown"}, ${row.kind}, ${description || "No description"}, rank ${row.rank}`; }) : ["- none"]), ...(hasMore && items.length > 0 ? [``, `Next cursor: ${items[items.length - 1]?.id}`] : []), @@ -353,6 +381,8 @@ export const getNeighboursTool = (graphId: string) => id: relationshipTable.id, sourceId: relationshipTable.sourceId, targetId: relationshipTable.targetId, + kind: relationshipTable.kind, + directed: relationshipTable.directed, description: relationshipTable.description, rank: relationshipTable.rank, }) @@ -387,8 +417,9 @@ export const getNeighboursTool = (graphId: string) => const neighbour = entityMap.get(neighbourId); const relationship = truncateWords(row.description, 30); const description = truncateWords(neighbour?.description ?? "", 20); + const direction = row.sourceId === entityId ? "outgoing" : "incoming"; - return `- ${neighbourId}, ${neighbour?.name ?? "Unknown"}, ${neighbour?.type ?? "Unknown"}, ${description || "No description"}; via ${row.id}, ${relationship || "No relationship description"}, rank ${row.rank}`; + return `- ${neighbourId}, ${neighbour?.name ?? "Unknown"}, ${neighbour?.type ?? "Unknown"}, ${description || "No description"}; via ${row.id}, ${row.kind}, ${row.directed ? direction : "undirected"}, ${formatRelationshipDirection(row)}, ${relationship || "No relationship description"}, rank ${row.rank}`; }) : ["- none"]), ...(hasMore && items.length > 0 ? [``, `Next cursor: ${items[items.length - 1]?.id}`] : []), @@ -430,10 +461,11 @@ export const getPathBetweenTool = (graphId: string) => .where(and(eq(entityTable.graphId, graphId), eq(entityTable.id, sourceEntityId))) .limit(1); - return [ - "## Path", - `- ${entity?.id ?? sourceEntityId}, ${entity?.name ?? "Unknown"}, ${entity?.type ?? "Unknown"}`, - ].join("\n"); + if (!entity) { + return "## Path\n- none found within 5 hops"; + } + + return ["## Path", `- ${entity.id}, ${entity.name}, ${entity.type}`].join("\n"); } const maxDepth = 5; @@ -451,6 +483,7 @@ export const getPathBetweenTool = (graphId: string) => id: relationshipTable.id, sourceId: relationshipTable.sourceId, targetId: relationshipTable.targetId, + directed: relationshipTable.directed, }) .from(relationshipTable) .where( @@ -476,7 +509,11 @@ export const getPathBetweenTool = (graphId: string) => nextFrontier.push(relationship.targetId); } - if (frontier.includes(relationship.targetId) && !visited.has(relationship.sourceId)) { + if ( + !relationship.directed && + frontier.includes(relationship.targetId) && + !visited.has(relationship.sourceId) + ) { visited.add(relationship.sourceId); previous.set(relationship.sourceId, { entityId: relationship.targetId, @@ -521,6 +558,10 @@ export const getPathBetweenTool = (graphId: string) => ? await db .select({ id: relationshipTable.id, + sourceId: relationshipTable.sourceId, + targetId: relationshipTable.targetId, + kind: relationshipTable.kind, + directed: relationshipTable.directed, description: relationshipTable.description, }) .from(relationshipTable) @@ -538,7 +579,7 @@ export const getPathBetweenTool = (graphId: string) => const relationship = relationshipMap.get(pathRelationshipIds[index]!); const description = truncateWords(relationship?.description ?? "", 30); lines.push( - `- ${pathRelationshipIds[index]}, ${description || "No relationship description"}` + `- ${pathRelationshipIds[index]}, ${relationship ? formatRelationshipDirection(relationship) : "Unknown direction"}, ${relationship?.kind ?? "RELATED"}, ${description || "No relationship description"}` ); } } diff --git a/packages/ai/src/tools/source.ts b/packages/ai/src/tools/source.ts index ad9ce424..1f40bca7 100644 --- a/packages/ai/src/tools/source.ts +++ b/packages/ai/src/tools/source.ts @@ -1,6 +1,7 @@ import { db } from "@kiwi/db"; import type { EmbeddingModelV3 } from "@ai-sdk/provider"; import { filesTable, sourcesTable, textUnitTable } from "@kiwi/db/tables/graph"; +import { currentSourcePredicate, currentSourceSql, visibleFilePredicate, visibleFileSql } from "@kiwi/db/source-validity"; import { and, asc, cosineDistance, eq, gt, inArray, or, sql, type SQL } from "drizzle-orm"; import { embed, tool } from "ai"; import { withAiSlot } from "../concurrency"; @@ -203,8 +204,7 @@ async function getScopedSources( onConsideredFileIds?.(fileIds); if (terms.length === 0) { - const clauses = [eq(sourcesTable.active, true), eq(filesTable.graphId, graphId)]; - + const clauses = [currentSourcePredicate(sourcesTable), eq(filesTable.graphId, graphId), visibleFilePredicate(filesTable)]; if (cursor) { clauses.push(gt(sourcesTable.id, cursor)); } @@ -306,7 +306,8 @@ async function getScopedSources( from sources source inner join text_units text_unit on text_unit.id = source.text_unit_id inner join files file on file.id = text_unit.file_id - where source.active = true + where ${currentSourceSql("source")} + and ${visibleFileSql("file")} and file.graph_id = ${graphId} ${fileScope} ${subjectScope} @@ -459,8 +460,9 @@ export const getSourceFileMetadataTool = (graphId: string, options: SourceToolOp .innerJoin(filesTable, eq(filesTable.id, textUnitTable.fileId)) .where( and( - eq(sourcesTable.active, true), + currentSourcePredicate(sourcesTable), eq(filesTable.graphId, graphId), + visibleFilePredicate(filesTable), inArray(sourcesTable.id, ids) ) ); diff --git a/packages/connectors/package.json b/packages/connectors/package.json new file mode 100644 index 00000000..7a32d9b9 --- /dev/null +++ b/packages/connectors/package.json @@ -0,0 +1,24 @@ +{ + "name": "@kiwi/connectors", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./credentials": "./src/credentials.ts", + "./types": "./src/types.ts", + "./github": "./src/github.ts", + "./gitlab": "./src/gitlab.ts", + "./webhooks": "./src/webhooks.ts" + }, + "scripts": { + "build": "bunx tsc --noEmit", + "test": "bun test" + }, + "dependencies": {}, + "devDependencies": { + "@types/bun": "catalog:" + }, + "peerDependencies": { + "typescript": "catalog:" + } +} diff --git a/packages/connectors/src/__tests__/credentials.test.ts b/packages/connectors/src/__tests__/credentials.test.ts new file mode 100644 index 00000000..78538265 --- /dev/null +++ b/packages/connectors/src/__tests__/credentials.test.ts @@ -0,0 +1,63 @@ +import { createCipheriv, hkdfSync, randomBytes } from "node:crypto"; +import { describe, expect, test } from "bun:test"; +import { + decryptConnectorCredentials, + decryptConnectorSecret, + encryptConnectorCredentials, + encryptConnectorSecret, +} from "../credentials"; + +const AUTH_SECRET = "unit-test-auth-secret"; + +describe("connector credentials", () => { + test("round-trips connector credentials and webhook secrets", () => { + const encrypted = encryptConnectorCredentials( + { + provider: "github", + appId: "123", + privateKeyPem: "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----", + clientId: "client-id", + clientSecret: "client-secret", + webhookSecret: "webhook-secret", + }, + AUTH_SECRET + ); + + expect(decryptConnectorCredentials(encrypted, AUTH_SECRET)).toEqual({ + provider: "github", + appId: "123", + privateKeyPem: "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----", + clientId: "client-id", + clientSecret: "client-secret", + webhookSecret: "webhook-secret", + }); + + expect(decryptConnectorSecret(encryptConnectorSecret("hook", AUTH_SECRET), AUTH_SECRET)).toBe("hook"); + }); + + test("rejects invalid versions and wrong secrets", () => { + const encrypted = encryptConnectorCredentials({ provider: "gitlab", accessToken: "token" }, AUTH_SECRET); + + expect(() => decryptConnectorCredentials(encrypted.replace("v1:", "v0:"), AUTH_SECRET)).toThrow( + "Invalid connector credentials" + ); + expect(() => decryptConnectorCredentials(encrypted, "wrong-secret")).toThrow("Invalid connector credentials"); + }); + + test("rejects invalid decrypted shapes", () => { + const encrypted = encryptUnchecked({ provider: "github", appId: "123" }, AUTH_SECRET); + + expect(() => decryptConnectorCredentials(encrypted, AUTH_SECRET)).toThrow("Invalid connector credentials"); + expect(() => encryptConnectorCredentials({ provider: "gitlab", accessToken: "" } as never, AUTH_SECRET)).toThrow( + "Invalid connector credentials" + ); + }); +}); + +function encryptUnchecked(value: unknown, secret: string): string { + const iv = randomBytes(12); + const key = Buffer.from(hkdfSync("sha256", secret, "kiwi-connector-credentials:v1", "connector-credential-encryption", 32)); + const cipher = createCipheriv("aes-256-gcm", key, iv, { authTagLength: 16 }); + const ciphertext = Buffer.concat([cipher.update(Buffer.from(JSON.stringify(value), "utf8")), cipher.final()]); + return ["v1", iv.toString("base64url"), cipher.getAuthTag().toString("base64url"), ciphertext.toString("base64url")].join(":"); +} diff --git a/packages/connectors/src/__tests__/github.test.ts b/packages/connectors/src/__tests__/github.test.ts new file mode 100644 index 00000000..be2cd15f --- /dev/null +++ b/packages/connectors/src/__tests__/github.test.ts @@ -0,0 +1,279 @@ +import { createHmac, generateKeyPairSync } from "node:crypto"; +import { describe, expect, test } from "bun:test"; +import { + createGitHubAppJwt, + createGitHubClient, + createGitHubInstallationToken, + getGitHubInstallationAccount, + loadGitHubRepositorySnapshot, + normalizeGitHubWebhookEvent, + verifyGitHubWebhookSignature, +} from "../github"; +import type { FetchLike, ProviderRepository } from "../types"; + +const GITHUB_REPOSITORY: ProviderRepository = { + provider: "github", + id: "1", + fullName: "acme/app", + name: "app", + htmlUrl: "https://github.com/acme/app", + defaultBranch: "main", + private: true, +}; + +describe("GitHub connector", () => { + test("creates app JWT and installation tokens", async () => { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString(); + const jwt = createGitHubAppJwt({ appId: "42", privateKeyPem, now: new Date("2026-01-01T00:00:00Z") }); + expect(jwt.split(".")).toHaveLength(3); + + const calls: RequestInit[] = []; + const fetchImpl: FetchLike = async (_input, init) => { + calls.push(init ?? {}); + return jsonResponse({ token: "installation-token", expires_at: "2026-01-01T01:00:00Z" }); + }; + + await expect( + createGitHubInstallationToken({ + credentials: { provider: "github", appId: "42", privateKeyPem }, + installationId: "99", + apiBaseUrl: "https://github.test", + fetch: fetchImpl, + now: new Date("2026-01-01T00:00:00Z"), + }) + ).resolves.toEqual({ token: "installation-token", expiresAt: "2026-01-01T01:00:00Z" }); + expect(calls[0]?.method).toBe("POST"); + expect(String((calls[0]?.headers as Record).authorization)).toMatch(/^Bearer /); + expect(calls[0]?.body).toBe(JSON.stringify({ permissions: { contents: "read", metadata: "read" } })); + }); + + test("loads GitHub installation account metadata", async () => { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString(); + const calls: string[] = []; + const fetchImpl: FetchLike = async (input) => { + calls.push(String(input)); + return jsonResponse({ + account: { login: "acme", type: "Organization" }, + repository_selection: "selected", + }); + }; + + await expect( + getGitHubInstallationAccount({ + credentials: { provider: "github", appId: "42", privateKeyPem }, + installationId: "99", + apiBaseUrl: "https://github.test", + fetch: fetchImpl, + now: new Date("2026-01-01T00:00:00Z"), + }) + ).resolves.toEqual({ + login: "acme", + type: "organization", + repositorySelection: "selected", + }); + expect(calls).toEqual(["https://github.test/app/installations/99"]); + }); + + test("lists repositories and branches with pagination", async () => { + const urls: string[] = []; + const fetchImpl: FetchLike = async (input) => { + urls.push(String(input)); + if (String(input).includes("/installation/repositories")) { + return jsonResponse({ + repositories: [ + { + id: 1, + full_name: "acme/app", + name: "app", + html_url: "https://github.com/acme/app", + default_branch: "main", + private: true, + }, + ], + }); + } + return jsonResponse([{ name: "main", commit: { sha: "commit-sha" } }]); + }; + const client = createGitHubClient({ + installationToken: "token", + apiBaseUrl: "https://github.test", + fetch: fetchImpl, + }); + + await expect(client.listRepositories()).resolves.toEqual([GITHUB_REPOSITORY]); + await expect(client.listBranches(GITHUB_REPOSITORY)).resolves.toEqual([ + { name: "main", commitSha: "commit-sha" }, + ]); + expect(urls).toContain("https://github.test/installation/repositories?per_page=100&page=1"); + expect(urls).toContain("https://github.test/repos/acme/app/branches?per_page=100&page=1"); + }); + + test("loads supported repository snapshots through tree and blob APIs", async () => { + const calls: string[] = []; + const fetchImpl: FetchLike = async (input) => { + const url = String(input); + calls.push(url); + if (url.endsWith("/branches/main")) { + return jsonResponse({ commit: { sha: "commit-sha", commit: { tree: { sha: "tree-sha" } } } }); + } + if (url.includes("/git/trees/tree-sha")) { + return jsonResponse({ + tree: [ + { type: "blob", path: "src/index.ts", sha: "blob-sha", size: 18 }, + { type: "blob", path: "dist/generated.ts", sha: "skip-sha", size: 10 }, + { type: "blob", path: "README.md", sha: "readme-sha", size: 10 }, + ], + }); + } + if (url.includes("/git/blobs/blob-sha")) { + return jsonResponse({ + encoding: "base64", + content: Buffer.from("export const ok = 1;", "utf8").toString("base64"), + }); + } + throw new Error(`unexpected URL ${url}`); + }; + + await expect( + loadGitHubRepositorySnapshot({ + installationToken: "token", + apiBaseUrl: "https://github.test", + fetch: fetchImpl, + repository: GITHUB_REPOSITORY, + branch: "main", + }) + ).resolves.toEqual({ + repository: GITHUB_REPOSITORY, + branch: { name: "main", commitSha: "commit-sha" }, + commitSha: "commit-sha", + files: [ + { + path: "src/index.ts", + size: 20, + checksum: "blob-sha", + htmlUrl: "https://github.com/acme/app/blob/commit-sha/src/index.ts", + rawUrl: "https://raw.githubusercontent.com/acme/app/commit-sha/src/index.ts", + content: "export const ok = 1;", + }, + ], + }); + expect(calls.some((url) => url.includes("skip-sha"))).toBe(false); + expect(calls.some((url) => url.includes("readme-sha"))).toBe(false); + }); + + test("rejects truncated GitHub repository trees", async () => { + const fetchImpl: FetchLike = async (input) => { + const url = String(input); + if (url.endsWith("/branches/main")) { + return jsonResponse({ commit: { sha: "commit-sha", commit: { tree: { sha: "tree-sha" } } } }); + } + if (url.includes("/git/trees/tree-sha")) { + return jsonResponse({ truncated: true, tree: [] }); + } + throw new Error(`unexpected URL ${url}`); + }; + + await expect( + loadGitHubRepositorySnapshot({ + installationToken: "token", + apiBaseUrl: "https://github.test", + fetch: fetchImpl, + repository: GITHUB_REPOSITORY, + branch: "main", + }) + ).rejects.toMatchObject({ + name: "ConnectorProviderError", + kind: "limit", + }); + }); + + test("normalizes incremental compare changes for supported code paths", async () => { + const client = createGitHubClient({ + installationToken: "token", + apiBaseUrl: "https://github.test", + fetch: async (input) => { + expect(String(input)).toBe("https://github.test/repos/acme/app/compare/commit-old...commit-new"); + return jsonResponse({ + status: "ahead", + files: [ + { filename: "src/modified.ts", status: "modified" }, + { filename: "src/added.ts", status: "added" }, + { filename: "src/deleted.ts", status: "removed" }, + { filename: "src/renamed.ts", status: "renamed", previous_filename: "src/old-name.ts" }, + { filename: "README.md", status: "renamed", previous_filename: "src/renamed-away.ts" }, + { filename: "README.md", status: "modified" }, + ], + }); + }, + }); + + await expect(client.compareRepository(GITHUB_REPOSITORY, "commit-old", "commit-new")).resolves.toEqual({ + fromCommitSha: "commit-old", + toCommitSha: "commit-new", + isIncremental: true, + changes: [ + { status: "modified", newPath: "src/modified.ts" }, + { status: "added", newPath: "src/added.ts" }, + { status: "deleted", oldPath: "src/deleted.ts" }, + { status: "renamed", oldPath: "src/old-name.ts", newPath: "src/renamed.ts" }, + { status: "deleted", oldPath: "src/renamed-away.ts" }, + ], + }); + }); + + test("marks non-forward GitHub compares as requiring a snapshot sync", async () => { + const client = createGitHubClient({ + installationToken: "token", + apiBaseUrl: "https://github.test", + fetch: async () => jsonResponse({ status: "diverged" }), + }); + + await expect(client.compareRepository(GITHUB_REPOSITORY, "commit-old", "commit-new")).resolves.toEqual({ + fromCommitSha: "commit-old", + toCommitSha: "commit-new", + isIncremental: false, + changes: [], + }); + }); + + test("verifies webhooks and normalizes push events", () => { + const body = JSON.stringify({ ref: "refs/heads/main" }); + const signature = `sha256=${createHmac("sha256", "secret").update(body).digest("hex")}`; + + expect(verifyGitHubWebhookSignature({ body, webhookSecret: "secret", signatureHeader: signature })).toBe(true); + expect(verifyGitHubWebhookSignature({ body, webhookSecret: "secret", signatureHeader: "sha256=bad" })).toBe( + false + ); + expect( + normalizeGitHubWebhookEvent({ + eventName: "push", + deliveryId: "delivery", + payload: { + ref: "refs/heads/main", + after: "commit-sha", + repository: { id: 1, full_name: "acme/app" }, + installation: { id: 99 }, + }, + }) + ).toMatchObject({ + provider: "github", + deliveryId: "delivery", + eventName: "push", + repositoryId: "1", + repositoryFullName: "acme/app", + branch: "main", + commitSha: "commit-sha", + installationId: "99", + }); + }); +}); + +function jsonResponse(value: unknown, init?: ResponseInit): Response { + return new Response(JSON.stringify(value), { + status: 200, + ...init, + headers: { "content-type": "application/json", ...init?.headers }, + }); +} diff --git a/packages/connectors/src/__tests__/gitlab.test.ts b/packages/connectors/src/__tests__/gitlab.test.ts new file mode 100644 index 00000000..c4795d52 --- /dev/null +++ b/packages/connectors/src/__tests__/gitlab.test.ts @@ -0,0 +1,254 @@ +import { describe, expect, test } from "bun:test"; +import { + createGitLabClient, + loadGitLabRepositorySnapshot, + normalizeGitLabBaseUrl, + normalizeGitLabWebhookEvent, + verifyGitLabWebhookToken, +} from "../gitlab"; +import type { FetchLike, ProviderRepository } from "../types"; + +const GITLAB_REPOSITORY: ProviderRepository = { + provider: "gitlab", + id: "7", + fullName: "acme/app", + name: "app", + htmlUrl: "https://gitlab.test/acme/app", + defaultBranch: "main", + private: true, +}; + +describe("GitLab connector", () => { + test("normalizes base URLs", () => { + expect(normalizeGitLabBaseUrl("https://gitlab.example.com///")).toBe("https://gitlab.example.com"); + expect(normalizeGitLabBaseUrl("https://gitlab.example.com/root/")).toBe("https://gitlab.example.com/root"); + expect(() => normalizeGitLabBaseUrl("ftp://gitlab.example.com")).toThrow( + "GitLab base URL must use HTTP or HTTPS" + ); + }); + + test("lists projects and branches", async () => { + const urls: string[] = []; + const fetchImpl: FetchLike = async (input) => { + const url = String(input); + urls.push(url); + if (url.includes("/projects?")) { + return jsonResponse([ + { + id: 7, + path_with_namespace: "acme/app", + name: "app", + web_url: "https://gitlab.test/acme/app", + default_branch: "main", + visibility: "private", + }, + ]); + } + return jsonResponse([{ name: "main", commit: { id: "commit-sha" } }]); + }; + const client = createGitLabClient({ baseUrl: "https://gitlab.test", accessToken: "token", fetch: fetchImpl }); + + await expect(client.listRepositories()).resolves.toEqual([GITLAB_REPOSITORY]); + await expect(client.listBranches(GITLAB_REPOSITORY)).resolves.toEqual([ + { name: "main", commitSha: "commit-sha" }, + ]); + expect(urls).toContain("https://gitlab.test/api/v4/projects?membership=true&per_page=100&page=1"); + expect(urls).toContain("https://gitlab.test/api/v4/projects/7/repository/branches?per_page=100&page=1"); + }); + + test("throws typed provider errors for non-json GitLab error responses", async () => { + const client = createGitLabClient({ + baseUrl: "https://gitlab.test", + accessToken: "token", + fetch: async () => new Response("upstream unavailable", { status: 500 }), + }); + + await expect(client.listBranches(GITLAB_REPOSITORY)).rejects.toMatchObject({ + name: "ConnectorProviderError", + kind: "provider", + }); + }); + + test("loads supported repository snapshots through tree and file APIs", async () => { + const calls: string[] = []; + const fetchImpl: FetchLike = async (input) => { + const url = String(input); + calls.push(url); + if (url.endsWith("/repository/branches/main")) { + return jsonResponse({ commit: { id: "commit-sha" } }); + } + if (url.includes("/repository/tree?")) { + return jsonResponse([ + { type: "blob", id: "file-sha", path: "src/index.ts" }, + { type: "blob", id: "generated-sha", path: "node_modules/pkg/index.ts" }, + { type: "blob", id: "readme-sha", path: "README.md" }, + ]); + } + if (url.includes("/repository/files/src%2Findex.ts/raw?")) { + return new Response("export const ok = 1;", { status: 200, headers: { "content-length": "20" } }); + } + throw new Error(`unexpected URL ${url}`); + }; + + await expect( + loadGitLabRepositorySnapshot({ + baseUrl: "https://gitlab.test", + accessToken: "token", + fetch: fetchImpl, + repository: GITLAB_REPOSITORY, + branch: "main", + }) + ).resolves.toEqual({ + repository: GITLAB_REPOSITORY, + branch: { name: "main", commitSha: "commit-sha" }, + commitSha: "commit-sha", + files: [ + { + path: "src/index.ts", + size: 20, + checksum: "file-sha", + htmlUrl: "https://gitlab.test/acme/app/-/blob/commit-sha/src/index.ts", + rawUrl: "https://gitlab.test/api/v4/projects/7/repository/files/src%2Findex.ts/raw?ref=commit-sha", + content: "export const ok = 1;", + }, + ], + }); + expect(calls.some((url) => url.includes("generated-sha"))).toBe(false); + expect(calls.some((url) => url.includes("readme-sha"))).toBe(false); + }); + + test("normalizes incremental compare changes for supported code paths", async () => { + const client = createGitLabClient({ + baseUrl: "https://gitlab.test", + accessToken: "token", + fetch: async (input) => { + expect(String(input)).toBe( + "https://gitlab.test/api/v4/projects/7/repository/compare?from=commit-old&to=commit-new&straight=true" + ); + return jsonResponse({ + compare_timeout: false, + diffs: [ + { + old_path: "src/modified.ts", + new_path: "src/modified.ts", + new_file: false, + renamed_file: false, + deleted_file: false, + }, + { + old_path: "src/added.ts", + new_path: "src/added.ts", + new_file: true, + renamed_file: false, + deleted_file: false, + }, + { + old_path: "src/deleted.ts", + new_path: "src/deleted.ts", + new_file: false, + renamed_file: false, + deleted_file: true, + }, + { + old_path: "src/old-name.ts", + new_path: "src/renamed.ts", + new_file: false, + renamed_file: true, + deleted_file: false, + }, + { + old_path: "src/renamed-away.ts", + new_path: "README.md", + new_file: false, + renamed_file: true, + deleted_file: false, + }, + { + old_path: "README.md", + new_path: "README.md", + new_file: false, + renamed_file: false, + deleted_file: false, + }, + ], + }); + }, + }); + + await expect(client.compareRepository(GITLAB_REPOSITORY, "commit-old", "commit-new")).resolves.toEqual({ + fromCommitSha: "commit-old", + toCommitSha: "commit-new", + isIncremental: true, + changes: [ + { status: "modified", newPath: "src/modified.ts" }, + { status: "added", newPath: "src/added.ts" }, + { status: "deleted", oldPath: "src/deleted.ts" }, + { status: "renamed", oldPath: "src/old-name.ts", newPath: "src/renamed.ts" }, + { status: "deleted", oldPath: "src/renamed-away.ts" }, + ], + }); + }); + + test("falls back to full snapshot when GitLab compare times out", async () => { + const client = createGitLabClient({ + baseUrl: "https://gitlab.test", + accessToken: "token", + fetch: async () => jsonResponse({ compare_timeout: true, diffs: [] }), + }); + + await expect(client.compareRepository(GITLAB_REPOSITORY, "commit-old", "commit-new")).resolves.toEqual({ + fromCommitSha: "commit-old", + toCommitSha: "commit-new", + isIncremental: false, + changes: [], + }); + }); + + test("rejects malformed GitLab compare responses", async () => { + const client = createGitLabClient({ + baseUrl: "https://gitlab.test", + accessToken: "token", + fetch: async () => + jsonResponse({ + compare_timeout: false, + diffs: [{ old_path: "src/index.ts", new_path: "src/index.ts" }], + }), + }); + + await expect(client.compareRepository(GITLAB_REPOSITORY, "commit-old", "commit-new")).rejects.toThrow( + "GitLab compare response is invalid" + ); + }); + + test("verifies webhook tokens and normalizes push events", () => { + expect(verifyGitLabWebhookToken({ webhookSecret: "secret", tokenHeader: "secret" })).toBe(true); + expect(verifyGitLabWebhookToken({ webhookSecret: "secret", tokenHeader: "wrong" })).toBe(false); + expect( + normalizeGitLabWebhookEvent({ + eventName: "push", + deliveryId: "delivery", + payload: { + ref: "refs/heads/main", + after: "commit-sha", + project: { id: 7, path_with_namespace: "acme/app" }, + }, + }) + ).toMatchObject({ + provider: "gitlab", + deliveryId: "delivery", + eventName: "push", + repositoryId: "7", + repositoryFullName: "acme/app", + branch: "main", + commitSha: "commit-sha", + }); + }); +}); + +function jsonResponse(value: unknown, init?: ResponseInit): Response { + return new Response(JSON.stringify(value), { + status: 200, + ...init, + headers: { "content-type": "application/json", ...init?.headers }, + }); +} diff --git a/packages/connectors/src/code-files.ts b/packages/connectors/src/code-files.ts new file mode 100644 index 00000000..c5fa6471 --- /dev/null +++ b/packages/connectors/src/code-files.ts @@ -0,0 +1,20 @@ +const SUPPORTED_CODE_EXTENSIONS: Record = { + ".js": true, + ".jsx": true, + ".ts": true, + ".tsx": true, + ".mts": true, + ".cts": true, +}; + +export function isSupportedCodePath(filePath: string): boolean { + const normalized = filePath.replaceAll("\\", "/"); + const lastSlash = normalized.lastIndexOf("/"); + const fileName = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized; + const extensionStart = fileName.lastIndexOf("."); + if (extensionStart < 0) { + return false; + } + + return SUPPORTED_CODE_EXTENSIONS[fileName.slice(extensionStart).toLowerCase()] === true; +} diff --git a/packages/connectors/src/credentials.ts b/packages/connectors/src/credentials.ts new file mode 100644 index 00000000..87e447a1 --- /dev/null +++ b/packages/connectors/src/credentials.ts @@ -0,0 +1,130 @@ +import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto"; +import type { ConnectorCredentials, ConnectorInstallationCredentials } from "./types"; + +const ENCRYPTION_VERSION = "v1"; +const ENCRYPTION_ALGORITHM = "aes-256-gcm"; +const ENCRYPTION_KEY_SALT = "kiwi-connector-credentials:v1"; +const ENCRYPTION_KEY_INFO = "connector-credential-encryption"; +const IV_BYTE_LENGTH = 12; +const AUTH_TAG_BYTE_LENGTH = 16; + +export type ConnectorSecretPayload = ConnectorCredentials | ConnectorInstallationCredentials | { secret: string }; + +export function encryptConnectorCredentials(credentials: ConnectorSecretPayload, secret: string): string { + assertValidConnectorCredentials(credentials); + const iv = randomBytes(IV_BYTE_LENGTH); + const cipher = createCipheriv(ENCRYPTION_ALGORITHM, deriveEncryptionKey(secret), iv, { + authTagLength: AUTH_TAG_BYTE_LENGTH, + }); + const plaintext = Buffer.from(JSON.stringify(credentials), "utf8"); + const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); + const authTag = cipher.getAuthTag(); + + return [ENCRYPTION_VERSION, encodeBase64Url(iv), encodeBase64Url(authTag), encodeBase64Url(ciphertext)].join(":"); +} + +export function decryptConnectorCredentials(value: string, secret: string): ConnectorSecretPayload { + const [version, rawIv, rawAuthTag, rawCiphertext, extra] = value.split(":"); + if (version !== ENCRYPTION_VERSION || !rawIv || !rawAuthTag || !rawCiphertext || extra !== undefined) { + throw new Error("Invalid connector credentials"); + } + + try { + const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, deriveEncryptionKey(secret), decodeBase64Url(rawIv), { + authTagLength: AUTH_TAG_BYTE_LENGTH, + }); + decipher.setAuthTag(decodeBase64Url(rawAuthTag)); + const plaintext = Buffer.concat([decipher.update(decodeBase64Url(rawCiphertext)), decipher.final()]).toString( + "utf8" + ); + const parsed = JSON.parse(plaintext) as ConnectorSecretPayload; + assertValidConnectorCredentials(parsed); + return parsed; + } catch (error) { + throw new Error("Invalid connector credentials", { cause: error }); + } +} + +export function encryptConnectorSecret(secretValue: string, secret: string): string { + return encryptConnectorCredentials({ secret: secretValue }, secret); +} + +export function decryptConnectorSecret(value: string, secret: string): string { + const payload = decryptConnectorCredentials(value, secret); + if (!isSecretPayload(payload)) { + throw new Error("Invalid connector secret"); + } + return payload.secret; +} + +export function assertValidConnectorCredentials(value: unknown): asserts value is ConnectorSecretPayload { + if (!isObject(value)) { + throw new Error("Invalid connector credentials"); + } + + if (isSecretPayload(value)) { + return; + } + + if (value.provider === "github") { + if (hasNonEmptyString(value, "appId") && hasNonEmptyString(value, "privateKeyPem")) { + assertOptionalString(value, "clientId"); + assertOptionalString(value, "clientSecret"); + assertOptionalString(value, "webhookSecret"); + return; + } + if (hasNonEmptyString(value, "installationId")) { + return; + } + } + + if (value.provider === "gitlab") { + if ( + hasNonEmptyString(value, "baseUrl") && + hasNonEmptyString(value, "clientId") && + hasNonEmptyString(value, "clientSecret") + ) { + assertOptionalString(value, "webhookSecret"); + return; + } + if (hasNonEmptyString(value, "accessToken")) { + assertOptionalString(value, "refreshToken"); + assertOptionalString(value, "expiresAt"); + return; + } + } + + throw new Error("Invalid connector credentials"); +} + +function deriveEncryptionKey(secret: string): Buffer { + return Buffer.from(hkdfSync("sha256", secret, ENCRYPTION_KEY_SALT, ENCRYPTION_KEY_INFO, 32)); +} + +function encodeBase64Url(value: Buffer): string { + return value.toString("base64url"); +} + +function decodeBase64Url(value: string): Buffer { + return Buffer.from(value, "base64url"); +} + +function isSecretPayload(value: object): value is { secret: string } { + return hasNonEmptyString(value, "secret"); +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function hasNonEmptyString(value: object, key: string): boolean { + const record = value as Record; + return typeof record[key] === "string" && record[key].trim().length > 0; +} + +function assertOptionalString(value: object, key: string) { + const record = value as Record; + if (record[key] !== undefined && typeof record[key] !== "string") { + throw new Error("Invalid connector credentials"); + } +} diff --git a/packages/connectors/src/github.ts b/packages/connectors/src/github.ts new file mode 100644 index 00000000..c365a65b --- /dev/null +++ b/packages/connectors/src/github.ts @@ -0,0 +1,571 @@ +import { createSign } from "node:crypto"; +import { isSupportedCodePath } from "./code-files"; +import type { + FetchLike, + GitHubConnectorCredentials, + NormalizedWebhookEvent, + ProviderBranch, + ProviderCodeFile, + ProviderInstallationAccount, + ProviderRepository, + ProviderRepositoryClient, + ProviderRepositoryDelta, + ProviderRepositorySnapshot, +} from "./types"; +import { ConnectorProviderError } from "./types"; +import { branchNameFromGitRef, verifyHmacSha256Signature } from "./webhooks"; + +export const GITHUB_API_BASE_URL = "https://api.github.com"; +export const MAX_REPOSITORY_CODE_FILES = 1_000; +export const MAX_REPOSITORY_CODE_BYTES = 100 * 1024 * 1024; +export const MAX_REPOSITORY_CODE_FILE_BYTES = 2 * 1024 * 1024; + +const GITHUB_JWT_TTL_SECONDS = 9 * 60; +const SKIPPED_PATH_SEGMENTS: Record = { + ".git": true, + ".next": true, + ".turbo": true, + build: true, + coverage: true, + dist: true, + node_modules: true, + out: true, + vendor: true, +}; + +type GitHubClientOptions = { + installationToken: string; + apiBaseUrl?: string; + fetch?: FetchLike; +}; + +type InstallationTokenOptions = { + credentials: GitHubConnectorCredentials; + installationId: string; + apiBaseUrl?: string; + fetch?: FetchLike; + now?: Date; +}; + +type SnapshotOptions = GitHubClientOptions & { + repository: ProviderRepository; + branch: string; + commitSha?: string; +}; + +export function createGitHubAppJwt(options: { + appId: string; + privateKeyPem: string; + now?: Date; + expiresInSeconds?: number; +}): string { + const nowSeconds = Math.floor((options.now?.getTime() ?? Date.now()) / 1_000); + const header = encodeJsonBase64Url({ alg: "RS256", typ: "JWT" }); + const payload = encodeJsonBase64Url({ + iat: nowSeconds - 60, + exp: nowSeconds + (options.expiresInSeconds ?? GITHUB_JWT_TTL_SECONDS), + iss: options.appId, + }); + const signingInput = `${header}.${payload}`; + const signature = createSign("RSA-SHA256").update(signingInput).sign(options.privateKeyPem, "base64url"); + return `${signingInput}.${signature}`; +} + +export async function createGitHubInstallationToken(options: InstallationTokenOptions): Promise<{ + token: string; + expiresAt: string; +}> { + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const response = await (options.fetch ?? fetch)( + `${apiBaseUrl}/app/installations/${encodeURIComponent(options.installationId)}/access_tokens`, + { + method: "POST", + headers: githubHeaders( + createGitHubAppJwt({ + appId: options.credentials.appId, + privateKeyPem: options.credentials.privateKeyPem, + now: options.now, + }) + ), + body: JSON.stringify({ permissions: { contents: "read", metadata: "read" } }), + } + ); + const json = await readJson(response); + if (!response.ok || !isObject(json) || typeof json.token !== "string" || typeof json.expires_at !== "string") { + throw new ConnectorProviderError("auth", "GitHub installation token request failed"); + } + return { token: json.token, expiresAt: json.expires_at }; +} + +export async function getGitHubInstallationAccount( + options: InstallationTokenOptions +): Promise { + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const response = await (options.fetch ?? fetch)( + `${apiBaseUrl}/app/installations/${encodeURIComponent(options.installationId)}`, + { + headers: githubHeaders( + createGitHubAppJwt({ + appId: options.credentials.appId, + privateKeyPem: options.credentials.privateKeyPem, + now: options.now, + }) + ), + } + ); + const json = await readJson(response); + if (!response.ok || !isObject(json) || !isObject(json.account) || typeof json.account.login !== "string") { + throw new ConnectorProviderError("provider", "GitHub installation response is invalid"); + } + + return { + login: json.account.login, + type: gitHubAccountType(json.account.type), + repositorySelection: + json.repository_selection === "all" || json.repository_selection === "selected" + ? json.repository_selection + : "unknown", + }; +} + +export function createGitHubClient(options: GitHubClientOptions): ProviderRepositoryClient { + return { + provider: "github", + async getRepository(repositoryId) { + return getGitHubRepository(options, repositoryId); + }, + async listRepositories() { + return listGitHubInstallationRepositories(options); + }, + async listBranches(repository) { + return listGitHubBranches({ ...options, repository }); + }, + async loadRepositorySnapshot(repository, branch, commitSha) { + return loadGitHubRepositorySnapshot({ ...options, repository, branch, commitSha }); + }, + async compareRepository(repository, fromCommitSha, toCommitSha) { + return compareGitHubRepository({ ...options, repository, fromCommitSha, toCommitSha }); + }, + async readFile(repository, path, commitSha) { + return readGitHubRepositoryFile({ ...options, repository, path, commitSha }); + }, + }; +} + +export async function getGitHubRepository( + options: GitHubClientOptions, + repositoryId: string +): Promise { + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const path = repositoryId.includes("/") + ? `/repos/${encodePathSegments(repositoryId)}` + : `/repositories/${encodeURIComponent(repositoryId)}`; + return mapGitHubRepository(await getGitHubJson(`${apiBaseUrl}${path}`, options.installationToken, options.fetch)); +} + +export async function listGitHubInstallationRepositories(options: GitHubClientOptions): Promise { + const repositories: ProviderRepository[] = []; + for (let page = 1; ; page += 1) { + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const json = await getGitHubJson( + `${apiBaseUrl}/installation/repositories?per_page=100&page=${page}`, + options.installationToken, + options.fetch + ); + if (!isObject(json) || !Array.isArray(json.repositories)) { + throw new ConnectorProviderError("provider", "GitHub repository response is invalid"); + } + for (const repo of json.repositories) { + repositories.push(mapGitHubRepository(repo)); + } + if (json.repositories.length < 100) { + return repositories; + } + } +} + +export async function listGitHubBranches( + options: GitHubClientOptions & { repository: ProviderRepository } +): Promise { + const [owner, repo] = splitFullName(options.repository.fullName); + const branches: ProviderBranch[] = []; + for (let page = 1; ; page += 1) { + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const json = await getGitHubJson( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches?per_page=100&page=${page}`, + options.installationToken, + options.fetch + ); + if (!Array.isArray(json)) { + throw new ConnectorProviderError("provider", "GitHub branches response is invalid"); + } + for (const branch of json) { + if ( + isObject(branch) && + typeof branch.name === "string" && + isObject(branch.commit) && + typeof branch.commit.sha === "string" + ) { + branches.push({ name: branch.name, commitSha: branch.commit.sha }); + } + } + if (json.length < 100) { + return branches; + } + } +} + +export async function loadGitHubRepositorySnapshot(options: SnapshotOptions): Promise { + const [owner, repo] = splitFullName(options.repository.fullName); + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + let treeSha: string; + let branch: ProviderBranch; + if (options.commitSha) { + const commitJson = await getGitHubJson( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${encodeURIComponent(options.commitSha)}`, + options.installationToken, + options.fetch + ); + if (!isObject(commitJson) || !isObject(commitJson.tree) || typeof commitJson.tree.sha !== "string") { + throw new ConnectorProviderError("not-found", "GitHub commit was not found"); + } + treeSha = commitJson.tree.sha; + branch = { name: options.branch, commitSha: options.commitSha }; + } else { + const branchJson = await getGitHubJson( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches/${encodeURIComponent(options.branch)}`, + options.installationToken, + options.fetch + ); + if (!isObject(branchJson) || !isObject(branchJson.commit) || typeof branchJson.commit.sha !== "string") { + throw new ConnectorProviderError("not-found", "GitHub branch was not found"); + } + treeSha = + isObject(branchJson.commit.commit) && + isObject(branchJson.commit.commit.tree) && + typeof branchJson.commit.commit.tree.sha === "string" + ? branchJson.commit.commit.tree.sha + : branchJson.commit.sha; + branch = { name: options.branch, commitSha: branchJson.commit.sha }; + } + const treeJson = await getGitHubJson( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(treeSha)}?recursive=1`, + options.installationToken, + options.fetch + ); + if (!isObject(treeJson) || !Array.isArray(treeJson.tree)) { + throw new ConnectorProviderError("provider", "GitHub tree response is invalid"); + } + if (treeJson.truncated === true) { + throw new ConnectorProviderError("limit", "GitHub repository tree is too large to load completely"); + } + + const files: ProviderCodeFile[] = []; + let totalBytes = 0; + for (const item of treeJson.tree) { + if (!isGitHubBlob(item) || !shouldLoadCodePath(item.path) || item.size > MAX_REPOSITORY_CODE_FILE_BYTES) { + continue; + } + if (files.length + 1 > MAX_REPOSITORY_CODE_FILES) { + throw new ConnectorProviderError("limit", "Repository contains too many supported code files"); + } + if (totalBytes + item.size > MAX_REPOSITORY_CODE_BYTES) { + throw new ConnectorProviderError("limit", "Repository contains too much supported code"); + } + const blobJson = await getGitHubJson( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/blobs/${encodeURIComponent(item.sha)}`, + options.installationToken, + options.fetch + ); + if (!isObject(blobJson) || typeof blobJson.content !== "string" || blobJson.encoding !== "base64") { + throw new ConnectorProviderError("provider", "GitHub blob response is invalid"); + } + const content = Buffer.from(blobJson.content.replaceAll("\n", ""), "base64").toString("utf8"); + const size = Buffer.byteLength(content, "utf8"); + if (size > MAX_REPOSITORY_CODE_FILE_BYTES || totalBytes + size > MAX_REPOSITORY_CODE_BYTES) { + throw new ConnectorProviderError("limit", "Repository contains too much supported code"); + } + files.push({ + path: item.path, + size, + checksum: item.sha, + htmlUrl: `https://github.com/${options.repository.fullName}/blob/${branch.commitSha}/${item.path}`, + rawUrl: `https://raw.githubusercontent.com/${options.repository.fullName}/${branch.commitSha}/${item.path}`, + content, + }); + totalBytes += size; + } + + return { repository: options.repository, branch, commitSha: branch.commitSha, files }; +} +export async function compareGitHubRepository( + options: GitHubClientOptions & { + repository: ProviderRepository; + fromCommitSha: string; + toCommitSha: string; + } +): Promise { + const [owner, repo] = splitFullName(options.repository.fullName); + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const json = await getGitHubJson( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/compare/${encodeURIComponent(options.fromCommitSha)}...${encodeURIComponent(options.toCommitSha)}`, + options.installationToken, + options.fetch + ); + if (!isObject(json) || !isGitHubCompareStatus(json.status)) { + throw new ConnectorProviderError("provider", "GitHub compare response is invalid"); + } + + if (json.status === "behind" || json.status === "diverged") { + return { + fromCommitSha: options.fromCommitSha, + toCommitSha: options.toCommitSha, + isIncremental: false, + changes: [], + }; + } + + if (!Array.isArray(json.files)) { + throw new ConnectorProviderError("provider", "GitHub compare response is invalid"); + } + + const changes: ProviderRepositoryDelta["changes"] = []; + for (const entry of json.files) { + if (!isGitHubCompareFile(entry)) { + throw new ConnectorProviderError("provider", "GitHub compare response is invalid"); + } + + switch (entry.status) { + case "added": + case "copied": + if (shouldLoadCodePath(entry.filename)) { + changes.push({ status: "added", newPath: entry.filename }); + } + break; + case "modified": + case "changed": + if (shouldLoadCodePath(entry.filename)) { + changes.push({ status: "modified", newPath: entry.filename }); + } + break; + case "removed": + if (shouldLoadCodePath(entry.filename)) { + changes.push({ status: "deleted", oldPath: entry.filename }); + } + break; + case "renamed": { + const oldSupported = shouldLoadCodePath(entry.previous_filename); + const newSupported = shouldLoadCodePath(entry.filename); + if (oldSupported && newSupported) { + changes.push({ status: "renamed", oldPath: entry.previous_filename, newPath: entry.filename }); + } else if (oldSupported) { + changes.push({ status: "deleted", oldPath: entry.previous_filename }); + } else if (newSupported) { + changes.push({ status: "added", newPath: entry.filename }); + } + break; + } + case "unchanged": + break; + default: + throw new ConnectorProviderError("provider", "GitHub compare response is invalid"); + } + } + + return { + fromCommitSha: options.fromCommitSha, + toCommitSha: options.toCommitSha, + isIncremental: true, + changes, + }; +} + +export async function readGitHubRepositoryFile( + options: GitHubClientOptions & { + repository: ProviderRepository; + path: string; + commitSha: string; + } +): Promise { + const [owner, repo] = splitFullName(options.repository.fullName); + const apiBaseUrl = normalizeApiBaseUrl(options.apiBaseUrl ?? GITHUB_API_BASE_URL); + const response = await (options.fetch ?? fetch)( + `${apiBaseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodePathSegments(options.path)}?ref=${encodeURIComponent(options.commitSha)}`, + { + headers: { + ...githubHeaders(options.installationToken), + Accept: "application/vnd.github.raw+json", + }, + } + ); + + if (!response.ok) { + throw new ConnectorProviderError("not-found", "GitHub file was not found"); + } + + const contentLength = response.headers.get("content-length"); + if (contentLength && Number(contentLength) > MAX_REPOSITORY_CODE_FILE_BYTES) { + throw new ConnectorProviderError("limit", "Repository file is too large"); + } + + const content = await response.text(); + if (Buffer.byteLength(content, "utf8") > MAX_REPOSITORY_CODE_FILE_BYTES) { + throw new ConnectorProviderError("limit", "Repository file is too large"); + } + + return content; +} + +export function verifyGitHubWebhookSignature(options: { + body: string | Buffer | Uint8Array; + webhookSecret: string; + signatureHeader: string | null | undefined; +}): boolean { + return verifyHmacSha256Signature({ + body: options.body, + secret: options.webhookSecret, + signatureHeader: options.signatureHeader, + prefix: "sha256=", + }); +} + +export function normalizeGitHubWebhookEvent(options: { + eventName: string; + deliveryId: string; + payload: unknown; +}): NormalizedWebhookEvent { + const payload = isObject(options.payload) ? options.payload : {}; + const repository = isObject(payload.repository) ? payload.repository : null; + const installation = isObject(payload.installation) ? payload.installation : null; + return { + provider: "github", + deliveryId: options.deliveryId, + eventName: options.eventName, + repositoryId: + repository && (typeof repository.id === "string" || typeof repository.id === "number") + ? String(repository.id) + : null, + repositoryFullName: repository && typeof repository.full_name === "string" ? repository.full_name : null, + branch: branchNameFromGitRef(payload.ref), + commitSha: typeof payload.after === "string" ? payload.after : null, + installationId: + installation && (typeof installation.id === "string" || typeof installation.id === "number") + ? String(installation.id) + : undefined, + raw: options.payload, + }; +} + +async function getGitHubJson(url: string, token: string, fetchImpl: FetchLike | undefined): Promise { + const response = await (fetchImpl ?? fetch)(url, { headers: githubHeaders(token) }); + const json = await readJson(response); + if (!response.ok) { + throw new ConnectorProviderError( + response.status === 404 ? "not-found" : "provider", + "GitHub API request failed" + ); + } + return json; +} + +function githubHeaders(token: string): Record { + return { + accept: "application/vnd.github+json", + authorization: `Bearer ${token}`, + "content-type": "application/json", + "x-github-api-version": "2022-11-28", + }; +} + +function mapGitHubRepository(value: unknown): ProviderRepository { + if ( + !isObject(value) || + typeof value.full_name !== "string" || + typeof value.name !== "string" || + (typeof value.id !== "string" && typeof value.id !== "number") + ) { + throw new ConnectorProviderError("provider", "GitHub repository response is invalid"); + } + return { + provider: "github", + id: String(value.id), + fullName: value.full_name, + name: value.name, + htmlUrl: typeof value.html_url === "string" ? value.html_url : `https://github.com/${value.full_name}`, + defaultBranch: typeof value.default_branch === "string" ? value.default_branch : null, + private: value.private === true, + }; +} + +function splitFullName(fullName: string): [string, string] { + const [owner, repo, ...extra] = fullName.split("/"); + if (!owner || !repo || extra.length > 0) { + throw new ConnectorProviderError("validation", "Repository full name is invalid"); + } + return [owner, repo]; +} + +function encodePathSegments(value: string): string { + return value.split("/").map(encodeURIComponent).join("/"); +} + +function gitHubAccountType(value: unknown): ProviderInstallationAccount["type"] { + if (value === "User") { + return "user"; + } + if (value === "Organization") { + return "organization"; + } + return null; +} + +async function readJson(response: Response): Promise { + const text = await response.text(); + return text.length === 0 ? null : JSON.parse(text); +} + +function shouldLoadCodePath(filePath: string): boolean { + const normalized = filePath.replaceAll("\\", "/"); + return ( + isSupportedCodePath(normalized) && + normalized.split("/").every((segment) => SKIPPED_PATH_SEGMENTS[segment] !== true) + ); +} + +function normalizeApiBaseUrl(value: string): string { + return value.replace(/\/+$/, ""); +} + +function encodeJsonBase64Url(value: unknown): string { + return Buffer.from(JSON.stringify(value), "utf8").toString("base64url"); +} + +function isGitHubBlob(value: unknown): value is { path: string; sha: string; size: number } { + return ( + isObject(value) && + value.type === "blob" && + typeof value.path === "string" && + typeof value.sha === "string" && + typeof value.size === "number" + ); +} +function isGitHubCompareStatus(value: unknown): value is "ahead" | "behind" | "diverged" | "identical" { + return value === "ahead" || value === "behind" || value === "diverged" || value === "identical"; +} + +function isGitHubCompareFile(value: unknown): value is { + filename: string; + status: "added" | "changed" | "copied" | "modified" | "removed" | "renamed" | "unchanged"; + previous_filename: string; +} { + return ( + isObject(value) && + typeof value.filename === "string" && + typeof value.status === "string" && + (value.status !== "renamed" || typeof value.previous_filename === "string") + ); +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/connectors/src/gitlab.ts b/packages/connectors/src/gitlab.ts new file mode 100644 index 00000000..baa48e02 --- /dev/null +++ b/packages/connectors/src/gitlab.ts @@ -0,0 +1,404 @@ +import { isSupportedCodePath } from "./code-files"; +import type { + FetchLike, + NormalizedWebhookEvent, + ProviderBranch, + ProviderCodeFile, + ProviderRepository, + ProviderRepositoryClient, + ProviderRepositoryDelta, + ProviderRepositorySnapshot, +} from "./types"; +import { ConnectorProviderError } from "./types"; +import { branchNameFromGitRef, verifySharedSecretToken } from "./webhooks"; +import { MAX_REPOSITORY_CODE_BYTES, MAX_REPOSITORY_CODE_FILES, MAX_REPOSITORY_CODE_FILE_BYTES } from "./github"; + +const SKIPPED_PATH_SEGMENTS: Record = { + ".git": true, + ".next": true, + ".turbo": true, + build: true, + coverage: true, + dist: true, + node_modules: true, + out: true, + vendor: true, +}; + +type GitLabClientOptions = { + baseUrl: string; + accessToken: string; + fetch?: FetchLike; +}; + +type SnapshotOptions = GitLabClientOptions & { + repository: ProviderRepository; + branch: string; + commitSha?: string; +}; + +export function normalizeGitLabBaseUrl(value: string): string { + const parsed = new URL(value.trim()); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + throw new ConnectorProviderError("validation", "GitLab base URL must use HTTP or HTTPS"); + } + parsed.pathname = parsed.pathname.replace(/\/+$/, ""); + parsed.search = ""; + parsed.hash = ""; + return parsed.toString().replace(/\/+$/, ""); +} + +export function createGitLabClient(options: GitLabClientOptions): ProviderRepositoryClient { + return { + provider: "gitlab", + async getRepository(repositoryId) { + return getGitLabProject(options, repositoryId); + }, + async listRepositories() { + return listGitLabProjects(options); + }, + async listBranches(repository) { + return listGitLabBranches({ ...options, repository }); + }, + async loadRepositorySnapshot(repository, branch, commitSha) { + return loadGitLabRepositorySnapshot({ ...options, repository, branch, commitSha }); + }, + async compareRepository(repository, fromCommitSha, toCommitSha) { + return compareGitLabRepository({ ...options, repository, fromCommitSha, toCommitSha }); + }, + async readFile(repository, path, commitSha) { + return readGitLabRepositoryFile({ ...options, repository, path, commitSha }); + }, + }; +} + +export async function getGitLabProject( + options: GitLabClientOptions, + repositoryId: string +): Promise { + return mapGitLabProject( + await getGitLabJson( + `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(repositoryId)}`, + options.accessToken, + options.fetch + ) + ); +} + +export async function listGitLabProjects(options: GitLabClientOptions): Promise { + const repositories: ProviderRepository[] = []; + for (let page = 1; ; page += 1) { + const json = await getGitLabJson( + `${gitLabApiBase(options.baseUrl)}/projects?membership=true&per_page=100&page=${page}`, + options.accessToken, + options.fetch + ); + if (!Array.isArray(json)) { + throw new ConnectorProviderError("provider", "GitLab projects response is invalid"); + } + for (const project of json) { + repositories.push(mapGitLabProject(project)); + } + if (json.length < 100) { + return repositories; + } + } +} + +export async function listGitLabBranches( + options: GitLabClientOptions & { repository: ProviderRepository } +): Promise { + const branches: ProviderBranch[] = []; + for (let page = 1; ; page += 1) { + const json = await getGitLabJson( + `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(options.repository.id)}/repository/branches?per_page=100&page=${page}`, + options.accessToken, + options.fetch + ); + if (!Array.isArray(json)) { + throw new ConnectorProviderError("provider", "GitLab branches response is invalid"); + } + for (const branch of json) { + if ( + isObject(branch) && + typeof branch.name === "string" && + isObject(branch.commit) && + typeof branch.commit.id === "string" + ) { + branches.push({ name: branch.name, commitSha: branch.commit.id }); + } + } + if (json.length < 100) { + return branches; + } + } +} + +export async function loadGitLabRepositorySnapshot(options: SnapshotOptions): Promise { + let commitSha = options.commitSha; + if (!commitSha) { + const branchJson = await getGitLabJson( + `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(options.repository.id)}/repository/branches/${encodeURIComponent(options.branch)}`, + options.accessToken, + options.fetch + ); + if (!isObject(branchJson) || !isObject(branchJson.commit) || typeof branchJson.commit.id !== "string") { + throw new ConnectorProviderError("not-found", "GitLab branch was not found"); + } + commitSha = branchJson.commit.id; + } + const branch: ProviderBranch = { name: options.branch, commitSha }; + const tree = await listGitLabTree(options, commitSha); + const files: ProviderCodeFile[] = []; + let totalBytes = 0; + + for (const item of tree) { + if (!isGitLabBlob(item) || !shouldLoadCodePath(item.path)) { + continue; + } + if (files.length + 1 > MAX_REPOSITORY_CODE_FILES) { + throw new ConnectorProviderError("limit", "Repository contains too many supported code files"); + } + const rawUrl = `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(options.repository.id)}/repository/files/${encodeURIComponent(item.path)}/raw?ref=${encodeURIComponent(branch.commitSha)}`; + const content = await getGitLabText(rawUrl, options.accessToken, options.fetch); + const size = Buffer.byteLength(content, "utf8"); + if (size > MAX_REPOSITORY_CODE_FILE_BYTES) { + continue; + } + if (totalBytes + size > MAX_REPOSITORY_CODE_BYTES) { + throw new ConnectorProviderError("limit", "Repository contains too much supported code"); + } + files.push({ + path: item.path, + size, + checksum: item.id, + htmlUrl: `${options.repository.htmlUrl}/-/blob/${branch.commitSha}/${item.path}`, + rawUrl, + content, + }); + totalBytes += size; + } + + return { repository: options.repository, branch, commitSha: branch.commitSha, files }; +} +export async function compareGitLabRepository( + options: GitLabClientOptions & { + repository: ProviderRepository; + fromCommitSha: string; + toCommitSha: string; + } +): Promise { + const json = await getGitLabJson( + `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(options.repository.id)}/repository/compare?from=${encodeURIComponent(options.fromCommitSha)}&to=${encodeURIComponent(options.toCommitSha)}&straight=true`, + options.accessToken, + options.fetch + ); + if (!isObject(json)) { + throw new ConnectorProviderError("provider", "GitLab compare response is invalid"); + } + + if (json.compare_timeout === true) { + return { + fromCommitSha: options.fromCommitSha, + toCommitSha: options.toCommitSha, + isIncremental: false, + changes: [], + }; + } + + if (!Array.isArray(json.diffs)) { + throw new ConnectorProviderError("provider", "GitLab compare response is invalid"); + } + + const changes: ProviderRepositoryDelta["changes"] = []; + for (const entry of json.diffs) { + if (!isGitLabCompareFile(entry)) { + throw new ConnectorProviderError("provider", "GitLab compare response is invalid"); + } + + if (entry.renamed_file) { + const oldSupported = shouldLoadCodePath(entry.old_path); + const newSupported = shouldLoadCodePath(entry.new_path); + if (oldSupported && newSupported) { + changes.push({ status: "renamed", oldPath: entry.old_path, newPath: entry.new_path }); + } else if (oldSupported) { + changes.push({ status: "deleted", oldPath: entry.old_path }); + } else if (newSupported) { + changes.push({ status: "added", newPath: entry.new_path }); + } + continue; + } + + if (entry.deleted_file) { + if (shouldLoadCodePath(entry.old_path)) { + changes.push({ status: "deleted", oldPath: entry.old_path }); + } + continue; + } + + if (entry.new_file) { + if (shouldLoadCodePath(entry.new_path)) { + changes.push({ status: "added", newPath: entry.new_path }); + } + continue; + } + + if (shouldLoadCodePath(entry.new_path)) { + changes.push({ status: "modified", newPath: entry.new_path }); + } + } + + return { + fromCommitSha: options.fromCommitSha, + toCommitSha: options.toCommitSha, + isIncremental: true, + changes, + }; +} + +export async function readGitLabRepositoryFile( + options: GitLabClientOptions & { + repository: ProviderRepository; + path: string; + commitSha: string; + } +): Promise { + const rawUrl = `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(options.repository.id)}/repository/files/${encodeURIComponent(options.path)}/raw?ref=${encodeURIComponent(options.commitSha)}`; + const content = await getGitLabText(rawUrl, options.accessToken, options.fetch); + if (Buffer.byteLength(content, "utf8") > MAX_REPOSITORY_CODE_FILE_BYTES) { + throw new ConnectorProviderError("limit", "Repository file is too large"); + } + return content; +} + +export function verifyGitLabWebhookToken(options: { + webhookSecret: string; + tokenHeader: string | null | undefined; +}): boolean { + return verifySharedSecretToken(options.tokenHeader, options.webhookSecret); +} + +export function normalizeGitLabWebhookEvent(options: { + eventName: string; + deliveryId: string; + payload: unknown; +}): NormalizedWebhookEvent { + const payload = isObject(options.payload) ? options.payload : {}; + const project = isObject(payload.project) ? payload.project : null; + return { + provider: "gitlab", + deliveryId: options.deliveryId, + eventName: options.eventName, + repositoryId: + project && (typeof project.id === "string" || typeof project.id === "number") ? String(project.id) : null, + repositoryFullName: + project && typeof project.path_with_namespace === "string" ? project.path_with_namespace : null, + branch: branchNameFromGitRef(payload.ref), + commitSha: typeof payload.after === "string" ? payload.after : null, + raw: options.payload, + }; +} + +async function listGitLabTree(options: SnapshotOptions, ref: string): Promise { + const entries: unknown[] = []; + for (let page = 1; ; page += 1) { + const json = await getGitLabJson( + `${gitLabApiBase(options.baseUrl)}/projects/${encodeURIComponent(options.repository.id)}/repository/tree?recursive=true&per_page=100&page=${page}&ref=${encodeURIComponent(ref)}`, + options.accessToken, + options.fetch + ); + if (!Array.isArray(json)) { + throw new ConnectorProviderError("provider", "GitLab tree response is invalid"); + } + entries.push(...json); + if (json.length < 100) { + return entries; + } + } +} + +async function getGitLabJson(url: string, token: string, fetchImpl: FetchLike | undefined): Promise { + const response = await (fetchImpl ?? fetch)(url, { + headers: { authorization: `Bearer ${token}`, accept: "application/json" }, + }); + const text = await response.text(); + if (!response.ok) { + throw new ConnectorProviderError( + response.status === 404 ? "not-found" : "provider", + "GitLab API request failed" + ); + } + return text.length === 0 ? null : JSON.parse(text); +} + +async function getGitLabText(url: string, token: string, fetchImpl: FetchLike | undefined): Promise { + const response = await (fetchImpl ?? fetch)(url, { headers: { authorization: `Bearer ${token}` } }); + const contentLength = response.headers.get("content-length"); + if (contentLength && Number(contentLength) > MAX_REPOSITORY_CODE_FILE_BYTES) { + throw new ConnectorProviderError("limit", "Repository contains a code file that is too large"); + } + const text = await response.text(); + if (!response.ok) { + throw new ConnectorProviderError( + response.status === 404 ? "not-found" : "provider", + "GitLab raw file request failed" + ); + } + return text; +} + +function mapGitLabProject(value: unknown): ProviderRepository { + if ( + !isObject(value) || + typeof value.path_with_namespace !== "string" || + typeof value.name !== "string" || + (typeof value.id !== "string" && typeof value.id !== "number") + ) { + throw new ConnectorProviderError("provider", "GitLab project response is invalid"); + } + return { + provider: "gitlab", + id: String(value.id), + fullName: value.path_with_namespace, + name: value.name, + htmlUrl: typeof value.web_url === "string" ? value.web_url : "", + defaultBranch: typeof value.default_branch === "string" ? value.default_branch : null, + private: value.visibility !== "public", + }; +} + +function gitLabApiBase(baseUrl: string): string { + return `${normalizeGitLabBaseUrl(baseUrl)}/api/v4`; +} + +function shouldLoadCodePath(filePath: string): boolean { + const normalized = filePath.replaceAll("\\", "/"); + return ( + isSupportedCodePath(normalized) && + normalized.split("/").every((segment) => SKIPPED_PATH_SEGMENTS[segment] !== true) + ); +} + +function isGitLabBlob(value: unknown): value is { id: string; path: string } { + return isObject(value) && value.type === "blob" && typeof value.id === "string" && typeof value.path === "string"; +} +function isGitLabCompareFile(value: unknown): value is { + old_path: string; + new_path: string; + new_file: boolean; + renamed_file: boolean; + deleted_file: boolean; +} { + return ( + isObject(value) && + typeof value.old_path === "string" && + typeof value.new_path === "string" && + typeof value.new_file === "boolean" && + typeof value.renamed_file === "boolean" && + typeof value.deleted_file === "boolean" + ); +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/connectors/src/index.ts b/packages/connectors/src/index.ts new file mode 100644 index 00000000..ee348555 --- /dev/null +++ b/packages/connectors/src/index.ts @@ -0,0 +1,5 @@ +export * from "./credentials"; +export * from "./types"; +export * from "./github"; +export * from "./gitlab"; +export * from "./webhooks"; diff --git a/packages/connectors/src/types.ts b/packages/connectors/src/types.ts new file mode 100644 index 00000000..bfef98f8 --- /dev/null +++ b/packages/connectors/src/types.ts @@ -0,0 +1,135 @@ +export type ConnectorProvider = "github" | "gitlab"; + +export type ProviderRepository = { + provider: ConnectorProvider; + id: string; + fullName: string; + name: string; + htmlUrl: string; + defaultBranch: string | null; + private: boolean; +}; + +export type ProviderBranch = { + name: string; + commitSha: string; +}; + +export type ProviderInstallationAccount = { + login: string; + type: "user" | "organization" | "group" | null; + repositorySelection: "all" | "selected" | "unknown"; +}; + +export type ProviderCodeFile = { + path: string; + size: number; + checksum: string; + htmlUrl: string; + rawUrl?: string; + content: string; +}; + +export type ProviderRepositorySnapshot = { + repository: ProviderRepository; + branch: ProviderBranch; + commitSha: string; + files: ProviderCodeFile[]; +}; +export type ProviderRepositoryChange = + | { + status: "added" | "modified"; + newPath: string; + } + | { + status: "deleted"; + oldPath: string; + } + | { + status: "renamed"; + oldPath: string; + newPath: string; + }; + +export type ProviderRepositoryDelta = { + fromCommitSha: string; + toCommitSha: string; + isIncremental: boolean; + changes: ProviderRepositoryChange[]; +}; + +export type ConnectorCredentials = GitHubConnectorCredentials | GitLabConnectorCredentials; + +export type ConnectorInstallationCredentials = GitHubInstallationCredentials | GitLabInstallationCredentials; + +export type GitHubConnectorCredentials = { + provider: "github"; + appId: string; + privateKeyPem: string; + clientId?: string; + clientSecret?: string; + webhookSecret?: string; +}; + +export type GitHubInstallationCredentials = { + provider: "github"; + installationId: string; +}; + +export type GitLabConnectorCredentials = { + provider: "gitlab"; + baseUrl: string; + clientId: string; + clientSecret: string; + webhookSecret?: string; +}; + +export type GitLabInstallationCredentials = { + provider: "gitlab"; + accessToken: string; + refreshToken?: string; + expiresAt?: string; +}; + +export type NormalizedWebhookEvent = { + provider: ConnectorProvider; + deliveryId: string; + eventName: string; + repositoryId: string | null; + repositoryFullName: string | null; + branch: string | null; + commitSha: string | null; + installationId?: string; + raw: unknown; +}; + +export type ProviderRepositoryClient = { + readonly provider: ConnectorProvider; + getRepository(repositoryId: string): Promise; + listRepositories(): Promise; + listBranches(repository: ProviderRepository): Promise; + loadRepositorySnapshot( + repository: ProviderRepository, + branch: string, + commitSha?: string + ): Promise; + compareRepository( + repository: ProviderRepository, + fromCommitSha: string, + toCommitSha: string + ): Promise; + readFile(repository: ProviderRepository, path: string, commitSha: string): Promise; +}; + +export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise; + +export class ConnectorProviderError extends Error { + constructor( + public readonly kind: "auth" | "limit" | "not-found" | "provider" | "validation", + message: string, + options?: { cause?: unknown } + ) { + super(message, options); + this.name = "ConnectorProviderError"; + } +} diff --git a/packages/connectors/src/webhooks.ts b/packages/connectors/src/webhooks.ts new file mode 100644 index 00000000..fd61f16d --- /dev/null +++ b/packages/connectors/src/webhooks.ts @@ -0,0 +1,34 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +export function verifyHmacSha256Signature(options: { + body: string | Buffer | Uint8Array; + secret: string; + signatureHeader: string | null | undefined; + prefix: string; +}): boolean { + if (!options.signatureHeader || options.secret.length === 0) { + return false; + } + + const expected = `${options.prefix}${createHmac("sha256", options.secret).update(options.body).digest("hex")}`; + const received = Buffer.from(options.signatureHeader, "utf8"); + const expectedBytes = Buffer.from(expected, "utf8"); + return received.byteLength === expectedBytes.byteLength && timingSafeEqual(received, expectedBytes); +} + +export function verifySharedSecretToken(receivedToken: string | null | undefined, expectedToken: string): boolean { + if (!receivedToken || expectedToken.length === 0) { + return false; + } + + const received = Buffer.from(receivedToken, "utf8"); + const expected = Buffer.from(expectedToken, "utf8"); + return received.byteLength === expected.byteLength && timingSafeEqual(received, expected); +} + +export function branchNameFromGitRef(ref: unknown): string | null { + if (typeof ref !== "string" || !ref.startsWith("refs/heads/")) { + return null; + } + return ref.slice("refs/heads/".length) || null; +} diff --git a/packages/connectors/tsconfig.json b/packages/connectors/tsconfig.json new file mode 100644 index 00000000..56cd751c --- /dev/null +++ b/packages/connectors/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "types": ["bun"] + }, + "exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/contracts/src/file-types.ts b/packages/contracts/src/file-types.ts index fd8b8fa9..a72e1043 100644 --- a/packages/contracts/src/file-types.ts +++ b/packages/contracts/src/file-types.ts @@ -17,6 +17,7 @@ export const FILE_TYPE_VALUES = [ "xml", "yaml", "toml", + "code", "text", ] as const; diff --git a/packages/contracts/src/routes.ts b/packages/contracts/src/routes.ts index 93ef44f2..34cef89b 100644 --- a/packages/contracts/src/routes.ts +++ b/packages/contracts/src/routes.ts @@ -498,6 +498,162 @@ export type GraphSuggestionApplySuccessData = { workflowRunId: string | null; warnings?: string[]; }; +export type ConnectorProvider = "github" | "gitlab"; +export type ConnectorStatus = "draft" | "active" | "disabled"; +export type ConnectorInstallationStatus = "active" | "disabled" | "pending"; +export type ConnectorAccountType = "user" | "organization" | "group" | null; +export type ConnectorRepositorySelection = "all" | "selected" | "unknown"; +export type RepositoryGraphBindingSyncStatus = "pending" | "syncing" | "synced" | "failed"; + +export type ConnectorRecord = { + id: string; + provider: ConnectorProvider; + name: string; + slug: string; + status: ConnectorStatus; + appId: string | null; + clientId: string | null; + createdAt: string; + updatedAt: string; +}; + +export type ConnectorInstallationRecord = { + id: string; + connectorId: string; + provider: ConnectorProvider; + providerInstallationId: string; + providerAccountLogin: string; + providerAccountType: ConnectorAccountType; + organizationId: string | null; + teamId: string | null; + repositorySelection: ConnectorRepositorySelection; + status: ConnectorInstallationStatus; + createdAt: string; + updatedAt: string; +}; + +export type ConnectorRepositoryRecord = { + provider: ConnectorProvider; + id: string; + fullName: string; + name: string; + htmlUrl: string; + defaultBranch: string | null; + private: boolean; +}; + +export type ConnectorBranchRecord = { + name: string; + commitSha: string; +}; + +export type RepositoryGraphBindingRecord = { + id: string; + graphId: string; + connectorInstallationId: string; + provider: ConnectorProvider; + providerRepositoryId: string; + repositoryFullName: string; + repositoryHtmlUrl: string; + branch: string; + lastSeenCommitSha: string | null; + lastSyncedCommitSha: string | null; + syncStatus: RepositoryGraphBindingSyncStatus; + syncErrorCode: string | null; + webhookEnabled: boolean; + createdAt: string; + updatedAt: string; +}; + +export type GitHubConnectorManifestStartInput = { + name: string; +}; + +export type GitHubConnectorManifestStartSuccessData = { + manifestUrl: string; + state: string; +}; + +export type ConnectorConnectStartSuccessData = { + redirectUrl: string; +}; + +export type GitLabConnectorCreateInput = { + name: string; + baseUrl: string; + clientId: string; + clientSecret: string; + webhookSecret: string; +}; + +export type ConnectorOwnerScopeInput = + | { + kind: "organization"; + } + | { + kind: "team"; + teamId: string; + }; + +export type RepositoryGraphCreateInput = { + connectorInstallationId: string; + repositoryId: string; + repositoryFullName: string; + repositoryHtmlUrl: string; + branch: string; + name: string; + owner: ConnectorOwnerScopeInput; +}; + +export type RepositoryGraphCreateSuccessData = { + graph: GraphRecord; + binding: RepositoryGraphBindingRecord; + workflowRunId: string | null; +}; + +export type RepositoryGraphBindingSyncSuccessData = { + binding: RepositoryGraphBindingRecord; + workflowRunId: string | null; +}; + +export type ConnectorListResponse = ApiResponse; + +export type ConnectorConnectStartResponse = ApiResponse< + ConnectorConnectStartSuccessData, + "UNAUTHORIZED" | "FORBIDDEN" | "GRAPH_NOT_FOUND" | "INVALID_GRAPH_OWNER" | "INTERNAL_SERVER_ERROR" +>; +export type GitHubConnectorManifestStartResponse = ApiResponse< + GitHubConnectorManifestStartSuccessData, + "UNAUTHORIZED" | "FORBIDDEN" | "INVALID_NAME" | "INTERNAL_SERVER_ERROR" +>; +export type GitLabConnectorCreateResponse = ApiResponse< + ConnectorRecord, + "UNAUTHORIZED" | "FORBIDDEN" | "INVALID_NAME" | "INTERNAL_SERVER_ERROR" +>; +export type ConnectorInstallationListResponse = ApiResponse< + ConnectorInstallationRecord[], + "UNAUTHORIZED" | "FORBIDDEN" | "INTERNAL_SERVER_ERROR" +>; +export type ConnectorRepositoryListResponse = ApiResponse< + ConnectorRepositoryRecord[], + "UNAUTHORIZED" | "FORBIDDEN" | "INTERNAL_SERVER_ERROR" +>; +export type ConnectorBranchListResponse = ApiResponse< + ConnectorBranchRecord[], + "UNAUTHORIZED" | "FORBIDDEN" | "INTERNAL_SERVER_ERROR" +>; +export type RepositoryGraphCreateResponse = ApiResponse< + RepositoryGraphCreateSuccessData, + "UNAUTHORIZED" | "FORBIDDEN" | "TEAM_NOT_FOUND" | "INVALID_GRAPH_OWNER" | "INVALID_NAME" | "INTERNAL_SERVER_ERROR" +>; +export type RepositoryGraphBindingResponse = ApiResponse< + RepositoryGraphBindingRecord, + "UNAUTHORIZED" | "FORBIDDEN" | "GRAPH_NOT_FOUND" | "INVALID_GRAPH_OWNER" | "INTERNAL_SERVER_ERROR" +>; +export type RepositoryGraphBindingSyncResponse = ApiResponse< + RepositoryGraphBindingSyncSuccessData, + "UNAUTHORIZED" | "FORBIDDEN" | "GRAPH_NOT_FOUND" | "INVALID_GRAPH_OWNER" | "INTERNAL_SERVER_ERROR" +>; export type TeamCreateResponse = ApiResponse< TeamCreateSuccessData, diff --git a/packages/contracts/src/source.ts b/packages/contracts/src/source.ts index 36c07cc7..59bdb6e1 100644 --- a/packages/contracts/src/source.ts +++ b/packages/contracts/src/source.ts @@ -18,6 +18,12 @@ export type TextUnitSourceChunk = text: string; startPage: number | null; endPage: number | null; + filePath?: string; + language?: string; + startLine?: number; + endLine?: number; + startColumn?: number; + endColumn?: number; regions?: SourceChunkRegion[]; } | { diff --git a/packages/db/package.json b/packages/db/package.json index b936370c..3803ae9b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -4,6 +4,7 @@ "type": "module", "exports": { "./utils": "./src/utils.ts", + "./source-validity": "./src/source-validity.ts", ".": "./src/index.ts", "./tables/*": "./src/tables/*.ts" }, diff --git a/packages/db/src/__tests__/migration-compat.test.ts b/packages/db/src/__tests__/migration-compat.test.ts index c81ef816..50568308 100644 --- a/packages/db/src/__tests__/migration-compat.test.ts +++ b/packages/db/src/__tests__/migration-compat.test.ts @@ -12,6 +12,30 @@ const jsonFileTypeMigration = new URL( "../../../../migrations/20260613080110_public_piledriver/migration.sql", import.meta.url ); +const codeGraphMigration = new URL( + "../../../../migrations/20260613184716_sturdy_triton/migration.sql", + import.meta.url +); +const codeGraphSnapshot = new URL("../../../../migrations/20260613184716_sturdy_triton/snapshot.json", import.meta.url); +const externalFileStorageMigration = new URL( + "../../../../migrations/20260613201908_mature_emma_frost/migration.sql", + import.meta.url +); +const externalFileStorageSnapshot = new URL( + "../../../../migrations/20260613201908_mature_emma_frost/snapshot.json", + import.meta.url +); +const sourceValidityMigration = new URL( + "../../../../migrations/20260614105716_tricky_plazm/migration.sql", + import.meta.url +); +const sourceValiditySnapshot = new URL( + "../../../../migrations/20260614105716_tricky_plazm/snapshot.json", + import.meta.url +); +const saveGraphModule = new URL("../../../../apps/worker/lib/save-graph.ts", import.meta.url); +const regenerateDescriptionsModule = new URL("../../../../apps/worker/lib/regenerate-descriptions.ts", import.meta.url); +const sourceReferenceModule = new URL("../../../../apps/api/src/lib/source-reference.ts", import.meta.url); describe("source reference migration compatibility", () => { test("uses an expand-only migration for source chunk backfill", async () => { @@ -46,3 +70,214 @@ describe("json file type migration compatibility", () => { expect(sql).toContain('ON CONFLICT ("organization_id", "file_type") DO NOTHING'); }); }); + +describe("code graph migration compatibility", () => { + test("adds directed relationship metadata and seeds code file type configs idempotently", async () => { + const sql = await Bun.file(codeGraphMigration).text(); + + expect(sql).toContain("ADD COLUMN IF NOT EXISTS \"kind\" text DEFAULT 'RELATED' NOT NULL"); + expect(sql).toContain('ADD COLUMN IF NOT EXISTS "directed" boolean DEFAULT false NOT NULL'); + expect(sql).toContain("'code'"); + expect(sql).toContain("'text'"); + expect(sql).toContain("'semantic'"); + expect(sql).toContain('ON CONFLICT ("organization_id", "file_type") DO NOTHING'); + }); + + test("keeps relationship metadata columns in the migration snapshot", async () => { + const snapshot = (await Bun.file(codeGraphSnapshot).json()) as { + ddl: Array>; + }; + const relationshipColumns = snapshot.ddl.filter( + (entry) => entry.entityType === "columns" && entry.table === "relationships" + ); + + expect(relationshipColumns).toContainEqual( + expect.objectContaining({ + name: "kind", + type: "text", + notNull: true, + default: "'RELATED'", + }) + ); + expect(relationshipColumns).toContainEqual( + expect.objectContaining({ + name: "directed", + type: "boolean", + notNull: true, + default: "false", + }) + ); + }); +}); + +describe("external file storage migration compatibility", () => { + test("adds explicit storage origin columns and constraints", async () => { + const sql = await Bun.file(externalFileStorageMigration).text(); + + expect(sql).toContain("ADD COLUMN \"storage_kind\" text DEFAULT 'internal' NOT NULL"); + expect(sql).toContain('ADD COLUMN "external_url" text'); + expect(sql).toContain('ADD COLUMN "external_provider" text'); + expect(sql).toContain('ADD CONSTRAINT "files_storage_origin_check"'); + expect(sql).toContain(`"storage_kind" = 'internal'`); + expect(sql).toContain(`"storage_kind" = 'external'`); + expect(sql).toContain('ADD CONSTRAINT "files_external_provider_check"'); + expect(sql).toContain(`"external_provider" IS NULL OR "external_provider" = 'github'`); + }); + + test("keeps external storage columns in the migration snapshot", async () => { + const snapshot = (await Bun.file(externalFileStorageSnapshot).json()) as { + ddl: Array>; + }; + const fileColumns = snapshot.ddl.filter((entry) => entry.entityType === "columns" && entry.table === "files"); + const fileChecks = snapshot.ddl.filter((entry) => entry.entityType === "checks" && entry.table === "files"); + + expect(fileColumns).toContainEqual( + expect.objectContaining({ + name: "storage_kind", + type: "text", + notNull: true, + default: "'internal'", + }) + ); + expect(fileColumns).toContainEqual(expect.objectContaining({ name: "external_url", type: "text" })); + expect(fileColumns).toContainEqual(expect.objectContaining({ name: "external_provider", type: "text" })); + expect(fileChecks).toContainEqual(expect.objectContaining({ name: "files_storage_origin_check" })); + expect(fileChecks).toContainEqual(expect.objectContaining({ name: "files_external_provider_check" })); + }); +}); + +describe("source validity migration compatibility", () => { + test("adds source validity without deleting historical sources", async () => { + const sql = await Bun.file(sourceValidityMigration).text(); + + expect(sql).toContain('ADD COLUMN IF NOT EXISTS "valid_until" timestamp with time zone'); + expect(sql).toContain('"active" = true AND "valid_until" IS NULL'); + expect(sql).toContain('CREATE INDEX IF NOT EXISTS "sources_current_id_idx"'); + expect(sql).toContain('CREATE INDEX IF NOT EXISTS "sources_entity_current_id_idx"'); + expect(sql).toContain('CREATE INDEX IF NOT EXISTS "sources_relationship_current_id_idx"'); + expect(sql).not.toMatch(/DELETE\s+FROM\s+"sources"/i); + }); + + test("keeps valid_until, connector tables, and indexes in the migration snapshot", async () => { + const snapshot = (await Bun.file(sourceValiditySnapshot).json()) as { + ddl: Array>; + }; + const sourceColumns = snapshot.ddl.filter( + (entry) => entry.entityType === "columns" && entry.table === "sources" + ); + const sourceIndexes = snapshot.ddl.filter( + (entry) => entry.entityType === "indexes" && entry.table === "sources" + ); + const connectorTables = snapshot.ddl.filter((entry) => entry.entityType === "tables"); + const connectorColumns = snapshot.ddl.filter((entry) => entry.entityType === "columns"); + const connectorIndexes = snapshot.ddl.filter((entry) => entry.entityType === "indexes"); + const connectorChecks = snapshot.ddl.filter((entry) => entry.entityType === "checks"); + const connectorFks = snapshot.ddl.filter((entry) => entry.entityType === "fks"); + + expect(sourceColumns).toContainEqual( + expect.objectContaining({ + name: "valid_until", + type: "timestamp with time zone", + notNull: false, + }) + ); + expect(sourceIndexes).toContainEqual( + expect.objectContaining({ + name: "sources_current_id_idx", + where: '"active" = true AND "valid_until" IS NULL', + }) + ); + expect(sourceIndexes).toContainEqual(expect.objectContaining({ name: "sources_entity_current_id_idx" })); + expect(sourceIndexes).toContainEqual(expect.objectContaining({ name: "sources_relationship_current_id_idx" })); + expect(connectorTables).toContainEqual(expect.objectContaining({ name: "connectors" })); + expect(connectorTables).toContainEqual(expect.objectContaining({ name: "connector_installations" })); + expect(connectorTables).toContainEqual(expect.objectContaining({ name: "repository_graph_bindings" })); + expect(connectorTables).toContainEqual(expect.objectContaining({ name: "connector_webhook_events" })); + expect(connectorColumns).toContainEqual( + expect.objectContaining({ table: "files", name: "repository_binding_id", type: "text" }) + ); + expect(connectorColumns).toContainEqual( + expect.objectContaining({ table: "connectors", name: "encrypted_credentials", notNull: true }) + ); + expect(connectorColumns).toContainEqual( + expect.objectContaining({ table: "repository_graph_bindings", name: "sync_status", default: "'pending'" }) + ); + expect(connectorIndexes).toContainEqual(expect.objectContaining({ name: "connectors_provider_status_idx" })); + expect(connectorIndexes).toContainEqual( + expect.objectContaining({ + name: "connector_installations_org_scope_unique", + isUnique: true, + where: '"team_id" IS NULL', + }) + ); + expect(connectorIndexes).toContainEqual( + expect.objectContaining({ + name: "connector_installations_team_scope_unique", + isUnique: true, + where: '"team_id" IS NOT NULL', + }) + ); + expect(connectorIndexes).toContainEqual( + expect.objectContaining({ name: "repository_graph_bindings_graph_unique", isUnique: true }) + ); + expect(connectorIndexes).toContainEqual( + expect.objectContaining({ name: "connector_webhook_events_delivery_unique", isUnique: true }) + ); + expect(connectorIndexes).toContainEqual( + expect.objectContaining({ name: "files_repository_binding_active_idx" }) + ); + expect(connectorChecks).toContainEqual(expect.objectContaining({ name: "connectors_provider_check" })); + expect(connectorChecks).toContainEqual( + expect.objectContaining({ name: "repository_graph_bindings_sync_status_check" }) + ); + expect(connectorFks).toContainEqual( + expect.objectContaining({ + name: "files_repository_binding_id_repository_graph_bindings_id_fk", + table: "files", + tableTo: "repository_graph_bindings", + }) + ); + }); + + test("adds connector tables and idempotent indexes to the migration sql", async () => { + const sql = await Bun.file(sourceValidityMigration).text(); + + expect(sql).toContain('CREATE TABLE IF NOT EXISTS "connectors"'); + expect(sql).toContain('CREATE TABLE IF NOT EXISTS "connector_installations"'); + expect(sql).toContain('CREATE TABLE IF NOT EXISTS "repository_graph_bindings"'); + expect(sql).toContain('CREATE TABLE IF NOT EXISTS "connector_webhook_events"'); + expect(sql).toContain('ADD COLUMN IF NOT EXISTS "repository_binding_id" text'); + expect(sql).toContain('ADD CONSTRAINT "files_repository_binding_id_repository_graph_bindings_id_fk"'); + expect(sql).not.toContain('CONSTRAINT "connector_installations_provider_scope_unique"'); + expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS "connector_installations_org_scope_unique"'); + expect(sql).toContain('WHERE "team_id" IS NULL'); + expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS "connector_installations_team_scope_unique"'); + expect(sql).toContain('WHERE "team_id" IS NOT NULL'); + expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS "connector_webhook_events_delivery_unique"'); + expect(sql).toContain("\"external_provider\" IS NULL OR \"external_provider\" in ('github', 'gitlab')"); + }); + test("invalidates older code sources instead of deleting them", async () => { + const source = await Bun.file(saveGraphModule).text(); + + expect(source).toContain("SET valid_until = NOW()"); + expect(source).toContain("old_file.file_type = 'code'"); + expect(source).toContain('currentSourceSql("old_source")'); + expect(source).not.toMatch(/DELETE\s+FROM\s+sources\s+source/i); + }); + + test("filters user-facing source references to current visible sources", async () => { + const source = await Bun.file(sourceReferenceModule).text(); + + expect(source).toContain("currentSourcePredicate(sourcesTable)"); + expect(source).toContain("visibleFilePredicate(filesTable)"); + }); + + test("deactivates descriptions with no unexpired visible sources", async () => { + const source = await Bun.file(regenerateDescriptionsModule).text(); + + expect(source).toContain("unexpiredSourcePredicate(sourcesTable)"); + expect(source).toContain("visibleFilePredicate(filesTable)"); + expect(source).toContain("set({ active: false })"); + expect(source).toContain("AND source.valid_until IS NULL"); + }); +}); diff --git a/packages/db/src/source-validity.ts b/packages/db/src/source-validity.ts new file mode 100644 index 00000000..07099062 --- /dev/null +++ b/packages/db/src/source-validity.ts @@ -0,0 +1,40 @@ +import { and, eq, isNull, sql, type SQL } from "drizzle-orm"; +import { filesTable, sourcesTable } from "./tables/graph"; + +type SourceValidityColumns = { + active: typeof sourcesTable.active; + validUntil: typeof sourcesTable.validUntil; +}; + +type FileVisibilityColumns = { + deleted: typeof filesTable.deleted; +}; + +export function currentSourcePredicate(source: SourceValidityColumns = sourcesTable): SQL { + return and(eq(source.active, true), isNull(source.validUntil))!; +} + +export function unexpiredSourcePredicate(source: Pick = sourcesTable): SQL { + return isNull(source.validUntil); +} + +export function visibleFilePredicate(file: FileVisibilityColumns = filesTable): SQL { + return eq(file.deleted, false); +} + +export function currentSourceSql(alias = "source"): SQL { + const quoted = quoteIdentifier(alias); + return sql.raw(`${quoted}."active" = true AND ${quoted}."valid_until" IS NULL`); +} + +export function visibleFileSql(alias = "file"): SQL { + return sql.raw(`${quoteIdentifier(alias)}."deleted" = false`); +} + +function quoteIdentifier(identifier: string) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) { + throw new Error("Invalid SQL identifier"); + } + + return `"${identifier}"`; +} diff --git a/packages/db/src/tables/connectors.ts b/packages/db/src/tables/connectors.ts new file mode 100644 index 00000000..2673deef --- /dev/null +++ b/packages/db/src/tables/connectors.ts @@ -0,0 +1,198 @@ +import { sql } from "drizzle-orm"; +import { boolean, check, index, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; +import { ulid } from "ulid"; +import { organizationTable, teamTable, userTable } from "./auth"; +import { graphTable } from "./graph"; + +export const CONNECTOR_PROVIDER_VALUES = ["github", "gitlab"] as const; +export type ConnectorProvider = (typeof CONNECTOR_PROVIDER_VALUES)[number]; + +export const CONNECTOR_STATUS_VALUES = ["draft", "active", "disabled"] as const; +export type ConnectorStatus = (typeof CONNECTOR_STATUS_VALUES)[number]; + +export const CONNECTOR_INSTALLATION_STATUS_VALUES = ["active", "disabled"] as const; +export type ConnectorInstallationStatus = (typeof CONNECTOR_INSTALLATION_STATUS_VALUES)[number]; + +export const REPOSITORY_GRAPH_SYNC_STATUS_VALUES = ["pending", "syncing", "synced", "failed"] as const; +export type RepositoryGraphSyncStatus = (typeof REPOSITORY_GRAPH_SYNC_STATUS_VALUES)[number]; + +export const CONNECTOR_WEBHOOK_EVENT_STATUS_VALUES = ["ignored", "enqueued", "duplicate", "failed"] as const; +export type ConnectorWebhookEventStatus = (typeof CONNECTOR_WEBHOOK_EVENT_STATUS_VALUES)[number]; + +export const connectorsTable = pgTable.withRLS( + "connectors", + { + id: text("id") + .primaryKey() + .$default(() => ulid()), + provider: text("provider", { enum: CONNECTOR_PROVIDER_VALUES }).notNull(), + name: text("name").notNull(), + slug: text("slug").notNull(), + status: text("status", { enum: CONNECTOR_STATUS_VALUES }).notNull().default("active"), + appId: text("app_id"), + appSlug: text("app_slug"), + clientId: text("client_id"), + encryptedCredentials: text("encrypted_credentials").notNull(), + webhookSecretEncrypted: text("webhook_secret_encrypted").notNull(), + createdByUserId: text("created_by_user_id").references(() => userTable.id, { + name: "connectors_created_by_user_id_user_id_fk", + onDelete: "set null", + }), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) + .defaultNow() + .$onUpdate(() => sql`NOW()`), + }, + (table) => [ + uniqueIndex("connectors_slug_unique").on(table.slug), + index("connectors_provider_status_idx").on(table.provider, table.status), + check("connectors_provider_check", sql`${table.provider} in ('github', 'gitlab')`), + check("connectors_status_check", sql`${table.status} in ('draft', 'active', 'disabled')`), + ] +); + +export const connectorInstallationsTable = pgTable.withRLS( + "connector_installations", + { + id: text("id") + .primaryKey() + .$default(() => ulid()), + connectorId: text("connector_id") + .notNull() + .references(() => connectorsTable.id, { + name: "connector_installations_connector_id_connectors_id_fk", + onDelete: "cascade", + }), + provider: text("provider", { enum: CONNECTOR_PROVIDER_VALUES }).notNull(), + providerInstallationId: text("provider_installation_id").notNull(), + providerAccountLogin: text("provider_account_login").notNull(), + providerAccountType: text("provider_account_type"), + organizationId: text("organization_id").references(() => organizationTable.id, { + name: "connector_installations_organization_id_organization_id_fk", + onDelete: "cascade", + }), + teamId: text("team_id").references(() => teamTable.id, { + name: "connector_installations_team_id_team_id_fk", + onDelete: "cascade", + }), + installedByUserId: text("installed_by_user_id").references(() => userTable.id, { + name: "connector_installations_installed_by_user_id_user_id_fk", + onDelete: "set null", + }), + encryptedCredentials: text("encrypted_credentials"), + repositorySelection: text("repository_selection").notNull().default("unknown"), + status: text("status", { enum: CONNECTOR_INSTALLATION_STATUS_VALUES }).notNull().default("active"), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) + .defaultNow() + .$onUpdate(() => sql`NOW()`), + }, + (table) => [ + uniqueIndex("connector_installations_org_scope_unique") + .on(table.connectorId, table.providerInstallationId, table.organizationId) + .where(sql`${table.teamId} is null`), + uniqueIndex("connector_installations_team_scope_unique") + .on(table.connectorId, table.providerInstallationId, table.organizationId, table.teamId) + .where(sql`${table.teamId} is not null`), + index("connector_installations_connector_status_idx").on(table.connectorId, table.status), + index("connector_installations_organization_idx").on(table.organizationId), + index("connector_installations_team_idx").on(table.teamId), + check("connector_installations_provider_check", sql`${table.provider} in ('github', 'gitlab')`), + check("connector_installations_status_check", sql`${table.status} in ('active', 'disabled')`), + check( + "connector_installations_owner_scope_check", + sql`(${table.organizationId} is not null and ${table.teamId} is null) or (${table.organizationId} is not null and ${table.teamId} is not null)` + ), + ] +); + +export const repositoryGraphBindingsTable = pgTable.withRLS( + "repository_graph_bindings", + { + id: text("id") + .primaryKey() + .$default(() => ulid()), + graphId: text("graph_id") + .notNull() + .references(() => graphTable.id, { + name: "repository_graph_bindings_graph_id_graphs_id_fk", + onDelete: "cascade", + }), + connectorInstallationId: text("connector_installation_id") + .notNull() + .references(() => connectorInstallationsTable.id, { + name: "repository_graph_bindings_connector_installation_id_fk", + onDelete: "restrict", + }), + provider: text("provider", { enum: CONNECTOR_PROVIDER_VALUES }).notNull(), + providerRepositoryId: text("provider_repository_id").notNull(), + repositoryFullName: text("repository_full_name").notNull(), + repositoryHtmlUrl: text("repository_html_url").notNull(), + branch: text("branch").notNull(), + lastSeenCommitSha: text("last_seen_commit_sha"), + lastSyncedCommitSha: text("last_synced_commit_sha"), + syncStatus: text("sync_status", { enum: REPOSITORY_GRAPH_SYNC_STATUS_VALUES }).notNull().default("pending"), + syncErrorCode: text("sync_error_code"), + webhookEnabled: boolean("webhook_enabled").notNull().default(true), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) + .defaultNow() + .$onUpdate(() => sql`NOW()`), + }, + (table) => [ + uniqueIndex("repository_graph_bindings_graph_unique").on(table.graphId), + uniqueIndex("repository_graph_bindings_repository_branch_unique").on( + table.connectorInstallationId, + table.providerRepositoryId, + table.branch + ), + index("repository_graph_bindings_provider_repo_branch_idx").on( + table.provider, + table.providerRepositoryId, + table.branch + ), + index("repository_graph_bindings_installation_status_idx").on(table.connectorInstallationId, table.syncStatus), + check("repository_graph_bindings_provider_check", sql`${table.provider} in ('github', 'gitlab')`), + check( + "repository_graph_bindings_sync_status_check", + sql`${table.syncStatus} in ('pending', 'syncing', 'synced', 'failed')` + ), + ] +); + +export const connectorWebhookEventsTable = pgTable.withRLS( + "connector_webhook_events", + { + id: text("id") + .primaryKey() + .$default(() => ulid()), + connectorId: text("connector_id") + .notNull() + .references(() => connectorsTable.id, { + name: "connector_webhook_events_connector_id_connectors_id_fk", + onDelete: "cascade", + }), + provider: text("provider", { enum: CONNECTOR_PROVIDER_VALUES }).notNull(), + deliveryId: text("delivery_id").notNull(), + eventName: text("event_name").notNull(), + providerRepositoryId: text("provider_repository_id"), + branch: text("branch"), + commitSha: text("commit_sha"), + status: text("status", { enum: CONNECTOR_WEBHOOK_EVENT_STATUS_VALUES }).notNull(), + errorCode: text("error_code"), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow(), + }, + (table) => [ + uniqueIndex("connector_webhook_events_delivery_unique").on(table.connectorId, table.provider, table.deliveryId), + index("connector_webhook_events_binding_lookup_idx").on( + table.provider, + table.providerRepositoryId, + table.branch + ), + check("connector_webhook_events_provider_check", sql`${table.provider} in ('github', 'gitlab')`), + check( + "connector_webhook_events_status_check", + sql`${table.status} in ('ignored', 'enqueued', 'duplicate', 'failed')` + ), + ] +); diff --git a/packages/db/src/tables/graph.ts b/packages/db/src/tables/graph.ts index 42f5442d..e3dcd3c7 100644 --- a/packages/db/src/tables/graph.ts +++ b/packages/db/src/tables/graph.ts @@ -138,6 +138,10 @@ export const filesTable = pgTable.withRLS( type: text("file_type").notNull(), mimeType: text("mime_type").notNull(), key: text("file_key").notNull(), + storageKind: text("storage_kind").notNull().default("internal"), + externalUrl: text("external_url"), + externalProvider: text("external_provider"), + repositoryBindingId: text("repository_binding_id"), checksum: text("checksum"), deleted: boolean("deleted").default(false), status: text("status", { enum: FILE_PROCESS_STATUS_VALUES }).notNull().default("processing"), @@ -165,9 +169,31 @@ export const filesTable = pgTable.withRLS( index("files_graph_active_id_idx") .on(table.graphId, table.id) .where(sql`${table.deleted} = false`), - index("files_graph_active_key_idx") + uniqueIndex("files_graph_active_key_idx") .on(table.graphId, table.key) .where(sql`${table.deleted} = false`), + check( + "files_storage_origin_check", + sql` + ( + ${table.storageKind} = 'internal' + AND ${table.externalUrl} IS NULL + AND ${table.externalProvider} IS NULL + ) + OR ( + ${table.storageKind} = 'external' + AND ${table.externalUrl} IS NOT NULL + AND ${table.externalProvider} IS NOT NULL + ) + ` + ), + check( + "files_external_provider_check", + sql`${table.externalProvider} IS NULL OR ${table.externalProvider} in ('github', 'gitlab')` + ), + index("files_repository_binding_active_idx") + .on(table.repositoryBindingId, table.createdAt, table.id) + .where(sql`${table.deleted} = false`), ] ); @@ -245,6 +271,8 @@ export const relationshipTable = pgTable.withRLS( graphId: text("graph_id") .notNull() .references(() => graphTable.id, { onDelete: "cascade" }), + kind: text("kind").notNull().default("RELATED"), + directed: boolean("directed").notNull().default(false), rank: doublePrecision("rank").notNull().default(0), description: text("description").notNull(), embedding: vector("embedding", { dimensions: 4096 }).notNull(), @@ -276,6 +304,7 @@ export const sourcesTable = pgTable.withRLS( .notNull() .references(() => textUnitTable.id, { onDelete: "cascade" }), active: boolean("active").notNull().default(false), + validUntil: timestamp("valid_until", { withTimezone: true, mode: "date" }), description: text("description").notNull(), sourceChunkIds: json("source_chunk_ids") .$type() @@ -292,6 +321,15 @@ export const sourcesTable = pgTable.withRLS( index("sources_active_id_idx").on(table.active, table.id), index("sources_entity_active_id_idx").on(table.entityId, table.active, table.id), index("sources_relationship_active_id_idx").on(table.relationshipId, table.active, table.id), + index("sources_current_id_idx") + .on(table.id) + .where(sql`${table.active} = true AND ${table.validUntil} IS NULL`), + index("sources_entity_current_id_idx") + .on(table.entityId, table.id) + .where(sql`${table.active} = true AND ${table.validUntil} IS NULL AND ${table.entityId} IS NOT NULL`), + index("sources_relationship_current_id_idx") + .on(table.relationshipId, table.id) + .where(sql`${table.active} = true AND ${table.validUntil} IS NULL AND ${table.relationshipId} IS NOT NULL`), index("sources_text_unit_idx").on(table.textUnitId), index("sources_description_trgm_idx").using("gin", table.description.op("gin_trgm_ops")), index("sources_embedding_diskann_idx").using("diskann", table.embedding.op("vector_cosine_ops")), diff --git a/packages/graph/package.json b/packages/graph/package.json index c9fb6d04..3fd882d4 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -7,6 +7,7 @@ "./file-type": "./src/file-type.ts", "./loader/*": "./src/loader/*.ts", "./chunker/*": "./src/chunking/*.ts", + "./code/*": "./src/code/*.ts", "./lib/*": "./src/lib/*.ts", "./unit": "./src/unit.ts", "./merge": "./src/merge.ts", @@ -31,6 +32,12 @@ "js-tiktoken": "^1.0.21", "jszip": "^3.10.1", "pdf-to-img": "^6.1.0", + "tree-sitter": "^0.25.0", + "tree-sitter-c": "^0.24.1", + "tree-sitter-javascript": "^0.25.0", + "tree-sitter-rust": "^0.24.0", + "tree-sitter-typescript": "^0.23.2", + "@tree-sitter-grammars/tree-sitter-zig": "^1.1.2", "ulid": "^3.0.2", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "catalog:" diff --git a/packages/graph/src/__tests__/dedupe.test.ts b/packages/graph/src/__tests__/dedupe.test.ts index daa7a813..7d120be0 100644 --- a/packages/graph/src/__tests__/dedupe.test.ts +++ b/packages/graph/src/__tests__/dedupe.test.ts @@ -284,4 +284,41 @@ describe("dedupe", () => { expect(deduped.entities).toHaveLength(2); }); + + test("does not merge relationships with different kinds", () => { + const graph: Graph = { + id: "graph-relationship-kinds", + units: [textUnit("unit-1", "file-1", "A imports and calls B.")], + entities: [ + { id: "entity-a", name: "A", type: "CODE_FUNCTION", description: "", sources: [] }, + { id: "entity-b", name: "B", type: "CODE_FUNCTION", description: "", sources: [] }, + ], + relationships: [ + { + id: "relationship-imports", + sourceId: "entity-a", + targetId: "entity-b", + kind: "IMPORTS", + directed: true, + strength: 0.9, + description: "", + sources: [], + }, + { + id: "relationship-calls", + sourceId: "entity-a", + targetId: "entity-b", + kind: "CALLS", + directed: true, + strength: 0.8, + description: "", + sources: [], + }, + ], + }; + + const deduped = dedupe(graph); + + expect(deduped.relationships.map((relationship) => relationship.kind).sort()).toEqual(["CALLS", "IMPORTS"]); + }); }); diff --git a/packages/graph/src/__tests__/extract.test.ts b/packages/graph/src/__tests__/extract.test.ts index 5eee900b..7373d924 100644 --- a/packages/graph/src/__tests__/extract.test.ts +++ b/packages/graph/src/__tests__/extract.test.ts @@ -330,4 +330,53 @@ describe("mergeGraphs", () => { expect(merged.entities).toEqual([]); expect(merged.relationships).toEqual([]); }); + + test("does not merge opposite directed relationships", () => { + const left: Graph = { + id: "graph-left-directed", + units: [textUnit("unit-left", "file-1", "A calls B.")], + entities: [ + { id: "entity-a-left", name: "A", type: "CODE_FUNCTION", description: "", sources: [] }, + { id: "entity-b-left", name: "B", type: "CODE_FUNCTION", description: "", sources: [] }, + ], + relationships: [ + { + id: "relationship-ab", + sourceId: "entity-a-left", + targetId: "entity-b-left", + kind: "CALLS", + directed: true, + strength: 0.8, + description: "", + sources: [], + }, + ], + }; + const right: Graph = { + id: "graph-right-directed", + units: [textUnit("unit-right", "file-1", "B calls A.")], + entities: [ + { id: "entity-a-right", name: "A", type: "CODE_FUNCTION", description: "", sources: [] }, + { id: "entity-b-right", name: "B", type: "CODE_FUNCTION", description: "", sources: [] }, + ], + relationships: [ + { + id: "relationship-ba", + sourceId: "entity-b-right", + targetId: "entity-a-right", + kind: "CALLS", + directed: true, + strength: 0.6, + description: "", + sources: [], + }, + ], + }; + + const merged = mergeGraphs(left, right); + + expect(merged.relationships).toHaveLength(2); + expect(merged.relationships.every((relationship) => relationship.kind === "CALLS")).toBe(true); + expect(merged.relationships.every((relationship) => relationship.directed === true)).toBe(true); + }); }); diff --git a/packages/graph/src/__tests__/file-type.test.ts b/packages/graph/src/__tests__/file-type.test.ts new file mode 100644 index 00000000..d12e12e1 --- /dev/null +++ b/packages/graph/src/__tests__/file-type.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test"; +import { inferGraphFileType } from "../file-type"; + +function file(name: string, type = "") { + return { name, type } as File; +} + +describe("inferGraphFileType", () => { + test("classifies supported source paths as code", () => { + expect(inferGraphFileType(file("src/index.ts"))).toBe("code"); + expect(inferGraphFileType(file("src/component.tsx"))).toBe("code"); + expect(inferGraphFileType(file("src/script.js"))).toBe("code"); + expect(inferGraphFileType(file("src/lib.rs"))).toBe("code"); + expect(inferGraphFileType(file("src/LIB.RS"))).toBe("code"); + expect(inferGraphFileType(file("src/main.zig"))).toBe("code"); + expect(inferGraphFileType(file("src/MAIN.ZIG"))).toBe("code"); + expect(inferGraphFileType(file("src/main.c"))).toBe("code"); + expect(inferGraphFileType(file("src/math.h"))).toBe("code"); + expect(inferGraphFileType(file("src/MATH.H"))).toBe("code"); + }); + + test("keeps text and structured formats out of code classification", () => { + expect(inferGraphFileType(file("README.md"))).toBe("text"); + expect(inferGraphFileType(file("data.json", "application/json"))).toBe("json"); + }); +}); diff --git a/packages/graph/src/chunking/factory.ts b/packages/graph/src/chunking/factory.ts index 3429aba6..c8e1b099 100644 --- a/packages/graph/src/chunking/factory.ts +++ b/packages/graph/src/chunking/factory.ts @@ -58,6 +58,7 @@ export const DEFAULT_FILE_TYPE_CHUNKING: Record ({ + fileId: input.fileId, + repositoryUrl: "https://github.com/acme/widgets.git", + repositoryName: "widgets", + commitSha: "commit-1", + path: input.path, + content: input.content, +}); +const repositoryPrefix = "https://github.com/acme/widgets.git"; + +function relationshipKinds(graph: ReturnType) { + return graph.relationships.map((relationship) => relationship.kind).sort(); +} + +function entityByName(graph: ReturnType, name: string) { + return graph.entities.find((entity) => entity.name === name); +} + +function hasRelationship( + graph: ReturnType, + kind: string, + sourceName: string, + targetName: string +) { + const source = entityByName(graph, sourceName); + const target = entityByName(graph, targetName); + return graph.relationships.some( + (relationship) => + relationship.kind === kind && relationship.sourceId === source?.id && relationship.targetId === target?.id + ); +} +function relationshipCount( + graph: ReturnType, + kind: string, + sourceName: string, + targetName: string +) { + const source = entityByName(graph, sourceName); + const target = entityByName(graph, targetName); + return graph.relationships.filter( + (relationship) => + relationship.kind === kind && relationship.sourceId === source?.id && relationship.targetId === target?.id + ).length; +} + +describe("code repository graph builder", () => { + test("builds code entities, directed relationships, and source metadata from TypeScript AST", () => { + const helper = baseFile({ + fileId: "file-helper", + path: "src/helper.ts", + content: "export function helper() {\n return 1;\n}\n", + }); + const index = baseFile({ + fileId: "file-index", + path: "src/index.ts", + content: [ + "import { helper } from './helper';", + "class Base {}", + "interface Runnable { run(): number }", + "export class Runner extends Base implements Runnable {", + " run() {", + " return helper();", + " }", + "}", + "export const makeRunner = () => new Runner();", + "export type RunnerId = string;", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([helper, index]); + + const graph = buildCodeFileGraph(index, manifest); + + expect(graph.entities.map((entity) => [entity.type, entity.name]).sort()).toContainEqual([ + "CODE_FILE", + `${repositoryPrefix}:src/index.ts`, + ]); + expect(graph.entities.map((entity) => [entity.type, entity.name]).sort()).toContainEqual([ + "CODE_CLASS", + `${repositoryPrefix}:src/index.ts#Runner`, + ]); + expect(graph.entities.map((entity) => [entity.type, entity.name]).sort()).toContainEqual([ + "CODE_INTERFACE", + `${repositoryPrefix}:src/index.ts#Runnable`, + ]); + expect(graph.entities.map((entity) => [entity.type, entity.name]).sort()).toContainEqual([ + "CODE_FUNCTION", + `${repositoryPrefix}:src/index.ts#makeRunner`, + ]); + expect(graph.entities.map((entity) => [entity.type, entity.name]).sort()).toContainEqual([ + "CODE_TYPE", + `${repositoryPrefix}:src/index.ts#RunnerId`, + ]); + expect(graph.entities.map((entity) => [entity.type, entity.name]).sort()).toContainEqual([ + "CODE_FUNCTION", + `${repositoryPrefix}:src/helper.ts#helper`, + ]); + expect(relationshipKinds(graph)).toContain("IMPORTS"); + expect(relationshipKinds(graph)).toContain("CALLS"); + expect(relationshipKinds(graph)).toContain("CONTAINS"); + expect(relationshipKinds(graph)).toContain("EXTENDS"); + expect(relationshipKinds(graph)).toContain("IMPLEMENTS"); + expect(graph.relationships.every((relationship) => relationship.directed === true)).toBe(true); + + const runEntity = graph.entities.find((entity) => entity.name === `${repositoryPrefix}:src/index.ts#Runner.run`); + expect(runEntity).toBeDefined(); + const runChunk = runEntity?.sources + .map((source) => graph.units.find((unit) => unit.id === source.unitId)?.chunks[0]) + .find(Boolean); + expect(runChunk).toMatchObject({ + type: "text", + filePath: "src/index.ts", + language: "typescript", + startLine: 5, + endLine: 7, + }); + expect(runChunk?.text).toContain("run() {"); + expect(runChunk?.text).toContain("helper()"); + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/index.ts#Runner.run`, + `${repositoryPrefix}:src/helper.ts#helper` + ) + ).toBe(true); + }); + + test("keeps named import resolution when reading manifests without explicit exports", () => { + const helper = baseFile({ + fileId: "file-legacy-helper", + path: "src/helper.ts", + content: "export function helper() { return 1; }\n", + }); + const consumer = baseFile({ + fileId: "file-legacy-consumer", + path: "src/legacy-consumer.ts", + content: "import { helper } from './helper';\nexport function run() { return helper(); }\n", + }); + const manifest = buildCodeRepositoryManifest([helper, consumer]); + const legacyManifest = { ...manifest }; + delete (legacyManifest as typeof legacyManifest & { exports?: unknown }).exports; + + const graph = buildCodeFileGraph(consumer, legacyManifest as typeof manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/legacy-consumer.ts#run`, + `${repositoryPrefix}:src/helper.ts#helper` + ) + ).toBe(true); + }); + + test("resolves index imports inside repository manifests", () => { + const dependency = baseFile({ + fileId: "file-module", + path: "src/lib/index.ts", + content: "export function dependency() { return 1; }\n", + }); + const consumer = baseFile({ + fileId: "file-consumer", + path: "src/consumer.ts", + content: "import { dependency } from './lib';\nexport function consumer() { return dependency(); }\n", + }); + const manifest = buildCodeRepositoryManifest([dependency, consumer]); + + const graph = buildCodeFileGraph(consumer, manifest); + + const dependencyEntity = graph.entities.find( + (entity) => entity.name === `${repositoryPrefix}:src/lib/index.ts#dependency` + ); + const consumerEntity = graph.entities.find( + (entity) => entity.name === `${repositoryPrefix}:src/consumer.ts#consumer` + ); + expect(dependencyEntity).toBeDefined(); + expect(consumerEntity).toBeDefined(); + expect( + graph.relationships.some( + (relationship) => + relationship.kind === "CALLS" && + relationship.sourceId === consumerEntity?.id && + relationship.targetId === dependencyEntity?.id + ) + ).toBe(true); + }); + test("resolves default namespace and barrel imports across files", () => { + const defaultHelper = baseFile({ + fileId: "file-default-helper", + path: "src/default-helper.ts", + content: "const helper = () => 1;\nexport default helper;\n", + }); + const helpers = baseFile({ + fileId: "file-helpers", + path: "src/helpers.ts", + content: "export function add() { return 1; }\n", + }); + const barrel = baseFile({ + fileId: "file-barrel", + path: "src/lib/index.ts", + content: "export { add } from '../helpers';\n", + }); + const consumer = baseFile({ + fileId: "file-default-consumer", + path: "src/default-consumer.ts", + content: [ + "import helper from './default-helper';", + "import * as helpers from './helpers';", + "import { add as addFromBarrel } from './lib';", + "export function run() {", + " return helper() + helpers.add() + addFromBarrel();", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([defaultHelper, helpers, barrel, consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/default-consumer.ts#run`, + `${repositoryPrefix}:src/default-helper.ts#helper` + ) + ).toBe(true); + expect( + relationshipCount( + graph, + "CALLS", + `${repositoryPrefix}:src/default-consumer.ts#run`, + `${repositoryPrefix}:src/helpers.ts#add` + ) + ).toBe(2); + expect( + hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/default-consumer.ts`, `${repositoryPrefix}:src/lib/index.ts`) + ).toBe(true); + }); + + test("resolves explicit extensions export-star barrels and default reexports", () => { + const defaultHelper = baseFile({ + fileId: "file-default-helper-declaration", + path: "src/default-helper.ts", + content: "export default function helper() { return 1; }\n", + }); + const shared = baseFile({ + fileId: "file-shared", + path: "src/shared.ts", + content: "export function sharedHelper() { return 1; }\n", + }); + const barrel = baseFile({ + fileId: "file-export-star-barrel", + path: "src/lib/index.ts", + content: [ + "export { default as helper } from '../default-helper';", + "export * from '../shared';", + ].join("\n"), + }); + const consumer = baseFile({ + fileId: "file-explicit-extension-consumer", + path: "src/use-barrel.ts", + content: [ + "import { helper, sharedHelper } from './lib/index.ts';", + "export function run() {", + " return helper() + sharedHelper();", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([defaultHelper, shared, barrel, consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/use-barrel.ts#run`, + `${repositoryPrefix}:src/default-helper.ts#helper` + ) + ).toBe(true); + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/use-barrel.ts#run`, + `${repositoryPrefix}:src/shared.ts#sharedHelper` + ) + ).toBe(true); + }); + test("resolves non-relative package imports to external symbols", () => { + const consumer = baseFile({ + fileId: "file-external-consumer", + path: "src/external-consumer.ts", + content: [ + "import { debounce as delay } from 'lodash';", + "import * as React from 'react';", + "export function run() {", + " delay();", + " return React.useMemo(() => 1, []);", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/external-consumer.ts#run`, + "widgets:external:lodash#debounce" + ) + ).toBe(true); + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/external-consumer.ts#run`, + "widgets:external:react#useMemo" + ) + ).toBe(true); + expect( + hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/external-consumer.ts`, "widgets:external:lodash") + ).toBe(true); + expect( + hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/external-consumer.ts`, "widgets:external:react") + ).toBe(true); + }); + test("prefers local definitions over imported ones when names overlap", () => { + const helper = baseFile({ + fileId: "file-shadow-helper", + path: "src/helper.ts", + content: "export function helper() { return 1; }\n", + }); + const consumer = baseFile({ + fileId: "file-shadow-consumer", + path: "src/shadow.ts", + content: [ + "import { helper } from './helper';", + "export function helperLocal() { return 2; }", + "export function run() {", + " return helperLocal();", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([helper, consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/shadow.ts#run`, + `${repositoryPrefix}:src/shadow.ts#helperLocal` + ) + ).toBe(true); + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/shadow.ts#run`, + `${repositoryPrefix}:src/helper.ts#helper` + ) + ).toBe(false); + }); + + test("resolves external default imports as callable symbols", () => { + const consumer = baseFile({ + fileId: "file-external-default-consumer", + path: "src/external-default.ts", + content: [ + "import nanoid from 'nanoid';", + "export function run() {", + " return nanoid();", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/external-default.ts#run`, + "widgets:external:nanoid#default" + ) + ).toBe(true); + }); + + test("resolves member calls on external default imports", () => { + const consumer = baseFile({ + fileId: "file-external-default-member-consumer", + path: "src/external-default-member.ts", + content: [ + "import React from 'react';", + "export function run() {", + " return React.useMemo(() => 1, []);", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/external-default-member.ts#run`, + "widgets:external:react#useMemo" + ) + ).toBe(true); + }); + + test("keeps nested namespace member paths for external modules", () => { + const consumer = baseFile({ + fileId: "file-external-namespace-consumer", + path: "src/external-namespace.ts", + content: [ + "import * as React from 'react';", + "export function run() {", + " return React.Children.only(null);", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([consumer]); + const graph = buildCodeFileGraph(consumer, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/external-namespace.ts#run`, + "widgets:external:react#Children.only" + ) + ).toBe(true); + }); + + test("resolves Rust fully qualified calls into nested modules", () => { + const mathRoot = baseFile({ + fileId: "file-rust-qualified-root", + path: "src/math/mod.rs", + content: "pub mod nested;\n", + }); + const nestedMath = baseFile({ + fileId: "file-rust-qualified-nested-module", + path: "src/math/nested.rs", + content: "pub fn add() -> i32 { 1 }\n", + }); + const main = baseFile({ + fileId: "file-rust-qualified-main", + path: "src/main.rs", + content: [ + "mod math;", + "pub fn run() -> i32 {", + " crate::math::nested::add()", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([mathRoot, nestedMath, main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/main.rs#run`, + `${repositoryPrefix}:src/math/nested.rs#add` + ) + ).toBe(true); + }); + + test("resolves Rust aliased module paths into nested modules", () => { + const mathRoot = baseFile({ + fileId: "file-rust-alias-root", + path: "src/math/mod.rs", + content: "pub mod nested;\n", + }); + const nestedMath = baseFile({ + fileId: "file-rust-alias-nested-module", + path: "src/math/nested.rs", + content: "pub fn add() -> i32 { 1 }\n", + }); + const main = baseFile({ + fileId: "file-rust-alias-main", + path: "src/main.rs", + content: [ + "mod math;", + "use crate::math as helpers;", + "pub fn run() -> i32 {", + " helpers::nested::add()", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([mathRoot, nestedMath, main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/main.rs#run`, + `${repositoryPrefix}:src/math/nested.rs#add` + ) + ).toBe(true); + }); + + test("keeps nested external Zig symbol paths", () => { + const main = baseFile({ + fileId: "file-zig-std-mem-main", + path: "src/std-mem.zig", + content: [ + 'const std = @import("std");', + "pub fn run() bool {", + ' return std.mem.eql(u8, "a", "b");', + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/std-mem.zig#run`, + "widgets:external:std#mem.eql" + ) + ).toBe(true); + }); + + test("avoids ambiguous system include call resolution", () => { + const main = baseFile({ + fileId: "file-c-ambiguous-system-main", + path: "src/ambiguous-stdio.c", + content: [ + "#include ", + "#include ", + 'int run(void) { return printf("hi"); }', + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + graph.relationships.some( + (relationship) => + relationship.kind === "CALLS" && + relationship.sourceId === entityByName(graph, `${repositoryPrefix}:src/ambiguous-stdio.c#run`)?.id + ) + ).toBe(false); + }); + + test("prefers local C declarations over wildcard external includes", () => { + const main = baseFile({ + fileId: "file-c-local-over-external-main", + path: "src/local-over-external.c", + content: [ + "#include ", + "int printf(void);", + "int run(void) { return printf(); }", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/local-over-external.c#run`, + `${repositoryPrefix}:src/local-over-external.c#printf` + ) + ).toBe(true); + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/local-over-external.c#run`, + "widgets:external:stdio.h#printf" + ) + ).toBe(false); + }); + + test("resolves Rust fully qualified calls without aliases", () => { + const math = baseFile({ + fileId: "file-rust-qualified-math", + path: "src/math.rs", + content: "pub fn add() -> i32 { 1 }\n", + }); + const nested = baseFile({ + fileId: "file-rust-qualified-nested", + path: "src/nested/mod.rs", + content: [ + "pub fn run() -> i32 {", + " crate::math::add() + super::math::add()", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([math, nested]); + const graph = buildCodeFileGraph(nested, manifest); + + expect( + relationshipCount(graph, "CALLS", `${repositoryPrefix}:src/nested/mod.rs#run`, `${repositoryPrefix}:src/math.rs#add`) + ).toBe(2); + }); + + test("resolves Zig package imports to external symbols", () => { + const main = baseFile({ + fileId: "file-zig-std-main", + path: "src/std-main.zig", + content: [ + 'const std = @import("std");', + "pub fn run() void {", + ' std.debug.print("hi", .{});', + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/std-main.zig#run`, + "widgets:external:std#debug.print" + ) + ).toBe(true); + expect(hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/std-main.zig`, "widgets:external:std")).toBe(true); + }); + + test("resolves C system includes to external symbols", () => { + const main = baseFile({ + fileId: "file-c-stdio-main", + path: "src/stdio-main.c", + content: '#include \nint run(void) { printf("hi"); return 0; }\n', + }); + const manifest = buildCodeRepositoryManifest([main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/stdio-main.c#run`, + "widgets:external:stdio.h#printf" + ) + ).toBe(true); + expect( + hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/stdio-main.c`, "widgets:external:stdio.h") + ).toBe(true); + }); + + test("resolves Rust imports and module calls across files", () => { + const math = baseFile({ + fileId: "file-rust-math", + path: "src/math.rs", + content: "pub fn add() -> i32 { 1 }\n", + }); + const main = baseFile({ + fileId: "file-rust-main", + path: "src/main.rs", + content: [ + "mod math;", + "use crate::math::add;", + "pub fn run() -> i32 {", + " add() + math::add()", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([math, main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + relationshipCount(graph, "CALLS", `${repositoryPrefix}:src/main.rs#run`, `${repositoryPrefix}:src/math.rs#add`) + ).toBe(2); + expect( + hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/main.rs`, `${repositoryPrefix}:src/math.rs`) + ).toBe(true); + }); + + test("resolves Rust aliases super paths and modrs modules", () => { + const rootMath = baseFile({ + fileId: "file-rust-root-math", + path: "src/math/mod.rs", + content: "pub fn add() -> i32 { 1 }\n", + }); + const shared = baseFile({ + fileId: "file-rust-shared", + path: "src/shared.rs", + content: "pub fn scale() -> i32 { 2 }\n", + }); + const nested = baseFile({ + fileId: "file-rust-nested", + path: "src/nested/mod.rs", + content: [ + "use crate::math as helpers;", + "use super::shared::{scale as shared_scale};", + "pub fn run() -> i32 {", + " helpers::add() + shared_scale()", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([rootMath, shared, nested]); + const graph = buildCodeFileGraph(nested, manifest); + const runEntity = entityByName(graph, `${repositoryPrefix}:src/nested/mod.rs#run`); + const runChunk = runEntity?.sources + .map((source) => graph.units.find((unit) => unit.id === source.unitId)?.chunks[0]) + .find(Boolean); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/nested/mod.rs#run`, + `${repositoryPrefix}:src/math/mod.rs#add` + ) + ).toBe(true); + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/nested/mod.rs#run`, + `${repositoryPrefix}:src/shared.rs#scale` + ) + ).toBe(true); + expect(runChunk?.language).toBe("rust"); + }); + + test("resolves Zig imports across files", () => { + const helpers = baseFile({ + fileId: "file-zig-helpers", + path: "src/helpers.zig", + content: "pub fn add() i32 { return 1; }\n", + }); + const main = baseFile({ + fileId: "file-zig-main", + path: "src/main.zig", + content: [ + 'const helpers = @import("helpers.zig");', + "pub fn run() i32 {", + " return helpers.add();", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([helpers, main]); + const graph = buildCodeFileGraph(main, manifest); + const runEntity = entityByName(graph, `${repositoryPrefix}:src/main.zig#run`); + const runChunk = runEntity?.sources + .map((source) => graph.units.find((unit) => unit.id === source.unitId)?.chunks[0]) + .find(Boolean); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/main.zig#run`, + `${repositoryPrefix}:src/helpers.zig#add` + ) + ).toBe(true); + expect(runChunk?.language).toBe("zig"); + }); + + test("resolves Zig parent-relative imports", () => { + const helpers = baseFile({ + fileId: "file-zig-parent-helpers", + path: "src/helpers.zig", + content: "pub fn add() i32 { return 1; }\n", + }); + const main = baseFile({ + fileId: "file-zig-parent-main", + path: "src/app/main.zig", + content: [ + 'const helpers = @import("../helpers.zig");', + "pub fn run() i32 {", + " return helpers.add();", + "}", + ].join("\n"), + }); + const manifest = buildCodeRepositoryManifest([helpers, main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/app/main.zig#run`, + `${repositoryPrefix}:src/helpers.zig#add` + ) + ).toBe(true); + }); + + test("resolves C header includes across files", () => { + const header = baseFile({ + fileId: "file-c-header", + path: "src/math.h", + content: "int add(int left, int right);\n", + }); + const main = baseFile({ + fileId: "file-c-main", + path: "src/main.c", + content: '#include "math.h"\nint run(void) { return add(1, 2); }\n', + }); + const manifest = buildCodeRepositoryManifest([header, main]); + const graph = buildCodeFileGraph(main, manifest); + const runEntity = entityByName(graph, `${repositoryPrefix}:src/main.c#run`); + const runChunk = runEntity?.sources + .map((source) => graph.units.find((unit) => unit.id === source.unitId)?.chunks[0]) + .find(Boolean); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/main.c#run`, + `${repositoryPrefix}:src/math.h#add` + ) + ).toBe(true); + expect( + hasRelationship(graph, "IMPORTS", `${repositoryPrefix}:src/main.c`, `${repositoryPrefix}:src/math.h`) + ).toBe(true); + expect(runChunk?.language).toBe("c"); + }); + + test("resolves parent-relative C header includes", () => { + const header = baseFile({ + fileId: "file-c-nested-header", + path: "src/include/math.h", + content: "int add(int left, int right);\n", + }); + const main = baseFile({ + fileId: "file-c-nested-main", + path: "src/app/main.c", + content: '#include "../include/math.h"\nint run(void) { return add(1, 2); }\n', + }); + const manifest = buildCodeRepositoryManifest([header, main]); + const graph = buildCodeFileGraph(main, manifest); + + expect( + hasRelationship( + graph, + "CALLS", + `${repositoryPrefix}:src/app/main.c#run`, + `${repositoryPrefix}:src/include/math.h#add` + ) + ).toBe(true); + }); + + test("keeps same-named functions in different modules distinct after merge and dedupe", () => { + const alpha = baseFile({ + fileId: "file-alpha", + path: "src/alpha/math.ts", + content: "export function normalize() { return 'alpha'; }\n", + }); + const beta = baseFile({ + fileId: "file-beta", + path: "src/beta/math.ts", + content: "export function normalize() { return 'beta'; }\n", + }); + const manifest = buildCodeRepositoryManifest([alpha, beta]); + + const graph = dedupe(mergeGraphs([buildCodeFileGraph(alpha, manifest), buildCodeFileGraph(beta, manifest)])); + + expect( + graph.entities + .filter((entity) => entity.type === "CODE_FUNCTION" && entity.name.endsWith("#normalize")) + .map((entity) => entity.name) + .sort() + ).toEqual([ + `${repositoryPrefix}:src/alpha/math.ts#normalize`, + `${repositoryPrefix}:src/beta/math.ts#normalize`, + ]); + }); + + test("keeps same repository names from different URLs distinct after dedupe", () => { + const left = baseFile({ + fileId: "file-left", + path: "src/math.ts", + content: "export function normalize() { return 'left'; }\n", + }); + const right = { + ...baseFile({ + fileId: "file-right", + path: "src/math.ts", + content: "export function normalize() { return 'right'; }\n", + }), + repositoryUrl: "https://gitlab.com/acme/widgets.git", + }; + const manifest = buildCodeRepositoryManifest([left, right]); + + const graph = dedupe(mergeGraphs([buildCodeFileGraph(left, manifest), buildCodeFileGraph(right, manifest)])); + + expect( + graph.entities + .filter((entity) => entity.type === "CODE_FUNCTION" && entity.name.endsWith("src/math.ts#normalize")) + .map((entity) => entity.name) + .sort() + ).toEqual([ + "https://github.com/acme/widgets.git:src/math.ts#normalize", + "https://gitlab.com/acme/widgets.git:src/math.ts#normalize", + ]); + }); + + test("returns an empty graph for unsupported code paths", () => { + const file = baseFile({ fileId: "file-readme", path: "README.md", content: "# docs" }); + const graph = buildCodeFileGraph(file, buildCodeRepositoryManifest([file])); + + expect(graph.units).toEqual([]); + expect(graph.entities).toEqual([]); + expect(graph.relationships).toEqual([]); + }); +}); diff --git a/packages/graph/src/code/file-path.ts b/packages/graph/src/code/file-path.ts new file mode 100644 index 00000000..4c8983d9 --- /dev/null +++ b/packages/graph/src/code/file-path.ts @@ -0,0 +1,7 @@ +import path from "node:path"; + +const SUPPORTED_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mts", ".cts", ".rs", ".zig", ".c", ".h"]); + +export function isSupportedCodePath(filePath: string): boolean { + return SUPPORTED_EXTENSIONS.has(path.posix.extname(filePath).toLowerCase()); +} diff --git a/packages/graph/src/code/identity.ts b/packages/graph/src/code/identity.ts new file mode 100644 index 00000000..3bb87e7c --- /dev/null +++ b/packages/graph/src/code/identity.ts @@ -0,0 +1,23 @@ +import { createHash } from "node:crypto"; +import type { CodeManifestDefinition, CodeRepositoryFile } from "./types"; + +export function entityName(definition: CodeManifestDefinition): string { + return `${fileEntityName(definition)}#${definition.qualifiedName}`; +} + +export function fileEntityName(file: Pick): string { + return `${file.repositoryUrl}:${file.path}`; +} + +export function fileEntityId(repositoryUrl: string, commitSha: string, filePath: string): string { + return stableId("code_file", repositoryUrl, commitSha, filePath); +} + +export function definitionKey(filePath: string, simpleName: string): string { + return `${filePath}\0${simpleName}`; +} + +export function stableId(prefix: string, ...parts: string[]): string { + const hash = createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32); + return `${prefix}_${hash}`; +} diff --git a/packages/graph/src/code/imports.ts b/packages/graph/src/code/imports.ts new file mode 100644 index 00000000..e3ce9f55 --- /dev/null +++ b/packages/graph/src/code/imports.ts @@ -0,0 +1,638 @@ +import path from "node:path"; +import { definitionKey } from "./identity"; +import type { + CodeManifestDefinition, + CodeManifestExport, + CodeManifestFile, + CodeLanguage, + ExportRecord, + ImportBinding, + ImportRecord, + ImportResolutionMode, + ParsedCodeFile, + TreeSitterNode, +} from "./types"; +import { childForField, fieldText, walk } from "./syntax"; + +const ECMASCRIPT_RESOLVE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx"]; +const ZIG_RESOLVE_EXTENSIONS = [".zig"]; + +export function collectImports(root: TreeSitterNode, language: CodeLanguage): ImportRecord[] { + switch (language) { + case "javascript": + case "typescript": + case "tsx": + return collectEcmaScriptImports(root); + case "rust": + return collectRustImports(root); + case "zig": + return collectZigImports(root); + case "c": + return collectCImports(root); + } +} + +export function collectExports(file: ParsedCodeFile): ExportRecord[] { + if (!isEcmaScriptLanguage(file.language)) { + return []; + } + + const exports: ExportRecord[] = []; + walk(file.root, (node) => { + if (node.type !== "export_statement") { + return; + } + + exports.push(...parseExportStatement(node)); + }); + return exports; +} + +export function buildManifestExports( + files: ParsedCodeFile[], + manifestFiles: CodeManifestFile[], + definitions: CodeManifestDefinition[] +): CodeManifestExport[] { + const filesByPath = new Map(manifestFiles.map((file) => [file.path, file])); + const definitionsByPathAndName = new Map( + definitions.map((definition) => [definitionKey(definition.path, definition.simpleName), definition]) + ); + const rawExportsByPath = new Map(files.map((file) => [file.path, collectExports(file)])); + const cache = new Map(); + + const resolveExportedDefinition = (filePath: string, exportedName: string, stack: Set) => { + const exportMatch = resolveFileExports(filePath, stack).find((entry) => entry.exportedName === exportedName); + if (exportMatch) { + return exportMatch; + } + + return definitionsByPathAndName.get(definitionKey(filePath, exportedName)) ?? null; + }; + + const resolveFileExports = (filePath: string, stack: Set): CodeManifestExport[] => { + const cached = cache.get(filePath); + if (cached) { + return cached; + } + + if (stack.has(filePath)) { + return []; + } + stack.add(filePath); + + const resolved = new Map(); + for (const record of rawExportsByPath.get(filePath) ?? []) { + if (record.kind === "local") { + const definition = definitionsByPathAndName.get(definitionKey(filePath, record.localName)); + if (definition) { + resolved.set(record.exportedName, { + ...definition, + exportedName: record.exportedName, + exportedPath: filePath, + }); + } + continue; + } + + const targetPath = resolveImportTargetPath( + { + specifier: record.specifier, + resolutionMode: record.resolutionMode, + }, + filePath, + filesByPath + ); + if (!targetPath) { + continue; + } + + if (record.kind === "reexport") { + const definition = resolveExportedDefinition(targetPath, record.importedName, stack); + if (definition) { + resolved.set(record.exportedName, { + ...definition, + exportedName: record.exportedName, + exportedPath: filePath, + }); + } + continue; + } + + for (const definition of resolveFileExports(targetPath, stack)) { + if (definition.exportedName === "default") { + continue; + } + resolved.set(definition.exportedName, { + ...definition, + exportedPath: filePath, + }); + } + } + + const exports = [...resolved.values()]; + cache.set(filePath, exports); + stack.delete(filePath); + return exports; + }; + + for (const file of files) { + resolveFileExports(file.path, new Set()); + } + + return [...cache.values()].flat(); +} + +export function importLocalNames(importRecord: ImportRecord): string[] { + return [ + ...importRecord.namedImports.map((binding) => binding.local), + ...(importRecord.defaultImport ? [importRecord.defaultImport] : []), + ...(importRecord.namespaceImport ? [importRecord.namespaceImport] : []), + ]; +} + +export function parseHeritage(text: string): Array<{ kind: "EXTENDS" | "IMPLEMENTS"; name: string }> { + const items: Array<{ kind: "EXTENDS" | "IMPLEMENTS"; name: string }> = []; + const head = text.split("{")[0] ?? text; + const extendsMatch = head.match(/\bextends\s+([A-Za-z_$][\w$]*)/u)?.[1]; + if (extendsMatch) items.push({ kind: "EXTENDS", name: extendsMatch }); + + const implementsMatch = head.match(/\bimplements\s+([^{}]+)/u)?.[1]; + if (implementsMatch) { + for (const implemented of implementsMatch.split(",")) { + const name = implemented.trim().match(/^([A-Za-z_$][\w$]*)/u)?.[1]; + if (name) items.push({ kind: "IMPLEMENTS", name }); + } + } + + return items; +} + +export function resolveImportTargetPath( + importRecord: Pick, + currentFilePath: string, + filesByPath: ReadonlyMap +): string | null { + switch (importRecord.resolutionMode) { + case "relative": + return resolveRelativeImportPath(importRecord.specifier, currentFilePath, filesByPath, ECMASCRIPT_RESOLVE_EXTENSIONS); + case "zig": + return resolveRelativeImportPath(importRecord.specifier, currentFilePath, filesByPath, ZIG_RESOLVE_EXTENSIONS); + case "c-local": + return resolveLocalIncludePath(importRecord.specifier, currentFilePath, filesByPath); + case "rust": + return resolveRustImportPath(importRecord.specifier, currentFilePath, filesByPath); + case "external": + return null; + } +} + +function collectEcmaScriptImports(root: TreeSitterNode): ImportRecord[] { + const imports: ImportRecord[] = []; + walk(root, (node) => { + if (node.type !== "import_statement") return; + const parsed = parseImportStatement(node); + if (parsed) imports.push(parsed); + }); + return imports; +} + +function collectRustImports(root: TreeSitterNode): ImportRecord[] { + const imports: ImportRecord[] = []; + walk(root, (node) => { + if (node.type === "mod_item") { + const moduleName = fieldText(node, "name"); + if (moduleName) { + imports.push({ + node, + specifier: `self::${moduleName}`, + resolutionMode: "rust", + namespaceImport: moduleName, + namedImports: [], + }); + } + return; + } + + if (node.type !== "use_declaration") { + return; + } + + imports.push(...parseRustUseDeclaration(node)); + }); + return imports; +} + +function collectZigImports(root: TreeSitterNode): ImportRecord[] { + const imports: ImportRecord[] = []; + walk(root, (node) => { + if (node.type !== "variable_declaration") { + return; + } + + const localName = node.namedChild(0)?.text; + const builtin = node.namedChild(1); + const specifier = builtin ? zigImportSpecifier(builtin) : null; + if (!localName || !specifier) { + return; + } + + imports.push({ + node, + specifier, + resolutionMode: specifier.endsWith(".zig") ? "zig" : "external", + namespaceImport: localName, + namedImports: [], + }); + }); + return imports; +} + +function collectCImports(root: TreeSitterNode): ImportRecord[] { + const imports: ImportRecord[] = []; + walk(root, (node) => { + if (node.type !== "preproc_include") { + return; + } + + const rawPath = childForField(node, "path")?.text ?? ""; + if (!rawPath) { + return; + } + + const isLocalInclude = rawPath.startsWith("\""); + imports.push({ + node, + specifier: unquote(rawPath), + resolutionMode: isLocalInclude ? "c-local" : "external", + namedImports: [], + importAllDefinitions: true, + }); + }); + return imports; +} + +function parseImportStatement(node: TreeSitterNode): ImportRecord | null { + const specifier = parseStringSpecifier(node); + if (!specifier) return null; + const defaultImport = collectDefaultImport(node); + const namespaceImport = collectNamespaceImport(node); + + return { + node, + specifier, + resolutionMode: specifier.startsWith(".") ? "relative" : "external", + namedImports: collectNamedImports(node), + ...(defaultImport ? { defaultImport } : {}), + ...(namespaceImport ? { namespaceImport } : {}), + }; +} + +function parseExportStatement(node: TreeSitterNode): ExportRecord[] { + const declaration = childForField(node, "declaration"); + if (declaration) { + const names = collectDeclarationNames(declaration); + if (node.text.startsWith("export default ")) { + const localName = names[0]; + return localName + ? [ + { + node, + kind: "local", + exportedName: "default", + localName, + }, + ] + : []; + } + + return names.map((localName) => ({ + node, + kind: "local" as const, + exportedName: localName, + localName, + })); + } + + const value = childForField(node, "value"); + if (value?.type === "identifier" && node.text.startsWith("export default ")) { + return [ + { + node, + kind: "local", + exportedName: "default", + localName: value.text, + }, + ]; + } + + const specifier = parseStringSpecifier(node); + const exportClause = namedChildren(node).find((child) => child.type === "export_clause"); + if (exportClause && specifier) { + return collectExportClauseBindings(exportClause).map((binding) => ({ + node, + kind: "reexport" as const, + exportedName: binding.local, + importedName: binding.imported, + specifier, + resolutionMode: specifier.startsWith(".") ? "relative" : "external", + })); + } + + if (exportClause) { + return collectExportClauseBindings(exportClause).map((binding) => ({ + node, + kind: "local" as const, + exportedName: binding.local, + localName: binding.imported, + })); + } + + return specifier + ? [ + { + node, + kind: "export-all", + specifier, + resolutionMode: specifier.startsWith(".") ? "relative" : "external", + }, + ] + : []; +} + +function parseRustUseDeclaration(node: TreeSitterNode): ImportRecord[] { + const argument = childForField(node, "argument"); + if (!argument) { + return []; + } + + if (argument.type === "scoped_use_list") { + const basePath = childForField(argument, "path")?.text; + const useList = namedChildren(argument).find((child) => child.type === "use_list"); + if (!basePath || !useList) { + return []; + } + + return namedChildren(useList).flatMap((child) => { + if (child.type === "use_as_clause") { + return parseRustFlatUse(node, childForField(child, "path")?.text, fieldText(child, "alias"), basePath); + } + return parseRustFlatUse(node, child.text, undefined, basePath); + }); + } + + if (argument.type === "use_as_clause") { + return parseRustFlatUse(node, childForField(argument, "path")?.text, fieldText(argument, "alias")); + } + + return parseRustFlatUse(node, argument.text); +} + +function parseRustFlatUse( + node: TreeSitterNode, + pathText: string | undefined, + alias?: string | null, + basePath?: string +): ImportRecord[] { + if (!pathText) { + return []; + } + + const fullPath = basePath ? `${basePath}::${pathText}` : pathText; + const segments = fullPath.split("::").filter(Boolean); + if (segments.length === 0) { + return []; + } + + const resolutionMode = rustResolutionMode(segments[0] ?? ""); + const lastSegment = segments.at(-1); + if (!lastSegment) { + return []; + } + + if (resolutionMode === "external" && segments.length >= 2) { + return [ + { + node, + specifier: segments.slice(0, -1).join("::"), + resolutionMode, + namedImports: [{ imported: lastSegment, local: alias ?? lastSegment }], + }, + ]; + } + + if (segments.length >= 3) { + return [ + { + node, + specifier: segments.slice(0, -1).join("::"), + resolutionMode, + namedImports: [{ imported: lastSegment, local: alias ?? lastSegment }], + }, + ]; + } + + return [ + { + node, + specifier: fullPath, + resolutionMode, + namespaceImport: alias ?? lastSegment, + namedImports: [], + }, + ]; +} + +function parseStringSpecifier(node: TreeSitterNode): string | null { + const source = childForField(node, "source")?.text; + return source ? unquote(source) : null; +} + +function collectNamedImports(node: TreeSitterNode): ImportBinding[] { + const imports: ImportBinding[] = []; + walk(node, (candidate) => { + if (candidate.type !== "import_specifier") { + return; + } + + const imported = fieldText(candidate, "name") ?? candidate.text.split(/\s+as\s+/u)[0]?.trim(); + const local = fieldText(candidate, "alias") ?? candidate.text.split(/\s+as\s+/u)[1]?.trim() ?? imported; + if (imported && local) { + imports.push({ imported: imported.replace(/^type\s+/u, ""), local }); + } + }); + return imports; +} + +function collectExportClauseBindings(node: TreeSitterNode): ImportBinding[] { + const bindings: ImportBinding[] = []; + walk(node, (candidate) => { + if (candidate.type !== "export_specifier") { + return; + } + + const imported = fieldText(candidate, "name") ?? candidate.text.split(/\s+as\s+/u)[0]?.trim(); + const local = fieldText(candidate, "alias") ?? candidate.text.split(/\s+as\s+/u)[1]?.trim() ?? imported; + if (imported && local) { + bindings.push({ imported, local }); + } + }); + return bindings; +} + +function collectDeclarationNames(node: TreeSitterNode): string[] { + if (node.type === "function_declaration" || node.type === "class_declaration") { + const name = fieldText(node, "name"); + return name ? [name] : []; + } + + const names: string[] = []; + walk(node, (candidate) => { + if (candidate.type === "variable_declarator") { + const name = fieldText(candidate, "name"); + if (name) { + names.push(name); + } + } + }); + return names; +} + +function collectDefaultImport(node: TreeSitterNode): string | undefined { + const clause = namedChildren(node).find((child) => child.type === "import_clause"); + if (!clause) return undefined; + + for (let index = 0; index < clause.namedChildCount; index += 1) { + const child = clause.namedChild(index); + if (child?.type === "identifier") { + return child.text; + } + } + + return undefined; +} + +function collectNamespaceImport(node: TreeSitterNode): string | undefined { + let namespaceImport: string | undefined; + walk(node, (candidate) => { + if (candidate.type === "namespace_import") { + namespaceImport = fieldText(candidate, "name") ?? candidate.namedChild(0)?.text ?? undefined; + } + }); + return namespaceImport; +} + +function zigImportSpecifier(node: TreeSitterNode): string | null { + if (node.type !== "builtin_function" || node.namedChild(0)?.text !== "@import") { + return null; + } + + const argumentList = node.namedChild(1); + const stringNode = argumentList?.namedChild(0); + return stringNode ? unquote(stringNode.text) : null; +} + +function resolveRelativeImportPath( + specifier: string, + currentFilePath: string, + filesByPath: ReadonlyMap, + extensions: readonly string[] +): string | null { + const basePath = path.posix.normalize(path.posix.join(path.posix.dirname(currentFilePath), specifier)); + const candidates = dedupePaths([ + basePath, + ...extensions.map((extension) => (basePath.endsWith(extension) ? basePath : `${basePath}${extension}`)), + ...extensions.map((extension) => path.posix.join(basePath, `index${extension}`)), + ]); + + return candidates.find((candidate) => filesByPath.has(candidate)) ?? null; +} + +function resolveLocalIncludePath( + specifier: string, + currentFilePath: string, + filesByPath: ReadonlyMap +): string | null { + const candidate = path.posix.normalize(path.posix.join(path.posix.dirname(currentFilePath), specifier)); + return filesByPath.has(candidate) ? candidate : null; +} + +function resolveRustImportPath( + specifier: string, + currentFilePath: string, + filesByPath: ReadonlyMap +): string | null { + const segments = specifier.split("::").filter(Boolean); + if (segments.length === 0) { + return null; + } + + let baseDirectory = rustModuleDirectory(currentFilePath); + if (segments[0] === "crate") { + baseDirectory = rustCrateRootDirectory(currentFilePath); + segments.shift(); + } else if (segments[0] === "self") { + segments.shift(); + } else if (segments[0] === "super") { + while (segments[0] === "super") { + baseDirectory = path.posix.dirname(baseDirectory); + segments.shift(); + } + if (segments[0] === "self") { + segments.shift(); + } + } else { + return null; + } + + if (segments.length === 0) { + return null; + } + + const modulePath = path.posix.join(baseDirectory, ...segments); + const candidates = [`${modulePath}.rs`, path.posix.join(modulePath, "mod.rs")]; + return candidates.find((candidate) => filesByPath.has(candidate)) ?? null; +} + +function rustCrateRootDirectory(currentFilePath: string) { + const segments = currentFilePath.split("/"); + const sourceIndex = segments.lastIndexOf("src"); + if (sourceIndex === -1) { + return path.posix.dirname(currentFilePath); + } + return segments.slice(0, sourceIndex + 1).join("/"); +} + +function rustModuleDirectory(currentFilePath: string) { + const baseName = path.posix.basename(currentFilePath); + if (baseName === "main.rs" || baseName === "lib.rs" || baseName === "mod.rs") { + return path.posix.dirname(currentFilePath); + } + return path.posix.join(path.posix.dirname(currentFilePath), path.posix.basename(currentFilePath, ".rs")); +} + +function rustResolutionMode(segment: string): ImportResolutionMode { + return segment === "crate" || segment === "self" || segment === "super" ? "rust" : "external"; +} + +function isEcmaScriptLanguage(language: CodeLanguage) { + return language === "javascript" || language === "typescript" || language === "tsx"; +} + +function namedChildren(node: TreeSitterNode): TreeSitterNode[] { + const children: TreeSitterNode[] = []; + for (let index = 0; index < node.namedChildCount; index += 1) { + const child = node.namedChild(index); + if (child) { + children.push(child); + } + } + return children; +} + +function dedupePaths(paths: string[]) { + return [...new Set(paths)]; +} + +function unquote(value: string): string { + return value.replace(/^['"<]|[>'"]$/gu, ""); +} diff --git a/packages/graph/src/code/metadata.ts b/packages/graph/src/code/metadata.ts new file mode 100644 index 00000000..4b75b31b --- /dev/null +++ b/packages/graph/src/code/metadata.ts @@ -0,0 +1,77 @@ +import type { CodeRepositoryFile } from "./repository"; + +export type CodeFileExternalMetadata = { + provider: "github"; + rawUrl: string; + htmlUrl: string; +}; + +export type CodeFileMetadata = Omit & { + external?: CodeFileExternalMetadata; +}; + +export function serializeCodeFileMetadata(metadata: CodeFileMetadata): string { + return JSON.stringify(metadata); +} + + +function isAllowedExternalUrl(value: unknown, host: string): value is string { + if (typeof value !== "string") { + return false; + } + + try { + const url = new URL(value); + return url.protocol === "https:" && url.hostname === host; + } catch { + return false; + } +} + +export function parseCodeFileMetadata(value: string | null | undefined): CodeFileMetadata | null { + if (!value) { + return null; + } + + try { + const parsed = JSON.parse(value) as Partial; + if ( + typeof parsed.repositoryUrl !== "string" || + typeof parsed.repositoryName !== "string" || + typeof parsed.commitSha !== "string" || + typeof parsed.path !== "string" + ) { + return null; + } + + const external = + parsed.external && + typeof parsed.external === "object" && + "provider" in parsed.external && + parsed.external.provider === "github" && + "rawUrl" in parsed.external && + isAllowedExternalUrl(parsed.external.rawUrl, "raw.githubusercontent.com") && + "htmlUrl" in parsed.external && + isAllowedExternalUrl(parsed.external.htmlUrl, "github.com") + ? { + provider: parsed.external.provider, + rawUrl: parsed.external.rawUrl, + htmlUrl: parsed.external.htmlUrl, + } + : undefined; + + if (parsed.external && !external) { + return null; + } + + return { + repositoryUrl: parsed.repositoryUrl, + repositoryName: parsed.repositoryName, + commitSha: parsed.commitSha, + path: parsed.path, + ...(external ? { external } : {}), + }; + } catch { + return null; + } +} diff --git a/packages/graph/src/code/parser.ts b/packages/graph/src/code/parser.ts new file mode 100644 index 00000000..97105d2e --- /dev/null +++ b/packages/graph/src/code/parser.ts @@ -0,0 +1,70 @@ +import { createRequire } from "node:module"; +import path from "node:path"; +import type { CodeLanguage, CodeRepositoryFile, ParsedCodeFile, TreeSitterLanguage, TreeSitterParser } from "./types"; + +const require = createRequire(import.meta.url); +const Parser = require("tree-sitter") as { new (): TreeSitterParser }; +const C = require("tree-sitter-c") as TreeSitterLanguage; +const JavaScript = require("tree-sitter-javascript") as TreeSitterLanguage; +const Rust = require("tree-sitter-rust") as TreeSitterLanguage; +const TypeScript = require("tree-sitter-typescript") as { + typescript: TreeSitterLanguage; + tsx: TreeSitterLanguage; +}; +const Zig = require("@tree-sitter-grammars/tree-sitter-zig") as TreeSitterLanguage; + +export function parseCodeFile(file: CodeRepositoryFile): ParsedCodeFile[] { + const language = detectCodeLanguage(file.path); + if (!language) { + return []; + } + + const parser = new Parser(); + parser.setLanguage(languageGrammar(language)); + const tree = parser.parse(file.content); + if (!tree) { + return []; + } + + return [{ ...file, language, root: tree.rootNode }]; +} + +function detectCodeLanguage(filePath: string): CodeLanguage | null { + switch (path.posix.extname(filePath).toLowerCase()) { + case ".js": + return "javascript"; + case ".jsx": + case ".tsx": + return "tsx"; + case ".ts": + case ".mts": + case ".cts": + return "typescript"; + case ".rs": + return "rust"; + case ".zig": + return "zig"; + case ".c": + case ".h": + return "c"; + default: + return null; + } +} + +function languageGrammar(language: CodeLanguage): TreeSitterLanguage { + switch (language) { + case "javascript": + return JavaScript; + case "tsx": + return TypeScript.tsx; + case "typescript": + return TypeScript.typescript; + case "rust": + return Rust; + case "zig": + return Zig; + case "c": + return C; + } +} diff --git a/packages/graph/src/code/repository.ts b/packages/graph/src/code/repository.ts new file mode 100644 index 00000000..1a77725a --- /dev/null +++ b/packages/graph/src/code/repository.ts @@ -0,0 +1,597 @@ +import path from "node:path"; +import type { Entity, Graph, Relationship, Source, TextUnitSourceChunk, Unit } from ".."; +import { definitionKey, entityName, fileEntityId, fileEntityName, stableId } from "./identity"; +import { buildManifestExports, collectImports, parseHeritage, resolveImportTargetPath } from "./imports"; +import { parseCodeFile } from "./parser"; +import { callName, collectDefinitions, nodeSnippet, spanSize, walk } from "./syntax"; +import type { + CodeManifestDefinition, + CodeManifestExport, + CodeManifestFile, + CodeRepositoryFile, + CodeRepositoryManifest, + Definition, + ImportRecord, + ParsedCodeFile, + TreeSitterNode, +} from "./types"; + +export { isSupportedCodePath } from "./file-path"; +export type { CodeManifestDefinition, CodeManifestFile, CodeRepositoryFile, CodeRepositoryManifest } from "./types"; + +export function buildCodeRepositoryManifest(files: CodeRepositoryFile[]): CodeRepositoryManifest { + const parsedFiles = files.flatMap(parseCodeFile); + const manifestFiles = parsedFiles.map((file) => manifestFile(file)); + const definitions = parsedFiles.flatMap((file) => + collectDefinitions(file).map(({ node: _node, ...definition }) => definition) + ); + const exports = buildManifestExports(parsedFiles, manifestFiles, definitions); + + return { + files: manifestFiles, + definitions, + exports, + }; +} + +export function buildCodeFileGraph(file: CodeRepositoryFile, manifest: CodeRepositoryManifest): Graph { + const parsed = parseCodeFile(file)[0]; + if (!parsed) { + return { + id: stableId("code_graph", file.repositoryUrl, file.commitSha, file.path), + units: [], + entities: [], + relationships: [], + }; + } + + return new CodeFileGraphBuilder(parsed, manifest).build(); +} + +class CodeFileGraphBuilder { + private readonly units: Unit[] = []; + private readonly entitiesById = new Map(); + private readonly relationshipsById = new Map(); + private readonly definitions: Definition[]; + private readonly definitionsBySimpleName: Map; + private readonly manifestFilesByPath: Map; + private readonly manifestDefinitionsByPathAndName: Map; + private readonly manifestDefinitionsByPath: Map; + private readonly manifestExportsByPathAndName: Map; + private readonly importTargetsByLocalName = new Map(); + private readonly namespaceImportPathsByLocalName = new Map(); + private readonly externalNamespaceImportSpecifiersByLocalName = new Map(); + private readonly wildcardExternalImportSpecifiers = new Set(); + private fileEntity!: Entity; + + constructor(private readonly file: ParsedCodeFile, manifest: CodeRepositoryManifest) { + this.definitions = collectDefinitions(file); + this.definitionsBySimpleName = new Map( + this.definitions.map((definition) => [definition.simpleName, definition]) + ); + this.manifestFilesByPath = new Map(manifest.files.map((file) => [file.path, file])); + this.manifestDefinitionsByPathAndName = new Map( + manifest.definitions.map((definition) => [definitionKey(definition.path, definition.simpleName), definition]) + ); + this.manifestDefinitionsByPath = new Map(); + for (const definition of manifest.definitions) { + const definitions = this.manifestDefinitionsByPath.get(definition.path) ?? []; + definitions.push(definition); + this.manifestDefinitionsByPath.set(definition.path, definitions); + } + this.manifestExportsByPathAndName = new Map( + (manifest.exports ?? []).map((entry) => [definitionKey(entry.exportedPath, entry.exportedName), entry]) + ); + } + + build(): Graph { + this.fileEntity = this.addEntity({ + id: fileEntityId(this.file.repositoryUrl, this.file.commitSha, this.file.path), + name: fileEntityName(this.file), + type: "CODE_FILE", + source: this.sourceFor({ + description: `Source file ${this.file.path} in ${this.file.repositoryName} at ${this.file.commitSha}.`, + text: this.file.path, + }), + }); + + for (const definition of this.definitions) { + this.addDefinition(definition); + } + + this.indexImports(); + this.indexCallsAndHeritage(); + + return { + id: stableId("code_graph", this.file.repositoryUrl, this.file.commitSha, this.file.path), + units: this.units, + entities: [...this.entitiesById.values()], + relationships: [...this.relationshipsById.values()], + }; + } + + private addDefinition(definition: Definition) { + const entity = this.addEntity({ + id: definition.entityId, + name: entityName(definition), + type: definition.type, + source: this.sourceFor({ + node: definition.node, + description: [ + `Defines ${definition.type.toLowerCase()} ${definition.qualifiedName} in ${this.file.path}.`, + nodeSnippet(this.file, definition.node), + ].join("\n\n"), + }), + }); + + const parent = definition.parentQualifiedName + ? this.definitions.find((candidate) => candidate.qualifiedName === definition.parentQualifiedName) + : undefined; + + this.addRelationship({ + source: parent ? this.entityForManifestDefinition(parent) : this.fileEntity, + target: entity, + kind: "CONTAINS", + strength: 1, + node: definition.node, + description: parent + ? `${parent.qualifiedName} contains ${definition.qualifiedName}.` + : `${this.file.path} contains ${definition.qualifiedName}.`, + }); + } + + private indexImports() { + for (const importRecord of collectImports(this.file.root, this.file.language)) { + const targetPath = resolveImportTargetPath(importRecord, this.file.path, this.manifestFilesByPath); + const targetFile = targetPath ? this.manifestFilesByPath.get(targetPath) : undefined; + const targetEntity = targetFile ? this.fileEntityForManifest(targetFile) : this.externalModuleEntity(importRecord); + + this.addRelationship({ + source: this.fileEntity, + target: targetEntity, + kind: "IMPORTS", + strength: 0.9, + node: importRecord.node, + description: `${this.file.path} imports ${importRecord.specifier}.`, + }); + + if (!targetPath) { + if (importRecord.importAllDefinitions) { + this.wildcardExternalImportSpecifiers.add(importRecord.specifier); + } + + for (const binding of importRecord.namedImports) { + this.importTargetsByLocalName.set( + binding.local, + this.externalSymbolEntity(importRecord.specifier, binding.imported, importRecord.node) + ); + } + + if (importRecord.defaultImport) { + this.importTargetsByLocalName.set( + importRecord.defaultImport, + this.externalSymbolEntity(importRecord.specifier, "default", importRecord.node) + ); + this.externalNamespaceImportSpecifiersByLocalName.set( + importRecord.defaultImport, + importRecord.specifier + ); + } + + if (importRecord.namespaceImport) { + this.importTargetsByLocalName.set(importRecord.namespaceImport, targetEntity); + this.externalNamespaceImportSpecifiersByLocalName.set( + importRecord.namespaceImport, + importRecord.specifier + ); + } + continue; + } + + if (importRecord.importAllDefinitions) { + for (const definition of this.manifestDefinitionsByPath.get(targetPath) ?? []) { + this.importTargetsByLocalName.set(definition.simpleName, this.entityForManifestDefinition(definition)); + } + } + + for (const binding of importRecord.namedImports) { + const targetDefinition = this.resolveImportedDefinition(targetPath, binding.imported); + if (targetDefinition) { + this.importTargetsByLocalName.set(binding.local, this.entityForManifestDefinition(targetDefinition)); + } + } + + if (importRecord.defaultImport) { + const defaultTarget = this.resolveImportedDefinition(targetPath, "default"); + this.importTargetsByLocalName.set( + importRecord.defaultImport, + defaultTarget ? this.entityForManifestDefinition(defaultTarget) : targetEntity + ); + } + + if (importRecord.namespaceImport) { + this.namespaceImportPathsByLocalName.set(importRecord.namespaceImport, targetPath); + this.importTargetsByLocalName.set(importRecord.namespaceImport, targetEntity); + } + } + } + + private indexCallsAndHeritage() { + walk(this.file.root, (node) => { + if (node.type === "call_expression") { + this.collectCall(node); + return; + } + + if (node.type === "class_declaration" || node.type === "interface_declaration") { + this.collectHeritage(node); + } + }); + } + + private collectCall(node: TreeSitterNode) { + const callerDefinition = this.enclosingDefinition(node); + const caller = callerDefinition ? this.entityForManifestDefinition(callerDefinition) : this.fileEntity; + const calleeName = callName(node); + if (!calleeName) { + return; + } + + const target = this.resolveCallee(callerDefinition ?? null, calleeName); + if (!target) { + return; + } + + this.addRelationship({ + source: caller, + target, + kind: "CALLS", + strength: 0.8, + node, + description: `${caller.name} calls ${target.name}.`, + }); + } + + private collectHeritage(node: TreeSitterNode) { + const sourceDefinition = this.enclosingDefinition(node); + if (!sourceDefinition) { + return; + } + + for (const heritage of parseHeritage(node.text)) { + const target = this.resolveName(heritage.name); + if (!target) { + continue; + } + + this.addRelationship({ + source: this.entityForManifestDefinition(sourceDefinition), + target, + kind: heritage.kind, + strength: 0.9, + node, + description: `${entityName(sourceDefinition)} ${heritage.kind.toLowerCase()} ${target.name}.`, + }); + } + } + + private resolveCallee(caller: Definition | null, name: string): Entity | null { + if (name.includes(".")) { + const segments = name.split("."); + const objectName = segments[0]; + const memberPath = segments.slice(1).join("."); + const finalName = segments.at(-1); + + if ((objectName === "this" || objectName === "self") && finalName && caller) { + const className = caller.qualifiedName.split(".").at(0); + const method = this.definitions.find( + (definition) => definition.qualifiedName === `${className}.${finalName}` + ); + if (method) { + return this.entityForManifestDefinition(method); + } + } + + if (this.file.language === "rust") { + const rustTarget = this.resolveRustQualifiedCallee(name, finalName); + if (rustTarget) { + return rustTarget; + } + } + + if (objectName && finalName) { + const namespacePath = this.namespaceImportPathsByLocalName.get(objectName); + if (namespacePath) { + const importedDefinition = this.resolveImportedDefinition(namespacePath, finalName); + if (importedDefinition) { + return this.entityForManifestDefinition(importedDefinition); + } + } + + const externalSpecifier = this.externalNamespaceImportSpecifiersByLocalName.get(objectName); + if (externalSpecifier && memberPath) { + return this.externalSymbolEntity(externalSpecifier, memberPath); + } + } + + return objectName ? (this.importTargetsByLocalName.get(objectName) ?? null) : null; + } + + return this.resolveName(name); + } + + private resolveName(name: string): Entity | null { + const definition = this.definitionsBySimpleName.get(name); + if (definition) { + return this.entityForManifestDefinition(definition); + } + + const imported = this.importTargetsByLocalName.get(name); + if (imported) { + return imported; + } + + if (this.wildcardExternalImportSpecifiers.size === 1) { + return this.externalSymbolEntity([...this.wildcardExternalImportSpecifiers][0]!, name); + } + + return null; + } + + private resolveImportedDefinition(targetPath: string, importedName: string): CodeManifestDefinition | CodeManifestExport | null { + return ( + this.manifestExportsByPathAndName.get(definitionKey(targetPath, importedName)) ?? + this.manifestDefinitionsByPathAndName.get(definitionKey(targetPath, importedName)) ?? + null + ); + } + + private enclosingDefinition(node: TreeSitterNode): Definition | null { + return ( + this.definitions + .filter( + (definition) => + definition.node.startIndex <= node.startIndex && definition.node.endIndex >= node.endIndex + ) + .sort((left, right) => spanSize(left.node) - spanSize(right.node))[0] ?? null + ); + } + + private entityForManifestDefinition(definition: CodeManifestDefinition): Entity { + return this.addEntity({ + id: definition.entityId, + name: entityName(definition), + type: definition.type, + }); + } + + private fileEntityForManifest(file: CodeManifestFile): Entity { + return this.addEntity({ + id: file.entityId, + name: fileEntityName(file), + type: "CODE_FILE", + }); + } + + private externalModuleEntity(importRecord: ImportRecord): Entity { + return this.addEntity({ + id: stableId("code_external", this.file.repositoryUrl, this.file.commitSha, importRecord.specifier), + name: `${this.file.repositoryName}:external:${importRecord.specifier}`, + type: "CODE_EXTERNAL_MODULE", + source: this.sourceFor({ + node: importRecord.node, + description: `External module ${importRecord.specifier} imported by ${this.file.path}.`, + }), + }); + } + private externalSymbolEntity(specifier: string, symbolName: string, node?: TreeSitterNode): Entity { + return this.addEntity({ + id: stableId("code_external_symbol", this.file.repositoryUrl, this.file.commitSha, specifier, symbolName), + name: `${this.file.repositoryName}:external:${specifier}#${symbolName}`, + type: "CODE_EXTERNAL_SYMBOL", + ...(node + ? { + source: this.sourceFor({ + node, + description: `External symbol ${symbolName} from ${specifier} referenced by ${this.file.path}.`, + }), + } + : {}), + }); + } + + private resolveRustQualifiedCallee(name: string, finalName: string | undefined): Entity | null { + if (!finalName) { + return null; + } + + const pathPrefix = name.slice(0, -(finalName.length + 1)); + const targetPath = this.resolveRustQualifiedTargetPath(pathPrefix); + if (!targetPath) { + return null; + } + + const targetDefinition = this.resolveImportedDefinition(targetPath, finalName); + return targetDefinition ? this.entityForManifestDefinition(targetDefinition) : null; + } + + private resolveRustQualifiedTargetPath(pathPrefix: string): string | null { + if (!pathPrefix) { + return null; + } + + if (pathPrefix.startsWith("crate::") || pathPrefix.startsWith("self::") || pathPrefix.startsWith("super::")) { + return resolveImportTargetPath( + { specifier: pathPrefix, resolutionMode: "rust" }, + this.file.path, + this.manifestFilesByPath + ); + } + + const [rootSegment, ...nestedSegments] = pathPrefix.split("::"); + if (!rootSegment) { + return null; + } + + const basePath = this.namespaceImportPathsByLocalName.get(rootSegment); + if (!basePath) { + return null; + } + + return this.resolveRustNestedModulePath(basePath, nestedSegments); + } + + private resolveRustNestedModulePath(basePath: string, nestedSegments: string[]): string | null { + let currentPath = basePath; + for (const segment of nestedSegments) { + const moduleDirectory = + path.posix.basename(currentPath) === "mod.rs" + ? path.posix.dirname(currentPath) + : path.posix.join(path.posix.dirname(currentPath), path.posix.basename(currentPath, ".rs")); + const candidateBase = path.posix.join(moduleDirectory, segment); + const nextPath = + [candidateBase, `${candidateBase}.rs`, path.posix.join(candidateBase, "mod.rs")].find((candidate) => + this.manifestFilesByPath.has(candidate) + ) ?? null; + if (!nextPath) { + return null; + } + currentPath = nextPath; + } + + return currentPath; + } + + private addEntity(input: { id: string; name: string; type: string; source?: Source }): Entity { + const existing = this.entitiesById.get(input.id); + if (existing) { + if (input.source && !existing.sources.some((source) => source.id === input.source?.id)) { + existing.sources.push(input.source); + } + return existing; + } + + const entity = { + id: input.id, + name: input.name, + type: input.type, + description: "", + sources: input.source ? [input.source] : [], + } satisfies Entity; + this.entitiesById.set(entity.id, entity); + return entity; + } + + private addRelationship(input: { + source: Entity; + target: Entity; + kind: string; + strength: number; + node: TreeSitterNode; + description: string; + }) { + const id = stableId( + "code_relationship", + this.file.repositoryUrl, + this.file.commitSha, + this.file.path, + input.kind, + input.source.id, + input.target.id, + String(input.node.startIndex), + String(input.node.endIndex) + ); + const source = this.sourceFor({ + node: input.node, + description: input.description, + }); + const existing = this.relationshipsById.get(id); + if (existing) { + if (!existing.sources.some((candidate) => candidate.id === source.id)) { + existing.sources.push(source); + } + existing.strength = Math.max(existing.strength, input.strength); + return existing; + } + + const relationship = { + id, + sourceId: input.source.id, + targetId: input.target.id, + kind: input.kind, + directed: true, + strength: input.strength, + description: "", + sources: [source], + } satisfies Relationship; + this.relationshipsById.set(relationship.id, relationship); + return relationship; + } + + private sourceFor(input: { node?: TreeSitterNode; description: string; text?: string }): Source { + const unit = this.unitFor(input.node, input.text); + return { + id: stableId("code_source", unit.id, input.description), + unitId: unit.id, + description: input.description, + sourceChunkIds: [1], + }; + } + + private unitFor(node?: TreeSitterNode, text?: string): Unit { + const content = text ?? (node ? nodeSnippet(this.file, node) : this.file.path); + const id = stableId( + "code_unit", + this.file.repositoryUrl, + this.file.commitSha, + this.file.path, + String(node?.startIndex ?? 0), + String(node?.endIndex ?? 0), + content + ); + const existing = this.units.find((unit) => unit.id === id); + if (existing) { + return existing; + } + + const chunk = { + id: 1, + type: "text", + text: content, + startPage: null, + endPage: null, + filePath: this.file.path, + language: this.file.language, + ...(node + ? { + startLine: node.startPosition.row + 1, + endLine: node.endPosition.row + 1, + startColumn: node.startPosition.column + 1, + endColumn: node.endPosition.column + 1, + } + : {}), + } satisfies TextUnitSourceChunk; + const unit = { + id, + fileId: this.file.fileId, + content, + startPage: null, + endPage: null, + chunks: [chunk], + } satisfies Unit; + this.units.push(unit); + return unit; + } +} + +function manifestFile(file: ParsedCodeFile): CodeManifestFile { + return { + fileId: file.fileId, + repositoryUrl: file.repositoryUrl, + repositoryName: file.repositoryName, + commitSha: file.commitSha, + path: file.path, + language: file.language, + entityId: fileEntityId(file.repositoryUrl, file.commitSha, file.path), + }; +} diff --git a/packages/graph/src/code/syntax.ts b/packages/graph/src/code/syntax.ts new file mode 100644 index 00000000..8e4da7d6 --- /dev/null +++ b/packages/graph/src/code/syntax.ts @@ -0,0 +1,259 @@ +import type { Definition, ParsedCodeFile, TreeSitterNode } from "./types"; +import { stableId } from "./identity"; + +export function collectDefinitions(file: ParsedCodeFile): Definition[] { + const definitions: Definition[] = []; + + walk(file.root, (node) => { + const definition = definitionFromNode(file, node); + if (definition) { + definitions.push(definition); + } + }); + + return definitions; +} + +export function walk(node: TreeSitterNode, visit: (node: TreeSitterNode) => void) { + visit(node); + for (const child of namedChildren(node)) { + walk(child, visit); + } +} + +export function childForField(node: TreeSitterNode, fieldName: string): TreeSitterNode | null { + return node.childForFieldName(fieldName); +} + +export function fieldText(node: TreeSitterNode, fieldName: string): string | null { + return childForField(node, fieldName)?.text ?? null; +} + +export function spanSize(node: TreeSitterNode) { + return node.endIndex - node.startIndex; +} + +export function nodeSnippet(file: ParsedCodeFile, node: TreeSitterNode): string { + return file.content.slice(node.startIndex, node.endIndex).trimEnd(); +} + +export function callName(node: TreeSitterNode): string | null { + const callee = childForField(node, "function"); + if (!callee) return null; + if (callee.type === "identifier" || callee.type === "field_identifier" || callee.type === "property_identifier") { + return callee.text; + } + + return memberLikeName(callee); +} + +function definitionFromNode(file: ParsedCodeFile, node: TreeSitterNode): Definition | null { + switch (node.type) { + case "function_declaration": + case "generator_function_declaration": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_FUNCTION"); + } + case "function_signature": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_FUNCTION"); + } + case "function_signature_item": + case "function_item": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + const parentQualifiedName = + node.type === "function_signature_item" ? enclosingRustTraitName(node) : enclosingRustImplTypeName(node); + return parentQualifiedName + ? definition(file, node, simpleName, `${parentQualifiedName}.${simpleName}`, "CODE_METHOD", parentQualifiedName) + : definition(file, node, simpleName, simpleName, "CODE_FUNCTION"); + } + case "function_definition": { + const simpleName = cFunctionName(node); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_FUNCTION"); + } + case "declaration": { + const simpleName = cFunctionName(node); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_FUNCTION"); + } + case "class_declaration": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_CLASS"); + } + case "struct_item": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_CLASS"); + } + case "interface_declaration": + case "trait_item": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_INTERFACE"); + } + case "type_alias_declaration": { + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition(file, node, simpleName, simpleName, "CODE_TYPE"); + } + case "method_definition": { + const simpleName = fieldText(node, "name"); + const className = enclosingClassName(node); + if (!simpleName || !className) return null; + return definition(file, node, simpleName, `${className}.${simpleName}`, "CODE_METHOD", className); + } + case "variable_declarator": { + const value = childForField(node, "value"); + if (!value || !["arrow_function", "function", "class"].includes(value.type)) { + return null; + } + + const simpleName = fieldText(node, "name"); + if (!simpleName) return null; + return definition( + file, + node, + simpleName, + simpleName, + value.type === "class" ? "CODE_CLASS" : "CODE_FUNCTION" + ); + } + default: + return null; + } +} + +function definition( + file: ParsedCodeFile, + node: TreeSitterNode, + simpleName: string, + qualifiedName: string, + type: string, + parentQualifiedName?: string +): Definition { + return { + entityId: stableId("code_entity", file.repositoryUrl, file.commitSha, file.path, qualifiedName), + fileId: file.fileId, + path: file.path, + repositoryUrl: file.repositoryUrl, + repositoryName: file.repositoryName, + commitSha: file.commitSha, + simpleName, + qualifiedName, + type, + node, + ...(parentQualifiedName ? { parentQualifiedName } : {}), + }; +} + +function namedChildren(node: TreeSitterNode): TreeSitterNode[] { + const children: TreeSitterNode[] = []; + for (let index = 0; index < node.namedChildCount; index += 1) { + const child = node.namedChild(index); + if (child) children.push(child); + } + return children; +} + +function memberLikeName(node: TreeSitterNode): string | null { + if (node.type === "member_expression") { + const object = childForField(node, "object")?.text ?? node.namedChild(0)?.text; + const property = childForField(node, "property")?.text ?? node.namedChild(1)?.text; + return object && property ? `${object}.${property}` : (property ?? null); + } + + if (node.type === "field_expression") { + const object = childForField(node, "object")?.text ?? childForField(node, "value")?.text ?? node.namedChild(0)?.text; + const property = childForField(node, "property")?.text ?? childForField(node, "field")?.text ?? node.namedChild(1)?.text; + return object && property ? `${object}.${property}` : (property ?? null); + } + + if (node.type === "scoped_identifier") { + const object = childForField(node, "path")?.text ?? node.namedChild(0)?.text; + const property = childForField(node, "name")?.text ?? node.namedChild(1)?.text; + return object && property ? `${object}.${property}` : (property ?? null); + } + + return null; +} + +function cFunctionName(node: TreeSitterNode): string | null { + const declarator = childForField(node, "declarator"); + const functionDeclarator = findNodeByType(declarator, "function_declarator"); + if (!functionDeclarator) { + return null; + } + + return fieldText(functionDeclarator, "declarator") ?? firstIdentifier(functionDeclarator); +} + +function findNodeByType(node: TreeSitterNode | null, type: string): TreeSitterNode | null { + if (!node) { + return null; + } + if (node.type === type) { + return node; + } + + for (const child of namedChildren(node)) { + const match = findNodeByType(child, type); + if (match) { + return match; + } + } + + return null; +} + +function firstIdentifier(node: TreeSitterNode): string | null { + if (node.type === "identifier" || node.type === "field_identifier" || node.type === "type_identifier") { + return node.text; + } + + for (const child of namedChildren(node)) { + const match = firstIdentifier(child); + if (match) { + return match; + } + } + + return null; +} + +function enclosingClassName(node: TreeSitterNode): string | null { + let current = node.parent; + while (current) { + if (current.type === "class_declaration") { + return fieldText(current, "name"); + } + current = current.parent; + } + return null; +} + +function enclosingRustImplTypeName(node: TreeSitterNode): string | null { + let current = node.parent; + while (current) { + if (current.type === "impl_item") { + return fieldText(current, "type"); + } + current = current.parent; + } + return null; +} + +function enclosingRustTraitName(node: TreeSitterNode): string | null { + let current = node.parent; + while (current) { + if (current.type === "trait_item") { + return fieldText(current, "name"); + } + current = current.parent; + } + return null; +} diff --git a/packages/graph/src/code/types.ts b/packages/graph/src/code/types.ts new file mode 100644 index 00000000..b66b1795 --- /dev/null +++ b/packages/graph/src/code/types.ts @@ -0,0 +1,121 @@ +export type TreeSitterLanguage = unknown; + +export type TreeSitterPoint = { + row: number; + column: number; +}; + +export type TreeSitterNode = { + type: string; + text: string; + startIndex: number; + endIndex: number; + startPosition: TreeSitterPoint; + endPosition: TreeSitterPoint; + namedChildCount: number; + parent: TreeSitterNode | null; + namedChild: (index: number) => TreeSitterNode | null; + childForFieldName: (name: string) => TreeSitterNode | null; +}; + +export type TreeSitterTree = { + rootNode: TreeSitterNode; +}; + +export type TreeSitterParser = { + setLanguage: (language: TreeSitterLanguage) => void; + parse: (source: string) => TreeSitterTree | null; +}; + +export type CodeLanguage = "javascript" | "typescript" | "tsx" | "rust" | "zig" | "c"; + +export type ImportResolutionMode = "relative" | "zig" | "rust" | "c-local" | "external"; + +export type CodeRepositoryFile = { + fileId: string; + repositoryUrl: string; + repositoryName: string; + commitSha: string; + path: string; + content: string; +}; + +export type CodeManifestFile = { + fileId: string; + repositoryUrl: string; + repositoryName: string; + commitSha: string; + path: string; + language: CodeLanguage; + entityId: string; +}; + +export type CodeManifestDefinition = { + entityId: string; + fileId: string; + path: string; + repositoryUrl: string; + repositoryName: string; + commitSha: string; + simpleName: string; + qualifiedName: string; + type: string; +}; + +export type CodeManifestExport = CodeManifestDefinition & { + exportedName: string; + exportedPath: string; +}; + +export type CodeRepositoryManifest = { + files: CodeManifestFile[]; + definitions: CodeManifestDefinition[]; + exports: CodeManifestExport[]; +}; + +export type ParsedCodeFile = CodeRepositoryFile & { + language: CodeLanguage; + root: TreeSitterNode; +}; + +export type Definition = CodeManifestDefinition & { + node: TreeSitterNode; + parentQualifiedName?: string; +}; + +export type ImportBinding = { + imported: string; + local: string; +}; + +export type ImportRecord = { + node: TreeSitterNode; + specifier: string; + resolutionMode: ImportResolutionMode; + defaultImport?: string; + namespaceImport?: string; + namedImports: ImportBinding[]; + importAllDefinitions?: boolean; +}; + +export type ExportRecord = + | { + node: TreeSitterNode; + kind: "local"; + exportedName: string; + localName: string; + } + | { + node: TreeSitterNode; + kind: "reexport"; + exportedName: string; + importedName: string; + specifier: string; + resolutionMode: ImportResolutionMode; + } + | { + node: TreeSitterNode; + kind: "export-all"; + specifier: string; + resolutionMode: ImportResolutionMode; + }; diff --git a/packages/graph/src/dedupe.ts b/packages/graph/src/dedupe.ts index a3cc1748..d855c9d5 100644 --- a/packages/graph/src/dedupe.ts +++ b/packages/graph/src/dedupe.ts @@ -1,5 +1,6 @@ import { ulid } from "ulid"; import type { Entity, Graph, Relationship, Source, Unit } from "."; +import { DEFAULT_RELATIONSHIP_KIND, normalizedRelationshipEndpoints, relationshipKey } from "./relationship-key"; const exactOnlyTypes = new Set(["DATE", "FACT"]); const organizationSuffixes = new Set([ @@ -243,13 +244,6 @@ const mergeUnits = (units: Unit[]) => { return [...merged.values()]; }; -const buildRelationshipKey = (sourceId: string, targetId: string) => { - const normalizedSourceId = sourceId <= targetId ? sourceId : targetId; - const normalizedTargetId = sourceId <= targetId ? targetId : sourceId; - - return `${normalizedSourceId}::${normalizedTargetId}`; -}; - export function dedupe(graph: Graph): Graph { const parents = graph.entities.map((_, index) => index); @@ -321,9 +315,17 @@ export function dedupe(graph: Graph): Graph { continue; } - const normalizedSourceId = sourceId <= targetId ? sourceId : targetId; - const normalizedTargetId = sourceId <= targetId ? targetId : sourceId; - const key = buildRelationshipKey(normalizedSourceId, normalizedTargetId); + const endpoints = normalizedRelationshipEndpoints({ + sourceId, + targetId, + directed: relationship.directed, + }); + const key = relationshipKey({ + sourceId: endpoints.sourceId, + targetId: endpoints.targetId, + kind: relationship.kind, + directed: endpoints.directed, + }); const existingRelationship = relationshipMap.get(key); if (existingRelationship) { @@ -347,8 +349,10 @@ export function dedupe(graph: Graph): Graph { relationshipMap.set(key, { ...relationship, - sourceId: normalizedSourceId, - targetId: normalizedTargetId, + sourceId: endpoints.sourceId, + targetId: endpoints.targetId, + kind: relationship.kind ?? DEFAULT_RELATIONSHIP_KIND, + directed: endpoints.directed, description: (relationship.description ?? "") .trim() .replace(/[\r\n]+/g, " ") diff --git a/packages/graph/src/file-type.ts b/packages/graph/src/file-type.ts index aab76a4d..c4b79354 100644 --- a/packages/graph/src/file-type.ts +++ b/packages/graph/src/file-type.ts @@ -1,4 +1,5 @@ import { FILE_TYPE_VALUES, type FileTypeValue } from "@kiwi/contracts/file-types"; +import { isSupportedCodePath } from "./code/file-path"; export const GRAPH_FILE_TYPES = FILE_TYPE_VALUES; export type GraphFileType = FileTypeValue; @@ -164,5 +165,9 @@ export function inferGraphFileType(file: Pick): GraphFile return "toml"; } + if (isSupportedCodePath(name)) { + return "code"; + } + return "text"; } diff --git a/packages/graph/src/index.ts b/packages/graph/src/index.ts index bf45342b..ad60a3f9 100644 --- a/packages/graph/src/index.ts +++ b/packages/graph/src/index.ts @@ -12,6 +12,8 @@ export type Relationship = { id: string; sourceId: string; targetId: string; + kind?: string; + directed?: boolean; strength: number; description?: string; sources: Source[]; diff --git a/packages/graph/src/loader/factory.ts b/packages/graph/src/loader/factory.ts index 6c2cfbbf..3e85ca6f 100644 --- a/packages/graph/src/loader/factory.ts +++ b/packages/graph/src/loader/factory.ts @@ -70,6 +70,7 @@ export const DEFAULT_DOCUMENT_MODES: Record) => `${entity.name}::${entity.type}`; -const getRelationshipKey = (relationship: Pick) => { - const sourceId = relationship.sourceId <= relationship.targetId ? relationship.sourceId : relationship.targetId; - const targetId = relationship.sourceId <= relationship.targetId ? relationship.targetId : relationship.sourceId; - - return `${sourceId}::${targetId}`; -}; - export function mergeGraphs(graphs: Graph[]): Graph; export function mergeGraphs(left: Graph, right: Graph): Graph; export function mergeGraphs(input: Graph[] | Graph, right?: Graph): Graph { @@ -52,9 +46,17 @@ export function mergeGraphs(input: Graph[] | Graph, right?: Graph): Graph { continue; } - const normalizedSourceId = sourceId <= targetId ? sourceId : targetId; - const normalizedTargetId = sourceId <= targetId ? targetId : sourceId; - const key = getRelationshipKey({ sourceId: normalizedSourceId, targetId: normalizedTargetId }); + const endpoints = normalizedRelationshipEndpoints({ + sourceId, + targetId, + directed: relationship.directed, + }); + const key = relationshipKey({ + sourceId: endpoints.sourceId, + targetId: endpoints.targetId, + kind: relationship.kind, + directed: endpoints.directed, + }); const existingRelationship = mergedRelationships.get(key); if (existingRelationship) { @@ -70,8 +72,10 @@ export function mergeGraphs(input: Graph[] | Graph, right?: Graph): Graph { mergedRelationships.set(key, { ...relationship, - sourceId: normalizedSourceId, - targetId: normalizedTargetId, + sourceId: endpoints.sourceId, + targetId: endpoints.targetId, + kind: relationship.kind ?? DEFAULT_RELATIONSHIP_KIND, + directed: endpoints.directed, sources: [...relationship.sources], }); } diff --git a/packages/graph/src/relationship-key.ts b/packages/graph/src/relationship-key.ts new file mode 100644 index 00000000..1c1783b9 --- /dev/null +++ b/packages/graph/src/relationship-key.ts @@ -0,0 +1,26 @@ +import type { Relationship } from "."; + +export const DEFAULT_RELATIONSHIP_KIND = "RELATED"; + +export function normalizedRelationshipEndpoints( + relationship: Pick +): { sourceId: string; targetId: string; directed: boolean } { + const directed = relationship.directed === true; + const sourceId = directed + ? relationship.sourceId + : relationship.sourceId <= relationship.targetId + ? relationship.sourceId + : relationship.targetId; + const targetId = directed + ? relationship.targetId + : relationship.sourceId <= relationship.targetId + ? relationship.targetId + : relationship.sourceId; + + return { sourceId, targetId, directed }; +} + +export function relationshipKey(relationship: Pick) { + const endpoints = normalizedRelationshipEndpoints(relationship); + return `${relationship.kind ?? DEFAULT_RELATIONSHIP_KIND}::${endpoints.directed ? "directed" : "undirected"}::${endpoints.sourceId}::${endpoints.targetId}`; +}