aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorSebastien Castiel <sebastien@castiel.me>2023-12-05 17:39:05 -0500
committerSebastien Castiel <sebastien@castiel.me>2023-12-05 17:39:05 -0500
commited55c696cd083bfaa5429082a9c5edd4ebde0cd8 (patch)
tree9f387881f85716bcb4a05d6fce13bfe59fbc9c17 /src/lib
parent1fd6e48807d455f66d9cd79db64cbf9e9f947c6c (diff)
First version
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/api.ts155
-rw-r--r--src/lib/prisma.ts17
-rw-r--r--src/lib/schemas.ts50
-rw-r--r--src/lib/utils.ts6
4 files changed, 228 insertions, 0 deletions
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 0000000..6ea26f4
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,155 @@
+import { getPrisma } from '@/lib/prisma'
+import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
+import { Expense } from '@prisma/client'
+import { v4 as uuidv4 } from 'uuid'
+
+export async function createGroup(groupFormValues: GroupFormValues) {
+ return getPrisma().group.create({
+ data: {
+ id: uuidv4(),
+ name: groupFormValues.name,
+ participants: {
+ createMany: {
+ data: groupFormValues.participants.map(({ name }) => ({
+ id: uuidv4(),
+ name,
+ })),
+ },
+ },
+ },
+ include: { participants: true },
+ })
+}
+
+export async function createExpense(
+ expenseFormValues: ExpenseFormValues,
+ groupId: string,
+): Promise<Expense> {
+ const group = await getGroup(groupId)
+ if (!group) throw new Error(`Invalid group ID: ${groupId}`)
+
+ for (const participant of [
+ expenseFormValues.paidBy,
+ ...expenseFormValues.paidFor,
+ ]) {
+ if (!group.participants.some((p) => p.id === participant))
+ throw new Error(`Invalid participant ID: ${participant}`)
+ }
+
+ return getPrisma().expense.create({
+ data: {
+ id: uuidv4(),
+ groupId,
+ amount: expenseFormValues.amount,
+ title: expenseFormValues.title,
+ paidById: expenseFormValues.paidBy,
+ paidFor: {
+ createMany: {
+ data: expenseFormValues.paidFor.map((paidFor) => ({
+ participantId: paidFor,
+ })),
+ },
+ },
+ },
+ })
+}
+
+export async function updateExpense(
+ groupId: string,
+ expenseId: string,
+ expenseFormValues: ExpenseFormValues,
+) {
+ const group = await getGroup(groupId)
+ if (!group) throw new Error(`Invalid group ID: ${groupId}`)
+
+ const existingExpense = await getExpense(groupId, expenseId)
+ if (!existingExpense) throw new Error(`Invalid expense ID: ${expenseId}`)
+
+ for (const participant of [
+ expenseFormValues.paidBy,
+ ...expenseFormValues.paidFor,
+ ]) {
+ if (!group.participants.some((p) => p.id === participant))
+ throw new Error(`Invalid participant ID: ${participant}`)
+ }
+
+ return getPrisma().expense.update({
+ where: { id: expenseId },
+ data: {
+ amount: expenseFormValues.amount,
+ title: expenseFormValues.title,
+ paidById: expenseFormValues.paidBy,
+ paidFor: {
+ connectOrCreate: expenseFormValues.paidFor.map((paidFor) => ({
+ where: {
+ expenseId_participantId: { expenseId, participantId: paidFor },
+ },
+ create: { participantId: paidFor },
+ })),
+ deleteMany: existingExpense.paidFor.filter(
+ (paidFor) =>
+ !expenseFormValues.paidFor.some(
+ (pf) => pf === paidFor.participantId,
+ ),
+ ),
+ },
+ },
+ })
+}
+
+export async function updateGroup(
+ groupId: string,
+ groupFormValues: GroupFormValues,
+) {
+ const existingGroup = await getGroup(groupId)
+ if (!existingGroup) throw new Error('Invalid group ID')
+
+ return getPrisma().group.update({
+ where: { id: groupId },
+ data: {
+ name: groupFormValues.name,
+ participants: {
+ deleteMany: existingGroup.participants.filter(
+ (p) => !groupFormValues.participants.some((p2) => p2.id === p.id),
+ ),
+ updateMany: groupFormValues.participants
+ .filter((participant) => participant.id !== undefined)
+ .map((participant) => ({
+ where: { id: participant.id },
+ data: {
+ name: participant.name,
+ },
+ })),
+ createMany: {
+ data: groupFormValues.participants
+ .filter((participant) => participant.id === undefined)
+ .map((participant) => ({
+ id: uuidv4(),
+ name: participant.name,
+ })),
+ },
+ },
+ },
+ })
+}
+
+export async function getGroup(groupId: string) {
+ return getPrisma().group.findUnique({
+ where: { id: groupId },
+ include: { participants: true },
+ })
+}
+
+export async function getGroupExpenses(groupId: string) {
+ return getPrisma().expense.findMany({
+ where: { groupId },
+ include: { paidFor: { include: { participant: true } }, paidBy: true },
+ })
+}
+
+export async function getExpense(groupId: string, expenseId: string) {
+ return getPrisma().expense.findUnique({
+ where: { id: expenseId },
+ include: { paidBy: true, paidFor: true },
+ })
+}
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
new file mode 100644
index 0000000..773547e
--- /dev/null
+++ b/src/lib/prisma.ts
@@ -0,0 +1,17 @@
+import { PrismaClient } from '@prisma/client'
+
+let prisma: PrismaClient
+
+export function getPrisma() {
+ if (!prisma) {
+ if (process.env.NODE_ENV === 'production') {
+ prisma = new PrismaClient()
+ } else {
+ if (!(global as any).prisma) {
+ ;(global as any).prisma = new PrismaClient()
+ }
+ prisma = (global as any).prisma
+ }
+ }
+ return prisma
+}
diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts
new file mode 100644
index 0000000..0219491
--- /dev/null
+++ b/src/lib/schemas.ts
@@ -0,0 +1,50 @@
+import * as z from 'zod'
+
+export const groupFormSchema = z
+ .object({
+ name: z
+ .string()
+ .min(2, 'Enter at least two characters.')
+ .max(50, 'Enter at most 50 characters.'),
+ participants: z
+ .array(
+ z.object({
+ id: z.string().optional(),
+ name: z
+ .string()
+ .min(2, 'Enter at least two characters.')
+ .max(50, 'Enter at most 50 characters.'),
+ }),
+ )
+ .min(1),
+ })
+ .superRefine(({ participants }, ctx) => {
+ participants.forEach((participant, i) => {
+ participants.slice(0, i).forEach((otherParticipant) => {
+ if (otherParticipant.name === participant.name) {
+ ctx.addIssue({
+ code: 'custom',
+ message: 'Another participant already has this name.',
+ path: ['participants', i, 'name'],
+ })
+ }
+ })
+ })
+ })
+
+export type GroupFormValues = z.infer<typeof groupFormSchema>
+
+export const expenseFormSchema = z.object({
+ title: z
+ .string({ required_error: 'Please enter a title.' })
+ .min(2, 'Enter at least two characters.'),
+ amount: z.coerce
+ .number({ required_error: 'You must enter an amount.' })
+ .min(0.01, 'The amount must be higher than 0.01.'),
+ paidBy: z.string({ required_error: 'You must select a participant.' }),
+ paidFor: z
+ .array(z.string())
+ .min(1, 'The expense must be paid for at least 1 participant.'),
+})
+
+export type ExpenseFormValues = z.infer<typeof expenseFormSchema>
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..ec79801
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}