Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions client/Api.ts

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions client/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Copyright Oxide Computer Company
*/

import { camelToSnake, processResponseBody, snakeify, isNotNull } from "./util";
import { camelToSnake, camelifyKeys, snakeifyKeys, isNotNull } from "./util";

/** Success responses from the API */
export type ApiSuccess<Data> = {
Expand Down Expand Up @@ -64,7 +64,9 @@ function encodeQueryParam(key: string, value: unknown) {
}

export async function handleResponse<Data>(
response: Response
response: Response,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformResponse?: (o: any) => void
): Promise<ApiResult<Data>> {
const common = { statusCode: response.status, headers: response.headers };

Expand All @@ -75,8 +77,13 @@ export async function handleResponse<Data>(
try {
// don't bother trying to parse empty responses like 204s
// TODO: is empty object what we want here?
respJson =
respText.length > 0 ? processResponseBody(JSON.parse(respText)) : {};
if (respText.length > 0) {
respJson = JSON.parse(respText);
transformResponse?.(respJson); // no assignment because this mutates the object
respJson = camelifyKeys(respJson);
} else {
respJson = {};
}
} catch (e) {
return {
type: "client_error",
Expand Down Expand Up @@ -120,6 +127,8 @@ export interface FullParams extends FetchParams {
body?: unknown;
host?: string;
method?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformResponse?: (o: any) => void;
}

export interface ApiConfig {
Expand Down Expand Up @@ -153,14 +162,15 @@ export class HttpClient {
path,
query,
host,
transformResponse,
...fetchParams
}: FullParams): Promise<ApiResult<Data>> {
const url = (host || this.host) + path + toQueryString(query);
const init = {
...mergeParams(this.baseParams, fetchParams),
body: JSON.stringify(snakeify(body), replacer),
body: JSON.stringify(snakeifyKeys(body), replacer),
};
return handleResponse(await fetch(url, init));
return handleResponse(await fetch(url, init), transformResponse);
}
}

Expand Down
4 changes: 2 additions & 2 deletions client/msw-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
} from "type-fest";
import { type ZodSchema } from "zod";
import type * as Api from "./Api";
import { snakeify } from "./util";
import { snakeifyKeys } from "./util";
import * as schema from "./validate";

type HandlerResult<T> = Json<T> | StrictResponse<Json<T>>;
Expand Down Expand Up @@ -1255,7 +1255,7 @@ const handler =
let body = undefined;
if (bodySchema) {
const rawBody = await req.json();
const result = bodySchema.transform(snakeify).safeParse(rawBody);
const result = bodySchema.transform(snakeifyKeys).safeParse(rawBody);
if (!result.success) return json(result.error.issues, { status: 400 });
body = result.data;
}
Expand Down
13 changes: 2 additions & 11 deletions client/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,9 @@ export const mapObj =
return newObj;
};

export const parseIfDate = (k: string | undefined, v: unknown) => {
if (typeof v === "string" && (k?.startsWith("time_") || k === "timestamp")) {
const d = new Date(v);
if (isNaN(d.getTime())) return v;
return d;
}
return v;
};
export const snakeifyKeys = mapObj(camelToSnake);

export const snakeify = mapObj(camelToSnake);

export const processResponseBody = mapObj(snakeToCamel, parseIfDate);
export const camelifyKeys = mapObj(snakeToCamel);

export function isNotNull<T>(value: T): value is NonNullable<T> {
return value != null;
Expand Down
Loading