From f96f128839a282f8a7b89affb02b04925b24dace Mon Sep 17 00:00:00 2001 From: Aravind Date: Thu, 17 Sep 2026 12:16:16 +0530 Subject: [PATCH] chore(ts-migration): migrate API transport to TypeScript Phase 4a: `demoUser` and `httpClient`. Type-only. The production bundle is byte-identical - built from this commit's parent and from this commit, all six chunk hashes match. `httpClient` is the contract boundary, so the annotations are deliberately conservative: - `KrowApiError` is a CAST, not a class. The three error sites still build a plain `Error` and assign `.name`, `.status`, `.code` and `.details` onto it exactly as before. `class KrowApiError extends Error` would have read better and changed three things that callers depend on: the prototype chain, `instanceof`, and how `name` comes to be set. - `RESOURCE_PATHS` becomes `Record`. That is the first thing in the repo to check the eighteen entity names against the eighteen in `ENTITY_NAMES`; until now the two lists agreed only by habit, and a divergence would have surfaced as `base44.entities.Whatever` being undefined with nothing to say why. - `createEntity`'s parameter stays `string` and its return stays inferred. Annotating the return `EntityClient` was tried and reverted: it makes `list()` return `Promise` where inference gives `Promise`, and `agentStore.js` does `qc.getQueryData(KEY) || (await ...list(...))`. React Query types `getQueryData` as `unknown`; `unknown || any` collapses to `any`, `unknown || any[]` stays `unknown`, and `.find` on the next line stopped compiling. Two new errors in a file this phase does not migrate, for no gain. `EntityClientFor` is ready for the phase that migrates those consumers. Unchanged and verified in the emitted output: `credentials: 'include'`, both header branches on `body === undefined`, the URLSearchParams query encoding with its repeated-array and undefined-omission rules, the `payload.data` unwrap that drops `meta`, the verbatim server message, `status: 0` / `code: 'unreachable'` for a transport failure, the `-created_date` and `limit` defaults, path construction through `encodeURIComponent`, and `bulkCreate`'s sequential `this.create` loop. `DEMO_USER` is annotated `User`, which does real work: unannotated, `role: 'admin'` widens to `string` and the default shape did not satisfy the type the app uses for the thing it defaults. Verified in isolation from the parallel feature work (d1425f9 plus these two files): tsc 64 -> 53, the eleven removed being exactly this file's, and a set-difference against the baseline showing none added. skill-check 1641/1642 with only the known stale-fixture failure, Owliver baseline 59/59, lint 0 errors, build succeeds with the API origin inlined. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- src/api/{demoUser.js => demoUser.ts} | 4 +- src/api/{httpClient.js => httpClient.ts} | 62 +++++++++++++++++++----- 2 files changed, 53 insertions(+), 13 deletions(-) rename src/api/{demoUser.js => demoUser.ts} (95%) rename src/api/{httpClient.js => httpClient.ts} (81%) 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