+37
-4
netlify/functions/get-upload-details.ts
+37
-4
netlify/functions/get-upload-details.ts
···
3
3
import { getDbClient } from './db';
4
4
import cookie from 'cookie';
5
5
6
+
const DEFAULT_PAGE_SIZE = 50;
7
+
const MAX_PAGE_SIZE = 100;
8
+
6
9
export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => {
7
10
try {
8
11
const uploadId = event.queryStringParameters?.uploadId;
12
+
const page = parseInt(event.queryStringParameters?.page || '1');
13
+
const pageSize = Math.min(
14
+
parseInt(event.queryStringParameters?.pageSize || String(DEFAULT_PAGE_SIZE)),
15
+
MAX_PAGE_SIZE
16
+
);
9
17
10
18
if (!uploadId) {
11
19
return {
···
15
23
};
16
24
}
17
25
26
+
if (page < 1 || pageSize < 1) {
27
+
return {
28
+
statusCode: 400,
29
+
headers: { 'Content-Type': 'application/json' },
30
+
body: JSON.stringify({ error: 'Invalid page or pageSize parameters' }),
31
+
};
32
+
}
33
+
18
34
// Get session from cookie
19
35
const cookies = event.headers.cookie ? cookie.parse(event.headers.cookie) : {};
20
36
const sessionId = cookies.atlast_session;
···
39
55
40
56
const sql = getDbClient();
41
57
42
-
// Verify upload belongs to user
58
+
// Verify upload belongs to user and get total count
43
59
const uploadCheck = await sql`
44
-
SELECT upload_id FROM user_uploads
60
+
SELECT upload_id, total_users FROM user_uploads
45
61
WHERE upload_id = ${uploadId} AND did = ${userSession.did}
46
62
`;
47
63
···
53
69
};
54
70
}
55
71
56
-
// Fetch detailed results for this upload
72
+
const totalUsers = (uploadCheck as any[])[0].total_users;
73
+
const totalPages = Math.ceil(totalUsers / pageSize);
74
+
const offset = (page - 1) * pageSize;
75
+
76
+
// Fetch paginated results with optimized query
57
77
const results = await sql`
58
78
SELECT
59
79
sa.source_username,
···
72
92
LEFT JOIN user_match_status ums ON am.id = ums.atproto_match_id AND ums.did = ${userSession.did}
73
93
WHERE usf.upload_id = ${uploadId}
74
94
ORDER BY sa.source_username
95
+
LIMIT ${pageSize}
96
+
OFFSET ${offset}
75
97
`;
76
98
77
99
// Group results by source username
···
110
132
headers: {
111
133
'Content-Type': 'application/json',
112
134
'Access-Control-Allow-Origin': '*',
135
+
'Cache-Control': 'private, max-age=600', // 10 minute browser cache
113
136
},
114
-
body: JSON.stringify({ results: searchResults }),
137
+
body: JSON.stringify({
138
+
results: searchResults,
139
+
pagination: {
140
+
page,
141
+
pageSize,
142
+
totalPages,
143
+
totalUsers,
144
+
hasNextPage: page < totalPages,
145
+
hasPrevPage: page > 1
146
+
}
147
+
}),
115
148
};
116
149
117
150
} catch (error) {
+53
-12
src/lib/apiClient.ts
+53
-12
src/lib/apiClient.ts
···
164
164
return data;
165
165
},
166
166
167
-
async getUploadDetails(uploadId: string): Promise<{
167
+
async getUploadDetails(
168
+
uploadId: string,
169
+
page: number = 1,
170
+
pageSize: number = 50
171
+
): Promise<{
168
172
results: SearchResult[];
173
+
pagination?: {
174
+
page: number;
175
+
pageSize: number;
176
+
totalPages: number;
177
+
totalUsers: number;
178
+
hasNextPage: boolean;
179
+
hasPrevPage: boolean;
180
+
};
169
181
}> {
170
-
// Check cache first
171
-
const cacheKey = `upload-details-${uploadId}`;
172
-
const cached = cache.get<any>(cacheKey, 10 * 60 * 1000); // 10 minute cache for specific upload
182
+
// Check cache first (cache by page)
183
+
const cacheKey = `upload-details-${uploadId}-p${page}-s${pageSize}`;
184
+
const cached = cache.get<any>(cacheKey, 10 * 60 * 1000);
173
185
if (cached) {
174
-
console.log('Returning cached upload details for', uploadId);
186
+
console.log('Returning cached upload details for', uploadId, 'page', page);
175
187
return cached;
176
188
}
177
189
178
-
const res = await fetch(`/.netlify/functions/get-upload-details?uploadId=${uploadId}`, {
179
-
credentials: 'include'
180
-
});
190
+
const res = await fetch(
191
+
`/.netlify/functions/get-upload-details?uploadId=${uploadId}&page=${page}&pageSize=${pageSize}`,
192
+
{ credentials: 'include' }
193
+
);
181
194
182
195
if (!res.ok) {
183
196
throw new Error('Failed to fetch upload details');
···
185
198
186
199
const data = await res.json();
187
200
188
-
// Cache upload details for 10 minutes
201
+
// Cache upload details page for 10 minutes
189
202
cache.set(cacheKey, data, 10 * 60 * 1000);
190
203
191
204
return data;
192
205
},
193
206
207
+
// Helper to load all pages (for backwards compatibility)
208
+
async getAllUploadDetails(uploadId: string): Promise<{ results: SearchResult[] }> {
209
+
const firstPage = await this.getUploadDetails(uploadId, 1, 100);
210
+
211
+
if (!firstPage.pagination || firstPage.pagination.totalPages === 1) {
212
+
return { results: firstPage.results };
213
+
}
214
+
215
+
// Load remaining pages
216
+
const allResults = [...firstPage.results];
217
+
const promises = [];
218
+
219
+
for (let page = 2; page <= firstPage.pagination.totalPages; page++) {
220
+
promises.push(this.getUploadDetails(uploadId, page, 100));
221
+
}
222
+
223
+
const remainingPages = await Promise.all(promises);
224
+
for (const pageData of remainingPages) {
225
+
allResults.push(...pageData.results);
226
+
}
227
+
228
+
return { results: allResults };
229
+
},
230
+
194
231
// Search Operations
195
232
async batchSearchActors(usernames: string[]): Promise<{ results: BatchSearchResult[] }> {
196
233
// Create cache key from sorted usernames (so order doesn't matter)
197
234
const cacheKey = `search-${usernames.slice().sort().join(',')}`;
198
-
const cached = cache.get<any>(cacheKey, 10 * 60 * 1000); // 10 minute cache for search results
235
+
const cached = cache.get<any>(cacheKey, 10 * 60 * 1000);
199
236
if (cached) {
200
237
console.log('Returning cached search results for', usernames.length, 'users');
201
238
return cached;
···
241
278
242
279
const data = await res.json();
243
280
281
+
// Invalidate uploads cache after following
282
+
cache.invalidate('uploads');
283
+
cache.invalidatePattern('upload-details');
284
+
244
285
return data;
245
286
},
246
287
···
275
316
const data = await res.json();
276
317
console.log(`Successfully saved ${data.matchedUsers} matches`);
277
318
278
-
// Invalidate uploads cache after saving
319
+
// Invalidate caches after saving
279
320
cache.invalidate('uploads');
280
-
cache.set(`upload-details-${uploadId}`, { results }, 10 * 60 * 1000);
321
+
cache.invalidatePattern('upload-details');
281
322
282
323
return data;
283
324
} else {