@@ -11,7 +11,7 @@ import multer from "multer";
1111import archiver from "archiver" ;
1212import { z } from "zod" ;
1313// @ts -ignore
14- import { PrismaClient } from "./generated/client" ;
14+ import { PrismaClient , Prisma } from "./generated/client" ;
1515import {
1616 sanitizeDrawingData ,
1717 validateImportedDrawing ,
@@ -112,6 +112,68 @@ const io = new Server(httpServer, {
112112 maxHttpBufferSize : 1e8 , // 100 MB
113113} ) ;
114114const prisma = new PrismaClient ( ) ;
115+ const parseJsonField = < T > ( rawValue : string | null | undefined , fallback : T ) : T => {
116+ if ( ! rawValue ) return fallback ;
117+ try {
118+ return JSON . parse ( rawValue ) as T ;
119+ } catch ( error ) {
120+ console . warn ( "Failed to parse JSON field" , { error, valuePreview : rawValue . slice ( 0 , 50 ) } ) ;
121+ return fallback ;
122+ }
123+ } ;
124+
125+ const DRAWINGS_CACHE_TTL_MS = ( ( ) => {
126+ const parsed = Number ( process . env . DRAWINGS_CACHE_TTL_MS ) ;
127+ if ( ! Number . isFinite ( parsed ) || parsed <= 0 ) {
128+ return 5_000 ;
129+ }
130+ return parsed ;
131+ } ) ( ) ;
132+ type DrawingsCacheEntry = { body : Buffer ; expiresAt : number } ;
133+ const drawingsCache = new Map < string , DrawingsCacheEntry > ( ) ;
134+
135+ const buildDrawingsCacheKey = ( keyParts : {
136+ searchTerm : string ;
137+ collectionFilter : string ;
138+ includeData : boolean ;
139+ } ) =>
140+ `${ keyParts . searchTerm } |${ keyParts . collectionFilter } |${
141+ keyParts . includeData ? "full" : "summary"
142+ } `;
143+
144+ const getCachedDrawingsBody = ( key : string ) : Buffer | null => {
145+ const entry = drawingsCache . get ( key ) ;
146+ if ( ! entry ) return null ;
147+ if ( Date . now ( ) > entry . expiresAt ) {
148+ drawingsCache . delete ( key ) ;
149+ return null ;
150+ }
151+ return entry . body ;
152+ } ;
153+
154+ const cacheDrawingsResponse = ( key : string , payload : any ) : Buffer => {
155+ const body = Buffer . from ( JSON . stringify ( payload ) ) ;
156+ drawingsCache . set ( key , {
157+ body,
158+ expiresAt : Date . now ( ) + DRAWINGS_CACHE_TTL_MS ,
159+ } ) ;
160+ return body ;
161+ } ;
162+
163+ const invalidateDrawingsCache = ( ) => {
164+ drawingsCache . clear ( ) ;
165+ } ;
166+
167+ // Cleanup cache every 60 seconds
168+ setInterval ( ( ) => {
169+ const now = Date . now ( ) ;
170+ for ( const [ key , entry ] of drawingsCache . entries ( ) ) {
171+ if ( now > entry . expiresAt ) {
172+ drawingsCache . delete ( key ) ;
173+ }
174+ }
175+ } , 60_000 ) . unref ( ) ; // unref so it doesn't keep the process alive if everything else stops
176+
115177const PORT = process . env . PORT || 8000 ;
116178
117179// Multer setup for file uploads with streaming support
@@ -189,7 +251,24 @@ app.use((req, res, next) => {
189251// Rate limiting middleware (basic implementation)
190252const requestCounts = new Map < string , { count : number ; resetTime : number } > ( ) ;
191253const RATE_LIMIT_WINDOW = 15 * 60 * 1000 ; // 15 minutes
192- const RATE_LIMIT_MAX_REQUESTS = 1000 ; // Max requests per window
254+
255+ // Cleanup rate limit map every 5 minutes
256+ setInterval ( ( ) => {
257+ const now = Date . now ( ) ;
258+ for ( const [ ip , data ] of requestCounts . entries ( ) ) {
259+ if ( now > data . resetTime ) {
260+ requestCounts . delete ( ip ) ;
261+ }
262+ }
263+ } , 5 * 60 * 1000 ) . unref ( ) ;
264+
265+ const RATE_LIMIT_MAX_REQUESTS = ( ( ) => {
266+ const parsed = Number ( process . env . RATE_LIMIT_MAX_REQUESTS ) ;
267+ if ( ! Number . isFinite ( parsed ) || parsed <= 0 ) {
268+ return 1000 ;
269+ }
270+ return parsed ;
271+ } ) ( ) ; // Max requests per window
193272
194273app . use ( ( req , res , next ) => {
195274 const ip = req . ip || req . connection . remoteAddress || "unknown" ;
@@ -486,36 +565,84 @@ app.get("/health", (req, res) => {
486565// GET /drawings
487566app . get ( "/drawings" , async ( req , res ) => {
488567 try {
489- const { search, collectionId } = req . query ;
568+ const { search, collectionId, includeData } = req . query ;
490569 const where : any = { } ;
570+ const searchTerm =
571+ typeof search === "string" && search . trim ( ) . length > 0
572+ ? search . trim ( )
573+ : undefined ;
491574
492- if ( search ) {
493- where . name = { contains : String ( search ) } ;
575+ if ( searchTerm ) {
576+ where . name = { contains : searchTerm } ;
494577 }
495578
579+ let collectionFilterKey = "default" ;
496580 if ( collectionId === "null" ) {
497581 where . collectionId = null ;
582+ collectionFilterKey = "null" ;
498583 } else if ( collectionId ) {
499- where . collectionId = String ( collectionId ) ;
584+ const normalizedCollectionId = String ( collectionId ) ;
585+ where . collectionId = normalizedCollectionId ;
586+ collectionFilterKey = `id:${ normalizedCollectionId } ` ;
500587 } else {
501588 // Default: Exclude trash, but include unorganized (null)
502589 where . OR = [ { collectionId : { not : "trash" } } , { collectionId : null } ] ;
503590 }
504591
505- const drawings = await prisma . drawing . findMany ( {
592+ const shouldIncludeData =
593+ typeof includeData === "string"
594+ ? includeData . toLowerCase ( ) === "true" || includeData === "1"
595+ : false ;
596+
597+ const cacheKey = buildDrawingsCacheKey ( {
598+ searchTerm : searchTerm ?? "" ,
599+ collectionFilter : collectionFilterKey ,
600+ includeData : shouldIncludeData ,
601+ } ) ;
602+
603+ const cachedBody = getCachedDrawingsBody ( cacheKey ) ;
604+ if ( cachedBody ) {
605+ res . setHeader ( "X-Cache" , "HIT" ) ;
606+ res . setHeader ( "Content-Type" , "application/json" ) ;
607+ return res . send ( cachedBody ) ;
608+ }
609+
610+ const summarySelect : Prisma . DrawingSelect = {
611+ id : true ,
612+ name : true ,
613+ collectionId : true ,
614+ preview : true ,
615+ version : true ,
616+ createdAt : true ,
617+ updatedAt : true ,
618+ } ;
619+
620+ const queryOptions : Prisma . DrawingFindManyArgs = {
506621 where,
507622 orderBy : { updatedAt : "desc" } ,
508- } ) ;
623+ } ;
509624
510- // Parse JSON strings for response
511- const parsedDrawings = drawings . map ( ( d : any ) => ( {
512- ...d ,
513- elements : JSON . parse ( d . elements ) ,
514- appState : JSON . parse ( d . appState ) ,
515- files : JSON . parse ( d . files || "{}" ) ,
516- } ) ) ;
625+ if ( ! shouldIncludeData ) {
626+ queryOptions . select = summarySelect ;
627+ }
628+
629+ const drawings = await prisma . drawing . findMany ( queryOptions ) ;
630+
631+ let responsePayload : any = drawings ;
632+
633+ if ( shouldIncludeData ) {
634+ responsePayload = drawings . map ( ( d : any ) => ( {
635+ ...d ,
636+ elements : parseJsonField ( d . elements , [ ] ) ,
637+ appState : parseJsonField ( d . appState , { } ) ,
638+ files : parseJsonField ( d . files , { } ) ,
639+ } ) ) ;
640+ }
517641
518- res . json ( parsedDrawings ) ;
642+ const body = cacheDrawingsResponse ( cacheKey , responsePayload ) ;
643+ res . setHeader ( "X-Cache" , "MISS" ) ;
644+ res . setHeader ( "Content-Type" , "application/json" ) ;
645+ return res . send ( body ) ;
519646 } catch ( error ) {
520647 console . error ( error ) ;
521648 res . status ( 500 ) . json ( { error : "Failed to fetch drawings" } ) ;
@@ -591,6 +718,7 @@ app.post("/drawings", async (req, res) => {
591718 files : JSON . stringify ( payload . files ?? { } ) ,
592719 } ,
593720 } ) ;
721+ invalidateDrawingsCache ( ) ;
594722
595723 res . json ( {
596724 ...newDrawing ,
@@ -668,6 +796,7 @@ app.put("/drawings/:id", async (req, res) => {
668796 where : { id } ,
669797 data,
670798 } ) ;
799+ invalidateDrawingsCache ( ) ;
671800
672801 console . log ( "[API] Update complete" , {
673802 id,
@@ -698,6 +827,7 @@ app.delete("/drawings/:id", async (req, res) => {
698827 try {
699828 const { id } = req . params ;
700829 await prisma . drawing . delete ( { where : { id } } ) ;
830+ invalidateDrawingsCache ( ) ;
701831 res . json ( { success : true } ) ;
702832 } catch ( error ) {
703833 res . status ( 500 ) . json ( { error : "Failed to delete drawing" } ) ;
@@ -724,6 +854,7 @@ app.post("/drawings/:id/duplicate", async (req, res) => {
724854 version : 1 ,
725855 } ,
726856 } ) ;
857+ invalidateDrawingsCache ( ) ;
727858
728859 res . json ( {
729860 ...newDrawing ,
@@ -794,6 +925,7 @@ app.delete("/collections/:id", async (req, res) => {
794925 where : { id } ,
795926 } ) ,
796927 ] ) ;
928+ invalidateDrawingsCache ( ) ;
797929
798930 res . json ( { success : true } ) ;
799931 } catch ( error ) {
@@ -1061,6 +1193,7 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
10611193 await prisma . $disconnect ( ) ;
10621194
10631195 res . json ( { success : true , message : "Database imported successfully" } ) ;
1196+ invalidateDrawingsCache ( ) ;
10641197 } catch ( error ) {
10651198 console . error ( error ) ;
10661199 if ( req . file ) {
0 commit comments