feat: implement backend API for Pantree Phase 1 MVP

- Project setup: package.json, tsconfig.json, jest.config.js, .env.example
- Config: env.ts, constants.ts (ALLOWED_UNITS, bcrypt rounds, JWT, deletion windows)
- DB: Knex connection, knexfile, migrations 001-006 (users, password_reset_tokens,
  pantry_items, recipes+recipe_ingredients, shopping_lists+items, deleted_records)
- Seeds: 10 seeded recipes with full ingredient lists
- Middleware: JWT authMiddleware (validates token + user existence), errorHandler
- Utils: Pino logger, JWT sign helper, Zod validators for all request shapes
- Services: authService (signup/signin/google/pwd-reset/soft-delete/restore),
  pantryService (CRUD + case-insensitive duplicate guard),
  recipeService (browse+filter+scale), shoppingListService (CRUD+merge logic),
  syncService (delta sync + tombstone cleanup), emailService (SendGrid)
- Routes: /v1/auth, /v1/pantry, /v1/recipes, /v1/shopping-lists, /v1/sync
- App: Express factory (createApp), server entry point
- Jobs: node-cron daily hard-delete + tombstone cleanup
- Tests: validators, utils, auth, pantry, recipes, shopping lists, sync
This commit is contained in:
Azriel
2026-05-10 15:00:15 +00:00
parent d755eea792
commit e633d693da
44 changed files with 3106 additions and 0 deletions

164
src/db/seeds/001_recipes.ts Normal file
View File

@@ -0,0 +1,164 @@
import type { Knex } from 'knex';
const recipes = [
{
name: 'Chocolate Chip Cookies',
servings: 24,
instructions: '1. Preheat oven to 375°F.\n2. Cream butter and sugars.\n3. Beat in eggs and vanilla.\n4. Mix in flour, baking soda, and salt.\n5. Stir in chocolate chips.\n6. Drop by spoonfuls onto baking sheet.\n7. Bake 9-11 minutes until golden.',
ingredients: [
{ item_name: 'All-Purpose Flour', quantity: 2.25, unit: 'cups' },
{ item_name: 'Butter', quantity: 1, unit: 'cups' },
{ item_name: 'Granulated Sugar', quantity: 0.75, unit: 'cups' },
{ item_name: 'Brown Sugar', quantity: 0.75, unit: 'cups' },
{ item_name: 'Eggs', quantity: 2, unit: 'pieces' },
{ item_name: 'Vanilla Extract', quantity: 1, unit: 'tsp' },
{ item_name: 'Baking Soda', quantity: 1, unit: 'tsp' },
{ item_name: 'Chocolate Chips', quantity: 2, unit: 'cups' },
],
},
{
name: 'Classic Pancakes',
servings: 4,
instructions: '1. Mix dry ingredients.\n2. Whisk wet ingredients separately.\n3. Combine wet and dry.\n4. Cook on greased griddle over medium heat.\n5. Flip when bubbles form.',
ingredients: [
{ item_name: 'All-Purpose Flour', quantity: 1.5, unit: 'cups' },
{ item_name: 'Milk', quantity: 1.25, unit: 'cups' },
{ item_name: 'Eggs', quantity: 1, unit: 'pieces' },
{ item_name: 'Butter', quantity: 3, unit: 'tbsp' },
{ item_name: 'Baking Powder', quantity: 2, unit: 'tsp' },
{ item_name: 'Granulated Sugar', quantity: 1, unit: 'tbsp' },
{ item_name: 'Salt', quantity: 1, unit: 'tsp' },
],
},
{
name: 'Spaghetti Bolognese',
servings: 4,
instructions: '1. Brown ground beef.\n2. Add onion and garlic, cook until soft.\n3. Add tomatoes and simmer 30 minutes.\n4. Cook pasta.\n5. Serve sauce over pasta.',
ingredients: [
{ item_name: 'Spaghetti', quantity: 400, unit: 'g' },
{ item_name: 'Ground Beef', quantity: 500, unit: 'g' },
{ item_name: 'Onion', quantity: 1, unit: 'whole' },
{ item_name: 'Garlic', quantity: 3, unit: 'cloves' },
{ item_name: 'Canned Tomatoes', quantity: 2, unit: 'can' },
{ item_name: 'Olive Oil', quantity: 2, unit: 'tbsp' },
{ item_name: 'Salt', quantity: 1, unit: 'tsp' },
],
},
{
name: 'Caesar Salad',
servings: 2,
instructions: '1. Wash and chop romaine.\n2. Make dressing with garlic, lemon, and parmesan.\n3. Toss lettuce with dressing.\n4. Top with croutons and extra parmesan.',
ingredients: [
{ item_name: 'Romaine Lettuce', quantity: 1, unit: 'whole' },
{ item_name: 'Parmesan Cheese', quantity: 0.5, unit: 'cups' },
{ item_name: 'Garlic', quantity: 2, unit: 'cloves' },
{ item_name: 'Lemon', quantity: 1, unit: 'whole' },
{ item_name: 'Olive Oil', quantity: 3, unit: 'tbsp' },
{ item_name: 'Croutons', quantity: 1, unit: 'cups' },
],
},
{
name: 'Banana Bread',
servings: 8,
instructions: '1. Preheat oven to 350°F.\n2. Mash bananas.\n3. Mix wet ingredients.\n4. Fold in dry ingredients.\n5. Pour into loaf pan.\n6. Bake 60-65 minutes.',
ingredients: [
{ item_name: 'Ripe Bananas', quantity: 3, unit: 'whole' },
{ item_name: 'All-Purpose Flour', quantity: 1.5, unit: 'cups' },
{ item_name: 'Butter', quantity: 0.33, unit: 'cups' },
{ item_name: 'Granulated Sugar', quantity: 0.75, unit: 'cups' },
{ item_name: 'Eggs', quantity: 1, unit: 'pieces' },
{ item_name: 'Baking Soda', quantity: 1, unit: 'tsp' },
{ item_name: 'Salt', quantity: 0.25, unit: 'tsp' },
],
},
{
name: 'Chicken Stir Fry',
servings: 4,
instructions: '1. Slice chicken and vegetables.\n2. Heat oil in wok.\n3. Cook chicken until done.\n4. Add vegetables and stir fry.\n5. Add sauce and toss.\n6. Serve over rice.',
ingredients: [
{ item_name: 'Chicken Breast', quantity: 500, unit: 'g' },
{ item_name: 'Bell Pepper', quantity: 2, unit: 'whole' },
{ item_name: 'Broccoli', quantity: 2, unit: 'cups' },
{ item_name: 'Soy Sauce', quantity: 3, unit: 'tbsp' },
{ item_name: 'Garlic', quantity: 3, unit: 'cloves' },
{ item_name: 'Vegetable Oil', quantity: 2, unit: 'tbsp' },
{ item_name: 'Rice', quantity: 2, unit: 'cups' },
],
},
{
name: 'Guacamole',
servings: 4,
instructions: '1. Halve and pit avocados.\n2. Scoop flesh into bowl.\n3. Mash with fork.\n4. Add lime juice, salt, onion, cilantro.\n5. Mix and adjust seasoning.',
ingredients: [
{ item_name: 'Avocado', quantity: 3, unit: 'whole' },
{ item_name: 'Lime', quantity: 1, unit: 'whole' },
{ item_name: 'Red Onion', quantity: 0.25, unit: 'whole' },
{ item_name: 'Cilantro', quantity: 2, unit: 'tbsp' },
{ item_name: 'Salt', quantity: 0.5, unit: 'tsp' },
],
},
{
name: 'Tomato Soup',
servings: 4,
instructions: '1. Sauté onion and garlic.\n2. Add tomatoes and broth.\n3. Simmer 20 minutes.\n4. Blend until smooth.\n5. Season with salt and pepper.',
ingredients: [
{ item_name: 'Canned Tomatoes', quantity: 2, unit: 'can' },
{ item_name: 'Onion', quantity: 1, unit: 'whole' },
{ item_name: 'Garlic', quantity: 2, unit: 'cloves' },
{ item_name: 'Vegetable Broth', quantity: 2, unit: 'cups' },
{ item_name: 'Olive Oil', quantity: 2, unit: 'tbsp' },
{ item_name: 'Salt', quantity: 1, unit: 'tsp' },
],
},
{
name: 'French Toast',
servings: 2,
instructions: '1. Whisk eggs, milk, and cinnamon.\n2. Dip bread slices.\n3. Cook on buttered pan until golden.\n4. Serve with maple syrup.',
ingredients: [
{ item_name: 'Bread', quantity: 4, unit: 'slices' },
{ item_name: 'Eggs', quantity: 2, unit: 'pieces' },
{ item_name: 'Milk', quantity: 0.25, unit: 'cups' },
{ item_name: 'Butter', quantity: 1, unit: 'tbsp' },
{ item_name: 'Cinnamon', quantity: 0.5, unit: 'tsp' },
],
},
{
name: 'Oatmeal',
servings: 1,
instructions: '1. Bring water or milk to boil.\n2. Add oats.\n3. Cook 5 minutes stirring occasionally.\n4. Top with fruit and honey.',
ingredients: [
{ item_name: 'Rolled Oats', quantity: 0.5, unit: 'cups' },
{ item_name: 'Milk', quantity: 1, unit: 'cups' },
{ item_name: 'Honey', quantity: 1, unit: 'tbsp' },
{ item_name: 'Salt', quantity: 1, unit: 'pinch' },
],
},
];
export async function seed(knex: Knex): Promise<void> {
// Clear existing
await knex('recipe_ingredients').delete();
await knex('recipes').delete();
for (const recipe of recipes) {
const [inserted] = await knex('recipes')
.insert({
name: recipe.name,
servings: recipe.servings,
instructions: recipe.instructions,
})
.returning('id');
const recipeId = inserted.id;
await knex('recipe_ingredients').insert(
recipe.ingredients.map((ing) => ({
recipe_id: recipeId,
item_name: ing.item_name,
item_name_lower: ing.item_name.toLowerCase(),
quantity: ing.quantity,
unit: ing.unit,
}))
);
}
}