From 2cde6c4a8f6b8cbcc6c5d29fc0e8ecd9bd0e6492 Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 21:09:35 +0530 Subject: [PATCH 01/20] Install typesense and lodash.debounce packages --- bun.lock | 11 +++++++++++ package.json | 3 +++ 2 files changed, 14 insertions(+) diff --git a/bun.lock b/bun.lock index cdd6554..d0f23f4 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "js-cookie": "^3.0.5", + "lodash.debounce": "^4.0.8", "lucide-react": "^0.446.0", "marked": "^15.0.6", "next": "14.2.13", @@ -78,12 +79,14 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "ts-jest": "^29.3.2", + "typesense": "^2.0.3", "zod": "^3.24.3", }, "devDependencies": { "@radix-ui/react-label": "^2.1.0", "@tailwindcss/typography": "^0.5.15", "@types/codemirror": "^5.60.15", + "@types/lodash.debounce": "^4.0.9", "@types/node": "^20", "@types/nodemailer": "^6.4.17", "@types/pg": "^8.11.10", @@ -970,6 +973,10 @@ "@types/koa__router": ["@types/koa__router@12.0.3", "", { "dependencies": { "@types/koa": "*" } }, "sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw=="], + "@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="], + + "@types/lodash.debounce": ["@types/lodash.debounce@4.0.9", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/memcached": ["@types/memcached@2.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg=="], @@ -2120,6 +2127,8 @@ "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], + "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], + "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -2834,6 +2843,8 @@ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typesense": ["typesense@2.0.3", "", { "dependencies": { "axios": "^1.7.2", "loglevel": "^1.8.1", "tslib": "^2.6.2" }, "peerDependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-fRJjFdDNZn6qF9XzIk+bB8n8cm0fiAx1SGcpLDfNcsGtp8znITfG+SO+l/qk63GCRXZwJGq7wrMDLFUvblJSHA=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], diff --git a/package.json b/package.json index 214e661..6a1839f 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "js-cookie": "^3.0.5", + "lodash.debounce": "^4.0.8", "lucide-react": "^0.446.0", "marked": "^15.0.6", "next": "14.2.13", @@ -97,12 +98,14 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "ts-jest": "^29.3.2", + "typesense": "^2.0.3", "zod": "^3.24.3" }, "devDependencies": { "@radix-ui/react-label": "^2.1.0", "@tailwindcss/typography": "^0.5.15", "@types/codemirror": "^5.60.15", + "@types/lodash.debounce": "^4.0.9", "@types/node": "^20", "@types/nodemailer": "^6.4.17", "@types/pg": "^8.11.10", From 16895804649bc505a1ae5accdd92fbbbf9a71baf Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 21:10:16 +0530 Subject: [PATCH 02/20] Setup typesense client and connection tester --- lib/typesense/client.ts | 84 ++++++++++++++++++++++++++++++++ lib/typesense/test-connection.ts | 13 +++++ 2 files changed, 97 insertions(+) create mode 100644 lib/typesense/client.ts create mode 100644 lib/typesense/test-connection.ts diff --git a/lib/typesense/client.ts b/lib/typesense/client.ts new file mode 100644 index 0000000..7801423 --- /dev/null +++ b/lib/typesense/client.ts @@ -0,0 +1,84 @@ +import Typesense from "typesense"; +import { config } from "dotenv"; + +config({ path: ".env.local" }); +let typesenseClient: Typesense.Client; + +export function getTypesenseClient() { + if (!typesenseClient) { + typesenseClient = new Typesense.Client({ + nodes: [ + { + host: process.env.TYPESENSE_HOST!, + port: parseInt(process.env.TYPESENSE_PORT!, 10), + protocol: process.env.TYPESENSE_PROTOCOL!, + }, + ], + apiKey: process.env.TYPESENSE_API_KEY!, + connectionTimeoutSeconds: 2, + retryIntervalSeconds: 0.1, + numRetries: 3, + }); + } + + return typesenseClient; +} + +// Helper function to handle search errors +export async function safeSearch( + searchFn: () => Promise, + fallback: T, +): Promise { + try { + return await searchFn(); + } catch (error) { + console.error("Typesense search error:", error); + return fallback; + } +} + +// Types for common search parameters +export interface SearchParams { + q: string; + query_by: string; + filter_by?: string; + sort_by?: string; + page?: number; + per_page?: number; + facet_by?: string; + max_facet_values?: number; +} + +// Common search result interface +export interface SearchResponse { + found: number; + hits: Array<{ + document: T; + highlights: Array<{ + field: string; + snippet: string; + }>; + }>; + facet_counts?: Array<{ + field_name: string; + counts: Array<{ + count: number; + value: string; + }>; + }>; + page: number; +} + +// Helper function to create search parameters +export function createSearchParams( + query: string, + queryBy: string[], + options: Partial = {}, +): SearchParams { + return { + q: query, + query_by: queryBy.join(","), + per_page: 10, + ...options, + }; +} diff --git a/lib/typesense/test-connection.ts b/lib/typesense/test-connection.ts new file mode 100644 index 0000000..dea4e31 --- /dev/null +++ b/lib/typesense/test-connection.ts @@ -0,0 +1,13 @@ +import { getTypesenseClient } from "./client"; + +export async function testTypesenseConnection() { + try { + const client = getTypesenseClient(); + const health = await client.health.retrieve(); + console.log("Typesense connection successful:", health); + return true; + } catch (error) { + console.error("Typesense connection failed:", error); + return false; + } +} From de3605724d0c555c114e5e54ae743f27502ec4f8 Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 21:10:37 +0530 Subject: [PATCH 03/20] Define a GET route for testing typesense connection --- app/api/typesense/health/route.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/api/typesense/health/route.ts diff --git a/app/api/typesense/health/route.ts b/app/api/typesense/health/route.ts new file mode 100644 index 0000000..0521b6f --- /dev/null +++ b/app/api/typesense/health/route.ts @@ -0,0 +1,15 @@ +import { testTypesenseConnection } from "@/lib/typesense/test-connection"; +import { NextResponse } from "next/server"; + +export async function GET() { + const isHealthy = await testTypesenseConnection(); + + if (!isHealthy) { + return NextResponse.json( + { error: "Typesense connection failed" }, + { status: 500 }, + ); + } + + return NextResponse.json({ status: "healthy" }); +} From 60d630dec68fb113e2ca69d1c42d771cf132b57a Mon Sep 17 00:00:00 2001 From: virinci Date: Sun, 4 May 2025 21:56:40 +0530 Subject: [PATCH 04/20] Add a field for querying by weights in typesense --- lib/typesense/client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/typesense/client.ts b/lib/typesense/client.ts index 7801423..e4610d5 100644 --- a/lib/typesense/client.ts +++ b/lib/typesense/client.ts @@ -41,6 +41,7 @@ export async function safeSearch( export interface SearchParams { q: string; query_by: string; + query_by_weights?: string; filter_by?: string; sort_by?: string; page?: number; @@ -73,11 +74,13 @@ export interface SearchResponse { export function createSearchParams( query: string, queryBy: string[], + weights?: number[], options: Partial = {}, ): SearchParams { return { q: query, query_by: queryBy.join(","), + query_by_weights: weights?.join(","), per_page: 10, ...options, }; From 0cc86e5e042280b314f11f86dbc050483bfbb6ef Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 21:58:44 +0530 Subject: [PATCH 05/20] Define typesense collections for entity database schemas --- lib/typesense/collections/contests.ts | 81 +++++++++++++++ lib/typesense/collections/index.ts | 25 +++++ lib/typesense/collections/orgs.ts | 71 +++++++++++++ lib/typesense/collections/problems.ts | 143 ++++++++++++++++++++++++++ lib/typesense/collections/users.ts | 72 +++++++++++++ 5 files changed, 392 insertions(+) create mode 100644 lib/typesense/collections/contests.ts create mode 100644 lib/typesense/collections/index.ts create mode 100644 lib/typesense/collections/orgs.ts create mode 100644 lib/typesense/collections/problems.ts create mode 100644 lib/typesense/collections/users.ts diff --git a/lib/typesense/collections/contests.ts b/lib/typesense/collections/contests.ts new file mode 100644 index 0000000..2d6ed67 --- /dev/null +++ b/lib/typesense/collections/contests.ts @@ -0,0 +1,81 @@ +import { + createSearchParams, + getTypesenseClient, + type SearchParams, + type SearchResponse, +} from "../client"; +import { type SelectContest } from "@/db/schema"; + +export const CONTESTS_SCHEMA = { + name: "contests", + fields: [ + { name: "id", type: "int32" }, + { name: "name_id", type: "string" }, + { name: "name", type: "string" }, + { name: "description", type: "string" }, + { name: "rules", type: "string" }, + { name: "organizer_id", type: "int32" }, + { name: "start_time", type: "int64" }, + { name: "end_time", type: "int64" }, + ], + default_sorting_field: "start_time", +}; + +export interface ContestDocument { + id: number; + name_id: string; + name: string; + description: string; + rules: string; + organizer_id: number; + start_time: number; + end_time: number; +} + +export function contestToDocument(contest: SelectContest): ContestDocument { + return { + id: contest.id, + name_id: contest.nameId, + name: contest.name, + description: contest.description, + rules: contest.rules, + organizer_id: contest.organizerId, + start_time: contest.startTime.getTime(), + end_time: contest.endTime.getTime(), + }; +} + +export async function searchContests( + query: string, + options: Partial = {}, +): Promise> { + const client = getTypesenseClient(); + + const searchParameters = createSearchParams( + query, + ["name", "name_id", "description"], + [3, 2, 1], + { + per_page: options.per_page || 10, + page: options.page || 1, + ...options, + }, + ); + + return await client + .collections("contests") + .documents() + .search(searchParameters); +} + +export async function upsertContest(contest: SelectContest) { + const client = getTypesenseClient(); + const document = contestToDocument(contest); + + return await client.collections("contests").documents().upsert(document); +} + +export async function deleteContest(id: number) { + const client = getTypesenseClient(); + return await client.collections("contests").documents(id.toString()).delete(); +} diff --git a/lib/typesense/collections/index.ts b/lib/typesense/collections/index.ts new file mode 100644 index 0000000..f945d17 --- /dev/null +++ b/lib/typesense/collections/index.ts @@ -0,0 +1,25 @@ +export * from "./problems"; +export * from "./contests"; +export * from "./users"; +export * from "./orgs"; + +import { getTypesenseClient } from "../client"; +import { PROBLEMS_SCHEMA } from "./problems"; +import { CONTESTS_SCHEMA } from "./contests"; +import { USERS_SCHEMA } from "./users"; +import { ORGS_SCHEMA } from "./orgs"; + +export async function initializeCollections() { + const client = getTypesenseClient(); + + const schemas = [PROBLEMS_SCHEMA, CONTESTS_SCHEMA, USERS_SCHEMA, ORGS_SCHEMA]; + + for (const schema of schemas) { + try { + await client.collections(schema.name).delete(); + } catch (error) { + // Collection might not exist, ignore + } + await client.collections().create(schema); + } +} diff --git a/lib/typesense/collections/orgs.ts b/lib/typesense/collections/orgs.ts new file mode 100644 index 0000000..313c03b --- /dev/null +++ b/lib/typesense/collections/orgs.ts @@ -0,0 +1,71 @@ +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; +import { SelectOrg } from "@/db/schema"; + +export const ORGS_SCHEMA = { + name: "orgs", + fields: [ + { name: "id", type: "int32" }, + { name: "name_id", type: "string" }, + { name: "name", type: "string" }, + { name: "about", type: "string", optional: true }, + { name: "avatar", type: "string", optional: true }, + { name: "created_at", type: "int64" }, + ], + default_sorting_field: "created_at", +}; + +export interface OrgDocument { + id: number; + name_id: string; + name: string; + about?: string; + avatar?: string; + created_at: number; +} + +export function orgToDocument(org: SelectOrg): OrgDocument { + return { + id: org.id, + name_id: org.nameId, + name: org.name, + about: org.about || undefined, + avatar: org.avatar || undefined, + created_at: org.createdAt.getTime(), + }; +} + +export async function searchOrgs( + query: string, + options: Partial = {}, +): Promise> { + const client = getTypesenseClient(); + + const searchParameters = createSearchParams( + query, + ["name", "name_id", "about"], + [3, 2, 1], + { + per_page: options.per_page || 10, + page: options.page || 1, + ...options, + }, + ); + + return await client.collections("orgs").documents().search(searchParameters); +} + +export async function upsertOrg(org: SelectOrg) { + const client = getTypesenseClient(); + const document = orgToDocument(org); + return await client.collections("orgs").documents().upsert(document); +} + +export async function deleteOrg(id: number) { + const client = getTypesenseClient(); + return await client.collections("orgs").documents(id.toString()).delete(); +} diff --git a/lib/typesense/collections/problems.ts b/lib/typesense/collections/problems.ts new file mode 100644 index 0000000..28ca311 --- /dev/null +++ b/lib/typesense/collections/problems.ts @@ -0,0 +1,143 @@ +import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { SelectProblem } from "@/db/schema"; +import { getRedis, CACHE_KEYS, CACHE_TTL } from "@/db/redis"; + +export const PROBLEMS_SCHEMA = { + name: "problems", + fields: [ + { name: "id", type: "int32" }, + { name: "code", type: "string" }, + { name: "title", type: "string" }, + { name: "description", type: "string" }, + { name: "allowed_languages", type: "string[]" }, + { name: "org_id", type: "int32" }, + { name: "created_at", type: "int64" }, + ], + default_sorting_field: "created_at", +}; + +export interface ProblemDocument { + id: number; + code: string; + title: string; + description: string; + allowed_languages: string[]; + org_id: number; + created_at: number; +} + +export function problemToDocument(problem: SelectProblem): ProblemDocument { + return { + id: problem.id, + code: problem.code, + title: problem.title, + description: problem.description, + allowed_languages: problem.allowedLanguages, + org_id: problem.orgId, + created_at: problem.createdAt.getTime(), + }; +} + +export async function searchProblems( + query: string, + options: Partial = {}, +): Promise> { + const redis = getRedis(); + const cacheKey = `${CACHE_KEYS.PROBLEM}search:${query}:${JSON.stringify(options)}`; + + // Try to get from cache first + const cached = await redis.get(cacheKey); + if (cached) { + return JSON.parse(cached); + } + + const client = getTypesenseClient(); + + const searchParameters = createSearchParams( + query, + ["title", "code", "description"], + [4, 2, 1], + { + per_page: options.per_page || 10, + page: options.page || 1, + ...options, + }, + ); + const searchParameters = createSearchParams( + query, + ["title", "code", "description"], + [4, 2, 1], + { + per_page: options.per_page || 10, + page: options.page || 1, + filter_by: options.filter_by, + sort_by: options.sort_by || "_text_match:desc,created_at:desc", + ...options, + }, + ); + + const results = await client + .collections("problems") + .documents() + .search(searchParameters); + + // Cache results + await redis.set( + cacheKey, + JSON.stringify(results), + "EX", + CACHE_TTL.MEDIUM, // Cache for 5 minutes + ); + + return results; +} + +export async function upsertProblem(problem: SelectProblem) { + const client = getTypesenseClient(); + const document = problemToDocument(problem); + + // Clear search cache when updating problems + const redis = getRedis(); + const keys = await redis.keys(`${CACHE_KEYS.PROBLEM}search:*`); + if (keys.length > 0) { + await redis.del(keys); + } + + return await client.collections("problems").documents().upsert(document); +} + +export async function deleteProblem(id: number) { + const client = getTypesenseClient(); + + // Clear search cache when deleting problems + const redis = getRedis(); + const keys = await redis.keys(`${CACHE_KEYS.PROBLEM}search:*`); + if (keys.length > 0) { + await redis.del(keys); + } + + return await client.collections("problems").documents(id.toString()).delete(); +} + +// Batch operations for initial data import +export async function batchUpsertProblems(problems: SelectProblem[]) { + const client = getTypesenseClient(); + const documents = problems.map(problemToDocument); + + return await client + .collections("problems") + .documents() + .import(documents, { action: "upsert" }); +} + +// Search problems by organization +export async function searchProblemsByOrg( + query: string, + orgId: number, + options: Partial = {}, +): Promise> { + return searchProblems(query, { + ...options, + filter_by: `org_id:=${orgId}`, + }); +} diff --git a/lib/typesense/collections/users.ts b/lib/typesense/collections/users.ts new file mode 100644 index 0000000..4e2fcf8 --- /dev/null +++ b/lib/typesense/collections/users.ts @@ -0,0 +1,72 @@ +import { + createSearchParams, + getTypesenseClient, + type SearchParams, + type SearchResponse, +} from "../client"; +import { type SelectUser } from "@/db/schema"; + +export const USERS_SCHEMA = { + name: "users", + fields: [ + { name: "id", type: "int32" }, + { name: "name_id", type: "string" }, + { name: "name", type: "string" }, + { name: "about", type: "string", optional: true }, + { name: "avatar", type: "string", optional: true }, + { name: "created_at", type: "int64" }, + ], + default_sorting_field: "created_at", +}; + +export interface UserDocument { + id: number; + name_id: string; + name: string; + about?: string; + avatar?: string; + created_at: number; +} + +export function userToDocument(user: SelectUser): UserDocument { + return { + id: user.id, + name_id: user.nameId, + name: user.name, + about: user.about || undefined, + avatar: user.avatar || undefined, + created_at: user.createdAt.getTime(), + }; +} + +export async function searchUsers( + query: string, + options: Partial = {}, +): Promise> { + const client = getTypesenseClient(); + + const searchParameters = createSearchParams( + query, + ["name", "name_id", "about"], + [3, 2, 1], + { + per_page: options.per_page || 10, + page: options.page || 1, + ...options, + }, + ); + + return await client.collections("users").documents().search(searchParameters); +} + +export async function upsertUser(user: SelectUser) { + const client = getTypesenseClient(); + const document = userToDocument(user); + + return await client.collections("users").documents().upsert(document); +} + +export async function deleteUser(id: number) { + const client = getTypesenseClient(); + return await client.collections("users").documents(id.toString()).delete(); +} From 169e40fee6b13d5f9930107f096558e753b6937c Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 21:59:42 +0530 Subject: [PATCH 06/20] Implement search routes using typesense --- app/api/search/contests/route.ts | 23 +++++++++++++++++++++++ app/api/search/orgs/route.ts | 23 +++++++++++++++++++++++ app/api/search/problems/route.ts | 23 +++++++++++++++++++++++ app/api/search/users/route.ts | 23 +++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 app/api/search/contests/route.ts create mode 100644 app/api/search/orgs/route.ts create mode 100644 app/api/search/problems/route.ts create mode 100644 app/api/search/users/route.ts diff --git a/app/api/search/contests/route.ts b/app/api/search/contests/route.ts new file mode 100644 index 0000000..ca1d0f5 --- /dev/null +++ b/app/api/search/contests/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchContests } from "@/lib/typesense/collections/contests"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + + try { + const results = await safeSearch( + () => searchContests(query, { page, per_page }), + { found: 0, hits: [], page } + ); + return NextResponse.json(results); + } catch (error) { + return NextResponse.json( + { error: "Search failed" }, + { status: 500 } + ); + } +} diff --git a/app/api/search/orgs/route.ts b/app/api/search/orgs/route.ts new file mode 100644 index 0000000..df36e16 --- /dev/null +++ b/app/api/search/orgs/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchOrgs } from "@/lib/typesense/collections/orgs"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + + try { + const results = await safeSearch( + () => searchOrgs(query, { page, per_page }), + { found: 0, hits: [], page } + ); + return NextResponse.json(results); + } catch (error) { + return NextResponse.json( + { error: "Search failed" }, + { status: 500 } + ); + } +} diff --git a/app/api/search/problems/route.ts b/app/api/search/problems/route.ts new file mode 100644 index 0000000..80a7f38 --- /dev/null +++ b/app/api/search/problems/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchProblems } from "@/lib/typesense/collections/problems"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + + try { + const results = await safeSearch( + () => searchProblems(query, { page, per_page }), + { found: 0, hits: [], page } + ); + return NextResponse.json(results); + } catch (error) { + return NextResponse.json( + { error: "Search failed" }, + { status: 500 } + ); + } +} diff --git a/app/api/search/users/route.ts b/app/api/search/users/route.ts new file mode 100644 index 0000000..ad1b3fa --- /dev/null +++ b/app/api/search/users/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchUsers } from "@/lib/typesense/collections/users"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + + try { + const results = await safeSearch( + () => searchUsers(query, { page, per_page }), + { found: 0, hits: [], page } + ); + return NextResponse.json(results); + } catch (error) { + return NextResponse.json( + { error: "Search failed" }, + { status: 500 } + ); + } +} From 95a6fe53ade89d1cf500607137ae7ea424dd5dab Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 22:02:32 +0530 Subject: [PATCH 07/20] Ignore typesense-data directory from Git --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 32ea24e..3fdbbcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +typesense-data bun.lockb # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. @@ -37,4 +38,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.env \ No newline at end of file +.env From fb09e836f3f67254bd4cd09a7e7d27e30322a72e Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 22:05:09 +0530 Subject: [PATCH 08/20] Setup docker-compose.yml for typesense --- docker-compose.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9cd3189 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + typesense: + image: typesense/typesense:28.0 + ports: + - "8108:8108" + volumes: + - ./typesense-data:/data + environment: + - TYPESENSE_API_KEY=your_api_key_here + - TYPESENSE_DATA_DIR=/data + - TYPESENSE_ENABLE_CORS=true From 5cee8ed00306c4c6fd3dc8c5026a2c332d6444c4 Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 22:05:49 +0530 Subject: [PATCH 09/20] Define sync hooks for all the typesense schemas --- lib/typesense/sync.ts | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 lib/typesense/sync.ts diff --git a/lib/typesense/sync.ts b/lib/typesense/sync.ts new file mode 100644 index 0000000..ecd1f55 --- /dev/null +++ b/lib/typesense/sync.ts @@ -0,0 +1,110 @@ +import { + SelectProblem, + SelectContest, + SelectUser, + SelectOrg, +} from "@/db/schema"; +import { upsertProblem, deleteProblem } from "./collections/problems"; +import { upsertContest, deleteContest } from "./collections/contests"; +import { upsertUser, deleteUser } from "./collections/users"; +import { upsertOrg, deleteOrg } from "./collections/orgs"; + +// Problem sync hooks +export async function syncProblemCreate(problem: SelectProblem) { + try { + await upsertProblem(problem); + } catch (error) { + console.error("Failed to sync problem creation:", error); + } +} + +export async function syncProblemUpdate(problem: SelectProblem) { + try { + await upsertProblem(problem); + } catch (error) { + console.error("Failed to sync problem update:", error); + } +} + +export async function syncProblemDelete(id: number) { + try { + await deleteProblem(id); + } catch (error) { + console.error("Failed to sync problem deletion:", error); + } +} + +// Contest sync hooks +export async function syncContestCreate(contest: SelectContest) { + try { + await upsertContest(contest); + } catch (error) { + console.error("Failed to sync contest creation:", error); + } +} + +export async function syncContestUpdate(contest: SelectContest) { + try { + await upsertContest(contest); + } catch (error) { + console.error("Failed to sync contest update:", error); + } +} + +export async function syncContestDelete(id: number) { + try { + await deleteContest(id); + } catch (error) { + console.error("Failed to sync contest deletion:", error); + } +} + +// User sync hooks +export async function syncUserCreate(user: SelectUser) { + try { + await upsertUser(user); + } catch (error) { + console.error("Failed to sync user creation:", error); + } +} + +export async function syncUserUpdate(user: SelectUser) { + try { + await upsertUser(user); + } catch (error) { + console.error("Failed to sync user update:", error); + } +} + +export async function syncUserDelete(id: number) { + try { + await deleteUser(id); + } catch (error) { + console.error("Failed to sync user deletion:", error); + } +} + +// Organization sync hooks +export async function syncOrgCreate(org: SelectOrg) { + try { + await upsertOrg(org); + } catch (error) { + console.error("Failed to sync organization creation:", error); + } +} + +export async function syncOrgUpdate(org: SelectOrg) { + try { + await upsertOrg(org); + } catch (error) { + console.error("Failed to sync organization update:", error); + } +} + +export async function syncOrgDelete(id: number) { + try { + await deleteOrg(id); + } catch (error) { + console.error("Failed to sync organization deletion:", error); + } +} From dca484c45791288430bb69f31e37be26539589cf Mon Sep 17 00:00:00 2001 From: myselfaryan Date: Sun, 4 May 2025 22:07:32 +0530 Subject: [PATCH 10/20] Format code --- app/api/search/contests/route.ts | 7 ++----- app/api/search/orgs/route.ts | 7 ++----- app/api/search/problems/route.ts | 7 ++----- app/api/search/users/route.ts | 7 ++----- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/api/search/contests/route.ts b/app/api/search/contests/route.ts index ca1d0f5..19e6953 100644 --- a/app/api/search/contests/route.ts +++ b/app/api/search/contests/route.ts @@ -11,13 +11,10 @@ export async function GET(request: NextRequest) { try { const results = await safeSearch( () => searchContests(query, { page, per_page }), - { found: 0, hits: [], page } + { found: 0, hits: [], page }, ); return NextResponse.json(results); } catch (error) { - return NextResponse.json( - { error: "Search failed" }, - { status: 500 } - ); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); } } diff --git a/app/api/search/orgs/route.ts b/app/api/search/orgs/route.ts index df36e16..2755d03 100644 --- a/app/api/search/orgs/route.ts +++ b/app/api/search/orgs/route.ts @@ -11,13 +11,10 @@ export async function GET(request: NextRequest) { try { const results = await safeSearch( () => searchOrgs(query, { page, per_page }), - { found: 0, hits: [], page } + { found: 0, hits: [], page }, ); return NextResponse.json(results); } catch (error) { - return NextResponse.json( - { error: "Search failed" }, - { status: 500 } - ); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); } } diff --git a/app/api/search/problems/route.ts b/app/api/search/problems/route.ts index 80a7f38..c5330ca 100644 --- a/app/api/search/problems/route.ts +++ b/app/api/search/problems/route.ts @@ -11,13 +11,10 @@ export async function GET(request: NextRequest) { try { const results = await safeSearch( () => searchProblems(query, { page, per_page }), - { found: 0, hits: [], page } + { found: 0, hits: [], page }, ); return NextResponse.json(results); } catch (error) { - return NextResponse.json( - { error: "Search failed" }, - { status: 500 } - ); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); } } diff --git a/app/api/search/users/route.ts b/app/api/search/users/route.ts index ad1b3fa..7b9daa7 100644 --- a/app/api/search/users/route.ts +++ b/app/api/search/users/route.ts @@ -11,13 +11,10 @@ export async function GET(request: NextRequest) { try { const results = await safeSearch( () => searchUsers(query, { page, per_page }), - { found: 0, hits: [], page } + { found: 0, hits: [], page }, ); return NextResponse.json(results); } catch (error) { - return NextResponse.json( - { error: "Search failed" }, - { status: 500 } - ); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); } } From 0fe2787be7107dceb1b0d3b27ff05e6f44500f22 Mon Sep 17 00:00:00 2001 From: virinci Date: Sun, 4 May 2025 23:39:24 +0530 Subject: [PATCH 11/20] Fix id string error in typesense schema --- lib/typesense/collections/contests.ts | 10 +++++----- lib/typesense/collections/orgs.ts | 8 ++++---- lib/typesense/collections/problems.ts | 25 ++++++++++--------------- lib/typesense/collections/users.ts | 8 ++++---- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/typesense/collections/contests.ts b/lib/typesense/collections/contests.ts index 2d6ed67..ea955aa 100644 --- a/lib/typesense/collections/contests.ts +++ b/lib/typesense/collections/contests.ts @@ -9,7 +9,7 @@ import { type SelectContest } from "@/db/schema"; export const CONTESTS_SCHEMA = { name: "contests", fields: [ - { name: "id", type: "int32" }, + { name: "id", type: "string" }, { name: "name_id", type: "string" }, { name: "name", type: "string" }, { name: "description", type: "string" }, @@ -22,7 +22,7 @@ export const CONTESTS_SCHEMA = { }; export interface ContestDocument { - id: number; + id: string; name_id: string; name: string; description: string; @@ -34,14 +34,14 @@ export interface ContestDocument { export function contestToDocument(contest: SelectContest): ContestDocument { return { - id: contest.id, + id: contest.id.toString(), name_id: contest.nameId, name: contest.name, description: contest.description, rules: contest.rules, organizer_id: contest.organizerId, - start_time: contest.startTime.getTime(), - end_time: contest.endTime.getTime(), + start_time: new Date(contest.startTime).getTime(), + end_time: new Date(contest.endTime).getTime(), }; } diff --git a/lib/typesense/collections/orgs.ts b/lib/typesense/collections/orgs.ts index 313c03b..bf19f4c 100644 --- a/lib/typesense/collections/orgs.ts +++ b/lib/typesense/collections/orgs.ts @@ -9,7 +9,7 @@ import { SelectOrg } from "@/db/schema"; export const ORGS_SCHEMA = { name: "orgs", fields: [ - { name: "id", type: "int32" }, + { name: "id", type: "string" }, { name: "name_id", type: "string" }, { name: "name", type: "string" }, { name: "about", type: "string", optional: true }, @@ -20,7 +20,7 @@ export const ORGS_SCHEMA = { }; export interface OrgDocument { - id: number; + id: string; name_id: string; name: string; about?: string; @@ -30,12 +30,12 @@ export interface OrgDocument { export function orgToDocument(org: SelectOrg): OrgDocument { return { - id: org.id, + id: org.id.toString(), name_id: org.nameId, name: org.name, about: org.about || undefined, avatar: org.avatar || undefined, - created_at: org.createdAt.getTime(), + created_at: new Date(org.createdAt).getTime(), }; } diff --git a/lib/typesense/collections/problems.ts b/lib/typesense/collections/problems.ts index 28ca311..aa9d274 100644 --- a/lib/typesense/collections/problems.ts +++ b/lib/typesense/collections/problems.ts @@ -1,11 +1,16 @@ -import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; import { SelectProblem } from "@/db/schema"; import { getRedis, CACHE_KEYS, CACHE_TTL } from "@/db/redis"; export const PROBLEMS_SCHEMA = { name: "problems", fields: [ - { name: "id", type: "int32" }, + { name: "id", type: "string" }, { name: "code", type: "string" }, { name: "title", type: "string" }, { name: "description", type: "string" }, @@ -17,7 +22,7 @@ export const PROBLEMS_SCHEMA = { }; export interface ProblemDocument { - id: number; + id: string; code: string; title: string; description: string; @@ -28,13 +33,13 @@ export interface ProblemDocument { export function problemToDocument(problem: SelectProblem): ProblemDocument { return { - id: problem.id, + id: problem.id.toString(), code: problem.code, title: problem.title, description: problem.description, allowed_languages: problem.allowedLanguages, org_id: problem.orgId, - created_at: problem.createdAt.getTime(), + created_at: new Date(problem.createdAt).getTime(), }; } @@ -53,16 +58,6 @@ export async function searchProblems( const client = getTypesenseClient(); - const searchParameters = createSearchParams( - query, - ["title", "code", "description"], - [4, 2, 1], - { - per_page: options.per_page || 10, - page: options.page || 1, - ...options, - }, - ); const searchParameters = createSearchParams( query, ["title", "code", "description"], diff --git a/lib/typesense/collections/users.ts b/lib/typesense/collections/users.ts index 4e2fcf8..2ba1fcd 100644 --- a/lib/typesense/collections/users.ts +++ b/lib/typesense/collections/users.ts @@ -9,7 +9,7 @@ import { type SelectUser } from "@/db/schema"; export const USERS_SCHEMA = { name: "users", fields: [ - { name: "id", type: "int32" }, + { name: "id", type: "string" }, { name: "name_id", type: "string" }, { name: "name", type: "string" }, { name: "about", type: "string", optional: true }, @@ -20,7 +20,7 @@ export const USERS_SCHEMA = { }; export interface UserDocument { - id: number; + id: string; name_id: string; name: string; about?: string; @@ -30,12 +30,12 @@ export interface UserDocument { export function userToDocument(user: SelectUser): UserDocument { return { - id: user.id, + id: user.id.toString(), name_id: user.nameId, name: user.name, about: user.about || undefined, avatar: user.avatar || undefined, - created_at: user.createdAt.getTime(), + created_at: new Date(user.createdAt).getTime(), }; } From d5f5d21affef3a3ddedc89274e4bd8a69ed4bb91 Mon Sep 17 00:00:00 2001 From: virinci Date: Sun, 4 May 2025 23:39:56 +0530 Subject: [PATCH 12/20] Initialize the Redis pub/sub in root layout --- app/layout.tsx | 3 +++ lib/init.ts | 20 ++++++++++++++++++++ lib/typesense/init.ts | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 lib/init.ts create mode 100644 lib/typesense/init.ts diff --git a/app/layout.tsx b/app/layout.tsx index 8c4cab4..383cd5c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,9 @@ import "@uiw/react-markdown-preview/markdown.css"; import { Toaster } from "@/components/ui/toaster"; import { AuthProvider } from "@/contexts/auth-context"; import { ThemeProvider } from "@/contexts/theme-context"; // Added import statement +import { initializeServices } from "@/lib/init"; + +initializeServices().catch(console.error); const geistSans = localFont({ src: "./fonts/GeistVF.woff", diff --git a/lib/init.ts b/lib/init.ts new file mode 100644 index 0000000..981e388 --- /dev/null +++ b/lib/init.ts @@ -0,0 +1,20 @@ +import { initializeTypesenseCollections } from "./typesense/init"; +import { initializeTypesenseSync } from "./typesense/subscriber"; +import { syncExistingData } from "./typesense/sync-existing"; + +let initialized = false; + +export async function initializeServices() { + if (initialized) return; + + // Initialize Typesense collections + await initializeTypesenseCollections(); + + // Initialize TypesenseSubscriber + initializeTypesenseSync(); + + // Sync existing data + await syncExistingData(); + + initialized = true; +} diff --git a/lib/typesense/init.ts b/lib/typesense/init.ts new file mode 100644 index 0000000..64dda83 --- /dev/null +++ b/lib/typesense/init.ts @@ -0,0 +1,24 @@ +import { getTypesenseClient } from "./client"; +import { PROBLEMS_SCHEMA } from "./collections/problems"; +import { CONTESTS_SCHEMA } from "./collections/contests"; +import { USERS_SCHEMA } from "./collections/users"; +import { ORGS_SCHEMA } from "./collections/orgs"; + +export async function initializeTypesenseCollections() { + const client = getTypesenseClient(); + const schemas = [PROBLEMS_SCHEMA, CONTESTS_SCHEMA, USERS_SCHEMA, ORGS_SCHEMA]; + + for (const schema of schemas) { + try { + // Check if collection exists + const exists = await client.collections(schema.name).exists(); + + if (!exists) { + await client.collections().create(schema); + console.log(`Created collection: ${schema.name}`); + } + } catch (error) { + console.error(`Error initializing collection ${schema.name}:`, error); + } + } +} From b2c16b3c36d7ada55443f049d95cf2de9cc57746 Mon Sep 17 00:00:00 2001 From: virinci Date: Sun, 4 May 2025 23:40:29 +0530 Subject: [PATCH 13/20] Add queue implementation for syncing to typesense --- lib/typesense/queue.ts | 54 ++++++++++++++++++ lib/typesense/subscriber.ts | 106 ++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 lib/typesense/queue.ts create mode 100644 lib/typesense/subscriber.ts diff --git a/lib/typesense/queue.ts b/lib/typesense/queue.ts new file mode 100644 index 0000000..d165f0b --- /dev/null +++ b/lib/typesense/queue.ts @@ -0,0 +1,54 @@ +import { getRedis } from "@/db/redis"; + +type SyncData = any; + +interface SyncOperation { + operation: "create" | "update" | "delete"; + entity: "problem" | "contest" | "user" | "org"; + data: SyncData; + id: number; + timestamp: number; +} + +export interface BatchedOperations { + problems: { + create: SyncData[]; + update: SyncData[]; + delete: number[]; + }; + contests: { + create: SyncData[]; + update: SyncData[]; + delete: number[]; + }; + users: { + create: SyncData[]; + update: SyncData[]; + delete: number[]; + }; + orgs: { + create: SyncData[]; + update: SyncData[]; + delete: number[]; + }; +} + +export const BATCH_WINDOW = 5000; // 5 seconds +export const MAX_BATCH_SIZE = 100; + +export const publishSync = async ( + operation: "create" | "update" | "delete", + entity: "problem" | "contest" | "user" | "org", + data: SyncData, +) => { + const redis = getRedis(); + const message: SyncOperation = { + operation, + entity, + data, + id: data.id, + timestamp: Date.now(), + }; + + await redis.publish("typesense:sync", JSON.stringify(message)); +}; diff --git a/lib/typesense/subscriber.ts b/lib/typesense/subscriber.ts new file mode 100644 index 0000000..4de0ef9 --- /dev/null +++ b/lib/typesense/subscriber.ts @@ -0,0 +1,106 @@ +import { Redis } from "ioredis"; +import { getRedis } from "@/db/redis"; +import { + syncProblemCreate, + syncProblemUpdate, + syncProblemDelete, + syncContestCreate, + syncContestUpdate, + syncContestDelete, + syncUserCreate, + syncUserUpdate, + syncUserDelete, + syncOrgCreate, + syncOrgUpdate, + syncOrgDelete, +} from "./sync"; +import { type BatchedOperations, MAX_BATCH_SIZE, BATCH_WINDOW } from "./queue"; + +class TypesenseSubscriber { + private batch: BatchedOperations = { + problems: { create: [], update: [], delete: [] }, + contests: { create: [], update: [], delete: [] }, + users: { create: [], update: [], delete: [] }, + orgs: { create: [], update: [], delete: [] }, + }; + + private batchTimeout: NodeJS.Timeout | null = null; + + constructor() { + const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379"); + + redis.subscribe("typesense:sync", (err) => { + if (err) console.error("Redis subscription error:", err); + }); + + redis.on("message", (channel, message) => { + if (channel === "typesense:sync") { + this.handleMessage(JSON.parse(message)); + } + }); + } + + private handleMessage(message: SyncOperation) { + const { entity, operation, data, id } = message; + + // Add to appropriate batch + this.batch[`${entity}s`][operation].push( + operation === "delete" ? id : data, + ); + + // Reset batch timeout + if (this.batchTimeout) clearTimeout(this.batchTimeout); + + // Process batch if max size reached or after window + if (this.getBatchSize() >= MAX_BATCH_SIZE) { + this.processBatch(); + } else { + this.batchTimeout = setTimeout(() => this.processBatch(), BATCH_WINDOW); + } + } + + private getBatchSize(): number { + return Object.values(this.batch).reduce( + (total, entityOps) => + total + + Object.values(entityOps).reduce((sum, ops) => sum + ops.length, 0), + 0, + ); + } + + private async processBatch() { + const currentBatch = this.batch; + this.batch = { + problems: { create: [], update: [], delete: [] }, + contests: { create: [], update: [], delete: [] }, + users: { create: [], update: [], delete: [] }, + orgs: { create: [], update: [], delete: [] }, + }; + + try { + await Promise.all([ + // Problems + ...currentBatch.problems.create.map(syncProblemCreate), + ...currentBatch.problems.update.map(syncProblemUpdate), + ...currentBatch.problems.delete.map(syncProblemDelete), + // Contests + ...currentBatch.contests.create.map(syncContestCreate), + ...currentBatch.contests.update.map(syncContestUpdate), + ...currentBatch.contests.delete.map(syncContestDelete), + // Users + ...currentBatch.users.create.map(syncUserCreate), + ...currentBatch.users.update.map(syncUserUpdate), + ...currentBatch.users.delete.map(syncUserDelete), + // Organizations + ...currentBatch.orgs.create.map(syncOrgCreate), + ...currentBatch.orgs.update.map(syncOrgUpdate), + ...currentBatch.orgs.delete.map(syncOrgDelete), + ]); + } catch (error) { + console.error("Batch processing error:", error); + // Could add retry logic or error reporting here + } + } +} + +export const initializeTypesenseSync = () => new TypesenseSubscriber(); From af625673f17ff558c25ddccaa41202c80b0de9f8 Mon Sep 17 00:00:00 2001 From: virinci Date: Sun, 4 May 2025 23:40:46 +0530 Subject: [PATCH 14/20] Initialization script for syncing existing database entries --- lib/typesense/sync-existing.ts | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/typesense/sync-existing.ts diff --git a/lib/typesense/sync-existing.ts b/lib/typesense/sync-existing.ts new file mode 100644 index 0000000..ccf7ae8 --- /dev/null +++ b/lib/typesense/sync-existing.ts @@ -0,0 +1,49 @@ +import { db } from "@/db/drizzle"; +import { problems, contests, users, orgs } from "@/db/schema"; +import { publishSync } from "./queue"; +import { getTypesenseClient } from "./client"; + +export async function syncExistingData() { + const client = getTypesenseClient(); + + try { + // Clear existing collections + const collections = ["problems", "contests", "users", "orgs"]; + for (const collection of collections) { + try { + await client + .collections(collection) + .documents() + .delete({ filter_by: "" }); + console.log(`Cleared collection: ${collection}`); + } catch (error) { + console.error(`Error clearing collection ${collection}:`, error); + } + } + + // Sync all existing data + const existingProblems = await db.select().from(problems); + for (const problem of existingProblems) { + await publishSync("create", "problem", problem); + } + + const existingContests = await db.select().from(contests); + for (const contest of existingContests) { + await publishSync("create", "contest", contest); + } + + const existingUsers = await db.select().from(users); + for (const user of existingUsers) { + await publishSync("create", "user", user); + } + + const existingOrgs = await db.select().from(orgs); + for (const org of existingOrgs) { + await publishSync("create", "org", org); + } + + console.log("Existing data sync initiated"); + } catch (error) { + console.error("Error syncing existing data:", error); + } +} From 449bffd93107f01fc20e970da7e49287bb2b63ee Mon Sep 17 00:00:00 2001 From: virinci Date: Mon, 5 May 2025 00:40:17 +0530 Subject: [PATCH 15/20] Impl search over org users route --- app/api/orgs/[orgId]/users/search/route.ts | 25 ++++++++++++ lib/typesense/collections/users.ts | 45 +++++++++++++--------- 2 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 app/api/orgs/[orgId]/users/search/route.ts diff --git a/app/api/orgs/[orgId]/users/search/route.ts b/app/api/orgs/[orgId]/users/search/route.ts new file mode 100644 index 0000000..cc740c5 --- /dev/null +++ b/app/api/orgs/[orgId]/users/search/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchUsers } from "@/lib/typesense/collections/users"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET( + request: NextRequest, + { params }: { params: { orgId: string } }, +) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + const orgId = parseInt(params.orgId, 10); + + try { + const results = await safeSearch( + () => searchUsers(query, orgId, { page, per_page }), + { found: 0, hits: [], page }, + ); + return NextResponse.json(results); + } catch (error) { + console.error("Users search error:", error); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); + } +} diff --git a/lib/typesense/collections/users.ts b/lib/typesense/collections/users.ts index 2ba1fcd..80291b8 100644 --- a/lib/typesense/collections/users.ts +++ b/lib/typesense/collections/users.ts @@ -1,46 +1,51 @@ -import { - createSearchParams, - getTypesenseClient, - type SearchParams, - type SearchResponse, -} from "../client"; -import { type SelectUser } from "@/db/schema"; +import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { SelectUser } from "@/db/schema"; export const USERS_SCHEMA = { name: "users", fields: [ { name: "id", type: "string" }, - { name: "name_id", type: "string" }, + { name: "org_id", type: "int32" }, { name: "name", type: "string" }, + { name: "name_id", type: "string" }, + { name: "email", type: "string" }, { name: "about", type: "string", optional: true }, { name: "avatar", type: "string", optional: true }, - { name: "created_at", type: "int64" }, + { name: "role", type: "string" }, + { name: "joined_at", type: "int64" }, ], - default_sorting_field: "created_at", + default_sorting_field: "joined_at", }; export interface UserDocument { id: string; - name_id: string; + org_id: number; name: string; + name_id: string; + email: string; about?: string; avatar?: string; - created_at: number; + role: "owner" | "organizer" | "member"; + joined_at: number; } -export function userToDocument(user: SelectUser): UserDocument { +export function userToDocument(user: any, orgId: number): UserDocument { return { id: user.id.toString(), - name_id: user.nameId, + org_id: orgId, name: user.name, + name_id: user.nameId, + email: user.email, about: user.about || undefined, avatar: user.avatar || undefined, - created_at: new Date(user.createdAt).getTime(), + role: user.role, + joined_at: new Date(user.joinedAt).getTime(), }; } export async function searchUsers( query: string, + orgId: number, options: Partial = {}, ): Promise> { const client = getTypesenseClient(); @@ -50,6 +55,8 @@ export async function searchUsers( ["name", "name_id", "about"], [3, 2, 1], { + filter_by: `org_id:=${orgId}`, + sort_by: "_text_match:desc,joined_at:desc", per_page: options.per_page || 10, page: options.page || 1, ...options, @@ -59,14 +66,14 @@ export async function searchUsers( return await client.collections("users").documents().search(searchParameters); } -export async function upsertUser(user: SelectUser) { +export async function upsertUser(user: any, orgId: number) { const client = getTypesenseClient(); - const document = userToDocument(user); + const document = userToDocument(user, orgId); return await client.collections("users").documents().upsert(document); } -export async function deleteUser(id: number) { +export async function deleteUser(id: string) { const client = getTypesenseClient(); - return await client.collections("users").documents(id.toString()).delete(); + return await client.collections("users").documents(id).delete(); } From c8ef58374faf63c76480a86ba332c2f2d00efcbe Mon Sep 17 00:00:00 2001 From: virinci Date: Mon, 5 May 2025 00:40:55 +0530 Subject: [PATCH 16/20] Impl search over org contests route --- app/api/orgs/[orgId]/contests/search/route.ts | 25 ++++++++++ lib/typesense/collections/contests.ts | 49 ++++++++++--------- 2 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 app/api/orgs/[orgId]/contests/search/route.ts diff --git a/app/api/orgs/[orgId]/contests/search/route.ts b/app/api/orgs/[orgId]/contests/search/route.ts new file mode 100644 index 0000000..59af05e --- /dev/null +++ b/app/api/orgs/[orgId]/contests/search/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchContests } from "@/lib/typesense/collections/contests"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET( + request: NextRequest, + { params }: { params: { orgId: string } }, +) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + const orgId = parseInt(params.orgId, 10); + + try { + const results = await safeSearch( + () => searchContests(query, orgId, { page, per_page }), + { found: 0, hits: [], page }, + ); + return NextResponse.json(results); + } catch (error) { + console.error("Contests search error:", error); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); + } +} diff --git a/lib/typesense/collections/contests.ts b/lib/typesense/collections/contests.ts index ea955aa..f5102b6 100644 --- a/lib/typesense/collections/contests.ts +++ b/lib/typesense/collections/contests.ts @@ -1,20 +1,15 @@ -import { - createSearchParams, - getTypesenseClient, - type SearchParams, - type SearchResponse, -} from "../client"; -import { type SelectContest } from "@/db/schema"; +import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; export const CONTESTS_SCHEMA = { name: "contests", fields: [ { name: "id", type: "string" }, - { name: "name_id", type: "string" }, + { name: "org_id", type: "int32" }, { name: "name", type: "string" }, + { name: "name_id", type: "string" }, { name: "description", type: "string" }, - { name: "rules", type: "string" }, - { name: "organizer_id", type: "int32" }, + { name: "problems", type: "string" }, // comma-separated problem codes + { name: "problem_count", type: "int32" }, { name: "start_time", type: "int64" }, { name: "end_time", type: "int64" }, ], @@ -23,23 +18,28 @@ export const CONTESTS_SCHEMA = { export interface ContestDocument { id: string; - name_id: string; + org_id: number; name: string; + name_id: string; description: string; - rules: string; - organizer_id: number; + problems: string; + problem_count: number; start_time: number; end_time: number; } -export function contestToDocument(contest: SelectContest): ContestDocument { +export function contestToDocument( + contest: any, + orgId: number, +): ContestDocument { return { id: contest.id.toString(), - name_id: contest.nameId, + org_id: orgId, name: contest.name, + name_id: contest.nameId, description: contest.description, - rules: contest.rules, - organizer_id: contest.organizerId, + problems: contest.problems || "", + problem_count: contest.problems ? contest.problems.split(",").length : 0, start_time: new Date(contest.startTime).getTime(), end_time: new Date(contest.endTime).getTime(), }; @@ -47,15 +47,18 @@ export function contestToDocument(contest: SelectContest): ContestDocument { export async function searchContests( query: string, + orgId: number, options: Partial = {}, ): Promise> { const client = getTypesenseClient(); const searchParameters = createSearchParams( query, - ["name", "name_id", "description"], - [3, 2, 1], + ["name", "name_id", "description", "problems"], + [3, 2, 1, 1], { + filter_by: `org_id:=${orgId}`, + sort_by: "_text_match:desc,start_time:desc", per_page: options.per_page || 10, page: options.page || 1, ...options, @@ -68,14 +71,14 @@ export async function searchContests( .search(searchParameters); } -export async function upsertContest(contest: SelectContest) { +export async function upsertContest(contest: any, orgId: number) { const client = getTypesenseClient(); - const document = contestToDocument(contest); + const document = contestToDocument(contest, orgId); return await client.collections("contests").documents().upsert(document); } -export async function deleteContest(id: number) { +export async function deleteContest(id: string) { const client = getTypesenseClient(); - return await client.collections("contests").documents(id.toString()).delete(); + return await client.collections("contests").documents(id).delete(); } From 3f7eb8bddd89c1878c252772bed7e6a304acfb9e Mon Sep 17 00:00:00 2001 From: virinci Date: Mon, 5 May 2025 00:41:11 +0530 Subject: [PATCH 17/20] Impl search over org groups route --- app/api/orgs/[orgId]/groups/search/route.ts | 25 +++++++ lib/typesense/collections/groups.ts | 81 +++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 app/api/orgs/[orgId]/groups/search/route.ts create mode 100644 lib/typesense/collections/groups.ts diff --git a/app/api/orgs/[orgId]/groups/search/route.ts b/app/api/orgs/[orgId]/groups/search/route.ts new file mode 100644 index 0000000..173dbdb --- /dev/null +++ b/app/api/orgs/[orgId]/groups/search/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchGroups } from "@/lib/typesense/collections/groups"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET( + request: NextRequest, + { params }: { params: { orgId: string } }, +) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + const orgId = parseInt(params.orgId, 10); + + try { + const results = await safeSearch( + () => searchGroups(query, orgId, { page, per_page }), + { found: 0, hits: [], page }, + ); + return NextResponse.json(results); + } catch (error) { + console.error("Groups search error:", error); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); + } +} diff --git a/lib/typesense/collections/groups.ts b/lib/typesense/collections/groups.ts new file mode 100644 index 0000000..72ca507 --- /dev/null +++ b/lib/typesense/collections/groups.ts @@ -0,0 +1,81 @@ +import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; + +export const GROUPS_SCHEMA = { + name: "groups", + fields: [ + { name: "id", type: "string" }, + { name: "org_id", type: "int32" }, + { name: "name", type: "string" }, + { name: "name_id", type: "string" }, + { name: "about", type: "string", optional: true }, + { name: "avatar", type: "string", optional: true }, + { name: "created_at", type: "int64" }, + { name: "users", type: "string" }, // newline separated emails + { name: "users_count", type: "int32" }, + ], + default_sorting_field: "created_at", +}; + +export interface GroupDocument { + id: string; + org_id: number; + name: string; + name_id: string; + about?: string; + avatar?: string; + created_at: number; + users: string; + users_count: number; +} + +export function groupToDocument(group: any, orgId: number): GroupDocument { + return { + id: group.id.toString(), + org_id: orgId, + name: group.name, + name_id: group.nameId, + about: group.about || undefined, + avatar: group.avatar || undefined, + created_at: new Date(group.createdAt).getTime(), + users: group.userEmails?.join("\n") || "", + users_count: group.userEmails?.length || 0, + }; +} + +export async function searchGroups( + query: string, + orgId: number, + options: Partial = {}, +): Promise> { + const client = getTypesenseClient(); + + const searchParameters = createSearchParams( + query, + ["name", "name_id", "about", "users"], + [3, 2, 1, 1], + { + filter_by: `org_id:=${orgId}`, + sort_by: "_text_match:desc,created_at:desc", + per_page: options.per_page || 10, + page: options.page || 1, + ...options, + }, + ); + + return await client + .collections("groups") + .documents() + .search(searchParameters); +} + +export async function upsertGroup(group: any, orgId: number) { + const client = getTypesenseClient(); + const document = groupToDocument(group, orgId); + + return await client.collections("groups").documents().upsert(document); +} + +export async function deleteGroup(id: string) { + const client = getTypesenseClient(); + return await client.collections("groups").documents(id).delete(); +} From 2f24111244c8448f3cb657db446808481f2ea6c2 Mon Sep 17 00:00:00 2001 From: virinci Date: Mon, 5 May 2025 00:41:48 +0530 Subject: [PATCH 18/20] Impl search over org problems route --- app/api/orgs/[orgId]/problems/search/route.ts | 25 +++++ lib/typesense/collections/problems.ts | 98 ++++--------------- 2 files changed, 44 insertions(+), 79 deletions(-) create mode 100644 app/api/orgs/[orgId]/problems/search/route.ts diff --git a/app/api/orgs/[orgId]/problems/search/route.ts b/app/api/orgs/[orgId]/problems/search/route.ts new file mode 100644 index 0000000..3da390b --- /dev/null +++ b/app/api/orgs/[orgId]/problems/search/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; +import { searchProblems } from "@/lib/typesense/collections/problems"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET( + request: NextRequest, + { params }: { params: { orgId: string } }, +) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get("q") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const per_page = parseInt(searchParams.get("per_page") || "10", 10); + const orgId = parseInt(params.orgId, 10); + + try { + const results = await safeSearch( + () => searchProblems(query, orgId, { page, per_page }), + { found: 0, hits: [], page }, + ); + return NextResponse.json(results); + } catch (error) { + console.error("Problems search error:", error); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); + } +} diff --git a/lib/typesense/collections/problems.ts b/lib/typesense/collections/problems.ts index aa9d274..99af77f 100644 --- a/lib/typesense/collections/problems.ts +++ b/lib/typesense/collections/problems.ts @@ -1,21 +1,14 @@ -import { - createSearchParams, - getTypesenseClient, - SearchParams, - SearchResponse, -} from "../client"; -import { SelectProblem } from "@/db/schema"; -import { getRedis, CACHE_KEYS, CACHE_TTL } from "@/db/redis"; +import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; export const PROBLEMS_SCHEMA = { name: "problems", fields: [ { name: "id", type: "string" }, + { name: "org_id", type: "int32" }, { name: "code", type: "string" }, { name: "title", type: "string" }, - { name: "description", type: "string" }, + { name: "description", type: "string", optional: true }, { name: "allowed_languages", type: "string[]" }, - { name: "org_id", type: "int32" }, { name: "created_at", type: "int64" }, ], default_sorting_field: "created_at", @@ -23,116 +16,63 @@ export const PROBLEMS_SCHEMA = { export interface ProblemDocument { id: string; + org_id: number; code: string; title: string; - description: string; + description?: string; allowed_languages: string[]; - org_id: number; created_at: number; } -export function problemToDocument(problem: SelectProblem): ProblemDocument { +export function problemToDocument( + problem: any, + orgId: number, +): ProblemDocument { return { id: problem.id.toString(), + org_id: orgId, code: problem.code, title: problem.title, description: problem.description, allowed_languages: problem.allowedLanguages, - org_id: problem.orgId, created_at: new Date(problem.createdAt).getTime(), }; } export async function searchProblems( query: string, + orgId: number, options: Partial = {}, ): Promise> { - const redis = getRedis(); - const cacheKey = `${CACHE_KEYS.PROBLEM}search:${query}:${JSON.stringify(options)}`; - - // Try to get from cache first - const cached = await redis.get(cacheKey); - if (cached) { - return JSON.parse(cached); - } - const client = getTypesenseClient(); const searchParameters = createSearchParams( query, ["title", "code", "description"], - [4, 2, 1], + [3, 2, 1], { + filter_by: `org_id:=${orgId}`, + sort_by: "_text_match:desc,created_at:desc", per_page: options.per_page || 10, page: options.page || 1, - filter_by: options.filter_by, - sort_by: options.sort_by || "_text_match:desc,created_at:desc", ...options, }, ); - const results = await client + return await client .collections("problems") .documents() .search(searchParameters); - - // Cache results - await redis.set( - cacheKey, - JSON.stringify(results), - "EX", - CACHE_TTL.MEDIUM, // Cache for 5 minutes - ); - - return results; } -export async function upsertProblem(problem: SelectProblem) { +export async function upsertProblem(problem: any, orgId: number) { const client = getTypesenseClient(); - const document = problemToDocument(problem); - - // Clear search cache when updating problems - const redis = getRedis(); - const keys = await redis.keys(`${CACHE_KEYS.PROBLEM}search:*`); - if (keys.length > 0) { - await redis.del(keys); - } + const document = problemToDocument(problem, orgId); return await client.collections("problems").documents().upsert(document); } -export async function deleteProblem(id: number) { - const client = getTypesenseClient(); - - // Clear search cache when deleting problems - const redis = getRedis(); - const keys = await redis.keys(`${CACHE_KEYS.PROBLEM}search:*`); - if (keys.length > 0) { - await redis.del(keys); - } - - return await client.collections("problems").documents(id.toString()).delete(); -} - -// Batch operations for initial data import -export async function batchUpsertProblems(problems: SelectProblem[]) { +export async function deleteProblem(id: string) { const client = getTypesenseClient(); - const documents = problems.map(problemToDocument); - - return await client - .collections("problems") - .documents() - .import(documents, { action: "upsert" }); -} - -// Search problems by organization -export async function searchProblemsByOrg( - query: string, - orgId: number, - options: Partial = {}, -): Promise> { - return searchProblems(query, { - ...options, - filter_by: `org_id:=${orgId}`, - }); + return await client.collections("problems").documents(id).delete(); } From 67d3c4f1c5b6a4858fe6af69e28723fa9354e0cb Mon Sep 17 00:00:00 2001 From: virinci Date: Mon, 5 May 2025 00:42:05 +0530 Subject: [PATCH 19/20] Impl search over org submissions route --- .../orgs/[orgId]/submissions/search/route.ts | 40 ++++++ lib/typesense/collections/submissions.ts | 136 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 app/api/orgs/[orgId]/submissions/search/route.ts create mode 100644 lib/typesense/collections/submissions.ts diff --git a/app/api/orgs/[orgId]/submissions/search/route.ts b/app/api/orgs/[orgId]/submissions/search/route.ts new file mode 100644 index 0000000..b6ab075 --- /dev/null +++ b/app/api/orgs/[orgId]/submissions/search/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { filterSubmissions } from "@/lib/typesense/collections/submissions"; +import { safeSearch } from "@/lib/typesense/client"; + +export async function GET( + request: NextRequest, + { params }: { params: { orgId: string } }, +) { + const searchParams = request.nextUrl.searchParams; + const orgId = parseInt(params.orgId, 10); + + // Extract filter parameters + const options = { + userNameId: searchParams.get("user") || undefined, + contestNameId: searchParams.get("contest") || undefined, + problemNameId: searchParams.get("problem") || undefined, + language: searchParams.get("language") || undefined, + status: searchParams.get("status") || undefined, + startTime: searchParams.get("start") + ? parseInt(searchParams.get("start")!, 10) + : undefined, + endTime: searchParams.get("end") + ? parseInt(searchParams.get("end")!, 10) + : undefined, + page: parseInt(searchParams.get("page") || "1", 10), + per_page: parseInt(searchParams.get("per_page") || "10", 10), + }; + + try { + const query = searchParams.get("q") || ""; + const results = await safeSearch( + () => searchSubmissions(query, orgId, options), + { found: 0, hits: [], page: options.page }, + ); + return NextResponse.json(results); + } catch (error) { + console.error("Submissions search error:", error); + return NextResponse.json({ error: "Search failed" }, { status: 500 }); + } +} diff --git a/lib/typesense/collections/submissions.ts b/lib/typesense/collections/submissions.ts new file mode 100644 index 0000000..9eba0d6 --- /dev/null +++ b/lib/typesense/collections/submissions.ts @@ -0,0 +1,136 @@ +import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; + +export const SUBMISSIONS_SCHEMA = { + name: "submissions", + fields: [ + { name: "id", type: "string" }, + { name: "org_id", type: "int32" }, + // User details for searching + { name: "user_name_id", type: "string" }, + { name: "user_name", type: "string" }, + // Contest details for searching + { name: "contest_name_id", type: "string" }, + { name: "contest_name", type: "string" }, + // Problem details for searching + { name: "problem_title", type: "string" }, + { name: "problem_code", type: "string" }, + // Submission details + { name: "language", type: "string" }, + { name: "status", type: "string" }, + { name: "submitted_at", type: "int64" }, + { name: "execution_time", type: "int32" }, + { name: "memory_usage", type: "int32" }, + ], + default_sorting_field: "submitted_at", +}; + +export interface SubmissionDocument { + id: string; + org_id: number; + // User details + user_name_id: string; + user_name: string; + // Contest details + contest_name_id: string; + contest_name: string; + // Problem details + problem_title: string; + problem_code: string; + // Submission details + language: string; + status: string; + submitted_at: number; + execution_time: number; + memory_usage: number; +} + +export function submissionToDocument( + submission: any, + orgId: number, +): SubmissionDocument { + return { + id: submission.id.toString(), + org_id: orgId, + // User details + user_name_id: submission.user.nameId, + user_name: submission.user.name, + // Contest details + contest_name_id: submission.contest.nameId, + contest_name: submission.contest.name, + // Problem details + problem_title: submission.problem.title, + problem_code: submission.problem.id, + // Submission details + language: submission.language, + status: submission.status, + submitted_at: new Date(submission.submittedAt).getTime(), + execution_time: submission.executionTime, + memory_usage: submission.memoryUsage, + }; +} + +export async function searchSubmissions( + query: string, + orgId: number, + options: { + userNameId?: string; + contestNameId?: string; + problemNameId?: string; + language?: string; + status?: string; + startTime?: number; + endTime?: number; + page?: number; + per_page?: number; + } = {}, +): Promise> { + const client = getTypesenseClient(); + + let filterBy = `org_id:=${orgId}`; + if (options.userNameId) filterBy += ` && user_name_id:=${options.userNameId}`; + if (options.contestNameId) + filterBy += ` && contest_name_id:=${options.contestNameId}`; + if (options.problemNameId) + filterBy += ` && contest_problem_name_id:=${options.problemNameId}`; + if (options.language) filterBy += ` && language:=${options.language}`; + if (options.status) filterBy += ` && status:=${options.status}`; + if (options.startTime) filterBy += ` && submitted_at:>=${options.startTime}`; + if (options.endTime) filterBy += ` && submitted_at:<=${options.endTime}`; + + const searchParameters = createSearchParams( + query, + [ + "user_name", + "user_name_id", + "contest_name", + "problem_title", + "problem_code", + ], + [3, 2, 2, 2, 1], + { + filter_by: filterBy, + sort_by: query + ? "_text_match:desc,submitted_at:desc" + : "submitted_at:desc", + per_page: options.per_page || 10, + page: options.page || 1, + }, + ); + + return await client + .collections("submissions") + .documents() + .search(searchParameters); +} + +export async function upsertSubmission(submission: any, orgId: number) { + const client = getTypesenseClient(); + const document = submissionToDocument(submission, orgId); + + return await client.collections("submissions").documents().upsert(document); +} + +export async function deleteSubmission(id: string) { + const client = getTypesenseClient(); + return await client.collections("submissions").documents(id).delete(); +} From 27def017f2154320f90a7e37b80a656f7d4c94bf Mon Sep 17 00:00:00 2001 From: virinci Date: Mon, 5 May 2025 00:46:38 +0530 Subject: [PATCH 20/20] Fix import errors for createSearchParams --- lib/typesense/collections/contests.ts | 7 ++++++- lib/typesense/collections/groups.ts | 7 ++++++- lib/typesense/collections/problems.ts | 7 ++++++- lib/typesense/collections/submissions.ts | 7 ++++++- lib/typesense/collections/users.ts | 7 ++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/typesense/collections/contests.ts b/lib/typesense/collections/contests.ts index f5102b6..95de583 100644 --- a/lib/typesense/collections/contests.ts +++ b/lib/typesense/collections/contests.ts @@ -1,4 +1,9 @@ -import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; export const CONTESTS_SCHEMA = { name: "contests", diff --git a/lib/typesense/collections/groups.ts b/lib/typesense/collections/groups.ts index 72ca507..5c5a391 100644 --- a/lib/typesense/collections/groups.ts +++ b/lib/typesense/collections/groups.ts @@ -1,4 +1,9 @@ -import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; export const GROUPS_SCHEMA = { name: "groups", diff --git a/lib/typesense/collections/problems.ts b/lib/typesense/collections/problems.ts index 99af77f..4a1c458 100644 --- a/lib/typesense/collections/problems.ts +++ b/lib/typesense/collections/problems.ts @@ -1,4 +1,9 @@ -import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; export const PROBLEMS_SCHEMA = { name: "problems", diff --git a/lib/typesense/collections/submissions.ts b/lib/typesense/collections/submissions.ts index 9eba0d6..99bee90 100644 --- a/lib/typesense/collections/submissions.ts +++ b/lib/typesense/collections/submissions.ts @@ -1,4 +1,9 @@ -import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; export const SUBMISSIONS_SCHEMA = { name: "submissions", diff --git a/lib/typesense/collections/users.ts b/lib/typesense/collections/users.ts index 80291b8..cdf135e 100644 --- a/lib/typesense/collections/users.ts +++ b/lib/typesense/collections/users.ts @@ -1,4 +1,9 @@ -import { getTypesenseClient, SearchParams, SearchResponse } from "../client"; +import { + createSearchParams, + getTypesenseClient, + SearchParams, + SearchResponse, +} from "../client"; import { SelectUser } from "@/db/schema"; export const USERS_SCHEMA = {