From 5b97bb69eb9670a7714c5e031fa919b12e3a87a2 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Sat, 4 Jul 2026 01:10:24 +0200 Subject: [PATCH 1/4] feat(integrations): add per-repo and per-board title filters Adds github_title_filters and linear_title_filters tables (one row per rule, no jsonb) with contains/regex match types. Enabled rules are loaded into the GitHub and Linear tool contexts so pull requests, commits, and issues with matching titles are skipped during content generation, and the GitHub webhook path drops filtered commits and release events before dispatching triggers. The integration detail pages get a Title filters section with one-click presets (docs, chore, ci/build, tests, dependency bumps, reverts, merge commits, wip) plus custom text or regex rules, with pause/resume and delete per rule. Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko --- .../integrations/github/[id]/page-client.tsx | 9 + .../integrations/linear/[id]/page-client.tsx | 7 + .../integrations/title-filters-section.tsx | 405 ++ apps/dashboard/src/constants/title-filters.ts | 79 + .../src/lib/orpc/routers/integrations.ts | 262 + apps/dashboard/src/lib/webhooks/github.ts | 8 + .../src/lib/webhooks/title-filters.ts | 82 + apps/dashboard/src/schemas/title-filters.ts | 47 + apps/dashboard/src/types/title-filters.ts | 29 + packages/ai/src/integrations/github.ts | 4 + packages/ai/src/integrations/linear.ts | 4 + packages/ai/src/integrations/title-filters.ts | 172 + packages/ai/src/tools/github.ts | 17 +- packages/ai/src/tools/linear.ts | 7 +- packages/ai/src/types/tools.ts | 9 + packages/ai/src/utils/title-filters.ts | 30 + .../db/migrations/0048_normal_blue_marvel.sql | 26 + .../db/migrations/meta/0048_snapshot.json | 5988 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/schema.ts | 76 +- 20 files changed, 7265 insertions(+), 3 deletions(-) create mode 100644 apps/dashboard/src/components/integrations/title-filters-section.tsx create mode 100644 apps/dashboard/src/constants/title-filters.ts create mode 100644 apps/dashboard/src/lib/webhooks/title-filters.ts create mode 100644 apps/dashboard/src/schemas/title-filters.ts create mode 100644 apps/dashboard/src/types/title-filters.ts create mode 100644 packages/ai/src/integrations/title-filters.ts create mode 100644 packages/ai/src/utils/title-filters.ts create mode 100644 packages/db/migrations/0048_normal_blue_marvel.sql create mode 100644 packages/db/migrations/meta/0048_snapshot.json diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx index b6c2ab988..17928bdc7 100644 --- a/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx +++ b/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx @@ -25,6 +25,7 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/button"; +import { TitleFiltersSection } from "@/components/integrations/title-filters-section"; import { useOrganizationsContext } from "@/components/providers/organization-provider"; import { dashboardOrpc } from "@/lib/orpc/query"; import type { GitHubIntegration, GitHubRepository } from "@/types/integrations"; @@ -644,6 +645,14 @@ export default function PageClient({ integrationId }: PageClientProps) { slug={activeOrganization?.slug ?? ""} /> + {primaryRepository ? ( + + ) : null} + {organizationId && integration.repositories.length > 0 ? (
diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/integrations/linear/[id]/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/integrations/linear/[id]/page-client.tsx index 4365caa87..7ee678f60 100644 --- a/apps/dashboard/src/app/(dashboard)/[slug]/integrations/linear/[id]/page-client.tsx +++ b/apps/dashboard/src/app/(dashboard)/[slug]/integrations/linear/[id]/page-client.tsx @@ -12,6 +12,7 @@ import { format } from "date-fns"; import dynamic from "next/dynamic"; import { useState } from "react"; import { Button } from "@/components/button"; +import { TitleFiltersSection } from "@/components/integrations/title-filters-section"; import { useOrganizationsContext } from "@/components/providers/organization-provider"; import { dashboardOrpc } from "@/lib/orpc/query"; import type { LinearIntegration } from "@/types/integrations"; @@ -186,6 +187,12 @@ export default function PageClient({ integrationId }: PageClientProps) {
+ + diff --git a/apps/dashboard/src/components/integrations/title-filters-section.tsx b/apps/dashboard/src/components/integrations/title-filters-section.tsx new file mode 100644 index 000000000..65c6ca69e --- /dev/null +++ b/apps/dashboard/src/components/integrations/title-filters-section.tsx @@ -0,0 +1,405 @@ +"use client"; + +import { + Add01Icon, + Delete02Icon, + HelpCircleIcon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Badge } from "@notra/ui/components/ui/badge"; +import { Input } from "@notra/ui/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@notra/ui/components/ui/select"; +import { Skeleton } from "@notra/ui/components/ui/skeleton"; +import { Switch } from "@notra/ui/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@notra/ui/components/ui/tooltip"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Loader2Icon } from "lucide-react"; +import { type FormEvent, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/button"; +import { + GITHUB_TITLE_FILTER_PRESETS, + LINEAR_TITLE_FILTER_PRESETS, + TITLE_FILTER_MATCH_TYPE_OPTIONS, +} from "@/constants/title-filters"; +import { dashboardOrpc } from "@/lib/orpc/query"; +import { + type CreateTitleFilterBody, + isValidTitleFilterRegex, + MAX_TITLE_FILTER_PATTERN_LENGTH, + type TitleFilterMatchType, +} from "@/schemas/title-filters"; +import type { + TitleFilter, + TitleFilterPreset, + TitleFiltersSectionProps, +} from "@/types/title-filters"; + +function getPresetForFilter(filter: TitleFilter, presets: TitleFilterPreset[]) { + return presets.find( + (preset) => + preset.matchType === filter.matchType && preset.pattern === filter.pattern + ); +} + +export function TitleFiltersSection({ + source, + organizationId, + targetId, +}: TitleFiltersSectionProps) { + const queryClient = useQueryClient(); + const isGithub = source === "github"; + const presets = isGithub + ? GITHUB_TITLE_FILTER_PRESETS + : LINEAR_TITLE_FILTER_PRESETS; + + const [matchType, setMatchType] = useState("contains"); + const [pattern, setPattern] = useState(""); + const [patternError, setPatternError] = useState(null); + + const listQueryKey = isGithub + ? dashboardOrpc.integrations.repositories.titleFilters.list.queryKey({ + input: { organizationId, repositoryId: targetId }, + }) + : dashboardOrpc.integrations.linear.titleFilters.list.queryKey({ + input: { organizationId, integrationId: targetId }, + }); + + const { data, isLoading, isError } = useQuery( + isGithub + ? dashboardOrpc.integrations.repositories.titleFilters.list.queryOptions({ + input: { organizationId, repositoryId: targetId }, + }) + : dashboardOrpc.integrations.linear.titleFilters.list.queryOptions({ + input: { organizationId, integrationId: targetId }, + }) + ); + + const filters = data?.filters ?? []; + + const invalidateList = () => { + queryClient.invalidateQueries({ queryKey: listQueryKey }); + }; + + const createMutation = useMutation( + { + mutationFn: (body) => + isGithub + ? dashboardOrpc.integrations.repositories.titleFilters.create.call({ + organizationId, + repositoryId: targetId, + ...body, + }) + : dashboardOrpc.integrations.linear.titleFilters.create.call({ + organizationId, + integrationId: targetId, + ...body, + }), + onSuccess: () => { + invalidateList(); + setPattern(""); + setPatternError(null); + }, + onError: (error) => { + toast.error(error.message); + }, + } + ); + + const toggleMutation = useMutation< + TitleFilter, + Error, + { filterId: string; enabled: boolean } + >({ + mutationFn: ({ filterId, enabled }) => + isGithub + ? dashboardOrpc.integrations.repositories.titleFilters.update.call({ + organizationId, + repositoryId: targetId, + filterId, + enabled, + }) + : dashboardOrpc.integrations.linear.titleFilters.update.call({ + organizationId, + integrationId: targetId, + filterId, + enabled, + }), + onSuccess: invalidateList, + onError: (error) => { + toast.error(error.message); + }, + }); + + const deleteMutation = useMutation< + { success: boolean }, + Error, + { filterId: string } + >({ + mutationFn: ({ filterId }) => + isGithub + ? dashboardOrpc.integrations.repositories.titleFilters.delete.call({ + organizationId, + repositoryId: targetId, + filterId, + }) + : dashboardOrpc.integrations.linear.titleFilters.delete.call({ + organizationId, + integrationId: targetId, + filterId, + }), + onSuccess: invalidateList, + onError: (error) => { + toast.error(error.message); + }, + }); + + const handleAdd = (event: FormEvent) => { + event.preventDefault(); + + const trimmedPattern = pattern.trim(); + if (!trimmedPattern) { + setPatternError("Enter a pattern first"); + return; + } + + if (matchType === "regex" && !isValidTitleFilterRegex(trimmedPattern)) { + setPatternError("Enter a valid regular expression"); + return; + } + + setPatternError(null); + createMutation.mutate({ matchType, pattern: trimmedPattern }); + }; + + const handleMatchTypeChange = (value: TitleFilterMatchType | null) => { + if (value) { + setMatchType(value); + setPatternError(null); + } + }; + + const availablePresets = presets.filter( + (preset) => + !filters.some( + (filter) => + filter.matchType === preset.matchType && + filter.pattern === preset.pattern + ) + ); + + const itemsLabel = isGithub + ? "pull requests, commits, and releases" + : "issues"; + + return ( +
+
+
+

Title filters

+ + + } + > + + + + Items whose title matches an active filter are skipped when Notra + reads your {isGithub ? "GitHub activity" : "Linear issues"} for + content generation and automations. Text filters match anywhere in + the title; regex filters are case-insensitive. + + +
+

+ Exclude {itemsLabel} from generated content by title. +

+
+ + {isLoading && } + + {!isLoading && isError && ( +
+ Failed to load title filters. +
+ )} + + {!(isLoading || isError) && ( +
+
+
+ + { + setPattern(event.target.value); + if (patternError) { + setPatternError(null); + } + }} + placeholder={ + matchType === "regex" ? "^docs(\\(.*\\))?:" : "docs:" + } + value={pattern} + /> + +
+ {patternError && ( +

{patternError}

+ )} + {availablePresets.length > 0 && ( +
+ + Quick add: + + {availablePresets.map((preset) => ( + + + createMutation.mutate({ + matchType: preset.matchType, + pattern: preset.pattern, + }) + } + size="sm" + type="button" + variant="outline" + /> + } + > + + {preset.label} + + +

{preset.description}

+

+ {preset.pattern} +

+
+
+ ))} +
+ )} +
+ +
+ {filters.length === 0 && ( +

+ No title filters yet. Everything is included. +

+ )} + {filters.map((filter) => { + const preset = getPresetForFilter(filter, presets); + + return ( +
+
+ + {filter.matchType === "regex" ? "Regex" : "Text"} + + + {filter.pattern} + + {preset && ( + + {preset.label} + + )} +
+
+ + + toggleMutation.mutate({ + filterId: filter.id, + enabled: checked, + }) + } + size="sm" + /> + } + /> + + {filter.enabled ? "Active" : "Paused"} + + + +
+
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/apps/dashboard/src/constants/title-filters.ts b/apps/dashboard/src/constants/title-filters.ts new file mode 100644 index 000000000..a1bb27b58 --- /dev/null +++ b/apps/dashboard/src/constants/title-filters.ts @@ -0,0 +1,79 @@ +import type { TitleFilterMatchType } from "@/schemas/title-filters"; +import type { TitleFilterPreset } from "@/types/title-filters"; + +export const TITLE_FILTER_MATCH_TYPE_OPTIONS: Array<{ + value: TitleFilterMatchType; + label: string; +}> = [ + { value: "contains", label: "Contains text" }, + { value: "regex", label: "Regex" }, +]; + +const TITLE_FILTER_PRESETS: TitleFilterPreset[] = [ + { + id: "docs", + label: "Docs", + description: 'Excludes "docs: ..." and "docs(scope): ..." titles', + matchType: "regex", + pattern: "^docs(\\(.*\\))?!?:", + }, + { + id: "chore", + label: "Chores", + description: 'Excludes "chore: ..." and "chore(scope): ..." titles', + matchType: "regex", + pattern: "^chore(\\(.*\\))?!?:", + }, + { + id: "ci", + label: "CI & build", + description: 'Excludes "ci: ..." and "build: ..." titles', + matchType: "regex", + pattern: "^(ci|build)(\\(.*\\))?!?:", + }, + { + id: "tests", + label: "Tests", + description: 'Excludes "test: ..." and "tests(scope): ..." titles', + matchType: "regex", + pattern: "^tests?(\\(.*\\))?!?:", + }, + { + id: "deps", + label: "Dependency bumps", + description: + 'Excludes dependency updates like "chore(deps): ..." and "bump ..."', + matchType: "regex", + pattern: "^(chore|fix|build)\\(deps.*\\)!?:|^bump ", + }, + { + id: "reverts", + label: "Reverts", + description: 'Excludes "revert ..." titles', + matchType: "regex", + pattern: "^revert\\b", + }, + { + id: "merges", + label: "Merge commits", + description: + 'Excludes "Merge branch ..." and "Merge pull request ..." titles', + matchType: "regex", + pattern: "^merge (branch|pull request|remote)\\b", + }, + { + id: "wip", + label: "WIP", + description: 'Excludes "wip: ..." and "[WIP] ..." titles', + matchType: "regex", + pattern: "^\\[?wip\\]?\\b", + }, +]; + +const LINEAR_PRESET_IDS = new Set(["docs", "chore", "tests", "wip"]); + +export const GITHUB_TITLE_FILTER_PRESETS = TITLE_FILTER_PRESETS; + +export const LINEAR_TITLE_FILTER_PRESETS = TITLE_FILTER_PRESETS.filter( + (preset) => LINEAR_PRESET_IDS.has(preset.id) +); diff --git a/apps/dashboard/src/lib/orpc/routers/integrations.ts b/apps/dashboard/src/lib/orpc/routers/integrations.ts index 82772435a..85d36ff1c 100644 --- a/apps/dashboard/src/lib/orpc/routers/integrations.ts +++ b/apps/dashboard/src/lib/orpc/routers/integrations.ts @@ -36,6 +36,16 @@ import { updateMcpServerIntegration, } from "@notra/ai/integrations/mcp"; import { refreshMcpToolIndexForIntegration } from "@notra/ai/integrations/mcp-tool-index"; +import { + createGithubTitleFilter, + createLinearTitleFilter, + deleteGithubTitleFilter, + deleteLinearTitleFilter, + getGithubTitleFilters, + getLinearTitleFilters, + setGithubTitleFilterEnabled, + setLinearTitleFilterEnabled, +} from "@notra/ai/integrations/title-filters"; import { deleteQstashSchedule } from "@notra/ai/qstash/triggers"; import { db } from "@notra/db/drizzle"; import { contentTriggers } from "@notra/db/schema"; @@ -65,11 +75,18 @@ import { updateRepositoryBodySchema, } from "@/schemas/integrations"; import { updateLinearIntegrationBodySchema } from "@/schemas/linear"; +import { + createTitleFilterBodySchema, + MAX_TITLE_FILTERS, + titleFilterIdSchema, + updateTitleFilterBodySchema, +} from "@/schemas/title-filters"; import type { GitHubIntegration, GitHubRepository, RepositoryOutput, } from "@/types/integrations"; +import type { TitleFilter } from "@/types/title-filters"; import { badRequest, conflict, @@ -211,6 +228,35 @@ function serializeListedIntegration(integration: { }; } +function serializeTitleFilter(filter: { + id: string; + matchType: "contains" | "regex"; + pattern: string; + enabled: boolean; + createdAt: Date; +}): TitleFilter { + return { + id: filter.id, + matchType: filter.matchType, + pattern: filter.pattern, + enabled: filter.enabled, + createdAt: filter.createdAt.toISOString(), + }; +} + +async function requireLinearIntegrationInOrganization( + organizationId: string, + integrationId: string +) { + const integration = await getLinearIntegrationById(integrationId); + + if (!integration || integration.organizationId !== organizationId) { + throw notFound("Linear integration not found"); + } + + return integration; +} + async function requireIntegrationInOrganization( organizationId: string, integrationId: string @@ -691,6 +737,114 @@ export const integrationsRouter = { config: input.config, }); }), + titleFilters: { + list: baseProcedure + .input(repositoryInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + + await requireRepositoryInOrganization( + input.organizationId, + input.repositoryId + ); + + const filters = await getGithubTitleFilters(input.repositoryId); + + return { filters: filters.map(serializeTitleFilter) }; + }), + create: baseProcedure + .input(repositoryInputSchema.and(createTitleFilterBodySchema)) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + await assertActiveSubscription(input.organizationId); + + await requireRepositoryInOrganization( + input.organizationId, + input.repositoryId + ); + + const existing = await getGithubTitleFilters(input.repositoryId); + if (existing.length >= MAX_TITLE_FILTERS) { + throw badRequest( + `You can add up to ${MAX_TITLE_FILTERS} title filters per repository` + ); + } + + try { + const created = await createGithubTitleFilter(input.repositoryId, { + matchType: input.matchType, + pattern: input.pattern, + }); + + if (!created) { + throw internalServerError("Failed to create title filter"); + } + + return serializeTitleFilter(created); + } catch (error) { + if (isUniqueConstraintError(error)) { + throw conflict("This title filter already exists"); + } + throw error; + } + }), + update: baseProcedure + .input(repositoryInputSchema.and(updateTitleFilterBodySchema)) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + await assertActiveSubscription(input.organizationId); + + await requireRepositoryInOrganization( + input.organizationId, + input.repositoryId + ); + + const updated = await setGithubTitleFilterEnabled( + input.repositoryId, + input.filterId, + input.enabled + ); + + if (!updated) { + throw notFound("Title filter not found"); + } + + return serializeTitleFilter(updated); + }), + delete: baseProcedure + .input(repositoryInputSchema.and(titleFilterIdSchema)) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + + await requireRepositoryInOrganization( + input.organizationId, + input.repositoryId + ); + + const deleted = await deleteGithubTitleFilter( + input.repositoryId, + input.filterId + ); + + if (!deleted) { + throw notFound("Title filter not found"); + } + + return { success: true }; + }), + }, webhook: { get: baseProcedure .input(repositoryInputSchema) @@ -876,6 +1030,114 @@ export const integrationsRouter = { return { success: true }; }), + titleFilters: { + list: baseProcedure + .input(integrationInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + + await requireLinearIntegrationInOrganization( + input.organizationId, + input.integrationId + ); + + const filters = await getLinearTitleFilters(input.integrationId); + + return { filters: filters.map(serializeTitleFilter) }; + }), + create: baseProcedure + .input(integrationInputSchema.and(createTitleFilterBodySchema)) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + await assertActiveSubscription(input.organizationId); + + await requireLinearIntegrationInOrganization( + input.organizationId, + input.integrationId + ); + + const existing = await getLinearTitleFilters(input.integrationId); + if (existing.length >= MAX_TITLE_FILTERS) { + throw badRequest( + `You can add up to ${MAX_TITLE_FILTERS} title filters per integration` + ); + } + + try { + const created = await createLinearTitleFilter(input.integrationId, { + matchType: input.matchType, + pattern: input.pattern, + }); + + if (!created) { + throw internalServerError("Failed to create title filter"); + } + + return serializeTitleFilter(created); + } catch (error) { + if (isUniqueConstraintError(error)) { + throw conflict("This title filter already exists"); + } + throw error; + } + }), + update: baseProcedure + .input(integrationInputSchema.and(updateTitleFilterBodySchema)) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + await assertActiveSubscription(input.organizationId); + + await requireLinearIntegrationInOrganization( + input.organizationId, + input.integrationId + ); + + const updated = await setLinearTitleFilterEnabled( + input.integrationId, + input.filterId, + input.enabled + ); + + if (!updated) { + throw notFound("Title filter not found"); + } + + return serializeTitleFilter(updated); + }), + delete: baseProcedure + .input(integrationInputSchema.and(titleFilterIdSchema)) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + }); + + await requireLinearIntegrationInOrganization( + input.organizationId, + input.integrationId + ); + + const deleted = await deleteLinearTitleFilter( + input.integrationId, + input.filterId + ); + + if (!deleted) { + throw notFound("Title filter not found"); + } + + return { success: true }; + }), + }, }, mcp: { list: baseProcedure diff --git a/apps/dashboard/src/lib/webhooks/github.ts b/apps/dashboard/src/lib/webhooks/github.ts index 4802b58f7..d906bd960 100644 --- a/apps/dashboard/src/lib/webhooks/github.ts +++ b/apps/dashboard/src/lib/webhooks/github.ts @@ -7,6 +7,7 @@ import { and, eq, sql } from "drizzle-orm"; import { checkLogRetention } from "@/lib/billing/check-log-retention"; import { dispatchEventTriggers } from "@/lib/webhooks/dispatch-event-triggers"; import { appendWebhookLog } from "@/lib/webhooks/logging"; +import { applyTitleFiltersToGithubEvent } from "@/lib/webhooks/title-filters"; import { type GitHubEventType, type GitHubWebhookPayload, @@ -392,6 +393,13 @@ export async function handleGitHubWebhook( }); } + if (processedEvent) { + processedEvent = await applyTitleFiltersToGithubEvent( + repositoryId, + processedEvent + ); + } + if (!processedEvent) { await appendWebhookLog({ organizationId, diff --git a/apps/dashboard/src/lib/webhooks/title-filters.ts b/apps/dashboard/src/lib/webhooks/title-filters.ts new file mode 100644 index 000000000..763d343bf --- /dev/null +++ b/apps/dashboard/src/lib/webhooks/title-filters.ts @@ -0,0 +1,82 @@ +import { getEnabledGithubTitleFilterRules } from "@notra/ai/integrations/title-filters"; +import type { TitleFilterRule } from "@notra/ai/types/tools"; +import { getCommitTitle, isTitleExcluded } from "@notra/ai/utils/title-filters"; +import type { GithubProcessedEvent } from "@/types/webhooks/webhooks"; + +function getStringField(data: Record, key: string) { + const value = data[key]; + return typeof value === "string" ? value : null; +} + +function isCommitExcluded(commit: unknown, rules: TitleFilterRule[]) { + if (typeof commit !== "object" || commit === null) { + return false; + } + + if (!("message" in commit) || typeof commit.message !== "string") { + return false; + } + + return isTitleExcluded(getCommitTitle(commit.message), rules); +} + +function filterReleaseEvent( + processedEvent: GithubProcessedEvent, + rules: TitleFilterRule[] +) { + const title = + getStringField(processedEvent.data, "name") ?? + getStringField(processedEvent.data, "tagName"); + + return isTitleExcluded(title, rules) ? null : processedEvent; +} + +function filterPushEvent( + processedEvent: GithubProcessedEvent, + rules: TitleFilterRule[] +) { + const commits = processedEvent.data.commits; + if (!Array.isArray(commits)) { + return processedEvent; + } + + const remainingCommits = commits.filter( + (commit) => !isCommitExcluded(commit, rules) + ); + + if (remainingCommits.length === 0) { + return null; + } + + if (remainingCommits.length === commits.length) { + return processedEvent; + } + + return { + ...processedEvent, + data: { + ...processedEvent.data, + commits: remainingCommits, + }, + }; +} + +export async function applyTitleFiltersToGithubEvent( + repositoryId: string, + processedEvent: GithubProcessedEvent +): Promise { + const rules = await getEnabledGithubTitleFilterRules(repositoryId); + if (rules.length === 0) { + return processedEvent; + } + + if (processedEvent.type === "release") { + return filterReleaseEvent(processedEvent, rules); + } + + if (processedEvent.type === "push") { + return filterPushEvent(processedEvent, rules); + } + + return processedEvent; +} diff --git a/apps/dashboard/src/schemas/title-filters.ts b/apps/dashboard/src/schemas/title-filters.ts new file mode 100644 index 000000000..b9a9dda51 --- /dev/null +++ b/apps/dashboard/src/schemas/title-filters.ts @@ -0,0 +1,47 @@ +// biome-ignore lint/performance/noNamespaceImport: Zod recommended way to import +import * as z from "zod"; + +export const TITLE_FILTER_MATCH_TYPES = ["contains", "regex"] as const; +export type TitleFilterMatchType = (typeof TITLE_FILTER_MATCH_TYPES)[number]; + +export const MAX_TITLE_FILTER_PATTERN_LENGTH = 256; +export const MAX_TITLE_FILTERS = 50; + +export function isValidTitleFilterRegex(pattern: string) { + try { + new RegExp(pattern, "i"); + return true; + } catch { + return false; + } +} + +export const titleFilterPatternSchema = z + .string() + .trim() + .min(1, "Pattern is required") + .max(MAX_TITLE_FILTER_PATTERN_LENGTH, "Pattern is too long"); + +export const createTitleFilterBodySchema = z + .object({ + matchType: z.enum(TITLE_FILTER_MATCH_TYPES), + pattern: titleFilterPatternSchema, + }) + .refine( + (value) => + value.matchType !== "regex" || isValidTitleFilterRegex(value.pattern), + { + message: "Enter a valid regular expression", + path: ["pattern"], + } + ); +export type CreateTitleFilterBody = z.infer; + +export const titleFilterIdSchema = z.object({ + filterId: z.string().min(1, "Filter ID is required"), +}); + +export const updateTitleFilterBodySchema = titleFilterIdSchema.extend({ + enabled: z.boolean(), +}); +export type UpdateTitleFilterBody = z.infer; diff --git a/apps/dashboard/src/types/title-filters.ts b/apps/dashboard/src/types/title-filters.ts new file mode 100644 index 000000000..59cc4dc85 --- /dev/null +++ b/apps/dashboard/src/types/title-filters.ts @@ -0,0 +1,29 @@ +import type { TitleFilterMatchType } from "@/schemas/title-filters"; + +export interface TitleFilter { + id: string; + matchType: TitleFilterMatchType; + pattern: string; + enabled: boolean; + createdAt: string; +} + +export interface TitleFiltersResponse { + filters: TitleFilter[]; +} + +export interface TitleFilterPreset { + id: string; + label: string; + description: string; + matchType: TitleFilterMatchType; + pattern: string; +} + +export type TitleFilterSource = "github" | "linear"; + +export interface TitleFiltersSectionProps { + source: TitleFilterSource; + organizationId: string; + targetId: string; +} diff --git a/packages/ai/src/integrations/github.ts b/packages/ai/src/integrations/github.ts index 6600c7cb8..a482d51fc 100644 --- a/packages/ai/src/integrations/github.ts +++ b/packages/ai/src/integrations/github.ts @@ -30,6 +30,7 @@ import type { GitHubToolRepositoryContext } from "../types/tools"; import { createOctokit } from "../utils/octokit"; import { redis } from "../utils/redis"; import { getConfiguredAppUrl } from "../utils/url"; +import { getEnabledGithubTitleFilterRules } from "./title-filters"; const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 16); @@ -1216,6 +1217,8 @@ export async function getGitHubToolRepositoryContextByIntegrationId( token = decryptToken(integration.encryptedToken); } + const titleFilters = await getEnabledGithubTitleFilterRules(integration.id); + return { integrationId: integration.id, organizationId: integration.organizationId, @@ -1223,6 +1226,7 @@ export async function getGitHubToolRepositoryContextByIntegrationId( repo, defaultBranch: integration.defaultBranch, token, + titleFilters, }; } diff --git a/packages/ai/src/integrations/linear.ts b/packages/ai/src/integrations/linear.ts index 571a0dd70..b131ba1be 100644 --- a/packages/ai/src/integrations/linear.ts +++ b/packages/ai/src/integrations/linear.ts @@ -5,6 +5,7 @@ import { customAlphabet } from "nanoid"; import { decryptToken, encryptToken } from "../crypto/token-encryption"; import type { CreateLinearIntegrationParams } from "../types/integrations"; import type { LinearToolContext } from "../types/tools"; +import { getEnabledLinearTitleFilterRules } from "./title-filters"; const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 16); @@ -193,10 +194,13 @@ export async function getLinearToolContextByIntegrationId( ); } + const titleFilters = await getEnabledLinearTitleFilterRules(integration.id); + return { integrationId: integration.id, organizationId: integration.organizationId, accessToken: decryptToken(integration.encryptedAccessToken), linearTeamId: integration.linearTeamId, + titleFilters, }; } diff --git a/packages/ai/src/integrations/title-filters.ts b/packages/ai/src/integrations/title-filters.ts new file mode 100644 index 000000000..561565e6b --- /dev/null +++ b/packages/ai/src/integrations/title-filters.ts @@ -0,0 +1,172 @@ +import { db } from "@notra/db/drizzle"; +import { githubTitleFilters, linearTitleFilters } from "@notra/db/schema"; +import { and, asc, eq } from "drizzle-orm"; +import { customAlphabet } from "nanoid"; +import type { TitleFilterMatchType, TitleFilterRule } from "../types/tools"; + +const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 16); + +interface CreateTitleFilterParams { + matchType: TitleFilterMatchType; + pattern: string; +} + +export async function getGithubTitleFilters(repositoryId: string) { + return db.query.githubTitleFilters.findMany({ + where: eq(githubTitleFilters.repositoryId, repositoryId), + orderBy: [asc(githubTitleFilters.createdAt)], + }); +} + +export async function getEnabledGithubTitleFilterRules( + repositoryId: string +): Promise { + const filters = await db + .select({ + matchType: githubTitleFilters.matchType, + pattern: githubTitleFilters.pattern, + }) + .from(githubTitleFilters) + .where( + and( + eq(githubTitleFilters.repositoryId, repositoryId), + eq(githubTitleFilters.enabled, true) + ) + ); + + return filters; +} + +export async function createGithubTitleFilter( + repositoryId: string, + params: CreateTitleFilterParams +) { + const [created] = await db + .insert(githubTitleFilters) + .values({ + id: nanoid(), + repositoryId, + matchType: params.matchType, + pattern: params.pattern, + enabled: true, + }) + .returning(); + + return created; +} + +export async function setGithubTitleFilterEnabled( + repositoryId: string, + filterId: string, + enabled: boolean +) { + const [updated] = await db + .update(githubTitleFilters) + .set({ enabled }) + .where( + and( + eq(githubTitleFilters.id, filterId), + eq(githubTitleFilters.repositoryId, repositoryId) + ) + ) + .returning(); + + return updated; +} + +export async function deleteGithubTitleFilter( + repositoryId: string, + filterId: string +) { + const [deleted] = await db + .delete(githubTitleFilters) + .where( + and( + eq(githubTitleFilters.id, filterId), + eq(githubTitleFilters.repositoryId, repositoryId) + ) + ) + .returning(); + + return deleted; +} + +export async function getLinearTitleFilters(integrationId: string) { + return db.query.linearTitleFilters.findMany({ + where: eq(linearTitleFilters.integrationId, integrationId), + orderBy: [asc(linearTitleFilters.createdAt)], + }); +} + +export async function getEnabledLinearTitleFilterRules( + integrationId: string +): Promise { + const filters = await db + .select({ + matchType: linearTitleFilters.matchType, + pattern: linearTitleFilters.pattern, + }) + .from(linearTitleFilters) + .where( + and( + eq(linearTitleFilters.integrationId, integrationId), + eq(linearTitleFilters.enabled, true) + ) + ); + + return filters; +} + +export async function createLinearTitleFilter( + integrationId: string, + params: CreateTitleFilterParams +) { + const [created] = await db + .insert(linearTitleFilters) + .values({ + id: nanoid(), + integrationId, + matchType: params.matchType, + pattern: params.pattern, + enabled: true, + }) + .returning(); + + return created; +} + +export async function setLinearTitleFilterEnabled( + integrationId: string, + filterId: string, + enabled: boolean +) { + const [updated] = await db + .update(linearTitleFilters) + .set({ enabled }) + .where( + and( + eq(linearTitleFilters.id, filterId), + eq(linearTitleFilters.integrationId, integrationId) + ) + ) + .returning(); + + return updated; +} + +export async function deleteLinearTitleFilter( + integrationId: string, + filterId: string +) { + const [deleted] = await db + .delete(linearTitleFilters) + .where( + and( + eq(linearTitleFilters.id, filterId), + eq(linearTitleFilters.integrationId, integrationId) + ) + ) + .returning(); + + return deleted; +} diff --git a/packages/ai/src/tools/github.ts b/packages/ai/src/tools/github.ts index 8866c7d60..e2d002666 100644 --- a/packages/ai/src/tools/github.ts +++ b/packages/ai/src/tools/github.ts @@ -10,6 +10,7 @@ import type { GitHubToolsAccessConfig, } from "@notra/ai/types/tools"; import { createOctokit } from "@notra/ai/utils/octokit"; +import { getCommitTitle, isTitleExcluded } from "@notra/ai/utils/title-filters"; import { type Tool, tool } from "ai"; // biome-ignore lint/performance/noNamespaceImport: Zod recommended way to import import * as z from "zod"; @@ -291,6 +292,13 @@ export function createGetPullRequestsTool( }, }) ); + + if (isTitleExcluded(pullRequest.data.title, resolved.titleFilters)) { + throw new Error( + `Pull request #${String(pull_number)} is excluded by this repository's title filters.` + ); + } + return { id: pullRequest.data.id, number: pullRequest.data.number, @@ -575,11 +583,18 @@ export function createGetCommitsByTimeframeTool( url: commit.html_url, })); - const commits = allowedCommitShas + const shaFilteredCommits = allowedCommitShas ? allCommits.filter((commit) => allowedCommitShas.has(commit.sha.trim().toLowerCase()) ) : allCommits; + const commits = shaFilteredCommits.filter( + (commit) => + !isTitleExcluded( + getCommitTitle(commit.message), + resolved.titleFilters + ) + ); const hasNextPage = nextPage !== undefined && diff --git a/packages/ai/src/tools/linear.ts b/packages/ai/src/tools/linear.ts index 8eb1024d1..596609bd8 100644 --- a/packages/ai/src/tools/linear.ts +++ b/packages/ai/src/tools/linear.ts @@ -6,6 +6,7 @@ import type { LinearToolsAccessConfig, } from "@notra/ai/types/tools"; import { createLinearClient } from "@notra/ai/utils/linear"; +import { isTitleExcluded } from "@notra/ai/utils/title-filters"; import { type Tool, tool } from "ai"; // biome-ignore lint/performance/noNamespaceImport: Zod recommended way to import import * as z from "zod"; @@ -101,8 +102,12 @@ export function createGetLinearIssuesTool( orderBy: "updatedAt" as never, }); + const visibleIssues = issues.nodes.filter( + (issue) => !isTitleExcluded(issue.title, resolved.titleFilters) + ); + const results = await Promise.all( - issues.nodes.map(async (issue) => { + visibleIssues.map(async (issue) => { const [state, assignee, labels] = await Promise.all([ issue.state, issue.assignee, diff --git a/packages/ai/src/types/tools.ts b/packages/ai/src/types/tools.ts index 061f5d8ef..d02e95bfc 100644 --- a/packages/ai/src/types/tools.ts +++ b/packages/ai/src/types/tools.ts @@ -15,6 +15,13 @@ export interface EditMarkdownContext { onUpdate: (markdown: string) => void; } +export type TitleFilterMatchType = "contains" | "regex"; + +export interface TitleFilterRule { + matchType: TitleFilterMatchType; + pattern: string; +} + export interface GitHubToolRepositoryContext { integrationId: string; organizationId: string; @@ -22,6 +29,7 @@ export interface GitHubToolRepositoryContext { repo: string; defaultBranch: string | null; token: string | undefined; + titleFilters: TitleFilterRule[]; } export interface GitHubToolsAccessConfig { @@ -46,6 +54,7 @@ export interface LinearToolContext { organizationId: string; accessToken: string; linearTeamId?: string | null; + titleFilters: TitleFilterRule[]; } export interface LinearToolsAccessConfig { diff --git a/packages/ai/src/utils/title-filters.ts b/packages/ai/src/utils/title-filters.ts new file mode 100644 index 000000000..4c7d73394 --- /dev/null +++ b/packages/ai/src/utils/title-filters.ts @@ -0,0 +1,30 @@ +import type { TitleFilterRule } from "../types/tools"; + +function matchesRule(title: string, rule: TitleFilterRule): boolean { + if (rule.matchType === "contains") { + return title.toLowerCase().includes(rule.pattern.toLowerCase()); + } + + try { + return new RegExp(rule.pattern, "i").test(title); + } catch { + return false; + } +} + +export function isTitleExcluded( + title: string | null | undefined, + rules: TitleFilterRule[] +): boolean { + if (!title || rules.length === 0) { + return false; + } + + const normalizedTitle = title.trim(); + return rules.some((rule) => matchesRule(normalizedTitle, rule)); +} + +export function getCommitTitle(message: string): string { + const newlineIndex = message.indexOf("\n"); + return newlineIndex === -1 ? message : message.slice(0, newlineIndex); +} diff --git a/packages/db/migrations/0048_normal_blue_marvel.sql b/packages/db/migrations/0048_normal_blue_marvel.sql new file mode 100644 index 000000000..746281aa0 --- /dev/null +++ b/packages/db/migrations/0048_normal_blue_marvel.sql @@ -0,0 +1,26 @@ +CREATE TABLE "github_title_filters" ( + "id" text PRIMARY KEY NOT NULL, + "repository_id" text NOT NULL, + "match_type" text NOT NULL, + "pattern" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "linear_title_filters" ( + "id" text PRIMARY KEY NOT NULL, + "integration_id" text NOT NULL, + "match_type" text NOT NULL, + "pattern" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "github_title_filters" ADD CONSTRAINT "github_title_filters_repository_id_github_integrations_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."github_integrations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "linear_title_filters" ADD CONSTRAINT "linear_title_filters_integration_id_linear_integrations_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."linear_integrations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "githubTitleFilters_repositoryId_idx" ON "github_title_filters" USING btree ("repository_id");--> statement-breakpoint +CREATE UNIQUE INDEX "githubTitleFilters_repository_matchType_pattern_uidx" ON "github_title_filters" USING btree ("repository_id","match_type","pattern");--> statement-breakpoint +CREATE INDEX "linearTitleFilters_integrationId_idx" ON "linear_title_filters" USING btree ("integration_id");--> statement-breakpoint +CREATE UNIQUE INDEX "linearTitleFilters_integration_matchType_pattern_uidx" ON "linear_title_filters" USING btree ("integration_id","match_type","pattern"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0048_snapshot.json b/packages/db/migrations/meta/0048_snapshot.json new file mode 100644 index 000000000..a1ca9f0c6 --- /dev/null +++ b/packages/db/migrations/meta/0048_snapshot.json @@ -0,0 +1,5988 @@ +{ + "id": "43116d8d-8bf9-40f5-89d6-4e40236fa44d", + "prevId": "93029cde-e8db-4cc5-bac2-9b72d065991c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "accounts_userId_idx": { + "name": "accounts_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_assets": { + "name": "brand_guideline_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "brand_guideline_asset_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aspect_ratio": { + "name": "aspect_ratio", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "brand_guideline_asset_variant", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineAssets_guidelineId_idx": { + "name": "brandGuidelineAssets_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineAssets_guideline_kind_idx": { + "name": "brandGuidelineAssets_guideline_kind_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineAssets_guideline_kind_variant_uidx": { + "name": "brandGuidelineAssets_guideline_kind_variant_uidx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_assets_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_assets_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_assets", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_colors": { + "name": "brand_guideline_colors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "brand_guideline_color_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "light_value": { + "name": "light_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dark_value": { + "name": "dark_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineColors_guidelineId_idx": { + "name": "brandGuidelineColors_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineColors_guideline_role_idx": { + "name": "brandGuidelineColors_guideline_role_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_colors_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_colors_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_colors", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_fonts": { + "name": "brand_guideline_fonts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "brand_guideline_font_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_height": { + "name": "line_height", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineFonts_guidelineId_idx": { + "name": "brandGuidelineFonts_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineFonts_guideline_role_idx": { + "name": "brandGuidelineFonts_guideline_role_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_fonts_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_fonts_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_fonts", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_screenshots": { + "name": "brand_guideline_screenshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "brand_guideline_screenshot_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_page": { + "name": "full_page", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineScreenshots_guidelineId_idx": { + "name": "brandGuidelineScreenshots_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineScreenshots_guideline_kind_uidx": { + "name": "brandGuidelineScreenshots_guideline_kind_uidx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_screenshots_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_screenshots_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_screenshots", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_tokens": { + "name": "brand_guideline_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "brand_guideline_token_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineTokens_guidelineId_idx": { + "name": "brandGuidelineTokens_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineTokens_guideline_type_idx": { + "name": "brandGuidelineTokens_guideline_type_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_tokens_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_tokens_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_tokens", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guidelines": { + "name": "brand_guidelines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "brand_settings_id": { + "name": "brand_settings_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "brand_guideline_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "context_dev_meta": { + "name": "context_dev_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_generation_error": { + "name": "last_generation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelines_brandSettingsId_uidx": { + "name": "brandGuidelines_brandSettingsId_uidx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelines_status_idx": { + "name": "brandGuidelines_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guidelines_brand_settings_id_brand_settings_id_fk": { + "name": "brand_guidelines_brand_settings_id_brand_settings_id_fk", + "tableFrom": "brand_guidelines", + "tableTo": "brand_settings", + "columnsFrom": [ + "brand_settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_references": { + "name": "brand_references", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "brand_settings_id": { + "name": "brand_settings_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "reference_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supermemory_document_id": { + "name": "supermemory_document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supermemory_memory_id": { + "name": "supermemory_memory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supermemory_synced_at": { + "name": "supermemory_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "supermemory_last_sync_error": { + "name": "supermemory_last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicable_to": { + "name": "applicable_to", + "type": "applicable_platform[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['all']::applicable_platform[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandReferences_brandSettingsId_idx": { + "name": "brandReferences_brandSettingsId_idx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_references_brand_settings_id_brand_settings_id_fk": { + "name": "brand_references_brand_settings_id_brand_settings_id_fk", + "tableFrom": "brand_references", + "tableTo": "brand_settings", + "columnsFrom": [ + "brand_settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_settings": { + "name": "brand_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Default'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_description": { + "name": "company_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tone_profile": { + "name": "tone_profile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_tone": { + "name": "custom_tone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'English'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandSettings_org_name_uidx": { + "name": "brandSettings_org_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSettings_org_default_uidx": { + "name": "brandSettings_org_default_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"brand_settings\".\"is_default\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSettings_organizationId_idx": { + "name": "brandSettings_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_settings_organization_id_organizations_id_fk": { + "name": "brand_settings_organization_id_organizations_id_fk", + "tableFrom": "brand_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_sitemap_pages": { + "name": "brand_sitemap_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sitemap_id": { + "name": "sitemap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "brand_sitemap_page_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redirect_target": { + "name": "redirect_target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "text_ratio": { + "name": "text_ratio", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "internal_links": { + "name": "internal_links", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_links": { + "name": "external_links", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "crawled_at": { + "name": "crawled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandSitemapPages_sitemapId_idx": { + "name": "brandSitemapPages_sitemapId_idx", + "columns": [ + { + "expression": "sitemap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSitemapPages_sitemap_category_idx": { + "name": "brandSitemapPages_sitemap_category_idx", + "columns": [ + { + "expression": "sitemap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSitemapPages_sitemap_url_uidx": { + "name": "brandSitemapPages_sitemap_url_uidx", + "columns": [ + { + "expression": "sitemap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_sitemap_pages_sitemap_id_brand_sitemaps_id_fk": { + "name": "brand_sitemap_pages_sitemap_id_brand_sitemaps_id_fk", + "tableFrom": "brand_sitemap_pages", + "tableTo": "brand_sitemaps", + "columnsFrom": [ + "sitemap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_sitemaps": { + "name": "brand_sitemaps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "brand_settings_id": { + "name": "brand_settings_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "brand_sitemap_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "total_pages": { + "name": "total_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_pages": { + "name": "indexed_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_pages": { + "name": "failed_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "context_dev_meta": { + "name": "context_dev_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_crawl_started_at": { + "name": "last_crawl_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_crawled_at": { + "name": "last_crawled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_crawl_error": { + "name": "last_crawl_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandSitemaps_brandSettingsId_idx": { + "name": "brandSitemaps_brandSettingsId_idx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSitemaps_brandSettings_url_uidx": { + "name": "brandSitemaps_brandSettings_url_uidx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_sitemaps_brand_settings_id_brand_settings_id_fk": { + "name": "brand_sitemaps_brand_settings_id_brand_settings_id_fk", + "tableFrom": "brand_sitemaps", + "tableTo": "brand_settings", + "columnsFrom": [ + "brand_settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_attachments": { + "name": "chat_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatAttachments_organizationId_createdAt_idx": { + "name": "chatAttachments_organizationId_createdAt_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatAttachments_userId_idx": { + "name": "chatAttachments_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_attachments_organization_id_organizations_id_fk": { + "name": "chat_attachments_organization_id_organizations_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_attachments_user_id_users_id_fk": { + "name": "chat_attachments_user_id_users_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_attachments_key_unique": { + "name": "chat_attachments_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "external_channel_source": { + "name": "external_channel_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_channel_id": { + "name": "external_channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatSessions_organizationId_idx": { + "name": "chatSessions_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatSessions_organizationId_deletedAt_idx": { + "name": "chatSessions_organizationId_deletedAt_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatSessions_org_externalChannel_uidx": { + "name": "chatSessions_org_externalChannel_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_channel_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_sessions\".\"external_channel_source\" IN ('discord', 'slack') AND \"chat_sessions\".\"external_channel_id\" IS NOT NULL AND \"chat_sessions\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_sessions_organization_id_organizations_id_fk": { + "name": "chat_sessions_organization_id_organizations_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connected_social_accounts": { + "name": "connected_social_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connectedSocialAccounts_organizationId_idx": { + "name": "connectedSocialAccounts_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connectedSocialAccounts_org_provider_account_uidx": { + "name": "connectedSocialAccounts_org_provider_account_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connected_social_accounts_organization_id_organizations_id_fk": { + "name": "connected_social_accounts_organization_id_organizations_id_fk", + "tableFrom": "connected_social_accounts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_trigger_lookback_windows": { + "name": "content_trigger_lookback_windows", + "schema": "", + "columns": { + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "window": { + "name": "window", + "type": "lookback_window", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_trigger_lookback_windows_trigger_id_content_triggers_id_fk": { + "name": "content_trigger_lookback_windows_trigger_id_content_triggers_id_fk", + "tableFrom": "content_trigger_lookback_windows", + "tableTo": "content_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_triggers": { + "name": "content_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled Schedule'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_config": { + "name": "source_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "output_type": { + "name": "output_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "output_config": { + "name": "output_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qstash_schedule_id": { + "name": "qstash_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_publish": { + "name": "auto_publish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contentTriggers_organizationId_idx": { + "name": "contentTriggers_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contentTriggers_organization_dedupe_uidx": { + "name": "contentTriggers_organization_dedupe_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_triggers_organization_id_organizations_id_fk": { + "name": "content_triggers_organization_id_organizations_id_fk", + "tableFrom": "content_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_app_installations": { + "name": "github_app_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_avatar_url": { + "name": "account_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_selection": { + "name": "repository_selection", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "githubAppInstallations_organizationId_idx": { + "name": "githubAppInstallations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubAppInstallations_createdByUserId_idx": { + "name": "githubAppInstallations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubAppInstallations_organization_installation_uidx": { + "name": "githubAppInstallations_organization_installation_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_app_installations_organization_id_organizations_id_fk": { + "name": "github_app_installations_organization_id_organizations_id_fk", + "tableFrom": "github_app_installations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_app_installations_created_by_user_id_users_id_fk": { + "name": "github_app_installations_created_by_user_id_users_id_fk", + "tableFrom": "github_app_installations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_integrations": { + "name": "github_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_token": { + "name": "encrypted_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_installation_id": { + "name": "github_app_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_private": { + "name": "github_repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_enabled": { + "name": "repository_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypted_webhook_secret": { + "name": "encrypted_webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "githubIntegrations_organizationId_idx": { + "name": "githubIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubIntegrations_createdByUserId_idx": { + "name": "githubIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubIntegrations_organization_owner_repo_uidx": { + "name": "githubIntegrations_organization_owner_repo_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_integrations_organization_id_organizations_id_fk": { + "name": "github_integrations_organization_id_organizations_id_fk", + "tableFrom": "github_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_integrations_created_by_user_id_users_id_fk": { + "name": "github_integrations_created_by_user_id_users_id_fk", + "tableFrom": "github_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_integrations_github_app_installation_id_github_app_installations_id_fk": { + "name": "github_integrations_github_app_installation_id_github_app_installations_id_fk", + "tableFrom": "github_integrations", + "tableTo": "github_app_installations", + "columnsFrom": [ + "github_app_installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_title_filters": { + "name": "github_title_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "githubTitleFilters_repositoryId_idx": { + "name": "githubTitleFilters_repositoryId_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubTitleFilters_repository_matchType_pattern_uidx": { + "name": "githubTitleFilters_repository_matchType_pattern_uidx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_title_filters_repository_id_github_integrations_id_fk": { + "name": "github_title_filters_repository_id_github_integrations_id_fk", + "tableFrom": "github_title_filters", + "tableTo": "github_integrations", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitations_organizationId_idx": { + "name": "invitations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_email_idx": { + "name": "invitations_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_organization_id_organizations_id_fk": { + "name": "invitations_organization_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_inviter_id_users_id_fk": { + "name": "invitations_inviter_id_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.linear_integrations": { + "name": "linear_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_access_token": { + "name": "encrypted_access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linear_organization_id": { + "name": "linear_organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linear_organization_name": { + "name": "linear_organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linear_team_id": { + "name": "linear_team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linear_team_name": { + "name": "linear_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_webhook_secret": { + "name": "encrypted_webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "linearIntegrations_organizationId_idx": { + "name": "linearIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearIntegrations_createdByUserId_idx": { + "name": "linearIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearIntegrations_org_linearOrg_team_uidx": { + "name": "linearIntegrations_org_linearOrg_team_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "linear_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "linear_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearIntegrations_org_linearOrg_no_team_uidx": { + "name": "linearIntegrations_org_linearOrg_no_team_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "linear_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"linear_integrations\".\"linear_team_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "linear_integrations_organization_id_organizations_id_fk": { + "name": "linear_integrations_organization_id_organizations_id_fk", + "tableFrom": "linear_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "linear_integrations_created_by_user_id_users_id_fk": { + "name": "linear_integrations_created_by_user_id_users_id_fk", + "tableFrom": "linear_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.linear_title_filters": { + "name": "linear_title_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "linearTitleFilters_integrationId_idx": { + "name": "linearTitleFilters_integrationId_idx", + "columns": [ + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearTitleFilters_integration_matchType_pattern_uidx": { + "name": "linearTitleFilters_integration_matchType_pattern_uidx", + "columns": [ + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "linear_title_filters_integration_id_linear_integrations_id_fk": { + "name": "linear_title_filters_integration_id_linear_integrations_id_fk", + "tableFrom": "linear_title_filters", + "tableTo": "linear_integrations", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_integrations": { + "name": "mcp_server_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_headers": { + "name": "encrypted_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_tool_sync_at": { + "name": "last_tool_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tool_sync_status": { + "name": "tool_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "tool_sync_error": { + "name": "tool_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "indexed_tool_count": { + "name": "indexed_tool_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcpServerIntegrations_organizationId_idx": { + "name": "mcpServerIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_createdByUserId_idx": { + "name": "mcpServerIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_org_id_uidx": { + "name": "mcpServerIntegrations_org_id_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_org_name_uidx": { + "name": "mcpServerIntegrations_org_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_integrations_organization_id_organizations_id_fk": { + "name": "mcp_server_integrations_organization_id_organizations_id_fk", + "tableFrom": "mcp_server_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_integrations_created_by_user_id_users_id_fk": { + "name": "mcp_server_integrations_created_by_user_id_users_id_fk", + "tableFrom": "mcp_server_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_session_tool_activations": { + "name": "mcp_session_tool_activations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_tool_index_id": { + "name": "mcp_tool_index_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_tool_name": { + "name": "runtime_tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_query": { + "name": "source_query", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mcpSessionToolActivations_session_tool_uidx": { + "name": "mcpSessionToolActivations_session_tool_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "surface", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_tool_index_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpSessionToolActivations_session_idx": { + "name": "mcpSessionToolActivations_session_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "surface", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpSessionToolActivations_expiresAt_idx": { + "name": "mcpSessionToolActivations_expiresAt_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_session_tool_activations_organization_id_organizations_id_fk": { + "name": "mcp_session_tool_activations_organization_id_organizations_id_fk", + "tableFrom": "mcp_session_tool_activations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_session_tool_activations_mcp_tool_index_id_mcp_tool_index_id_fk": { + "name": "mcp_session_tool_activations_mcp_tool_index_id_mcp_tool_index_id_fk", + "tableFrom": "mcp_session_tool_activations", + "tableTo": "mcp_tool_index", + "columnsFrom": [ + "mcp_tool_index_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpSessionToolActivations_org_tool_fk": { + "name": "mcpSessionToolActivations_org_tool_fk", + "tableFrom": "mcp_session_tool_activations", + "tableTo": "mcp_tool_index", + "columnsFrom": [ + "organization_id", + "mcp_tool_index_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tool_index": { + "name": "mcp_tool_index", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_integration_id": { + "name": "server_integration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_tool_name": { + "name": "server_tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_tool_name": { + "name": "runtime_tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "search_text": { + "name": "search_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_indexed_at": { + "name": "last_indexed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcpToolIndex_server_tool_uidx": { + "name": "mcpToolIndex_server_tool_uidx", + "columns": [ + { + "expression": "server_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "server_tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_org_id_uidx": { + "name": "mcpToolIndex_org_id_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_org_runtime_tool_uidx": { + "name": "mcpToolIndex_org_runtime_tool_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "runtime_tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_organizationId_status_idx": { + "name": "mcpToolIndex_organizationId_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_serverIntegrationId_status_idx": { + "name": "mcpToolIndex_serverIntegrationId_status_idx", + "columns": [ + { + "expression": "server_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_searchText_gin_idx": { + "name": "mcpToolIndex_searchText_gin_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"search_text\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "mcp_tool_index_organization_id_organizations_id_fk": { + "name": "mcp_tool_index_organization_id_organizations_id_fk", + "tableFrom": "mcp_tool_index", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_tool_index_server_integration_id_mcp_server_integrations_id_fk": { + "name": "mcp_tool_index_server_integration_id_mcp_server_integrations_id_fk", + "tableFrom": "mcp_tool_index", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "server_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpToolIndex_org_server_fk": { + "name": "mcpToolIndex_org_server_fk", + "tableFrom": "mcp_tool_index", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "organization_id", + "server_integration_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "members_organizationId_idx": { + "name": "members_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_userId_idx": { + "name": "members_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_tokens": { + "name": "oauth_access_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauthAccessTokens_clientId_idx": { + "name": "oauthAccessTokens_clientId_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAccessTokens_sessionId_idx": { + "name": "oauthAccessTokens_sessionId_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAccessTokens_userId_idx": { + "name": "oauthAccessTokens_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAccessTokens_refreshId_idx": { + "name": "oauthAccessTokens_refreshId_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_access_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_session_id_sessions_id_fk": { + "name": "oauth_access_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_tokens_user_id_users_id_fk": { + "name": "oauth_access_tokens_user_id_users_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk": { + "name": "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_refresh_tokens", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_tokens_token_unique": { + "name": "oauth_access_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauthClients_userId_idx": { + "name": "oauthClients_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_clients_user_id_users_id_fk": { + "name": "oauth_clients_user_id_users_id_fk", + "tableFrom": "oauth_clients", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_clients_client_id_unique": { + "name": "oauth_clients_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consents": { + "name": "oauth_consents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauthConsents_clientId_idx": { + "name": "oauthConsents_clientId_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthConsents_userId_idx": { + "name": "oauthConsents_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consents_client_id_oauth_clients_client_id_fk": { + "name": "oauth_consents_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consents_user_id_users_id_fk": { + "name": "oauth_consents_user_id_users_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_tokens": { + "name": "oauth_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauthRefreshTokens_clientId_idx": { + "name": "oauthRefreshTokens_clientId_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthRefreshTokens_sessionId_idx": { + "name": "oauthRefreshTokens_sessionId_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthRefreshTokens_userId_idx": { + "name": "oauthRefreshTokens_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_session_id_sessions_id_fk": { + "name": "oauth_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_user_id_users_id_fk": { + "name": "oauth_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_tokens_token_unique": { + "name": "oauth_refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_settings": { + "name": "organization_notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_content_creation": { + "name": "scheduled_content_creation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scheduled_content_failed": { + "name": "scheduled_content_failed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scheduled_content_skipped": { + "name": "scheduled_content_skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketing_emails": { + "name": "marketing_emails", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "orgNotificationSettings_organizationId_uidx": { + "name": "orgNotificationSettings_organizationId_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_notification_settings_organization_id_organizations_id_fk": { + "name": "organization_notification_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_notification_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heard_about_notra_source": { + "name": "heard_about_notra_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heard_about_notra_other": { + "name": "heard_about_notra_other", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarding_dismissed": { + "name": "onboarding_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "organizations_slug_uidx": { + "name": "organizations_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_collections": { + "name": "post_collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "post_collection_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_source": { + "name": "name_source", + "type": "post_collection_name_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'generated'" + }, + "content_types": { + "name": "content_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "source_metadata": { + "name": "source_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expected_post_count": { + "name": "expected_post_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completed_post_count": { + "name": "completed_post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "post_collections_org_created_at_idx": { + "name": "post_collections_org_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_collections_source_idx": { + "name": "post_collections_source_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_collections_chat_source_uidx": { + "name": "post_collections_chat_source_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"post_collections\".\"source\" = 'chat' AND \"post_collections\".\"source_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_collections_organization_id_organizations_id_fk": { + "name": "post_collections_organization_id_organizations_id_fk", + "tableFrom": "post_collections", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recommendations": { + "name": "recommendations", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "source_metadata": { + "name": "source_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "posts_org_slug_uidx": { + "name": "posts_org_slug_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"posts\".\"slug\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_org_createdAt_id_idx": { + "name": "posts_org_createdAt_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_collection_id_idx": { + "name": "posts_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_organization_id_organizations_id_fk": { + "name": "posts_organization_id_organizations_id_fk", + "tableFrom": "posts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_collection_id_post_collections_id_fk": { + "name": "posts_collection_id_post_collections_id_fk", + "tableFrom": "posts", + "tableTo": "post_collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repository_outputs": { + "name": "repository_outputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "output_type": { + "name": "output_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repositoryOutputs_repositoryId_idx": { + "name": "repositoryOutputs_repositoryId_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositoryOutputs_repository_outputType_uidx": { + "name": "repositoryOutputs_repository_outputType_uidx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "output_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repository_outputs_repository_id_github_integrations_id_fk": { + "name": "repository_outputs_repository_id_github_integrations_id_fk", + "tableFrom": "repository_outputs", + "tableTo": "github_integrations", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_userId_idx": { + "name": "sessions_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_organizationId_idx": { + "name": "skills_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skills_org_name_uidx": { + "name": "skills_org_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_organization_id_organizations_id_fk": { + "name": "skills_organization_id_organizations_id_fk", + "tableFrom": "skills", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hide_personal_data": { + "name": "hide_personal_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_agent_stats": { + "name": "show_agent_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verifications_identifier_idx": { + "name": "verifications_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.applicable_platform": { + "name": "applicable_platform", + "schema": "public", + "values": [ + "all", + "twitter", + "linkedin", + "blog" + ] + }, + "public.brand_guideline_asset_kind": { + "name": "brand_guideline_asset_kind", + "schema": "public", + "values": [ + "logo", + "wordmark" + ] + }, + "public.brand_guideline_asset_variant": { + "name": "brand_guideline_asset_variant", + "schema": "public", + "values": [ + "light", + "dark" + ] + }, + "public.brand_guideline_color_role": { + "name": "brand_guideline_color_role", + "schema": "public", + "values": [ + "primary", + "secondary", + "accent", + "background", + "foreground", + "neutral", + "custom" + ] + }, + "public.brand_guideline_font_role": { + "name": "brand_guideline_font_role", + "schema": "public", + "values": [ + "heading", + "body", + "button", + "unknown" + ] + }, + "public.brand_guideline_screenshot_kind": { + "name": "brand_guideline_screenshot_kind", + "schema": "public", + "values": [ + "desktop_hero", + "desktop_full_page", + "mobile_hero" + ] + }, + "public.brand_guideline_status": { + "name": "brand_guideline_status", + "schema": "public", + "values": [ + "queued", + "generating", + "ready", + "failed" + ] + }, + "public.brand_guideline_token_type": { + "name": "brand_guideline_token_type", + "schema": "public", + "values": [ + "spacing", + "radius", + "shadow", + "component", + "unknown" + ] + }, + "public.brand_sitemap_page_category": { + "name": "brand_sitemap_page_category", + "schema": "public", + "values": [ + "crawled", + "redirect", + "queued", + "failed" + ] + }, + "public.brand_sitemap_status": { + "name": "brand_sitemap_status", + "schema": "public", + "values": [ + "queued", + "crawling", + "ready", + "failed" + ] + }, + "public.lookback_window": { + "name": "lookback_window", + "schema": "public", + "values": [ + "current_day", + "yesterday", + "last_7_days", + "last_14_days", + "last_30_days" + ] + }, + "public.post_collection_name_source": { + "name": "post_collection_name_source", + "schema": "public", + "values": [ + "generated", + "user", + "backfill" + ] + }, + "public.post_collection_source": { + "name": "post_collection_source", + "schema": "public", + "values": [ + "manual", + "chat", + "schedule", + "automation", + "api", + "backfill" + ] + }, + "public.post_status": { + "name": "post_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.reference_type": { + "name": "reference_type", + "schema": "public", + "values": [ + "twitter_post", + "linkedin_post", + "blog_post", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index a3af4e4c5..7572b1391 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -337,6 +337,13 @@ "when": 1782775312760, "tag": "0047_majestic_sasquatch", "breakpoints": true + }, + { + "idx": 48, + "version": "7", + "when": 1783094227368, + "tag": "0048_normal_blue_marvel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 624e9210c..77f850bc4 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -482,6 +482,58 @@ export const linearIntegrations = pgTable( ] ); +export const githubTitleFilters = pgTable( + "github_title_filters", + { + id: text("id").primaryKey(), + repositoryId: text("repository_id") + .notNull() + .references(() => githubIntegrations.id, { onDelete: "cascade" }), + matchType: text("match_type").$type<"contains" | "regex">().notNull(), + pattern: text("pattern").notNull(), + enabled: boolean("enabled").default(true).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + index("githubTitleFilters_repositoryId_idx").on(table.repositoryId), + uniqueIndex("githubTitleFilters_repository_matchType_pattern_uidx").on( + table.repositoryId, + table.matchType, + table.pattern + ), + ] +); + +export const linearTitleFilters = pgTable( + "linear_title_filters", + { + id: text("id").primaryKey(), + integrationId: text("integration_id") + .notNull() + .references(() => linearIntegrations.id, { onDelete: "cascade" }), + matchType: text("match_type").$type<"contains" | "regex">().notNull(), + pattern: text("pattern").notNull(), + enabled: boolean("enabled").default(true).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + index("linearTitleFilters_integrationId_idx").on(table.integrationId), + uniqueIndex("linearTitleFilters_integration_matchType_pattern_uidx").on( + table.integrationId, + table.matchType, + table.pattern + ), + ] +); + export const mcpServerIntegrations = pgTable( "mcp_server_integrations", { @@ -1384,6 +1436,17 @@ export const githubIntegrationsRelations = relations( references: [users.id], }), outputs: many(repositoryOutputs), + titleFilters: many(githubTitleFilters), + }) +); + +export const githubTitleFiltersRelations = relations( + githubTitleFilters, + ({ one }) => ({ + repository: one(githubIntegrations, { + fields: [githubTitleFilters.repositoryId], + references: [githubIntegrations.id], + }), }) ); @@ -1403,7 +1466,7 @@ export const githubAppInstallationsRelations = relations( export const linearIntegrationsRelations = relations( linearIntegrations, - ({ one }) => ({ + ({ one, many }) => ({ organization: one(organizations, { fields: [linearIntegrations.organizationId], references: [organizations.id], @@ -1412,6 +1475,17 @@ export const linearIntegrationsRelations = relations( fields: [linearIntegrations.createdByUserId], references: [users.id], }), + titleFilters: many(linearTitleFilters), + }) +); + +export const linearTitleFiltersRelations = relations( + linearTitleFilters, + ({ one }) => ({ + integration: one(linearIntegrations, { + fields: [linearTitleFilters.integrationId], + references: [linearIntegrations.id], + }), }) ); From f4f328d119655ea94b1416e42a22f1bc266e3443 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Sat, 4 Jul 2026 17:04:12 +0200 Subject: [PATCH 2/4] fix(integrations): address capy, cubic, and react doctor review findings Enforce the 50-filter cap atomically with a pg advisory lock inside the create transaction instead of a racy check-then-insert. Store match_type as a pg enum and dedupe patterns case-insensitively via a lower(pattern) unique index (migration 0048 regenerated, previously unapplied). Rebuild headCommit when push-event commits are filtered so excluded titles no longer leak through webhook payloads. Base commit pagination on the SHA-filtered count so title filters cannot end paging early. Render a title filter section per repository instead of only the primary one. Split the title filters component into smaller pieces with inline query invalidation, and give react-doctor full git history for baseline comparison. Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko --- .github/workflows/react-doctor.yml | 2 + .../integrations/github/[id]/page-client.tsx | 12 +- .../integrations/title-filters-section.tsx | 348 ++++++++++-------- .../src/lib/orpc/routers/integrations.ts | 22 +- .../src/lib/webhooks/title-filters.ts | 26 +- apps/dashboard/src/schemas/title-filters.ts | 1 - apps/dashboard/src/types/title-filters.ts | 27 ++ packages/ai/src/constants/title-filters.ts | 1 + packages/ai/src/integrations/title-filters.ts | 84 +++-- packages/ai/src/tools/github.ts | 2 +- ...blue_marvel.sql => 0048_dusty_cerebro.sql} | 9 +- .../db/migrations/meta/0048_snapshot.json | 24 +- packages/db/migrations/meta/_journal.json | 4 +- packages/db/src/schema.ts | 13 +- 14 files changed, 362 insertions(+), 213 deletions(-) create mode 100644 packages/ai/src/constants/title-filters.ts rename packages/db/migrations/{0048_normal_blue_marvel.sql => 0048_dusty_cerebro.sql} (84%) diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml index 52d51f964..bcd5bc1e5 100644 --- a/.github/workflows/react-doctor.yml +++ b/.github/workflows/react-doctor.yml @@ -30,6 +30,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 - uses: millionco/react-doctor@v2 # Advisory by default: React Doctor reports findings on every PR — a diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx index 17928bdc7..c006a0aeb 100644 --- a/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx +++ b/apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx @@ -645,13 +645,19 @@ export default function PageClient({ integrationId }: PageClientProps) { slug={activeOrganization?.slug ?? ""} /> - {primaryRepository ? ( + {integration.repositories.map((repo) => ( 1 + ? `${repo.owner}/${repo.repo}` + : undefined + } /> - ) : null} + ))} {organizationId && integration.repositories.length > 0 ? (
diff --git a/apps/dashboard/src/components/integrations/title-filters-section.tsx b/apps/dashboard/src/components/integrations/title-filters-section.tsx index 65c6ca69e..11446ef71 100644 --- a/apps/dashboard/src/components/integrations/title-filters-section.tsx +++ b/apps/dashboard/src/components/integrations/title-filters-section.tsx @@ -41,7 +41,10 @@ import { } from "@/schemas/title-filters"; import type { TitleFilter, + TitleFilterAddFormProps, TitleFilterPreset, + TitleFilterPresetListProps, + TitleFilterRowProps, TitleFiltersSectionProps, } from "@/types/title-filters"; @@ -52,10 +55,155 @@ function getPresetForFilter(filter: TitleFilter, presets: TitleFilterPreset[]) { ); } +function TitleFilterAddForm({ + matchType, + pattern, + patternError, + isPending, + onMatchTypeChange, + onPatternChange, + onSubmit, +}: TitleFilterAddFormProps) { + return ( + <> +
+ + onPatternChange(event.target.value)} + placeholder={matchType === "regex" ? "^docs(\\(.*\\))?:" : "docs:"} + value={pattern} + /> + +
+ {patternError && ( +

{patternError}

+ )} + + ); +} + +function TitleFilterPresetList({ + presets, + disabled, + onAdd, +}: TitleFilterPresetListProps) { + if (presets.length === 0) { + return null; + } + + return ( +
+ Quick add: + {presets.map((preset) => ( + + onAdd(preset)} + size="sm" + type="button" + variant="outline" + /> + } + > + + {preset.label} + + +

{preset.description}

+

+ {preset.pattern} +

+
+
+ ))} +
+ ); +} + +function TitleFilterRow({ + filter, + presetLabel, + togglePending, + deletePending, + onToggle, + onDelete, +}: TitleFilterRowProps) { + return ( +
+
+ + {filter.matchType === "regex" ? "Regex" : "Text"} + + {filter.pattern} + {presetLabel && ( + + {presetLabel} + + )} +
+
+ + + } + /> + + {filter.enabled ? "Active" : "Paused"} + + + +
+
+ ); +} + export function TitleFiltersSection({ source, organizationId, targetId, + targetLabel, }: TitleFiltersSectionProps) { const queryClient = useQueryClient(); const isGithub = source === "github"; @@ -87,10 +235,6 @@ export function TitleFiltersSection({ const filters = data?.filters ?? []; - const invalidateList = () => { - queryClient.invalidateQueries({ queryKey: listQueryKey }); - }; - const createMutation = useMutation( { mutationFn: (body) => @@ -106,7 +250,7 @@ export function TitleFiltersSection({ ...body, }), onSuccess: () => { - invalidateList(); + queryClient.invalidateQueries({ queryKey: listQueryKey }); setPattern(""); setPatternError(null); }, @@ -135,7 +279,9 @@ export function TitleFiltersSection({ filterId, enabled, }), - onSuccess: invalidateList, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: listQueryKey }); + }, onError: (error) => { toast.error(error.message); }, @@ -158,7 +304,9 @@ export function TitleFiltersSection({ integrationId: targetId, filterId, }), - onSuccess: invalidateList, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: listQueryKey }); + }, onError: (error) => { toast.error(error.message); }, @@ -189,6 +337,13 @@ export function TitleFiltersSection({ } }; + const handlePatternChange = (value: string) => { + setPattern(value); + if (patternError) { + setPatternError(null); + } + }; + const availablePresets = presets.filter( (preset) => !filters.some( @@ -207,6 +362,9 @@ export function TitleFiltersSection({

Title filters

+ {targetLabel && ( + {targetLabel} + )}
-
- - { - setPattern(event.target.value); - if (patternError) { - setPatternError(null); - } - }} - placeholder={ - matchType === "regex" ? "^docs(\\(.*\\))?:" : "docs:" - } - value={pattern} - /> - -
- {patternError && ( -

{patternError}

- )} - {availablePresets.length > 0 && ( -
- - Quick add: - - {availablePresets.map((preset) => ( - - - createMutation.mutate({ - matchType: preset.matchType, - pattern: preset.pattern, - }) - } - size="sm" - type="button" - variant="outline" - /> - } - > - - {preset.label} - - -

{preset.description}

-

- {preset.pattern} -

-
-
- ))} -
- )} + pattern={pattern} + patternError={patternError} + /> + + createMutation.mutate({ + matchType: preset.matchType, + pattern: preset.pattern, + }) + } + presets={availablePresets} + />
@@ -337,66 +428,19 @@ export function TitleFiltersSection({ No title filters yet. Everything is included.

)} - {filters.map((filter) => { - const preset = getPresetForFilter(filter, presets); - - return ( -
-
- - {filter.matchType === "regex" ? "Regex" : "Text"} - - - {filter.pattern} - - {preset && ( - - {preset.label} - - )} -
-
- - - toggleMutation.mutate({ - filterId: filter.id, - enabled: checked, - }) - } - size="sm" - /> - } - /> - - {filter.enabled ? "Active" : "Paused"} - - - -
-
- ); - })} + {filters.map((filter) => ( + deleteMutation.mutate({ filterId: filter.id })} + onToggle={(enabled) => + toggleMutation.mutate({ filterId: filter.id, enabled }) + } + presetLabel={getPresetForFilter(filter, presets)?.label} + togglePending={toggleMutation.isPending} + /> + ))}
)} diff --git a/apps/dashboard/src/lib/orpc/routers/integrations.ts b/apps/dashboard/src/lib/orpc/routers/integrations.ts index 85d36ff1c..62e0e75ff 100644 --- a/apps/dashboard/src/lib/orpc/routers/integrations.ts +++ b/apps/dashboard/src/lib/orpc/routers/integrations.ts @@ -45,6 +45,7 @@ import { getLinearTitleFilters, setGithubTitleFilterEnabled, setLinearTitleFilterEnabled, + TitleFilterLimitError, } from "@notra/ai/integrations/title-filters"; import { deleteQstashSchedule } from "@notra/ai/qstash/triggers"; import { db } from "@notra/db/drizzle"; @@ -77,7 +78,6 @@ import { import { updateLinearIntegrationBodySchema } from "@/schemas/linear"; import { createTitleFilterBodySchema, - MAX_TITLE_FILTERS, titleFilterIdSchema, updateTitleFilterBodySchema, } from "@/schemas/title-filters"; @@ -769,13 +769,6 @@ export const integrationsRouter = { input.repositoryId ); - const existing = await getGithubTitleFilters(input.repositoryId); - if (existing.length >= MAX_TITLE_FILTERS) { - throw badRequest( - `You can add up to ${MAX_TITLE_FILTERS} title filters per repository` - ); - } - try { const created = await createGithubTitleFilter(input.repositoryId, { matchType: input.matchType, @@ -788,6 +781,9 @@ export const integrationsRouter = { return serializeTitleFilter(created); } catch (error) { + if (error instanceof TitleFilterLimitError) { + throw badRequest(error.message); + } if (isUniqueConstraintError(error)) { throw conflict("This title filter already exists"); } @@ -1062,13 +1058,6 @@ export const integrationsRouter = { input.integrationId ); - const existing = await getLinearTitleFilters(input.integrationId); - if (existing.length >= MAX_TITLE_FILTERS) { - throw badRequest( - `You can add up to ${MAX_TITLE_FILTERS} title filters per integration` - ); - } - try { const created = await createLinearTitleFilter(input.integrationId, { matchType: input.matchType, @@ -1081,6 +1070,9 @@ export const integrationsRouter = { return serializeTitleFilter(created); } catch (error) { + if (error instanceof TitleFilterLimitError) { + throw badRequest(error.message); + } if (isUniqueConstraintError(error)) { throw conflict("This title filter already exists"); } diff --git a/apps/dashboard/src/lib/webhooks/title-filters.ts b/apps/dashboard/src/lib/webhooks/title-filters.ts index 763d343bf..9710fec3a 100644 --- a/apps/dashboard/src/lib/webhooks/title-filters.ts +++ b/apps/dashboard/src/lib/webhooks/title-filters.ts @@ -31,6 +31,22 @@ function filterReleaseEvent( return isTitleExcluded(title, rules) ? null : processedEvent; } +function toHeadCommit(commit: unknown) { + if (typeof commit !== "object" || commit === null) { + return null; + } + + if (!("id" in commit) || typeof commit.id !== "string") { + return null; + } + + if (!("message" in commit) || typeof commit.message !== "string") { + return null; + } + + return { id: commit.id, message: commit.message }; +} + function filterPushEvent( processedEvent: GithubProcessedEvent, rules: TitleFilterRule[] @@ -48,7 +64,12 @@ function filterPushEvent( return null; } - if (remainingCommits.length === commits.length) { + const headCommitExcluded = isCommitExcluded( + processedEvent.data.headCommit, + rules + ); + + if (remainingCommits.length === commits.length && !headCommitExcluded) { return processedEvent; } @@ -57,6 +78,9 @@ function filterPushEvent( data: { ...processedEvent.data, commits: remainingCommits, + headCommit: headCommitExcluded + ? toHeadCommit(remainingCommits.at(-1)) + : processedEvent.data.headCommit, }, }; } diff --git a/apps/dashboard/src/schemas/title-filters.ts b/apps/dashboard/src/schemas/title-filters.ts index b9a9dda51..3b19aa7b7 100644 --- a/apps/dashboard/src/schemas/title-filters.ts +++ b/apps/dashboard/src/schemas/title-filters.ts @@ -5,7 +5,6 @@ export const TITLE_FILTER_MATCH_TYPES = ["contains", "regex"] as const; export type TitleFilterMatchType = (typeof TITLE_FILTER_MATCH_TYPES)[number]; export const MAX_TITLE_FILTER_PATTERN_LENGTH = 256; -export const MAX_TITLE_FILTERS = 50; export function isValidTitleFilterRegex(pattern: string) { try { diff --git a/apps/dashboard/src/types/title-filters.ts b/apps/dashboard/src/types/title-filters.ts index 59cc4dc85..b8a3b1c2d 100644 --- a/apps/dashboard/src/types/title-filters.ts +++ b/apps/dashboard/src/types/title-filters.ts @@ -1,3 +1,4 @@ +import type { FormEvent } from "react"; import type { TitleFilterMatchType } from "@/schemas/title-filters"; export interface TitleFilter { @@ -26,4 +27,30 @@ export interface TitleFiltersSectionProps { source: TitleFilterSource; organizationId: string; targetId: string; + targetLabel?: string; +} + +export interface TitleFilterAddFormProps { + matchType: TitleFilterMatchType; + pattern: string; + patternError: string | null; + isPending: boolean; + onMatchTypeChange: (value: TitleFilterMatchType | null) => void; + onPatternChange: (value: string) => void; + onSubmit: (event: FormEvent) => void; +} + +export interface TitleFilterPresetListProps { + presets: TitleFilterPreset[]; + disabled: boolean; + onAdd: (preset: TitleFilterPreset) => void; +} + +export interface TitleFilterRowProps { + filter: TitleFilter; + presetLabel?: string; + togglePending: boolean; + deletePending: boolean; + onToggle: (enabled: boolean) => void; + onDelete: () => void; } diff --git a/packages/ai/src/constants/title-filters.ts b/packages/ai/src/constants/title-filters.ts new file mode 100644 index 000000000..8f3572777 --- /dev/null +++ b/packages/ai/src/constants/title-filters.ts @@ -0,0 +1 @@ +export const MAX_TITLE_FILTERS = 50; diff --git a/packages/ai/src/integrations/title-filters.ts b/packages/ai/src/integrations/title-filters.ts index 561565e6b..536e6642c 100644 --- a/packages/ai/src/integrations/title-filters.ts +++ b/packages/ai/src/integrations/title-filters.ts @@ -1,7 +1,8 @@ import { db } from "@notra/db/drizzle"; import { githubTitleFilters, linearTitleFilters } from "@notra/db/schema"; -import { and, asc, eq } from "drizzle-orm"; +import { and, asc, count, eq, sql } from "drizzle-orm"; import { customAlphabet } from "nanoid"; +import { MAX_TITLE_FILTERS } from "../constants/title-filters"; import type { TitleFilterMatchType, TitleFilterRule } from "../types/tools"; const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 16); @@ -11,6 +12,13 @@ interface CreateTitleFilterParams { pattern: string; } +export class TitleFilterLimitError extends Error { + constructor() { + super(`You can add up to ${MAX_TITLE_FILTERS} title filters`); + this.name = "TitleFilterLimitError"; + } +} + export async function getGithubTitleFilters(repositoryId: string) { return db.query.githubTitleFilters.findMany({ where: eq(githubTitleFilters.repositoryId, repositoryId), @@ -41,18 +49,33 @@ export async function createGithubTitleFilter( repositoryId: string, params: CreateTitleFilterParams ) { - const [created] = await db - .insert(githubTitleFilters) - .values({ - id: nanoid(), - repositoryId, - matchType: params.matchType, - pattern: params.pattern, - enabled: true, - }) - .returning(); + return db.transaction(async (tx) => { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtext(${`github_title_filters:${repositoryId}`}))` + ); - return created; + const [existing] = await tx + .select({ value: count() }) + .from(githubTitleFilters) + .where(eq(githubTitleFilters.repositoryId, repositoryId)); + + if ((existing?.value ?? 0) >= MAX_TITLE_FILTERS) { + throw new TitleFilterLimitError(); + } + + const [created] = await tx + .insert(githubTitleFilters) + .values({ + id: nanoid(), + repositoryId, + matchType: params.matchType, + pattern: params.pattern, + enabled: true, + }) + .returning(); + + return created; + }); } export async function setGithubTitleFilterEnabled( @@ -121,18 +144,33 @@ export async function createLinearTitleFilter( integrationId: string, params: CreateTitleFilterParams ) { - const [created] = await db - .insert(linearTitleFilters) - .values({ - id: nanoid(), - integrationId, - matchType: params.matchType, - pattern: params.pattern, - enabled: true, - }) - .returning(); + return db.transaction(async (tx) => { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtext(${`linear_title_filters:${integrationId}`}))` + ); - return created; + const [existing] = await tx + .select({ value: count() }) + .from(linearTitleFilters) + .where(eq(linearTitleFilters.integrationId, integrationId)); + + if ((existing?.value ?? 0) >= MAX_TITLE_FILTERS) { + throw new TitleFilterLimitError(); + } + + const [created] = await tx + .insert(linearTitleFilters) + .values({ + id: nanoid(), + integrationId, + matchType: params.matchType, + pattern: params.pattern, + enabled: true, + }) + .returning(); + + return created; + }); } export async function setLinearTitleFilterEnabled( diff --git a/packages/ai/src/tools/github.ts b/packages/ai/src/tools/github.ts index e2d002666..7c1ed71e6 100644 --- a/packages/ai/src/tools/github.ts +++ b/packages/ai/src/tools/github.ts @@ -598,7 +598,7 @@ export function createGetCommitsByTimeframeTool( const hasNextPage = nextPage !== undefined && - !(allowedCommitShas && commits.length === 0); + !(allowedCommitShas && shaFilteredCommits.length === 0); return { commits, diff --git a/packages/db/migrations/0048_normal_blue_marvel.sql b/packages/db/migrations/0048_dusty_cerebro.sql similarity index 84% rename from packages/db/migrations/0048_normal_blue_marvel.sql rename to packages/db/migrations/0048_dusty_cerebro.sql index 746281aa0..515a8da7b 100644 --- a/packages/db/migrations/0048_normal_blue_marvel.sql +++ b/packages/db/migrations/0048_dusty_cerebro.sql @@ -1,7 +1,8 @@ +CREATE TYPE "public"."title_filter_match_type" AS ENUM('contains', 'regex');--> statement-breakpoint CREATE TABLE "github_title_filters" ( "id" text PRIMARY KEY NOT NULL, "repository_id" text NOT NULL, - "match_type" text NOT NULL, + "match_type" "title_filter_match_type" NOT NULL, "pattern" text NOT NULL, "enabled" boolean DEFAULT true NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, @@ -11,7 +12,7 @@ CREATE TABLE "github_title_filters" ( CREATE TABLE "linear_title_filters" ( "id" text PRIMARY KEY NOT NULL, "integration_id" text NOT NULL, - "match_type" text NOT NULL, + "match_type" "title_filter_match_type" NOT NULL, "pattern" text NOT NULL, "enabled" boolean DEFAULT true NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, @@ -21,6 +22,6 @@ CREATE TABLE "linear_title_filters" ( ALTER TABLE "github_title_filters" ADD CONSTRAINT "github_title_filters_repository_id_github_integrations_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."github_integrations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "linear_title_filters" ADD CONSTRAINT "linear_title_filters_integration_id_linear_integrations_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."linear_integrations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint CREATE INDEX "githubTitleFilters_repositoryId_idx" ON "github_title_filters" USING btree ("repository_id");--> statement-breakpoint -CREATE UNIQUE INDEX "githubTitleFilters_repository_matchType_pattern_uidx" ON "github_title_filters" USING btree ("repository_id","match_type","pattern");--> statement-breakpoint +CREATE UNIQUE INDEX "githubTitleFilters_repository_matchType_pattern_uidx" ON "github_title_filters" USING btree ("repository_id","match_type",lower("pattern"));--> statement-breakpoint CREATE INDEX "linearTitleFilters_integrationId_idx" ON "linear_title_filters" USING btree ("integration_id");--> statement-breakpoint -CREATE UNIQUE INDEX "linearTitleFilters_integration_matchType_pattern_uidx" ON "linear_title_filters" USING btree ("integration_id","match_type","pattern"); \ No newline at end of file +CREATE UNIQUE INDEX "linearTitleFilters_integration_matchType_pattern_uidx" ON "linear_title_filters" USING btree ("integration_id","match_type",lower("pattern")); \ No newline at end of file diff --git a/packages/db/migrations/meta/0048_snapshot.json b/packages/db/migrations/meta/0048_snapshot.json index a1ca9f0c6..6c7061993 100644 --- a/packages/db/migrations/meta/0048_snapshot.json +++ b/packages/db/migrations/meta/0048_snapshot.json @@ -1,5 +1,5 @@ { - "id": "43116d8d-8bf9-40f5-89d6-4e40236fa44d", + "id": "a3ba73c0-64ce-4799-a033-942012dfbce3", "prevId": "93029cde-e8db-4cc5-bac2-9b72d065991c", "version": "7", "dialect": "postgresql", @@ -2656,7 +2656,8 @@ }, "match_type": { "name": "match_type", - "type": "text", + "type": "title_filter_match_type", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -2720,9 +2721,9 @@ "nulls": "last" }, { - "expression": "pattern", - "isExpression": false, + "expression": "lower(\"pattern\")", "asc": true, + "isExpression": true, "nulls": "last" } ], @@ -3137,7 +3138,8 @@ }, "match_type": { "name": "match_type", - "type": "text", + "type": "title_filter_match_type", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -3201,9 +3203,9 @@ "nulls": "last" }, { - "expression": "pattern", - "isExpression": false, + "expression": "lower(\"pattern\")", "asc": true, + "isExpression": true, "nulls": "last" } ], @@ -5973,6 +5975,14 @@ "blog_post", "custom" ] + }, + "public.title_filter_match_type": { + "name": "title_filter_match_type", + "schema": "public", + "values": [ + "contains", + "regex" + ] } }, "schemas": {}, diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 7572b1391..2c0ea32d7 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -341,8 +341,8 @@ { "idx": 48, "version": "7", - "when": 1783094227368, - "tag": "0048_normal_blue_marvel", + "when": 1783177064975, + "tag": "0048_dusty_cerebro", "breakpoints": true } ] diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 77f850bc4..ffd90410f 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -23,6 +23,11 @@ export const lookbackWindowEnum = pgEnum("lookback_window", [ export const postStatusEnum = pgEnum("post_status", ["draft", "published"]); +export const titleFilterMatchTypeEnum = pgEnum("title_filter_match_type", [ + "contains", + "regex", +]); + export const postCollectionSourceEnum = pgEnum("post_collection_source", [ "manual", "chat", @@ -489,7 +494,7 @@ export const githubTitleFilters = pgTable( repositoryId: text("repository_id") .notNull() .references(() => githubIntegrations.id, { onDelete: "cascade" }), - matchType: text("match_type").$type<"contains" | "regex">().notNull(), + matchType: titleFilterMatchTypeEnum("match_type").notNull(), pattern: text("pattern").notNull(), enabled: boolean("enabled").default(true).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), @@ -503,7 +508,7 @@ export const githubTitleFilters = pgTable( uniqueIndex("githubTitleFilters_repository_matchType_pattern_uidx").on( table.repositoryId, table.matchType, - table.pattern + sql`lower(${table.pattern})` ), ] ); @@ -515,7 +520,7 @@ export const linearTitleFilters = pgTable( integrationId: text("integration_id") .notNull() .references(() => linearIntegrations.id, { onDelete: "cascade" }), - matchType: text("match_type").$type<"contains" | "regex">().notNull(), + matchType: titleFilterMatchTypeEnum("match_type").notNull(), pattern: text("pattern").notNull(), enabled: boolean("enabled").default(true).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), @@ -529,7 +534,7 @@ export const linearTitleFilters = pgTable( uniqueIndex("linearTitleFilters_integration_matchType_pattern_uidx").on( table.integrationId, table.matchType, - table.pattern + sql`lower(${table.pattern})` ), ] ); From e9d544d145c86b2e914e8739488cf9956313332d Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Sat, 4 Jul 2026 17:18:25 +0200 Subject: [PATCH 3/4] fix(db): collapse title filter schema changes into one clean migration The regenerated-in-place 0048 diverged from the copy already applied to the staging database, breaking the drizzle migration ledger on deploy. Regenerate a single 0048 with the final shape (match_type enum and case-insensitive unique pattern indexes); staging will be reset so it applies fresh. Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko --- .../{0048_dusty_cerebro.sql => 0048_hesitant_kate_bishop.sql} | 0 packages/db/migrations/meta/0048_snapshot.json | 2 +- packages/db/migrations/meta/_journal.json | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/db/migrations/{0048_dusty_cerebro.sql => 0048_hesitant_kate_bishop.sql} (100%) diff --git a/packages/db/migrations/0048_dusty_cerebro.sql b/packages/db/migrations/0048_hesitant_kate_bishop.sql similarity index 100% rename from packages/db/migrations/0048_dusty_cerebro.sql rename to packages/db/migrations/0048_hesitant_kate_bishop.sql diff --git a/packages/db/migrations/meta/0048_snapshot.json b/packages/db/migrations/meta/0048_snapshot.json index 6c7061993..ec3901944 100644 --- a/packages/db/migrations/meta/0048_snapshot.json +++ b/packages/db/migrations/meta/0048_snapshot.json @@ -1,5 +1,5 @@ { - "id": "a3ba73c0-64ce-4799-a033-942012dfbce3", + "id": "2b68acc1-dd54-47f5-8b0f-1b25661f1574", "prevId": "93029cde-e8db-4cc5-bac2-9b72d065991c", "version": "7", "dialect": "postgresql", diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 2c0ea32d7..f619132cd 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -341,8 +341,8 @@ { "idx": 48, "version": "7", - "when": 1783177064975, - "tag": "0048_dusty_cerebro", + "when": 1783178289019, + "tag": "0048_hesitant_kate_bishop", "breakpoints": true } ] From 81fac61e0b7b175f3139bbd23914fbba75d56b91 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Sat, 4 Jul 2026 18:12:32 +0200 Subject: [PATCH 4/4] feat(integrations): competitor-style editable filter rows and layout polish Filter patterns are now inline-editable inputs that save on blur or Enter, backed by an update endpoint that accepts pattern and match type changes with duplicate detection. Restructure the section so rules list first and the composer sits in a muted band below, drop the per-row badge in favor of an inline label, add lockfile and typo-fix presets under a Suggestions group, rename the heading to Filters, tighten the tooltip copy, and capitalize the match type select value. Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko --- .../integrations/title-filters-section.tsx | 260 +++++++++++------- apps/dashboard/src/constants/title-filters.ts | 14 + .../src/lib/orpc/routers/integrations.ts | 62 +++-- apps/dashboard/src/schemas/title-filters.ts | 38 ++- apps/dashboard/src/types/title-filters.ts | 3 +- packages/ai/src/integrations/title-filters.ts | 26 +- 6 files changed, 275 insertions(+), 128 deletions(-) diff --git a/apps/dashboard/src/components/integrations/title-filters-section.tsx b/apps/dashboard/src/components/integrations/title-filters-section.tsx index 11446ef71..fe24a210a 100644 --- a/apps/dashboard/src/components/integrations/title-filters-section.tsx +++ b/apps/dashboard/src/components/integrations/title-filters-section.tsx @@ -6,7 +6,6 @@ import { HelpCircleIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { Badge } from "@notra/ui/components/ui/badge"; import { Input } from "@notra/ui/components/ui/input"; import { Select, @@ -24,7 +23,7 @@ import { } from "@notra/ui/components/ui/tooltip"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2Icon } from "lucide-react"; -import { type FormEvent, useState } from "react"; +import { type FormEvent, type KeyboardEvent, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { @@ -65,11 +64,11 @@ function TitleFilterAddForm({ onSubmit, }: TitleFilterAddFormProps) { return ( - <> +
{ + setValue(event.target.value); + setInvalid(false); + }} + onKeyDown={handleKeyDown} + value={value} + /> + {value === filter.pattern && ( + + {inlineLabel} + + )} +
@@ -185,7 +233,7 @@ function TitleFilterRow({
+ {invalid && ( +

+ Enter a valid regular expression +

+ )}
); } @@ -260,30 +313,34 @@ export function TitleFiltersSection({ } ); - const toggleMutation = useMutation< + const updateMutation = useMutation< TitleFilter, Error, - { filterId: string; enabled: boolean } + { + filterId: string; + enabled?: boolean; + matchType?: TitleFilterMatchType; + pattern?: string; + } >({ - mutationFn: ({ filterId, enabled }) => + mutationFn: (body) => isGithub ? dashboardOrpc.integrations.repositories.titleFilters.update.call({ organizationId, repositoryId: targetId, - filterId, - enabled, + ...body, }) : dashboardOrpc.integrations.linear.titleFilters.update.call({ organizationId, integrationId: targetId, - filterId, - enabled, + ...body, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: listQueryKey }); }, onError: (error) => { toast.error(error.message); + queryClient.invalidateQueries({ queryKey: listQueryKey }); }, }); @@ -353,15 +410,15 @@ export function TitleFiltersSection({ ) ); - const itemsLabel = isGithub - ? "pull requests, commits, and releases" - : "issues"; + const description = isGithub + ? "Pull requests, commits, and releases matching these patterns are skipped when generating content." + : "Issues matching these patterns are skipped when generating content."; return (
-

Title filters

+

Filters

{targetLabel && ( {targetLabel} )} @@ -378,16 +435,12 @@ export function TitleFiltersSection({ - Items whose title matches an active filter are skipped when Notra - reads your {isGithub ? "GitHub activity" : "Linear issues"} for - content generation and automations. Text filters match anywhere in - the title; regex filters are case-insensitive. + Titles matching an active filter are skipped. Matching is + case-insensitive. Click a pattern to edit it.
-

- Exclude {itemsLabel} from generated content by title. -

+

{description}

{isLoading && } @@ -400,16 +453,39 @@ export function TitleFiltersSection({ {!(isLoading || isError) && (
-
- + {filters.length === 0 ? ( +

+ No title filters yet. Everything is included. Start with a + suggestion below or add your own. +

+ ) : ( +
+ {filters.map((filter) => ( + + deleteMutation.mutate({ filterId: filter.id }) + } + onPatternSave={(nextPattern) => + updateMutation.mutate({ + filterId: filter.id, + matchType: filter.matchType, + pattern: nextPattern, + }) + } + onToggle={(enabled) => + updateMutation.mutate({ filterId: filter.id, enabled }) + } + presetLabel={getPresetForFilter(filter, presets)?.label} + updatePending={updateMutation.isPending} + /> + ))} +
+ )} + +
@@ -420,27 +496,15 @@ export function TitleFiltersSection({ } presets={availablePresets} /> -
- -
- {filters.length === 0 && ( -

- No title filters yet. Everything is included. -

- )} - {filters.map((filter) => ( - deleteMutation.mutate({ filterId: filter.id })} - onToggle={(enabled) => - toggleMutation.mutate({ filterId: filter.id, enabled }) - } - presetLabel={getPresetForFilter(filter, presets)?.label} - togglePending={toggleMutation.isPending} - /> - ))} +
)} diff --git a/apps/dashboard/src/constants/title-filters.ts b/apps/dashboard/src/constants/title-filters.ts index a1bb27b58..bf1c7373e 100644 --- a/apps/dashboard/src/constants/title-filters.ts +++ b/apps/dashboard/src/constants/title-filters.ts @@ -46,6 +46,20 @@ const TITLE_FILTER_PRESETS: TitleFilterPreset[] = [ matchType: "regex", pattern: "^(chore|fix|build)\\(deps.*\\)!?:|^bump ", }, + { + id: "lockfiles", + label: "Lockfile updates", + description: 'Excludes lockfile bumps like "update bun.lock"', + matchType: "regex", + pattern: "update.*lock", + }, + { + id: "typos", + label: "Typo fixes", + description: 'Excludes "fix typo" style titles', + matchType: "regex", + pattern: "^fix.*typo", + }, { id: "reverts", label: "Reverts", diff --git a/apps/dashboard/src/lib/orpc/routers/integrations.ts b/apps/dashboard/src/lib/orpc/routers/integrations.ts index 62e0e75ff..03b489eb6 100644 --- a/apps/dashboard/src/lib/orpc/routers/integrations.ts +++ b/apps/dashboard/src/lib/orpc/routers/integrations.ts @@ -43,9 +43,9 @@ import { deleteLinearTitleFilter, getGithubTitleFilters, getLinearTitleFilters, - setGithubTitleFilterEnabled, - setLinearTitleFilterEnabled, TitleFilterLimitError, + updateGithubTitleFilter, + updateLinearTitleFilter, } from "@notra/ai/integrations/title-filters"; import { deleteQstashSchedule } from "@notra/ai/qstash/triggers"; import { db } from "@notra/db/drizzle"; @@ -804,17 +804,28 @@ export const integrationsRouter = { input.repositoryId ); - const updated = await setGithubTitleFilterEnabled( - input.repositoryId, - input.filterId, - input.enabled - ); + try { + const updated = await updateGithubTitleFilter( + input.repositoryId, + input.filterId, + { + enabled: input.enabled, + matchType: input.matchType, + pattern: input.pattern, + } + ); - if (!updated) { - throw notFound("Title filter not found"); - } + if (!updated) { + throw notFound("Title filter not found"); + } - return serializeTitleFilter(updated); + return serializeTitleFilter(updated); + } catch (error) { + if (isUniqueConstraintError(error)) { + throw conflict("This title filter already exists"); + } + throw error; + } }), delete: baseProcedure .input(repositoryInputSchema.and(titleFilterIdSchema)) @@ -1093,17 +1104,28 @@ export const integrationsRouter = { input.integrationId ); - const updated = await setLinearTitleFilterEnabled( - input.integrationId, - input.filterId, - input.enabled - ); + try { + const updated = await updateLinearTitleFilter( + input.integrationId, + input.filterId, + { + enabled: input.enabled, + matchType: input.matchType, + pattern: input.pattern, + } + ); - if (!updated) { - throw notFound("Title filter not found"); - } + if (!updated) { + throw notFound("Title filter not found"); + } - return serializeTitleFilter(updated); + return serializeTitleFilter(updated); + } catch (error) { + if (isUniqueConstraintError(error)) { + throw conflict("This title filter already exists"); + } + throw error; + } }), delete: baseProcedure .input(integrationInputSchema.and(titleFilterIdSchema)) diff --git a/apps/dashboard/src/schemas/title-filters.ts b/apps/dashboard/src/schemas/title-filters.ts index 3b19aa7b7..02300478a 100644 --- a/apps/dashboard/src/schemas/title-filters.ts +++ b/apps/dashboard/src/schemas/title-filters.ts @@ -40,7 +40,39 @@ export const titleFilterIdSchema = z.object({ filterId: z.string().min(1, "Filter ID is required"), }); -export const updateTitleFilterBodySchema = titleFilterIdSchema.extend({ - enabled: z.boolean(), -}); +export const updateTitleFilterBodySchema = titleFilterIdSchema + .extend({ + enabled: z.boolean().optional(), + matchType: z.enum(TITLE_FILTER_MATCH_TYPES).optional(), + pattern: titleFilterPatternSchema.optional(), + }) + .superRefine((value, ctx) => { + if (value.enabled === undefined && value.pattern === undefined) { + ctx.addIssue({ + code: "custom", + message: "At least one field must be provided", + path: ["enabled"], + }); + } + + if (value.pattern !== undefined && value.matchType === undefined) { + ctx.addIssue({ + code: "custom", + message: "Match type is required when updating the pattern", + path: ["matchType"], + }); + } + + if ( + value.matchType === "regex" && + value.pattern !== undefined && + !isValidTitleFilterRegex(value.pattern) + ) { + ctx.addIssue({ + code: "custom", + message: "Enter a valid regular expression", + path: ["pattern"], + }); + } + }); export type UpdateTitleFilterBody = z.infer; diff --git a/apps/dashboard/src/types/title-filters.ts b/apps/dashboard/src/types/title-filters.ts index b8a3b1c2d..bcf4c514d 100644 --- a/apps/dashboard/src/types/title-filters.ts +++ b/apps/dashboard/src/types/title-filters.ts @@ -49,8 +49,9 @@ export interface TitleFilterPresetListProps { export interface TitleFilterRowProps { filter: TitleFilter; presetLabel?: string; - togglePending: boolean; + updatePending: boolean; deletePending: boolean; onToggle: (enabled: boolean) => void; + onPatternSave: (pattern: string) => void; onDelete: () => void; } diff --git a/packages/ai/src/integrations/title-filters.ts b/packages/ai/src/integrations/title-filters.ts index 536e6642c..e8b7778f0 100644 --- a/packages/ai/src/integrations/title-filters.ts +++ b/packages/ai/src/integrations/title-filters.ts @@ -12,6 +12,20 @@ interface CreateTitleFilterParams { pattern: string; } +interface UpdateTitleFilterParams { + enabled?: boolean; + matchType?: TitleFilterMatchType; + pattern?: string; +} + +function toTitleFilterUpdateSet(params: UpdateTitleFilterParams) { + return { + ...(params.enabled !== undefined ? { enabled: params.enabled } : {}), + ...(params.matchType !== undefined ? { matchType: params.matchType } : {}), + ...(params.pattern !== undefined ? { pattern: params.pattern } : {}), + }; +} + export class TitleFilterLimitError extends Error { constructor() { super(`You can add up to ${MAX_TITLE_FILTERS} title filters`); @@ -78,14 +92,14 @@ export async function createGithubTitleFilter( }); } -export async function setGithubTitleFilterEnabled( +export async function updateGithubTitleFilter( repositoryId: string, filterId: string, - enabled: boolean + params: UpdateTitleFilterParams ) { const [updated] = await db .update(githubTitleFilters) - .set({ enabled }) + .set(toTitleFilterUpdateSet(params)) .where( and( eq(githubTitleFilters.id, filterId), @@ -173,14 +187,14 @@ export async function createLinearTitleFilter( }); } -export async function setLinearTitleFilterEnabled( +export async function updateLinearTitleFilter( integrationId: string, filterId: string, - enabled: boolean + params: UpdateTitleFilterParams ) { const [updated] = await db .update(linearTitleFilters) - .set({ enabled }) + .set(toTitleFilterUpdateSet(params)) .where( and( eq(linearTitleFilters.id, filterId),