Files
pantree/src/services/pantryService.js

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,
};