161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
if (process.env.NODE_ENV !== "test")
|
|
require("server-only");
|
|
import getPool from "@/lib/server/db";
|
|
import { requireActiveGroup } from "@/lib/server/groups";
|
|
import { listTagsForEntries, normalizeTags, requireExistingTagsForGroup, setEntryTags } from "@/lib/server/tags";
|
|
import { listBucketTags } from "@/lib/server/buckets";
|
|
import { enforceUserWriteRateLimit } from "@/lib/server/rate-limit";
|
|
import type { Entry } from "@/lib/shared/types";
|
|
|
|
type EntryRow = {
|
|
id: number;
|
|
entry_type: "SPENDING" | "INCOME";
|
|
amount_dollars: string | number;
|
|
occurred_at: string;
|
|
necessity: "NECESSARY" | "BOTH" | "UNNECESSARY";
|
|
purchase_type: string;
|
|
notes: string | null;
|
|
receipt_id: number | null;
|
|
source_schedule_id: number | null;
|
|
};
|
|
|
|
export { requireActiveGroup };
|
|
|
|
export async function listEntries(groupId: number): Promise<Entry[]> {
|
|
const pool = getPool();
|
|
const rows = (await pool.query<EntryRow>(
|
|
`select id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id,
|
|
source_schedule_id
|
|
from entries
|
|
where group_id = $1
|
|
order by occurred_at desc, id desc`,
|
|
[groupId]
|
|
)).rows;
|
|
const entryIds = rows.map((row: EntryRow) => Number(row.id));
|
|
const tagsMap = await listTagsForEntries({ groupId, entryIds });
|
|
return rows.map((row: EntryRow) => ({
|
|
id: Number(row.id),
|
|
entryType: row.entry_type,
|
|
amountDollars: Number(row.amount_dollars),
|
|
occurredAt: row.occurred_at,
|
|
necessity: row.necessity,
|
|
purchaseType: row.purchase_type,
|
|
notes: row.notes,
|
|
receiptId: row.receipt_id,
|
|
bucketId: null,
|
|
tags: tagsMap.get(Number(row.id)) || [],
|
|
sourceScheduleId: row.source_schedule_id == null ? null : Number(row.source_schedule_id)
|
|
}));
|
|
}
|
|
|
|
export async function createEntry(input: {
|
|
groupId: number;
|
|
userId: number;
|
|
entryType: "SPENDING" | "INCOME";
|
|
amountDollars: number;
|
|
occurredAt: string;
|
|
necessity: "NECESSARY" | "BOTH" | "UNNECESSARY";
|
|
purchaseType: string;
|
|
notes?: string;
|
|
tags?: string[];
|
|
bucketId?: number | null;
|
|
sourceScheduleId?: number | null;
|
|
}) {
|
|
await enforceUserWriteRateLimit({ userId: input.userId, scope: "entries:create" });
|
|
const pool = getPool();
|
|
const rows = (await pool.query<EntryRow>(
|
|
`insert into entries(group_id, created_by, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes,
|
|
source_schedule_id)
|
|
values($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
returning id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id,
|
|
source_schedule_id`,
|
|
[
|
|
input.groupId,
|
|
input.userId,
|
|
input.entryType,
|
|
input.amountDollars,
|
|
input.occurredAt,
|
|
input.necessity,
|
|
input.purchaseType,
|
|
input.notes || null,
|
|
input.sourceScheduleId ?? null
|
|
]
|
|
)).rows;
|
|
const row = rows[0];
|
|
const bucketTags = input.bucketId ? await listBucketTags(input.bucketId, input.groupId) : [];
|
|
const tags = normalizeTags([...(input.tags || []), ...bucketTags]);
|
|
const tagIds = await requireExistingTagsForGroup({ groupId: input.groupId, tags });
|
|
await setEntryTags({ entryId: Number(row.id), groupId: input.groupId, tagIds });
|
|
return {
|
|
id: Number(row.id),
|
|
entryType: row.entry_type,
|
|
amountDollars: Number(row.amount_dollars),
|
|
occurredAt: row.occurred_at,
|
|
necessity: row.necessity,
|
|
purchaseType: row.purchase_type,
|
|
notes: row.notes,
|
|
receiptId: row.receipt_id,
|
|
bucketId: null,
|
|
tags,
|
|
sourceScheduleId: row.source_schedule_id == null ? null : Number(row.source_schedule_id)
|
|
} as Entry;
|
|
}
|
|
|
|
export async function updateEntry(input: {
|
|
id: number;
|
|
groupId: number;
|
|
userId: number;
|
|
entryType: "SPENDING" | "INCOME";
|
|
amountDollars: number;
|
|
occurredAt: string;
|
|
necessity: "NECESSARY" | "BOTH" | "UNNECESSARY";
|
|
purchaseType: string;
|
|
notes?: string;
|
|
tags?: string[];
|
|
bucketId?: number | null;
|
|
}) {
|
|
await enforceUserWriteRateLimit({ userId: input.userId, scope: "entries:update" });
|
|
const pool = getPool();
|
|
const rows = (await pool.query<EntryRow>(
|
|
`update entries
|
|
set entry_type=$1, amount_dollars=$2, occurred_at=$3, necessity=$4, purchase_type=$5, notes=$6
|
|
where id=$7 and group_id=$8
|
|
returning id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id,
|
|
source_schedule_id`,
|
|
[
|
|
input.entryType,
|
|
input.amountDollars,
|
|
input.occurredAt,
|
|
input.necessity,
|
|
input.purchaseType,
|
|
input.notes || null,
|
|
input.id,
|
|
input.groupId
|
|
]
|
|
)).rows;
|
|
if (!rows[0]) return null;
|
|
const bucketTags = input.bucketId ? await listBucketTags(input.bucketId, input.groupId) : [];
|
|
const tags = normalizeTags([...(input.tags || []), ...bucketTags]);
|
|
const tagIds = await requireExistingTagsForGroup({ groupId: input.groupId, tags });
|
|
await setEntryTags({ entryId: Number(input.id), groupId: input.groupId, tagIds });
|
|
return {
|
|
id: Number(rows[0].id),
|
|
entryType: rows[0].entry_type,
|
|
amountDollars: Number(rows[0].amount_dollars),
|
|
occurredAt: rows[0].occurred_at,
|
|
necessity: rows[0].necessity,
|
|
purchaseType: rows[0].purchase_type,
|
|
notes: rows[0].notes,
|
|
receiptId: rows[0].receipt_id,
|
|
bucketId: null,
|
|
tags,
|
|
sourceScheduleId: rows[0].source_schedule_id == null ? null : Number(rows[0].source_schedule_id)
|
|
} as Entry;
|
|
}
|
|
|
|
export async function deleteEntry(input: { id: number; groupId: number; userId: number }) {
|
|
await enforceUserWriteRateLimit({ userId: input.userId, scope: "entries:delete" });
|
|
const pool = getPool();
|
|
await pool.query("delete from entries where id=$1 and group_id=$2", [input.id, input.groupId]);
|
|
}
|