feat(contracts): vacay and journey routes declare their response schemas

The vacay plan, calendars, years, entries, stats and shares, and the
journey list, create, tracks, stats, share link and every success answer
get response schemas in shared and a @ResponseContract on their handler.
The holiday proxies pass the provider's answer through and are marked
exempt. The journey e2e stubs return whole rows, as the service does.
This commit is contained in:
Maurice
2026-10-10 15:54:15 +02:00
parent 8e01dbb92f
commit e21f2d97b6
7 changed files with 360 additions and 14 deletions
+72 -7
View File
@@ -115,7 +115,10 @@ export type JourneyPhotoUpdateRequest = z.infer<typeof journeyPhotoUpdateRequest
*/
export const journalPluginPhotoInputSchema = z.strictObject({
name: z.string().trim().min(1).max(255),
content_base64: z.string().min(1).max(14 * 1024 * 1024),
content_base64: z
.string()
.min(1)
.max(14 * 1024 * 1024),
caption: z.string().max(2000).optional(),
});
export type JournalPluginPhotoInput = z.infer<typeof journalPluginPhotoInputSchema>;
@@ -138,15 +141,19 @@ export type JourneyEntryUpdateRequest = z.infer<typeof journeyEntryUpdateRequest
* uploaded' for the empty case. Without this the pipe would answer
* 'body: Invalid input: expected object' first and the message would change.
*/
export const journeyEntryPhotoUploadRequestSchema = z.looseObject({
caption: z.unknown().optional(),
}).optional();
export const journeyEntryPhotoUploadRequestSchema = z
.looseObject({
caption: z.unknown().optional(),
})
.optional();
export type JourneyEntryPhotoUploadRequest = z.infer<typeof journeyEntryPhotoUploadRequestSchema>;
/** Multipart field alongside the uploaded video — absent body for the same reason. */
export const journeyGalleryVideoRequestSchema = z.looseObject({
duration_ms: z.unknown().optional(),
}).optional();
export const journeyGalleryVideoRequestSchema = z
.looseObject({
duration_ms: z.unknown().optional(),
})
.optional();
export type JourneyGalleryVideoRequest = z.infer<typeof journeyGalleryVideoRequestSchema>;
/** Free-form: per-user display preferences, forwarded whole. */
@@ -182,3 +189,61 @@ export const journeyTracksResponseSchema = z.object({
tracks: z.array(journeyTrackSchema),
});
export type JourneyTracksResponse = z.infer<typeof journeyTracksResponseSchema>;
// ── Responses ───────────────────────────────────────────────────────────────
/**
* A journeys row as the routes return it. Unlike most tables its times are epoch
* milliseconds, and its flags SQLite 0/1, as stored.
*/
export const journeySchema = z.object({
id: z.number(),
user_id: z.number(),
title: z.string(),
subtitle: z.string().nullable(),
cover_gradient: z.string().nullable(),
status: z.string().nullable(),
created_at: z.number(),
updated_at: z.number(),
cover_image: z.string().nullable(),
show_trip_tracks: z.number(),
show_verdict: z.number(),
show_mood: z.number(),
show_weather: z.number(),
status_override: z.string().nullable(),
photo_location: z.number(),
});
export type JourneyRecord = z.infer<typeof journeySchema>;
/** A journey on the dashboard, with its stat pills. */
export const journeyListItemSchema = journeySchema.extend({
entry_count: z.number(),
photo_count: z.number(),
place_count: z.number(),
trip_date_min: z.string().nullable(),
trip_date_max: z.string().nullable(),
});
export type JourneyListItem = z.infer<typeof journeyListItemSchema>;
/** GET /api/journeys */
export const journeyListResponseSchema = z.object({ journeys: z.array(journeyListItemSchema) });
export type JourneyListResponse = z.infer<typeof journeyListResponseSchema>;
/** A journey's public share link and what it shows. */
export const journeyShareLinkSchema = z.object({
token: z.string(),
created_at: z.string().nullable(),
share_timeline: z.boolean(),
share_gallery: z.boolean(),
share_map: z.boolean(),
newest_first: z.boolean(),
});
export type JourneyShareLink = z.infer<typeof journeyShareLinkSchema>;
/** GET :id/share-link: the link, or null while the journey is not published. */
export const journeyShareLinkResponseSchema = z.object({ link: journeyShareLinkSchema.nullable() });
export type JourneyShareLinkResponse = z.infer<typeof journeyShareLinkResponseSchema>;
/** POST :id/share-link: the token, and whether this call minted it. */
export const journeyShareLinkSetResponseSchema = z.object({ token: z.string(), created: z.boolean() });
export type JourneyShareLinkSetResponse = z.infer<typeof journeyShareLinkSetResponseSchema>;
+191 -1
View File
@@ -127,6 +127,196 @@ export const vacayShareUpdateRequestSchema = z.object({
});
export type VacayShareUpdateRequest = z.infer<typeof vacayShareUpdateRequestSchema>;
/** Plan / entries / stats payloads are wide and DB-derived; kept open. */
/**
* Plan / entries / stats payloads, kept open.
* @deprecated Use the typed response schemas below (vacayPlanResponseSchema, vacayEntriesResponseSchema, ...).
*/
export const vacayPlanDataSchema = open;
export type VacayPlanData = z.infer<typeof vacayPlanDataSchema>;
// ── Responses ───────────────────────────────────────────────────────────────
// The plan's own flags come back as booleans (the service converts them); the
// other rows keep SQLite's storage spelling, as the routes have always answered.
/** A holiday calendar on a plan (public or school holidays of one region). */
export const vacayHolidayCalendarSchema = z.object({
id: z.number(),
plan_id: z.number(),
type: z.enum(['public_holiday', 'school_holiday']),
region: z.string(),
label: z.string().nullable(),
color: z.string(),
sort_order: z.number(),
});
export type VacayHolidayCalendar = z.infer<typeof vacayHolidayCalendarSchema>;
/** A vacation plan with its settings and holiday calendars. */
export const vacayPlanSchema = z.object({
id: z.number(),
owner_id: z.number(),
block_weekends: z.boolean(),
holidays_enabled: z.boolean(),
holidays_region: z.string().nullable(),
school_holidays_enabled: z.boolean(),
company_holidays_enabled: z.boolean(),
carry_over_enabled: z.boolean(),
weekend_days: z.string().nullable(),
week_start: z.number(),
holiday_calendars: z.array(vacayHolidayCalendarSchema),
});
export type VacayPlan = z.infer<typeof vacayPlanSchema>;
/** A person on a plan, or one who could be invited to it. */
export const vacayUserSchema = z.object({ id: z.number(), username: z.string(), email: z.string() });
export type VacayUser = z.infer<typeof vacayUserSchema>;
/** GET /plan: the caller's active plan, its people and its open invitations. */
export const vacayPlanResponseSchema = z.object({
plan: vacayPlanSchema,
users: z.array(vacayUserSchema.extend({ color: z.string() })),
pendingInvites: z.array(
z.object({
id: z.number(),
user_id: z.number(),
username: z.string(),
email: z.string(),
created_at: z.string().nullable(),
}),
),
incomingInvites: z.array(
z.object({
id: z.number(),
plan_id: z.number(),
username: z.string(),
email: z.string(),
created_at: z.string().nullable(),
}),
),
isOwner: z.boolean(),
isFused: z.boolean(),
});
export type VacayPlanResponse = z.infer<typeof vacayPlanResponseSchema>;
/** PUT /plan: the plan after the write. */
export const vacayPlanUpdateResponseSchema = z.object({ plan: vacayPlanSchema });
export type VacayPlanUpdateResponse = z.infer<typeof vacayPlanUpdateResponseSchema>;
/** POST/PUT plan/holiday-calendars */
export const vacayHolidayCalendarResponseSchema = z.object({ calendar: vacayHolidayCalendarSchema });
export type VacayHolidayCalendarResponse = z.infer<typeof vacayHolidayCalendarResponseSchema>;
/** GET available-users: who could be invited to fuse with the plan. */
export const vacayAvailableUsersResponseSchema = z.object({ users: z.array(vacayUserSchema) });
export type VacayAvailableUsersResponse = z.infer<typeof vacayAvailableUsersResponseSchema>;
/** GET/POST /years, DELETE /years/:year: the plan's years after the call. */
export const vacayYearsResponseSchema = z.object({ years: z.array(z.number()) });
export type VacayYearsResponse = z.infer<typeof vacayYearsResponseSchema>;
/** A user's leave-year settings (#737). */
export const vacayYearSettingsSchema = z.object({
user_id: z.number(),
year_type: z.string(),
year_start_month: z.number(),
year_start_day: z.number(),
hire_date: z.string().nullable(),
});
export type VacayYearSettings = z.infer<typeof vacayYearSettingsSchema>;
/** GET/PUT year-settings */
export const vacayYearSettingsResponseSchema = z.object({ settings: vacayYearSettingsSchema });
export type VacayYearSettingsResponse = z.infer<typeof vacayYearSettingsResponseSchema>;
/** GET entries/:year: every entry in the viewer's leave-year window, and the company holidays in it. */
export const vacayEntriesResponseSchema = z.object({
entries: z.array(
z.object({
id: z.number(),
plan_id: z.number(),
user_id: z.number(),
date: z.string(),
note: z.string().nullable(),
fraction: z.number(),
kind: z.string().nullable(),
person_name: z.string(),
person_color: z.string(),
}),
),
companyHolidays: z.array(
z.object({
id: z.number(),
plan_id: z.number(),
date: z.string(),
note: z.string().nullable(),
fraction: z.number(),
}),
),
});
export type VacayEntriesResponse = z.infer<typeof vacayEntriesResponseSchema>;
/** POST entries/toggle: what the click did to the day. */
export const vacayToggleEntryResponseSchema = z.object({
action: z.enum(['added', 'updated', 'removed']),
fraction: z.number().optional(),
kind: z.string().optional(),
});
export type VacayToggleEntryResponse = z.infer<typeof vacayToggleEntryResponseSchema>;
/** POST entries/company-holiday: what the click did to the company holiday. */
export const vacayCompanyHolidayResponseSchema = z.object({
action: z.enum(['added', 'updated', 'removed']),
fraction: z.number().optional(),
});
export type VacayCompanyHolidayResponse = z.infer<typeof vacayCompanyHolidayResponseSchema>;
/** GET stats/:year: one row per person on the plan. */
export const vacayStatsResponseSchema = z.object({
stats: z.array(
z.object({
user_id: z.number(),
person_name: z.string(),
person_color: z.string(),
year: z.number(),
vacation_days: z.number().nullable(),
carried_over: z.number().nullable(),
total_available: z.number(),
used: z.number(),
remaining: z.number(),
comp_used: z.number(),
window_start: z.string(),
window_end: z.string(),
}),
),
});
export type VacayStatsResponse = z.infer<typeof vacayStatsResponseSchema>;
/** GET /shares: the caller's outgoing shares and the calendars shared with them. */
export const vacaySharesResponseSchema = z.object({
outgoing: z.array(z.object({ id: z.number(), user_id: z.number(), username: z.string() })),
incoming: z.array(
z.object({ id: z.number(), owner_id: z.number(), username: z.string(), color: z.string(), hidden: z.boolean() }),
),
});
export type VacaySharesResponse = z.infer<typeof vacaySharesResponseSchema>;
/** GET shares/available-users: who the caller could share with (usernames only). */
export const vacayShareAvailableUsersResponseSchema = z.object({
users: z.array(z.object({ id: z.number(), username: z.string() })),
});
export type VacayShareAvailableUsersResponse = z.infer<typeof vacayShareAvailableUsersResponseSchema>;
/** GET shares/calendars/:year: the calendars shared with the caller, over the caller's window. */
export const vacaySharedCalendarsResponseSchema = z.object({
calendars: z.array(
z.object({
share_id: z.number(),
owner_id: z.number(),
owner_name: z.string(),
color: z.string(),
hidden: z.boolean(),
entries: z.array(z.object({ date: z.string(), fraction: z.number(), kind: z.string().nullable() })),
companyHolidays: z.array(z.object({ date: z.string() })),
}),
),
});
export type VacaySharedCalendarsResponse = z.infer<typeof vacaySharedCalendarsResponseSchema>;