// Gardens data layer: zod-validated shapes for /api/v1/gardens plus the // react-query hooks the /gardens page uses. import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { z } from 'zod' import { ApiError, api } from './api' import type { UnitPref } from './units' const unitPrefSchema = z.enum(['metric', 'imperial']) export const gardenSchema = z.object({ id: z.number(), ownerId: z.number(), name: z.string(), widthCm: z.number(), heightCm: z.number(), unitPref: unitPrefSchema, notes: z.string(), version: z.number(), createdAt: z.string(), updatedAt: z.string(), }) export type Garden = z.infer const gardensKey = ['gardens'] as const export const gardensQueryOptions = queryOptions({ queryKey: gardensKey, queryFn: async (): Promise => z.array(gardenSchema).parse(await api.get('/gardens')), }) export function useGardens() { return useQuery(gardensQueryOptions) } export interface GardenInput { name: string widthCm: number heightCm: number unitPref: UnitPref notes: string } export function useCreateGarden() { const qc = useQueryClient() return useMutation({ mutationFn: async (input: GardenInput): Promise => gardenSchema.parse(await api.post('/gardens', input)), onSuccess: () => qc.invalidateQueries({ queryKey: gardensKey }), }) } export interface GardenUpdate extends GardenInput { id: number version: number } export function useUpdateGarden() { const qc = useQueryClient() return useMutation({ // id travels with the mutation variables (not the hook) so callers don't need // a placeholder id before a garden is chosen. mutationFn: async ({ id, ...input }: GardenUpdate): Promise => gardenSchema.parse(await api.patch(`/gardens/${id}`, input)), onSuccess: () => qc.invalidateQueries({ queryKey: gardensKey }), }) } export function useDeleteGarden() { const qc = useQueryClient() return useMutation({ mutationFn: (id: number) => api.delete(`/gardens/${id}`), onSuccess: () => qc.invalidateQueries({ queryKey: gardensKey }), }) } /** * If err is a 409 version conflict, return the fresh server row it carries * (under `current`), so a form can rebase onto it; otherwise null. */ export function conflictGarden(err: unknown): Garden | null { if (err instanceof ApiError && err.isConflict && err.body && typeof err.body === 'object') { const current = (err.body as { current?: unknown }).current const parsed = gardenSchema.safeParse(current) if (parsed.success) return parsed.data } return null }