-
Notifications
You must be signed in to change notification settings - Fork 9
feat(api): migrate agent templates management endpoints #543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Changes from all commits
29c4b72
ff5e04f
3c7acab
ff853b8
2067efc
31f9bc3
550cd89
bca13d9
3837ad8
513b812
62ff368
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { toggleTemplateFavoriteHandler } from "@/lib/templates/toggleTemplateFavoriteHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A 200 NextResponse carrying the CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * PUT /api/agents/templates/{id}/favorite | ||
| * | ||
| * Idempotently sets whether the authenticated account has favorited the | ||
| * template: `{ is_favourite: true }` upserts a row, `false` deletes it. | ||
| * | ||
| * @param request - Incoming request; body is JSON-encoded. | ||
| * @param context - Route context. | ||
| * @param context.params - Promise resolving to `{ id }`, the template UUID. | ||
| * @returns A 200 NextResponse with `{ status: "success" }` on success. | ||
| */ | ||
| export async function PUT( | ||
| request: NextRequest, | ||
| context: { params: Promise<{ id: string }> }, | ||
| ): Promise<NextResponse> { | ||
| return toggleTemplateFavoriteHandler(request, context.params); | ||
| } | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const fetchCache = "force-no-store"; | ||
| export const revalidate = 0; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { updateTemplateHandler } from "@/lib/templates/updateTemplateHandler"; | ||
| import { deleteTemplateHandler } from "@/lib/templates/deleteTemplateHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A 200 NextResponse carrying the CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * PATCH /api/agents/templates/{id} | ||
| * | ||
| * Updates one or more fields on an template the authenticated account | ||
| * owns. Supplying `share_emails` replaces existing shares. | ||
| * | ||
| * @param request - Incoming request; body is JSON-encoded. | ||
| * @param context - Route context. | ||
| * @param context.params - Promise resolving to `{ id }`, the template UUID. | ||
| * @returns A 200 NextResponse with `{ status, template }` on success. | ||
| */ | ||
| export async function PATCH( | ||
| request: NextRequest, | ||
| context: { params: Promise<{ id: string }> }, | ||
| ): Promise<NextResponse> { | ||
| return updateTemplateHandler(request, context.params); | ||
| } | ||
|
|
||
| /** | ||
| * DELETE /api/agents/templates/{id} | ||
| * | ||
| * Permanently removes an template the authenticated account owns. | ||
| * | ||
| * @param request - Incoming request; auth is read from headers. | ||
| * @param context - Route context. | ||
| * @param context.params - Promise resolving to `{ id }`, the template UUID. | ||
| * @returns A 200 NextResponse with `{ status: "success" }` on success. | ||
| */ | ||
| export async function DELETE( | ||
| request: NextRequest, | ||
| context: { params: Promise<{ id: string }> }, | ||
| ): Promise<NextResponse> { | ||
| return deleteTemplateHandler(request, context.params); | ||
| } | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const fetchCache = "force-no-store"; | ||
| export const revalidate = 0; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { listTemplatesHandler } from "@/lib/templates/listTemplatesHandler"; | ||
| import { createTemplateHandler } from "@/lib/templates/createTemplateHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns A 200 NextResponse carrying the CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/agents/templates | ||
| * | ||
| * Returns every template visible to the authenticated account (own, | ||
| * public, and shared) with an embedded creator block (id/name/image/is_admin), | ||
| * the caller's `is_favourite` flag, and `shared_emails` for private templates. | ||
| * | ||
| * @param request - Incoming request; auth is read from headers. | ||
| * @returns A 200 NextResponse with `{ status, templates }`. | ||
| */ | ||
| export async function GET(request: NextRequest): Promise<NextResponse> { | ||
| return listTemplatesHandler(request); | ||
| } | ||
|
|
||
| /** | ||
| * POST /api/agents/templates | ||
| * | ||
| * Creates a new template owned by the authenticated account. When | ||
| * `is_private=true`, `share_emails` recipients are upserted into the shares | ||
| * table. | ||
| * | ||
| * @param request - Incoming request; body is JSON-encoded. | ||
| * @returns A 201 NextResponse with `{ status, template }` on success. | ||
| */ | ||
| export async function POST(request: NextRequest): Promise<NextResponse> { | ||
| return createTemplateHandler(request); | ||
| } | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const fetchCache = "force-no-store"; | ||
| export const revalidate = 0; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,14 @@ export const FLAMINGO_GENERATE_URL = | |
| /** Snapshot expiration duration (7 days) */ | ||
| export const SNAPSHOT_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; | ||
|
|
||
| /** | ||
| * Email addresses with platform-admin privileges. | ||
| * | ||
| * Surfaced as `creator.is_admin` in `/api/agents/templates` so clients can flag | ||
| * official Recoup templates. Mirrors `chat/lib/admin.ts`. | ||
| */ | ||
| export const ADMIN_EMAILS: readonly string[] = ["sidney+1@recoupable.com"] as const; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DRY - Why is |
||
|
|
||
| // EVALS | ||
| export const EVAL_ACCOUNT_ID = "fb678396-a68f-4294-ae50-b8cacf9ce77b"; | ||
| export const EVAL_ACCESS_TOKEN = process.env.EVAL_ACCESS_TOKEN || ""; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import supabase from "@/lib/supabase/serverClient"; | ||
|
|
||
| /** | ||
| * Removes the favorite row for `(template_id, account)`. Idempotent. | ||
| * | ||
| * @param templateId - The template UUID | ||
| * @param accountId - The account UUID whose favorite is being removed | ||
| * @returns True on success, false on database error. | ||
| */ | ||
| export async function deleteTemplateFavorite( | ||
| templateId: string, | ||
| accountId: string, | ||
| ): Promise<boolean> { | ||
| const { error } = await supabase | ||
| .from("agent_template_favorites") | ||
| .delete() | ||
| .eq("template_id", templateId) | ||
| .eq("user_id", accountId); | ||
|
|
||
| if (error) { | ||
| console.error("Error deleting template_favorite:", error); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import supabase from "@/lib/supabase/serverClient"; | ||
|
|
||
| /** | ||
| * Inserts a favorite row for `(template_id, account)`. Idempotent — a | ||
| * pre-existing row (Postgres unique-violation 23505) is treated as success. | ||
| * | ||
| * @param templateId - The template UUID | ||
| * @param accountId - The favoriting account UUID | ||
| * @returns True if the favorite exists after the call, false on unexpected error. | ||
| */ | ||
| export async function insertTemplateFavorite( | ||
| templateId: string, | ||
| accountId: string, | ||
| ): Promise<boolean> { | ||
| const { error } = await supabase | ||
| .from("agent_template_favorites") | ||
| .insert({ template_id: templateId, user_id: accountId }) | ||
| .select("template_id") | ||
| .maybeSingle(); | ||
|
|
||
| if (error && error.code !== "23505") { | ||
| console.error("Error inserting template_favorite:", error); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||
| import supabase from "@/lib/supabase/serverClient"; | ||||||
| import type { Tables } from "@/types/database.types"; | ||||||
|
|
||||||
| /** | ||||||
| * Selects raw `template_favorites` rows for the given account. Throws on | ||||||
| * database error so callers can distinguish a real failure from "account | ||||||
| * has no favorites". | ||||||
| */ | ||||||
| export async function selectTemplateFavorites( | ||||||
| accountId: string, | ||||||
| ): Promise<Tables<"agent_template_favorites">[]> { | ||||||
| const { data, error } = await supabase | ||||||
| .from("agent_template_favorites") | ||||||
| .select("*") | ||||||
| .eq("user_id", accountId); | ||||||
|
|
||||||
| if (error) { | ||||||
| console.error("Error selecting template_favorites:", error); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The error log message now references Prompt for AI agents
Suggested change
|
||||||
| throw new Error(`selectTemplateFavorites failed: ${error.message}`); | ||||||
| } | ||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||
|
|
||||||
| return data ?? []; | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import supabase from "@/lib/supabase/serverClient"; | ||
|
|
||
| /** | ||
| * Deletes every `template_shares` row for the given template id. | ||
| * | ||
| * Throws on database error so callers cannot silently continue with an | ||
| * inconsistent share state. | ||
| * | ||
| * @param templateId - The template UUID | ||
| * @throws If the Supabase delete fails. | ||
| */ | ||
| export async function deleteTemplateShares(templateId: string): Promise<void> { | ||
| const { error } = await supabase | ||
| .from("agent_template_shares") | ||
| .delete() | ||
| .eq("template_id", templateId); | ||
|
|
||
| if (error) { | ||
| console.error("Error deleting template_shares:", error); | ||
| throw new Error(`deleteTemplateShares failed: ${error.message}`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import supabase from "@/lib/supabase/serverClient"; | ||
| import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; | ||
|
|
||
| /** | ||
| * Resolves the supplied emails to account ids and upserts an | ||
| * `template_shares` row for each. Unknown emails are silently ignored. | ||
| * | ||
| * Throws on database error so callers can distinguish a real write failure | ||
| * from "nothing to insert" (the latter returns 0). | ||
| * | ||
| * @param templateId - The template UUID | ||
| * @param emails - Email addresses to share with | ||
| * @returns Number of shares inserted (pre-existing rows count as 0). | ||
| * @throws If the Supabase upsert fails. | ||
| */ | ||
| export async function insertTemplateShares(templateId: string, emails: string[]): Promise<number> { | ||
| if (!Array.isArray(emails) || emails.length === 0) return 0; | ||
|
|
||
| const accountEmails = await selectAccountEmails({ emails }); | ||
| const rows = accountEmails | ||
| .filter(row => row.account_id !== null) | ||
| .map(row => ({ template_id: templateId, user_id: row.account_id! })); | ||
|
|
||
| if (rows.length === 0) return 0; | ||
|
|
||
| const { data, error } = await supabase | ||
| .from("agent_template_shares") | ||
| .upsert(rows, { | ||
| onConflict: "template_id,user_id", | ||
| ignoreDuplicates: true, | ||
| }) | ||
| .select(); | ||
|
|
||
| if (error) { | ||
| console.error("Error inserting template_shares:", error); | ||
| throw new Error(`insertTemplateShares failed: ${error.message}`); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return data?.length ?? 0; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import supabase from "@/lib/supabase/serverClient"; | ||
| import type { Tables } from "@/types/database.types"; | ||
|
|
||
| /** | ||
| * Selects all template_shares rows for the given template ids. Throws on | ||
| * database error so callers cannot misread a DB failure as "no shares" and | ||
| * deny access (e.g. the toggle-favorite visibility check would otherwise | ||
| * 403 a legitimate sharee on a transient query failure). | ||
| * | ||
| * @param templateIds - Array of template UUIDs | ||
| * @returns Array of share rows (may be empty). | ||
| */ | ||
| export async function selectTemplateShares( | ||
| templateIds: string[], | ||
| ): Promise<Tables<"agent_template_shares">[]> { | ||
| if (!Array.isArray(templateIds) || templateIds.length === 0) return []; | ||
|
|
||
| const { data, error } = await supabase | ||
| .from("agent_template_shares") | ||
| .select("*") | ||
| .in("template_id", templateIds); | ||
|
|
||
| if (error) { | ||
| console.error("Error selecting template_shares:", error); | ||
| throw new Error(`selectTemplateShares failed: ${error.message}`); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return data ?? []; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||
| import supabase from "@/lib/supabase/serverClient"; | ||||||
|
|
||||||
| /** | ||||||
| * Deletes an template row by id. Cascades remove dependent shares / | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Fix the typo in the function docstring ( Prompt for AI agents
Suggested change
|
||||||
| * favorites at the database level. | ||||||
| * | ||||||
| * @param id - The template UUID | ||||||
| * @returns True if the delete succeeded, false otherwise. | ||||||
| */ | ||||||
| export async function deleteTemplate(id: string): Promise<boolean> { | ||||||
| const { error } = await supabase.from("agent_templates").delete().eq("id", id); | ||||||
|
|
||||||
| if (error) { | ||||||
| console.error("Error deleting template:", error); | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| return true; | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import supabase from "@/lib/supabase/serverClient"; | ||
| import type { Tables, TablesInsert } from "@/types/database.types"; | ||
|
|
||
| /** | ||
| * Inserts a new template row. | ||
| * | ||
| * @param row - The template insert payload (must include creator). | ||
| * @returns The newly created templates row, or null on error. | ||
| */ | ||
| export async function insertTemplate( | ||
| row: TablesInsert<"agent_templates">, | ||
| ): Promise<Tables<"agent_templates"> | null> { | ||
| const { data, error } = await supabase.from("agent_templates").insert(row).select("*").single(); | ||
|
|
||
| if (error) { | ||
| console.error("Error inserting template:", error); | ||
| return null; | ||
| } | ||
|
|
||
| return data; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: This change keeps the favorite toggle wired to
/api/agents/templates/{id}/favorite, so clients calling the migrated/api/templates/{id}/favoriteendpoint will still fail (404).Prompt for AI agents
Tip: Review your code locally with the cubic CLI to iterate faster.