diff --git a/src/api/demoUser.js b/src/api/demoUser.ts similarity index 95% rename from src/api/demoUser.js rename to src/api/demoUser.ts index 633dd1b..8721818 100644 --- a/src/api/demoUser.js +++ b/src/api/demoUser.ts @@ -1,3 +1,5 @@ +import type { User } from '@/types/user'; + /** * The shape of a signed-in user, before the server has answered. * @@ -22,7 +24,7 @@ * the server has never stored still has to resolve to something rather than to * `undefined`. */ -export const DEMO_USER = { +export const DEMO_USER: User = { id: 'user_demo', full_name: 'Alex Rivera', email: 'demo@krow.app', diff --git a/src/api/httpClient.js b/src/api/httpClient.ts similarity index 81% rename from src/api/httpClient.js rename to src/api/httpClient.ts index 05d4d6b..ff58c3b 100644 --- a/src/api/httpClient.js +++ b/src/api/httpClient.ts @@ -27,6 +27,9 @@ * did not answer. */ +import type { ApiErrorResponse, KrowApiError } from '@/types/api'; +import type { EntityName, EntityResourcePath } from '@/types/entities'; + /** * Where the API lives. * @@ -93,7 +96,13 @@ if (import.meta.env?.DEV && /^https?:\/\//i.test(API_BASE_URL)) { * `user-activity` is three special cases wearing a trench coat, and a wrong * guess here is a 404 at runtime instead of a mistake anyone can see. */ -const RESOURCE_PATHS = { +/* `Record` is doing two checks at once, and + both were previously unavailable. Every one of the eighteen names must be + present as a key — so this map and `ENTITY_NAMES` in `base44Client.js` can no + longer drift apart silently — and every value must be one of the declared + paths, so a typo in a resource segment is a compile error rather than a 404 + at run time. The object itself is unchanged, key for key. */ +const RESOURCE_PATHS: Record = { JobPosting: 'job-postings', JobApplication: 'job-applications', AIInterview: 'ai-interviews', @@ -128,7 +137,7 @@ const RESOURCE_PATHS = { * `got === undefined` against real values and matched nothing, and sending the * string "undefined" would be a filter on a value no column holds. */ -function queryString(params) { +function queryString(params: Record) { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value === undefined) continue; @@ -153,9 +162,14 @@ function queryString(params) { * ride along as properties — new information a local store never had, and * additive, so nothing that only reads `.message` notices. */ -function apiError(status, payload) { +function apiError(status: number, payload: ApiErrorResponse | null): KrowApiError { const body = payload?.error; - const error = new Error(body?.message || `Request failed with status ${status}`); + /* Cast, not `class KrowApiError extends Error`. The properties below are + assigned onto a plain Error exactly as they always were, so the prototype + chain, `instanceof Error`, and the `error.name === 'KrowApiError'` test + callers use all behave identically. A subclass would change all three, and + the emitted JavaScript with it. */ + const error = new Error(body?.message || `Request failed with status ${status}`) as KrowApiError; error.name = 'KrowApiError'; error.status = status; error.code = body?.code || 'internal'; @@ -171,10 +185,14 @@ function apiError(status, payload) { * reads it (§4.2), and surfacing it would mean changing what the six methods * return, which is the one thing Phase 2D must not do. */ -async function request(method, path, { query, body } = {}) { +async function request( + method: string, + path: string, + { query, body }: { query?: Record; body?: unknown } = {}, +) { const url = `${API_BASE_URL}${path}${query ? queryString(query) : ''}`; - let response; + let response: Response; try { response = await fetch(url, { method, @@ -199,7 +217,7 @@ async function request(method, path, { query, body } = {}) { `Cannot reach the Krow API at ${API_BASE_URL}. Is the Go API running on ` + `127.0.0.1:8080, and is the Vite dev server proxying /api to it? ` + `(${method} ${path})` - ); + ) as KrowApiError; error.name = 'KrowApiError'; error.status = 0; error.code = 'unreachable'; @@ -221,7 +239,7 @@ async function request(method, path, { query, body } = {}) { if (!response.ok) throw apiError(response.status, payload); if (payload === null) { - const error = new Error(`${method} ${path} returned no JSON body`); + const error = new Error(`${method} ${path} returned no JSON body`) as KrowApiError; error.name = 'KrowApiError'; error.status = response.status; error.code = 'internal'; @@ -242,7 +260,7 @@ export { request }; * before the first login and after a session expires — so callers need to tell * it apart from a real failure rather than treating both as "something broke". */ -export function isUnauthenticated(error) { +export function isUnauthenticated(error: any) { return Boolean(error) && (error.status === 401 || error.code === 'unauthorized'); } @@ -256,13 +274,33 @@ export function isUnauthenticated(error) { * `list(sort = '-created_date', limit = 100)`, and several call sites rely on * them rather than passing their own. */ -export function createEntity(name) { - const path = RESOURCE_PATHS[name]; +export function createEntity(name: string) { + /* `name` stays `string`, and the cast below is deliberate. Typing the + parameter as `EntityName` would be tighter and would immediately break the + one caller — `base44Client.js` builds the entity map by mapping over a + plain string array — which is a file this phase does not touch. + + The return type is left inferred, and that is a decision rather than an + omission. Annotating it `EntityClient` was tried and reverted: it + makes `list()` return `Promise` where inference gives + `Promise`, and two call sites in `src/lib/agents/agentStore.js` do + + const rows = qc.getQueryData(KEY) || (await …list('-created_date', 200)); + + React Query types `getQueryData` as `unknown`, and `unknown || any` + collapses to `any` while `unknown || any[]` stays `unknown` — so `.find` + on the next line stopped compiling. Two new errors in a file this phase is + not migrating, for no gain: the surface is structurally identical either + way, and the emitted JavaScript is byte-for-byte the same. + + `EntityClientFor` and the generated record types are ready for the phase + that migrates those consumers. Nothing here has to change then. */ + const path = RESOURCE_PATHS[name as EntityName]; if (!path) throw new Error(`No API resource path is declared for entity ${name}`); const base = `/${path}`; return { - entityName: name, + entityName: name as EntityName, /** * `sort` is always sent, even when empty. `?sort=` is not the same as