This commit is contained in:
+24
-14
@@ -1,22 +1,32 @@
|
||||
import { generateSlug } from '~~/shared/utils/slug'
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { participants, pots } from '~~/server/database/schema'
|
||||
import { generateSlug } from '~~/shared/utils/slug';
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { participants, pots } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const name = typeof body?.name === 'string' ? body.name.trim() : ''
|
||||
const currency = typeof body?.currency === 'string' ? body.currency.trim().toUpperCase() : ''
|
||||
const creatorName = typeof body?.creatorName === 'string' ? body.creatorName.trim() : ''
|
||||
const body = await readBody(event);
|
||||
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
||||
const currency =
|
||||
typeof body?.currency === 'string'
|
||||
? body.currency.trim().toUpperCase()
|
||||
: '';
|
||||
const creatorName =
|
||||
typeof body?.creatorName === 'string' ? body.creatorName.trim() : '';
|
||||
|
||||
if (!name || !currency || !creatorName) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'name, currency and creatorName are required' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'name, currency and creatorName are required',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
const slug = generateSlug()
|
||||
const db = useDb(event);
|
||||
const slug = generateSlug();
|
||||
|
||||
await db.insert(pots).values({ slug, name, currency })
|
||||
const [creator] = await db.insert(participants).values({ potSlug: slug, name: creatorName }).returning()
|
||||
await db.insert(pots).values({ slug, name, currency });
|
||||
const [creator] = await db
|
||||
.insert(participants)
|
||||
.values({ potSlug: slug, name: creatorName })
|
||||
.returning();
|
||||
|
||||
return { slug, name, currency, creator }
|
||||
})
|
||||
return { slug, name, currency, creator };
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { calculateBalances } from '~~/shared/utils/balances'
|
||||
import { calculateBalances } from '~~/shared/utils/balances';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
|
||||
const pot = await getPotOrThrow(slug)
|
||||
const participants = await getParticipants(slug)
|
||||
const expenses = await getExpensesWithParticipants(slug)
|
||||
const { netBalances, settlements } = calculateBalances(participants, expenses)
|
||||
const pot = await getPotOrThrow(slug);
|
||||
const participants = await getParticipants(slug);
|
||||
const expenses = await getExpensesWithParticipants(slug);
|
||||
const { netBalances, settlements } = calculateBalances(
|
||||
participants,
|
||||
expenses,
|
||||
);
|
||||
|
||||
return { pot, participants, expenses, netBalances, settlements }
|
||||
})
|
||||
return { pot, participants, expenses, netBalances, settlements };
|
||||
});
|
||||
|
||||
@@ -1,35 +1,62 @@
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema'
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const body = await readBody(event)
|
||||
const description = typeof body?.description === 'string' ? body.description.trim() : ''
|
||||
const amountCents = Number(body?.amountCents)
|
||||
const payerId = Number(body?.payerId)
|
||||
const participantIds: number[] = Array.isArray(body?.participantIds) ? body.participantIds.map(Number) : []
|
||||
const body = await readBody(event);
|
||||
const description =
|
||||
typeof body?.description === 'string' ? body.description.trim() : '';
|
||||
const amountCents = Number(body?.amountCents);
|
||||
const payerId = Number(body?.payerId);
|
||||
const participantIds: number[] = Array.isArray(body?.participantIds)
|
||||
? body.participantIds.map(Number)
|
||||
: [];
|
||||
|
||||
if (!description) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'description is required' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'description is required',
|
||||
});
|
||||
}
|
||||
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'amountCents must be a positive integer' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'amountCents must be a positive integer',
|
||||
});
|
||||
}
|
||||
if (participantIds.length === 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'at least one participant must be selected' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'at least one participant must be selected',
|
||||
});
|
||||
}
|
||||
|
||||
const potParticipants = await getParticipants(slug)
|
||||
const validIds = new Set(potParticipants.map(p => p.id))
|
||||
if (!validIds.has(payerId) || participantIds.some(id => !validIds.has(id))) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'payerId and participantIds must reference participants of this Pot' })
|
||||
const potParticipants = await getParticipants(slug);
|
||||
const validIds = new Set(potParticipants.map((p) => p.id));
|
||||
if (
|
||||
!validIds.has(payerId) ||
|
||||
participantIds.some((id) => !validIds.has(id))
|
||||
) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage:
|
||||
'payerId and participantIds must reference participants of this Pot',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
const [expense] = await db.insert(expenses).values({ potSlug: slug, description, amountCents, payerId }).returning()
|
||||
await db.insert(expenseParticipants).values(participantIds.map(participantId => ({ expenseId: expense!.id, participantId })))
|
||||
const db = useDb(event);
|
||||
const [expense] = await db
|
||||
.insert(expenses)
|
||||
.values({ potSlug: slug, description, amountCents, payerId })
|
||||
.returning();
|
||||
await db.insert(expenseParticipants).values(
|
||||
participantIds.map((participantId) => ({
|
||||
expenseId: expense!.id,
|
||||
participantId,
|
||||
})),
|
||||
);
|
||||
|
||||
return { ...expense, participantIds }
|
||||
})
|
||||
return { ...expense, participantIds };
|
||||
});
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema'
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
const id = Number(getRouterParam(event, 'id'))
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const db = useDb()
|
||||
const [existing] = await db.select().from(expenses).where(and(eq(expenses.id, id), eq(expenses.potSlug, slug)))
|
||||
const db = useDb(event);
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(expenses)
|
||||
.where(and(eq(expenses.id, id), eq(expenses.potSlug, slug)));
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Expense not found' })
|
||||
throw createError({ statusCode: 404, statusMessage: 'Expense not found' });
|
||||
}
|
||||
|
||||
await db.delete(expenseParticipants).where(eq(expenseParticipants.expenseId, id))
|
||||
await db.delete(expenses).where(eq(expenses.id, id))
|
||||
await db
|
||||
.delete(expenseParticipants)
|
||||
.where(eq(expenseParticipants.expenseId, id));
|
||||
await db.delete(expenses).where(eq(expenses.id, id));
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -1,43 +1,74 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema'
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
const id = Number(getRouterParam(event, 'id'))
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const body = await readBody(event)
|
||||
const description = typeof body?.description === 'string' ? body.description.trim() : ''
|
||||
const amountCents = Number(body?.amountCents)
|
||||
const payerId = Number(body?.payerId)
|
||||
const participantIds: number[] = Array.isArray(body?.participantIds) ? body.participantIds.map(Number) : []
|
||||
const body = await readBody(event);
|
||||
const description =
|
||||
typeof body?.description === 'string' ? body.description.trim() : '';
|
||||
const amountCents = Number(body?.amountCents);
|
||||
const payerId = Number(body?.payerId);
|
||||
const participantIds: number[] = Array.isArray(body?.participantIds)
|
||||
? body.participantIds.map(Number)
|
||||
: [];
|
||||
|
||||
if (!description) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'description is required' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'description is required',
|
||||
});
|
||||
}
|
||||
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'amountCents must be a positive integer' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'amountCents must be a positive integer',
|
||||
});
|
||||
}
|
||||
if (participantIds.length === 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'at least one participant must be selected' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'at least one participant must be selected',
|
||||
});
|
||||
}
|
||||
|
||||
const potParticipants = await getParticipants(slug)
|
||||
const validIds = new Set(potParticipants.map(p => p.id))
|
||||
if (!validIds.has(payerId) || participantIds.some(pid => !validIds.has(pid))) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'payerId and participantIds must reference participants of this Pot' })
|
||||
const potParticipants = await getParticipants(slug);
|
||||
const validIds = new Set(potParticipants.map((p) => p.id));
|
||||
if (
|
||||
!validIds.has(payerId) ||
|
||||
participantIds.some((pid) => !validIds.has(pid))
|
||||
) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage:
|
||||
'payerId and participantIds must reference participants of this Pot',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
const [existing] = await db.select().from(expenses).where(and(eq(expenses.id, id), eq(expenses.potSlug, slug)))
|
||||
const db = useDb(event);
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(expenses)
|
||||
.where(and(eq(expenses.id, id), eq(expenses.potSlug, slug)));
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Expense not found' })
|
||||
throw createError({ statusCode: 404, statusMessage: 'Expense not found' });
|
||||
}
|
||||
|
||||
await db.update(expenses).set({ description, amountCents, payerId }).where(eq(expenses.id, id))
|
||||
await db.delete(expenseParticipants).where(eq(expenseParticipants.expenseId, id))
|
||||
await db.insert(expenseParticipants).values(participantIds.map(participantId => ({ expenseId: id, participantId })))
|
||||
await db
|
||||
.update(expenses)
|
||||
.set({ description, amountCents, payerId })
|
||||
.where(eq(expenses.id, id));
|
||||
await db
|
||||
.delete(expenseParticipants)
|
||||
.where(eq(expenseParticipants.expenseId, id));
|
||||
await db
|
||||
.insert(expenseParticipants)
|
||||
.values(
|
||||
participantIds.map((participantId) => ({ expenseId: id, participantId })),
|
||||
);
|
||||
|
||||
return { id, description, amountCents, payerId, participantIds }
|
||||
})
|
||||
return { id, description, amountCents, payerId, participantIds };
|
||||
});
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { participants } from '~~/server/database/schema'
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { participants } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const body = await readBody(event)
|
||||
const name = typeof body?.name === 'string' ? body.name.trim() : ''
|
||||
const body = await readBody(event);
|
||||
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
||||
if (!name) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'name is required' })
|
||||
throw createError({ statusCode: 400, statusMessage: 'name is required' });
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
const [participant] = await db.insert(participants).values({ potSlug: slug, name }).returning()
|
||||
return participant
|
||||
})
|
||||
const db = useDb(event);
|
||||
const [participant] = await db
|
||||
.insert(participants)
|
||||
.values({ potSlug: slug, name })
|
||||
.returning();
|
||||
return participant;
|
||||
});
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { expenseParticipants, expenses, participants } from '~~/server/database/schema'
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import {
|
||||
expenseParticipants,
|
||||
expenses,
|
||||
participants,
|
||||
} from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
const id = Number(getRouterParam(event, 'id'))
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const db = useDb()
|
||||
const [existing] = await db.select().from(participants).where(and(eq(participants.id, id), eq(participants.potSlug, slug)))
|
||||
const db = useDb(event);
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(participants)
|
||||
.where(and(eq(participants.id, id), eq(participants.potSlug, slug)));
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Participant not found' })
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Participant not found',
|
||||
});
|
||||
}
|
||||
|
||||
const [asPayer] = await db.select().from(expenses).where(eq(expenses.payerId, id)).limit(1)
|
||||
const [asSplit] = await db.select().from(expenseParticipants).where(eq(expenseParticipants.participantId, id)).limit(1)
|
||||
const [asPayer] = await db
|
||||
.select()
|
||||
.from(expenses)
|
||||
.where(eq(expenses.payerId, id))
|
||||
.limit(1);
|
||||
const [asSplit] = await db
|
||||
.select()
|
||||
.from(expenseParticipants)
|
||||
.where(eq(expenseParticipants.participantId, id))
|
||||
.limit(1);
|
||||
if (asPayer || asSplit) {
|
||||
throw createError({ statusCode: 409, statusMessage: 'Cannot remove a participant with expenses or payments' })
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Cannot remove a participant with expenses or payments',
|
||||
});
|
||||
}
|
||||
|
||||
await db.delete(participants).where(eq(participants.id, id))
|
||||
await db.delete(participants).where(eq(participants.id, id));
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { isValidPaymentLink } from '~~/shared/utils/paymentLink'
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { participants } from '~~/server/database/schema'
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { isValidPaymentLink } from '~~/shared/utils/paymentLink';
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { participants } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
const id = Number(getRouterParam(event, 'id'))
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const body = await readBody(event)
|
||||
const paymentLink = typeof body?.paymentLink === 'string' ? body.paymentLink.trim() : ''
|
||||
const body = await readBody(event);
|
||||
const paymentLink =
|
||||
typeof body?.paymentLink === 'string' ? body.paymentLink.trim() : '';
|
||||
if (paymentLink && !isValidPaymentLink(paymentLink)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'paymentLink must be a valid http(s) URL' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'paymentLink must be a valid http(s) URL',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
const [existing] = await db.select().from(participants).where(and(eq(participants.id, id), eq(participants.potSlug, slug)))
|
||||
const db = useDb(event);
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(participants)
|
||||
.where(and(eq(participants.id, id), eq(participants.potSlug, slug)));
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Participant not found' })
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Participant not found',
|
||||
});
|
||||
}
|
||||
|
||||
const [updated] = await db.update(participants)
|
||||
const [updated] = await db
|
||||
.update(participants)
|
||||
.set({ paymentLink: paymentLink || null })
|
||||
.where(eq(participants.id, id))
|
||||
.returning()
|
||||
.returning();
|
||||
|
||||
return updated
|
||||
})
|
||||
return updated;
|
||||
});
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
import { useDb } from '~~/server/database/client'
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema'
|
||||
import { useDb } from '~~/server/database/client';
|
||||
import { expenseParticipants, expenses } from '~~/server/database/schema';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')!
|
||||
await getPotOrThrow(slug)
|
||||
const slug = getRouterParam(event, 'slug')!;
|
||||
await getPotOrThrow(slug);
|
||||
|
||||
const body = await readBody(event)
|
||||
const fromId = Number(body?.fromId)
|
||||
const toId = Number(body?.toId)
|
||||
const amountCents = Number(body?.amountCents)
|
||||
const body = await readBody(event);
|
||||
const fromId = Number(body?.fromId);
|
||||
const toId = Number(body?.toId);
|
||||
const amountCents = Number(body?.amountCents);
|
||||
|
||||
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'amountCents must be a positive integer' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'amountCents must be a positive integer',
|
||||
});
|
||||
}
|
||||
if (fromId === toId) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'fromId and toId must differ' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'fromId and toId must differ',
|
||||
});
|
||||
}
|
||||
|
||||
const potParticipants = await getParticipants(slug)
|
||||
const validIds = new Set(potParticipants.map(p => p.id))
|
||||
const potParticipants = await getParticipants(slug);
|
||||
const validIds = new Set(potParticipants.map((p) => p.id));
|
||||
if (!validIds.has(fromId) || !validIds.has(toId)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'fromId and toId must reference participants of this Pot' })
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'fromId and toId must reference participants of this Pot',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
const [payment] = await db.insert(expenses)
|
||||
.values({ potSlug: slug, type: 'payment', description: 'Settlement', amountCents, payerId: fromId })
|
||||
.returning()
|
||||
await db.insert(expenseParticipants).values({ expenseId: payment!.id, participantId: toId })
|
||||
const db = useDb(event);
|
||||
const [payment] = await db
|
||||
.insert(expenses)
|
||||
.values({
|
||||
potSlug: slug,
|
||||
type: 'payment',
|
||||
description: 'Settlement',
|
||||
amountCents,
|
||||
payerId: fromId,
|
||||
})
|
||||
.returning();
|
||||
await db
|
||||
.insert(expenseParticipants)
|
||||
.values({ expenseId: payment!.id, participantId: toId });
|
||||
|
||||
return { ...payment, participantIds: [toId] }
|
||||
})
|
||||
return { ...payment, participantIds: [toId] };
|
||||
});
|
||||
|
||||
+12
-11
@@ -1,17 +1,18 @@
|
||||
import { createClient } from '@libsql/client'
|
||||
import { drizzle } from 'drizzle-orm/libsql'
|
||||
import * as schema from './schema'
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import * as schema from './schema';
|
||||
import type { H3Event } from 'h3';
|
||||
|
||||
let instance: ReturnType<typeof drizzle<typeof schema>> | undefined
|
||||
let instance: ReturnType<typeof drizzle<typeof schema>> | undefined;
|
||||
|
||||
export function useDb() {
|
||||
export function useDb(event: H3Event) {
|
||||
if (!instance) {
|
||||
const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig(event);
|
||||
const client = createClient({
|
||||
url: config.databaseUrl,
|
||||
authToken: config.databaseAuthToken || undefined,
|
||||
})
|
||||
instance = drizzle(client, { schema })
|
||||
url: config.database.url,
|
||||
authToken: config.database.authToken || undefined,
|
||||
});
|
||||
instance = drizzle(client, { schema });
|
||||
}
|
||||
return instance
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
"indexes": {
|
||||
"expense_participants_expense_id_idx": {
|
||||
"name": "expense_participants_expense_id_idx",
|
||||
"columns": [
|
||||
"expense_id"
|
||||
],
|
||||
"columns": ["expense_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -36,12 +34,8 @@
|
||||
"name": "expense_participants_expense_id_expenses_id_fk",
|
||||
"tableFrom": "expense_participants",
|
||||
"tableTo": "expenses",
|
||||
"columnsFrom": [
|
||||
"expense_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["expense_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -49,12 +43,8 @@
|
||||
"name": "expense_participants_participant_id_participants_id_fk",
|
||||
"tableFrom": "expense_participants",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"participant_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["participant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -113,9 +103,7 @@
|
||||
"indexes": {
|
||||
"expenses_pot_slug_idx": {
|
||||
"name": "expenses_pot_slug_idx",
|
||||
"columns": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columns": ["pot_slug"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -124,12 +112,8 @@
|
||||
"name": "expenses_pot_slug_pots_slug_fk",
|
||||
"tableFrom": "expenses",
|
||||
"tableTo": "pots",
|
||||
"columnsFrom": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"slug"
|
||||
],
|
||||
"columnsFrom": ["pot_slug"],
|
||||
"columnsTo": ["slug"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -137,12 +121,8 @@
|
||||
"name": "expenses_payer_id_participants_id_fk",
|
||||
"tableFrom": "expenses",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"payer_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["payer_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -187,9 +167,7 @@
|
||||
"indexes": {
|
||||
"participants_pot_slug_idx": {
|
||||
"name": "participants_pot_slug_idx",
|
||||
"columns": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columns": ["pot_slug"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -198,12 +176,8 @@
|
||||
"name": "participants_pot_slug_pots_slug_fk",
|
||||
"tableFrom": "participants",
|
||||
"tableTo": "pots",
|
||||
"columnsFrom": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"slug"
|
||||
],
|
||||
"columnsFrom": ["pot_slug"],
|
||||
"columnsTo": ["slug"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -262,4 +236,4 @@
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
"indexes": {
|
||||
"expense_participants_expense_id_idx": {
|
||||
"name": "expense_participants_expense_id_idx",
|
||||
"columns": [
|
||||
"expense_id"
|
||||
],
|
||||
"columns": ["expense_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -36,12 +34,8 @@
|
||||
"name": "expense_participants_expense_id_expenses_id_fk",
|
||||
"tableFrom": "expense_participants",
|
||||
"tableTo": "expenses",
|
||||
"columnsFrom": [
|
||||
"expense_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["expense_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -49,12 +43,8 @@
|
||||
"name": "expense_participants_participant_id_participants_id_fk",
|
||||
"tableFrom": "expense_participants",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"participant_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["participant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -121,9 +111,7 @@
|
||||
"indexes": {
|
||||
"expenses_pot_slug_idx": {
|
||||
"name": "expenses_pot_slug_idx",
|
||||
"columns": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columns": ["pot_slug"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -132,12 +120,8 @@
|
||||
"name": "expenses_pot_slug_pots_slug_fk",
|
||||
"tableFrom": "expenses",
|
||||
"tableTo": "pots",
|
||||
"columnsFrom": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"slug"
|
||||
],
|
||||
"columnsFrom": ["pot_slug"],
|
||||
"columnsTo": ["slug"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -145,12 +129,8 @@
|
||||
"name": "expenses_payer_id_participants_id_fk",
|
||||
"tableFrom": "expenses",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"payer_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["payer_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -202,9 +182,7 @@
|
||||
"indexes": {
|
||||
"participants_pot_slug_idx": {
|
||||
"name": "participants_pot_slug_idx",
|
||||
"columns": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columns": ["pot_slug"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -213,12 +191,8 @@
|
||||
"name": "participants_pot_slug_pots_slug_fk",
|
||||
"tableFrom": "participants",
|
||||
"tableTo": "pots",
|
||||
"columnsFrom": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"slug"
|
||||
],
|
||||
"columnsFrom": ["pot_slug"],
|
||||
"columnsTo": ["slug"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -277,4 +251,4 @@
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
"indexes": {
|
||||
"expense_participants_expense_id_idx": {
|
||||
"name": "expense_participants_expense_id_idx",
|
||||
"columns": [
|
||||
"expense_id"
|
||||
],
|
||||
"columns": ["expense_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -36,12 +34,8 @@
|
||||
"name": "expense_participants_expense_id_expenses_id_fk",
|
||||
"tableFrom": "expense_participants",
|
||||
"tableTo": "expenses",
|
||||
"columnsFrom": [
|
||||
"expense_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["expense_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -49,12 +43,8 @@
|
||||
"name": "expense_participants_participant_id_participants_id_fk",
|
||||
"tableFrom": "expense_participants",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"participant_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["participant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -121,9 +111,7 @@
|
||||
"indexes": {
|
||||
"expenses_pot_slug_idx": {
|
||||
"name": "expenses_pot_slug_idx",
|
||||
"columns": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columns": ["pot_slug"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -132,12 +120,8 @@
|
||||
"name": "expenses_pot_slug_pots_slug_fk",
|
||||
"tableFrom": "expenses",
|
||||
"tableTo": "pots",
|
||||
"columnsFrom": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"slug"
|
||||
],
|
||||
"columnsFrom": ["pot_slug"],
|
||||
"columnsTo": ["slug"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -145,12 +129,8 @@
|
||||
"name": "expenses_payer_id_participants_id_fk",
|
||||
"tableFrom": "expenses",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"payer_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["payer_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -202,9 +182,7 @@
|
||||
"indexes": {
|
||||
"participants_pot_slug_idx": {
|
||||
"name": "participants_pot_slug_idx",
|
||||
"columns": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columns": ["pot_slug"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
@@ -213,12 +191,8 @@
|
||||
"name": "participants_pot_slug_pots_slug_fk",
|
||||
"tableFrom": "participants",
|
||||
"tableTo": "pots",
|
||||
"columnsFrom": [
|
||||
"pot_slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"slug"
|
||||
],
|
||||
"columnsFrom": ["pot_slug"],
|
||||
"columnsTo": ["slug"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+55
-33
@@ -1,41 +1,63 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const pots = sqliteTable('pots', {
|
||||
slug: text('slug').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
currency: text('currency').notNull(),
|
||||
createdAt: integer('created_at').notNull().default(sql`(unixepoch())`),
|
||||
})
|
||||
createdAt: integer('created_at')
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export const participants = sqliteTable('participants', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
potSlug: text('pot_slug').notNull().references(() => pots.slug),
|
||||
name: text('name').notNull(),
|
||||
paymentLink: text('payment_link'),
|
||||
createdAt: integer('created_at').notNull().default(sql`(unixepoch())`),
|
||||
}, table => [
|
||||
index('participants_pot_slug_idx').on(table.potSlug),
|
||||
])
|
||||
export const participants = sqliteTable(
|
||||
'participants',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
potSlug: text('pot_slug')
|
||||
.notNull()
|
||||
.references(() => pots.slug),
|
||||
name: text('name').notNull(),
|
||||
paymentLink: text('payment_link'),
|
||||
createdAt: integer('created_at')
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
},
|
||||
(table) => [index('participants_pot_slug_idx').on(table.potSlug)],
|
||||
);
|
||||
|
||||
export const expenses = sqliteTable('expenses', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
potSlug: text('pot_slug').notNull().references(() => pots.slug),
|
||||
// 'payment' rows are settlements: payerId is who paid, and the single row
|
||||
// in expenseParticipants is who received it — same shape as a one-person
|
||||
// split expense, which is mathematically what a settlement is.
|
||||
type: text('type').notNull().default('expense'),
|
||||
description: text('description').notNull(),
|
||||
amountCents: integer('amount_cents').notNull(),
|
||||
payerId: integer('payer_id').notNull().references(() => participants.id),
|
||||
createdAt: integer('created_at').notNull().default(sql`(unixepoch())`),
|
||||
}, table => [
|
||||
index('expenses_pot_slug_idx').on(table.potSlug),
|
||||
])
|
||||
export const expenses = sqliteTable(
|
||||
'expenses',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
potSlug: text('pot_slug')
|
||||
.notNull()
|
||||
.references(() => pots.slug),
|
||||
// 'payment' rows are settlements: payerId is who paid, and the single row
|
||||
// in expenseParticipants is who received it — same shape as a one-person
|
||||
// split expense, which is mathematically what a settlement is.
|
||||
type: text('type').notNull().default('expense'),
|
||||
description: text('description').notNull(),
|
||||
amountCents: integer('amount_cents').notNull(),
|
||||
payerId: integer('payer_id')
|
||||
.notNull()
|
||||
.references(() => participants.id),
|
||||
createdAt: integer('created_at')
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
},
|
||||
(table) => [index('expenses_pot_slug_idx').on(table.potSlug)],
|
||||
);
|
||||
|
||||
export const expenseParticipants = sqliteTable('expense_participants', {
|
||||
expenseId: integer('expense_id').notNull().references(() => expenses.id),
|
||||
participantId: integer('participant_id').notNull().references(() => participants.id),
|
||||
}, table => [
|
||||
index('expense_participants_expense_id_idx').on(table.expenseId),
|
||||
])
|
||||
export const expenseParticipants = sqliteTable(
|
||||
'expense_participants',
|
||||
{
|
||||
expenseId: integer('expense_id')
|
||||
.notNull()
|
||||
.references(() => expenses.id),
|
||||
participantId: integer('participant_id')
|
||||
.notNull()
|
||||
.references(() => participants.id),
|
||||
},
|
||||
(table) => [index('expense_participants_expense_id_idx').on(table.expenseId)],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator'
|
||||
import { useDb } from '../database/client'
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||
import { useDb } from '../database/client';
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
await migrate(useDb(), { migrationsFolder: 'server/database/migrations' })
|
||||
})
|
||||
await migrate(useDb(event), {
|
||||
migrationsFolder: 'server/database/migrations',
|
||||
});
|
||||
});
|
||||
|
||||
+42
-28
@@ -1,47 +1,61 @@
|
||||
import { eq, inArray } from 'drizzle-orm'
|
||||
import { useDb } from '../database/client'
|
||||
import { expenseParticipants, expenses, participants, pots } from '../database/schema'
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { useDb } from '../database/client';
|
||||
import {
|
||||
expenseParticipants,
|
||||
expenses,
|
||||
participants,
|
||||
pots,
|
||||
} from '../database/schema';
|
||||
|
||||
export interface ExpenseWithDetails {
|
||||
id: number
|
||||
type: string
|
||||
description: string
|
||||
amountCents: number
|
||||
payerId: number
|
||||
participantIds: number[]
|
||||
createdAt: number
|
||||
id: number;
|
||||
type: string;
|
||||
description: string;
|
||||
amountCents: number;
|
||||
payerId: number;
|
||||
participantIds: number[];
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export async function getPotOrThrow(slug: string) {
|
||||
const db = useDb()
|
||||
const [pot] = await db.select().from(pots).where(eq(pots.slug, slug))
|
||||
const db = useDb(event);
|
||||
const [pot] = await db.select().from(pots).where(eq(pots.slug, slug));
|
||||
if (!pot) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Pot not found' })
|
||||
throw createError({ statusCode: 404, statusMessage: 'Pot not found' });
|
||||
}
|
||||
return pot
|
||||
return pot;
|
||||
}
|
||||
|
||||
export async function getParticipants(slug: string) {
|
||||
const db = useDb()
|
||||
return db.select().from(participants).where(eq(participants.potSlug, slug))
|
||||
const db = useDb(event);
|
||||
return db.select().from(participants).where(eq(participants.potSlug, slug));
|
||||
}
|
||||
|
||||
export async function getExpensesWithParticipants(slug: string): Promise<ExpenseWithDetails[]> {
|
||||
const db = useDb()
|
||||
const expenseRows = await db.select().from(expenses).where(eq(expenses.potSlug, slug)).orderBy(expenses.createdAt)
|
||||
if (expenseRows.length === 0) return []
|
||||
export async function getExpensesWithParticipants(
|
||||
slug: string,
|
||||
): Promise<ExpenseWithDetails[]> {
|
||||
const db = useDb(event);
|
||||
const expenseRows = await db
|
||||
.select()
|
||||
.from(expenses)
|
||||
.where(eq(expenses.potSlug, slug))
|
||||
.orderBy(expenses.createdAt);
|
||||
if (expenseRows.length === 0) return [];
|
||||
|
||||
const expenseIds = expenseRows.map(row => row.id)
|
||||
const links = await db.select().from(expenseParticipants).where(inArray(expenseParticipants.expenseId, expenseIds))
|
||||
const expenseIds = expenseRows.map((row) => row.id);
|
||||
const links = await db
|
||||
.select()
|
||||
.from(expenseParticipants)
|
||||
.where(inArray(expenseParticipants.expenseId, expenseIds));
|
||||
|
||||
const participantIdsByExpense = new Map<number, number[]>()
|
||||
const participantIdsByExpense = new Map<number, number[]>();
|
||||
for (const link of links) {
|
||||
const list = participantIdsByExpense.get(link.expenseId) ?? []
|
||||
list.push(link.participantId)
|
||||
participantIdsByExpense.set(link.expenseId, list)
|
||||
const list = participantIdsByExpense.get(link.expenseId) ?? [];
|
||||
list.push(link.participantId);
|
||||
participantIdsByExpense.set(link.expenseId, list);
|
||||
}
|
||||
|
||||
return expenseRows.map(row => ({
|
||||
return expenseRows.map((row) => ({
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
description: row.description,
|
||||
@@ -49,5 +63,5 @@ export async function getExpensesWithParticipants(slug: string): Promise<Expense
|
||||
payerId: row.payerId,
|
||||
participantIds: participantIdsByExpense.get(row.id) ?? [],
|
||||
createdAt: row.createdAt,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user