diff --git a/apps/website/app/api/supabase/insert/account/route.ts b/apps/website/app/api/supabase/insert/account/route.ts new file mode 100644 index 000000000..a5d642398 --- /dev/null +++ b/apps/website/app/api/supabase/insert/account/route.ts @@ -0,0 +1,108 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, +} from "~/utils/supabase/dbUtils"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +type AccountDataInput = TablesInsert<"Account">; +type AccountRecord = Tables<"Account">; + +const getOrCreateAccount = async ( + supabasePromise: ReturnType, + accountData: AccountDataInput, +): Promise> => { + const { + person_id, + platform_id, + active = true, + write_permission = true, + } = accountData; + + if ( + person_id === undefined || + person_id === null || + platform_id === undefined || + platform_id === null + ) { + return { + entity: null, + error: "Missing required fields: person_id or platform_id.", + details: "Both person_id and platform_id are required.", + created: false, + status: 400, + }; + } + + const supabase = await supabasePromise; + + const result = await getOrCreateEntity<"Account">( + supabase, + "Account", + "id, person_id, platform_id, active, write_permission", + { person_id: person_id, platform_id: platform_id }, + { person_id, platform_id, active, write_permission }, + "Account", + ); + + if ( + result.error && + result.details && + result.status === 400 && + result.details.includes("violates foreign key constraint") + ) { + if (result.details.includes("Account_person_id_fkey")) { + return { + ...result, + error: `Invalid person_id: No Person record found for ID ${person_id}.`, + }; + } else if (result.details.includes("Account_platform_id_fkey")) { + return { + ...result, + error: `Invalid platform_id: No Space record found for ID ${platform_id}.`, + }; + } + } + return result; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + try { + const body: AccountDataInput = await request.json(); + + if (body.person_id === undefined || body.person_id === null) { + return createApiResponse(request, { + error: "Missing or invalid person_id.", + status: 400, + }); + } + if (body.platform_id === undefined || body.platform_id === null) { + return createApiResponse(request, { + error: "Missing or invalid platform_id.", + status: 400, + }); + } + + const result = await getOrCreateAccount(supabasePromise, body); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/account"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/agents/route.ts b/apps/website/app/api/supabase/insert/agents/route.ts new file mode 100644 index 000000000..ea82d3829 --- /dev/null +++ b/apps/website/app/api/supabase/insert/agents/route.ts @@ -0,0 +1,71 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, +} from "~/utils/supabase/dbUtils"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { Database, Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +type AgentDataInput = TablesInsert<"Agent">; +type AgentRecord = Tables<"Agent">; + +const getOrCreateAgentByType = async ( + supabasePromise: ReturnType, + agentType: Database["public"]["Enums"]["EntityType"], +): Promise> => { + if (!agentType) { + return { + entity: null, + error: "Missing or invalid 'type' for Agent.", + details: "Agent 'type' is required and cannot be empty.", + created: false, + status: 400, + }; + } + + const supabase = await supabasePromise; + + return getOrCreateEntity<"Agent">( + supabase, + "Agent", + "id, type", + { type: agentType }, + { type: agentType }, + "Agent", + ); +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + try { + const body: AgentDataInput = await request.json(); + const { type } = body; + + if (!type || typeof type !== "string" || type.trim() === "") { + return createApiResponse(request, { + error: "Validation Error: Missing or invalid type for Agent.", + status: 400, + }); + } + + const result = await getOrCreateAgentByType(supabasePromise, type); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/agents"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/content-embedding/batch/route.ts b/apps/website/app/api/supabase/insert/content-embedding/batch/route.ts new file mode 100644 index 000000000..6e28cadf2 --- /dev/null +++ b/apps/website/app/api/supabase/insert/content-embedding/batch/route.ts @@ -0,0 +1,105 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { + processAndInsertBatch, + BatchProcessResult, +} from "~/utils/supabase/dbUtils"; +import { + inputProcessing, + outputProcessing, + type ApiInputEmbeddingItem, + type ApiOutputEmbeddingRecord, +} from "../route"; + +const batchInsertEmbeddingsProcess = async ( + supabase: Awaited>, + embeddingItems: ApiInputEmbeddingItem[], +): Promise> => { + // groupBy is node21 only. Group by model. + // Note: This means that later index values may be totally wrong. + const by_model: { [key: string]: ApiInputEmbeddingItem[] } = {}; + for (let i = 0; i < embeddingItems.length; i++) { + const inputItem = embeddingItems[i]; + if (inputItem !== undefined && inputItem.model !== undefined) { + if (by_model[inputItem.model] === undefined) { + by_model[inputItem.model] = [inputItem]; + } else { + by_model[inputItem.model]!.push(inputItem); + } + } else { + return { + status: 400, + error: `Element ${i} undefined or does not have a model`, + }; + } + } + const globalResults: ApiOutputEmbeddingRecord[] = []; + const partial_errors = []; + let created = true; // TODO: Maybe transmit from below + for (const table_name of Object.keys(by_model)) { + const embeddingItemsSet = by_model[table_name]; + const results = await processAndInsertBatch< + "ContentEmbedding_openai_text_embedding_3_small_1536", + ApiInputEmbeddingItem, + ApiOutputEmbeddingRecord + >( + supabase, + embeddingItemsSet!, + table_name, + "*", // Select all fields, adjust if needed for ContentEmbeddingRecord + "ContentEmbedding", + inputProcessing!, + outputProcessing, + ); + if (results.error || results.data === undefined) + return { ...results, data: undefined }; + globalResults.push(...results.data); + if (results.partial_errors !== undefined) + partial_errors.push(...results.partial_errors); + } + return { + data: globalResults, + partial_errors, + status: created ? 201 : 200, + }; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabase = await createClient(); + + try { + const body: ApiInputEmbeddingItem[] = await request.json(); + if (!Array.isArray(body)) { + return createApiResponse(request, { + error: "Request body must be an array of embedding items.", + status: 400, + }); + } + + const result = await batchInsertEmbeddingsProcess(supabase, body); + + return createApiResponse(request, { + data: result.data, + error: result.error, + details: result.details, + ...(result.partial_errors && { + meta: { partial_errors: result.partial_errors }, + }), + status: result.status, + created: result.status === 201, + }); + } catch (e: unknown) { + return handleRouteError( + request, + e, + `/api/supabase/insert/content-embedding/batch`, + ); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/content-embedding/route.ts b/apps/website/app/api/supabase/insert/content-embedding/route.ts new file mode 100644 index 000000000..7e551738a --- /dev/null +++ b/apps/website/app/api/supabase/insert/content-embedding/route.ts @@ -0,0 +1,249 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, + known_embedding_tables, + ItemProcessor, + ItemValidator, +} from "~/utils/supabase/dbUtils"; +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +// Use the first known ContentEmbedding table, as they have the same structure +export type ContentEmbeddingDataInput = + TablesInsert<"ContentEmbedding_openai_text_embedding_3_small_1536">; +export type ContentEmbeddingRecord = + Tables<"ContentEmbedding_openai_text_embedding_3_small_1536">; + +export type ApiInputEmbeddingItem = Omit< + ContentEmbeddingDataInput, + "vector" +> & { + vector: number[]; // Vector is passed in as a number[] +}; + +export type ApiOutputEmbeddingRecord = Omit< + ContentEmbeddingRecord, + "vector" +> & { + vector: number[]; // Vector is passed in as a number[] +}; + +export const inputValidation: ItemValidator = (data) => { + const { target_id, model, vector } = data; + + // --- Start of validation --- + if ( + target_id === undefined || + target_id === null || + typeof target_id !== "number" + ) { + return "Missing or invalid target_id."; + } + if ( + !model || + typeof model !== "string" || + known_embedding_tables[model] == undefined + ) { + return "Missing or invalid model name."; + } + const { table_size } = known_embedding_tables[model]; + + if ( + !vector || + !Array.isArray(vector) || + !vector.every((v) => typeof v === "number") + ) { + return "Missing or invalid vector. Must be an array of numbers."; + } + if (vector.length != table_size) { + return `Invalid vector length. Expected ${table_size}, got ${vector.length}.`; + } + if (data.obsolete !== undefined && typeof data.obsolete !== "boolean") { + // Check original data for obsolete presence + return "Invalid type for obsolete. Must be a boolean."; + } + return null; +}; + +export const inputProcessing: ItemProcessor< + ApiInputEmbeddingItem, + ContentEmbeddingDataInput +> = (data) => { + const error = inputValidation(data); + if (error) { + return { valid: false, error }; + } + return { + valid: true, + data: { ...data, vector: JSON.stringify(data.vector) }, + }; +}; + +export const outputValidation: ItemValidator = ( + data, +) => { + const { model, vector } = data; + if ( + !model || + typeof model !== "string" || + known_embedding_tables[model] == undefined + ) { + return "Missing or invalid model name."; + } + + const { table_size } = known_embedding_tables[model]; + + if (vector.length != table_size) { + return `Invalid vector length. Expected ${table_size}, got ${vector.length}.`; + } + return null; +}; + +export const outputProcessing: ItemProcessor< + ContentEmbeddingRecord, + ApiOutputEmbeddingRecord +> = (data) => { + try { + const processedData = { ...data, vector: JSON.parse(data.vector) }; + const error = outputValidation(processedData); + if (error) { + return { valid: false, error }; + } + return { + valid: true, + data: processedData, + }; + } catch (error) { + if (error instanceof SyntaxError) { + return { valid: false, error: error.message }; + } + throw error; + } +}; + +// Renamed and refactored +const processAndCreateEmbedding = async ( + supabasePromise: ReturnType, + data: ApiInputEmbeddingItem, +): Promise> => { + const { valid, error, processedItem } = inputProcessing(data); + if ( + !valid || + processedItem === undefined || + processedItem.model === undefined + ) { + return { + entity: null, + error: error || "unknown error", + created: false, + status: 400, + }; + } + const supabase = await supabasePromise; + const table_data = known_embedding_tables[processedItem.model]; + + if (!table_data) { + return { + entity: null, + error: "unknown model", + created: false, + status: 400, + }; + } + + const { table_name } = table_data; + // Using getOrCreateEntity, forcing create path by providing non-matching criteria + // This standardizes return type and error handling (e.g., FK violations from dbUtils) + const result = + await getOrCreateEntity<"ContentEmbedding_openai_text_embedding_3_small_1536">( + supabase, + table_name, + "*", // Select all fields for the record + { id: -1 }, // Non-matching criteria to force "create" path + processedItem, + "ContentEmbedding", + ); + + // getOrCreateEntity handles general 23503, but we can make the message more specific if needed + if ( + result.error && + result.details && + result.status === 400 && + result.details.includes("violates foreign key constraint") + ) { + if ( + result.details.toLowerCase().includes( + // Check for target_id FK, adapt if FK name is different + `${table_name.toLowerCase()}_target_id_fkey`, + ) || + result.details.toLowerCase().includes("target_id") + ) { + return { + ...result, + entity: null, + error: `Invalid target_id: No Content record found for ID ${processedItem.target_id}.`, + }; + } + } + + if (result.error || !result.entity) { + return { + ...result, + entity: null, + }; + } + + const processedResult = outputProcessing(result.entity); + if (!processedResult.valid || !processedResult.processedItem) + return { + ...result, + error: processedResult.error || "unknown error", + entity: null, + status: 500, + }; + return { + ...result, + entity: processedResult.processedItem, + }; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + try { + const body: ApiInputEmbeddingItem = await request.json(); + + // Minimal validation here, more detailed in processAndCreateEmbedding + if (!body || typeof body !== "object") { + return createApiResponse(request, { + error: "Invalid request body: expected a JSON object.", + status: 400, + }); + } + + const result = await processAndCreateEmbedding(supabasePromise, body); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, // Will be true if successful create + }); + } catch (e: unknown) { + return handleRouteError( + request, + e, + `/api/supabase/insert/content-embedding`, + // TODO replace with a generic name + ); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/content/batch/route.ts b/apps/website/app/api/supabase/insert/content/batch/route.ts new file mode 100644 index 000000000..200fa720e --- /dev/null +++ b/apps/website/app/api/supabase/insert/content/batch/route.ts @@ -0,0 +1,63 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { + validateAndInsertBatch, + BatchProcessResult, // Import BatchProcessResult for the return type +} from "~/utils/supabase/dbUtils"; // Ensure this path is correct +import { + inputValidation, + type ContentDataInput, + type ContentRecord, +} from "../route"; + +// Updated batchInsertContentProcess to use the generic utility +const batchInsertContentProcess = async ( + supabase: Awaited>, + contentItems: ContentDataInput[], +): Promise> => { + return validateAndInsertBatch<"Content">( + supabase, + contentItems, + "Content", // Table name + "*", // Select query (can be more specific, e.g., "id, text, scale, ...") + "Content", // Entity name for logging + inputValidation, + null, + ); +}; + +export const POST = async (request: NextRequest): Promise => { + const supabase = await createClient(); + + try { + const body: ContentDataInput[] = await request.json(); + if (!Array.isArray(body)) { + return createApiResponse(request, { + error: "Request body must be an array of content items.", + status: 400, + }); + } + + const result = await batchInsertContentProcess(supabase, body); + + return createApiResponse(request, { + data: result.data, + error: result.error, + details: result.details, + ...(result.partial_errors && { + meta: { partial_errors: result.partial_errors }, + }), + status: result.status, + created: result.status === 201, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/content/batch"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/content/route.ts b/apps/website/app/api/supabase/insert/content/route.ts new file mode 100644 index 000000000..dfba27b52 --- /dev/null +++ b/apps/website/app/api/supabase/insert/content/route.ts @@ -0,0 +1,172 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, + ItemValidator, +} from "~/utils/supabase/dbUtils"; // Ensure path is correct +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; // Ensure path is correct +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +export type ContentDataInput = TablesInsert<"Content">; +export type ContentRecord = Tables<"Content">; + +export const inputValidation: ItemValidator = ( + data: ContentDataInput, +) => { + const { author_id, created, last_modified, scale, space_id, text } = data; + + // --- Start of extensive validation --- + if (!text || typeof text !== "string") return "Invalid or missing text."; + if (!scale || typeof scale !== "string") return "Invalid or missing scale."; + if ( + space_id === undefined || + space_id === null || + typeof space_id !== "number" + ) + return "Invalid or missing space_id."; + if ( + author_id === undefined || + author_id === null || + typeof author_id !== "number" + ) + return "Invalid or missing author_id."; + if (created) + try { + new Date(created); // Validate date format + new Date(last_modified); // Validate date format + } catch (e) { + return "Invalid date format for created or last_modified."; + } + if (last_modified) + try { + new Date(last_modified); // Validate date format + } catch (e) { + return "Invalid date format for created or last_modified."; + } + // --- End of extensive validation --- + + return null; +}; + +// Renamed and refactored +const processAndUpsertContentEntry = async ( + supabasePromise: ReturnType, + data: ContentDataInput, +): Promise> => { + const { space_id, author_id, source_local_id, document_id, part_of_id } = + data; + + const error = inputValidation(data); + if (error !== null) { + return { + entity: null, + error, + created: false, + status: 400, + }; + } + + const supabase = await supabasePromise; + + let matchCriteria: Record | null = null; + if (source_local_id && space_id !== undefined && space_id !== null) { + matchCriteria = { space_id: space_id, source_local_id: source_local_id }; + } + // If no solid matchCriteria for a "get", getOrCreateEntity will likely proceed to "create". + // If there are unique constraints other than (space_id, source_local_id), it will handle race conditions. + + const result = await getOrCreateEntity<"Content">( + supabase, + "Content", + "*", // Select all fields for ContentRecord + matchCriteria || { id: -1 }, // Use a non-matching criteria if no specific lookup needed, to force create path if not found + data, // This will be used for insert if not found or for update in some extended utilities. + "Content", + ); + + // Custom handling for specific foreign key errors + if ( + result.error && + result.details && + result.status === 400 && + result.details.includes("violates foreign key constraint") + ) { + const details = result.details.toLowerCase(); + if ( + details.includes("content_space_id_fkey") || + details.includes("space_id") + ) { + // Be more general with FK name if it changes + return { + ...result, + error: `Invalid space_id: No Space record found for ID ${space_id}.`, + }; + } + if ( + details.includes("content_author_id_fkey") || + details.includes("author_id") + ) { + return { + ...result, + error: `Invalid author_id: No Account record found for ID ${author_id}.`, + }; + } + if ( + document_id && + (details.includes("content_document_id_fkey") || + details.includes("document_id")) + ) { + return { + ...result, + error: `Invalid document_id: No Document record found for ID ${document_id}.`, + }; + } + if ( + part_of_id && + (details.includes("content_part_of_id_fkey") || + details.includes("part_of_id")) + ) { + return { + ...result, + error: `Invalid part_of_id: No Content record found for ID ${part_of_id}.`, + }; + } + } + return result; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + try { + const body: ContentDataInput = await request.json(); + + // Most validation is now inside processAndUpsertContentEntry + // Minimal check here, or rely on processAndUpsertContentEntry for all field validation + if (!body || typeof body !== "object") { + return createApiResponse(request, { + error: "Invalid request body: expected a JSON object.", + status: 400, + }); + } + + const result = await processAndUpsertContentEntry(supabasePromise, body); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/content"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/document/route.ts b/apps/website/app/api/supabase/insert/document/route.ts new file mode 100644 index 000000000..faa1ec9c9 --- /dev/null +++ b/apps/website/app/api/supabase/insert/document/route.ts @@ -0,0 +1,144 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, +} from "~/utils/supabase/dbUtils"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +type DocumentDataInput = TablesInsert<"Document">; +type DocumentRecord = Tables<"Document">; + +const createDocument = async ( + supabasePromise: ReturnType, + data: DocumentDataInput, +): Promise> => { + const { + space_id, + source_local_id, + url, + metadata: rawMetadata, + created, + last_modified, + author_id, + } = data; + + if ( + space_id === undefined || + space_id === null || + !created || + !last_modified || + author_id === undefined || + author_id === null + ) { + return { + entity: null, + error: + "Missing required fields: space_id, created, last_modified, or author_id.", + created: false, + status: 400, + }; + } + + const processedMetadata = + rawMetadata && typeof rawMetadata === "object" + ? JSON.stringify(rawMetadata) + : typeof rawMetadata === "string" + ? rawMetadata + : null; + + const documentToInsert = { + space_id, + source_local_id: source_local_id || null, + url: url || null, + metadata: processedMetadata as any, + created, + last_modified, + author_id, + }; + + const supabase = await supabasePromise; + + const result = await getOrCreateEntity<"Document">( + supabase, + "Document", + "id, space_id, source_local_id, url, metadata, created, last_modified, author_id", + { id: -1 }, + documentToInsert, + "Document", + ); + + if ( + result.error && + result.details && + result.status === 400 && + result.details.includes("violates foreign key constraint") + ) { + if (result.details.includes("space_id_fkey")) { + return { + ...result, + error: `Invalid space_id: No Space record found for ID ${space_id}.`, + }; + } + if (result.details.includes("author_id_fkey")) { + return { + ...result, + error: `Invalid author_id: No Account record found for ID ${author_id}.`, + }; + } + } + + return result; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + try { + const body: DocumentDataInput = await request.json(); + + if (body.space_id === undefined || body.space_id === null) { + return createApiResponse(request, { + error: "Missing required field: space_id.", + status: 400, + }); + } + if (!body.created) { + return createApiResponse(request, { + error: "Missing required field: created.", + status: 400, + }); + } + if (!body.last_modified) { + return createApiResponse(request, { + error: "Missing required field: last_modified.", + status: 400, + }); + } + if (body.author_id === undefined || body.author_id === null) { + return createApiResponse(request, { + error: "Missing required field: author_id.", + status: 400, + }); + } + + const result = await createDocument(supabasePromise, body); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/document"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/person/route.ts b/apps/website/app/api/supabase/insert/person/route.ts new file mode 100644 index 000000000..c4f53f7e6 --- /dev/null +++ b/apps/website/app/api/supabase/insert/person/route.ts @@ -0,0 +1,199 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, +} from "~/utils/supabase/dbUtils"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, // Assuming OPTIONS might be added later +} from "~/utils/supabase/apiUtils"; +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +type PersonDataInput = TablesInsert<"Person">; +type PersonRecord = Tables<"Person">; +type AccountRecord = Tables<"Account">; + +// Kept for the final API response structure +// type PersonWithAccountResult = { +// person: PersonRecord | null; +// account: AccountRecord | null; +// person_created?: boolean; +// account_created?: boolean; +// }; + +const getOrCreatePersonInternal = async ( + supabasePromise: ReturnType, + email: string, + name: string, + orcid: string | null | undefined, +): Promise> => { + const supabase = await supabasePromise; + const agent_response = await getOrCreateEntity<"Agent">( + supabase, + "Agent", + "id, type", + { type: "Person" }, + { type: "Person" }, + "Agent", + ); + if (agent_response.error || agent_response.entity === null) + return agent_response as any as GetOrCreateEntityResult; + return getOrCreateEntity<"Person">( + supabase, + "Person", + "id, name, email, orcid, type", + { email: email.trim() }, + { + id: agent_response.entity.id, + email: email.trim(), + name: name.trim(), + orcid: orcid || null, + }, + "Person", + ); +}; + +const getOrCreateAccountInternal = async ( + supabasePromise: ReturnType, + personId: number, + platformId: number, + isActive: boolean, + writePermission?: boolean, +): Promise> => { + const supabase = await supabasePromise; + const result = await getOrCreateEntity<"Account">( + supabase, + "Account", + "id, person_id, platform_id, active, write_permission", + { person_id: personId, platform_id: platformId }, + { + person_id: personId, + platform_id: platformId, + active: isActive, + write_permission: writePermission === undefined ? true : writePermission, // Default to true if undefined + }, + "Account", + ); + + // Custom handling for specific foreign key errors + if ( + result.error && + result.details && + result.status === 400 && + result.details.includes("violates foreign key constraint") + ) { + if (result.details.includes("Account_person_id_fkey")) { + return { + ...result, + error: `Invalid person_id for Account: No Person record found for ID ${personId}.`, + }; + } + if (result.details.includes("Account_platform_id_fkey")) { + return { + ...result, + error: `Invalid platform_id for Account: No Space record found for ID ${platformId}.`, + }; + } + } + return result; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + // MAP: Punting the joint creation of person and account. Create the account after the person. + try { + const body: PersonDataInput = await request.json(); + const { + name, + email, + orcid = null, // Default from input + // account_platform_id, + // account_active = true, // Default from input + // account_write_permission, // No default here, handled in getOrCreateAccountInternal + } = body; + + // Initial input validation + if (!name || typeof name !== "string" || name.trim() === "") { + return createApiResponse(request, { + error: "Missing or invalid name for Person.", + status: 400, + }); + } + if (!email || typeof email !== "string" || email.trim() === "") { + return createApiResponse(request, { + error: "Missing or invalid email for Person.", + status: 400, + }); + } + // if ( + // account_platform_id === undefined || + // account_platform_id === null || + // typeof account_platform_id !== "number" + // ) { + // return createApiResponse(request, { + // error: "Missing or invalid account_platform_id for Account.", + // status: 400, + // }); + // } + + // Get or Create Person + const personResult = await getOrCreatePersonInternal( + supabasePromise, // Pass the promise + email, + name, + orcid, + ); + + if (personResult.error || !personResult.entity) { + return createApiResponse(request, { + error: personResult.error || "Failed to process Person.", + details: personResult.details, + status: personResult.status || 500, + }); + } + + // // Get or Create Account + // const accountResult = await getOrCreateAccountInternal( + // supabasePromise, // Pass the promise again, it will resolve the same client or a new one if needed by createClient impl. + // personResult.entity.id, + // account_platform_id, + // account_active, + // account_write_permission, + // ); + + // if (accountResult.error || !accountResult.entity) { + // // If account creation fails, return error but include successfully processed person + // return createApiResponse(request, { + // error: accountResult.error || "Failed to process Account.", + // details: accountResult.details, + // status: accountResult.status || 500, + // // Optionally include person data if account failed + // // data: { person: personResult.entity, person_created: personResult.created } + // }); + // } + + // const responsePayload: PersonWithAccountResult = { + // person: personResult.entity, + // account: accountResult.entity, + // person_created: personResult.created, + // account_created: accountResult.created, + // }; + + // const overallStatus = + // personResult.created || accountResult.created ? 201 : 200; + const overallStatus = personResult.created ? 201 : 200; + + return createApiResponse(request, { + data: personResult, + status: overallStatus, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/person"); + } +}; + +// If you need an OPTIONS handler for this route: +// export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/platform/route.ts b/apps/website/app/api/supabase/insert/platform/route.ts new file mode 100644 index 000000000..45a543af8 --- /dev/null +++ b/apps/website/app/api/supabase/insert/platform/route.ts @@ -0,0 +1,86 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, +} from "~/utils/supabase/dbUtils"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +type PlatformDataInput = TablesInsert<"Platform">; +type PlatformRecord = Tables<"Platform">; + +const getOrCreatePlatformFromURL = async ( + supabase: ReturnType, + url: string, +): Promise> => { + let platformName: string | null = null; + let platformUrl: string | null = null; + const lowerCaseURL = url.toLowerCase(); + + if (lowerCaseURL.includes("roamresearch.com")) { + platformName = "roamresearch"; + platformUrl = "https://roamresearch.com"; + } else { + console.warn("Could not determine platform from URL:", url); + return { + error: "Could not determine platform from URL.", + entity: null, + created: false, + status: 400, + }; + } + + if (!platformName || !platformUrl) { + return { + error: "Platform name or URL could not be derived.", + entity: null, + created: false, + status: 400, + }; + } + + const resolvedSupabaseClient = await supabase; + return getOrCreateEntity<"Platform">( + resolvedSupabaseClient, + "Platform", + "id, name, url", + { url: platformUrl }, + { name: platformName, url: platformUrl }, + "Platform", + ); +}; + +export const POST = async (request: NextRequest): Promise => { + const supabase = createClient(); + + try { + const body: PlatformDataInput = await request.json(); + const { url } = body; + + if (!url || typeof url !== "string") { + return createApiResponse(request, { + error: "Missing or invalid url in request body.", + status: 400, + }); + } + + const result = await getOrCreatePlatformFromURL(supabase, url); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/platform"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/api/supabase/insert/space/route.ts b/apps/website/app/api/supabase/insert/space/route.ts new file mode 100644 index 000000000..23b928259 --- /dev/null +++ b/apps/website/app/api/supabase/insert/space/route.ts @@ -0,0 +1,121 @@ +import { createClient } from "~/utils/supabase/server"; +import { NextResponse, NextRequest } from "next/server"; +import { + getOrCreateEntity, + GetOrCreateEntityResult, +} from "~/utils/supabase/dbUtils"; +import { + createApiResponse, + handleRouteError, + defaultOptionsHandler, +} from "~/utils/supabase/apiUtils"; +import { Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +type SpaceDataInput = TablesInsert<"Space">; +type SpaceRecord = Tables<"Space">; + +// Renamed and refactored helper function +export const processAndGetOrCreateSpace = async ( + supabasePromise: ReturnType, + data: SpaceDataInput, +): Promise> => { + const { name, url, platform_id } = data; + + // --- Start of validation --- + if (!name || typeof name !== "string" || name.trim() === "") { + return { + entity: null, + error: "Missing or invalid name.", + created: false, + status: 400, + }; + } + if (!url || typeof url !== "string" || url.trim() === "") { + return { + entity: null, + error: "Missing or invalid URL.", + created: false, + status: 400, + }; + } + if ( + platform_id === undefined || + platform_id === null || + typeof platform_id !== "number" + ) { + return { + entity: null, + error: "Missing or invalid platform_id.", + created: false, + status: 400, + }; + } + // --- End of validation --- + + const normalizedUrl = url.trim().replace(/\/$/, ""); + const trimmedName = name.trim(); + const supabase = await supabasePromise; + + const result = await getOrCreateEntity<"Space">( + supabase, + "Space", + "id, name, url, platform_id", + { url: normalizedUrl, platform_id: platform_id }, + { + name: trimmedName, + url: normalizedUrl, + platform_id: platform_id, + }, + "Space", + ); + + // Custom handling for specific foreign key error related to platform_id + if ( + result.error && + result.details && + result.status === 400 && + result.details.includes("violates foreign key constraint") + ) { + if ( + result.details.toLowerCase().includes("platform_id_fkey") || + result.details.toLowerCase().includes("platform_id") + ) { + return { + ...result, + error: `Invalid platform_id: No Space record found for ID ${platform_id}.`, + }; + } + } + + return result; +}; + +export const POST = async (request: NextRequest): Promise => { + const supabasePromise = createClient(); + + try { + const body: SpaceDataInput = await request.json(); + + // Minimal validation here, more detailed in the helper + if (!body || typeof body !== "object") { + return createApiResponse(request, { + error: "Invalid request body: expected a JSON object.", + status: 400, + }); + } + + const result = await processAndGetOrCreateSpace(supabasePromise, body); + + return createApiResponse(request, { + data: result.entity, + error: result.error, + details: result.details, + status: result.status, + created: result.created, + }); + } catch (e: unknown) { + return handleRouteError(request, e, "/api/supabase/insert/space"); + } +}; + +export const OPTIONS = defaultOptionsHandler; diff --git a/apps/website/app/utils/supabase/apiUtils.ts b/apps/website/app/utils/supabase/apiUtils.ts new file mode 100644 index 000000000..1703d27ec --- /dev/null +++ b/apps/website/app/utils/supabase/apiUtils.ts @@ -0,0 +1,88 @@ +import { NextResponse, NextRequest } from "next/server"; +import cors from "~/utils/llm/cors"; // Assuming this path is correct and accessible + +/** + * Sends a standardized JSON response. + * @param request The original NextRequest. + * @param data The data payload for successful responses. + * @param error An error message string if the operation failed. + * @param details Optional detailed error information. + * @param status The HTTP status code for the response. + * @param created A boolean indicating if a resource was created (influences status code: 201 vs 200). + */ +export function createApiResponse( + request: NextRequest, + payload: { + data?: T | null; + error?: string | null; + details?: string | null; + status: number; + created?: boolean; + }, +): NextResponse { + let response: NextResponse; + const { data, error, details, status, created } = payload; + + if (error) { + // console.error(`API Error (status ${status}): ${error}`, details || ""); // Logging done by callers or specific error handlers + response = NextResponse.json( + { error, details: details || undefined }, + { status }, + ); + } else if (data !== undefined && data !== null) { + const effectiveStatus = created ? 201 : status === 201 ? 200 : status; + response = NextResponse.json(data, { status: effectiveStatus }); + } else { + // Fallback for unexpected state (e.g. no error, but no data for a success status) + console.error( + `API Response Error: Attempted to send success response (status ${status}) with no data and no error.`, + ); + response = NextResponse.json( + { + error: + "An unexpected server error occurred during response generation.", + }, + { status: 500 }, + ); + } + return cors(request, response) as NextResponse; +} + +/** + * Handles errors caught in the main try-catch block of an API route. + * Differentiates JSON parsing errors from other errors. + */ +export function handleRouteError( + request: NextRequest, + error: unknown, + routeName: string, +): NextResponse { + console.error(`API route error in ${routeName}:`, error); + if ( + error instanceof SyntaxError && + error.message.toLowerCase().includes("json") + ) { + return createApiResponse(request, { + error: "Invalid JSON in request body.", + status: 400, + }); + } + const message = + error instanceof Error + ? error.message + : "An unexpected error occurred processing your request."; + return createApiResponse(request, { + error: message, + status: 500, + }); +} + +/** + * Default OPTIONS handler for CORS preflight requests. + */ +export async function defaultOptionsHandler( + request: NextRequest, +): Promise { + const response = new NextResponse(null, { status: 204 }); + return cors(request, response) as NextResponse; +} diff --git a/apps/website/app/utils/supabase/dbUtils.ts b/apps/website/app/utils/supabase/dbUtils.ts new file mode 100644 index 000000000..daf8533cb --- /dev/null +++ b/apps/website/app/utils/supabase/dbUtils.ts @@ -0,0 +1,425 @@ +import type { SupabaseClient, PostgrestError } from "@supabase/supabase-js"; +import { OK } from "zod"; +import { Database, Tables, TablesInsert } from "~/utils/supabase/types.gen"; + +export const known_embedding_tables: { + [key: string]: { + table_name: keyof Database["public"]["Tables"]; + table_size: number; + }; +} = { + openai_text_embedding_3_small_1536: { + table_name: "ContentEmbedding_openai_text_embedding_3_small_1536", + table_size: 1536, + }, +}; + +export type GetOrCreateEntityResult = { + entity: T | null; + error: string | null; + details?: string; // For detailed error messages, e.g., from Supabase + created: boolean; + status: number; // HTTP status code to suggest +}; + +/** + * Generic function to get an entity or create it if it doesn't exist. + * Handles common race conditions for unique constraint violations. + * + * @param supabase Supabase client instance. + * @param tableName The name of the table. + * @param selectQuery The select string, e.g., "id, name, url". + * @param matchCriteria An object representing the WHERE clause for lookup, e.g., { url: "some-url" }. + * @param insertData Data to insert if the entity is not found. + * @param entityName A friendly name for the entity, used in logging and error messages. + * @returns Promise> + */ +export async function getOrCreateEntity< + TableName extends keyof Database["public"]["Tables"], +>( + supabase: SupabaseClient, + tableName: keyof Database["public"]["Tables"], + selectQuery: string, + matchCriteria: Record, + insertData: TablesInsert, // Flexible insert data + entityName: string = tableName, +): Promise>> { + // 1. Try to fetch existing entity + let queryBuilder = supabase.from(tableName).select(selectQuery); + for (const key in matchCriteria) { + queryBuilder = queryBuilder.eq(key, matchCriteria[key]); + } + const { data: existingEntity, error: fetchError } = + await queryBuilder.maybeSingle>(); + + if (fetchError) { + console.error(`Error fetching ${entityName} by`, matchCriteria, fetchError); + return { + entity: null, + error: `Database error while fetching ${entityName}.`, + details: fetchError.message, + created: false, + status: 500, + }; + } + + if (existingEntity) { + console.log(`Found existing ${entityName}:`, existingEntity); + return { + entity: existingEntity, + error: null, + created: false, + status: 200, + }; + } + + // 2. Create new entity if not found + console.log( + `${entityName} not found with criteria`, + matchCriteria, + `creating new one...`, + ); + const { data: newEntity, error: insertError } = await supabase + .from(tableName) + .insert(insertData) + .select(selectQuery) + .single>(); + + if (insertError) { + console.error( + `Error inserting new ${entityName}:`, + insertData, + insertError, + ); + // Handle race condition: unique constraint violation (PostgreSQL error code '23505') + if (insertError.code === "23505") { + console.warn( + `Unique constraint violation on ${entityName} insert for`, + matchCriteria, + `Attempting to re-fetch.`, + ); + let reFetchQueryBuilder = supabase.from(tableName).select(selectQuery); + for (const key in matchCriteria) { + reFetchQueryBuilder = reFetchQueryBuilder.eq(key, matchCriteria[key]); + } + const { data: reFetchedEntity, error: reFetchError } = + await reFetchQueryBuilder.maybeSingle>(); + + if (reFetchError) { + console.error( + `Error re-fetching ${entityName} after unique constraint violation:`, + reFetchError, + ); + return { + entity: null, + error: `Database error after unique constraint violation for ${entityName}.`, + details: reFetchError.message, + created: false, + status: 500, + }; + } + if (reFetchedEntity) { + console.log(`Found ${entityName} on re-fetch:`, reFetchedEntity); + return { + entity: reFetchedEntity, + error: null, + created: false, + status: 200, // Successfully fetched, though not created by this call + }; + } + return { + entity: null, + error: `Unique constraint violation on ${entityName} insert, and re-fetch failed to find the entity.`, + details: insertError.message, // Original insert error + created: false, + status: 409, // Conflict, and couldn't resolve by re-fetching + }; + } + // Handle foreign key constraint violations (PostgreSQL error code '23503') + if (insertError.code === "23503") { + return { + entity: null, + error: `Invalid reference: A foreign key constraint was violated while creating ${entityName}.`, + details: insertError.message, // Specific FK details are in the message + created: false, + status: 400, // Usually due to bad input ID + }; + } + + return { + entity: null, + error: `Database error while inserting ${entityName}.`, + details: insertError.message, + created: false, + status: 500, + }; + } + + if (!newEntity) { + // This case should ideally not be reached if insertError is null and .single() is used. + console.error( + `New ${entityName} was not returned after insert, despite no reported Supabase error.`, + ); + return { + entity: null, + error: `Failed to retrieve new ${entityName} after insert operation.`, + details: + "The insert operation might have appeared successful but returned no data.", + created: false, // Unsure if created + status: 500, + }; + } + + console.log(`Created new ${entityName}:`, newEntity); + return { entity: newEntity, error: null, created: true, status: 201 }; +} + +export type BatchItemValidator = ( + item: TInput, + index: number, +) => { valid: boolean; error?: string; processedItem?: TProcessed }; + +export type ItemProcessor = (item: TInput) => { + valid: boolean; + error?: string; + processedItem?: TProcessed; +}; + +export type ItemValidator = (item: T) => string | null; + +export type BatchProcessResult = { + data?: TRecord[]; + error?: string; + details?: string; // For DB error details + partial_errors?: { index: number; error: string }[]; + status: number; // HTTP status to suggest +}; + +export async function InsertValidatedBatch< + TableName extends keyof Database["public"]["Tables"], +>( + supabase: SupabaseClient, + items: TablesInsert[], + tableName: string, + selectQuery: string, // e.g., "id, field1, field2" or "*" + entityName: string = tableName, // For logging +): Promise>> { + const { data: newRecords, error: insertError } = await supabase + .from(tableName) + .insert(items) + .select(selectQuery); // Use the provided select query + + if (insertError) { + console.error(`Error batch inserting ${entityName}:`, insertError); + return { + error: `Database error during batch insert of ${entityName}.`, + details: insertError.message, + status: 500, + }; + } + + const newRecordsTyped = newRecords as Tables[]; // Assert type + + if (!newRecordsTyped || newRecordsTyped.length !== items.length) { + console.warn( + `Batch insert ${entityName}: Mismatch between processed count (${items.length}) and DB returned count (${newRecordsTyped?.length || 0}).`, + ); + return { + error: `Batch insert of ${entityName} might have partially failed or returned unexpected data.`, + status: 500, // Or a more specific error + }; + } + + console.log( + `Successfully batch inserted ${newRecordsTyped.length} ${entityName} records.`, + ); + return { data: newRecordsTyped, status: 201 }; +} + +export async function validateAndInsertBatch< + TableName extends keyof Database["public"]["Tables"], +>( + supabase: SupabaseClient, + items: TablesInsert[], + tableName: string, + selectQuery: string, // e.g., "id, field1, field2" or "*" + entityName: string = tableName, // For logging + inputValidator: ItemValidator> | null, + outputValidator: ItemValidator> | null, +): Promise>> { + let validatedItems: TablesInsert[] = []; + const validationErrors: { index: number; error: string }[] = []; + if (!Array.isArray(items) || items.length === 0) { + return { + error: `Request body must be a non-empty array of ${entityName} items.`, + status: 400, + }; + } + + if (inputValidator !== null) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (!item) { + // Handles undefined/null items in the array itself + validationErrors.push({ + index: i, + error: "Item is undefined or null.", + }); + continue; + } + const error = inputValidator(item); + if (error !== null) { + validationErrors.push({ + index: i, + error: error || "Validation failed.", + }); + } else { + validatedItems.push(item); + } + } + + if (validationErrors.length > 0) { + return { + error: `Validation failed for one or more ${entityName} items.`, + partial_errors: validationErrors, + status: 400, + }; + } + } else { + validatedItems = items; + } + const result = await InsertValidatedBatch( + supabase, + validatedItems, + tableName, + selectQuery, + entityName, + ); + if (result.error || !result.data) { + return result; + } + if (outputValidator !== null) { + const validatedResults: Tables[] = []; + for (let i = 0; i < result.data.length; i++) { + const item = result.data[i]; + if (!item) { + // Handles undefined/null items in the array itself + validationErrors.push({ + index: i, + error: "Returned item is undefined or null.", + }); + continue; + } + const error = outputValidator(item); + if (error !== null) { + validationErrors.push({ + index: i, + error: error || "Validation failed.", + }); + } else { + validatedResults.push(item); + } + } + + if (validationErrors.length > 0) { + return { + error: `Validation failed for one or more ${entityName} results.`, + partial_errors: validationErrors, + status: 500, + }; + } + } + return result; +} + +export async function processAndInsertBatch< + TableName extends keyof Database["public"]["Tables"], + InputType, + OutputType, +>( + supabase: SupabaseClient, + items: InputType[], + tableName: string, + selectQuery: string, // e.g., "id, field1, field2" or "*" + entityName: string = tableName, // For logging + inputProcessor: ItemProcessor>, + outputProcessor: ItemProcessor, OutputType>, +): Promise> { + let processedItems: TablesInsert[] = []; + const validationErrors: { index: number; error: string }[] = []; + if (!Array.isArray(items) || items.length === 0) { + return { + error: `Request body must be a non-empty array of ${entityName} items.`, + status: 400, + }; + } + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (!item) { + // Handles undefined/null items in the array itself + validationErrors.push({ + index: i, + error: "Item is undefined or null.", + }); + continue; + } + const { valid, error, processedItem } = inputProcessor(item); + if (!valid || !processedItem) { + validationErrors.push({ + index: i, + error: error || "Validation failed.", + }); + } else { + processedItems.push(processedItem); + } + } + + if (validationErrors.length > 0) { + return { + error: `Validation failed for one or more ${entityName} items.`, + partial_errors: validationErrors, + status: 400, + }; + } + const result = await InsertValidatedBatch( + supabase, + processedItems, + tableName, + selectQuery, + entityName, + ); + if (result.error || !result.data) { + return { ...result, data: [] }; + } + const processedResults: OutputType[] = []; + for (let i = 0; i < result.data.length; i++) { + const item = result.data[i]; + if (!item) { + // Handles undefined/null items in the array itself + validationErrors.push({ + index: i, + error: "Returned item is undefined or null.", + }); + continue; + } + const { valid, error, processedItem } = outputProcessor(item); + if (!valid || !processedItem) { + validationErrors.push({ + index: i, + error: error || "Result validation failed.", + }); + } else { + processedResults.push(processedItem); + } + } + + if (validationErrors.length > 0) { + return { + error: `Validation failed for one or more ${entityName} results.`, + partial_errors: validationErrors, + status: 500, + }; + } + return { ...result, data: processedResults }; +} diff --git a/apps/website/app/utils/supabase/server.ts b/apps/website/app/utils/supabase/server.ts new file mode 100644 index 000000000..1434dbebf --- /dev/null +++ b/apps/website/app/utils/supabase/server.ts @@ -0,0 +1,44 @@ +import { createServerClient, type CookieOptions } from "@supabase/ssr"; +import { cookies } from "next/headers"; +import { Database } from "~/utils/supabase/types.gen"; + +export async function createClient() { + const cookieStore = await cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll( + cookiesToSet: { + name: string; + value: string; + options: CookieOptions; + }[], + ) { + try { + cookiesToSet.forEach( + ({ + name, + value, + options, + }: { + name: string; + value: string; + options: CookieOptions; + }) => cookieStore.set(name, value, options), + ); + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }, + ); +} diff --git a/apps/website/app/utils/supabase/types.gen.ts b/apps/website/app/utils/supabase/types.gen.ts new file mode 100644 index 000000000..a3981b363 --- /dev/null +++ b/apps/website/app/utils/supabase/types.gen.ts @@ -0,0 +1,803 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + public: { + Tables: { + Account: { + Row: { + active: boolean + id: number + person_id: number + platform_id: number + write_permission: boolean + } + Insert: { + active?: boolean + id?: number + person_id: number + platform_id: number + write_permission: boolean + } + Update: { + active?: boolean + id?: number + person_id?: number + platform_id?: number + write_permission?: boolean + } + Relationships: [ + { + foreignKeyName: "Account_person_id_fkey" + columns: ["person_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Account_platform_id_fkey" + columns: ["platform_id"] + isOneToOne: false + referencedRelation: "Platform" + referencedColumns: ["id"] + }, + ] + } + Agent: { + Row: { + id: number + type: Database["public"]["Enums"]["EntityType"] + } + Insert: { + id?: number + type: Database["public"]["Enums"]["EntityType"] + } + Update: { + id?: number + type?: Database["public"]["Enums"]["EntityType"] + } + Relationships: [] + } + AutomatedAgent: { + Row: { + deterministic: boolean | null + id: number + metadata: Json + name: string + version: string | null + } + Insert: { + deterministic?: boolean | null + id: number + metadata?: Json + name: string + version?: string | null + } + Update: { + deterministic?: boolean | null + id?: number + metadata?: Json + name?: string + version?: string | null + } + Relationships: [ + { + foreignKeyName: "automated_agent_id_fkey" + columns: ["id"] + isOneToOne: true + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + ] + } + Concept: { + Row: { + arity: number + author_id: number | null + content: Json + created: string + description: string | null + epistemic_status: Database["public"]["Enums"]["EpistemicStatus"] + id: number + is_schema: boolean + last_modified: string + name: string + represented_by_id: number | null + schema_id: number | null + space_id: number | null + } + Insert: { + arity?: number + author_id?: number | null + content?: Json + created: string + description?: string | null + epistemic_status?: Database["public"]["Enums"]["EpistemicStatus"] + id?: number + is_schema?: boolean + last_modified: string + name: string + represented_by_id?: number | null + schema_id?: number | null + space_id?: number | null + } + Update: { + arity?: number + author_id?: number | null + content?: Json + created?: string + description?: string | null + epistemic_status?: Database["public"]["Enums"]["EpistemicStatus"] + id?: number + is_schema?: boolean + last_modified?: string + name?: string + represented_by_id?: number | null + schema_id?: number | null + space_id?: number | null + } + Relationships: [ + { + foreignKeyName: "Concept_author_id_fkey" + columns: ["author_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Concept_represented_by_id_fkey" + columns: ["represented_by_id"] + isOneToOne: false + referencedRelation: "Content" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Concept_schema_id_fkey" + columns: ["schema_id"] + isOneToOne: false + referencedRelation: "Concept" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Concept_space_id_fkey" + columns: ["space_id"] + isOneToOne: false + referencedRelation: "Space" + referencedColumns: ["id"] + }, + ] + } + concept_contributors: { + Row: { + concept_id: number + contributor_id: number + } + Insert: { + concept_id: number + contributor_id: number + } + Update: { + concept_id?: number + contributor_id?: number + } + Relationships: [ + { + foreignKeyName: "concept_contributors_concept_id_fkey" + columns: ["concept_id"] + isOneToOne: false + referencedRelation: "Concept" + referencedColumns: ["id"] + }, + { + foreignKeyName: "concept_contributors_contributor_id_fkey" + columns: ["contributor_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + ] + } + Content: { + Row: { + author_id: number | null + created: string + creator_id: number | null + document_id: number + id: number + last_modified: string + metadata: Json + part_of_id: number | null + scale: Database["public"]["Enums"]["Scale"] + source_local_id: string | null + space_id: number | null + text: string + } + Insert: { + author_id?: number | null + created: string + creator_id?: number | null + document_id: number + id?: number + last_modified: string + metadata?: Json + part_of_id?: number | null + scale: Database["public"]["Enums"]["Scale"] + source_local_id?: string | null + space_id?: number | null + text: string + } + Update: { + author_id?: number | null + created?: string + creator_id?: number | null + document_id?: number + id?: number + last_modified?: string + metadata?: Json + part_of_id?: number | null + scale?: Database["public"]["Enums"]["Scale"] + source_local_id?: string | null + space_id?: number | null + text?: string + } + Relationships: [ + { + foreignKeyName: "Content_author_id_fkey" + columns: ["author_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Content_creator_id_fkey" + columns: ["creator_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Content_document_id_fkey" + columns: ["document_id"] + isOneToOne: false + referencedRelation: "Document" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Content_part_of_id_fkey" + columns: ["part_of_id"] + isOneToOne: false + referencedRelation: "Content" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Content_space_id_fkey" + columns: ["space_id"] + isOneToOne: false + referencedRelation: "Space" + referencedColumns: ["id"] + }, + ] + } + content_contributors: { + Row: { + content_id: number + contributor_id: number + } + Insert: { + content_id: number + contributor_id: number + } + Update: { + content_id?: number + contributor_id?: number + } + Relationships: [ + { + foreignKeyName: "content_contributors_content_id_fkey" + columns: ["content_id"] + isOneToOne: false + referencedRelation: "Content" + referencedColumns: ["id"] + }, + { + foreignKeyName: "content_contributors_contributor_id_fkey" + columns: ["contributor_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + ] + } + ContentEmbedding_openai_text_embedding_3_small_1536: { + Row: { + model: Database["public"]["Enums"]["EmbeddingName"] + obsolete: boolean | null + target_id: number + vector: string + } + Insert: { + model?: Database["public"]["Enums"]["EmbeddingName"] + obsolete?: boolean | null + target_id: number + vector: string + } + Update: { + model?: Database["public"]["Enums"]["EmbeddingName"] + obsolete?: boolean | null + target_id?: number + vector?: string + } + Relationships: [ + { + foreignKeyName: "ContentEmbedding_openai_text_embedding_3_small_1_target_id_fkey" + columns: ["target_id"] + isOneToOne: true + referencedRelation: "Content" + referencedColumns: ["id"] + }, + ] + } + Document: { + Row: { + author_id: number + contents: unknown | null + created: string + id: number + last_modified: string + metadata: Json + source_local_id: string | null + space_id: number | null + url: string | null + } + Insert: { + author_id: number + contents?: unknown | null + created: string + id?: number + last_modified: string + metadata?: Json + source_local_id?: string | null + space_id?: number | null + url?: string | null + } + Update: { + author_id?: number + contents?: unknown | null + created?: string + id?: number + last_modified?: string + metadata?: Json + source_local_id?: string | null + space_id?: number | null + url?: string | null + } + Relationships: [ + { + foreignKeyName: "Document_author_id_fkey" + columns: ["author_id"] + isOneToOne: false + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + { + foreignKeyName: "Document_space_id_fkey" + columns: ["space_id"] + isOneToOne: false + referencedRelation: "Space" + referencedColumns: ["id"] + }, + ] + } + Person: { + Row: { + email: string + id: number + name: string + orcid: string | null + } + Insert: { + email: string + id: number + name: string + orcid?: string | null + } + Update: { + email?: string + id?: number + name?: string + orcid?: string | null + } + Relationships: [ + { + foreignKeyName: "person_id_fkey" + columns: ["id"] + isOneToOne: true + referencedRelation: "Agent" + referencedColumns: ["id"] + }, + ] + } + Platform: { + Row: { + id: number + name: string + url: string + } + Insert: { + id?: number + name: string + url: string + } + Update: { + id?: number + name?: string + url?: string + } + Relationships: [] + } + Space: { + Row: { + id: number + name: string + platform_id: number + url: string | null + } + Insert: { + id?: number + name: string + platform_id: number + url?: string | null + } + Update: { + id?: number + name?: string + platform_id?: number + url?: string | null + } + Relationships: [ + { + foreignKeyName: "Space_platform_id_fkey" + columns: ["platform_id"] + isOneToOne: false + referencedRelation: "Platform" + referencedColumns: ["id"] + }, + ] + } + SpaceAccess: { + Row: { + account_id: number + editor: boolean + id: number + space_id: number | null + } + Insert: { + account_id: number + editor: boolean + id?: number + space_id?: number | null + } + Update: { + account_id?: number + editor?: boolean + id?: number + space_id?: number | null + } + Relationships: [ + { + foreignKeyName: "SpaceAccess_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "Account" + referencedColumns: ["id"] + }, + { + foreignKeyName: "SpaceAccess_space_id_fkey" + columns: ["space_id"] + isOneToOne: false + referencedRelation: "Space" + referencedColumns: ["id"] + }, + ] + } + sync_info: { + Row: { + failure_count: number | null + id: number + last_task_end: string | null + last_task_start: string | null + status: Database["public"]["Enums"]["task_status"] | null + sync_function: string | null + sync_target: number | null + task_times_out_at: string | null + worker: string + } + Insert: { + failure_count?: number | null + id?: number + last_task_end?: string | null + last_task_start?: string | null + status?: Database["public"]["Enums"]["task_status"] | null + sync_function?: string | null + sync_target?: number | null + task_times_out_at?: string | null + worker: string + } + Update: { + failure_count?: number | null + id?: number + last_task_end?: string | null + last_task_start?: string | null + status?: Database["public"]["Enums"]["task_status"] | null + sync_function?: string | null + sync_target?: number | null + task_times_out_at?: string | null + worker?: string + } + Relationships: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + end_sync_task: { + Args: { + s_target: number + s_function: string + s_worker: string + s_status: Database["public"]["Enums"]["task_status"] + } + Returns: undefined + } + get_nodes_needing_sync: { + Args: { nodes_from_roam: Json } + Returns: { + uid_to_sync: string + }[] + } + match_content_embeddings: { + Args: { + query_embedding: string + match_threshold: number + match_count: number + current_document_id?: number + } + Returns: { + content_id: number + roam_uid: string + text_content: string + similarity: number + }[] + } + match_embeddings_for_subset_nodes: { + Args: { p_query_embedding: string; p_subset_roam_uids: string[] } + Returns: { + content_id: number + roam_uid: string + text_content: string + similarity: number + }[] + } + propose_sync_task: { + Args: { + s_target: number + s_function: string + s_worker: string + timeout: unknown + task_interval: unknown + } + Returns: unknown + } + } + Enums: { + EmbeddingName: + | "openai_text_embedding_ada2_1536" + | "openai_text_embedding_3_small_512" + | "openai_text_embedding_3_small_1536" + | "openai_text_embedding_3_large_256" + | "openai_text_embedding_3_large_1024" + | "openai_text_embedding_3_large_3072" + EntityType: + | "Platform" + | "Space" + | "Account" + | "Person" + | "AutomatedAgent" + | "Document" + | "Content" + | "Concept" + | "ConceptSchema" + | "ContentLink" + | "Occurrence" + EpistemicStatus: + | "certainly_not" + | "strong_evidence_against" + | "could_be_false" + | "unknown" + | "uncertain" + | "contentious" + | "could_be_true" + | "strong_evidence_for" + | "certain" + Scale: + | "document" + | "post" + | "chunk_unit" + | "section" + | "block" + | "field" + | "paragraph" + | "quote" + | "sentence" + | "phrase" + task_status: "active" | "timeout" | "complete" | "failed" + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type DefaultSchema = Database[Extract] + +export type Tables< + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & + DefaultSchema["Views"]) + ? (DefaultSchema["Tables"] & + DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + DefaultSchemaEnumNameOrOptions extends + | keyof DefaultSchema["Enums"] + | { schema: keyof Database }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] + ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema["CompositeTypes"] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] + ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never + +export const Constants = { + public: { + Enums: { + EmbeddingName: [ + "openai_text_embedding_ada2_1536", + "openai_text_embedding_3_small_512", + "openai_text_embedding_3_small_1536", + "openai_text_embedding_3_large_256", + "openai_text_embedding_3_large_1024", + "openai_text_embedding_3_large_3072", + ], + EntityType: [ + "Platform", + "Space", + "Account", + "Person", + "AutomatedAgent", + "Document", + "Content", + "Concept", + "ConceptSchema", + "ContentLink", + "Occurrence", + ], + EpistemicStatus: [ + "certainly_not", + "strong_evidence_against", + "could_be_false", + "unknown", + "uncertain", + "contentious", + "could_be_true", + "strong_evidence_for", + "certain", + ], + Scale: [ + "document", + "post", + "chunk_unit", + "section", + "block", + "field", + "paragraph", + "quote", + "sentence", + "phrase", + ], + task_status: ["active", "timeout", "complete", "failed"], + }, + }, +} as const + diff --git a/apps/website/package.json b/apps/website/package.json index cb202fc63..7c4b40d38 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -13,6 +13,7 @@ "@repo/types": "*", "@repo/ui": "*", "@sindresorhus/slugify": "^2.2.1", + "@supabase/ssr": "^0.6.1", "gray-matter": "^4.0.3", "next": "^15.0.3", "openai": "^4.97.0", diff --git a/package-lock.json b/package-lock.json index 8d676b83c..3e46c8126 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6744,26 +6744,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "apps/roam/node_modules/ws": { - "version": "8.14.2", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "apps/roam/node_modules/xdg-basedir": { "version": "4.0.0", "license": "MIT", @@ -6907,6 +6887,7 @@ "@repo/types": "*", "@repo/ui": "*", "@sindresorhus/slugify": "^2.2.1", + "@supabase/ssr": "^0.6.1", "gray-matter": "^4.0.3", "next": "^15.0.3", "openai": "^4.97.0", @@ -9029,6 +9010,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@supabase/auth-js": { + "version": "2.69.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", + "integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==", + "peer": true, + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "peer": true, + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", + "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "peer": true, + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "peer": true, + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz", + "integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==", + "dependencies": { + "cookie": "^1.0.1" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "peer": true, + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.49.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz", + "integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==", + "peer": true, + "dependencies": { + "@supabase/auth-js": "2.69.1", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.4", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -9301,6 +9367,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "peer": true + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -9367,6 +9439,15 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", @@ -10950,6 +11031,14 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/core-js": { "version": "3.39.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", @@ -20021,6 +20110,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",