Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/api/agents/templates/[id]/favorite/route.ts
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);
Copy link
Copy Markdown

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}/favorite endpoint will still fail (404).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/agents/templates/[id]/favorite/route.ts, line 32:

<comment>This change keeps the favorite toggle wired to `/api/agents/templates/{id}/favorite`, so clients calling the migrated `/api/templates/{id}/favorite` endpoint will still fail (404).</comment>

<file context>
@@ -29,7 +29,7 @@ export async function PUT(
   context: { params: Promise<{ id: string }> },
 ): Promise<NextResponse> {
-  return toggleAgentTemplateFavoriteHandler(request, context.params);
+  return toggleTemplateFavoriteHandler(request, context.params);
 }
 
</file context>

Tip: Review your code locally with the cubic CLI to iterate faster.

}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
55 changes: 55 additions & 0 deletions app/api/agents/templates/[id]/route.ts
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;
48 changes: 48 additions & 0 deletions app/api/agents/templates/route.ts
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;
8 changes: 8 additions & 0 deletions lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY - Why is admin_emails required? Admin is already an existing concept with libs, etc.


// EVALS
export const EVAL_ACCOUNT_ID = "fb678396-a68f-4294-ae50-b8cacf9ce77b";
export const EVAL_ACCESS_TOKEN = process.env.EVAL_ACCESS_TOKEN || "";
Expand Down
26 changes: 26 additions & 0 deletions lib/supabase/template_favorites/deleteTemplateFavorite.ts
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;
}
27 changes: 27 additions & 0 deletions lib/supabase/template_favorites/insertTemplateFavorite.ts
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;
}
23 changes: 23 additions & 0 deletions lib/supabase/template_favorites/selectTemplateFavorites.ts
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The error log message now references template_favorites, but this function still queries agent_template_favorites. This makes DB failures harder to trace because logs no longer match the actual table being accessed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/template_favorites/selectTemplateFavorites.ts, line 19:

<comment>The error log message now references `template_favorites`, but this function still queries `agent_template_favorites`. This makes DB failures harder to trace because logs no longer match the actual table being accessed.</comment>

<file context>
@@ -16,7 +16,7 @@ export async function selectAgentTemplateFavorites(
 
   if (error) {
-    console.error("Error selecting agent_template_favorites:", error);
+    console.error("Error selecting template_favorites:", error);
     return [];
   }
</file context>
Suggested change
console.error("Error selecting template_favorites:", error);
console.error("Error selecting agent_template_favorites:", error);

throw new Error(`selectTemplateFavorites failed: ${error.message}`);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return data ?? [];
}
22 changes: 22 additions & 0 deletions lib/supabase/template_shares/deleteTemplateShares.ts
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}`);
}
}
40 changes: 40 additions & 0 deletions lib/supabase/template_shares/insertTemplateShares.ts
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}`);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return data?.length ?? 0;
}
29 changes: 29 additions & 0 deletions lib/supabase/template_shares/selectTemplateShares.ts
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}`);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return data ?? [];
}
19 changes: 19 additions & 0 deletions lib/supabase/templates/deleteTemplate.ts
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 /
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Fix the typo in the function docstring (an templatea template) to keep API documentation clear.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/templates/deleteTemplate.ts, line 4:

<comment>Fix the typo in the function docstring (`an template` → `a template`) to keep API documentation clear.</comment>

<file context>
@@ -1,17 +1,17 @@
 
 /**
- * Deletes an agent template row by id. Cascades remove dependent shares /
+ * Deletes an template row by id. Cascades remove dependent shares /
  * favorites at the database level.
  *
</file context>
Suggested change
* Deletes an template row by id. Cascades remove dependent shares /
* Deletes a template row by id. Cascades remove dependent shares /

* 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;
}
21 changes: 21 additions & 0 deletions lib/supabase/templates/insertTemplate.ts
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;
}
Loading
Loading