feat(16-02): add userId scoping to item, category, totals, and CSV services
- All functions accept userId as second parameter, no more prodDb defaults - All queries filter by eq(table.userId, userId) for data isolation - Get-by-id, update, delete use and() for composite id+userId conditions - deleteCategory uses dynamic getOrCreateUncategorized(db, userId) not hardcoded ID - CSV import scopes category lookup/creation and item creation to userId - CSV export filters items by userId - Category service converted from sync SQLite to async Postgres patterns
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db as prodDb } from "../../db/index.ts";
|
||||
import { categories, items } from "../../db/schema.ts";
|
||||
import type { CreateItem } from "../../shared/types.ts";
|
||||
|
||||
type Db = typeof prodDb;
|
||||
|
||||
export async function getAllItems(db: Db = prodDb) {
|
||||
export async function getAllItems(db: Db, userId: number) {
|
||||
return db
|
||||
.select({
|
||||
id: items.id,
|
||||
@@ -24,10 +24,11 @@ export async function getAllItems(db: Db = prodDb) {
|
||||
categoryIcon: categories.icon,
|
||||
})
|
||||
.from(items)
|
||||
.innerJoin(categories, eq(items.categoryId, categories.id));
|
||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||
.where(eq(items.userId, userId));
|
||||
}
|
||||
|
||||
export async function getItemById(db: Db = prodDb, id: number) {
|
||||
export async function getItemById(db: Db, userId: number, id: number) {
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: items.id,
|
||||
@@ -43,13 +44,14 @@ export async function getItemById(db: Db = prodDb, id: number) {
|
||||
updatedAt: items.updatedAt,
|
||||
})
|
||||
.from(items)
|
||||
.where(eq(items.id, id));
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
export async function createItem(
|
||||
db: Db = prodDb,
|
||||
db: Db,
|
||||
userId: number,
|
||||
data: Partial<CreateItem> & {
|
||||
name: string;
|
||||
categoryId: number;
|
||||
@@ -64,6 +66,7 @@ export async function createItem(
|
||||
priceCents: data.priceCents ?? null,
|
||||
quantity: data.quantity ?? 1,
|
||||
categoryId: data.categoryId,
|
||||
userId,
|
||||
notes: data.notes ?? null,
|
||||
productUrl: data.productUrl ?? null,
|
||||
imageFilename: data.imageFilename ?? null,
|
||||
@@ -75,7 +78,8 @@ export async function createItem(
|
||||
}
|
||||
|
||||
export async function updateItem(
|
||||
db: Db = prodDb,
|
||||
db: Db,
|
||||
userId: number,
|
||||
id: number,
|
||||
data: Partial<{
|
||||
name: string;
|
||||
@@ -89,25 +93,28 @@ export async function updateItem(
|
||||
imageSourceUrl: string;
|
||||
}>,
|
||||
) {
|
||||
// Check if item exists first
|
||||
// Check if item exists and belongs to user
|
||||
const [existing] = await db
|
||||
.select({ id: items.id })
|
||||
.from(items)
|
||||
.where(eq(items.id, id));
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const [row] = await db
|
||||
.update(items)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(items.id, id))
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)))
|
||||
.returning();
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function duplicateItem(db: Db = prodDb, id: number) {
|
||||
const [source] = await db.select().from(items).where(eq(items.id, id));
|
||||
export async function duplicateItem(db: Db, userId: number, id: number) {
|
||||
const [source] = await db
|
||||
.select()
|
||||
.from(items)
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||
|
||||
if (!source) return null;
|
||||
|
||||
@@ -118,6 +125,7 @@ export async function duplicateItem(db: Db = prodDb, id: number) {
|
||||
weightGrams: source.weightGrams,
|
||||
priceCents: source.priceCents,
|
||||
categoryId: source.categoryId,
|
||||
userId,
|
||||
notes: source.notes,
|
||||
productUrl: source.productUrl,
|
||||
imageFilename: source.imageFilename,
|
||||
@@ -129,13 +137,18 @@ export async function duplicateItem(db: Db = prodDb, id: number) {
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function deleteItem(db: Db = prodDb, id: number) {
|
||||
export async function deleteItem(db: Db, userId: number, id: number) {
|
||||
// Get item first (for image cleanup info)
|
||||
const [item] = await db.select().from(items).where(eq(items.id, id));
|
||||
const [item] = await db
|
||||
.select()
|
||||
.from(items)
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
await db.delete(items).where(eq(items.id, id));
|
||||
await db
|
||||
.delete(items)
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user