113 lines
2.8 KiB
JavaScript
113 lines
2.8 KiB
JavaScript
'use strict';
|
|
|
|
const { getDb } = require('../db/knex');
|
|
const { AppError } = require('../utils/errors');
|
|
|
|
function formatItem(row) {
|
|
return {
|
|
id: row.id,
|
|
item_name: row.item_name,
|
|
quantity: row.quantity,
|
|
last_modified: row.last_modified,
|
|
created_at: row.created_at,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns all pantry items for the authenticated user, ordered alphabetically.
|
|
*/
|
|
async function getPantryItems(userId) {
|
|
const db = getDb();
|
|
const items = await db('pantry_items')
|
|
.where({ user_id: userId })
|
|
.orderBy('item_name_lower', 'asc');
|
|
return items.map(formatItem);
|
|
}
|
|
|
|
/**
|
|
* Adds a new pantry item.
|
|
* Rejects with 409 if an item with the same name already exists (case-insensitive).
|
|
* Auto-merge is intentionally not performed — explicit > implicit.
|
|
*/
|
|
async function addPantryItem(userId, { item_name, quantity }) {
|
|
const db = getDb();
|
|
const trimmedName = item_name.trim();
|
|
|
|
const existing = await db('pantry_items')
|
|
.where({ user_id: userId })
|
|
.whereRaw('item_name_lower = LOWER(?)', [trimmedName])
|
|
.first();
|
|
|
|
if (existing) {
|
|
throw new AppError(
|
|
409,
|
|
'DUPLICATE_ITEM',
|
|
`'${existing.item_name}' already exists in your pantry.`,
|
|
{ existing_item: formatItem(existing) }
|
|
);
|
|
}
|
|
|
|
const [item] = await db('pantry_items')
|
|
.insert({ user_id: userId, item_name: trimmedName, quantity })
|
|
.returning('*');
|
|
|
|
return formatItem(item);
|
|
}
|
|
|
|
/**
|
|
* Updates the quantity of an existing pantry item.
|
|
* Server sets last_modified = NOW() — server clock is authoritative.
|
|
* Verifies ownership: item must belong to the requesting user.
|
|
*/
|
|
async function updatePantryItem(userId, itemId, { quantity }) {
|
|
const db = getDb();
|
|
|
|
const existing = await db('pantry_items')
|
|
.where({ id: itemId, user_id: userId })
|
|
.first();
|
|
|
|
if (!existing) {
|
|
throw new AppError(404, 'NOT_FOUND', 'Pantry item not found.');
|
|
}
|
|
|
|
const now = new Date();
|
|
const [item] = await db('pantry_items')
|
|
.where({ id: itemId, user_id: userId })
|
|
.update({ quantity, last_modified: now, updated_at: now })
|
|
.returning('*');
|
|
|
|
return formatItem(item);
|
|
}
|
|
|
|
/**
|
|
* Deletes a pantry item and records a tombstone for sync.
|
|
* Verifies ownership before deletion.
|
|
*/
|
|
async function deletePantryItem(userId, itemId) {
|
|
const db = getDb();
|
|
|
|
const existing = await db('pantry_items')
|
|
.where({ id: itemId, user_id: userId })
|
|
.first();
|
|
|
|
if (!existing) {
|
|
throw new AppError(404, 'NOT_FOUND', 'Pantry item not found.');
|
|
}
|
|
|
|
await db.transaction(async (trx) => {
|
|
await trx('pantry_items').where({ id: itemId, user_id: userId }).delete();
|
|
await trx('deleted_records').insert({
|
|
user_id: userId,
|
|
record_type: 'pantry_item',
|
|
record_id: itemId,
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
getPantryItems,
|
|
addPantryItem,
|
|
updatePantryItem,
|
|
deletePantryItem,
|
|
};
|