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<EntityName, EntityResourcePath>`. 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<any>` was tried and reverted: it makes
`list()` return `Promise<any[]>` where inference gives `Promise<any>`, 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<K>` 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
@@ -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',
|
||||
@@ -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<EntityName, EntityResourcePath>` 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<EntityName, EntityResourcePath> = {
|
||||
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<string, unknown>) {
|
||||
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<string, unknown>; 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<any>` was tried and reverted: it
|
||||
makes `list()` return `Promise<any[]>` where inference gives
|
||||
`Promise<any>`, 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<K>` 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
|
||||
Reference in New Issue
Block a user