Files
Red-Bull-Tracker/src/lib/appwriteEntries.ts
T
Ned Halksworth f108214df8 intial commit
2026-05-15 21:36:13 +01:00

180 lines
5.1 KiB
TypeScript

import type { Models } from "appwrite";
import { flavourMeta } from "../data/flavours";
import type { EntryDraft, RedBullEntry } from "../types";
import { appwriteConfig, ID, Permission, Query, Role, tablesDB } from "./appwrite";
import { makeId, makeImportKey } from "./metrics";
type EntryRow = Models.Row & {
userId: string;
cans: number;
flavour: string;
flavourAccent: string;
sizeMl: number;
pricePerCan: number;
dateTime: string;
notes?: string;
store?: string;
sugarFree: boolean;
caffeineMgPerCan?: number;
importKey: string;
source: RedBullEntry["source"];
};
export async function listEntries(userId: string) {
const rows: EntryRow[] = [];
const limit = 100;
let offset = 0;
while (true) {
const response = await tablesDB.listRows<EntryRow>({
databaseId: appwriteConfig.databaseId,
tableId: appwriteConfig.collectionId,
queries: [
Query.equal("userId", userId),
Query.orderDesc("dateTime"),
Query.limit(limit),
Query.offset(offset),
],
});
rows.push(...response.rows);
if (response.rows.length < limit) break;
offset += limit;
}
return rows.map(fromRow);
}
export async function createEntry(userId: string, draft: EntryDraft) {
const entry = buildEntry(userId, draft);
const row = await tablesDB.createRow<EntryRow>({
databaseId: appwriteConfig.databaseId,
tableId: appwriteConfig.collectionId,
rowId: ID.custom(entry.id),
data: toRowData(entry),
permissions: userRowPermissions(userId),
});
return fromRow(row);
}
export async function createEntries(userId: string, drafts: EntryDraft[]) {
const saved: RedBullEntry[] = [];
for (const draft of drafts) {
saved.push(await createEntry(userId, draft));
}
return saved;
}
export async function updateEntry(userId: string, id: string, draft: EntryDraft) {
const entry = buildEntry(userId, draft, id);
const row = await tablesDB.updateRow<EntryRow>({
databaseId: appwriteConfig.databaseId,
tableId: appwriteConfig.collectionId,
rowId: id,
data: toRowData(entry),
permissions: userRowPermissions(userId),
});
return fromRow(row);
}
export async function deleteEntry(id: string) {
await tablesDB.deleteRow({
databaseId: appwriteConfig.databaseId,
tableId: appwriteConfig.collectionId,
rowId: id,
});
}
export function buildEntry(userId: string, draft: EntryDraft, id: string = makeId()): RedBullEntry {
const meta = flavourMeta(draft.flavour);
const entry: RedBullEntry = {
id,
userId,
cans: draft.cans,
flavour: draft.flavour,
flavourAccent: draft.flavourAccent || meta.accent,
sizeMl: draft.sizeMl,
pricePerCan: draft.pricePerCan,
dateTime: new Date(draft.dateTime).toISOString(),
notes: draft.notes ?? "",
store: draft.store ?? "",
sugarFree: draft.sugarFree || Boolean(meta.sugarFree),
caffeineMgPerCan: draft.caffeineMgPerCan,
importKey: "",
source: draft.source ?? "manual",
};
entry.importKey = makeImportKey(entry);
return entry;
}
export function isDuplicateDraft(existing: RedBullEntry[], draft: EntryDraft) {
const key = makeImportKey({
...draft,
dateTime: new Date(draft.dateTime).toISOString(),
notes: draft.notes ?? "",
store: draft.store ?? "",
});
return existing.some((entry) => entry.importKey === key || makeImportKey(entry) === key);
}
export function appwriteErrorMessage(error: unknown) {
if (error instanceof Error) {
if (/permissions?.*create|action 'create'|create.*permissions?/i.test(error.message)) {
return "Appwrite table permissions need Users -> Create, with Row Security enabled on intake_entries.";
}
if (/not authorized|401|unauthorized/i.test(error.message)) {
return "Appwrite denied the table request. Enable Row Security on intake_entries and grant table-level Users -> Create; rows are then read by per-user row permissions.";
}
return error.message;
}
return "Appwrite request failed.";
}
function fromRow(row: EntryRow): RedBullEntry {
return {
id: row.$id,
userId: row.userId,
cans: row.cans,
flavour: row.flavour,
flavourAccent: row.flavourAccent,
sizeMl: row.sizeMl,
pricePerCan: row.pricePerCan,
dateTime: row.dateTime,
notes: row.notes ?? "",
store: row.store ?? "",
sugarFree: row.sugarFree,
caffeineMgPerCan: row.caffeineMgPerCan,
importKey: row.importKey || makeImportKey(row),
source: row.source ?? "manual",
createdAt: row.$createdAt,
updatedAt: row.$updatedAt,
};
}
function toRowData(entry: RedBullEntry) {
return {
userId: entry.userId,
cans: entry.cans,
flavour: entry.flavour,
flavourAccent: entry.flavourAccent,
sizeMl: entry.sizeMl,
pricePerCan: entry.pricePerCan,
dateTime: entry.dateTime,
notes: entry.notes ?? "",
store: entry.store ?? "",
sugarFree: entry.sugarFree,
caffeineMgPerCan: entry.caffeineMgPerCan,
importKey: entry.importKey,
source: entry.source,
};
}
function userRowPermissions(userId: string) {
const role = Role.user(userId);
return [Permission.read(role), Permission.update(role), Permission.delete(role)];
}