Created
September 18, 2025 18:00
-
-
Save vastus/c93c8bd63a9caa330f1a810e278b498f to your computer and use it in GitHub Desktop.
Zod usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Result} from '@badrap/result' | |
| import {ErrorMeta, NotFoundError, StatusError} from 'app/errors' | |
| import {has, merge} from 'lodash/fp' | |
| import {z} from 'zod' | |
| export const zAPIError = z.object({ | |
| error: z.object({ | |
| message: z.string().min(1), | |
| meta: z.record(z.string(), z.unknown()).nullish(), | |
| }), | |
| }) | |
| export type APIError = z.infer<typeof zAPIError> | |
| export const zAPISpace = z.object({ | |
| space_id: z.string(), | |
| name: z.string(), | |
| }) | |
| export type APISpace = z.infer<typeof zAPISpace> | |
| export const zAPIUser = z.object({ | |
| user_id: z.string(), | |
| email: z.string(), | |
| name: z.string(), | |
| // inserted_at: z.string().datetime(), | |
| // updated_at: z.string().datetime(), | |
| }) | |
| export type APIUser = z.infer<typeof zAPIUser> | |
| export const zAPIMessage = z.object({ | |
| user_id: z.string(), | |
| channel_id: z.string(), | |
| message_id: z.string(), | |
| content: z.string(), | |
| inserted_at: z.string().datetime(), | |
| updated_at: z.string(), | |
| user: zAPIUser.pick({email: true, name: true}), | |
| }) | |
| export type APIMessage = z.infer<typeof zAPIMessage> | |
| export const zAPIAuthWebTokenResp = z.object({ | |
| token: z.string().min(3), | |
| }) | |
| export type APIAuthWebTokenResp = z.infer<typeof zAPIAuthWebTokenResp> | |
| export const zAPIChannel = z.object({ | |
| space_id: z.string(), | |
| channel_id: z.string(), | |
| name: z.string(), | |
| // inserted_at: z.string().datetime(), | |
| // updated_at: z.string().datetime(), | |
| }) | |
| export type APIChannel = z.infer<typeof zAPIChannel> | |
| export const zAPISpaceList = z.array(zAPISpace) | |
| export type APISpaceList = z.infer<typeof zAPISpaceList> | |
| export const zAPISpaceShow = zAPISpace.extend({ | |
| channels: z.array(zAPIChannel), | |
| }) | |
| export type APISpaceShow = z.infer<typeof zAPISpaceShow> | |
| export async function doFetch<T extends z.ZodType>( | |
| url: string, | |
| schema: T, | |
| init: RequestInit = {}, | |
| ): Promise<Result<z.infer<T>>> { | |
| const response = await fetch(url, init) | |
| // API must always return JSON | |
| const body = await response.json() | |
| if (!response.ok) { | |
| const parsed = zAPIError.safeParse(body) | |
| if (!parsed.success) { | |
| return Result.err( | |
| new ErrorMeta(`request failed: unable to parse API error`, { | |
| parsedData: body, | |
| parseError: parsed.error, | |
| }), | |
| ) | |
| } | |
| const status = response.status | |
| const {message, meta} = parsed.data.error | |
| if (status === 404) { | |
| return Result.err(new NotFoundError(message, meta || {})) | |
| } | |
| return Result.err(new StatusError(status, message, meta || {})) | |
| } | |
| const parsed = schema.safeParse(body.data) | |
| if (!parsed.success) { | |
| return Result.err(parsed.error) | |
| } | |
| return Result.ok(parsed.data) | |
| } | |
| export async function getJSON<T extends z.ZodType>( | |
| url: string, | |
| schema: T, | |
| init: RequestInit = {}, | |
| ): Promise<Result<z.infer<T>>> { | |
| init = merge( | |
| { | |
| headers: { | |
| accept: 'application/json', | |
| 'content-type': 'application/json', | |
| }, | |
| }, | |
| init, | |
| ) | |
| return await doFetch(url, schema, init) | |
| // if (!response.ok) { | |
| // const status = response.status | |
| // const text = await response.text() | |
| // return Result.err(new Error(`failed to GET ${url}: ${status} ${text}`)) | |
| // } | |
| // const body = await response.json() | |
| // const parsed = schema.safeParse(body.data) | |
| // if (!parsed.success) { | |
| // return Result.err(parsed.error) | |
| // } | |
| // return Result.ok(parsed.data) | |
| } | |
| export async function post<T extends z.ZodType>( | |
| url: string, | |
| schema: T, | |
| init: RequestInit = {}, | |
| ): Promise<Result<z.infer<T>>> { | |
| init.method = 'POST' | |
| const response = await fetch(url, init) | |
| if (!response.ok) { | |
| const status = response.status | |
| const text = await response.text() | |
| return Result.err(new Error(`failed to POST ${url}: ${status} ${text}`)) | |
| } | |
| const body = await response.json() | |
| if (!has('data', body)) { | |
| return Result.err(new Error(`no 'data' key in body`)) | |
| } | |
| const parsed = schema.safeParse(body.data) | |
| if (!parsed.success) { | |
| return Result.err(parsed.error) | |
| } | |
| return Result.ok(parsed.data) | |
| } | |
| export async function postJSON<T extends z.ZodType>( | |
| data: unknown, | |
| url: string, | |
| schema: T, | |
| init: RequestInit = {}, | |
| ): Promise<Result<z.infer<T>>> { | |
| init = merge({headers: {'content-type': 'application/json'}}, init) | |
| init.method = 'POST' | |
| init.body = JSON.stringify(data) | |
| return await post(url, schema, init) | |
| } | |
| export async function postForm<T extends z.ZodType>( | |
| data: FormData, | |
| url: string, | |
| schema: T, | |
| init: RequestInit = {}, | |
| ): Promise<Result<z.infer<T>>> { | |
| init.method = 'POST' | |
| init.body = data | |
| return await post(url, schema, init) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Result} from '@badrap/result' | |
| import {z} from 'zod' | |
| import {config} from 'app/config' | |
| import { | |
| getJSON, | |
| postJSON, | |
| zAPIAuthWebTokenResp, | |
| zAPIChannel, | |
| zAPIMessage, | |
| zAPISpace, | |
| zAPISpaceList, | |
| zAPISpaceShow, | |
| zAPIUser, | |
| type APIAuthWebTokenResp, | |
| type APIChannel, | |
| type APIMessage, | |
| type APISpace, | |
| type APISpaceList, | |
| type APISpaceShow, | |
| type APIUser, | |
| } from './http' | |
| import {isNil} from 'lodash/fp' | |
| const {API_URL} = config() | |
| const zOAuthProvider = z.enum(['github', 'google']) | |
| export type OAuthProvider = z.infer<typeof zOAuthProvider> | |
| export function mkURL(path: string) { | |
| return [API_URL, path].join('') | |
| } | |
| export async function getMessagesByChannel( | |
| token: string, | |
| params: { | |
| channel_id: string | |
| earliest_message_id?: string | |
| }, | |
| ): Promise<Result<APIMessage[]>> { | |
| const {channel_id, earliest_message_id} = params | |
| let url = mkURL(`/v1/channels/${channel_id}/messages`) | |
| if (!isNil(earliest_message_id)) { | |
| url += `?earliest_message_id=${earliest_message_id}` | |
| } | |
| const schema = z.array(zAPIMessage) | |
| const messagesR = await getJSON(url, schema, { | |
| headers: {authorization: `Bearer ${token}`}, | |
| }) | |
| return messagesR | |
| } | |
| export async function getMe(token: string): Promise<Result<APIUser>> { | |
| const url = mkURL(`/v1/users/me`) | |
| return await getJSON(url, zAPIUser, { | |
| headers: {authorization: `Bearer ${token}`}, | |
| }) | |
| } | |
| export async function sendMessage( | |
| token: string, | |
| params: any, | |
| ): Promise<Result<APIMessage>> { | |
| const {channel_id} = params | |
| const url = mkURL(`/v1/channels/${channel_id}/messages`) | |
| const messageR = await postJSON(params, url, zAPIMessage, { | |
| headers: {authorization: `Bearer ${token}`}, | |
| }) | |
| return messageR | |
| } | |
| export async function auth(params: { | |
| provider: OAuthProvider | |
| code: string | |
| }): Promise<Result<APIAuthWebTokenResp>> { | |
| const {code, provider} = params | |
| const url = mkURL(`/v1/auth/${provider}/token`) | |
| return await postJSON({code}, url, zAPIAuthWebTokenResp) | |
| } | |
| export async function getChannels( | |
| token: string, | |
| params: {space_id: string}, | |
| ): Promise<Result<APIChannel[]>> { | |
| const {space_id} = params | |
| const url = mkURL(`/v1/spaces/${space_id}/channels`) | |
| const schema = z.array(zAPIChannel) | |
| const result = await getJSON(url, schema, { | |
| headers: {authorization: `Bearer ${token}`}, | |
| }) | |
| return result | |
| } | |
| export async function scopedSpaces( | |
| token: string, | |
| ): Promise<Result<APISpaceList>> { | |
| const url = mkURL(`/v1/spaces`) | |
| const schema = zAPISpaceList | |
| const result = await getJSON(url, schema, { | |
| headers: {authorization: `Bearer ${token}`}, | |
| }) | |
| return result | |
| } | |
| export async function getSpace( | |
| token: string, | |
| params: Pick<APISpace, 'space_id'>, | |
| ): Promise<Result<APISpaceShow>> { | |
| const url = mkURL(`/v1/spaces/${params.space_id}`) | |
| const schema = zAPISpaceShow | |
| const result = await getJSON(url, schema, { | |
| headers: {authorization: `Bearer ${token}`}, | |
| }) | |
| return result | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment