slug prog #1
This commit is contained in:
parent
630c6dabcf
commit
a6d729509e
10 changed files with 222 additions and 175 deletions
|
|
@ -16,6 +16,11 @@ export function setHouseholdSlugProvider(provider: (() => string | null) | null)
|
|||
householdSlugProvider = provider
|
||||
}
|
||||
|
||||
// Expose current household slug for SDK convenience (temporary during migration)
|
||||
export function getHouseholdSlug(): string | null {
|
||||
return householdSlugProvider ? householdSlugProvider() : null
|
||||
}
|
||||
|
||||
export function setAuthTokenProvider(provider: (() => string | null) | null) {
|
||||
authTokenProvider = provider
|
||||
}
|
||||
|
|
|
|||
123
src/api/sdk.ts
123
src/api/sdk.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { api } from '@/api/client'
|
||||
import { api, getHouseholdSlug, fetchApi } from '@/api/client'
|
||||
import type { components } from '@/api/types'
|
||||
import {
|
||||
toDate,
|
||||
|
|
@ -33,6 +33,12 @@ function httpError(response: Response, error: unknown): Error {
|
|||
return new Error(`${response.status} ${response.statusText || 'HTTP error'}`)
|
||||
}
|
||||
|
||||
function requireSlug(explicit?: string): string {
|
||||
const slug = explicit ?? getHouseholdSlug() ?? null
|
||||
if (!slug) throw new Error('Missing household slug')
|
||||
return slug
|
||||
}
|
||||
|
||||
// decodeLookup moved to domain/decoders to be reused across SDK and other modules
|
||||
|
||||
// Shopping list mapped view types now come from domain/types
|
||||
|
|
@ -156,52 +162,66 @@ export async function listRecipes(params?: { q?: string | null; cursor?: string
|
|||
if (params.cursor !== undefined) query.cursor = params.cursor
|
||||
if (typeof params.limit === 'number') query.limit = params.limit
|
||||
}
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes', { params: { query } })
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/recipes', { params: { path: { householdSlug }, query } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return fromOpenApiPage(data ?? null, (r) => decodeRecipe(r))
|
||||
}
|
||||
|
||||
export async function getRecipe(id: number | string): Promise<Recipe> {
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes/{recipe_id}', { params: { path: { recipe_id: Number(id) } } })
|
||||
export async function getRecipe(householdSlug: string, id: number | string): Promise<Recipe> {
|
||||
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/recipes/{recipe_id}', {
|
||||
params: { path: { householdSlug: requireSlug(householdSlug), recipe_id: Number(id) } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = decodeRecipe(data)
|
||||
if (!mapped) throw new Error('Recipe not found')
|
||||
return mapped
|
||||
}
|
||||
|
||||
export async function saveRecipe(recipe: components['schemas']['Recipe-Input']): Promise<Recipe | null> {
|
||||
const { data, error, response } = await api.POST('/api/v1/recipes', { body: recipe, params: { cookie: { user_id: 0 } } })
|
||||
export async function saveRecipe(recipe: components['schemas']['RecipeCreate']): Promise<Recipe | null> {
|
||||
const householdSlug = requireSlug()
|
||||
// Map "Recipe-Input" to "RecipeCreate"
|
||||
const body: components['schemas']['RecipeCreate'] = {
|
||||
name: recipe.name,
|
||||
link: recipe.link,
|
||||
serves: recipe.serves,
|
||||
imageUrls: recipe.imageUrls ?? [],
|
||||
ingredients: recipe.ingredients ?? [],
|
||||
}
|
||||
const { data, error, response } = await api.POST('/api/v1/households/{householdSlug}/recipes', { params: { path: { householdSlug } }, body })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeRecipe(data)
|
||||
}
|
||||
|
||||
export async function deleteRecipe(id: number | string): Promise<void> {
|
||||
const { error, response } = await api.DELETE('/api/v1/recipes/{recipe_id}', { params: { path: { recipe_id: Number(id) }, cookie: { user_id: 0 } } })
|
||||
const householdSlug = requireSlug()
|
||||
const { error, response } = await api.DELETE('/api/v1/households/{householdSlug}/recipes/{recipe_id}', { params: { path: { householdSlug, recipe_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
}
|
||||
|
||||
export async function parseRecipe(url: string): Promise<Recipe | null> {
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes/parse', { params: { query: { url }, cookie: { user_id: 0 } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const res = await fetchApi('/api/v1/recipes/parse?url=' + encodeURIComponent(url), { method: 'GET' })
|
||||
if (!res.ok) throw httpError(res, null)
|
||||
const data = await res.json()
|
||||
return decodeRecipe(data)
|
||||
}
|
||||
|
||||
export async function parseIngredients(lines: string[]): Promise<Ingredient[]> {
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes/ingredients/parse', {
|
||||
params: { query: { ingredients: lines } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeIngredients(data ?? [])
|
||||
const url = '/api/v1/recipes/ingredients/parse?' + new URLSearchParams(lines.map((v) => ['ingredients', v]))
|
||||
const res = await fetchApi(url, { method: 'GET' })
|
||||
if (!res.ok) throw httpError(res, null)
|
||||
const data = await res.json()
|
||||
return decodeIngredients(Array.isArray(data) ? data : [])
|
||||
}
|
||||
|
||||
export async function parseProduct(
|
||||
ingredient: Pick<components['schemas']['Ingredient'], 'name' | 'line'>,
|
||||
url: string
|
||||
): Promise<components['schemas']['Product'] | null> {
|
||||
const body: components['schemas']['ProductUrl'] = { url, tags: [ingredient.name, ingredient.line] }
|
||||
const res = await api.POST('/api/v1/products', { body })
|
||||
if (!res.response.ok) throw httpError(res.response, res.error)
|
||||
return res.data ?? null
|
||||
const body = { url, tags: [ingredient.name, ingredient.line] }
|
||||
const res = await fetchApi('/api/v1/products', { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } })
|
||||
if (!res.ok) throw httpError(res, null)
|
||||
return (await res.json()) ?? null
|
||||
}
|
||||
|
||||
// Persons
|
||||
|
|
@ -212,8 +232,10 @@ async function listPersons(params?: { q?: string | null; cursor?: string | null;
|
|||
if (params.cursor !== undefined) query.cursor = params.cursor
|
||||
if (typeof params.limit === 'number') query.limit = params.limit
|
||||
}
|
||||
const { data, error, response } = await api.GET('/api/v1/persons', { params: { query } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const url = '/api/v1/persons' + (Object.keys(query).length ? ('?' + new URLSearchParams(query as Record<string, string>)) : '')
|
||||
const res = await fetchApi(url, { method: 'GET' })
|
||||
if (!res.ok) throw httpError(res, null)
|
||||
const data = await res.json()
|
||||
const normalized = Array.isArray(data) ? { items: data } : (data ?? null)
|
||||
return fromOpenApiPage<components['schemas']['Person'], components['schemas']['Person']>(normalized, (p) => p)
|
||||
}
|
||||
|
|
@ -228,8 +250,9 @@ export async function searchPersons(name: string): Promise<Page<components['sche
|
|||
|
||||
// Meals
|
||||
export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {
|
||||
const { data, error, response } = await api.GET('/api/v1/meals/upcoming', {
|
||||
params: { query: { from: from.toISOString(), to: to.toISOString() } },
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/meals/upcoming', {
|
||||
params: { path: { householdSlug }, query: { from: from.toISOString(), to: to.toISOString() } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const list = Array.isArray(data) ? data : []
|
||||
|
|
@ -240,7 +263,8 @@ export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {
|
|||
}
|
||||
|
||||
export async function getMeal(id: number | string): Promise<Meal> {
|
||||
const { data, error, response } = await api.GET('/api/v1/meals/{meal_id}', { params: { path: { meal_id: Number(id) } } })
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/meals/{meal_id}', { params: { path: { householdSlug, meal_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = decodeMeal(data)
|
||||
if (!mapped) throw new Error('Meal not found')
|
||||
|
|
@ -249,23 +273,25 @@ export async function getMeal(id: number | string): Promise<Meal> {
|
|||
|
||||
export async function saveMeal(meal: components['schemas']['Meal-Input']): Promise<Meal | null> {
|
||||
const hasId = typeof meal.id === 'number' && meal.id >= 0
|
||||
const householdSlug = requireSlug()
|
||||
if (hasId) {
|
||||
const { data, error, response } = await api.PUT('/api/v1/meals/{meal_id}', {
|
||||
params: { path: { meal_id: Number(meal.id) } },
|
||||
const { data, error, response } = await api.PUT('/api/v1/households/{householdSlug}/meals/{meal_id}', {
|
||||
params: { path: { householdSlug, meal_id: Number(meal.id) } },
|
||||
body: meal,
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeMeal(data)
|
||||
} else {
|
||||
const { data, error, response } = await api.POST('/api/v1/meals', { body: meal })
|
||||
const { data, error, response } = await api.POST('/api/v1/households/{householdSlug}/meals', { params: { path: { householdSlug } }, body: meal })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeMeal(data)
|
||||
}
|
||||
}
|
||||
|
||||
export async function markMealConsumed(mealId: number | string): Promise<Meal> {
|
||||
const { data, error, response } = await api.POST('/api/v1/meals/{meal_id}/consumed', {
|
||||
params: { path: { meal_id: Number(mealId) }, cookie: { user_id: 0 } },
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.POST('/api/v1/households/{householdSlug}/meals/{meal_id}/consumed', {
|
||||
params: { path: { householdSlug, meal_id: Number(mealId) } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = decodeMeal(data)
|
||||
|
|
@ -274,37 +300,27 @@ export async function markMealConsumed(mealId: number | string): Promise<Meal> {
|
|||
}
|
||||
|
||||
export async function deleteMeal(mealId: number | string): Promise<void> {
|
||||
const { error, response } = await api.DELETE('/api/v1/meals/{meal_id}', {
|
||||
params: { path: { meal_id: Number(mealId) }, cookie: { user_id: 0 } },
|
||||
const householdSlug = requireSlug()
|
||||
const { error, response } = await api.DELETE('/api/v1/households/{householdSlug}/meals/{meal_id}', {
|
||||
params: { path: { householdSlug, meal_id: Number(mealId) } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
}
|
||||
|
||||
// Shopping
|
||||
export async function getMyShoppingList(): Promise<Ingredient[]> {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/current/me/ingredients', { params: { cookie: { user_id: 0 } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeIngredients(data ?? [])
|
||||
}
|
||||
|
||||
export async function saveMyShoppingList(items: components['schemas']['Ingredient'][]): Promise<Ingredient[]> {
|
||||
const { data, error, response } = await api.POST('/api/v1/shopping/current/me/ingredients', {
|
||||
body: items,
|
||||
params: { cookie: { user_id: 0 } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeIngredients(data ?? [])
|
||||
}
|
||||
// getMyShoppingList/saveMyShoppingList endpoints removed in v2; not used by UI currently
|
||||
|
||||
export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/{list_id}', { params: { path: { list_id: Number(id) } } })
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/shopping/{list_id}', { params: { path: { householdSlug, list_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = mapPurchasedShoppingList(data)
|
||||
return mapped?.list ?? null
|
||||
}
|
||||
|
||||
export async function getCurrentShoppingList(): Promise<CurrentShoppingListDTO> {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/current')
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/shopping/current', { params: { path: { householdSlug } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = mapCurrentShoppingList(data)
|
||||
if (!mapped) throw new Error('Failed to map current shopping list')
|
||||
|
|
@ -333,9 +349,10 @@ export async function purchaseShoppingList(
|
|||
storeName: 'home',
|
||||
items,
|
||||
}
|
||||
const { data, error, response } = await api.POST('/api/v1/shopping', {
|
||||
const householdSlug = requireSlug()
|
||||
const { data, error, response } = await api.POST('/api/v1/households/{householdSlug}/shopping', {
|
||||
params: { path: { householdSlug } },
|
||||
body,
|
||||
params: { cookie: { user_id: 0 } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = mapPurchasedShoppingList(data)
|
||||
|
|
@ -343,16 +360,18 @@ export async function purchaseShoppingList(
|
|||
}
|
||||
|
||||
export async function requestMeal(mealId: number | string): Promise<void> {
|
||||
const { error, response } = await api.POST('/api/v1/shopping/current/meals/me', {
|
||||
const householdSlug = requireSlug()
|
||||
const { error, response } = await api.POST('/api/v1/households/{householdSlug}/shopping/current/meals/me', {
|
||||
params: { path: { householdSlug } },
|
||||
body: { mealId: Number(mealId) },
|
||||
params: { cookie: { user_id: 0 } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
}
|
||||
|
||||
export async function unrequestMeal(mealId: number | string): Promise<void> {
|
||||
const { error, response } = await api.DELETE('/api/v1/shopping/current/meals/{meal_id}', {
|
||||
params: { path: { meal_id: Number(mealId) }, cookie: { user_id: 0 } },
|
||||
const householdSlug = requireSlug()
|
||||
const { error, response } = await api.DELETE('/api/v1/households/{householdSlug}/shopping/current/meals/{meal_id}', {
|
||||
params: { path: { householdSlug, meal_id: Number(mealId) } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
}
|
||||
|
|
|
|||
176
src/api/types.ts
176
src/api/types.ts
|
|
@ -393,7 +393,7 @@ export interface components {
|
|||
};
|
||||
/** Mealslookup */
|
||||
mealsLookup: {
|
||||
[key: string]: components["schemas"]["Meal-Output"];
|
||||
[key: string]: components["schemas"]["Meal"];
|
||||
};
|
||||
/** Shoppinglistlookup */
|
||||
shoppingListLookup: {
|
||||
|
|
@ -401,7 +401,7 @@ export interface components {
|
|||
};
|
||||
/** Recipeslookup */
|
||||
recipesLookup: {
|
||||
[key: string]: components["schemas"]["Recipe-Output"];
|
||||
[key: string]: components["schemas"]["Recipe"];
|
||||
};
|
||||
};
|
||||
/** HTTPValidationError */
|
||||
|
|
@ -521,7 +521,7 @@ export interface components {
|
|||
consumedDate?: string | null;
|
||||
};
|
||||
/** Meal */
|
||||
"Meal-Input": {
|
||||
Meal: {
|
||||
/**
|
||||
* Id
|
||||
* @default -1
|
||||
|
|
@ -541,34 +541,7 @@ export interface components {
|
|||
/** Consumers */
|
||||
consumers?: components["schemas"]["Person"][];
|
||||
/** Recipes */
|
||||
recipes?: components["schemas"]["MealRecipe-Input"][];
|
||||
/** Extraingredients */
|
||||
extraIngredients?: components["schemas"]["Ingredient"][];
|
||||
/** Purchasedate */
|
||||
purchaseDate?: string | null;
|
||||
};
|
||||
/** Meal */
|
||||
"Meal-Output": {
|
||||
/**
|
||||
* Id
|
||||
* @default -1
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Suggesteddate
|
||||
* Format: date-time
|
||||
*/
|
||||
suggestedDate: string;
|
||||
/** Consumeddate */
|
||||
consumedDate?: string | null;
|
||||
/** Chefs */
|
||||
chefs?: components["schemas"]["Person"][];
|
||||
/** Cleanup */
|
||||
cleanup?: components["schemas"]["Person"][];
|
||||
/** Consumers */
|
||||
consumers?: components["schemas"]["Person"][];
|
||||
/** Recipes */
|
||||
recipes?: components["schemas"]["MealRecipe-Output"][];
|
||||
recipes?: components["schemas"]["MealRecipe"][];
|
||||
/** Extraingredients */
|
||||
extraIngredients?: components["schemas"]["Ingredient"][];
|
||||
/** Purchasedate */
|
||||
|
|
@ -579,25 +552,89 @@ export interface components {
|
|||
/** Mealid */
|
||||
mealId: number;
|
||||
};
|
||||
/** MealRecipe */
|
||||
"MealRecipe-Input": {
|
||||
/** Mealid */
|
||||
mealId: number;
|
||||
/** Recipeid */
|
||||
recipeId: number;
|
||||
/** Servings */
|
||||
servings: number;
|
||||
recipe?: components["schemas"]["Recipe-Input"] | null;
|
||||
/** MealIn */
|
||||
MealIn: {
|
||||
/**
|
||||
* Id
|
||||
* @default -1
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Suggesteddate
|
||||
* Format: date-time
|
||||
*/
|
||||
suggestedDate: string;
|
||||
/** Consumeddate */
|
||||
consumedDate?: string | null;
|
||||
/** Chefs */
|
||||
chefs: components["schemas"]["MemberRef"][];
|
||||
/** Cleanup */
|
||||
cleanup: components["schemas"]["MemberRef"][];
|
||||
/** Consumers */
|
||||
consumers: components["schemas"]["MemberRef"][];
|
||||
/**
|
||||
* Recipes
|
||||
* @default []
|
||||
*/
|
||||
recipes: components["schemas"]["MealRecipeIn"][];
|
||||
/**
|
||||
* Extraingredients
|
||||
* @default []
|
||||
*/
|
||||
extraIngredients: components["schemas"]["Ingredient"][];
|
||||
};
|
||||
/** MealOut */
|
||||
MealOut: {
|
||||
/**
|
||||
* Id
|
||||
* @default -1
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Suggesteddate
|
||||
* Format: date-time
|
||||
*/
|
||||
suggestedDate: string;
|
||||
/** Consumeddate */
|
||||
consumedDate?: string | null;
|
||||
/** Chefs */
|
||||
chefs: components["schemas"]["MemberRef"][];
|
||||
/** Cleanup */
|
||||
cleanup: components["schemas"]["MemberRef"][];
|
||||
/** Consumers */
|
||||
consumers: components["schemas"]["MemberRef"][];
|
||||
/** Recipes */
|
||||
recipes: components["schemas"]["MealRecipe"][];
|
||||
/** Extraingredients */
|
||||
extraIngredients: components["schemas"]["Ingredient"][];
|
||||
/** Purchasedate */
|
||||
purchaseDate?: string | null;
|
||||
};
|
||||
/** MealRecipe */
|
||||
"MealRecipe-Output": {
|
||||
MealRecipe: {
|
||||
/** Mealid */
|
||||
mealId: number;
|
||||
/** Recipeid */
|
||||
recipeId: number;
|
||||
/** Servings */
|
||||
servings: number;
|
||||
recipe?: components["schemas"]["Recipe-Output"] | null;
|
||||
recipe?: components["schemas"]["Recipe"] | null;
|
||||
};
|
||||
/** MealRecipeIn */
|
||||
MealRecipeIn: {
|
||||
/** Mealid */
|
||||
mealId: number;
|
||||
/** Recipeid */
|
||||
recipeId: number;
|
||||
/** Servings */
|
||||
servings: number;
|
||||
};
|
||||
/** MemberRef */
|
||||
MemberRef: {
|
||||
/** Id */
|
||||
id: number;
|
||||
/** Displayname */
|
||||
displayName: string;
|
||||
};
|
||||
/** Ok */
|
||||
Ok: {
|
||||
|
|
@ -685,7 +722,7 @@ export interface components {
|
|||
list: components["schemas"]["ShoppingListOut"];
|
||||
/** Mealslookup */
|
||||
mealsLookup: {
|
||||
[key: string]: components["schemas"]["Meal-Output"];
|
||||
[key: string]: components["schemas"]["Meal"];
|
||||
};
|
||||
/** Ingredientslookup */
|
||||
ingredientsLookup: {
|
||||
|
|
@ -693,44 +730,11 @@ export interface components {
|
|||
};
|
||||
/** Recipeslookup */
|
||||
recipesLookup: {
|
||||
[key: string]: components["schemas"]["Recipe-Output"];
|
||||
[key: string]: components["schemas"]["Recipe"];
|
||||
};
|
||||
};
|
||||
/** Recipe */
|
||||
"Recipe-Input": {
|
||||
/**
|
||||
* Id
|
||||
* @default -1
|
||||
*/
|
||||
id: number;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Link */
|
||||
link: string;
|
||||
/** Serves */
|
||||
serves: number;
|
||||
/** Imageurls */
|
||||
imageUrls?: string[];
|
||||
/** Ingredients */
|
||||
ingredients?: components["schemas"]["Ingredient"][];
|
||||
/** Basedonrecipe */
|
||||
basedOnRecipe?: number | null;
|
||||
/**
|
||||
* Datecreated
|
||||
* Format: date-time
|
||||
*/
|
||||
dateCreated?: string;
|
||||
/** Createdbyid */
|
||||
createdById: number;
|
||||
createdBy?: components["schemas"]["Person"] | null;
|
||||
/** Datehidden */
|
||||
dateHidden?: string | null;
|
||||
/** Hiddenbyid */
|
||||
hiddenById?: number | null;
|
||||
hiddenBy?: components["schemas"]["Person"] | null;
|
||||
};
|
||||
/** Recipe */
|
||||
"Recipe-Output": {
|
||||
Recipe: {
|
||||
/**
|
||||
* Id
|
||||
* @default -1
|
||||
|
|
@ -1382,7 +1386,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Output"][];
|
||||
"application/json": components["schemas"]["MealOut"][];
|
||||
};
|
||||
};
|
||||
403: components["responses"]["Problem403"];
|
||||
|
|
@ -1415,7 +1419,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Output"];
|
||||
"application/json": components["schemas"]["MealOut"];
|
||||
};
|
||||
};
|
||||
403: components["responses"]["Problem403"];
|
||||
|
|
@ -1443,7 +1447,7 @@ export interface operations {
|
|||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Input"];
|
||||
"application/json": components["schemas"]["MealIn"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
@ -1453,7 +1457,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Output"];
|
||||
"application/json": components["schemas"]["MealOut"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Problem400"];
|
||||
|
|
@ -1488,7 +1492,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Output"];
|
||||
"application/json": components["schemas"]["MealOut"];
|
||||
};
|
||||
};
|
||||
403: components["responses"]["Problem403"];
|
||||
|
|
@ -1526,7 +1530,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Output"];
|
||||
"application/json": components["schemas"]["MealOut"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Problem400"];
|
||||
|
|
@ -1554,7 +1558,7 @@ export interface operations {
|
|||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Input"];
|
||||
"application/json": components["schemas"]["MealIn"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
@ -1564,7 +1568,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Meal-Output"];
|
||||
"application/json": components["schemas"]["MealOut"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Problem400"];
|
||||
|
|
|
|||
|
|
@ -213,16 +213,8 @@ function addPerson(list: PeopleKey, person: Person) {
|
|||
|
||||
async function selectRecipe(recipe: { id: number | string }) {
|
||||
// Refetch to get additional details
|
||||
const r = await getRecipe(recipe.id)
|
||||
|
||||
if (r.createdBy) {
|
||||
addPersonIfNotExists(meal.chefs, r.createdBy)
|
||||
addPersonIfNotExists(meal.consumers, r.createdBy)
|
||||
|
||||
if (meal.cleanup.length === 0) {
|
||||
addPersonIfNotExists(meal.cleanup, r.createdBy)
|
||||
}
|
||||
}
|
||||
const slug = typeof route.params.householdSlug === 'string' ? route.params.householdSlug : ''
|
||||
const r = await getRecipe(slug, recipe.id)
|
||||
|
||||
meal.recipes.push({ recipe: r, recipeId: r.id, mealId: meal.id, servings: r.serves })
|
||||
}
|
||||
|
|
@ -230,8 +222,8 @@ async function selectRecipe(recipe: { id: number | string }) {
|
|||
async function onEditAdditionalIngredients(editing: boolean) {
|
||||
if (editing && meal.extraIngredients.length === 0) {
|
||||
addIngredient()
|
||||
} else {
|
||||
meal.extraIngredients = meal.extraIngredients.filter((i) => !!i.line)
|
||||
} else {
|
||||
meal.extraIngredients = meal.extraIngredients.filter((i: Ingredient) => !!i.line)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ import { useAlert } from '@/composables/useAlert'
|
|||
import { parseQueryString } from '@/router/helpers'
|
||||
import { getRecipe, parseRecipe, saveRecipe as saveRecipeApi, deleteRecipe as deleteRecipeApi } from '@/api/sdk'
|
||||
import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
|
||||
import type { Recipe as DomainRecipe, Ingredient, RecipeInput } from '@/domain/types'
|
||||
import type { Recipe as DomainRecipe, Ingredient } from '@/domain/types'
|
||||
import type { components } from '@/api/types'
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, required: false, default: undefined },
|
||||
|
|
@ -108,7 +109,8 @@ function parseLink() {
|
|||
async function refreshRecipe() {
|
||||
const id = props.id ? parseInt(props.id) : null
|
||||
if (id !== null && id >= 0) {
|
||||
const r = await getRecipe(id)
|
||||
const slug = typeof route.params.householdSlug === 'string' ? route.params.householdSlug : ''
|
||||
const r = await getRecipe(slug, id)
|
||||
recipe.value = r
|
||||
link.value = r.link ?? ''
|
||||
return
|
||||
|
|
@ -134,7 +136,7 @@ function deleteIngredient(ingredient: Ingredient) {
|
|||
}
|
||||
|
||||
async function saveRecipe() {
|
||||
const saved = recipe.value ? await saveRecipeApi(toRecipeInput(recipe.value)) : null
|
||||
const saved = recipe.value ? await saveRecipeApi(toRecipeCreate(recipe.value)) : null
|
||||
if (saved && saved.id >= 0) {
|
||||
showAlert({ heading: 'Recipe saved', message: 'Your recipe has been saved', type: 'success' })
|
||||
router.push(`/recipes/${saved.id}`)
|
||||
|
|
@ -147,7 +149,6 @@ function createFromScratch() {
|
|||
recipe.value = {
|
||||
id: -1,
|
||||
name: 'My new recipe',
|
||||
createdById: -1,
|
||||
link: '',
|
||||
ingredients: [],
|
||||
imageUrls: [],
|
||||
|
|
@ -189,21 +190,13 @@ watch(
|
|||
)
|
||||
|
||||
// expose functions for template binding names (automatic in <script setup>)
|
||||
function toRecipeInput(r: DomainRecipe): RecipeInput {
|
||||
function toRecipeCreate(r: DomainRecipe): components['schemas']['RecipeCreate'] {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
link: r.link,
|
||||
serves: r.serves,
|
||||
imageUrls: r.imageUrls ?? [],
|
||||
ingredients: r.ingredients ?? [],
|
||||
basedOnRecipe: r.basedOnRecipe ?? null,
|
||||
// let backend set created/hidden dates
|
||||
createdById: r.createdById,
|
||||
createdBy: r.createdBy ?? null,
|
||||
// dateHidden omitted
|
||||
hiddenById: r.hiddenById ?? null,
|
||||
hiddenBy: r.hiddenBy ?? null,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { server, http, HttpResponse } from './test-setup'
|
||||
import { getMeal, markMealConsumed, getUpcomingMeals } from '@/api/sdk'
|
||||
import { setHouseholdSlugProvider } from '@/api/client'
|
||||
|
||||
describe('meals api (typed client)', () => {
|
||||
it('gets a meal by id', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/meals/:id', ({ params }) => {
|
||||
http.get('*/api/v1/households/:householdSlug/meals/:id', ({ params }) => {
|
||||
return HttpResponse.json({ id: params.id, suggestedDate: '2025-01-01T00:00:00Z' })
|
||||
})
|
||||
)
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const meal = await getMeal(10)
|
||||
expect(meal.id).toBeDefined()
|
||||
})
|
||||
|
||||
it('marks a meal consumed', async () => {
|
||||
server.use(
|
||||
http.post('*/api/v1/meals/:id/consumed', () => {
|
||||
http.post('*/api/v1/households/:householdSlug/meals/:id/consumed', () => {
|
||||
return HttpResponse.json({ id: 10, consumedDate: '2025-01-02T00:00:00Z' })
|
||||
})
|
||||
)
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const meal = await markMealConsumed(10)
|
||||
expect(meal.consumedDate).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
it('lists upcoming meals', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/meals/upcoming', ({ request }) => {
|
||||
http.get('*/api/v1/households/:householdSlug/meals/upcoming', ({ request }) => {
|
||||
const url = new URL(request.url)
|
||||
if (!url.searchParams.get('from') || !url.searchParams.get('to')) {
|
||||
return new HttpResponse(null, { status: 400 })
|
||||
|
|
@ -36,6 +39,7 @@ describe('meals api (typed client)', () => {
|
|||
])
|
||||
})
|
||||
)
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const list = await getUpcomingMeals(new Date('2025-01-01T00:00:00Z'), new Date('2025-01-03T00:00:00Z'))
|
||||
expect(Array.isArray(list)).toBe(true)
|
||||
expect(list[0].suggestedDate).toBeInstanceOf(Date)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { getRecipe } from '@/api/sdk'
|
|||
describe('recipes api (errors)', () => {
|
||||
it('throws on 404 getRecipe', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/recipes/:id', () => new HttpResponse(null, { status: 404 }))
|
||||
http.get('*/api/v1/households/:householdSlug/recipes/:id', () => new HttpResponse(null, { status: 404 }))
|
||||
)
|
||||
await expect(getRecipe('999')).rejects.toBeTruthy()
|
||||
await expect(getRecipe('the-smiths', '999')).rejects.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { server, http, HttpResponse } from './test-setup'
|
||||
import { getRecipe } from '@/api/sdk'
|
||||
import { setHouseholdSlugProvider } from '@/api/client'
|
||||
|
||||
// Tests validate the typed client wrapper behavior without needing the backend
|
||||
|
||||
describe('recipes api (typed client)', () => {
|
||||
it('gets a recipe by id', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/recipes/:id', ({ params }) => {
|
||||
http.get('*/api/v1/households/:householdSlug/recipes/:id', ({ params }) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('MSW handler hit with params:', params)
|
||||
const { id } = params
|
||||
|
|
@ -15,7 +16,8 @@ describe('recipes api (typed client)', () => {
|
|||
})
|
||||
)
|
||||
|
||||
const data = await getRecipe('123')
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const data = await getRecipe('the-smiths', '123')
|
||||
expect(data).toBeTruthy()
|
||||
expect(data.name).toBe('Pancakes')
|
||||
})
|
||||
|
|
|
|||
23
tests/recipes.api.v2.test.ts
Normal file
23
tests/recipes.api.v2.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { server, http, HttpResponse } from './test-setup'
|
||||
import { getRecipe } from '@/api/sdk'
|
||||
import { setHouseholdSlugProvider } from '@/api/client'
|
||||
|
||||
// Drive migration to path-scoped recipes via householdSlug
|
||||
|
||||
describe('recipes api v2 (household-scoped)', () => {
|
||||
it('gets a recipe by id with householdSlug path', async () => {
|
||||
server.use(
|
||||
http.get('*/api/v1/households/:householdSlug/recipes/:recipe_id', ({ params }) => {
|
||||
const { householdSlug, recipe_id } = params
|
||||
expect(householdSlug).toBe('the-smiths')
|
||||
return HttpResponse.json({ id: Number(recipe_id), name: 'Pancakes', ingredients: [] }, { status: 200 })
|
||||
})
|
||||
)
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
|
||||
const data = await getRecipe('the-smiths', 123)
|
||||
expect(data).toBeTruthy()
|
||||
expect(data.name).toBe('Pancakes')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,30 +1,35 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { server, http, HttpResponse } from './test-setup'
|
||||
import { getCurrentShoppingList, getShoppingList, requestMeal, unrequestMeal, purchaseShoppingList } from '@/api/sdk'
|
||||
import { setHouseholdSlugProvider } from '@/api/client'
|
||||
|
||||
describe('shopping api (typed client)', () => {
|
||||
it('gets current shopping list', async () => {
|
||||
server.use(http.get('*/api/v1/shopping/current', () => HttpResponse.json({ outstandingItems: [] })))
|
||||
server.use(http.get('*/api/v1/households/:householdSlug/shopping/current', () => HttpResponse.json({ outstandingItems: [], requestedMeals: [], purchasedItems: [], ingredientsLookup: {}, mealsLookup: {}, shoppingListLookup: {}, recipesLookup: {} })))
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const list = await getCurrentShoppingList()
|
||||
expect(list).toBeTruthy()
|
||||
})
|
||||
|
||||
it('gets a purchased shopping list by id', async () => {
|
||||
server.use(http.get('*/api/v1/shopping/:id', () => HttpResponse.json({ list: { id: 99, items: [] } })))
|
||||
server.use(http.get('*/api/v1/households/:householdSlug/shopping/:id', () => HttpResponse.json({ list: { id: 99, items: [] }, mealsLookup: {}, ingredientsLookup: {}, recipesLookup: {} })))
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const list = await getShoppingList(99)
|
||||
expect(list.id).toBe(99)
|
||||
})
|
||||
|
||||
it('requests and unrequests a meal', async () => {
|
||||
server.use(http.post('*/api/v1/shopping/current/meals/me', () => HttpResponse.json([{ id: 1 }])))
|
||||
server.use(http.post('*/api/v1/households/:householdSlug/shopping/current/meals/me', () => HttpResponse.json({ id: 1, personId: 1, mealId: 44, createdDate: new Date().toISOString(), kind: 'requestedMeal' })))
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
await requestMeal(44)
|
||||
|
||||
server.use(http.delete('*/api/v1/shopping/current/meals/:id', () => new HttpResponse(null, { status: 204 })))
|
||||
server.use(http.delete('*/api/v1/households/:householdSlug/shopping/current/meals/:id', () => new HttpResponse(null, { status: 204 })))
|
||||
await unrequestMeal(44)
|
||||
})
|
||||
|
||||
it('purchases a list', async () => {
|
||||
server.use(http.post('*/api/v1/shopping', () => HttpResponse.json({ list: { id: 1, items: [] } })))
|
||||
server.use(http.post('*/api/v1/households/:householdSlug/shopping', () => HttpResponse.json({ list: { id: 1, items: [] }, mealsLookup: {}, ingredientsLookup: {}, recipesLookup: {} })))
|
||||
setHouseholdSlugProvider(() => 'the-smiths')
|
||||
const list = await purchaseShoppingList([{ type: 'existing', id: 1, personId: 42 }])
|
||||
expect(list.id).toBe(1)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue