Add Turso/Vercel deployment configuration

- Updated database connection to support Turso auth token
- Added vercel.json with bun build configuration
- Updated environment schema for production deployment
- Added new features and components for production readiness
This commit is contained in:
2025-07-12 01:42:43 -04:00
parent 2d217fab47
commit a1b40e7a9c
75 changed files with 8821 additions and 1803 deletions
+168 -17
View File
@@ -1,18 +1,19 @@
import { z } from "zod";
import { eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { clients } from "~/server/db/schema";
import { clients, invoices } from "~/server/db/schema";
import { TRPCError } from "@trpc/server";
const createClientSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email").optional(),
phone: z.string().optional(),
addressLine1: z.string().optional(),
addressLine2: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
postalCode: z.string().optional(),
country: z.string().optional(),
name: z.string().min(1, "Name is required").max(255, "Name is too long"),
email: z.string().email("Invalid email").optional().or(z.literal("")),
phone: z.string().max(50, "Phone number is too long").optional().or(z.literal("")),
addressLine1: z.string().max(255, "Address is too long").optional().or(z.literal("")),
addressLine2: z.string().max(255, "Address is too long").optional().or(z.literal("")),
city: z.string().max(100, "City name is too long").optional().or(z.literal("")),
state: z.string().max(50, "State name is too long").optional().or(z.literal("")),
postalCode: z.string().max(20, "Postal code is too long").optional().or(z.literal("")),
country: z.string().max(100, "Country name is too long").optional().or(z.literal("")),
});
const updateClientSchema = createClientSchema.partial().extend({
@@ -21,16 +22,25 @@ const updateClientSchema = createClientSchema.partial().extend({
export const clientsRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
try {
return await ctx.db.query.clients.findMany({
where: eq(clients.createdById, ctx.session.user.id),
orderBy: (clients, { desc }) => [desc(clients.createdAt)],
});
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch clients",
cause: error,
});
}
}),
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.clients.findFirst({
try {
const client = await ctx.db.query.clients.findFirst({
where: eq(clients.id, input.id),
with: {
invoices: {
@@ -38,33 +48,174 @@ export const clientsRouter = createTRPCRouter({
},
},
});
if (!client) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Client not found",
});
}
// Check if user owns this client
if (client.createdById !== ctx.session.user.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to view this client",
});
}
return client;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch client",
cause: error,
});
}
}),
create: protectedProcedure
.input(createClientSchema)
.mutation(async ({ ctx, input }) => {
return await ctx.db.insert(clients).values({
...input,
try {
// Clean up empty strings to null, but preserve required fields
const cleanInput = Object.fromEntries(
Object.entries(input).map(([key, value]) => [
key,
value === "" ? null : value,
])
);
const [client] = await ctx.db.insert(clients).values({
name: input.name, // Ensure name is included
...cleanInput,
createdById: ctx.session.user.id,
}).returning();
if (!client) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create client",
});
}
return client;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create client",
cause: error,
});
}
}),
update: protectedProcedure
.input(updateClientSchema)
.mutation(async ({ ctx, input }) => {
try {
const { id, ...data } = input;
return await ctx.db
// Verify client exists and belongs to user
const existingClient = await ctx.db.query.clients.findFirst({
where: eq(clients.id, id),
});
if (!existingClient) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Client not found",
});
}
if (existingClient.createdById !== ctx.session.user.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to update this client",
});
}
// Clean up empty strings to null
const cleanData = Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
value === "" ? null : value,
])
);
const [updatedClient] = await ctx.db
.update(clients)
.set({
...data,
...cleanData,
updatedAt: new Date(),
})
.where(eq(clients.id, id));
.where(eq(clients.id, id))
.returning();
if (!updatedClient) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update client",
});
}
return updatedClient;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update client",
cause: error,
});
}
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
return await ctx.db.delete(clients).where(eq(clients.id, input.id));
try {
// Verify client exists and belongs to user
const client = await ctx.db.query.clients.findFirst({
where: eq(clients.id, input.id),
});
if (!client) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Client not found",
});
}
if (client.createdById !== ctx.session.user.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to delete this client",
});
}
// Check if client has invoices
const clientInvoices = await ctx.db.query.invoices.findMany({
where: eq(invoices.clientId, input.id),
});
if (clientInvoices.length > 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot delete client with existing invoices. Please delete or reassign the invoices first.",
});
}
await ctx.db.delete(clients).where(eq(clients.id, input.id));
return { success: true };
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete client",
cause: error,
});
}
}),
});