|
| 1 | +import { ORPCError } from '@orpc/client' |
| 2 | +import z from 'zod' |
| 3 | +import { cacheEntrySchema } from '../db' |
| 4 | +import { base } from './base' |
| 5 | + |
| 6 | +export const cacheEntriesRouter = base |
| 7 | + .prefix('/cache-entries') |
| 8 | + .tag('Cache Entries') |
| 9 | + .router({ |
| 10 | + get: base |
| 11 | + .route({ |
| 12 | + method: 'GET', |
| 13 | + path: '/{id}', |
| 14 | + summary: 'Get cache entry', |
| 15 | + description: 'Retrieve a single cache entry by its id.', |
| 16 | + }) |
| 17 | + .input(z.object({ id: z.string() })) |
| 18 | + .output(cacheEntrySchema) |
| 19 | + .errors({ |
| 20 | + NOT_FOUND: { |
| 21 | + message: 'Cache entry not found', |
| 22 | + }, |
| 23 | + }) |
| 24 | + .handler(async ({ input, context }) => { |
| 25 | + const cacheEntry = await context.db |
| 26 | + .selectFrom('cache_entries') |
| 27 | + .where('id', '=', input.id) |
| 28 | + .selectAll() |
| 29 | + .executeTakeFirst() |
| 30 | + if (!cacheEntry) throw new ORPCError('NOT_FOUND') |
| 31 | + |
| 32 | + return cacheEntry |
| 33 | + }), |
| 34 | + match: base |
| 35 | + .route({ |
| 36 | + method: 'GET', |
| 37 | + path: '/match', |
| 38 | + summary: 'Match cache entry', |
| 39 | + description: |
| 40 | + 'Find the best matching cache entry using the primary key and optional restore keys across the given scopes. Returns the matched entry along with the match type, or null if no match is found. Basically what the cache server does when deciding which cache entry to restore for a given cache restore request.', |
| 41 | + }) |
| 42 | + .input( |
| 43 | + z.object({ |
| 44 | + primaryKey: z.string().describe('The primary cache key to match against'), |
| 45 | + restoreKeys: z |
| 46 | + .array(z.string()) |
| 47 | + .optional() |
| 48 | + .describe('Optional fallback keys to try if the primary key does not match'), |
| 49 | + scopes: z.array(z.string()).describe('Scopes to search within, checked in order'), |
| 50 | + version: z.string().describe('Cache version identifier'), |
| 51 | + }), |
| 52 | + ) |
| 53 | + .output( |
| 54 | + z |
| 55 | + .object({ |
| 56 | + match: cacheEntrySchema, |
| 57 | + type: z |
| 58 | + .enum(['exact-primary', 'prefixed-primary', 'exact-restore', 'prefixed-restore']) |
| 59 | + .describe( |
| 60 | + 'How the match was found: exact-primary (exact primary key match), prefixed-primary (primary key prefix match), exact-restore (exact restore key match), prefixed-restore (restore key prefix match)', |
| 61 | + ), |
| 62 | + }) |
| 63 | + .nullable(), |
| 64 | + ) |
| 65 | + .handler(async ({ input, context }) => { |
| 66 | + const cacheEntry = await context.storage.matchCacheEntry({ |
| 67 | + keys: [input.primaryKey, ...(input.restoreKeys ?? [])], |
| 68 | + scopes: input.scopes, |
| 69 | + version: input.version, |
| 70 | + }) |
| 71 | + |
| 72 | + return cacheEntry ?? null |
| 73 | + }), |
| 74 | + findMany: base |
| 75 | + .route({ |
| 76 | + method: 'GET', |
| 77 | + path: '/', |
| 78 | + summary: 'List cache entries', |
| 79 | + description: |
| 80 | + 'Retrieve a paginated list of cache entries, optionally filtered by key, version, and scope.', |
| 81 | + }) |
| 82 | + .input( |
| 83 | + z.object({ |
| 84 | + key: z.string().optional().describe('Filter by exact cache key'), |
| 85 | + version: z.string().optional().describe('Filter by exact cache version'), |
| 86 | + scope: z.string().optional().describe('Filter by exact cache scope'), |
| 87 | + itemsPerPage: z |
| 88 | + .number() |
| 89 | + .int() |
| 90 | + .positive() |
| 91 | + .min(1) |
| 92 | + .max(100) |
| 93 | + .default(20) |
| 94 | + .describe('Number of items per page'), |
| 95 | + page: z.number().int().positive().min(1).default(1).describe('Page number'), |
| 96 | + }), |
| 97 | + ) |
| 98 | + .output( |
| 99 | + z.object({ |
| 100 | + total: z.number(), |
| 101 | + items: z.array(cacheEntrySchema), |
| 102 | + }), |
| 103 | + ) |
| 104 | + .handler(async ({ input, context }) => { |
| 105 | + const query = context.db.selectFrom('cache_entries') |
| 106 | + if (input.key) query.where('key', '=', input.key) |
| 107 | + if (input.version) query.where('version', '=', input.version) |
| 108 | + if (input.scope) query.where('scope', '=', input.scope) |
| 109 | + |
| 110 | + const [cacheEntries, countResult] = await Promise.all([ |
| 111 | + query |
| 112 | + .selectAll() |
| 113 | + .limit(input.itemsPerPage) |
| 114 | + .offset((input.page - 1) * input.itemsPerPage) |
| 115 | + .execute(), |
| 116 | + query.select((eb) => [eb.fn.countAll<number>().as('count')]).executeTakeFirst(), |
| 117 | + ]) |
| 118 | + |
| 119 | + return { |
| 120 | + total: countResult?.count ?? 0, |
| 121 | + items: cacheEntries, |
| 122 | + } |
| 123 | + }), |
| 124 | + delete: base |
| 125 | + .route({ |
| 126 | + method: 'DELETE', |
| 127 | + path: '/{id}', |
| 128 | + summary: 'Delete cache entry', |
| 129 | + description: |
| 130 | + 'Delete a single cache entry by its id. Triggers cleanup of orphaned storage locations.', |
| 131 | + }) |
| 132 | + .input( |
| 133 | + z.object({ |
| 134 | + id: z.string(), |
| 135 | + }), |
| 136 | + ) |
| 137 | + .handler(async ({ input, context }) => { |
| 138 | + await context.db.deleteFrom('cache_entries').where('id', '=', input.id).execute() |
| 139 | + context.event.waitUntil(runTask('cleanup:storage-locations')) |
| 140 | + }), |
| 141 | + deleteMany: base |
| 142 | + .route({ |
| 143 | + method: 'DELETE', |
| 144 | + path: '/', |
| 145 | + summary: 'Delete cache entries', |
| 146 | + description: |
| 147 | + 'Delete multiple cache entries matching the given filters. Triggers cleanup of orphaned storage locations.', |
| 148 | + }) |
| 149 | + .input( |
| 150 | + z.object({ |
| 151 | + key: z.string().optional().describe('Filter by exact cache key'), |
| 152 | + version: z.string().optional().describe('Filter by exact cache version'), |
| 153 | + scope: z.string().optional().describe('Filter by exact cache scope'), |
| 154 | + }), |
| 155 | + ) |
| 156 | + .handler(async ({ input, context }) => { |
| 157 | + const query = context.db.deleteFrom('cache_entries') |
| 158 | + if (input.key) query.where('key', '=', input.key) |
| 159 | + if (input.version) query.where('version', '=', input.version) |
| 160 | + if (input.scope) query.where('scope', '=', input.scope) |
| 161 | + |
| 162 | + await query.execute() |
| 163 | + context.event.waitUntil(runTask('cleanup:storage-locations')) |
| 164 | + }), |
| 165 | + }) |
0 commit comments