add zod schemas & inferences

This commit is contained in:
thierryverse 2025-11-14 00:25:08 +02:00
parent 0c41e487d8
commit 77d49ca11c
10 changed files with 143 additions and 92 deletions

View file

@ -0,0 +1,35 @@
import { z } from "zod";
export const loginRequest = z.object({
email: z.string().email(),
password: z.string().min(1),
grant_type: z.string().optional(),
});
export const loginResponse = z.object({
access_token: z.string(),
token_type: z.string(),
});
export const registerRequest = z.object({
email: z.string().email(),
password: z.string().min(1),
is_active: z.boolean().optional(),
is_superuser: z.boolean().optional(),
is_verified: z.boolean().optional(),
});
export const registerResponse = z.object({
id: z.number(),
email: z.string().email(),
is_active: z.boolean(),
is_superuser: z.boolean(),
is_verified: z.boolean(),
pages_limit: z.number(),
pages_used: z.number(),
});
export type LoginRequest = z.infer<typeof loginRequest>;
export type LoginResponse = z.infer<typeof loginResponse>;
export type RegisterRequest = z.infer<typeof registerRequest>;
export type RegisterResponse = z.infer<typeof registerResponse>;

View file

@ -0,0 +1,48 @@
import { z } from "zod";
import { type Message } from "@ai-sdk/react";
import { paginationQueryParams } from ".";
export const chatTypeEnum = z.enum(["QNA"]);
export const chatSummary = z.object({
created_at: z.string(),
id: z.number(),
type: chatTypeEnum,
title: z.string(),
search_space_id: z.number(),
state_version: z.number(),
});
export const chatDetails = chatSummary.extend({
initial_connectors: z.array(z.string()),
messages: z.array(z.any()),
});
export const getChatDetailsRequest = chatSummary.pick({ id: true });
export const getChatsBySearchSpaceRequest = chatSummary.pick({
search_space_id: true,
}).merge(paginationQueryParams);
export const deleteChatResponse = z.object({
message: z.literal("Chat deleted successfully"),
});
export const deleteChatRequest = chatSummary.pick({ id: true });
export const createChatRequest = chatDetails
.omit({ created_at: true, id: true, state_version: true });
export const updateChatRequest = chatDetails
.omit({ created_at: true, state_version: true });
export type ChatSummary = z.infer<typeof chatSummary>;
export type ChatDetails = z.infer<typeof chatDetails> & { messages: Message[] };
export type GetChatDetailsRequest = z.infer<typeof getChatDetailsRequest>;
export type GetChatsBySearchSpaceRequest = z.infer<
typeof getChatsBySearchSpaceRequest
>;
export type DeleteChatResponse = z.infer<typeof deleteChatResponse>;
export type DeleteChatRequest = z.infer<typeof deleteChatRequest>;
export type CreateChatRequest = z.infer<typeof createChatRequest>;
export type UpdateChatRequest = z.infer<typeof updateChatRequest>;

View file

@ -0,0 +1,8 @@
import { z } from "zod";
export const paginationQueryParams = z.object({
limit: z.number().optional(),
skip: z.number().optional(),
});
export type PaginationQueryParams = z.infer<typeof paginationQueryParams>;

View file

@ -1,50 +0,0 @@
import { LoginRequest, LoginResponse, RegisterRequest, RegisterResponse } from "./contracts";
export class AuthApiService {
login = async (request: LoginRequest) : Promise<LoginResponse> => {
const requestBody = new URLSearchParams();
requestBody.append("username", request.email);
requestBody.append("password", request.password);
requestBody.append("grant_type", "password");
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/jwt/login`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: requestBody.toString(),
}
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || `HTTP ${response.status}`);
}
return data;
};
register = async (request: RegisterRequest) : Promise<RegisterResponse> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || `HTTP ${response.status}`);
}
return data;
};
}

View file

@ -1,34 +0,0 @@
/**
* LOGIN
*/
export type LoginRequest = {
email: string;
password: string;
grant_type?: string;
};
export type LoginResponse = {
access_token: string;
token_type: string;
};
/**
* REGISTER
*/
export type RegisterRequest = {
email: string;
password: string;
is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
};
export type RegisterResponse = {
id: number;
email: string;
is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
pages_limit: number;
pages_used: number;
};

View file

@ -33,7 +33,7 @@ export class BaseApiService {
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
) {
) : Promise<T> {
const defaultOptions: RequestOptions = {
headers: {
"Content-Type": "application/json",
@ -55,7 +55,10 @@ export class BaseApiService {
// Serialize body
if (body) {
if (mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === "application/json") {
if (
mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() ===
"application/json"
) {
requestBody = JSON.stringify(body);
}
@ -125,7 +128,7 @@ export class BaseApiService {
async get<T>(
url: string,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
options?: Omit<RequestOptions, "method">
) {
return this.request(url, undefined, responseSchema, {
...options,
@ -137,7 +140,7 @@ export class BaseApiService {
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
@ -149,7 +152,7 @@ export class BaseApiService {
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
@ -161,7 +164,7 @@ export class BaseApiService {
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
@ -169,3 +172,8 @@ export class BaseApiService {
});
}
}
export const baseApiService = new BaseApiService(
typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "",
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || ""
);

View file

@ -1 +0,0 @@
// Will contain a ChatApiService class that will be used to make API calls

View file

@ -1 +0,0 @@
// Will contains contracts for all chat related APIs

View file

@ -0,0 +1,30 @@
export class AppError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
export class NetworkError extends AppError {
constructor(message: string) {
super(message);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message);
}
}
export class AuthenticationError extends AppError {
constructor(message: string) {
super(message);
}
}
export class AuthorizationError extends AppError {
constructor(message: string) {
super(message);
}
}

View file

@ -1,6 +1,14 @@
import { Message } from "@ai-sdk/react";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getChatTitleFromMessages(messages: Message[]) {
const userMessages = messages.filter((msg) => msg.role === "user");
if (userMessages.length === 0) return "Untitled Chat";
return userMessages[0].content;
}