setup boilerplate for data and rendering

This includes zod types creation for API response validation, and
exploration of path based resolution of rendering particles.
This commit is contained in:
talksik
2026-03-17 14:23:51 -07:00
parent 60330f65e0
commit 4273e324e5
19 changed files with 729 additions and 592 deletions
+90 -91
View File
@@ -1,17 +1,19 @@
import { useSessionStore } from "@/stores/session-store";
import type { z } from "zod";
import {
DepotObjectSchema,
HumanSchema,
ListNetworksResponseSchema,
NetworkSchema,
PrepareUploadResponseSchema,
SignInResponseSchema,
} from "./types";
import type {
CreateStreamParticleRequest,
CreateStreamRequest,
Human,
MarkSeenBatchRequest,
AddMembersRequest,
CreateNetworkRequest,
PrepareUploadRequest,
PrepareUploadResponse,
RequestCodeRequest,
SignInRequest,
SignInResponse,
StartupResponse,
Stream,
StreamParticle,
} from "./types";
export class ApiError extends Error {
@@ -37,11 +39,11 @@ class ApiClient {
this.config = config;
}
private async request<T>(
private async fetch(
method: string,
path: string,
body?: unknown,
): Promise<T> {
): Promise<Response> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
@@ -67,109 +69,106 @@ class ApiClient {
throw new ApiError(response.status, text);
}
if (response.status === 204) {
return undefined as T;
}
return response;
}
return response.json() as Promise<T>;
private async request<T>(
schema: z.ZodType<T>,
method: string,
path: string,
body?: unknown,
): Promise<T> {
const response = await this.fetch(method, path, body);
const json = await response.json();
return schema.parse(json);
}
private async requestVoid(
method: string,
path: string,
body?: unknown,
): Promise<void> {
await this.fetch(method, path, body);
}
// --- Auth ---
async requestCode(data: RequestCodeRequest): Promise<void> {
await this.request<void>("POST", "/auth/request-code", data);
await this.requestVoid("POST", "/auth/request-code", data);
}
async signIn(data: SignInRequest): Promise<SignInResponse> {
return this.request<SignInResponse>("POST", "/auth/sign-in", data);
async signIn(data: SignInRequest) {
return this.request(SignInResponseSchema, "POST", "/auth/sign-in", data);
}
async me(): Promise<Human> {
return this.request<Human>("GET", "/auth/me");
async me() {
return this.request(HumanSchema, "GET", "/auth/me");
}
async signOut(): Promise<void> {
await this.request<void>("POST", "/auth/sign-out");
await this.requestVoid("POST", "/auth/sign-out");
}
// --- Startup ---
async startup(): Promise<StartupResponse> {
return this.request<StartupResponse>("GET", "/startup");
}
// --- Streams ---
async createStream(
networkId: string,
data: CreateStreamRequest,
): Promise<Stream> {
return this.request<Stream>(
"POST",
`/networks/${networkId}/streams`,
data,
// TODO: security: require passing in the particle id once api deprecates this
async getParticleDownloadUrl(objectId: string): Promise<string> {
const response = await this.fetch(
"GET",
`/particles/${objectId}/download`,
);
}
async createStreamParticle(
streamId: string,
data: CreateStreamParticleRequest,
): Promise<StreamParticle> {
return this.request<StreamParticle>(
"POST",
`/streams/${streamId}/particles`,
data,
);
}
// --- Particles ---
async markSeen(particleId: string): Promise<void> {
await this.request<void>("POST", `/particles/${particleId}/seen`);
}
async ackParticle(particleId: string): Promise<void> {
await this.request<void>("POST", `/particles/${particleId}/ack`);
}
async markSeenBatch(data: MarkSeenBatchRequest): Promise<void> {
await this.request<void>("POST", "/particles/seen", data);
}
async getParticleDownloadUrl(particleId: string): Promise<string> {
const token = this.config.getToken();
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch(
`${this.config.baseUrl}/particles/${particleId}/download`,
{ headers, redirect: "follow" },
);
if (response.status === 401) {
this.config.onUnauthorized();
throw new ApiError(401, "Unauthorized");
}
if (!response.ok) {
throw new ApiError(response.status, "Failed to get download URL");
}
return response.url;
}
// --- Depot ---
async prepareUpload(
data: PrepareUploadRequest,
): Promise<PrepareUploadResponse> {
return this.request<PrepareUploadResponse>("POST", "/depot/upload", data);
async prepareUpload(data: PrepareUploadRequest) {
return this.request(
PrepareUploadResponseSchema,
"POST",
"/depot/upload",
data,
);
}
async confirmUpload(objectId: string): Promise<void> {
await this.request<void>("POST", `/depot/objects/${objectId}/confirm`);
async confirmUpload(objectId: string) {
return this.request(
DepotObjectSchema,
"POST",
`/depot/objects/${objectId}/confirm`,
);
}
// --- Networks ---
async listNetworks() {
return this.request(
ListNetworksResponseSchema,
"GET",
"/networks",
);
}
async createNetwork(data: CreateNetworkRequest) {
return this.request(NetworkSchema, "POST", "/networks", data);
}
async getNetwork(id: string) {
return this.request(NetworkSchema, "GET", `/networks/${id}`);
}
async addMembers(networkId: string, data: AddMembersRequest): Promise<void> {
await this.requestVoid(
"POST",
`/networks/${networkId}/members`,
data,
);
}
async removeMember(networkId: string, email: string): Promise<void> {
await this.requestVoid(
"DELETE",
`/networks/${networkId}/members/${email}`,
);
}
}