From 01d5b324eb297c20182d3a4337488a2b5297b144 Mon Sep 17 00:00:00 2001 From: Rui Costa Date: Tue, 4 Nov 2025 21:01:04 -0500 Subject: [PATCH] feat: add code note type and migrate to Upstash Redis + Piston --- package.json | 1 + pnpm-lock.yaml | 20 +- src/__tests__/integration.test.ts | 40 ++++ src/db/schema.ts | 2 +- src/lib/cache.ts | 101 ++++------ src/lib/openapi-schemas.ts | 14 +- src/lib/validation.ts | 6 +- src/routes/code/crud.ts | 308 +++++++++++++++++++----------- src/routes/notes/crud.ts | 2 +- src/routes/notes/trash.ts | 4 + 10 files changed, 309 insertions(+), 189 deletions(-) diff --git a/package.json b/package.json index 6a8a3c7..66ad7c8 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@sentry/node": "^10.20.0", "@sentry/profiling-node": "^10.20.0", "@types/ws": "^8.18.1", + "@upstash/redis": "^1.35.6", "dotenv": "^17.0.1", "dotenv-flow": "^4.1.0", "drizzle-orm": "^0.44.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e48706..417ba33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 + '@upstash/redis': + specifier: ^1.35.6 + version: 1.35.6 dotenv: specifier: ^17.0.1 version: 17.2.2 @@ -40,7 +43,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: ^0.44.2 - version: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(postgres@3.4.7) + version: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(@upstash/redis@1.35.6)(postgres@3.4.7) hono: specifier: ^4.8.3 version: 4.9.6 @@ -1541,6 +1544,9 @@ packages: cpu: [x64] os: [win32] + '@upstash/redis@1.35.6': + resolution: {integrity: sha512-aSEIGJgJ7XUfTYvhQcQbq835re7e/BXjs8Janq6Pvr6LlmTZnyqwT97RziZLO/8AVUL037RLXqqiQC6kCt+5pA==} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -3706,6 +3712,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} @@ -5497,6 +5506,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@upstash/redis@1.35.6': + dependencies: + uncrypto: 0.1.3 + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5864,10 +5877,11 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(postgres@3.4.7): + drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(@upstash/redis@1.35.6)(postgres@3.4.7): optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/pg': 8.15.5 + '@upstash/redis': 1.35.6 postgres: 3.4.7 duplexer2@0.1.4: @@ -7728,6 +7742,8 @@ snapshots: uglify-js@3.19.3: optional: true + uncrypto@0.1.3: {} + undici-types@7.10.0: {} unicode-emoji-modifier-base@1.0.0: {} diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts index 67a458c..2e38d14 100644 --- a/src/__tests__/integration.test.ts +++ b/src/__tests__/integration.test.ts @@ -232,5 +232,45 @@ describe("Test Infrastructure Integration", () => { expect(updatedNote.type).toBe("diagram"); }); + + it("should create a code note when type is specified", async () => { + const user = await createTestUser(); + const note = await createTestNote(user.id, null, { + type: "code", + title: "Code Snippet", + content: '{"language":"javascript","code":"console.log(\'Hello\');"}', + }); + + expect(note.type).toBe("code"); + expect(note.title).toBe("Code Snippet"); + expect(note.content).toContain("javascript"); + }); + + it("should query notes by code type", async () => { + const user = await createTestUser(); + const folder = await createTestFolder(user.id); + + // Create mixed note types including code + await createTestNote(user.id, folder.id, { type: "note", title: "Regular Note" }); + await createTestNote(user.id, folder.id, { type: "diagram", title: "Diagram" }); + await createTestNote(user.id, folder.id, { + type: "code", + title: "Code 1", + content: '{"language":"python"}', + }); + await createTestNote(user.id, folder.id, { + type: "code", + title: "Code 2", + content: '{"language":"typescript"}', + }); + + // Query only code notes + const codeNotes = await db.query.notes.findMany({ + where: eq(notes.type, "code"), + }); + + expect(codeNotes).toHaveLength(2); + expect(codeNotes.every((n) => n.type === "code")).toBe(true); + }); }); }); diff --git a/src/db/schema.ts b/src/db/schema.ts index 47c310b..89e87bd 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -44,7 +44,7 @@ export const notes = pgTable( title: text("title").notNull(), content: text("content").default(""), - type: text("type", { enum: ["note", "diagram"] }) + type: text("type", { enum: ["note", "diagram", "code"] }) .default("note") .notNull(), diff --git a/src/lib/cache.ts b/src/lib/cache.ts index f4217bc..7eac7c4 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,54 +1,28 @@ -import { Cluster, ClusterOptions } from "ioredis"; +import { Redis } from "@upstash/redis"; import { logger } from "./logger"; import { db, folders } from "../db"; import { eq } from "drizzle-orm"; import * as Sentry from "@sentry/node"; -let client: Cluster | null = null; +let client: Redis | null = null; -export function getCacheClient(): Cluster | null { - if (!process.env.VALKEY_HOST) { - logger.warn("VALKEY_HOST not configured, caching disabled"); +export function getCacheClient(): Redis | null { + if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) { + logger.warn("Upstash Redis not configured, caching disabled"); return null; } if (!client) { try { - const clusterOptions: ClusterOptions = { - dnsLookup: (address, callback) => callback(null, address), - redisOptions: { - tls: process.env.NODE_ENV === "production" ? {} : undefined, - connectTimeout: 5000, - }, - clusterRetryStrategy: (times) => { - if (times > 3) { - logger.error("Valkey connection failed after 3 retries"); - return null; - } - return Math.min(times * 200, 2000); - }, - }; - - client = new Cluster( - [ - { - host: process.env.VALKEY_HOST, - port: parseInt(process.env.VALKEY_PORT || "6379"), - }, - ], - clusterOptions - ); - - client.on("error", (err) => { - logger.error("Valkey client error", { error: err.message }, err); + client = new Redis({ + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, }); - client.on("connect", () => { - logger.info("Connected to Valkey cluster"); - }); + logger.info("Connected to Upstash Redis"); } catch (error) { logger.error( - "Failed to initialize Valkey client", + "Failed to initialize Upstash Redis client", { error: error instanceof Error ? error.message : String(error), }, @@ -63,9 +37,9 @@ export function getCacheClient(): Cluster | null { export async function closeCache(): Promise { if (client) { - await client.disconnect(); + // Upstash REST client doesn't need explicit disconnection client = null; - logger.info("Valkey connection closed"); + logger.info("Upstash Redis connection closed"); } } @@ -85,20 +59,20 @@ export async function getCache(key: string): Promise { async (span) => { const startTime = Date.now(); try { - const data = await cache.get(key); + const data = await cache.get(key); const duration = Date.now() - startTime; const hit = data !== null; // Set Sentry span attributes span.setAttribute("cache.hit", hit); if (data) { - span.setAttribute("cache.item_size", data.length); + span.setAttribute("cache.item_size", JSON.stringify(data).length); } // Log cache operation with metrics logger.cacheOperation("get", key, hit, duration); - return data ? JSON.parse(data) : null; + return data ? (JSON.parse(data) as T) : null; } catch (error) { span.setStatus({ code: 2, message: "error" }); // SPAN_STATUS_ERROR logger.cacheError("get", key, error instanceof Error ? error : new Error(String(error))); @@ -164,16 +138,12 @@ export async function deleteCache(...keys: string[]): Promise { async (span) => { const startTime = Date.now(); try { - // In cluster mode, keys may hash to different slots - // Use pipeline to delete individually (more efficient than separate awaits) + // Delete keys individually (Upstash REST API) if (keys.length === 1) { await cache.del(keys[0]); } else { - const pipeline = cache.pipeline(); - for (const key of keys) { - pipeline.del(key); - } - await pipeline.exec(); + // Use Promise.all for parallel deletion + await Promise.all(keys.map((key) => cache.del(key))); } const duration = Date.now() - startTime; @@ -197,36 +167,31 @@ export async function deleteCachePattern(pattern: string): Promise { try { const keys: string[] = []; + let cursor = "0"; + + // Use SCAN to find keys matching pattern + do { + // Upstash REST API supports SCAN + const result = await cache.scan(Number(cursor), { + match: pattern, + count: 100, + }); - // In cluster mode, we need to scan all master nodes - const nodes = cache.nodes("master"); - - for (const node of nodes) { - let cursor = "0"; - do { - // Scan each master node individually - const result = await node.scan(cursor, "MATCH", pattern, "COUNT", 100); - cursor = result[0]; - keys.push(...result[1]); - } while (cursor !== "0"); - } + cursor = String(result[0]); + keys.push(...result[1]); + } while (cursor !== "0"); if (keys.length > 0) { - // Delete in batches using pipeline (cluster mode compatible) + // Delete in batches of 100 const batchSize = 100; for (let i = 0; i < keys.length; i += batchSize) { const batch = keys.slice(i, i + batchSize); - const pipeline = cache.pipeline(); - for (const key of batch) { - pipeline.del(key); - } - await pipeline.exec(); + await Promise.all(batch.map((key) => cache.del(key))); } logger.info(`Deleted cache keys matching pattern`, { pattern, keyCount: keys.length, - nodeCount: nodes.length, }); } } catch (error) { @@ -295,7 +260,7 @@ export async function invalidateNoteCounts(userId: string, folderId: string | nu } } - // Delete all cache keys using pipeline for cluster compatibility + // Delete all cache keys if (cacheKeys.length > 0) { await deleteCache(...cacheKeys); logger.debug("Invalidated note counts cache", { diff --git a/src/lib/openapi-schemas.ts b/src/lib/openapi-schemas.ts index babea37..83ece36 100644 --- a/src/lib/openapi-schemas.ts +++ b/src/lib/openapi-schemas.ts @@ -306,8 +306,8 @@ export const noteSchema = z title: z.string().openapi({ example: "[ENCRYPTED]", description: "Encrypted note title" }), content: z.string().openapi({ example: "[ENCRYPTED]", description: "Encrypted note content" }), type: z - .enum(["note", "diagram"]) - .openapi({ example: "note", description: "Note type: 'note' or 'diagram'" }), + .enum(["note", "diagram", "code"]) + .openapi({ example: "note", description: "Note type: 'note', 'diagram', or 'code'" }), encryptedTitle: z .string() .nullable() @@ -375,9 +375,9 @@ export const createNoteRequestSchema = z .max(20) .optional() .openapi({ example: ["work"], description: "Up to 20 tags, max 50 chars each" }), - type: z.enum(["note", "diagram"]).default("note").optional().openapi({ + type: z.enum(["note", "diagram", "code"]).default("note").optional().openapi({ example: "note", - description: "Note type: 'note' or 'diagram' (defaults to 'note' if not specified)", + description: "Note type: 'note', 'diagram', or 'code' (defaults to 'note' if not specified)", }), encryptedTitle: z .string() @@ -419,9 +419,9 @@ export const updateNoteRequestSchema = z .optional() .openapi({ example: ["work"], description: "Up to 20 tags" }), type: z - .enum(["note", "diagram"]) + .enum(["note", "diagram", "code"]) .optional() - .openapi({ example: "note", description: "Note type: 'note' or 'diagram'" }), + .openapi({ example: "note", description: "Note type: 'note', 'diagram', or 'code'" }), encryptedTitle: z .string() .optional() @@ -482,7 +482,7 @@ export const notesQueryParamsSchema = z description: "Filter by hidden status", }), type: z - .enum(["note", "diagram"]) + .enum(["note", "diagram", "code"]) .optional() .openapi({ param: { name: "type", in: "query" }, diff --git a/src/lib/validation.ts b/src/lib/validation.ts index b7386b1..c3bc283 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -45,7 +45,7 @@ export const createNoteSchema = z.object({ ), starred: z.boolean().optional(), tags: z.array(z.string().max(50)).max(20).optional(), - type: z.enum(["note", "diagram"]).default("note").optional(), + type: z.enum(["note", "diagram", "code"]).default("note").optional(), encryptedTitle: z.string().optional(), encryptedContent: z.string().optional(), @@ -71,7 +71,7 @@ export const updateNoteSchema = z.object({ deleted: z.boolean().optional(), hidden: z.boolean().optional(), tags: z.array(z.string().max(50)).max(20).optional(), - type: z.enum(["note", "diagram"]).optional(), + type: z.enum(["note", "diagram", "code"]).optional(), encryptedTitle: z.string().optional(), encryptedContent: z.string().optional(), @@ -91,7 +91,7 @@ export const notesQuerySchema = z archived: z.coerce.boolean().optional(), deleted: z.coerce.boolean().optional(), hidden: z.coerce.boolean().optional(), - type: z.enum(["note", "diagram"]).optional(), + type: z.enum(["note", "diagram", "code"]).optional(), search: z .string() .max(100) diff --git a/src/routes/code/crud.ts b/src/routes/code/crud.ts index fdae774..f49d229 100644 --- a/src/routes/code/crud.ts +++ b/src/routes/code/crud.ts @@ -4,7 +4,6 @@ import { z } from "@hono/zod-openapi"; import { logger } from "../../lib/logger"; import { executeCodeRequestSchema, - codeSubmissionResponseSchema, codeExecutionStatusSchema, languageSchema, codeHealthResponseSchema, @@ -13,17 +12,70 @@ import { const crudRouter = new OpenAPIHono(); -const JUDGE0_API_URL = process.env.JUDGE0_API_URL || "https://judge0-ce.p.rapidapi.com"; -const JUDGE0_API_KEY = process.env.JUDGE0_API_KEY; -const JUDGE0_API_HOST = process.env.JUDGE0_API_HOST || "judge0-ce.p.rapidapi.com"; +// Piston API configuration (private VPC endpoint) +const PISTON_API_URL = process.env.PISTON_API_URL; -if (!JUDGE0_API_KEY) { - logger.error("JUDGE0_API_KEY environment variable is required - code execution disabled"); +if (!PISTON_API_URL) { + logger.error("PISTON_API_URL environment variable is required - code execution disabled"); process.exit(1); } -async function makeJudge0Request(endpoint: string, options: RequestInit = {}) { - const url = `${JUDGE0_API_URL}${endpoint}`; +// Language mapping from Judge0 language_id to Piston language names +const JUDGE0_TO_PISTON_LANGUAGE: Record = { + 63: "javascript", // JavaScript (Node.js 12.14.0) + 74: "typescript", // TypeScript (3.7.4) + 71: "python", // Python (3.8.1) + 62: "java", // Java (OpenJDK 13.0.1) + 54: "cpp", // C++ (GCC 9.2.0) + 50: "c", // C (GCC 9.2.0) + 51: "csharp", // C# (Mono 6.6.0.161) + 60: "go", // Go (1.13.5) + 73: "rust", // Rust (1.40.0) + 68: "php", // PHP (7.4.1) + 72: "ruby", // Ruby (2.7.0) + 78: "kotlin", // Kotlin (1.3.70) + 83: "swift", // Swift (5.2.3) + 46: "bash", // Bash (5.0.0) + 82: null, // SQL (SQLite 3.27.2) - not supported by Piston +}; + +// Piston language to version mapping +const PISTON_LANGUAGE_MAP: Record = { + javascript: "javascript", + typescript: "typescript", + python: "python", + java: "java", + cpp: "c++", // Note: frontend sends "cpp", Piston expects "c++" + c: "c", + csharp: "csharp", + go: "go", + rust: "rust", + php: "php", + ruby: "ruby", + kotlin: "kotlin", + swift: "swift", + bash: "bash", +}; + +const PISTON_VERSION_MAP: Record = { + javascript: "20.11.1", + typescript: "5.0.3", + python: "3.12.0", + java: "15.0.2", + cpp: "10.2.0", + c: "10.2.0", + csharp: "5.0.201", + go: "1.16.2", + rust: "1.68.2", + php: "8.2.3", + ruby: "3.0.1", + kotlin: "1.8.20", + swift: "5.3.3", + bash: "5.2.0", +}; + +async function makePistonRequest(endpoint: string, options: RequestInit = {}) { + const url = `${PISTON_API_URL}${endpoint}`; const start = Date.now(); const controller = new AbortController(); @@ -34,8 +86,6 @@ async function makeJudge0Request(endpoint: string, options: RequestInit = {}) { ...options, signal: controller.signal, headers: { - "X-RapidAPI-Key": JUDGE0_API_KEY!, - "X-RapidAPI-Host": JUDGE0_API_HOST, "Content-Type": "application/json", ...options.headers, }, @@ -44,7 +94,7 @@ async function makeJudge0Request(endpoint: string, options: RequestInit = {}) { clearTimeout(timeoutId); const duration = Date.now() - start; - logger.debug("Judge0 API request", { + logger.debug("Piston API request", { method: options.method || "GET", endpoint, duration, @@ -53,31 +103,15 @@ async function makeJudge0Request(endpoint: string, options: RequestInit = {}) { if (!response.ok) { const errorBody = await response.text().catch(() => ""); - logger.error("Judge0 API Error", { + logger.error("Piston API Error", { status: response.status, statusText: response.statusText, endpoint, errorBody: errorBody.substring(0, 200), }); - let clientMessage = "Code execution failed. Please try again."; - let statusCode: number = response.status; - - if (response.status === 429) { - clientMessage = - "Code execution service is temporarily busy. Please try again in a few minutes."; - } else if (response.status === 401 || response.status === 403) { - clientMessage = - "Code execution service is temporarily unavailable. Please contact support."; - statusCode = 503; - } else if (response.status >= 500) { - clientMessage = - "Code execution service is temporarily unavailable. Please try again later."; - statusCode = 503; - } - - throw new HTTPException(statusCode as 500 | 503, { - message: clientMessage, + throw new HTTPException(503, { + message: "Code execution service temporarily unavailable", }); } @@ -90,14 +124,14 @@ async function makeJudge0Request(endpoint: string, options: RequestInit = {}) { } if (error instanceof Error && error.name === "AbortError") { - logger.error("Judge0 API timeout", { endpoint }); + logger.error("Piston API timeout", { endpoint }); throw new HTTPException(504, { message: "Code execution timed out. Please try again.", }); } logger.error( - "Judge0 API request failed", + "Piston API request failed", { endpoint, error: error instanceof Error ? error.message : String(error), @@ -110,13 +144,13 @@ async function makeJudge0Request(endpoint: string, options: RequestInit = {}) { } } -// POST /api/code/execute - Execute code +// POST /api/code/execute - Execute code (now synchronous with Piston) const executeCodeRoute = createRoute({ method: "post", path: "/execute", summary: "Execute code", description: - "Submit code for execution via Judge0. Returns a token that can be used to check execution status. Supports 50+ programming languages.", + "Execute code immediately via Piston. Returns execution results synchronously. Supports 14+ programming languages.", tags: ["Code Execution"], request: { body: { @@ -129,22 +163,19 @@ const executeCodeRoute = createRoute({ }, responses: { 200: { - description: "Code submitted successfully", + description: "Code executed successfully", content: { "application/json": { - schema: codeSubmissionResponseSchema, + schema: codeExecutionStatusSchema, }, }, }, 400: { - description: "Invalid request body", + description: "Invalid request body or unsupported language", }, 401: { description: "Unauthorized - Invalid or missing authentication", }, - 429: { - description: "Rate limit exceeded - Too many requests", - }, 503: { description: "Code execution service temporarily unavailable", }, @@ -159,20 +190,90 @@ crudRouter.openapi(executeCodeRoute, async (c) => { try { const body = c.req.valid("json"); - const submissionData = { - ...body, - source_code: Buffer.from(body.source_code).toString("base64"), - stdin: Buffer.from(body.stdin || "").toString("base64"), + // Convert Judge0 language_id to Piston language + const pistonLanguageKey = JUDGE0_TO_PISTON_LANGUAGE[body.language_id]; + + if (pistonLanguageKey === null) { + return c.json( + { + stdout: "", + stderr: "Language not supported by Piston execution service", + compile_output: null, + message: null, + status: { + id: 6, + description: "Not Supported", + }, + time: "0", + memory: null, + }, + 200 + ); + } + + if (pistonLanguageKey === undefined) { + throw new HTTPException(400, { + message: `Invalid language_id: ${body.language_id}`, + }); + } + + const pistonLanguage = PISTON_LANGUAGE_MAP[pistonLanguageKey]; + const pistonVersion = PISTON_VERSION_MAP[pistonLanguageKey]; + + if (!pistonLanguage || !pistonVersion) { + return c.json( + { + stdout: "", + stderr: `Language ${pistonLanguageKey} is not supported`, + compile_output: null, + message: null, + status: { + id: 6, + description: "Not Supported", + }, + time: "0", + memory: null, + }, + 200 + ); + } + + // Prepare Piston request + const pistonRequest = { + language: pistonLanguage, + version: pistonVersion, + files: [ + { + content: body.source_code, + }, + ], + stdin: body.stdin || "", }; - const response = await makeJudge0Request("/submissions?base64_encoded=true", { + // Execute code on Piston (synchronous) + const response = await makePistonRequest("/execute", { method: "POST", - body: JSON.stringify(submissionData), + body: JSON.stringify(pistonRequest), }); const result = await response.json(); - return c.json(result); + // Convert Piston response to Judge0-compatible format for frontend + const formattedResponse = { + stdout: result.run?.stdout || "", + stderr: result.run?.stderr || "", + compile_output: result.compile?.output || null, + message: result.run?.signal ? `Process killed by signal: ${result.run.signal}` : null, + status: { + id: result.run?.code === 0 ? 3 : 6, + description: result.run?.code === 0 ? "Accepted" : "Runtime Error", + }, + time: result.run?.cpu_time ? String(result.run.cpu_time) : "0", + memory: result.run?.memory || null, + token: null, // No token needed for synchronous execution + }; + + return c.json(formattedResponse); } catch (error) { if (error instanceof HTTPException) { throw error; @@ -186,85 +287,58 @@ crudRouter.openapi(executeCodeRoute, async (c) => { error instanceof Error ? error : undefined ); throw new HTTPException(500, { - message: "Failed to submit code for execution", + message: "Failed to execute code", }); } }); -// GET /api/code/status/:token - Get execution status +// GET /api/code/status/:token - Get execution status (legacy endpoint, now unused) const getStatusRoute = createRoute({ method: "get", path: "/status/{token}", - summary: "Get execution status", + summary: "Get execution status (deprecated)", description: - "Check the status of a code execution submission. Returns stdout, stderr, execution time, memory usage, and status information.", + "Legacy endpoint for Judge0 compatibility. Piston executes synchronously, so this endpoint is no longer needed.", tags: ["Code Execution"], request: { params: tokenParamSchema, }, responses: { 200: { - description: "Execution status retrieved successfully", + description: "Status endpoint deprecated", content: { "application/json": { schema: codeExecutionStatusSchema, }, }, }, - 400: { - description: "Invalid token format", - }, 401: { description: "Unauthorized - Invalid or missing authentication", }, - 404: { - description: "Submission not found", - }, - 503: { - description: "Code execution service temporarily unavailable", + 410: { + description: "Endpoint deprecated - use /execute directly", }, }, security: [{ Bearer: [] }], }); crudRouter.openapi(getStatusRoute, async (c) => { - try { - const { token } = c.req.valid("param"); - - const response = await makeJudge0Request(`/submissions/${token}?base64_encoded=true`); - - const result = await response.json(); - - if (result.stdout) { - result.stdout = Buffer.from(result.stdout, "base64").toString("utf-8"); - } - if (result.stderr) { - result.stderr = Buffer.from(result.stderr, "base64").toString("utf-8"); - } - if (result.compile_output) { - result.compile_output = Buffer.from(result.compile_output, "base64").toString("utf-8"); - } - if (result.message) { - result.message = Buffer.from(result.message, "base64").toString("utf-8"); - } - - return c.json(result); - } catch (error) { - if (error instanceof HTTPException) { - throw error; - } - - logger.error( - "Status check error", - { - error: error instanceof Error ? error.message : String(error), + // Return 410 Gone to indicate this endpoint is deprecated + return c.json( + { + stdout: "", + stderr: "This endpoint is deprecated. Piston executes code synchronously.", + compile_output: null, + message: "Use POST /api/code/execute for immediate results", + status: { + id: 6, + description: "Deprecated", }, - error instanceof Error ? error : undefined - ); - throw new HTTPException(500, { - message: "Failed to check execution status", - }); - } + time: "0", + memory: null, + }, + 410 + ); }); // GET /api/code/languages - Get supported languages @@ -273,7 +347,7 @@ const getLanguagesRoute = createRoute({ path: "/languages", summary: "Get supported languages", description: - "Returns a list of all programming languages supported by the code execution service, including language IDs and versions.", + "Returns a list of all programming languages supported by Piston, with Judge0-compatible IDs for frontend compatibility.", tags: ["Code Execution"], responses: { 200: { @@ -296,14 +370,26 @@ const getLanguagesRoute = createRoute({ crudRouter.openapi(getLanguagesRoute, async (c) => { try { - const response = await makeJudge0Request("/languages"); - const result = await response.json(); - return c.json(result); + // Return static list of supported languages with Judge0-compatible IDs + const languages = [ + { id: 63, name: "JavaScript (Node.js 20.11.1)" }, + { id: 74, name: "TypeScript (5.0.3)" }, + { id: 71, name: "Python (3.12.0)" }, + { id: 62, name: "Java (OpenJDK 15.0.2)" }, + { id: 54, name: "C++ (GCC 10.2.0)" }, + { id: 50, name: "C (GCC 10.2.0)" }, + { id: 51, name: "C# (Mono 5.0.201)" }, + { id: 60, name: "Go (1.16.2)" }, + { id: 73, name: "Rust (1.68.2)" }, + { id: 68, name: "PHP (8.2.3)" }, + { id: 72, name: "Ruby (3.0.1)" }, + { id: 78, name: "Kotlin (1.8.20)" }, + { id: 83, name: "Swift (5.3.3)" }, + { id: 46, name: "Bash (5.2.0)" }, + ]; + + return c.json(languages); } catch (error) { - if (error instanceof HTTPException) { - throw error; - } - logger.error( "Languages fetch error", { @@ -322,7 +408,7 @@ const getHealthRoute = createRoute({ method: "get", path: "/health", summary: "Health check", - description: "Check the health status of the code execution service and Judge0 connection.", + description: "Check the health status of the code execution service and Piston connection.", tags: ["Code Execution"], responses: { 200: { @@ -358,7 +444,15 @@ const getHealthRoute = createRoute({ crudRouter.openapi(getHealthRoute, async (c) => { try { - const response = await makeJudge0Request("/languages"); + // Test Piston with a simple JavaScript execution + const response = await makePistonRequest("/execute", { + method: "POST", + body: JSON.stringify({ + language: "javascript", + version: "20.11.1", + files: [{ content: "console.log('health check')" }], + }), + }); if (response.ok) { return c.json({ @@ -378,7 +472,7 @@ crudRouter.openapi(getHealthRoute, async (c) => { } } catch (error) { logger.error( - "Judge0 health check failed", + "Piston health check failed", { error: error instanceof Error ? error.message : String(error), }, diff --git a/src/routes/notes/crud.ts b/src/routes/notes/crud.ts index e26fa45..741cfdc 100644 --- a/src/routes/notes/crud.ts +++ b/src/routes/notes/crud.ts @@ -286,7 +286,7 @@ const createNoteHandler: RouteHandler = async (c) => { folderId?: string | null; starred?: boolean; tags?: string[]; - type?: "note" | "diagram"; + type?: "note" | "diagram" | "code"; encryptedTitle?: string; encryptedContent?: string; iv?: string; diff --git a/src/routes/notes/trash.ts b/src/routes/notes/trash.ts index f040090..1c36bcf 100644 --- a/src/routes/notes/trash.ts +++ b/src/routes/notes/trash.ts @@ -3,6 +3,7 @@ import { HTTPException } from "hono/http-exception"; import { db, notes } from "../../db"; import { eq, and, count } from "drizzle-orm"; import { emptyTrashResponseSchema } from "../../lib/openapi-schemas"; +import { deleteCachePattern } from "../../lib/cache"; const trashRouter = new OpenAPIHono(); @@ -44,6 +45,9 @@ trashRouter.openapi(emptyTrashRoute, async (c) => { await db.delete(notes).where(and(eq(notes.userId, userId), eq(notes.deleted, true))); + // Invalidate all note counts cache for this user (global and all folders) + await deleteCachePattern(`notes:${userId}:*`); + return c.json( { success: true,