a cache for slack profile pictures and emojis
1/**
2 * Complete typed route definitions for all Cachet API endpoints
3 */
4
5import type { SlackCache } from "../cache";
6import * as handlers from "../handlers";
7import { createAnalyticsWrapper } from "../lib/analytics-wrapper";
8import type { SlackWrapper } from "../slackWrapper";
9import {
10 apiResponse,
11 createRoute,
12 pathParam,
13 queryParam,
14} from "../types/routes";
15
16// Factory function to create all routes with injected dependencies
17export function createApiRoutes(cache: SlackCache, slackApp: SlackWrapper) {
18 // Inject dependencies into handlers
19 handlers.injectDependencies(cache, slackApp);
20
21 const withAnalytics = createAnalyticsWrapper(cache);
22
23 return {
24 "/health": {
25 GET: createRoute(
26 withAnalytics("/health", "GET", handlers.handleHealthCheck),
27 {
28 summary: "Health check",
29 description:
30 "Check if the service is healthy and operational. Add ?detailed=true for comprehensive health information including Slack API status, queue depth, and memory usage.",
31 tags: ["Health"],
32 parameters: {
33 query: [
34 queryParam(
35 "detailed",
36 "boolean",
37 "Return detailed health check information",
38 false,
39 false,
40 ),
41 ],
42 },
43 responses: Object.fromEntries([
44 apiResponse(200, "Service is healthy", {
45 type: "object",
46 properties: {
47 status: {
48 type: "string",
49 example: "healthy",
50 enum: ["healthy", "degraded", "unhealthy"],
51 },
52 cache: { type: "boolean", example: true },
53 uptime: { type: "number", example: 123456 },
54 checks: {
55 type: "object",
56 description: "Detailed checks (only with ?detailed=true)",
57 properties: {
58 database: {
59 type: "object",
60 properties: {
61 status: { type: "boolean" },
62 latency: { type: "number", description: "ms" },
63 },
64 },
65 slackApi: {
66 type: "object",
67 properties: {
68 status: { type: "boolean" },
69 error: { type: "string" },
70 },
71 },
72 queueDepth: {
73 type: "number",
74 description: "Number of users queued for update",
75 },
76 memoryUsage: {
77 type: "object",
78 properties: {
79 heapUsed: { type: "number", description: "MB" },
80 heapTotal: { type: "number", description: "MB" },
81 percentage: { type: "number" },
82 details: {
83 type: "object",
84 properties: {
85 heapUsedMiB: {
86 type: "number",
87 description: "Precise heap used in MiB",
88 },
89 heapTotalMiB: {
90 type: "number",
91 description: "Precise heap total in MiB",
92 },
93 heapPercent: {
94 type: "number",
95 description: "Precise heap percentage",
96 },
97 rssMiB: {
98 type: "number",
99 description: "Resident Set Size in MiB",
100 },
101 externalMiB: {
102 type: "number",
103 description: "External memory in MiB",
104 },
105 arrayBuffersMiB: {
106 type: "number",
107 description: "Array buffers in MiB",
108 },
109 },
110 },
111 },
112 },
113 },
114 },
115 },
116 }),
117 apiResponse(503, "Service is unhealthy"),
118 ]),
119 },
120 ),
121 },
122
123 "/users/:id": {
124 GET: createRoute(
125 withAnalytics("/users/:id", "GET", handlers.handleGetUser),
126 {
127 summary: "Get user information",
128 description: "Retrieve cached user profile information from Slack",
129 tags: ["Users"],
130 parameters: {
131 path: [pathParam("id", "string", "Slack user ID", "U062UG485EE")],
132 },
133 responses: Object.fromEntries([
134 apiResponse(200, "User information retrieved successfully", {
135 type: "object",
136 properties: {
137 id: { type: "string", example: "U062UG485EE" },
138 userId: { type: "string", example: "U062UG485EE" },
139 displayName: { type: "string", example: "Kieran Klukas" },
140 pronouns: { type: "string", example: "he/him" },
141 imageUrl: {
142 type: "string",
143 example: "https://avatars.slack-edge.com/...",
144 },
145 },
146 }),
147 apiResponse(404, "User not found"),
148 ]),
149 },
150 ),
151 },
152
153 "/users/:id/r": {
154 GET: createRoute(
155 withAnalytics("/users/:id/r", "GET", handlers.handleUserRedirect),
156 {
157 summary: "Redirect to user profile image",
158 description: "Direct redirect to the user's cached profile image URL",
159 tags: ["Users"],
160 parameters: {
161 path: [pathParam("id", "string", "Slack user ID", "U062UG485EE")],
162 },
163 responses: Object.fromEntries([
164 apiResponse(302, "Redirect to user image"),
165 apiResponse(307, "Temporary redirect to default avatar"),
166 apiResponse(404, "User not found"),
167 ]),
168 },
169 ),
170 },
171
172 "/users/:id/purge": {
173 POST: createRoute(
174 withAnalytics("/users/:id/purge", "POST", handlers.handlePurgeUser),
175 {
176 summary: "Purge user cache",
177 description:
178 "Remove a specific user from the cache (requires authentication)",
179 tags: ["Users", "Admin"],
180 requiresAuth: true,
181 parameters: {
182 path: [
183 pathParam(
184 "id",
185 "string",
186 "Slack user ID to purge",
187 "U062UG485EE",
188 ),
189 ],
190 },
191 responses: Object.fromEntries([
192 apiResponse(200, "User cache purged successfully", {
193 type: "object",
194 properties: {
195 message: { type: "string", example: "User cache purged" },
196 userId: { type: "string", example: "U062UG485EE" },
197 success: { type: "boolean", example: true },
198 },
199 }),
200 apiResponse(401, "Unauthorized"),
201 ]),
202 },
203 ),
204 },
205
206 "/emojis": {
207 GET: createRoute(
208 withAnalytics("/emojis", "GET", handlers.handleListEmojis),
209 {
210 summary: "List all emojis",
211 description:
212 "Get a list of all cached custom emojis from the Slack workspace",
213 tags: ["Emojis"],
214 responses: Object.fromEntries([
215 apiResponse(200, "List of emojis retrieved successfully", {
216 type: "array",
217 items: {
218 type: "object",
219 properties: {
220 name: { type: "string", example: "hackshark" },
221 imageUrl: {
222 type: "string",
223 example: "https://emoji.slack-edge.com/...",
224 },
225 alias: { type: "string", nullable: true, example: null },
226 },
227 },
228 }),
229 ]),
230 },
231 ),
232 },
233
234 "/emojis/:name": {
235 GET: createRoute(
236 withAnalytics("/emojis/:name", "GET", handlers.handleGetEmoji),
237 {
238 summary: "Get emoji information",
239 description: "Retrieve information about a specific custom emoji",
240 tags: ["Emojis"],
241 parameters: {
242 path: [
243 pathParam(
244 "name",
245 "string",
246 "Emoji name (without colons)",
247 "hackshark",
248 ),
249 ],
250 },
251 responses: Object.fromEntries([
252 apiResponse(200, "Emoji information retrieved successfully", {
253 type: "object",
254 properties: {
255 name: { type: "string", example: "hackshark" },
256 imageUrl: {
257 type: "string",
258 example: "https://emoji.slack-edge.com/...",
259 },
260 alias: { type: "string", nullable: true, example: null },
261 },
262 }),
263 apiResponse(404, "Emoji not found"),
264 ]),
265 },
266 ),
267 },
268
269 "/emojis/:name/r": {
270 GET: createRoute(
271 withAnalytics("/emojis/:name/r", "GET", handlers.handleEmojiRedirect),
272 {
273 summary: "Redirect to emoji image",
274 description: "Direct redirect to the emoji's cached image URL",
275 tags: ["Emojis"],
276 parameters: {
277 path: [
278 pathParam(
279 "name",
280 "string",
281 "Emoji name (without colons)",
282 "hackshark",
283 ),
284 ],
285 },
286 responses: Object.fromEntries([
287 apiResponse(302, "Redirect to emoji image"),
288 apiResponse(404, "Emoji not found"),
289 ]),
290 },
291 ),
292 },
293
294 "/reset": {
295 POST: createRoute(
296 withAnalytics("/reset", "POST", handlers.handleResetCache),
297 {
298 summary: "Reset entire cache",
299 description: "Clear all cached data (requires authentication)",
300 tags: ["Admin"],
301 requiresAuth: true,
302 responses: Object.fromEntries([
303 apiResponse(200, "Cache reset successfully", {
304 type: "object",
305 properties: {
306 message: { type: "string", example: "Cache has been reset" },
307 users: { type: "number", example: 42 },
308 emojis: { type: "number", example: 1337 },
309 },
310 }),
311 apiResponse(401, "Unauthorized"),
312 ]),
313 },
314 ),
315 },
316
317 "/api/stats/essential": {
318 GET: createRoute(
319 withAnalytics(
320 "/api/stats/essential",
321 "GET",
322 handlers.handleGetEssentialStats,
323 ),
324 {
325 summary: "Get essential analytics",
326 description: "Fast-loading essential statistics for the dashboard",
327 tags: ["Analytics"],
328 parameters: {
329 query: [
330 queryParam(
331 "days",
332 "number",
333 "Number of days to analyze",
334 false,
335 7,
336 ),
337 ],
338 },
339 responses: Object.fromEntries([
340 apiResponse(200, "Essential stats retrieved successfully", {
341 type: "object",
342 properties: {
343 totalRequests: { type: "number", example: 12345 },
344 averageResponseTime: { type: "number", example: 23.5 },
345 uptime: { type: "number", example: 99.9 },
346 period: { type: "string", example: "7 days" },
347 },
348 }),
349 ]),
350 },
351 ),
352 },
353
354 "/api/stats/charts": {
355 GET: createRoute(
356 withAnalytics("/api/stats/charts", "GET", handlers.handleGetChartData),
357 {
358 summary: "Get chart data",
359 description: "Time-series data for request and latency charts",
360 tags: ["Analytics"],
361 parameters: {
362 query: [
363 queryParam(
364 "days",
365 "number",
366 "Number of days to analyze",
367 false,
368 7,
369 ),
370 ],
371 },
372 responses: Object.fromEntries([
373 apiResponse(200, "Chart data retrieved successfully", {
374 type: "array",
375 items: {
376 type: "object",
377 properties: {
378 time: { type: "string", example: "2024-01-01T12:00:00Z" },
379 count: { type: "number", example: 42 },
380 averageResponseTime: { type: "number", example: 25.3 },
381 },
382 },
383 }),
384 ]),
385 },
386 ),
387 },
388
389 "/api/stats/useragents": {
390 GET: createRoute(
391 withAnalytics(
392 "/api/stats/useragents",
393 "GET",
394 handlers.handleGetUserAgents,
395 ),
396 {
397 summary: "Get user agents statistics",
398 description:
399 "List of user agents accessing the service with request counts",
400 tags: ["Analytics"],
401 parameters: {
402 query: [
403 queryParam(
404 "days",
405 "number",
406 "Number of days to analyze",
407 false,
408 7,
409 ),
410 ],
411 },
412 responses: Object.fromEntries([
413 apiResponse(200, "User agents data retrieved successfully", {
414 type: "array",
415 items: {
416 type: "object",
417 properties: {
418 userAgent: { type: "string", example: "Mozilla/5.0..." },
419 count: { type: "number", example: 123 },
420 },
421 },
422 }),
423 ]),
424 },
425 ),
426 },
427
428 "/stats": {
429 GET: createRoute(
430 withAnalytics("/stats", "GET", handlers.handleGetStats),
431 {
432 summary: "Get complete analytics (legacy)",
433 description:
434 "Legacy endpoint returning all analytics data in one response",
435 tags: ["Analytics", "Legacy"],
436 parameters: {
437 query: [
438 queryParam(
439 "days",
440 "number",
441 "Number of days to analyze",
442 false,
443 7,
444 ),
445 ],
446 },
447 responses: Object.fromEntries([
448 apiResponse(200, "Complete analytics data retrieved", {
449 type: "object",
450 properties: {
451 totalRequests: { type: "number" },
452 averageResponseTime: { type: "number" },
453 chartData: { type: "array" },
454 userAgents: { type: "array" },
455 },
456 }),
457 ]),
458 },
459 ),
460 },
461 };
462}