mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
Derive client domain types from the shared schema contracts
Add entity/response Zod schemas to @trek/shared (place, trip, assignment, day, budget, packing, reservation), each matched against the producing server service, and re-export them from client types.ts instead of the hand-written duplicates that had drifted (name/title, amount/total_price, owner_id/user_id, cover_url/cover_image, ...). Updates the call sites and test fixtures the corrected types surfaced; type-only, no runtime behaviour change.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { assignmentPlaceSchema } from '../place/place.schema';
|
||||
|
||||
/**
|
||||
* Assignment API contract — single source of truth for the place↔day itinerary
|
||||
@@ -11,6 +12,38 @@ import { z } from 'zod';
|
||||
* request schemas + the bespoke 404/400 controller messages pin the rest.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Assignment participant embedded on an assignment
|
||||
* (server/src/services/queryHelpers.ts -> loadParticipantsByAssignmentIds).
|
||||
*/
|
||||
export const assignmentParticipantSchema = z.object({
|
||||
user_id: z.number(),
|
||||
username: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
});
|
||||
export type AssignmentParticipant = z.infer<typeof assignmentParticipantSchema>;
|
||||
|
||||
/**
|
||||
* Assignment entity as returned by the day/assignment endpoints
|
||||
* (server/src/services/queryHelpers.ts -> formatAssignmentWithPlace, and
|
||||
* assignmentService.getAssignmentWithPlace). The embedded `place` is the trimmed
|
||||
* assignment-place projection, NOT the full place pool entity. `assignment_time`
|
||||
* /`assignment_end_time` carry the per-assignment override times.
|
||||
*/
|
||||
export const assignmentSchema = z.object({
|
||||
id: z.number(),
|
||||
day_id: z.number(),
|
||||
place_id: z.number(),
|
||||
order_index: z.number(),
|
||||
notes: z.string().nullable().optional(),
|
||||
assignment_time: z.string().nullable().optional(),
|
||||
assignment_end_time: z.string().nullable().optional(),
|
||||
participants: z.array(assignmentParticipantSchema).optional(),
|
||||
created_at: z.string().optional(),
|
||||
place: assignmentPlaceSchema,
|
||||
});
|
||||
export type Assignment = z.infer<typeof assignmentSchema>;
|
||||
|
||||
export const assignmentCreateRequestSchema = z.object({
|
||||
place_id: z.union([z.number(), z.string()]),
|
||||
notes: z.string().nullable().optional(),
|
||||
|
||||
@@ -12,6 +12,44 @@ import { z } from 'zod';
|
||||
* linked reservation's metadata (and broadcasts reservation:updated).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Budget item member as embedded on a budget item
|
||||
* (server/src/services/budgetService.ts -> loadItemMembers). `paid` is the raw
|
||||
* SQLite INTEGER (0/1); `avatar_url` is the resolved avatar (avatarUrl()).
|
||||
*/
|
||||
export const budgetItemMemberSchema = z.object({
|
||||
user_id: z.number(),
|
||||
paid: z.number(),
|
||||
username: z.string(),
|
||||
avatar_url: z.string().nullable().optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
budget_item_id: z.number().optional(),
|
||||
});
|
||||
export type BudgetItemMember = z.infer<typeof budgetItemMemberSchema>;
|
||||
|
||||
/**
|
||||
* Budget item entity as returned by the budget list/create/update endpoints
|
||||
* (server/src/services/budgetService.ts). Columns of the `budget_items` table
|
||||
* plus the embedded `members` array. total_price is SQLite REAL.
|
||||
*/
|
||||
export const budgetItemSchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
category: z.string(),
|
||||
name: z.string(),
|
||||
total_price: z.number(),
|
||||
persons: z.number().nullable().optional(),
|
||||
days: z.number().nullable().optional(),
|
||||
note: z.string().nullable().optional(),
|
||||
reservation_id: z.number().nullable().optional(),
|
||||
paid_by_user_id: z.number().nullable().optional(),
|
||||
expense_date: z.string().nullable().optional(),
|
||||
sort_order: z.number().optional(),
|
||||
created_at: z.string().optional(),
|
||||
members: z.array(budgetItemMemberSchema).optional(),
|
||||
});
|
||||
export type BudgetItem = z.infer<typeof budgetItemSchema>;
|
||||
|
||||
export const budgetCreateItemRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
category: z.string().optional(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { assignmentSchema } from '../assignment/assignment.schema';
|
||||
|
||||
/**
|
||||
* Day + day-note API contract — single source of truth for the
|
||||
@@ -11,6 +12,39 @@ import { z } from 'zod';
|
||||
* (the legacy validateStringLengths middleware) — reproduced in the controller.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Day note entity (server day_notes table / dayNoteService). `sort_order` is
|
||||
* SQLite REAL; `icon` defaults to a note emoji.
|
||||
*/
|
||||
export const dayNoteSchema = z.object({
|
||||
id: z.number(),
|
||||
day_id: z.number(),
|
||||
trip_id: z.number().optional(),
|
||||
text: z.string(),
|
||||
time: z.string().nullable().optional(),
|
||||
icon: z.string().nullable().optional(),
|
||||
sort_order: z.number().optional(),
|
||||
created_at: z.string().optional(),
|
||||
});
|
||||
export type DayNote = z.infer<typeof dayNoteSchema>;
|
||||
|
||||
/**
|
||||
* Day entity as returned by the day list/get endpoints
|
||||
* (server/src/services/dayService.ts -> listDays). Columns of the `days` table
|
||||
* plus the embedded `assignments` and `notes_items` arrays.
|
||||
*/
|
||||
export const daySchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
day_number: z.number().optional(),
|
||||
date: z.string().nullable().optional(),
|
||||
title: z.string().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
assignments: z.array(assignmentSchema).optional(),
|
||||
notes_items: z.array(dayNoteSchema).optional(),
|
||||
});
|
||||
export type Day = z.infer<typeof daySchema>;
|
||||
|
||||
export const dayCreateRequestSchema = z.object({
|
||||
date: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
|
||||
@@ -13,6 +13,56 @@ import { z } from 'zod';
|
||||
|
||||
const open = z.record(z.string(), z.unknown());
|
||||
|
||||
/**
|
||||
* Packing item entity as returned by the packing endpoints
|
||||
* (server/src/services/packingService.ts -> SELECT * FROM packing_items).
|
||||
* `checked` is the raw SQLite INTEGER (0/1). Columns match the packing_items
|
||||
* table (see server DB): weight_grams/bag_id are nullable, quantity defaults 1.
|
||||
*/
|
||||
export const packingItemSchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
name: z.string(),
|
||||
checked: z.number(),
|
||||
category: z.string().nullable().optional(),
|
||||
sort_order: z.number(),
|
||||
weight_grams: z.number().nullable().optional(),
|
||||
bag_id: z.number().nullable().optional(),
|
||||
quantity: z.number().optional(),
|
||||
created_at: z.string().optional(),
|
||||
});
|
||||
export type PackingItem = z.infer<typeof packingItemSchema>;
|
||||
|
||||
/**
|
||||
* Packing bag member embedded on a bag (server packingService -> listBags).
|
||||
* `avatar` is the resolved avatar URL.
|
||||
*/
|
||||
export const packingBagMemberSchema = z.object({
|
||||
user_id: z.number(),
|
||||
username: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
});
|
||||
export type PackingBagMember = z.infer<typeof packingBagMemberSchema>;
|
||||
|
||||
/**
|
||||
* Packing bag entity (server packingService -> listBags). Columns of the
|
||||
* packing_bags table plus the embedded `members` array (and the optional
|
||||
* `assigned_username` join present on updateBag).
|
||||
*/
|
||||
export const packingBagSchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
weight_limit_grams: z.number().nullable().optional(),
|
||||
sort_order: z.number(),
|
||||
user_id: z.number().nullable().optional(),
|
||||
assigned_username: z.string().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
members: z.array(packingBagMemberSchema).optional(),
|
||||
});
|
||||
export type PackingBag = z.infer<typeof packingBagSchema>;
|
||||
|
||||
export const packingCreateItemRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
category: z.string().optional(),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { categorySchema } from '../category/category.schema';
|
||||
import { tagSchema } from '../tag/tag.schema';
|
||||
|
||||
/**
|
||||
* Place API contract — single source of truth for the /api/trips/:tripId/places
|
||||
@@ -14,6 +16,90 @@ import { z } from 'zod';
|
||||
|
||||
const open = z.record(z.string(), z.unknown());
|
||||
|
||||
/**
|
||||
* Embedded category as returned on a place — a trimmed projection of the
|
||||
* categories row (id/name/color/icon), built inline by placeService and
|
||||
* getPlaceWithTags. `null` when the place has no category_id.
|
||||
*/
|
||||
export const placeCategorySchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
name: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
icon: z.string().nullable(),
|
||||
})
|
||||
.nullable();
|
||||
export type PlaceCategory = z.infer<typeof placeCategorySchema>;
|
||||
|
||||
/**
|
||||
* Full place entity as returned by the place list / get / create / update
|
||||
* endpoints (server/src/services/placeService.ts -> getPlaceWithTags). All
|
||||
* columns of the `places` table (see server/data DB) plus the joined `category`
|
||||
* projection and `tags` array. Numbers (lat/lng/price) are SQLite REAL, ids are
|
||||
* INTEGER; provider-derived columns are nullable.
|
||||
*/
|
||||
export const placeSchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
lat: z.number().nullable().optional(),
|
||||
lng: z.number().nullable().optional(),
|
||||
address: z.string().nullable().optional(),
|
||||
category_id: z.number().nullable().optional(),
|
||||
price: z.number().nullable().optional(),
|
||||
currency: z.string().nullable().optional(),
|
||||
reservation_status: z.string().nullable().optional(),
|
||||
reservation_notes: z.string().nullable().optional(),
|
||||
reservation_datetime: z.string().nullable().optional(),
|
||||
place_time: z.string().nullable().optional(),
|
||||
end_time: z.string().nullable().optional(),
|
||||
duration_minutes: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
image_url: z.string().nullable().optional(),
|
||||
google_place_id: z.string().nullable().optional(),
|
||||
osm_id: z.string().nullable().optional(),
|
||||
route_geometry: z.string().nullable().optional(),
|
||||
website: z.string().nullable().optional(),
|
||||
phone: z.string().nullable().optional(),
|
||||
transport_mode: z.string().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
category: placeCategorySchema.optional(),
|
||||
tags: z.array(tagSchema.partial()).optional(),
|
||||
});
|
||||
export type Place = z.infer<typeof placeSchema>;
|
||||
|
||||
/**
|
||||
* Trimmed place projection embedded inside a day-assignment response
|
||||
* (server/src/services/queryHelpers.ts -> formatAssignmentWithPlace). This is a
|
||||
* SUBSET of the full place: no trip_id / osm_id / route_geometry / created_at /
|
||||
* reservation_* — only the fields the planner needs to render the itinerary card.
|
||||
*/
|
||||
export const assignmentPlaceSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
lat: z.number().nullable().optional(),
|
||||
lng: z.number().nullable().optional(),
|
||||
address: z.string().nullable().optional(),
|
||||
category_id: z.number().nullable().optional(),
|
||||
price: z.number().nullable().optional(),
|
||||
currency: z.string().nullable().optional(),
|
||||
place_time: z.string().nullable().optional(),
|
||||
end_time: z.string().nullable().optional(),
|
||||
duration_minutes: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
image_url: z.string().nullable().optional(),
|
||||
transport_mode: z.string().nullable().optional(),
|
||||
google_place_id: z.string().nullable().optional(),
|
||||
website: z.string().nullable().optional(),
|
||||
phone: z.string().nullable().optional(),
|
||||
category: placeCategorySchema.optional(),
|
||||
tags: z.array(tagSchema.partial()).optional(),
|
||||
});
|
||||
export type AssignmentPlace = z.infer<typeof assignmentPlaceSchema>;
|
||||
|
||||
export const placeCreateRequestSchema = open.and(z.object({ name: z.string().min(1) }));
|
||||
export type PlaceCreateRequest = z.infer<typeof placeCreateRequestSchema>;
|
||||
|
||||
|
||||
@@ -15,6 +15,91 @@ import { z } from 'zod';
|
||||
|
||||
const open = z.record(z.string(), z.unknown());
|
||||
|
||||
/**
|
||||
* A reservation endpoint (flight/train leg terminal) — row of the
|
||||
* reservation_endpoints table (server/src/services/reservationService.ts).
|
||||
*/
|
||||
export const reservationEndpointSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
reservation_id: z.number().optional(),
|
||||
role: z.enum(['from', 'to', 'stop']),
|
||||
sequence: z.number(),
|
||||
name: z.string(),
|
||||
code: z.string().nullable(),
|
||||
lat: z.number(),
|
||||
lng: z.number(),
|
||||
timezone: z.string().nullable(),
|
||||
local_time: z.string().nullable(),
|
||||
local_date: z.string().nullable(),
|
||||
});
|
||||
export type ReservationEndpoint = z.infer<typeof reservationEndpointSchema>;
|
||||
|
||||
/**
|
||||
* Reservation entity as returned by the reservation list endpoint
|
||||
* (server/src/services/reservationService.ts -> listReservations). Columns of
|
||||
* the `reservations` table plus the joined day_number / place_name / linked
|
||||
* accommodation fields and the computed `day_positions` + `endpoints`.
|
||||
* `accommodation_id` is stored as TEXT in the DB.
|
||||
*/
|
||||
export const reservationSchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
day_id: z.number().nullable().optional(),
|
||||
end_day_id: z.number().nullable().optional(),
|
||||
place_id: z.number().nullable().optional(),
|
||||
assignment_id: z.number().nullable().optional(),
|
||||
title: z.string(),
|
||||
reservation_time: z.string().nullable().optional(),
|
||||
reservation_end_time: z.string().nullable().optional(),
|
||||
location: z.string().nullable().optional(),
|
||||
confirmation_number: z.string().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
status: z.string(),
|
||||
type: z.string(),
|
||||
accommodation_id: z.union([z.number(), z.string()]).nullable().optional(),
|
||||
metadata: z.string().nullable().optional(),
|
||||
needs_review: z.number().optional(),
|
||||
day_plan_position: z.number().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
// joined / computed in listReservations
|
||||
day_number: z.number().nullable().optional(),
|
||||
place_name: z.string().nullable().optional(),
|
||||
accommodation_place_id: z.number().nullable().optional(),
|
||||
accommodation_name: z.string().nullable().optional(),
|
||||
accommodation_start_day_id: z.number().nullable().optional(),
|
||||
accommodation_end_day_id: z.number().nullable().optional(),
|
||||
day_positions: z.record(z.string(), z.number()).nullable().optional(),
|
||||
endpoints: z.array(reservationEndpointSchema).optional(),
|
||||
});
|
||||
export type Reservation = z.infer<typeof reservationSchema>;
|
||||
|
||||
/**
|
||||
* Accommodation entity as returned by listAccommodations / getAccommodationWithPlace
|
||||
* (server/src/services/dayService.ts). Columns of the day_accommodations table
|
||||
* plus the joined place fields and (on list) the linked reservation_title.
|
||||
*/
|
||||
export const accommodationSchema = z.object({
|
||||
id: z.number(),
|
||||
trip_id: z.number(),
|
||||
place_id: z.number().nullable().optional(),
|
||||
start_day_id: z.number(),
|
||||
end_day_id: z.number(),
|
||||
check_in: z.string().nullable().optional(),
|
||||
check_in_end: z.string().nullable().optional(),
|
||||
check_out: z.string().nullable().optional(),
|
||||
confirmation: z.string().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
// joined in listAccommodations / getAccommodationWithPlace
|
||||
place_name: z.string().nullable().optional(),
|
||||
place_address: z.string().nullable().optional(),
|
||||
place_image: z.string().nullable().optional(),
|
||||
place_lat: z.number().nullable().optional(),
|
||||
place_lng: z.number().nullable().optional(),
|
||||
reservation_title: z.string().nullable().optional(),
|
||||
});
|
||||
export type Accommodation = z.infer<typeof accommodationSchema>;
|
||||
|
||||
/** Reservation create: title is required; the many optional fields stay open. */
|
||||
export const reservationCreateRequestSchema = open.and(z.object({ title: z.string().min(1) }));
|
||||
export type ReservationCreateRequest = z.infer<typeof reservationCreateRequestSchema>;
|
||||
|
||||
@@ -13,6 +13,51 @@ import { z } from 'zod';
|
||||
* permission checks + audit logging. Trip rows are wide, so responses stay open.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Trip entity as returned by the trip list / get / create / update endpoints
|
||||
* (server/src/services/tripService.ts -> TRIP_SELECT). Columns of the `trips`
|
||||
* table plus the computed list fields (day_count, place_count, is_owner as 0/1,
|
||||
* owner_username, shared_count). `is_archived` is the raw SQLite INTEGER.
|
||||
*/
|
||||
export const tripSchema = z.object({
|
||||
id: z.number(),
|
||||
user_id: z.number(),
|
||||
title: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
start_date: z.string().nullable().optional(),
|
||||
end_date: z.string().nullable().optional(),
|
||||
currency: z.string(),
|
||||
cover_image: z.string().nullable().optional(),
|
||||
is_archived: z.number(),
|
||||
reminder_days: z.number(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
// computed in TRIP_SELECT (list/get)
|
||||
day_count: z.number().optional(),
|
||||
place_count: z.number().optional(),
|
||||
is_owner: z.number().optional(),
|
||||
owner_username: z.string().optional(),
|
||||
shared_count: z.number().optional(),
|
||||
});
|
||||
export type Trip = z.infer<typeof tripSchema>;
|
||||
|
||||
/**
|
||||
* Trip member as returned by the members endpoint
|
||||
* (server/src/services/tripService.ts -> listMembers). Owner + collaborators
|
||||
* share this shape; `avatar_url` is resolved from the stored avatar.
|
||||
*/
|
||||
export const tripMemberSchema = z.object({
|
||||
id: z.number(),
|
||||
username: z.string(),
|
||||
email: z.string().optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
avatar_url: z.string().nullable().optional(),
|
||||
role: z.string().optional(),
|
||||
added_at: z.string().nullable().optional(),
|
||||
invited_by_username: z.string().nullable().optional(),
|
||||
});
|
||||
export type TripMember = z.infer<typeof tripMemberSchema>;
|
||||
|
||||
export const tripCreateRequestSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
description: z.string().nullable().optional(),
|
||||
|
||||
Reference in New Issue
Block a user