Compare commits
No commits in common. "b08561c9bbc23a5a30aaa6ea57a72775a985102c" and "211489ca5544ae3ac28b374466f39c124f095192" have entirely different histories.
b08561c9bb
...
211489ca55
8 changed files with 133 additions and 357 deletions
12
README.md
12
README.md
|
|
@ -98,15 +98,3 @@ Environment
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
- Unit tests use MSW; the client defaults to a localhost base in tests for easy mocking
|
- Unit tests use MSW; the client defaults to a localhost base in tests for easy mocking
|
||||||
|
|
||||||
### CurrentShoppingList item kinds
|
|
||||||
|
|
||||||
The OpenAPI spec models current shopping list items as distinct kinds:
|
|
||||||
- outstandingItems: ListIngredientItem[]
|
|
||||||
- requestedMeals: RequestedMealItem[]
|
|
||||||
- purchasedItems: ListIngredientItem[]
|
|
||||||
|
|
||||||
The SDK maps these to a domain DTO (`CurrentShoppingListDTO`) and may attach refs (`ingredient`, `recipe`, `meal`, `list`) for convenience. UI code should:
|
|
||||||
- Prefer stable IDs (`ingredientId`, `mealId`, `recipeId`, `listId`) for actions and lookups
|
|
||||||
- Treat attached refs as optional view helpers (never required)
|
|
||||||
- Keep all normalization at the boundary (decoders); avoid casts and runtime type checks in app code
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,18 @@
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import type { components } from '@/api/types'
|
import type { components } from '@/api/types'
|
||||||
import {
|
import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient, decodeLookup } from '@/domain/decoders'
|
||||||
toDate,
|
|
||||||
decodeMeal,
|
|
||||||
decodeRecipe,
|
|
||||||
decodeIngredients,
|
|
||||||
decodeShoppingList,
|
|
||||||
decodeShoppingListItems,
|
|
||||||
decodeListIngredientItems,
|
|
||||||
decodeRequestedMealItems,
|
|
||||||
decodeIngredient,
|
|
||||||
decodeLookup,
|
|
||||||
} from '@/domain/decoders'
|
|
||||||
import type {
|
import type {
|
||||||
Recipe,
|
Recipe,
|
||||||
Meal,
|
Meal,
|
||||||
Ingredient,
|
Ingredient,
|
||||||
ShoppingList,
|
ShoppingList,
|
||||||
ShoppingListItemWithRefs,
|
ShoppingListItemWithRefs,
|
||||||
ListIngredientItemWithRefs,
|
|
||||||
RequestedMealItemWithRefs,
|
|
||||||
CurrentShoppingListDTO,
|
CurrentShoppingListDTO,
|
||||||
PurchasedShoppingListDTO,
|
PurchasedShoppingListDTO,
|
||||||
ShoppingLookups,
|
ShoppingLookups,
|
||||||
} from '@/domain/types'
|
} from '@/domain/types'
|
||||||
import { fromOpenApiPage, type Page } from '@/domain/pagination'
|
import { fromOpenApiPage, type Page } from '@/domain/pagination'
|
||||||
|
|
||||||
import type { PurchaseRequest } from '@/domain/commands'
|
|
||||||
function httpError(response: Response, error: unknown): Error {
|
function httpError(response: Response, error: unknown): Error {
|
||||||
if (error instanceof Error) return error
|
if (error instanceof Error) return error
|
||||||
if (typeof error === 'string') return new Error(error)
|
if (typeof error === 'string') return new Error(error)
|
||||||
|
|
@ -38,32 +24,34 @@ function httpError(response: Response, error: unknown): Error {
|
||||||
// Shopping list mapped view types now come from domain/types
|
// Shopping list mapped view types now come from domain/types
|
||||||
|
|
||||||
function attachItemRefs(
|
function attachItemRefs(
|
||||||
items: Array<ShoppingListItemWithRefs | ListIngredientItemWithRefs | RequestedMealItemWithRefs> | null | undefined,
|
items: Array<ShoppingListItemWithRefs> | null | undefined,
|
||||||
lookups: ShoppingLookups
|
lookups: ShoppingLookups
|
||||||
): void {
|
): void {
|
||||||
if (!Array.isArray(items)) return
|
if (!Array.isArray(items)) return
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
// ingredient ref
|
const ingredientId = item.ingredientId ?? undefined
|
||||||
if ('ingredientId' in item && item.ingredientId !== undefined && lookups.ingredientsLookup) {
|
const mealId = item.mealId ?? undefined
|
||||||
const v = lookups.ingredientsLookup[String(item.ingredientId)]
|
const recipeId = item.recipeId ?? undefined
|
||||||
|
const listId = item.listId ?? undefined
|
||||||
|
const created = item.createdDate
|
||||||
|
|
||||||
|
if (ingredientId !== undefined && lookups.ingredientsLookup) {
|
||||||
|
const v = lookups.ingredientsLookup[String(ingredientId)]
|
||||||
if (v !== undefined) item.ingredient = v
|
if (v !== undefined) item.ingredient = v
|
||||||
}
|
}
|
||||||
// meal ref (present on all item types)
|
if (mealId !== undefined && lookups.mealsLookup) {
|
||||||
if ('mealId' in item && item.mealId !== undefined && lookups.mealsLookup) {
|
const v = lookups.mealsLookup[String(mealId)]
|
||||||
const v = lookups.mealsLookup[String(item.mealId)]
|
|
||||||
if (v !== undefined) item.meal = v
|
if (v !== undefined) item.meal = v
|
||||||
}
|
}
|
||||||
// recipe ref
|
if (recipeId !== undefined && lookups.recipesLookup) {
|
||||||
if ('recipeId' in item && item.recipeId !== undefined && lookups.recipesLookup) {
|
const v = lookups.recipesLookup[String(recipeId)]
|
||||||
const v = lookups.recipesLookup[String(item.recipeId)]
|
|
||||||
if (v !== undefined) item.recipe = v
|
if (v !== undefined) item.recipe = v
|
||||||
}
|
}
|
||||||
// list ref
|
if (listId !== undefined && lookups.shoppingListLookup) {
|
||||||
if ('listId' in item && item.listId !== undefined && lookups.shoppingListLookup) {
|
const v = lookups.shoppingListLookup[String(listId)]
|
||||||
const v = lookups.shoppingListLookup[String(item.listId)]
|
|
||||||
if (v !== undefined) item.list = v
|
if (v !== undefined) item.list = v
|
||||||
}
|
}
|
||||||
if ('createdDate' in item) item.createdDate = toDate(item.createdDate)
|
if (created !== undefined) item.createdDate = toDate(created)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,9 +115,9 @@ export function mapCurrentShoppingList(dto: components['schemas']['CurrentShoppi
|
||||||
}
|
}
|
||||||
|
|
||||||
const dtoOut: CurrentShoppingListDTO = {
|
const dtoOut: CurrentShoppingListDTO = {
|
||||||
outstandingItems: decodeListIngredientItems(outstandingRaw ?? []),
|
outstandingItems: decodeShoppingListItems(outstandingRaw ?? []),
|
||||||
requestedMeals: decodeRequestedMealItems(requestedRaw ?? []),
|
requestedMeals: decodeShoppingListItems(requestedRaw ?? []),
|
||||||
purchasedItems: decodeListIngredientItems(purchasedRaw ?? []),
|
purchasedItems: decodeShoppingListItems(purchasedRaw ?? []),
|
||||||
...(ingredientsLookup && { ingredientsLookup }),
|
...(ingredientsLookup && { ingredientsLookup }),
|
||||||
...(mealsLookup && { mealsLookup }),
|
...(mealsLookup && { mealsLookup }),
|
||||||
...(recipesLookup && { recipesLookup }),
|
...(recipesLookup && { recipesLookup }),
|
||||||
|
|
@ -311,26 +299,33 @@ export async function getCurrentShoppingList(): Promise<CurrentShoppingListDTO>
|
||||||
return mapped
|
return mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurchaseRequest comes from domain/commands
|
type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null }
|
||||||
|
type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null }
|
||||||
|
export type PurchaseRequest = PurchaseExisting | PurchaseRefs
|
||||||
|
|
||||||
export async function purchaseShoppingList(
|
export async function purchaseShoppingList(
|
||||||
completedRequests: PurchaseRequest[]
|
completedRequests: PurchaseRequest[]
|
||||||
): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||||
if (!Array.isArray(completedRequests) || completedRequests.length === 0) return null
|
if (!Array.isArray(completedRequests) || completedRequests.length === 0) return null
|
||||||
// Map incoming requests: if id provided and >= 0, use it; otherwise send identifiers for ingredient/recipe/meal
|
// Map incoming requests: if id provided and >= 0, use it; otherwise send identifiers for ingredient/recipe/meal
|
||||||
const items: components['schemas']['IngredientPurchaseItemIn'][] = completedRequests.map((i) => ({
|
const items: components['schemas']['ShoppingListItem'][] = completedRequests.map((i) =>
|
||||||
personId: i.personId,
|
i.type === 'existing'
|
||||||
ingredientId: i.ingredientId ?? -1,
|
? { id: i.id, personId: i.personId, ingredientId: i.ingredientId ?? null }
|
||||||
recipeId: i.type === 'refs' ? i.recipeId ?? null : null,
|
: {
|
||||||
mealId: i.type === 'refs' ? i.mealId ?? null : null,
|
id: -1,
|
||||||
createdDate: null,
|
personId: i.personId,
|
||||||
}))
|
ingredientId: i.ingredientId ?? null,
|
||||||
|
recipeId: i.recipeId ?? null,
|
||||||
|
mealId: i.mealId ?? null,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (items.length === 0) return null
|
if (items.length === 0) return null
|
||||||
|
|
||||||
const body: components['schemas']['PurchaseListIn'] = {
|
const body: components['schemas']['ShoppingList'] = {
|
||||||
// Default to a valid StoreNameOut per updated OpenAPI ("home" | "coles" | "woolworths")
|
id: -1,
|
||||||
storeName: 'home',
|
storeName: '',
|
||||||
|
purchasedById: -1,
|
||||||
items,
|
items,
|
||||||
}
|
}
|
||||||
const { data, error, response } = await api.POST('/api/v1/shopping', {
|
const { data, error, response } = await api.POST('/api/v1/shopping', {
|
||||||
|
|
@ -356,6 +351,3 @@ export async function unrequestMeal(mealId: number | string): Promise<void> {
|
||||||
})
|
})
|
||||||
if (!response.ok) throw httpError(response, error)
|
if (!response.ok) throw httpError(response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export domain command types for convenience at SDK surface
|
|
||||||
export type { PurchaseRequest } from '@/domain/commands'
|
|
||||||
|
|
|
||||||
229
src/api/types.ts
229
src/api/types.ts
|
|
@ -340,25 +340,25 @@ export interface components {
|
||||||
/** CurrentShoppingList */
|
/** CurrentShoppingList */
|
||||||
CurrentShoppingList: {
|
CurrentShoppingList: {
|
||||||
/** Outstandingitems */
|
/** Outstandingitems */
|
||||||
outstandingItems: components["schemas"]["ListIngredientItem"][];
|
outstandingItems: components["schemas"]["ShoppingListItem"][];
|
||||||
/** Requestedmeals */
|
/** Requestedmeals */
|
||||||
requestedMeals: components["schemas"]["RequestedMealItem"][];
|
requestedMeals: components["schemas"]["ShoppingListItem"][];
|
||||||
/** Purchaseditems */
|
/** Purchaseditems */
|
||||||
purchasedItems: components["schemas"]["ListIngredientItem"][];
|
purchasedItems?: components["schemas"]["ShoppingListItem"][];
|
||||||
/** Ingredientslookup */
|
/** Ingredientslookup */
|
||||||
ingredientsLookup: {
|
ingredientsLookup?: {
|
||||||
[key: string]: components["schemas"]["Ingredient"];
|
[key: string]: components["schemas"]["Ingredient"];
|
||||||
};
|
};
|
||||||
/** Mealslookup */
|
/** Mealslookup */
|
||||||
mealsLookup: {
|
mealsLookup?: {
|
||||||
[key: string]: components["schemas"]["Meal-Output"];
|
[key: string]: components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
/** Shoppinglistlookup */
|
/** Shoppinglistlookup */
|
||||||
shoppingListLookup: {
|
shoppingListLookup?: {
|
||||||
[key: string]: components["schemas"]["ShoppingListOut"];
|
[key: string]: components["schemas"]["ShoppingList"];
|
||||||
};
|
};
|
||||||
/** Recipeslookup */
|
/** Recipeslookup */
|
||||||
recipesLookup: {
|
recipesLookup?: {
|
||||||
[key: string]: components["schemas"]["Recipe-Output"];
|
[key: string]: components["schemas"]["Recipe-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -404,49 +404,6 @@ export interface components {
|
||||||
mealId?: number | null;
|
mealId?: number | null;
|
||||||
product?: components["schemas"]["Product"] | null;
|
product?: components["schemas"]["Product"] | null;
|
||||||
};
|
};
|
||||||
/** IngredientPurchaseItemIn */
|
|
||||||
IngredientPurchaseItemIn: {
|
|
||||||
/** Ingredientid */
|
|
||||||
ingredientId: number;
|
|
||||||
/** Personid */
|
|
||||||
personId: number;
|
|
||||||
/** Createddate */
|
|
||||||
createdDate?: string | null;
|
|
||||||
/** Mealid */
|
|
||||||
mealId?: number | null;
|
|
||||||
/** Recipeid */
|
|
||||||
recipeId?: number | null;
|
|
||||||
};
|
|
||||||
/** ListIngredientItem */
|
|
||||||
ListIngredientItem: {
|
|
||||||
/**
|
|
||||||
* Kind
|
|
||||||
* @default ingredient
|
|
||||||
* @constant
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
kind: "ingredient";
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* @default -1
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/** Ingredientid */
|
|
||||||
ingredientId: number;
|
|
||||||
/** Personid */
|
|
||||||
personId: number;
|
|
||||||
/**
|
|
||||||
* Createddate
|
|
||||||
* Format: date-time
|
|
||||||
*/
|
|
||||||
createdDate: string;
|
|
||||||
/** Listid */
|
|
||||||
listId?: number | null;
|
|
||||||
/** Mealid */
|
|
||||||
mealId?: number | null;
|
|
||||||
/** Recipeid */
|
|
||||||
recipeId?: number | null;
|
|
||||||
};
|
|
||||||
/** LoginBody */
|
/** LoginBody */
|
||||||
LoginBody: {
|
LoginBody: {
|
||||||
/** Username */
|
/** Username */
|
||||||
|
|
@ -511,33 +468,6 @@ export interface components {
|
||||||
/** Mealid */
|
/** Mealid */
|
||||||
mealId: number;
|
mealId: number;
|
||||||
};
|
};
|
||||||
/** MealOut */
|
|
||||||
MealOut: {
|
|
||||||
/**
|
|
||||||
* 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"][];
|
|
||||||
/** Extraingredients */
|
|
||||||
extraIngredients: components["schemas"]["Ingredient"][];
|
|
||||||
/** Purchasedate */
|
|
||||||
purchaseDate?: string | null;
|
|
||||||
};
|
|
||||||
/** MealRecipe */
|
/** MealRecipe */
|
||||||
"MealRecipe-Input": {
|
"MealRecipe-Input": {
|
||||||
/** Mealid */
|
/** Mealid */
|
||||||
|
|
@ -581,10 +511,10 @@ export interface components {
|
||||||
*/
|
*/
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
/** Page[RecipeOut] */
|
/** Page[Recipe] */
|
||||||
Page_RecipeOut_: {
|
Page_Recipe_: {
|
||||||
/** Items */
|
/** Items */
|
||||||
items: components["schemas"]["RecipeOut"][];
|
items: components["schemas"]["Recipe-Output"][];
|
||||||
/** Nextcursor */
|
/** Nextcursor */
|
||||||
nextCursor?: string | null;
|
nextCursor?: string | null;
|
||||||
/** Prevcursor */
|
/** Prevcursor */
|
||||||
|
|
@ -655,25 +585,19 @@ export interface components {
|
||||||
/** Tags */
|
/** Tags */
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
/** PurchaseListIn */
|
|
||||||
PurchaseListIn: {
|
|
||||||
storeName: components["schemas"]["StoreNameOut"];
|
|
||||||
/** Items */
|
|
||||||
items: components["schemas"]["IngredientPurchaseItemIn"][];
|
|
||||||
};
|
|
||||||
/** PurchasedShoppingList */
|
/** PurchasedShoppingList */
|
||||||
PurchasedShoppingList: {
|
PurchasedShoppingList: {
|
||||||
list: components["schemas"]["ShoppingListOut"];
|
list: components["schemas"]["ShoppingList"];
|
||||||
/** Mealslookup */
|
/** Mealslookup */
|
||||||
mealsLookup: {
|
mealsLookup?: {
|
||||||
[key: string]: components["schemas"]["Meal-Output"];
|
[key: string]: components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
/** Ingredientslookup */
|
/** Ingredientslookup */
|
||||||
ingredientsLookup: {
|
ingredientsLookup?: {
|
||||||
[key: string]: components["schemas"]["Ingredient"];
|
[key: string]: components["schemas"]["Ingredient"];
|
||||||
};
|
};
|
||||||
/** Recipeslookup */
|
/** Recipeslookup */
|
||||||
recipesLookup: {
|
recipesLookup?: {
|
||||||
[key: string]: components["schemas"]["Recipe-Output"];
|
[key: string]: components["schemas"]["Recipe-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -743,82 +667,54 @@ export interface components {
|
||||||
hiddenById?: number | null;
|
hiddenById?: number | null;
|
||||||
hiddenBy?: components["schemas"]["Person"] | null;
|
hiddenBy?: components["schemas"]["Person"] | null;
|
||||||
};
|
};
|
||||||
/** RecipeOut */
|
/** ShoppingList */
|
||||||
RecipeOut: {
|
ShoppingList: {
|
||||||
/**
|
/**
|
||||||
* Id
|
* Id
|
||||||
* @default -1
|
* @default -1
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/** Name */
|
|
||||||
name: string;
|
|
||||||
/** Link */
|
|
||||||
link: string;
|
|
||||||
/** Serves */
|
|
||||||
serves: number;
|
|
||||||
/** Imageurls */
|
|
||||||
imageUrls: string[];
|
|
||||||
/** Ingredients */
|
|
||||||
ingredients: components["schemas"]["Ingredient"][];
|
|
||||||
/** Basedonrecipe */
|
|
||||||
basedOnRecipe?: number | null;
|
|
||||||
/**
|
/**
|
||||||
* Datecreated
|
* Createddate
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
*/
|
*/
|
||||||
dateCreated: string;
|
createdDate?: string;
|
||||||
/** Createdbyid */
|
/** @default */
|
||||||
createdById: number;
|
storeName: components["schemas"]["StoreEnum"];
|
||||||
createdBy?: components["schemas"]["Person"] | null;
|
|
||||||
/** Datehidden */
|
|
||||||
dateHidden?: string | null;
|
|
||||||
/** Hiddenbyid */
|
|
||||||
hiddenById?: number | null;
|
|
||||||
hiddenBy?: components["schemas"]["Person"] | null;
|
|
||||||
};
|
|
||||||
/** RequestedMealItem */
|
|
||||||
RequestedMealItem: {
|
|
||||||
/**
|
/**
|
||||||
* Kind
|
* Purchasedbyid
|
||||||
* @default requestedMeal
|
|
||||||
* @constant
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
kind: "requestedMeal";
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* @default -1
|
* @default -1
|
||||||
*/
|
*/
|
||||||
id: number;
|
|
||||||
/** Personid */
|
|
||||||
personId: number;
|
|
||||||
/** Mealid */
|
|
||||||
mealId: number;
|
|
||||||
/**
|
|
||||||
* Createddate
|
|
||||||
* Format: date-time
|
|
||||||
*/
|
|
||||||
createdDate: string;
|
|
||||||
};
|
|
||||||
/** ShoppingListOut */
|
|
||||||
ShoppingListOut: {
|
|
||||||
/** Id */
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Createddate
|
|
||||||
* Format: date-time
|
|
||||||
*/
|
|
||||||
createdDate: string;
|
|
||||||
/**
|
|
||||||
* Storename
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
storeName: "woolworths" | "coles" | "home";
|
|
||||||
/** Purchasedbyid */
|
|
||||||
purchasedById: number;
|
purchasedById: number;
|
||||||
purchasedBy?: components["schemas"]["Person"] | null;
|
purchasedBy?: components["schemas"]["Person"] | null;
|
||||||
/** Items */
|
/** Items */
|
||||||
items: components["schemas"]["ListIngredientItem"][];
|
items?: components["schemas"]["ShoppingListItem"][];
|
||||||
|
};
|
||||||
|
/** ShoppingListItem */
|
||||||
|
ShoppingListItem: {
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
* @default -1
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/** Listid */
|
||||||
|
listId?: number | null;
|
||||||
|
/**
|
||||||
|
* Personid
|
||||||
|
* @default -1
|
||||||
|
*/
|
||||||
|
personId: number;
|
||||||
|
/** Ingredientid */
|
||||||
|
ingredientId?: number | null;
|
||||||
|
/** Recipeid */
|
||||||
|
recipeId?: number | null;
|
||||||
|
/** Mealid */
|
||||||
|
mealId?: number | null;
|
||||||
|
/**
|
||||||
|
* Createddate
|
||||||
|
* Format: date-time
|
||||||
|
*/
|
||||||
|
createdDate?: string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* StoreEnum
|
* StoreEnum
|
||||||
|
|
@ -834,11 +730,6 @@ export interface components {
|
||||||
/** Error Type */
|
/** Error Type */
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* StoreNameOut
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
StoreNameOut: "woolworths" | "coles" | "home";
|
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** @description Bad Request */
|
/** @description Bad Request */
|
||||||
|
|
@ -931,7 +822,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["RecipeOut"];
|
"application/json": components["schemas"]["Recipe-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
400: components["responses"]["Problem400"];
|
400: components["responses"]["Problem400"];
|
||||||
|
|
@ -1016,7 +907,7 @@ export interface operations {
|
||||||
* "total": 1
|
* "total": 1
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
"application/json": components["schemas"]["Page_RecipeOut_"];
|
"application/json": components["schemas"]["Page_Recipe_"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
|
|
@ -1051,7 +942,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["RecipeOut"];
|
"application/json": components["schemas"]["Recipe-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
400: components["responses"]["Problem400"];
|
400: components["responses"]["Problem400"];
|
||||||
|
|
@ -1083,7 +974,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["RecipeOut"];
|
"application/json": components["schemas"]["Recipe-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
404: components["responses"]["Problem404"];
|
404: components["responses"]["Problem404"];
|
||||||
|
|
@ -1181,7 +1072,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["MealOut"];
|
"application/json": components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
404: components["responses"]["Problem404"];
|
404: components["responses"]["Problem404"];
|
||||||
|
|
@ -1217,7 +1108,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["MealOut"];
|
"application/json": components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
400: components["responses"]["Problem400"];
|
400: components["responses"]["Problem400"];
|
||||||
|
|
@ -1252,7 +1143,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["MealOut"];
|
"application/json": components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
404: components["responses"]["Problem404"];
|
404: components["responses"]["Problem404"];
|
||||||
|
|
@ -1286,7 +1177,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["MealOut"];
|
"application/json": components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
400: components["responses"]["Problem400"];
|
400: components["responses"]["Problem400"];
|
||||||
|
|
@ -1322,7 +1213,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["MealOut"];
|
"application/json": components["schemas"]["Meal-Output"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
400: components["responses"]["Problem400"];
|
400: components["responses"]["Problem400"];
|
||||||
|
|
@ -1402,7 +1293,7 @@ export interface operations {
|
||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["PurchaseListIn"];
|
"application/json": components["schemas"]["ShoppingList"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
|
@ -1524,7 +1415,7 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["RequestedMealItem"];
|
"application/json": components["schemas"]["ShoppingListItem"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
404: components["responses"]["Problem404"];
|
404: components["responses"]["Problem404"];
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import * as sdk from '@/api/sdk'
|
import * as sdk from '@/api/sdk'
|
||||||
import type { ListIngredientItemWithRefs, RequestedMealItemWithRefs, Product, Meal } from '@/domain/types'
|
import type { ShoppingListItemWithRefs, Product, Meal } from '@/domain/types'
|
||||||
|
|
||||||
export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: ListIngredientItemWithRefs[] }
|
export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: ShoppingListItemWithRefs[] }
|
||||||
export type GroupByName = { type: 'name'; name: string; shoppingListItems: ListIngredientItemWithRefs[] }
|
export type GroupByName = { type: 'name'; name: string; shoppingListItems: ShoppingListItemWithRefs[] }
|
||||||
export type Group = GroupByProduct | GroupByName
|
export type Group = GroupByProduct | GroupByName
|
||||||
|
|
||||||
export function groupsToItems(groups: Group[]): ListIngredientItemWithRefs[] {
|
export function groupsToItems(groups: Group[]): ShoppingListItemWithRefs[] {
|
||||||
return groups.map((g) => g.shoppingListItems).flat()
|
return groups.map((g) => g.shoppingListItems).flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uniqueMeals(shoppingListItems: Array<ListIngredientItemWithRefs | RequestedMealItemWithRefs>): Meal[] {
|
export function uniqueMeals(shoppingListItems: ShoppingListItemWithRefs[]): Meal[] {
|
||||||
const mealsWithDuplicates = shoppingListItems.map((item) => item.meal).filter((m): m is Meal => !!m)
|
const mealsWithDuplicates = shoppingListItems.map((item) => item.meal).filter((m): m is Meal => !!m)
|
||||||
const mealsLookup: Record<string | number, Meal> = mealsWithDuplicates.reduce<Record<string | number, Meal>>(
|
const mealsLookup: Record<string | number, Meal> = mealsWithDuplicates.reduce<Record<string | number, Meal>>(
|
||||||
(acc, meal) => {
|
(acc, meal) => {
|
||||||
|
|
@ -21,7 +21,7 @@ export function uniqueMeals(shoppingListItems: Array<ListIngredientItemWithRefs
|
||||||
return Object.values(mealsLookup)
|
return Object.values(mealsLookup)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function itemsToGroups(shoppingListItems: ListIngredientItemWithRefs[]): Group[] {
|
export function itemsToGroups(shoppingListItems: ShoppingListItemWithRefs[]): Group[] {
|
||||||
const ingredients_by_product_id: Record<string | number, GroupByProduct> = {}
|
const ingredients_by_product_id: Record<string | number, GroupByProduct> = {}
|
||||||
const ingredients_by_name: Record<string, GroupByName> = {}
|
const ingredients_by_name: Record<string, GroupByName> = {}
|
||||||
for (const item of shoppingListItems) {
|
for (const item of shoppingListItems) {
|
||||||
|
|
@ -52,8 +52,8 @@ export function itemsToGroups(shoppingListItems: ListIngredientItemWithRefs[]):
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShopping() {
|
export function useShopping() {
|
||||||
const groupsFrom = (items?: ListIngredientItemWithRefs[]): Group[] => itemsToGroups(items ?? [])
|
const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => itemsToGroups(items ?? [])
|
||||||
const mealsFrom = (items?: Array<ListIngredientItemWithRefs | RequestedMealItemWithRefs>): Meal[] => uniqueMeals(items ?? [])
|
const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => uniqueMeals(items ?? [])
|
||||||
return {
|
return {
|
||||||
getCurrentShoppingList: sdk.getCurrentShoppingList,
|
getCurrentShoppingList: sdk.getCurrentShoppingList,
|
||||||
getShoppingList: sdk.getShoppingList,
|
getShoppingList: sdk.getShoppingList,
|
||||||
|
|
@ -68,14 +68,14 @@ export function useShopping() {
|
||||||
async purchaseFromGroups(groups: Group[]) {
|
async purchaseFromGroups(groups: Group[]) {
|
||||||
const items = groupsToItems(groups).map((i): sdk.PurchaseRequest => {
|
const items = groupsToItems(groups).map((i): sdk.PurchaseRequest => {
|
||||||
if (typeof i.id === 'number' && i.id >= 0) {
|
if (typeof i.id === 'number' && i.id >= 0) {
|
||||||
return { type: 'existing', id: i.id, personId: i.personId, ingredientId: i.ingredientId ?? null }
|
return { type: 'existing', id: i.id, personId: i.personId, ingredientId: i.ingredient?.id ?? null }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: 'refs',
|
type: 'refs',
|
||||||
personId: i.personId,
|
personId: i.personId,
|
||||||
ingredientId: i.ingredientId ?? null,
|
ingredientId: i.ingredient?.id ?? null,
|
||||||
recipeId: i.recipeId ?? null,
|
recipeId: i.recipe?.id ?? null,
|
||||||
mealId: i.mealId ?? null,
|
mealId: i.meal?.id ?? null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!items?.length) return null
|
if (!items?.length) return null
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Domain command types: stable UI intents, translated to OpenAPI at the SDK boundary
|
|
||||||
|
|
||||||
export type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null }
|
|
||||||
export type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null }
|
|
||||||
export type PurchaseRequest = PurchaseExisting | PurchaseRefs
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Recipe, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem, ShoppingListItemWithRefs, MealInput, ListIngredientItem, RequestedMealItem, ListIngredientItemWithRefs, RequestedMealItemWithRefs } from './types'
|
import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem, ShoppingListItemWithRefs, MealInput } from './types'
|
||||||
import type { components } from '@/api/types'
|
import type { components } from '@/api/types'
|
||||||
|
|
||||||
export function toDate(value: string | Date | null | undefined): Date | null {
|
export function toDate(value: string | Date | null | undefined): Date | null {
|
||||||
|
|
@ -23,25 +23,16 @@ export function decodeLookup<TIn, TOut>(
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeRecipe(
|
export function decodeRecipe(r: RecipeOut | null | undefined): Recipe {
|
||||||
r: components['schemas']['RecipeOut'] | components['schemas']['Recipe-Output'] | null | undefined
|
|
||||||
): Recipe {
|
|
||||||
if (!r) throw new Error('Invalid recipe payload')
|
if (!r) throw new Error('Invalid recipe payload')
|
||||||
// Normalize arrays that may be optional in legacy Recipe-Output
|
|
||||||
const imageUrls = r.imageUrls ?? []
|
|
||||||
const ingredients = r.ingredients ?? []
|
|
||||||
return {
|
return {
|
||||||
...r,
|
...r,
|
||||||
imageUrls,
|
|
||||||
ingredients,
|
|
||||||
dateCreated: toDate(r.dateCreated),
|
dateCreated: toDate(r.dateCreated),
|
||||||
dateHidden: toDate(r.dateHidden),
|
dateHidden: toDate(r.dateHidden),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeMeal(
|
export function decodeMeal(m: MealOut | null | undefined): Meal {
|
||||||
m: components['schemas']['MealOut'] | components['schemas']['Meal-Output'] | null | undefined
|
|
||||||
): Meal {
|
|
||||||
if (!m) throw new Error('Invalid meal payload')
|
if (!m) throw new Error('Invalid meal payload')
|
||||||
const recipes = Array.isArray(m.recipes)
|
const recipes = Array.isArray(m.recipes)
|
||||||
? m.recipes.map((mr) => decodeMealRecipe(mr))
|
? m.recipes.map((mr) => decodeMealRecipe(mr))
|
||||||
|
|
@ -79,7 +70,7 @@ export function decodeIngredients(list: components['schemas']['Ingredient'][] |
|
||||||
return list.map((i) => decodeIngredient(i))
|
return list.map((i) => decodeIngredient(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeShoppingListItem(i: components['schemas']['ListIngredientItem'] | null | undefined): ShoppingListItem {
|
function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem {
|
||||||
if (!i) throw new Error('Invalid shopping list item payload')
|
if (!i) throw new Error('Invalid shopping list item payload')
|
||||||
return {
|
return {
|
||||||
...i,
|
...i,
|
||||||
|
|
@ -87,13 +78,13 @@ function decodeShoppingListItem(i: components['schemas']['ListIngredientItem'] |
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeShoppingListItems(list: components['schemas']['ListIngredientItem'][] | null | undefined): ShoppingListItemWithRefs[] {
|
export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItemWithRefs[] {
|
||||||
if (!Array.isArray(list)) return []
|
if (!Array.isArray(list)) return []
|
||||||
// Build a new array with item clones to allow optional refs to be attached later
|
// Build a new array with item clones to allow optional refs to be attached later
|
||||||
return list.map((raw) => ({ ...decodeShoppingListItem(raw) }))
|
return list.map((raw) => ({ ...decodeShoppingListItem(raw) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeShoppingList(v: components['schemas']['ShoppingListOut'] | null | undefined): ShoppingList {
|
export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList {
|
||||||
if (!v) throw new Error('Invalid shopping list payload')
|
if (!v) throw new Error('Invalid shopping list payload')
|
||||||
const { items: rawItems, ...rest } = v
|
const { items: rawItems, ...rest } = v
|
||||||
const items = Array.isArray(rawItems) ? decodeShoppingListItems(rawItems) : undefined
|
const items = Array.isArray(rawItems) ? decodeShoppingListItems(rawItems) : undefined
|
||||||
|
|
@ -104,33 +95,6 @@ export function decodeShoppingList(v: components['schemas']['ShoppingListOut'] |
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New item decoders for tightened CurrentShoppingList
|
|
||||||
function decodeListIngredientItem(i: components['schemas']['ListIngredientItem'] | null | undefined): ListIngredientItem {
|
|
||||||
if (!i) throw new Error('Invalid list ingredient item payload')
|
|
||||||
return {
|
|
||||||
...i,
|
|
||||||
createdDate: toDate(i.createdDate),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeListIngredientItems(list: components['schemas']['ListIngredientItem'][] | null | undefined): ListIngredientItemWithRefs[] {
|
|
||||||
if (!Array.isArray(list)) return []
|
|
||||||
return list.map((raw) => ({ ...decodeListIngredientItem(raw) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeRequestedMealItem(i: components['schemas']['RequestedMealItem'] | null | undefined): RequestedMealItem {
|
|
||||||
if (!i) throw new Error('Invalid requested meal item payload')
|
|
||||||
return {
|
|
||||||
...i,
|
|
||||||
createdDate: toDate(i.createdDate),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeRequestedMealItems(list: components['schemas']['RequestedMealItem'][] | null | undefined): RequestedMealItemWithRefs[] {
|
|
||||||
if (!Array.isArray(list)) return []
|
|
||||||
return list.map((raw) => ({ ...decodeRequestedMealItem(raw) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary
|
// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary
|
||||||
export function toMealInput(meal: Meal): MealInput {
|
export function toMealInput(meal: Meal): MealInput {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,6 @@ import type { components } from '@/api/types'
|
||||||
// Utility mapped types
|
// Utility mapped types
|
||||||
export type Replace<T, M> = Omit<T, keyof M> & M
|
export type Replace<T, M> = Omit<T, keyof M> & M
|
||||||
export type WithDates<T, K extends keyof T> = Replace<T, { [P in K]: Date | null }>
|
export type WithDates<T, K extends keyof T> = Replace<T, { [P in K]: Date | null }>
|
||||||
// Convert selected array keys to their non-nullable array counterparts
|
|
||||||
export type NonNullableArrays<T, K extends keyof T> = Replace<T, { [P in K]: NonNullable<T[P]> extends Array<infer U> ? U[] : T[P] }>
|
|
||||||
// Make selected keys required (non-optional) on a type
|
|
||||||
export type RequiredKeys<T, K extends keyof T> = Replace<T, { [P in K]-?: NonNullable<T[P]> }>
|
|
||||||
|
|
||||||
// Common helpers (intentionally minimal to avoid unused exports)
|
// Common helpers (intentionally minimal to avoid unused exports)
|
||||||
export type Lookup<T> = Record<string, T>
|
export type Lookup<T> = Record<string, T>
|
||||||
|
|
@ -14,8 +10,8 @@ export type Lookup<T> = Record<string, T>
|
||||||
export type WithRefs<T, Refs extends object> = T & { [K in keyof Refs]?: Refs[K] | undefined }
|
export type WithRefs<T, Refs extends object> = T & { [K in keyof Refs]?: Refs[K] | undefined }
|
||||||
|
|
||||||
// Domain type aliases
|
// Domain type aliases
|
||||||
export type RecipeOut = components['schemas']['RecipeOut']
|
export type RecipeOut = components['schemas']['Recipe-Output']
|
||||||
export type MealOut = components['schemas']['MealOut']
|
export type MealOut = components['schemas']['Meal-Output']
|
||||||
export type Ingredient = components['schemas']['Ingredient']
|
export type Ingredient = components['schemas']['Ingredient']
|
||||||
export type Product = components['schemas']['Product']
|
export type Product = components['schemas']['Product']
|
||||||
export type Person = components['schemas']['Person']
|
export type Person = components['schemas']['Person']
|
||||||
|
|
@ -31,26 +27,24 @@ export type MealRecipe = Replace<MealRecipeOut, { recipe: Recipe | null }>
|
||||||
|
|
||||||
// Meal with decoded dates and nested MealRecipe with decoded recipe dates
|
// Meal with decoded dates and nested MealRecipe with decoded recipe dates
|
||||||
// Arrays (chefs, consumers, cleanup, recipes, extraIngredients) are non-nullable per OpenAPI spec
|
// Arrays (chefs, consumers, cleanup, recipes, extraIngredients) are non-nullable per OpenAPI spec
|
||||||
type MealBase = Replace<WithDates<MealOut, 'suggestedDate' | 'consumedDate' | 'purchaseDate'>, { recipes: MealRecipe[] }>
|
export type Meal = Replace<
|
||||||
export type Meal = RequiredKeys<
|
WithDates<MealOut, 'suggestedDate' | 'consumedDate' | 'purchaseDate'>,
|
||||||
NonNullableArrays<MealBase, 'chefs' | 'consumers' | 'cleanup' | 'extraIngredients'>,
|
{
|
||||||
'recipes' | 'chefs' | 'consumers' | 'cleanup' | 'extraIngredients'
|
recipes: MealRecipe[]
|
||||||
|
chefs: Person[]
|
||||||
|
consumers: Person[]
|
||||||
|
cleanup: Person[]
|
||||||
|
extraIngredients: Ingredient[]
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
// Shopping domain shapes with dates normalized
|
// Shopping domain shapes with dates normalized
|
||||||
// Items inside purchased lists and current lists share the ListIngredientItem shape
|
export type ShoppingListItem = WithDates<components['schemas']['ShoppingListItem'], 'createdDate'>
|
||||||
export type ShoppingListItem = WithDates<components['schemas']['ListIngredientItem'], 'createdDate'>
|
type ShoppingListBase = WithDates<components['schemas']['ShoppingList'], 'createdDate'>
|
||||||
type ShoppingListBase = WithDates<components['schemas']['ShoppingListOut'], 'createdDate'>
|
|
||||||
export type ShoppingList = Replace<ShoppingListBase, { items?: ShoppingListItem[] | undefined }>
|
export type ShoppingList = Replace<ShoppingListBase, { items?: ShoppingListItem[] | undefined }>
|
||||||
|
|
||||||
// Current shopping list item types (tightened OpenAPI)
|
// Refs attached to shopping list items
|
||||||
export type ListIngredientItem = WithDates<components['schemas']['ListIngredientItem'], 'createdDate'>
|
|
||||||
export type RequestedMealItem = WithDates<components['schemas']['RequestedMealItem'], 'createdDate'>
|
|
||||||
|
|
||||||
// Refs attached to items
|
|
||||||
export type ShoppingListItemWithRefs = WithRefs<ShoppingListItem, { ingredient: Ingredient; recipe: Recipe; meal: Meal; list: ShoppingList }>
|
export type ShoppingListItemWithRefs = WithRefs<ShoppingListItem, { ingredient: Ingredient; recipe: Recipe; meal: Meal; list: ShoppingList }>
|
||||||
export type ListIngredientItemWithRefs = WithRefs<ListIngredientItem, { ingredient: Ingredient; recipe: Recipe; meal: Meal; list: ShoppingList }>
|
|
||||||
export type RequestedMealItemWithRefs = WithRefs<RequestedMealItem, { meal: Meal }>
|
|
||||||
|
|
||||||
export type ShoppingListWithRefs = Replace<ShoppingList, { items?: ShoppingListItemWithRefs[] }>
|
export type ShoppingListWithRefs = Replace<ShoppingList, { items?: ShoppingListItemWithRefs[] }>
|
||||||
|
|
||||||
|
|
@ -64,9 +58,9 @@ export type ShoppingLookups = {
|
||||||
|
|
||||||
// DTO shapes returned by SDK for shopping pages
|
// DTO shapes returned by SDK for shopping pages
|
||||||
export type CurrentShoppingListDTO = {
|
export type CurrentShoppingListDTO = {
|
||||||
outstandingItems: ListIngredientItemWithRefs[]
|
outstandingItems: ShoppingListItemWithRefs[]
|
||||||
requestedMeals: RequestedMealItemWithRefs[]
|
requestedMeals: ShoppingListItemWithRefs[]
|
||||||
purchasedItems: ListIngredientItemWithRefs[]
|
purchasedItems: ShoppingListItemWithRefs[]
|
||||||
} & ShoppingLookups
|
} & ShoppingLookups
|
||||||
|
|
||||||
export type PurchasedShoppingListDTO = ShoppingLookups & {
|
export type PurchasedShoppingListDTO = ShoppingLookups & {
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { mapCurrentShoppingList, mapPurchasedShoppingList } from '@/api/sdk'
|
|
||||||
|
|
||||||
// These tests lock boundary behavior for partial/missing lookups and date normalization
|
|
||||||
|
|
||||||
describe('shopping mappers boundary', () => {
|
|
||||||
it('handles missing lookups gracefully (no refs attached)', () => {
|
|
||||||
const dto = {
|
|
||||||
outstandingItems: [
|
|
||||||
{ ingredientId: 10, mealId: 1, recipeId: 5, listId: 7, createdDate: '2025-01-01T00:00:00Z' },
|
|
||||||
],
|
|
||||||
requestedMeals: [
|
|
||||||
{ mealId: 2, createdDate: '2025-01-02T00:00:00Z' },
|
|
||||||
],
|
|
||||||
purchasedItems: [],
|
|
||||||
}
|
|
||||||
const mapped = mapCurrentShoppingList(dto as any)
|
|
||||||
expect(mapped.outstandingItems[0].ingredient).toBeUndefined()
|
|
||||||
expect(mapped.outstandingItems[0].meal).toBeUndefined()
|
|
||||||
expect(mapped.outstandingItems[0].recipe).toBeUndefined()
|
|
||||||
expect(mapped.outstandingItems[0].list).toBeUndefined()
|
|
||||||
expect(mapped.requestedMeals[0].meal).toBeUndefined()
|
|
||||||
// Dates normalized
|
|
||||||
expect(mapped.outstandingItems[0].createdDate).toBeInstanceOf(Date)
|
|
||||||
expect(mapped.requestedMeals[0].createdDate).toBeInstanceOf(Date)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('normalizes dates on purchased list and items even with partial lookups', () => {
|
|
||||||
const dto = {
|
|
||||||
ingredientsLookup: { 10: { id: 10, name: 'Milk' } },
|
|
||||||
list: {
|
|
||||||
id: 11,
|
|
||||||
createdDate: '2025-03-03T00:00:00Z',
|
|
||||||
items: [
|
|
||||||
{ ingredientId: 10, listId: 11, createdDate: '2025-03-03T00:00:00Z' },
|
|
||||||
{ ingredientId: 99, listId: 11, createdDate: '2025-03-03T00:00:00Z' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const mapped = mapPurchasedShoppingList(dto as any)
|
|
||||||
expect(mapped.list.createdDate).toBeInstanceOf(Date)
|
|
||||||
expect(mapped.list.items[0].createdDate).toBeInstanceOf(Date)
|
|
||||||
expect(mapped.list.items[1].createdDate).toBeInstanceOf(Date)
|
|
||||||
// First item gets ingredient ref, second does not
|
|
||||||
expect(mapped.list.items[0].ingredient?.name).toBe('Milk')
|
|
||||||
expect(mapped.list.items[1].ingredient).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Reference in a new issue