From fb36ae56786319d441fbd0c77901bf1b0f65b6d9 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 31 May 2026 20:22:54 +0200 Subject: [PATCH] Format the shared package and drop an unused import to satisfy the lint gate The i18n and schema changes added code that wasn't prettier-formatted, and place.schema.ts imported categorySchema without using it. Run prettier over shared and remove the import so 'npm run lint' + 'format:check' pass. --- shared/src/admin/admin.schema.spec.ts | 54 ++++-- shared/src/admin/admin.schema.ts | 16 +- shared/src/airport/airport.schema.spec.ts | 29 ++- .../src/assignment/assignment.schema.spec.ts | 30 +++- shared/src/assignment/assignment.schema.ts | 15 +- shared/src/atlas/atlas.schema.spec.ts | 41 ++++- shared/src/atlas/atlas.schema.ts | 8 +- shared/src/auth/auth.schema.spec.ts | 77 ++++++-- shared/src/backup/backup.schema.spec.ts | 20 ++- shared/src/backup/backup.schema.ts | 4 +- shared/src/budget/budget.schema.spec.ts | 40 ++++- shared/src/budget/budget.schema.ts | 24 ++- shared/src/category/category.schema.spec.ts | 24 ++- shared/src/collab/collab.schema.spec.ts | 64 +++++-- shared/src/collab/collab.schema.ts | 16 +- shared/src/day/day.schema.spec.ts | 35 +++- shared/src/day/day.schema.ts | 3 +- shared/src/file/file.schema.spec.ts | 26 ++- shared/src/i18n/es/dashboard.ts | 3 +- shared/src/i18n/fr/dashboard.ts | 11 +- shared/src/i18n/i18n-parity.spec.ts | 3 +- shared/src/i18n/it/dashboard.ts | 7 +- shared/src/i18n/nl/dashboard.ts | 4 +- shared/src/i18n/uk/dashboard.ts | 3 +- shared/src/journey/journey.schema.spec.ts | 73 ++++++-- shared/src/journey/journey.schema.ts | 16 +- shared/src/maps/maps.schema.spec.ts | 47 +++-- shared/src/maps/maps.schema.ts | 12 +- .../notification/notification.schema.spec.ts | 43 +++-- .../src/notification/notification.schema.ts | 12 +- shared/src/oauth/oauth.schema.spec.ts | 62 ++++++- shared/src/oauth/oauth.schema.ts | 4 +- shared/src/oidc/oidc.schema.spec.ts | 14 +- shared/src/packing/packing.schema.spec.ts | 37 +++- shared/src/packing/packing.schema.ts | 28 ++- shared/src/place/place.schema.spec.ts | 28 ++- shared/src/place/place.schema.ts | 16 +- .../reservation/reservation.schema.spec.ts | 39 ++++- shared/src/reservation/reservation.schema.ts | 28 ++- shared/src/sanitize/sanitize.spec.ts | 165 ++++++++++-------- shared/src/sanitize/sanitize.ts | 52 ++++-- shared/src/settings/settings.schema.spec.ts | 29 ++- shared/src/share/share.schema.spec.ts | 12 +- .../system-notice.schema.spec.ts | 84 ++++++--- shared/src/tag/tag.schema.spec.ts | 20 ++- shared/src/todo/todo.schema.spec.ts | 37 +++- shared/src/todo/todo.schema.ts | 4 +- shared/src/trip/trip.schema.spec.ts | 24 ++- shared/src/vacay/vacay.schema.spec.ts | 46 +++-- shared/src/vacay/vacay.schema.ts | 20 ++- 50 files changed, 1131 insertions(+), 378 deletions(-) diff --git a/shared/src/admin/admin.schema.spec.ts b/shared/src/admin/admin.schema.spec.ts index b5f0c398..72249943 100644 --- a/shared/src/admin/admin.schema.spec.ts +++ b/shared/src/admin/admin.schema.spec.ts @@ -1,33 +1,67 @@ +import { + adminUserCreateRequestSchema, + adminPermissionsRequestSchema, + adminInviteCreateRequestSchema, + adminFeatureToggleRequestSchema, +} from './admin.schema'; + import { describe, it, expect } from 'vitest'; -import { adminUserCreateRequestSchema, adminPermissionsRequestSchema, adminInviteCreateRequestSchema, adminFeatureToggleRequestSchema } from './admin.schema'; describe('adminUserCreateRequestSchema', () => { it('requires an email; role limited to user/admin', () => { - expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', password: 'p', role: 'admin' }).success).toBe(true); - expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true); - expect(adminUserCreateRequestSchema.safeParse({ password: 'p' }).success).toBe(false); - expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' }).success).toBe(false); + expect( + adminUserCreateRequestSchema.safeParse({ + email: 'a@b.c', + password: 'p', + role: 'admin', + }).success, + ).toBe(true); + expect( + adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success, + ).toBe(true); + expect( + adminUserCreateRequestSchema.safeParse({ password: 'p' }).success, + ).toBe(false); + expect( + adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' }) + .success, + ).toBe(false); }); }); describe('adminPermissionsRequestSchema', () => { it('requires a permissions record', () => { - expect(adminPermissionsRequestSchema.safeParse({ permissions: { trip_edit: { user: true } } }).success).toBe(true); + expect( + adminPermissionsRequestSchema.safeParse({ + permissions: { trip_edit: { user: true } }, + }).success, + ).toBe(true); expect(adminPermissionsRequestSchema.safeParse({}).success).toBe(false); }); }); describe('adminInviteCreateRequestSchema', () => { it('accepts optional uses/expiry/role', () => { - expect(adminInviteCreateRequestSchema.safeParse({ max_uses: 5, expires_in_days: 7 }).success).toBe(true); + expect( + adminInviteCreateRequestSchema.safeParse({ + max_uses: 5, + expires_in_days: 7, + }).success, + ).toBe(true); expect(adminInviteCreateRequestSchema.safeParse({}).success).toBe(true); - expect(adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success).toBe(false); + expect( + adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success, + ).toBe(false); }); }); describe('adminFeatureToggleRequestSchema', () => { it('requires a boolean enabled', () => { - expect(adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success).toBe(true); - expect(adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false); + expect( + adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success, + ).toBe(true); + expect( + adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success, + ).toBe(false); }); }); diff --git a/shared/src/admin/admin.schema.ts b/shared/src/admin/admin.schema.ts index e5d5efe2..59bb2da1 100644 --- a/shared/src/admin/admin.schema.ts +++ b/shared/src/admin/admin.schema.ts @@ -14,21 +14,29 @@ export const adminUserCreateRequestSchema = z.object({ username: z.string().optional(), role: z.enum(['user', 'admin']).optional(), }); -export type AdminUserCreateRequest = z.infer; +export type AdminUserCreateRequest = z.infer< + typeof adminUserCreateRequestSchema +>; export const adminPermissionsRequestSchema = z.object({ permissions: z.record(z.string(), z.unknown()), }); -export type AdminPermissionsRequest = z.infer; +export type AdminPermissionsRequest = z.infer< + typeof adminPermissionsRequestSchema +>; export const adminInviteCreateRequestSchema = z.object({ max_uses: z.number().optional(), expires_in_days: z.number().optional(), role: z.enum(['user', 'admin']).optional(), }); -export type AdminInviteCreateRequest = z.infer; +export type AdminInviteCreateRequest = z.infer< + typeof adminInviteCreateRequestSchema +>; export const adminFeatureToggleRequestSchema = z.object({ enabled: z.boolean(), }); -export type AdminFeatureToggleRequest = z.infer; +export type AdminFeatureToggleRequest = z.infer< + typeof adminFeatureToggleRequestSchema +>; diff --git a/shared/src/airport/airport.schema.spec.ts b/shared/src/airport/airport.schema.spec.ts index aea21133..9661ef5a 100644 --- a/shared/src/airport/airport.schema.spec.ts +++ b/shared/src/airport/airport.schema.spec.ts @@ -1,20 +1,35 @@ -import { describe, it, expect } from 'vitest'; import { airportSchema, airportSearchQuerySchema } from './airport.schema'; +import { describe, it, expect } from 'vitest'; + describe('airportSchema', () => { it('accepts a full airport record', () => { const parsed = airportSchema.parse({ - iata: 'BER', icao: 'EDDB', name: 'Berlin Brandenburg', city: 'Berlin', - country: 'DE', lat: 52.36, lng: 13.5, tz: 'Europe/Berlin', + iata: 'BER', + icao: 'EDDB', + name: 'Berlin Brandenburg', + city: 'Berlin', + country: 'DE', + lat: 52.36, + lng: 13.5, + tz: 'Europe/Berlin', }); expect(parsed.iata).toBe('BER'); }); it('allows a null icao (smaller fields can be missing one)', () => { - expect(airportSchema.safeParse({ - iata: 'XXX', icao: null, name: 'Test', city: 'Test', country: 'DE', - lat: 0, lng: 0, tz: 'UTC', - }).success).toBe(true); + expect( + airportSchema.safeParse({ + iata: 'XXX', + icao: null, + name: 'Test', + city: 'Test', + country: 'DE', + lat: 0, + lng: 0, + tz: 'UTC', + }).success, + ).toBe(true); }); }); diff --git a/shared/src/assignment/assignment.schema.spec.ts b/shared/src/assignment/assignment.schema.spec.ts index adb3519c..aefcb948 100644 --- a/shared/src/assignment/assignment.schema.spec.ts +++ b/shared/src/assignment/assignment.schema.spec.ts @@ -1,29 +1,45 @@ -import { describe, it, expect } from 'vitest'; import { assignmentCreateRequestSchema, assignmentMoveRequestSchema, assignmentParticipantsRequestSchema, } from './assignment.schema'; +import { describe, it, expect } from 'vitest'; + describe('assignmentCreateRequestSchema', () => { it('requires a place_id; notes optional/nullable', () => { - expect(assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(true); - expect(assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null }).success).toBe(true); + expect( + assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success, + ).toBe(true); + expect( + assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null }) + .success, + ).toBe(true); expect(assignmentCreateRequestSchema.safeParse({}).success).toBe(false); }); }); describe('assignmentMoveRequestSchema', () => { it('requires new_day_id; order_index optional', () => { - expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success).toBe(true); - expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 }).success).toBe(true); + expect( + assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success, + ).toBe(true); + expect( + assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 }) + .success, + ).toBe(true); expect(assignmentMoveRequestSchema.safeParse({}).success).toBe(false); }); }); describe('assignmentParticipantsRequestSchema', () => { it('requires a numeric user_ids array', () => { - expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true); - expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false); + expect( + assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }) + .success, + ).toBe(true); + expect( + assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success, + ).toBe(false); }); }); diff --git a/shared/src/assignment/assignment.schema.ts b/shared/src/assignment/assignment.schema.ts index 6c6ac82b..4c5624f6 100644 --- a/shared/src/assignment/assignment.schema.ts +++ b/shared/src/assignment/assignment.schema.ts @@ -1,6 +1,7 @@ -import { z } from 'zod'; import { assignmentPlaceSchema } from '../place/place.schema'; +import { z } from 'zod'; + /** * Assignment API contract — single source of truth for the place↔day itinerary * endpoints under /api/trips/:tripId/days/:dayId/assignments and @@ -48,12 +49,16 @@ export const assignmentCreateRequestSchema = z.object({ place_id: z.union([z.number(), z.string()]), notes: z.string().nullable().optional(), }); -export type AssignmentCreateRequest = z.infer; +export type AssignmentCreateRequest = z.infer< + typeof assignmentCreateRequestSchema +>; export const assignmentReorderRequestSchema = z.object({ orderedIds: z.array(z.number()), }); -export type AssignmentReorderRequest = z.infer; +export type AssignmentReorderRequest = z.infer< + typeof assignmentReorderRequestSchema +>; export const assignmentMoveRequestSchema = z.object({ new_day_id: z.union([z.number(), z.string()]), @@ -70,4 +75,6 @@ export type AssignmentTimeRequest = z.infer; export const assignmentParticipantsRequestSchema = z.object({ user_ids: z.array(z.number()), }); -export type AssignmentParticipantsRequest = z.infer; +export type AssignmentParticipantsRequest = z.infer< + typeof assignmentParticipantsRequestSchema +>; diff --git a/shared/src/atlas/atlas.schema.spec.ts b/shared/src/atlas/atlas.schema.spec.ts index e7ca010a..bf23e243 100644 --- a/shared/src/atlas/atlas.schema.spec.ts +++ b/shared/src/atlas/atlas.schema.spec.ts @@ -1,29 +1,54 @@ -import { describe, it, expect } from 'vitest'; import { markRegionRequestSchema, createBucketItemRequestSchema, regionGeoSchema, } from './atlas.schema'; +import { describe, it, expect } from 'vitest'; + describe('markRegionRequestSchema', () => { it('requires both name and country_code', () => { - expect(markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }).success).toBe(true); - expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(false); + expect( + markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }) + .success, + ).toBe(true); + expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe( + false, + ); }); }); describe('createBucketItemRequestSchema', () => { it('requires a name; coordinates and metadata optional/nullable', () => { - expect(createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success).toBe(true); - expect(createBucketItemRequestSchema.safeParse({ name: 'Tokyo', lat: 35, lng: 139, country_code: null }).success).toBe(true); + expect( + createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success, + ).toBe(true); + expect( + createBucketItemRequestSchema.safeParse({ + name: 'Tokyo', + lat: 35, + lng: 139, + country_code: null, + }).success, + ).toBe(true); expect(createBucketItemRequestSchema.safeParse({}).success).toBe(false); }); }); describe('regionGeoSchema', () => { it('accepts a FeatureCollection with opaque features', () => { - expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] }).success).toBe(true); - expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [{ anything: true }] }).success).toBe(true); - expect(regionGeoSchema.safeParse({ type: 'Other', features: [] }).success).toBe(false); + expect( + regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] }) + .success, + ).toBe(true); + expect( + regionGeoSchema.safeParse({ + type: 'FeatureCollection', + features: [{ anything: true }], + }).success, + ).toBe(true); + expect( + regionGeoSchema.safeParse({ type: 'Other', features: [] }).success, + ).toBe(false); }); }); diff --git a/shared/src/atlas/atlas.schema.ts b/shared/src/atlas/atlas.schema.ts index 6e6e39bf..992c92fd 100644 --- a/shared/src/atlas/atlas.schema.ts +++ b/shared/src/atlas/atlas.schema.ts @@ -29,7 +29,9 @@ export const createBucketItemRequestSchema = z.object({ notes: z.string().nullable().optional(), target_date: z.string().nullable().optional(), }); -export type CreateBucketItemRequest = z.infer; +export type CreateBucketItemRequest = z.infer< + typeof createBucketItemRequestSchema +>; export const updateBucketItemRequestSchema = z.object({ name: z.string().optional(), @@ -39,7 +41,9 @@ export const updateBucketItemRequestSchema = z.object({ country_code: z.string().nullable().optional(), target_date: z.string().nullable().optional(), }); -export type UpdateBucketItemRequest = z.infer; +export type UpdateBucketItemRequest = z.infer< + typeof updateBucketItemRequestSchema +>; /** A bucket-list item row (DB-shaped; kept open). */ export const bucketItemSchema = open; diff --git a/shared/src/auth/auth.schema.spec.ts b/shared/src/auth/auth.schema.spec.ts index 959d2537..29fdb5aa 100644 --- a/shared/src/auth/auth.schema.spec.ts +++ b/shared/src/auth/auth.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { registerRequestSchema, loginRequestSchema, @@ -10,38 +9,84 @@ import { mcpTokenCreateRequestSchema, } from './auth.schema'; +import { describe, it, expect } from 'vitest'; + describe('registerRequestSchema', () => { it('requires email + password; username/invite optional', () => { - expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true); - expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw', invite_token: 't' }).success).toBe(true); - expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false); + expect( + registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }) + .success, + ).toBe(true); + expect( + registerRequestSchema.safeParse({ + email: 'a@b.c', + password: 'pw', + invite_token: 't', + }).success, + ).toBe(true); + expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe( + false, + ); }); }); describe('loginRequestSchema', () => { it('requires email + password', () => { - expect(loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true); - expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false); + expect( + loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success, + ).toBe(true); + expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe( + false, + ); }); }); describe('forgot/reset/change password schemas', () => { it('validate their required fields', () => { - expect(forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true); - expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }).success).toBe(true); - expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw', mfa_code: '123456' }).success).toBe(true); - expect(resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success).toBe(false); - expect(changePasswordRequestSchema.safeParse({ current_password: 'a', new_password: 'b' }).success).toBe(true); - expect(changePasswordRequestSchema.safeParse({ new_password: 'b' }).success).toBe(false); + expect( + forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success, + ).toBe(true); + expect( + resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }) + .success, + ).toBe(true); + expect( + resetPasswordRequestSchema.safeParse({ + token: 't', + new_password: 'pw', + mfa_code: '123456', + }).success, + ).toBe(true); + expect( + resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success, + ).toBe(false); + expect( + changePasswordRequestSchema.safeParse({ + current_password: 'a', + new_password: 'b', + }).success, + ).toBe(true); + expect( + changePasswordRequestSchema.safeParse({ new_password: 'b' }).success, + ).toBe(false); }); }); describe('mfa + mcp-token schemas', () => { it('validate their fields', () => { - expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }).success).toBe(true); - expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't' }).success).toBe(false); - expect(mfaEnableRequestSchema.safeParse({ code: '123456' }).success).toBe(true); - expect(mcpTokenCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(true); + expect( + mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }) + .success, + ).toBe(true); + expect( + mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't' }).success, + ).toBe(false); + expect(mfaEnableRequestSchema.safeParse({ code: '123456' }).success).toBe( + true, + ); + expect(mcpTokenCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe( + true, + ); expect(mcpTokenCreateRequestSchema.safeParse({}).success).toBe(true); }); }); diff --git a/shared/src/backup/backup.schema.spec.ts b/shared/src/backup/backup.schema.spec.ts index 3ae7f210..6779ea98 100644 --- a/shared/src/backup/backup.schema.spec.ts +++ b/shared/src/backup/backup.schema.spec.ts @@ -1,14 +1,26 @@ -import { describe, it, expect } from 'vitest'; import { autoBackupSettingsRequestSchema } from './backup.schema'; +import { describe, it, expect } from 'vitest'; + describe('autoBackupSettingsRequestSchema', () => { it('accepts the known toggles and stays permissive for extras', () => { - expect(autoBackupSettingsRequestSchema.safeParse({ enabled: true, interval: 'daily', keep_days: 7 }).success).toBe(true); - expect(autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' }).success).toBe(true); + expect( + autoBackupSettingsRequestSchema.safeParse({ + enabled: true, + interval: 'daily', + keep_days: 7, + }).success, + ).toBe(true); + expect( + autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' }) + .success, + ).toBe(true); expect(autoBackupSettingsRequestSchema.safeParse({}).success).toBe(true); }); it('rejects a non-boolean enabled', () => { - expect(autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false); + expect( + autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success, + ).toBe(false); }); }); diff --git a/shared/src/backup/backup.schema.ts b/shared/src/backup/backup.schema.ts index d9e16e89..e9996538 100644 --- a/shared/src/backup/backup.schema.ts +++ b/shared/src/backup/backup.schema.ts @@ -16,4 +16,6 @@ export const autoBackupSettingsRequestSchema = z time: z.string().optional(), }) .passthrough(); -export type AutoBackupSettingsRequest = z.infer; +export type AutoBackupSettingsRequest = z.infer< + typeof autoBackupSettingsRequestSchema +>; diff --git a/shared/src/budget/budget.schema.spec.ts b/shared/src/budget/budget.schema.spec.ts index 59c59c45..93e73140 100644 --- a/shared/src/budget/budget.schema.spec.ts +++ b/shared/src/budget/budget.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { budgetCreateItemRequestSchema, budgetUpdateMembersRequestSchema, @@ -6,31 +5,54 @@ import { budgetReorderItemsRequestSchema, } from './budget.schema'; +import { describe, it, expect } from 'vitest'; + describe('budgetCreateItemRequestSchema', () => { it('requires a name; money/meta fields optional + nullable', () => { - expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success).toBe(true); - expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel', total_price: 200, persons: null }).success).toBe(true); + expect( + budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success, + ).toBe(true); + expect( + budgetCreateItemRequestSchema.safeParse({ + name: 'Hotel', + total_price: 200, + persons: null, + }).success, + ).toBe(true); expect(budgetCreateItemRequestSchema.safeParse({}).success).toBe(false); }); }); describe('budgetUpdateMembersRequestSchema', () => { it('requires a numeric user_ids array', () => { - expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true); - expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false); + expect( + budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success, + ).toBe(true); + expect( + budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success, + ).toBe(false); }); }); describe('budgetToggleMemberPaidRequestSchema', () => { it('requires a boolean paid', () => { - expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success).toBe(true); - expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success).toBe(false); + expect( + budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success, + ).toBe(true); + expect( + budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success, + ).toBe(false); }); }); describe('budgetReorderItemsRequestSchema', () => { it('requires numeric ids', () => { - expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true); - expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false); + expect( + budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }) + .success, + ).toBe(true); + expect( + budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success, + ).toBe(false); }); }); diff --git a/shared/src/budget/budget.schema.ts b/shared/src/budget/budget.schema.ts index a6c0bc67..b12b1208 100644 --- a/shared/src/budget/budget.schema.ts +++ b/shared/src/budget/budget.schema.ts @@ -59,7 +59,9 @@ export const budgetCreateItemRequestSchema = z.object({ note: z.string().nullable().optional(), expense_date: z.string().nullable().optional(), }); -export type BudgetCreateItemRequest = z.infer; +export type BudgetCreateItemRequest = z.infer< + typeof budgetCreateItemRequestSchema +>; /** Update accepts the same fields plus total_price changes; all optional. */ export const budgetUpdateItemRequestSchema = z.object({ @@ -71,24 +73,34 @@ export const budgetUpdateItemRequestSchema = z.object({ note: z.string().nullable().optional(), expense_date: z.string().nullable().optional(), }); -export type BudgetUpdateItemRequest = z.infer; +export type BudgetUpdateItemRequest = z.infer< + typeof budgetUpdateItemRequestSchema +>; export const budgetUpdateMembersRequestSchema = z.object({ user_ids: z.array(z.number()), }); -export type BudgetUpdateMembersRequest = z.infer; +export type BudgetUpdateMembersRequest = z.infer< + typeof budgetUpdateMembersRequestSchema +>; export const budgetToggleMemberPaidRequestSchema = z.object({ paid: z.boolean(), }); -export type BudgetToggleMemberPaidRequest = z.infer; +export type BudgetToggleMemberPaidRequest = z.infer< + typeof budgetToggleMemberPaidRequestSchema +>; export const budgetReorderItemsRequestSchema = z.object({ orderedIds: z.array(z.number()), }); -export type BudgetReorderItemsRequest = z.infer; +export type BudgetReorderItemsRequest = z.infer< + typeof budgetReorderItemsRequestSchema +>; export const budgetReorderCategoriesRequestSchema = z.object({ orderedCategories: z.array(z.string()), }); -export type BudgetReorderCategoriesRequest = z.infer; +export type BudgetReorderCategoriesRequest = z.infer< + typeof budgetReorderCategoriesRequestSchema +>; diff --git a/shared/src/category/category.schema.spec.ts b/shared/src/category/category.schema.spec.ts index de52fe56..8d7e8caa 100644 --- a/shared/src/category/category.schema.spec.ts +++ b/shared/src/category/category.schema.spec.ts @@ -1,20 +1,32 @@ -import { describe, it, expect } from 'vitest'; import { categorySchema, createCategoryRequestSchema, updateCategoryRequestSchema, } from './category.schema'; +import { describe, it, expect } from 'vitest'; + describe('categorySchema', () => { it('accepts a full category', () => { - expect(categorySchema.safeParse({ id: 1, name: 'Food', color: '#fff', icon: '🍔' }).success).toBe(true); + expect( + categorySchema.safeParse({ + id: 1, + name: 'Food', + color: '#fff', + icon: '🍔', + }).success, + ).toBe(true); }); }); describe('createCategoryRequestSchema', () => { it('requires a non-empty name; colour and icon are optional', () => { - expect(createCategoryRequestSchema.safeParse({ name: 'Food' }).success).toBe(true); - expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(false); + expect( + createCategoryRequestSchema.safeParse({ name: 'Food' }).success, + ).toBe(true); + expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe( + false, + ); expect(createCategoryRequestSchema.safeParse({}).success).toBe(false); }); }); @@ -22,6 +34,8 @@ describe('createCategoryRequestSchema', () => { describe('updateCategoryRequestSchema', () => { it('allows every field to be omitted (the service COALESCEs)', () => { expect(updateCategoryRequestSchema.safeParse({}).success).toBe(true); - expect(updateCategoryRequestSchema.safeParse({ color: '#000' }).success).toBe(true); + expect( + updateCategoryRequestSchema.safeParse({ color: '#000' }).success, + ).toBe(true); }); }); diff --git a/shared/src/collab/collab.schema.spec.ts b/shared/src/collab/collab.schema.spec.ts index bba9872f..e0f401bf 100644 --- a/shared/src/collab/collab.schema.spec.ts +++ b/shared/src/collab/collab.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { collabNoteCreateRequestSchema, collabPollCreateRequestSchema, @@ -7,41 +6,78 @@ import { collabReactionRequestSchema, } from './collab.schema'; +import { describe, it, expect } from 'vitest'; + describe('collabNoteCreateRequestSchema', () => { it('requires a non-empty title; the rest is optional', () => { - expect(collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success).toBe(true); - expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(false); + expect( + collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success, + ).toBe(true); + expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe( + false, + ); expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false); }); }); describe('collabPollCreateRequestSchema', () => { it('requires a question and at least two options', () => { - expect(collabPollCreateRequestSchema.safeParse({ question: 'Where?', options: ['A', 'B'] }).success).toBe(true); - expect(collabPollCreateRequestSchema.safeParse({ question: 'Where?', options: ['A'] }).success).toBe(false); - expect(collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success).toBe(false); + expect( + collabPollCreateRequestSchema.safeParse({ + question: 'Where?', + options: ['A', 'B'], + }).success, + ).toBe(true); + expect( + collabPollCreateRequestSchema.safeParse({ + question: 'Where?', + options: ['A'], + }).success, + ).toBe(false); + expect( + collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success, + ).toBe(false); }); }); describe('collabPollVoteRequestSchema', () => { it('requires a numeric option_index', () => { - expect(collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success).toBe(true); - expect(collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success).toBe(false); + expect( + collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success, + ).toBe(true); + expect( + collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success, + ).toBe(false); }); }); describe('collabMessageCreateRequestSchema', () => { it('requires text, caps it at 5000, allows a nullable reply_to', () => { - expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }).success).toBe(true); - expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 }).success).toBe(true); - expect(collabMessageCreateRequestSchema.safeParse({ text: '' }).success).toBe(false); - expect(collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) }).success).toBe(false); + expect( + collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }) + .success, + ).toBe(true); + expect( + collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 }) + .success, + ).toBe(true); + expect( + collabMessageCreateRequestSchema.safeParse({ text: '' }).success, + ).toBe(false); + expect( + collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) }) + .success, + ).toBe(false); }); }); describe('collabReactionRequestSchema', () => { it('requires a non-empty emoji', () => { - expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(true); - expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(false); + expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe( + true, + ); + expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe( + false, + ); }); }); diff --git a/shared/src/collab/collab.schema.ts b/shared/src/collab/collab.schema.ts index 38ef7a94..93badd80 100644 --- a/shared/src/collab/collab.schema.ts +++ b/shared/src/collab/collab.schema.ts @@ -18,7 +18,9 @@ export const collabNoteCreateRequestSchema = z.object({ color: z.string().optional(), website: z.string().optional(), }); -export type CollabNoteCreateRequest = z.infer; +export type CollabNoteCreateRequest = z.infer< + typeof collabNoteCreateRequestSchema +>; export const collabNoteUpdateRequestSchema = z.object({ title: z.string().optional(), @@ -28,7 +30,9 @@ export const collabNoteUpdateRequestSchema = z.object({ pinned: z.union([z.boolean(), z.number()]).optional(), website: z.string().optional(), }); -export type CollabNoteUpdateRequest = z.infer; +export type CollabNoteUpdateRequest = z.infer< + typeof collabNoteUpdateRequestSchema +>; export const collabPollCreateRequestSchema = z.object({ question: z.string().min(1), @@ -37,7 +41,9 @@ export const collabPollCreateRequestSchema = z.object({ multiple_choice: z.boolean().optional(), deadline: z.string().optional(), }); -export type CollabPollCreateRequest = z.infer; +export type CollabPollCreateRequest = z.infer< + typeof collabPollCreateRequestSchema +>; export const collabPollVoteRequestSchema = z.object({ option_index: z.number(), @@ -48,7 +54,9 @@ export const collabMessageCreateRequestSchema = z.object({ text: z.string().min(1).max(5000), reply_to: z.number().nullable().optional(), }); -export type CollabMessageCreateRequest = z.infer; +export type CollabMessageCreateRequest = z.infer< + typeof collabMessageCreateRequestSchema +>; export const collabReactionRequestSchema = z.object({ emoji: z.string().min(1), diff --git a/shared/src/day/day.schema.spec.ts b/shared/src/day/day.schema.spec.ts index 65e535ca..b25b226a 100644 --- a/shared/src/day/day.schema.spec.ts +++ b/shared/src/day/day.schema.spec.ts @@ -1,30 +1,49 @@ -import { describe, it, expect } from 'vitest'; import { dayCreateRequestSchema, dayNoteCreateRequestSchema, dayNoteUpdateRequestSchema, } from './day.schema'; +import { describe, it, expect } from 'vitest'; + describe('dayCreateRequestSchema', () => { it('accepts an optional date + notes', () => { expect(dayCreateRequestSchema.safeParse({}).success).toBe(true); - expect(dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' }).success).toBe(true); + expect( + dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' }) + .success, + ).toBe(true); }); }); describe('dayNoteCreateRequestSchema', () => { it('requires non-empty text capped at 500, time capped at 150', () => { - expect(dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success).toBe(true); - expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(false); - expect(dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false); - expect(dayNoteCreateRequestSchema.safeParse({ text: 'ok', time: 'y'.repeat(151) }).success).toBe(false); + expect( + dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success, + ).toBe(true); + expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe( + false, + ); + expect( + dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success, + ).toBe(false); + expect( + dayNoteCreateRequestSchema.safeParse({ + text: 'ok', + time: 'y'.repeat(151), + }).success, + ).toBe(false); }); }); describe('dayNoteUpdateRequestSchema', () => { it('allows omitting text and caps the lengths', () => { expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true); - expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(true); - expect(dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false); + expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe( + true, + ); + expect( + dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success, + ).toBe(false); }); }); diff --git a/shared/src/day/day.schema.ts b/shared/src/day/day.schema.ts index fafc271a..2b4ab749 100644 --- a/shared/src/day/day.schema.ts +++ b/shared/src/day/day.schema.ts @@ -1,6 +1,7 @@ -import { z } from 'zod'; import { assignmentSchema } from '../assignment/assignment.schema'; +import { z } from 'zod'; + /** * Day + day-note API contract — single source of truth for the * /api/trips/:tripId/days and /api/trips/:tripId/days/:dayId/notes endpoints. diff --git a/shared/src/file/file.schema.spec.ts b/shared/src/file/file.schema.spec.ts index 3c4e36cd..25a3ee3b 100644 --- a/shared/src/file/file.schema.spec.ts +++ b/shared/src/file/file.schema.spec.ts @@ -1,18 +1,34 @@ +import { + fileUpdateRequestSchema, + fileLinkRequestSchema, + photoVariantSchema, +} from './file.schema'; + import { describe, it, expect } from 'vitest'; -import { fileUpdateRequestSchema, fileLinkRequestSchema, photoVariantSchema } from './file.schema'; describe('fileUpdateRequestSchema', () => { it('accepts optional metadata, nullable ids, an empty body', () => { - expect(fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 }).success).toBe(true); - expect(fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' }).success).toBe(true); + expect( + fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 }) + .success, + ).toBe(true); + expect( + fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' }) + .success, + ).toBe(true); expect(fileUpdateRequestSchema.safeParse({}).success).toBe(true); }); }); describe('fileLinkRequestSchema', () => { it('accepts any subset of reservation/assignment/place ids', () => { - expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(true); - expect(fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null }).success).toBe(true); + expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe( + true, + ); + expect( + fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null }) + .success, + ).toBe(true); expect(fileLinkRequestSchema.safeParse({}).success).toBe(true); }); }); diff --git a/shared/src/i18n/es/dashboard.ts b/shared/src/i18n/es/dashboard.ts index a2345cd9..d1f53d80 100644 --- a/shared/src/i18n/es/dashboard.ts +++ b/shared/src/i18n/es/dashboard.ts @@ -146,7 +146,8 @@ const dashboard: TranslationStrings = { 'dashboard.confirm.copy.willCopy': 'Se copiará', 'dashboard.confirm.copy.will1': 'Días, lugares y asignaciones por día', 'dashboard.confirm.copy.will2': 'Alojamientos y reservas', - 'dashboard.confirm.copy.will3': 'Partidas de presupuesto y orden de categorías', + 'dashboard.confirm.copy.will3': + 'Partidas de presupuesto y orden de categorías', 'dashboard.confirm.copy.will4': 'Listas de equipaje (sin marcar)', 'dashboard.confirm.copy.will5': 'Tareas (sin asignar ni marcar)', 'dashboard.confirm.copy.will6': 'Notas del día', diff --git a/shared/src/i18n/fr/dashboard.ts b/shared/src/i18n/fr/dashboard.ts index bf431647..42e39430 100644 --- a/shared/src/i18n/fr/dashboard.ts +++ b/shared/src/i18n/fr/dashboard.ts @@ -109,7 +109,7 @@ const dashboard: TranslationStrings = { 'dashboard.mobile.currencyConverter': 'Convertisseur de devises', 'dashboard.filter.planned': 'Planifiés', 'dashboard.hero.badgeLive': 'EN DIRECT', - 'dashboard.hero.badgeToday': 'DÉBUTE AUJOURD\'HUI', + 'dashboard.hero.badgeToday': "DÉBUTE AUJOURD'HUI", 'dashboard.hero.badgeTomorrow': 'DEMAIN', 'dashboard.hero.badgeNext': 'À SUIVRE', 'dashboard.hero.badgeRecent': 'RÉCENT', @@ -135,16 +135,17 @@ const dashboard: TranslationStrings = { 'dashboard.atlas.acrossAllTrips': 'sur tous les voyages', 'dashboard.atlas.distanceFlown': 'Distance parcourue en avion', 'dashboard.atlas.kmUnit': 'km', - 'dashboard.atlas.aroundEquator': '≈ {count}× le tour de l\'équateur', + 'dashboard.atlas.aroundEquator': "≈ {count}× le tour de l'équateur", 'dashboard.card.idea': 'Idée', 'dashboard.card.buddyOne': 'Compagnon', 'dashboard.fx.from': 'De', 'dashboard.fx.to': 'Vers', 'dashboard.fx.unavailable': 'Taux indisponible', 'dashboard.tz.searchPlaceholder': 'Rechercher un fuseau horaire…', - 'dashboard.tz.empty': 'Pas encore d\'autres fuseaux horaires — ajoutez-en un avec +', + 'dashboard.tz.empty': + "Pas encore d'autres fuseaux horaires — ajoutez-en un avec +", 'dashboard.upcoming.title': 'Prochaines réservations', - 'dashboard.upcoming.empty': 'Rien de réservé pour l\'instant.', + 'dashboard.upcoming.empty': "Rien de réservé pour l'instant.", 'dashboard.confirm.copy.title': 'Copier ce voyage ?', 'dashboard.confirm.copy.willCopy': 'Sera copié', 'dashboard.confirm.copy.will1': 'Jours, lieux et affectations par jour', @@ -159,7 +160,7 @@ const dashboard: TranslationStrings = { 'dashboard.confirm.copy.wont3': 'Fichiers et photos', 'dashboard.confirm.copy.wont4': 'Jetons de partage', 'dashboard.confirm.copy.confirm': 'Copier le voyage', - 'dashboard.aria.toggleView': 'Changer d\'affichage', + 'dashboard.aria.toggleView': "Changer d'affichage", 'dashboard.aria.filter': 'Filtrer', 'dashboard.aria.duplicate': 'Dupliquer', 'dashboard.aria.refreshRates': 'Actualiser les taux', diff --git a/shared/src/i18n/i18n-parity.spec.ts b/shared/src/i18n/i18n-parity.spec.ts index 701e8e73..070fa82e 100644 --- a/shared/src/i18n/i18n-parity.spec.ts +++ b/shared/src/i18n/i18n-parity.spec.ts @@ -1,7 +1,8 @@ -import { describe, it, expect } from 'vitest'; // @ts-expect-error — plain .mjs script with no .d.ts; import as JS module. import { checkParity } from '../../scripts/i18n-parity.mjs'; +import { describe, it, expect } from 'vitest'; + /** * Enforces the file-set contract for the i18n migration: every non-en locale * dir must contain the exact same domain files as en/. diff --git a/shared/src/i18n/it/dashboard.ts b/shared/src/i18n/it/dashboard.ts index e0d0537f..3a704c2f 100644 --- a/shared/src/i18n/it/dashboard.ts +++ b/shared/src/i18n/it/dashboard.ts @@ -124,7 +124,7 @@ const dashboard: TranslationStrings = { 'dashboard.hero.dayLeft': 'Giorno rimasto', 'dashboard.hero.daysLeft': 'Giorni rimasti', 'dashboard.hero.lastDay': 'Ultimo giorno', - 'dashboard.hero.untilStart': 'All\'inizio', + 'dashboard.hero.untilStart': "All'inizio", 'dashboard.hero.startsIn': 'Si parte tra', 'dashboard.atlas.countriesVisited': 'Atlas · Paesi visitati', 'dashboard.atlas.ofTotal': 'di {total}', @@ -135,14 +135,15 @@ const dashboard: TranslationStrings = { 'dashboard.atlas.acrossAllTrips': 'su tutti i viaggi', 'dashboard.atlas.distanceFlown': 'Distanza in volo', 'dashboard.atlas.kmUnit': 'km', - 'dashboard.atlas.aroundEquator': '≈ {count}× intorno all\'equatore', + 'dashboard.atlas.aroundEquator': "≈ {count}× intorno all'equatore", 'dashboard.card.idea': 'Idea', 'dashboard.card.buddyOne': 'Compagno', 'dashboard.fx.from': 'Da', 'dashboard.fx.to': 'A', 'dashboard.fx.unavailable': 'Tasso non disponibile', 'dashboard.tz.searchPlaceholder': 'Cerca fuso orario…', - 'dashboard.tz.empty': 'Ancora nessun altro fuso orario — aggiungine uno con +', + 'dashboard.tz.empty': + 'Ancora nessun altro fuso orario — aggiungine uno con +', 'dashboard.upcoming.title': 'Prossime prenotazioni', 'dashboard.upcoming.empty': 'Niente ancora prenotato.', 'dashboard.confirm.copy.title': 'Copiare questo viaggio?', diff --git a/shared/src/i18n/nl/dashboard.ts b/shared/src/i18n/nl/dashboard.ts index 0034e1f4..9f3c61d6 100644 --- a/shared/src/i18n/nl/dashboard.ts +++ b/shared/src/i18n/nl/dashboard.ts @@ -153,14 +153,14 @@ const dashboard: TranslationStrings = { 'dashboard.confirm.copy.wontCopy': 'Wordt niet gekopieerd', 'dashboard.confirm.copy.wont1': 'Medewerkers & ledentoewijzingen', 'dashboard.confirm.copy.wont2': 'Gedeelde notities, peilingen & berichten', - 'dashboard.confirm.copy.wont3': 'Bestanden & foto\'s', + 'dashboard.confirm.copy.wont3': "Bestanden & foto's", 'dashboard.confirm.copy.wont4': 'Deeltokens', 'dashboard.confirm.copy.confirm': 'Reis kopiëren', 'dashboard.aria.toggleView': 'Weergave wisselen', 'dashboard.aria.filter': 'Filter', 'dashboard.aria.duplicate': 'Dupliceren', 'dashboard.aria.refreshRates': 'Koersen vernieuwen', - 'dashboard.aria.swapCurrencies': 'Valuta\'s omwisselen', + 'dashboard.aria.swapCurrencies': "Valuta's omwisselen", 'dashboard.aria.addTimezone': 'Tijdzone toevoegen', 'dashboard.aria.removeTimezone': '{city} verwijderen', }; diff --git a/shared/src/i18n/uk/dashboard.ts b/shared/src/i18n/uk/dashboard.ts index 159716f4..15ab12ee 100644 --- a/shared/src/i18n/uk/dashboard.ts +++ b/shared/src/i18n/uk/dashboard.ts @@ -153,7 +153,8 @@ const dashboard: TranslationStrings = { 'dashboard.fx.to': 'У', 'dashboard.fx.unavailable': 'Курс недоступний', 'dashboard.tz.searchPlaceholder': 'Пошук часового поясу…', - 'dashboard.tz.empty': 'Інших часових поясів поки немає — додайте за допомогою +', + 'dashboard.tz.empty': + 'Інших часових поясів поки немає — додайте за допомогою +', 'dashboard.upcoming.title': 'Найближчі бронювання', 'dashboard.upcoming.empty': 'Поки нічого не заброньовано.', 'dashboard.aria.toggleView': 'Перемкнути вигляд', diff --git a/shared/src/journey/journey.schema.spec.ts b/shared/src/journey/journey.schema.spec.ts index 0661883b..6fec24e1 100644 --- a/shared/src/journey/journey.schema.spec.ts +++ b/shared/src/journey/journey.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { journeyCreateRequestSchema, journeyAddTripRequestSchema, @@ -8,48 +7,92 @@ import { journeyShareLinkRequestSchema, } from './journey.schema'; +import { describe, it, expect } from 'vitest'; + describe('journeyCreateRequestSchema', () => { it('requires a title; subtitle + trip_ids optional', () => { - expect(journeyCreateRequestSchema.safeParse({ title: 'Trip of a lifetime' }).success).toBe(true); - expect(journeyCreateRequestSchema.safeParse({ title: 'X', trip_ids: [1, '2'] }).success).toBe(true); - expect(journeyCreateRequestSchema.safeParse({ subtitle: 'no title' }).success).toBe(false); + expect( + journeyCreateRequestSchema.safeParse({ title: 'Trip of a lifetime' }) + .success, + ).toBe(true); + expect( + journeyCreateRequestSchema.safeParse({ title: 'X', trip_ids: [1, '2'] }) + .success, + ).toBe(true); + expect( + journeyCreateRequestSchema.safeParse({ subtitle: 'no title' }).success, + ).toBe(false); }); }); describe('journeyAddTripRequestSchema', () => { it('requires a trip_id (string or number)', () => { - expect(journeyAddTripRequestSchema.safeParse({ trip_id: 5 }).success).toBe(true); - expect(journeyAddTripRequestSchema.safeParse({ trip_id: '5' }).success).toBe(true); + expect(journeyAddTripRequestSchema.safeParse({ trip_id: 5 }).success).toBe( + true, + ); + expect( + journeyAddTripRequestSchema.safeParse({ trip_id: '5' }).success, + ).toBe(true); expect(journeyAddTripRequestSchema.safeParse({}).success).toBe(false); }); }); describe('journeyReorderEntriesRequestSchema', () => { it('requires a non-empty orderedIds array', () => { - expect(journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true); - expect(journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [] }).success).toBe(false); + expect( + journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [3, 1, 2] }) + .success, + ).toBe(true); + expect( + journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [] }).success, + ).toBe(false); }); }); describe('journeyContributorRequestSchema', () => { it('requires user_id; role limited to editor/viewer', () => { - expect(journeyContributorRequestSchema.safeParse({ user_id: 2 }).success).toBe(true); - expect(journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'editor' }).success).toBe(true); - expect(journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'admin' }).success).toBe(false); + expect( + journeyContributorRequestSchema.safeParse({ user_id: 2 }).success, + ).toBe(true); + expect( + journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'editor' }) + .success, + ).toBe(true); + expect( + journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'admin' }) + .success, + ).toBe(false); }); }); describe('journeyProviderPhotosRequestSchema', () => { it('requires a provider; accepts single asset_id or a batch', () => { - expect(journeyProviderPhotosRequestSchema.safeParse({ provider: 'immich', asset_id: 'a1' }).success).toBe(true); - expect(journeyProviderPhotosRequestSchema.safeParse({ provider: 'immich', asset_ids: ['a1', 'a2'] }).success).toBe(true); - expect(journeyProviderPhotosRequestSchema.safeParse({ asset_id: 'a1' }).success).toBe(false); + expect( + journeyProviderPhotosRequestSchema.safeParse({ + provider: 'immich', + asset_id: 'a1', + }).success, + ).toBe(true); + expect( + journeyProviderPhotosRequestSchema.safeParse({ + provider: 'immich', + asset_ids: ['a1', 'a2'], + }).success, + ).toBe(true); + expect( + journeyProviderPhotosRequestSchema.safeParse({ asset_id: 'a1' }).success, + ).toBe(false); }); }); describe('journeyShareLinkRequestSchema', () => { it('accepts optional share toggles', () => { - expect(journeyShareLinkRequestSchema.safeParse({ share_timeline: true, share_gallery: false }).success).toBe(true); + expect( + journeyShareLinkRequestSchema.safeParse({ + share_timeline: true, + share_gallery: false, + }).success, + ).toBe(true); expect(journeyShareLinkRequestSchema.safeParse({}).success).toBe(true); }); }); diff --git a/shared/src/journey/journey.schema.ts b/shared/src/journey/journey.schema.ts index 9e374ac2..1c8e5735 100644 --- a/shared/src/journey/journey.schema.ts +++ b/shared/src/journey/journey.schema.ts @@ -28,13 +28,17 @@ export type JourneyAddTripRequest = z.infer; export const journeyReorderEntriesRequestSchema = z.object({ orderedIds: z.array(z.union([z.string(), z.number()])).min(1), }); -export type JourneyReorderEntriesRequest = z.infer; +export type JourneyReorderEntriesRequest = z.infer< + typeof journeyReorderEntriesRequestSchema +>; export const journeyContributorRequestSchema = z.object({ user_id: z.union([z.string(), z.number()]), role: z.enum(['editor', 'viewer']).optional(), }); -export type JourneyContributorRequest = z.infer; +export type JourneyContributorRequest = z.infer< + typeof journeyContributorRequestSchema +>; export const journeyProviderPhotosRequestSchema = z.object({ provider: z.string().min(1), @@ -43,11 +47,15 @@ export const journeyProviderPhotosRequestSchema = z.object({ caption: z.string().optional(), passphrase: z.string().optional(), }); -export type JourneyProviderPhotosRequest = z.infer; +export type JourneyProviderPhotosRequest = z.infer< + typeof journeyProviderPhotosRequestSchema +>; export const journeyShareLinkRequestSchema = z.object({ share_timeline: z.boolean().optional(), share_gallery: z.boolean().optional(), share_map: z.boolean().optional(), }); -export type JourneyShareLinkRequest = z.infer; +export type JourneyShareLinkRequest = z.infer< + typeof journeyShareLinkRequestSchema +>; diff --git a/shared/src/maps/maps.schema.spec.ts b/shared/src/maps/maps.schema.spec.ts index e0af3e6e..b831901e 100644 --- a/shared/src/maps/maps.schema.spec.ts +++ b/shared/src/maps/maps.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { mapsSearchRequestSchema, mapsAutocompleteRequestSchema, @@ -6,34 +5,58 @@ import { mapsResolveUrlRequestSchema, } from './maps.schema'; +import { describe, it, expect } from 'vitest'; + describe('mapsSearchRequestSchema', () => { it('requires a non-empty query', () => { - expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe(true); - expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe(false); + expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe( + true, + ); + expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe( + false, + ); expect(mapsSearchRequestSchema.safeParse({}).success).toBe(false); }); }); describe('mapsAutocompleteRequestSchema', () => { it('caps input at 200 chars and allows an optional locationBias', () => { - expect(mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success).toBe(true); - expect(mapsAutocompleteRequestSchema.safeParse({ input: 'x'.repeat(201) }).success).toBe(false); - expect(mapsAutocompleteRequestSchema.safeParse({ - input: 'be', locationBias: { low: { lat: 1, lng: 2 }, high: { lat: 3, lng: 4 } }, - }).success).toBe(true); + expect( + mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success, + ).toBe(true); + expect( + mapsAutocompleteRequestSchema.safeParse({ input: 'x'.repeat(201) }) + .success, + ).toBe(false); + expect( + mapsAutocompleteRequestSchema.safeParse({ + input: 'be', + locationBias: { low: { lat: 1, lng: 2 }, high: { lat: 3, lng: 4 } }, + }).success, + ).toBe(true); }); }); describe('mapsReverseQuerySchema', () => { it('requires lat and lng as strings (the route parses them downstream)', () => { - expect(mapsReverseQuerySchema.safeParse({ lat: '52.5', lng: '13.4' }).success).toBe(true); - expect(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe(false); + expect( + mapsReverseQuerySchema.safeParse({ lat: '52.5', lng: '13.4' }).success, + ).toBe(true); + expect(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe( + false, + ); }); }); describe('mapsResolveUrlRequestSchema', () => { it('requires a non-empty url', () => { - expect(mapsResolveUrlRequestSchema.safeParse({ url: 'https://maps.app.goo.gl/x' }).success).toBe(true); - expect(mapsResolveUrlRequestSchema.safeParse({ url: '' }).success).toBe(false); + expect( + mapsResolveUrlRequestSchema.safeParse({ + url: 'https://maps.app.goo.gl/x', + }).success, + ).toBe(true); + expect(mapsResolveUrlRequestSchema.safeParse({ url: '' }).success).toBe( + false, + ); }); }); diff --git a/shared/src/maps/maps.schema.ts b/shared/src/maps/maps.schema.ts index 8477f548..f982a61a 100644 --- a/shared/src/maps/maps.schema.ts +++ b/shared/src/maps/maps.schema.ts @@ -27,7 +27,9 @@ export const mapsAutocompleteRequestSchema = z.object({ lang: z.string().optional(), locationBias: z.object({ low: latLng, high: latLng }).optional(), }); -export type MapsAutocompleteRequest = z.infer; +export type MapsAutocompleteRequest = z.infer< + typeof mapsAutocompleteRequestSchema +>; export const mapsReverseQuerySchema = z.object({ lat: z.string().min(1), @@ -59,13 +61,17 @@ export const mapsAutocompleteResultSchema = z.object({ suggestions: z.array(mapsAutocompleteSuggestionSchema), source: z.string(), }); -export type MapsAutocompleteResult = z.infer; +export type MapsAutocompleteResult = z.infer< + typeof mapsAutocompleteResultSchema +>; export const mapsPlaceDetailsResultSchema = z.object({ place: placeRecord.nullable(), disabled: z.boolean().optional(), }); -export type MapsPlaceDetailsResult = z.infer; +export type MapsPlaceDetailsResult = z.infer< + typeof mapsPlaceDetailsResultSchema +>; export const mapsPlacePhotoResultSchema = z.object({ photoUrl: z.string().nullable(), diff --git a/shared/src/notification/notification.schema.spec.ts b/shared/src/notification/notification.schema.spec.ts index bb599a46..d425eccd 100644 --- a/shared/src/notification/notification.schema.spec.ts +++ b/shared/src/notification/notification.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { preferencesUpdateRequestSchema, notificationRespondRequestSchema, @@ -6,31 +5,55 @@ import { inAppListResultSchema, } from './notification.schema'; +import { describe, it, expect } from 'vitest'; + describe('preferencesUpdateRequestSchema', () => { it('accepts a nested event/channel/enabled matrix', () => { - expect(preferencesUpdateRequestSchema.safeParse({ trip_invite: { inapp: true, email: false } }).success).toBe(true); - expect(preferencesUpdateRequestSchema.safeParse({ trip_invite: { inapp: 'yes' } }).success).toBe(false); + expect( + preferencesUpdateRequestSchema.safeParse({ + trip_invite: { inapp: true, email: false }, + }).success, + ).toBe(true); + expect( + preferencesUpdateRequestSchema.safeParse({ + trip_invite: { inapp: 'yes' }, + }).success, + ).toBe(false); }); }); describe('notificationRespondRequestSchema', () => { it('only accepts positive/negative', () => { - expect(notificationRespondRequestSchema.safeParse({ response: 'positive' }).success).toBe(true); - expect(notificationRespondRequestSchema.safeParse({ response: 'maybe' }).success).toBe(false); + expect( + notificationRespondRequestSchema.safeParse({ response: 'positive' }) + .success, + ).toBe(true); + expect( + notificationRespondRequestSchema.safeParse({ response: 'maybe' }).success, + ).toBe(false); }); }); describe('channelTestResultSchema', () => { it('accepts a success result and an error result', () => { - expect(channelTestResultSchema.safeParse({ success: true }).success).toBe(true); - expect(channelTestResultSchema.safeParse({ success: false, error: 'SMTP down' }).success).toBe(true); + expect(channelTestResultSchema.safeParse({ success: true }).success).toBe( + true, + ); + expect( + channelTestResultSchema.safeParse({ success: false, error: 'SMTP down' }) + .success, + ).toBe(true); }); }); describe('inAppListResultSchema', () => { it('accepts the list envelope with open notification rows', () => { - expect(inAppListResultSchema.safeParse({ - notifications: [{ id: 1, type: 'info', anything: 'goes' }], total: 1, unread_count: 0, - }).success).toBe(true); + expect( + inAppListResultSchema.safeParse({ + notifications: [{ id: 1, type: 'info', anything: 'goes' }], + total: 1, + unread_count: 0, + }).success, + ).toBe(true); }); }); diff --git a/shared/src/notification/notification.schema.ts b/shared/src/notification/notification.schema.ts index bfaf6b29..129c85f1 100644 --- a/shared/src/notification/notification.schema.ts +++ b/shared/src/notification/notification.schema.ts @@ -18,10 +18,14 @@ export const preferencesUpdateRequestSchema = z.record( z.string(), z.record(z.string(), z.boolean()), ); -export type PreferencesUpdateRequest = z.infer; +export type PreferencesUpdateRequest = z.infer< + typeof preferencesUpdateRequestSchema +>; export const testSmtpRequestSchema = z.object({ email: z.string().optional() }); -export const testWebhookRequestSchema = z.object({ url: z.string().optional() }); +export const testWebhookRequestSchema = z.object({ + url: z.string().optional(), +}); export const testNtfyRequestSchema = z.object({ topic: z.string().optional(), server: z.string().optional(), @@ -39,7 +43,9 @@ export type ChannelTestResult = z.infer; export const notificationRespondRequestSchema = z.object({ response: z.enum(['positive', 'negative']), }); -export type NotificationRespondRequest = z.infer; +export type NotificationRespondRequest = z.infer< + typeof notificationRespondRequestSchema +>; /** A single in-app notification row (DB-shaped; kept open). */ export const notificationRowSchema = z.record(z.string(), z.unknown()); diff --git a/shared/src/oauth/oauth.schema.spec.ts b/shared/src/oauth/oauth.schema.spec.ts index 7803fa73..5bb0ff5f 100644 --- a/shared/src/oauth/oauth.schema.spec.ts +++ b/shared/src/oauth/oauth.schema.spec.ts @@ -1,25 +1,71 @@ +import { + oauthTokenRequestSchema, + oauthConsentRequestSchema, + oauthClientCreateRequestSchema, +} from './oauth.schema'; + import { describe, it, expect } from 'vitest'; -import { oauthTokenRequestSchema, oauthConsentRequestSchema, oauthClientCreateRequestSchema } from './oauth.schema'; describe('oauthTokenRequestSchema', () => { it('is permissive across grant types and passes extras through', () => { - expect(oauthTokenRequestSchema.safeParse({ grant_type: 'authorization_code', client_id: 'c', code: 'x', redirect_uri: 'u', code_verifier: 'v' }).success).toBe(true); - expect(oauthTokenRequestSchema.safeParse({ grant_type: 'client_credentials', client_id: 'c', client_secret: 's', scope: 'a b' }).success).toBe(true); + expect( + oauthTokenRequestSchema.safeParse({ + grant_type: 'authorization_code', + client_id: 'c', + code: 'x', + redirect_uri: 'u', + code_verifier: 'v', + }).success, + ).toBe(true); + expect( + oauthTokenRequestSchema.safeParse({ + grant_type: 'client_credentials', + client_id: 'c', + client_secret: 's', + scope: 'a b', + }).success, + ).toBe(true); expect(oauthTokenRequestSchema.safeParse({}).success).toBe(true); }); }); describe('oauthConsentRequestSchema', () => { it('requires the PKCE consent fields + approved flag', () => { - expect(oauthConsentRequestSchema.safeParse({ client_id: 'c', redirect_uri: 'u', scope: 's', code_challenge: 'cc', code_challenge_method: 'S256', approved: true }).success).toBe(true); - expect(oauthConsentRequestSchema.safeParse({ client_id: 'c', redirect_uri: 'u', scope: 's', code_challenge: 'cc', code_challenge_method: 'S256' }).success).toBe(false); + expect( + oauthConsentRequestSchema.safeParse({ + client_id: 'c', + redirect_uri: 'u', + scope: 's', + code_challenge: 'cc', + code_challenge_method: 'S256', + approved: true, + }).success, + ).toBe(true); + expect( + oauthConsentRequestSchema.safeParse({ + client_id: 'c', + redirect_uri: 'u', + scope: 's', + code_challenge: 'cc', + code_challenge_method: 'S256', + }).success, + ).toBe(false); }); }); describe('oauthClientCreateRequestSchema', () => { it('requires name + allowed_scopes', () => { - expect(oauthClientCreateRequestSchema.safeParse({ name: 'CLI', allowed_scopes: ['trips:read'] }).success).toBe(true); - expect(oauthClientCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(false); - expect(oauthClientCreateRequestSchema.safeParse({ allowed_scopes: [] }).success).toBe(false); + expect( + oauthClientCreateRequestSchema.safeParse({ + name: 'CLI', + allowed_scopes: ['trips:read'], + }).success, + ).toBe(true); + expect( + oauthClientCreateRequestSchema.safeParse({ name: 'CLI' }).success, + ).toBe(false); + expect( + oauthClientCreateRequestSchema.safeParse({ allowed_scopes: [] }).success, + ).toBe(false); }); }); diff --git a/shared/src/oauth/oauth.schema.ts b/shared/src/oauth/oauth.schema.ts index 263437e8..14438eb9 100644 --- a/shared/src/oauth/oauth.schema.ts +++ b/shared/src/oauth/oauth.schema.ts @@ -41,4 +41,6 @@ export const oauthClientCreateRequestSchema = z.object({ allowed_scopes: z.array(z.string()), allows_client_credentials: z.boolean().optional(), }); -export type OauthClientCreateRequest = z.infer; +export type OauthClientCreateRequest = z.infer< + typeof oauthClientCreateRequestSchema +>; diff --git a/shared/src/oidc/oidc.schema.spec.ts b/shared/src/oidc/oidc.schema.spec.ts index 1cf17211..dc180f81 100644 --- a/shared/src/oidc/oidc.schema.spec.ts +++ b/shared/src/oidc/oidc.schema.spec.ts @@ -1,10 +1,18 @@ +import { + oidcCallbackQuerySchema, + oidcExchangeQuerySchema, +} from './oidc.schema'; + import { describe, it, expect } from 'vitest'; -import { oidcCallbackQuerySchema, oidcExchangeQuerySchema } from './oidc.schema'; describe('oidcCallbackQuerySchema', () => { it('accepts code+state, an error, or nothing (all optional)', () => { - expect(oidcCallbackQuerySchema.safeParse({ code: 'c', state: 's' }).success).toBe(true); - expect(oidcCallbackQuerySchema.safeParse({ error: 'access_denied' }).success).toBe(true); + expect( + oidcCallbackQuerySchema.safeParse({ code: 'c', state: 's' }).success, + ).toBe(true); + expect( + oidcCallbackQuerySchema.safeParse({ error: 'access_denied' }).success, + ).toBe(true); expect(oidcCallbackQuerySchema.safeParse({}).success).toBe(true); }); }); diff --git a/shared/src/packing/packing.schema.spec.ts b/shared/src/packing/packing.schema.spec.ts index a8e07d4f..0b221c5c 100644 --- a/shared/src/packing/packing.schema.spec.ts +++ b/shared/src/packing/packing.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { packingCreateItemRequestSchema, packingImportRequestSchema, @@ -6,30 +5,52 @@ import { packingSaveTemplateRequestSchema, } from './packing.schema'; +import { describe, it, expect } from 'vitest'; + describe('packingCreateItemRequestSchema', () => { it('requires a non-empty name; category/checked optional', () => { - expect(packingCreateItemRequestSchema.safeParse({ name: 'Socks' }).success).toBe(true); - expect(packingCreateItemRequestSchema.safeParse({ name: 'Socks', category: 'Clothes', checked: true }).success).toBe(true); - expect(packingCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(false); + expect( + packingCreateItemRequestSchema.safeParse({ name: 'Socks' }).success, + ).toBe(true); + expect( + packingCreateItemRequestSchema.safeParse({ + name: 'Socks', + category: 'Clothes', + checked: true, + }).success, + ).toBe(true); + expect(packingCreateItemRequestSchema.safeParse({ name: '' }).success).toBe( + false, + ); }); }); describe('packingImportRequestSchema', () => { it('accepts an array of open item rows', () => { - expect(packingImportRequestSchema.safeParse({ items: [{ name: 'a' }, { name: 'b', anything: 1 }] }).success).toBe(true); + expect( + packingImportRequestSchema.safeParse({ + items: [{ name: 'a' }, { name: 'b', anything: 1 }], + }).success, + ).toBe(true); }); }); describe('packingCreateBagRequestSchema', () => { it('requires a name', () => { - expect(packingCreateBagRequestSchema.safeParse({ name: 'Carry-on' }).success).toBe(true); + expect( + packingCreateBagRequestSchema.safeParse({ name: 'Carry-on' }).success, + ).toBe(true); expect(packingCreateBagRequestSchema.safeParse({}).success).toBe(false); }); }); describe('packingSaveTemplateRequestSchema', () => { it('requires a name', () => { - expect(packingSaveTemplateRequestSchema.safeParse({ name: 'Summer' }).success).toBe(true); - expect(packingSaveTemplateRequestSchema.safeParse({ name: '' }).success).toBe(false); + expect( + packingSaveTemplateRequestSchema.safeParse({ name: 'Summer' }).success, + ).toBe(true); + expect( + packingSaveTemplateRequestSchema.safeParse({ name: '' }).success, + ).toBe(false); }); }); diff --git a/shared/src/packing/packing.schema.ts b/shared/src/packing/packing.schema.ts index 2c7e87ff..8aed7d05 100644 --- a/shared/src/packing/packing.schema.ts +++ b/shared/src/packing/packing.schema.ts @@ -68,7 +68,9 @@ export const packingCreateItemRequestSchema = z.object({ category: z.string().optional(), checked: z.boolean().optional(), }); -export type PackingCreateItemRequest = z.infer; +export type PackingCreateItemRequest = z.infer< + typeof packingCreateItemRequestSchema +>; export const packingUpdateItemRequestSchema = z.object({ name: z.string().optional(), @@ -78,7 +80,9 @@ export const packingUpdateItemRequestSchema = z.object({ bag_id: z.number().nullable().optional(), quantity: z.number().optional(), }); -export type PackingUpdateItemRequest = z.infer; +export type PackingUpdateItemRequest = z.infer< + typeof packingUpdateItemRequestSchema +>; export const packingImportRequestSchema = z.object({ items: z.array(open), @@ -94,7 +98,9 @@ export const packingCreateBagRequestSchema = z.object({ name: z.string().min(1), color: z.string().optional(), }); -export type PackingCreateBagRequest = z.infer; +export type PackingCreateBagRequest = z.infer< + typeof packingCreateBagRequestSchema +>; export const packingUpdateBagRequestSchema = z.object({ name: z.string().optional(), @@ -102,19 +108,27 @@ export const packingUpdateBagRequestSchema = z.object({ weight_limit_grams: z.number().nullable().optional(), user_id: z.number().nullable().optional(), }); -export type PackingUpdateBagRequest = z.infer; +export type PackingUpdateBagRequest = z.infer< + typeof packingUpdateBagRequestSchema +>; export const packingBagMembersRequestSchema = z.object({ user_ids: z.array(z.number()), }); -export type PackingBagMembersRequest = z.infer; +export type PackingBagMembersRequest = z.infer< + typeof packingBagMembersRequestSchema +>; export const packingSaveTemplateRequestSchema = z.object({ name: z.string().min(1), }); -export type PackingSaveTemplateRequest = z.infer; +export type PackingSaveTemplateRequest = z.infer< + typeof packingSaveTemplateRequestSchema +>; export const packingCategoryAssigneesRequestSchema = z.object({ user_ids: z.array(z.number()), }); -export type PackingCategoryAssigneesRequest = z.infer; +export type PackingCategoryAssigneesRequest = z.infer< + typeof packingCategoryAssigneesRequestSchema +>; diff --git a/shared/src/place/place.schema.spec.ts b/shared/src/place/place.schema.spec.ts index 678f0da8..f7c40f08 100644 --- a/shared/src/place/place.schema.spec.ts +++ b/shared/src/place/place.schema.spec.ts @@ -1,27 +1,43 @@ -import { describe, it, expect } from 'vitest'; import { placeCreateRequestSchema, placeBulkDeleteRequestSchema, placeImportListRequestSchema, } from './place.schema'; +import { describe, it, expect } from 'vitest'; + describe('placeCreateRequestSchema', () => { it('requires a name and keeps the other place fields open', () => { - expect(placeCreateRequestSchema.safeParse({ name: 'Spot', lat: 1, lng: 2, anything: true }).success).toBe(true); + expect( + placeCreateRequestSchema.safeParse({ + name: 'Spot', + lat: 1, + lng: 2, + anything: true, + }).success, + ).toBe(true); expect(placeCreateRequestSchema.safeParse({ lat: 1 }).success).toBe(false); }); }); describe('placeBulkDeleteRequestSchema', () => { it('requires a numeric ids array', () => { - expect(placeBulkDeleteRequestSchema.safeParse({ ids: [1, 2] }).success).toBe(true); - expect(placeBulkDeleteRequestSchema.safeParse({ ids: ['a'] }).success).toBe(false); + expect( + placeBulkDeleteRequestSchema.safeParse({ ids: [1, 2] }).success, + ).toBe(true); + expect(placeBulkDeleteRequestSchema.safeParse({ ids: ['a'] }).success).toBe( + false, + ); }); }); describe('placeImportListRequestSchema', () => { it('requires a non-empty url', () => { - expect(placeImportListRequestSchema.safeParse({ url: 'http://x' }).success).toBe(true); - expect(placeImportListRequestSchema.safeParse({ url: '' }).success).toBe(false); + expect( + placeImportListRequestSchema.safeParse({ url: 'http://x' }).success, + ).toBe(true); + expect(placeImportListRequestSchema.safeParse({ url: '' }).success).toBe( + false, + ); }); }); diff --git a/shared/src/place/place.schema.ts b/shared/src/place/place.schema.ts index beb62598..87ae31e1 100644 --- a/shared/src/place/place.schema.ts +++ b/shared/src/place/place.schema.ts @@ -1,7 +1,7 @@ -import { z } from 'zod'; -import { categorySchema } from '../category/category.schema'; import { tagSchema } from '../tag/tag.schema'; +import { z } from 'zod'; + /** * Place API contract — single source of truth for the /api/trips/:tripId/places * endpoints (place pool CRUD, GPX/map/list imports, image search, bulk delete). @@ -100,7 +100,9 @@ export const assignmentPlaceSchema = z.object({ }); export type AssignmentPlace = z.infer; -export const placeCreateRequestSchema = open.and(z.object({ name: z.string().min(1) })); +export const placeCreateRequestSchema = open.and( + z.object({ name: z.string().min(1) }), +); export type PlaceCreateRequest = z.infer; export const placeUpdateRequestSchema = open; @@ -109,12 +111,16 @@ export type PlaceUpdateRequest = z.infer; export const placeBulkDeleteRequestSchema = z.object({ ids: z.array(z.number()), }); -export type PlaceBulkDeleteRequest = z.infer; +export type PlaceBulkDeleteRequest = z.infer< + typeof placeBulkDeleteRequestSchema +>; export const placeImportListRequestSchema = z.object({ url: z.string().min(1), }); -export type PlaceImportListRequest = z.infer; +export type PlaceImportListRequest = z.infer< + typeof placeImportListRequestSchema +>; /** Query filters for the place list. */ export const placeListQuerySchema = z.object({ diff --git a/shared/src/reservation/reservation.schema.spec.ts b/shared/src/reservation/reservation.schema.spec.ts index 3a4bd05e..d2fb7bd2 100644 --- a/shared/src/reservation/reservation.schema.spec.ts +++ b/shared/src/reservation/reservation.schema.spec.ts @@ -1,27 +1,52 @@ -import { describe, it, expect } from 'vitest'; import { reservationCreateRequestSchema, reservationPositionsRequestSchema, accommodationCreateRequestSchema, } from './reservation.schema'; +import { describe, it, expect } from 'vitest'; + describe('reservationCreateRequestSchema', () => { it('requires a title and keeps the other booking fields open', () => { - expect(reservationCreateRequestSchema.safeParse({ title: 'Hotel', anything: 1, metadata: {} }).success).toBe(true); - expect(reservationCreateRequestSchema.safeParse({ location: 'x' }).success).toBe(false); + expect( + reservationCreateRequestSchema.safeParse({ + title: 'Hotel', + anything: 1, + metadata: {}, + }).success, + ).toBe(true); + expect( + reservationCreateRequestSchema.safeParse({ location: 'x' }).success, + ).toBe(false); }); }); describe('reservationPositionsRequestSchema', () => { it('requires positions with id + day_plan_position', () => { - expect(reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1, day_plan_position: 0 }], day_id: 3 }).success).toBe(true); - expect(reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1 }] }).success).toBe(false); + expect( + reservationPositionsRequestSchema.safeParse({ + positions: [{ id: 1, day_plan_position: 0 }], + day_id: 3, + }).success, + ).toBe(true); + expect( + reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1 }] }) + .success, + ).toBe(false); }); }); describe('accommodationCreateRequestSchema', () => { it('requires place + start/end day; check-in/out optional', () => { - expect(accommodationCreateRequestSchema.safeParse({ place_id: 2, start_day_id: 10, end_day_id: 11 }).success).toBe(true); - expect(accommodationCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(false); + expect( + accommodationCreateRequestSchema.safeParse({ + place_id: 2, + start_day_id: 10, + end_day_id: 11, + }).success, + ).toBe(true); + expect( + accommodationCreateRequestSchema.safeParse({ place_id: 2 }).success, + ).toBe(false); }); }); diff --git a/shared/src/reservation/reservation.schema.ts b/shared/src/reservation/reservation.schema.ts index 7655d95a..d2033ff9 100644 --- a/shared/src/reservation/reservation.schema.ts +++ b/shared/src/reservation/reservation.schema.ts @@ -101,17 +101,27 @@ export const accommodationSchema = z.object({ export type Accommodation = z.infer; /** 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; +export const reservationCreateRequestSchema = open.and( + z.object({ title: z.string().min(1) }), +); +export type ReservationCreateRequest = z.infer< + typeof reservationCreateRequestSchema +>; export const reservationUpdateRequestSchema = open; -export type ReservationUpdateRequest = z.infer; +export type ReservationUpdateRequest = z.infer< + typeof reservationUpdateRequestSchema +>; export const reservationPositionsRequestSchema = z.object({ - positions: z.array(z.object({ id: z.number(), day_plan_position: z.number() })), + positions: z.array( + z.object({ id: z.number(), day_plan_position: z.number() }), + ), day_id: z.union([z.number(), z.string()]).nullable().optional(), }); -export type ReservationPositionsRequest = z.infer; +export type ReservationPositionsRequest = z.infer< + typeof reservationPositionsRequestSchema +>; export const accommodationCreateRequestSchema = z.object({ place_id: z.union([z.number(), z.string()]), @@ -123,7 +133,11 @@ export const accommodationCreateRequestSchema = z.object({ confirmation: z.string().nullable().optional(), notes: z.string().nullable().optional(), }); -export type AccommodationCreateRequest = z.infer; +export type AccommodationCreateRequest = z.infer< + typeof accommodationCreateRequestSchema +>; export const accommodationUpdateRequestSchema = open; -export type AccommodationUpdateRequest = z.infer; +export type AccommodationUpdateRequest = z.infer< + typeof accommodationUpdateRequestSchema +>; diff --git a/shared/src/sanitize/sanitize.spec.ts b/shared/src/sanitize/sanitize.spec.ts index dbae3b21..27eb19ac 100644 --- a/shared/src/sanitize/sanitize.spec.ts +++ b/shared/src/sanitize/sanitize.spec.ts @@ -1,118 +1,139 @@ -import { describe, it, expect } from 'vitest' -import { sanitizeInlineHtml, sanitizeRichTextHtml, escapeHtml } from './sanitize' +import { + sanitizeInlineHtml, + sanitizeRichTextHtml, + escapeHtml, +} from './sanitize'; + +import { describe, it, expect } from 'vitest'; describe('escapeHtml', () => { it('escapes the five metacharacters', () => { - expect(escapeHtml(`a & b < c > d " e ' f`)).toBe('a & b < c > d " e ' f') - }) + expect(escapeHtml(`a & b < c > d " e ' f`)).toBe( + 'a & b < c > d " e ' f', + ); + }); it('escapes ampersands first (no double-escape of entities)', () => { - expect(escapeHtml('<')).toBe('&lt;') - }) + expect(escapeHtml('<')).toBe('&lt;'); + }); it('returns empty string for empty input', () => { - expect(escapeHtml('')).toBe('') - }) + expect(escapeHtml('')).toBe(''); + }); it('leaves plain ASCII text untouched', () => { - expect(escapeHtml('Paris Adventure 2026')).toBe('Paris Adventure 2026') - }) + expect(escapeHtml('Paris Adventure 2026')).toBe('Paris Adventure 2026'); + }); it('neutralises a script tag without sanitising', () => { - expect(escapeHtml('')).toBe('<script>alert(1)</script>') - }) -}) + expect(escapeHtml('')).toBe( + '<script>alert(1)</script>', + ); + }); +}); describe('sanitizeInlineHtml', () => { it('returns empty string for empty input', () => { - expect(sanitizeInlineHtml('')).toBe('') - }) + expect(sanitizeInlineHtml('')).toBe(''); + }); it('preserves the allowed inline tags', () => { - expect(sanitizeInlineHtml('a b c')).toBe('a b c') - expect(sanitizeInlineHtml('x')).toBe('x') - }) + expect(sanitizeInlineHtml('a b c')).toBe( + 'a b c', + ); + expect(sanitizeInlineHtml('x')).toBe('x'); + }); it('strips text') - expect(out).not.toContain('alert(1) text'); + expect(out).not.toContain(' (no img tag in inline allow-list)', () => { - expect(sanitizeInlineHtml('')).toBe('') - }) + expect(sanitizeInlineHtml('')).toBe(''); + }); it('strips on* event handlers from preserved tags', () => { - const out = sanitizeInlineHtml('hi') - expect(out).not.toContain('onclick') - expect(out).toContain('hi') - }) + const out = sanitizeInlineHtml('hi'); + expect(out).not.toContain('onclick'); + expect(out).toContain('hi'); + }); it('strips style attribute (CSS-injection surface)', () => { - const out = sanitizeInlineHtml('x') - expect(out).not.toContain('style=') - expect(out).not.toContain('javascript:') - }) + const out = sanitizeInlineHtml( + 'x', + ); + expect(out).not.toContain('style='); + expect(out).not.toContain('javascript:'); + }); it('strips iframe / object / embed / svg-with-script', () => { - expect(sanitizeInlineHtml('')).toBe('') - expect(sanitizeInlineHtml('')).toBe('') - expect(sanitizeInlineHtml('')).toBe('') - expect(sanitizeInlineHtml('')).not.toContain('script') - }) + expect(sanitizeInlineHtml('')).toBe(''); + expect(sanitizeInlineHtml('')).toBe(''); + expect(sanitizeInlineHtml('')).toBe(''); + expect( + sanitizeInlineHtml(''), + ).not.toContain('script'); + }); it('does not preserve href / target on the inline tag set', () => { // is not in the inline allow-list, so href can never appear here. - const out = sanitizeInlineHtml('x') - expect(out).toBe('x') - }) + const out = sanitizeInlineHtml('x'); + expect(out).toBe('x'); + }); it('keeps user text content when the wrapping tag is stripped', () => { - expect(sanitizeInlineHtml('hello')).toBe('hello') - }) -}) + expect(sanitizeInlineHtml('hello')).toBe('hello'); + }); +}); describe('sanitizeRichTextHtml', () => { it('preserves the full prose tag set', () => { - const html = '

hello world

  • one
' - const out = sanitizeRichTextHtml(html) - expect(out).toContain('

') - expect(out).toContain('world') - expect(out).toContain('

  • one
  • ') - }) + const html = '

    hello world

    • one
    '; + const out = sanitizeRichTextHtml(html); + expect(out).toContain('

    '); + expect(out).toContain('world'); + expect(out).toContain('

  • one
  • '); + }); it('still strips ') - expect(out).not.toContain('onclick') - expect(out).not.toContain('style=') - expect(out).not.toContain('hi

    ', + ); + expect(out).not.toContain('onclick'); + expect(out).not.toContain('style='); + expect(out).not.toContain(' { - const out = sanitizeRichTextHtml('x') - expect(out).not.toContain('javascript:') - }) + const out = sanitizeRichTextHtml('x'); + expect(out).not.toContain('javascript:'); + }); it('blocks data: hrefs that smuggle scripts', () => { - const out = sanitizeRichTextHtml('x') - expect(out).not.toContain('data:text/html') - }) + const out = sanitizeRichTextHtml( + 'x', + ); + expect(out).not.toContain('data:text/html'); + }); it('keeps http(s) hrefs intact', () => { - const out = sanitizeRichTextHtml('link') - expect(out).toContain('href="https://example.com"') - }) + const out = sanitizeRichTextHtml('link'); + expect(out).toContain('href="https://example.com"'); + }); it('strips disallowed tags but keeps their content', () => { - expect(sanitizeRichTextHtml('

    beforemiddleafter

    ')).toContain('middle') - }) + expect( + sanitizeRichTextHtml('

    beforemiddleafter

    '), + ).toContain('middle'); + }); it('drops mathml + svg shorthand vectors', () => { - const mathPayload = '' - const out = sanitizeRichTextHtml(mathPayload) - expect(out).not.toContain('/g, '>') .replace(/"/g, '"') - .replace(/'/g, ''') + .replace(/'/g, '''); } /** @@ -58,13 +82,13 @@ export function escapeHtml(value: string): string { * built-in URL allow-list. */ export function sanitizeInlineHtml(html: string): string { - if (!html) return '' + if (!html) return ''; return DOMPurify.sanitize(html, { ALLOWED_TAGS: [...INLINE_TAGS], ALLOWED_ATTR: [], KEEP_CONTENT: true, ALLOW_DATA_ATTR: false, - }) + }); } /** @@ -73,10 +97,10 @@ export function sanitizeInlineHtml(html: string): string { * the inline sanitiser plus block-level markup and anchors with safe attrs. */ export function sanitizeRichTextHtml(html: string): string { - if (!html) return '' + if (!html) return ''; return DOMPurify.sanitize(html, { ALLOWED_TAGS: [...FULL_TAGS], ALLOWED_ATTR: [...SAFE_ATTRIBUTES], ALLOW_DATA_ATTR: false, - }) + }); } diff --git a/shared/src/settings/settings.schema.spec.ts b/shared/src/settings/settings.schema.spec.ts index b72662a4..beee0f14 100644 --- a/shared/src/settings/settings.schema.spec.ts +++ b/shared/src/settings/settings.schema.spec.ts @@ -1,18 +1,35 @@ +import { + settingUpsertRequestSchema, + settingsBulkRequestSchema, + MASKED_SETTING_VALUE, +} from './settings.schema'; + import { describe, it, expect } from 'vitest'; -import { settingUpsertRequestSchema, settingsBulkRequestSchema, MASKED_SETTING_VALUE } from './settings.schema'; describe('settingUpsertRequestSchema', () => { it('requires a key; value is any/optional', () => { - expect(settingUpsertRequestSchema.safeParse({ key: 'theme', value: 'dark' }).success).toBe(true); - expect(settingUpsertRequestSchema.safeParse({ key: 'theme' }).success).toBe(true); - expect(settingUpsertRequestSchema.safeParse({ value: 'dark' }).success).toBe(false); + expect( + settingUpsertRequestSchema.safeParse({ key: 'theme', value: 'dark' }) + .success, + ).toBe(true); + expect(settingUpsertRequestSchema.safeParse({ key: 'theme' }).success).toBe( + true, + ); + expect( + settingUpsertRequestSchema.safeParse({ value: 'dark' }).success, + ).toBe(false); }); }); describe('settingsBulkRequestSchema', () => { it('requires a settings record', () => { - expect(settingsBulkRequestSchema.safeParse({ settings: { a: 1, b: 'x' } }).success).toBe(true); - expect(settingsBulkRequestSchema.safeParse({ settings: {} }).success).toBe(true); + expect( + settingsBulkRequestSchema.safeParse({ settings: { a: 1, b: 'x' } }) + .success, + ).toBe(true); + expect(settingsBulkRequestSchema.safeParse({ settings: {} }).success).toBe( + true, + ); expect(settingsBulkRequestSchema.safeParse({}).success).toBe(false); }); }); diff --git a/shared/src/share/share.schema.spec.ts b/shared/src/share/share.schema.spec.ts index bd597f78..b4962a77 100644 --- a/shared/src/share/share.schema.spec.ts +++ b/shared/src/share/share.schema.spec.ts @@ -1,13 +1,19 @@ -import { describe, it, expect } from 'vitest'; import { shareLinkRequestSchema } from './share.schema'; +import { describe, it, expect } from 'vitest'; + describe('shareLinkRequestSchema', () => { it('accepts any subset of section toggles (all optional) and an empty body', () => { - expect(shareLinkRequestSchema.safeParse({ share_map: true, share_budget: false }).success).toBe(true); + expect( + shareLinkRequestSchema.safeParse({ share_map: true, share_budget: false }) + .success, + ).toBe(true); expect(shareLinkRequestSchema.safeParse({}).success).toBe(true); }); it('rejects a non-boolean toggle', () => { - expect(shareLinkRequestSchema.safeParse({ share_map: 'yes' }).success).toBe(false); + expect(shareLinkRequestSchema.safeParse({ share_map: 'yes' }).success).toBe( + false, + ); }); }); diff --git a/shared/src/system-notice/system-notice.schema.spec.ts b/shared/src/system-notice/system-notice.schema.spec.ts index f1934df2..73f8c981 100644 --- a/shared/src/system-notice/system-notice.schema.spec.ts +++ b/shared/src/system-notice/system-notice.schema.spec.ts @@ -1,42 +1,78 @@ -import { describe, it, expect } from 'vitest'; import { systemNoticeDtoSchema } from './system-notice.schema'; +import { describe, it, expect } from 'vitest'; + describe('systemNoticeDtoSchema', () => { it('accepts a minimal notice (required fields only)', () => { const parsed = systemNoticeDtoSchema.parse({ - id: 'welcome', display: 'modal', severity: 'info', - titleKey: 'notice.welcome.title', bodyKey: 'notice.welcome.body', dismissible: true, + id: 'welcome', + display: 'modal', + severity: 'info', + titleKey: 'notice.welcome.title', + bodyKey: 'notice.welcome.body', + dismissible: true, }); expect(parsed.id).toBe('welcome'); }); it('accepts a rich notice with media, highlights and a nav CTA', () => { - expect(systemNoticeDtoSchema.safeParse({ - id: 'release', display: 'banner', severity: 'warn', - titleKey: 't', bodyKey: 'b', dismissible: false, - bodyParams: { version: '3.1' }, - icon: 'sparkles', - media: { src: '/img.png', altKey: 'alt', placement: 'hero' }, - highlights: [{ labelKey: 'h1', iconName: 'check' }], - cta: { kind: 'nav', labelKey: 'open', href: '/whats-new' }, - }).success).toBe(true); + expect( + systemNoticeDtoSchema.safeParse({ + id: 'release', + display: 'banner', + severity: 'warn', + titleKey: 't', + bodyKey: 'b', + dismissible: false, + bodyParams: { version: '3.1' }, + icon: 'sparkles', + media: { src: '/img.png', altKey: 'alt', placement: 'hero' }, + highlights: [{ labelKey: 'h1', iconName: 'check' }], + cta: { kind: 'nav', labelKey: 'open', href: '/whats-new' }, + }).success, + ).toBe(true); }); it('accepts an action CTA with the discriminated-union shape', () => { - expect(systemNoticeDtoSchema.safeParse({ - id: 'x', display: 'toast', severity: 'critical', - titleKey: 't', bodyKey: 'b', dismissible: true, - cta: { kind: 'action', labelKey: 'do', actionId: 'reload', dismissOnAction: true }, - }).success).toBe(true); + expect( + systemNoticeDtoSchema.safeParse({ + id: 'x', + display: 'toast', + severity: 'critical', + titleKey: 't', + bodyKey: 'b', + dismissible: true, + cta: { + kind: 'action', + labelKey: 'do', + actionId: 'reload', + dismissOnAction: true, + }, + }).success, + ).toBe(true); }); it('rejects an unknown display value and a malformed CTA', () => { - expect(systemNoticeDtoSchema.safeParse({ - id: 'x', display: 'popup', severity: 'info', titleKey: 't', bodyKey: 'b', dismissible: true, - }).success).toBe(false); - expect(systemNoticeDtoSchema.safeParse({ - id: 'x', display: 'modal', severity: 'info', titleKey: 't', bodyKey: 'b', dismissible: true, - cta: { kind: 'nav', labelKey: 'open' }, - }).success).toBe(false); + expect( + systemNoticeDtoSchema.safeParse({ + id: 'x', + display: 'popup', + severity: 'info', + titleKey: 't', + bodyKey: 'b', + dismissible: true, + }).success, + ).toBe(false); + expect( + systemNoticeDtoSchema.safeParse({ + id: 'x', + display: 'modal', + severity: 'info', + titleKey: 't', + bodyKey: 'b', + dismissible: true, + cta: { kind: 'nav', labelKey: 'open' }, + }).success, + ).toBe(false); }); }); diff --git a/shared/src/tag/tag.schema.spec.ts b/shared/src/tag/tag.schema.spec.ts index 890afc8b..3f7c7032 100644 --- a/shared/src/tag/tag.schema.spec.ts +++ b/shared/src/tag/tag.schema.spec.ts @@ -1,15 +1,29 @@ +import { + tagSchema, + createTagRequestSchema, + updateTagRequestSchema, +} from './tag.schema'; + import { describe, it, expect } from 'vitest'; -import { tagSchema, createTagRequestSchema, updateTagRequestSchema } from './tag.schema'; describe('tagSchema', () => { it('accepts a full tag', () => { - expect(tagSchema.safeParse({ id: 1, user_id: 5, name: 'Beach', color: '#10b981' }).success).toBe(true); + expect( + tagSchema.safeParse({ + id: 1, + user_id: 5, + name: 'Beach', + color: '#10b981', + }).success, + ).toBe(true); }); }); describe('createTagRequestSchema', () => { it('requires a non-empty name; colour optional', () => { - expect(createTagRequestSchema.safeParse({ name: 'Beach' }).success).toBe(true); + expect(createTagRequestSchema.safeParse({ name: 'Beach' }).success).toBe( + true, + ); expect(createTagRequestSchema.safeParse({ name: '' }).success).toBe(false); }); }); diff --git a/shared/src/todo/todo.schema.spec.ts b/shared/src/todo/todo.schema.spec.ts index 25d7042c..fc224025 100644 --- a/shared/src/todo/todo.schema.spec.ts +++ b/shared/src/todo/todo.schema.spec.ts @@ -1,30 +1,51 @@ -import { describe, it, expect } from 'vitest'; import { todoCreateItemRequestSchema, todoUpdateItemRequestSchema, todoReorderRequestSchema, } from './todo.schema'; +import { describe, it, expect } from 'vitest'; + describe('todoCreateItemRequestSchema', () => { it('requires a name; metadata optional with the service shapes', () => { - expect(todoCreateItemRequestSchema.safeParse({ name: 'Book hotel' }).success).toBe(true); - expect(todoCreateItemRequestSchema.safeParse({ name: 'X', due_date: '2026-07-01', priority: 2, assigned_user_id: 3 }).success).toBe(true); - expect(todoCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(false); + expect( + todoCreateItemRequestSchema.safeParse({ name: 'Book hotel' }).success, + ).toBe(true); + expect( + todoCreateItemRequestSchema.safeParse({ + name: 'X', + due_date: '2026-07-01', + priority: 2, + assigned_user_id: 3, + }).success, + ).toBe(true); + expect(todoCreateItemRequestSchema.safeParse({ name: '' }).success).toBe( + false, + ); // priority is numeric (matches the service), not a string - expect(todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' }).success).toBe(false); + expect( + todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' }) + .success, + ).toBe(false); }); }); describe('todoUpdateItemRequestSchema', () => { it('allows every field to be omitted and accepts checked', () => { expect(todoUpdateItemRequestSchema.safeParse({}).success).toBe(true); - expect(todoUpdateItemRequestSchema.safeParse({ checked: true }).success).toBe(true); + expect( + todoUpdateItemRequestSchema.safeParse({ checked: true }).success, + ).toBe(true); }); }); describe('todoReorderRequestSchema', () => { it('requires an array of numeric ids', () => { - expect(todoReorderRequestSchema.safeParse({ orderedIds: [1, 2, 3] }).success).toBe(true); - expect(todoReorderRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false); + expect( + todoReorderRequestSchema.safeParse({ orderedIds: [1, 2, 3] }).success, + ).toBe(true); + expect( + todoReorderRequestSchema.safeParse({ orderedIds: ['a'] }).success, + ).toBe(false); }); }); diff --git a/shared/src/todo/todo.schema.ts b/shared/src/todo/todo.schema.ts index 2519adb7..dab8bef4 100644 --- a/shared/src/todo/todo.schema.ts +++ b/shared/src/todo/todo.schema.ts @@ -39,4 +39,6 @@ export type TodoReorderRequest = z.infer; export const todoCategoryAssigneesRequestSchema = z.object({ user_ids: z.array(z.number()), }); -export type TodoCategoryAssigneesRequest = z.infer; +export type TodoCategoryAssigneesRequest = z.infer< + typeof todoCategoryAssigneesRequestSchema +>; diff --git a/shared/src/trip/trip.schema.spec.ts b/shared/src/trip/trip.schema.spec.ts index fdca8559..84378005 100644 --- a/shared/src/trip/trip.schema.spec.ts +++ b/shared/src/trip/trip.schema.spec.ts @@ -1,14 +1,23 @@ -import { describe, it, expect } from 'vitest'; import { tripCreateRequestSchema, tripUpdateRequestSchema, tripAddMemberRequestSchema, } from './trip.schema'; +import { describe, it, expect } from 'vitest'; + describe('tripCreateRequestSchema', () => { it('requires a title; dates/currency/reminder optional', () => { - expect(tripCreateRequestSchema.safeParse({ title: 'Japan' }).success).toBe(true); - expect(tripCreateRequestSchema.safeParse({ title: 'Japan', start_date: '2026-07-01', day_count: 7 }).success).toBe(true); + expect(tripCreateRequestSchema.safeParse({ title: 'Japan' }).success).toBe( + true, + ); + expect( + tripCreateRequestSchema.safeParse({ + title: 'Japan', + start_date: '2026-07-01', + day_count: 7, + }).success, + ).toBe(true); expect(tripCreateRequestSchema.safeParse({}).success).toBe(false); }); }); @@ -16,13 +25,18 @@ describe('tripCreateRequestSchema', () => { describe('tripUpdateRequestSchema', () => { it('is fully partial and accepts is_archived + cover_image', () => { expect(tripUpdateRequestSchema.safeParse({}).success).toBe(true); - expect(tripUpdateRequestSchema.safeParse({ is_archived: 1, cover_image: null }).success).toBe(true); + expect( + tripUpdateRequestSchema.safeParse({ is_archived: 1, cover_image: null }) + .success, + ).toBe(true); }); }); describe('tripAddMemberRequestSchema', () => { it('requires an identifier', () => { - expect(tripAddMemberRequestSchema.safeParse({ identifier: 'bob@x.y' }).success).toBe(true); + expect( + tripAddMemberRequestSchema.safeParse({ identifier: 'bob@x.y' }).success, + ).toBe(true); expect(tripAddMemberRequestSchema.safeParse({}).success).toBe(false); }); }); diff --git a/shared/src/vacay/vacay.schema.spec.ts b/shared/src/vacay/vacay.schema.spec.ts index c329e25b..6393c4ad 100644 --- a/shared/src/vacay/vacay.schema.spec.ts +++ b/shared/src/vacay/vacay.schema.spec.ts @@ -1,4 +1,3 @@ -import { describe, it, expect } from 'vitest'; import { vacayAddHolidayCalendarRequestSchema, vacayInviteRequestSchema, @@ -6,34 +5,61 @@ import { vacayAddYearRequestSchema, } from './vacay.schema'; +import { describe, it, expect } from 'vitest'; + describe('vacayAddHolidayCalendarRequestSchema', () => { it('requires a region; label/color/sort_order optional', () => { - expect(vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY' }).success).toBe(true); - expect(vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY', label: null }).success).toBe(true); - expect(vacayAddHolidayCalendarRequestSchema.safeParse({}).success).toBe(false); + expect( + vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY' }) + .success, + ).toBe(true); + expect( + vacayAddHolidayCalendarRequestSchema.safeParse({ + region: 'DE-BY', + label: null, + }).success, + ).toBe(true); + expect(vacayAddHolidayCalendarRequestSchema.safeParse({}).success).toBe( + false, + ); }); }); describe('vacayInviteRequestSchema', () => { it('accepts a numeric or string user_id', () => { - expect(vacayInviteRequestSchema.safeParse({ user_id: 2 }).success).toBe(true); - expect(vacayInviteRequestSchema.safeParse({ user_id: '2' }).success).toBe(true); + expect(vacayInviteRequestSchema.safeParse({ user_id: 2 }).success).toBe( + true, + ); + expect(vacayInviteRequestSchema.safeParse({ user_id: '2' }).success).toBe( + true, + ); expect(vacayInviteRequestSchema.safeParse({}).success).toBe(false); }); }); describe('vacayToggleEntryRequestSchema', () => { it('requires a date; target_user_id optional', () => { - expect(vacayToggleEntryRequestSchema.safeParse({ date: '2026-07-01' }).success).toBe(true); - expect(vacayToggleEntryRequestSchema.safeParse({ date: '2026-07-01', target_user_id: 3 }).success).toBe(true); + expect( + vacayToggleEntryRequestSchema.safeParse({ date: '2026-07-01' }).success, + ).toBe(true); + expect( + vacayToggleEntryRequestSchema.safeParse({ + date: '2026-07-01', + target_user_id: 3, + }).success, + ).toBe(true); expect(vacayToggleEntryRequestSchema.safeParse({}).success).toBe(false); }); }); describe('vacayAddYearRequestSchema', () => { it('accepts a numeric or string year', () => { - expect(vacayAddYearRequestSchema.safeParse({ year: 2027 }).success).toBe(true); - expect(vacayAddYearRequestSchema.safeParse({ year: '2027' }).success).toBe(true); + expect(vacayAddYearRequestSchema.safeParse({ year: 2027 }).success).toBe( + true, + ); + expect(vacayAddYearRequestSchema.safeParse({ year: '2027' }).success).toBe( + true, + ); expect(vacayAddYearRequestSchema.safeParse({}).success).toBe(false); }); }); diff --git a/shared/src/vacay/vacay.schema.ts b/shared/src/vacay/vacay.schema.ts index 0d3fb7f3..1f5d3b41 100644 --- a/shared/src/vacay/vacay.schema.ts +++ b/shared/src/vacay/vacay.schema.ts @@ -22,7 +22,9 @@ export const vacayAddHolidayCalendarRequestSchema = z.object({ color: z.string().optional(), sort_order: z.number().optional(), }); -export type VacayAddHolidayCalendarRequest = z.infer; +export type VacayAddHolidayCalendarRequest = z.infer< + typeof vacayAddHolidayCalendarRequestSchema +>; export const vacaySetColorRequestSchema = z.object({ color: z.string().optional(), @@ -38,7 +40,9 @@ export type VacayInviteRequest = z.infer; export const vacayInviteActionRequestSchema = z.object({ plan_id: z.number().optional(), }); -export type VacayInviteActionRequest = z.infer; +export type VacayInviteActionRequest = z.infer< + typeof vacayInviteActionRequestSchema +>; export const vacayAddYearRequestSchema = z.object({ year: z.union([z.number(), z.string()]), @@ -49,19 +53,25 @@ export const vacayToggleEntryRequestSchema = z.object({ date: z.string().min(1), target_user_id: z.union([z.number(), z.string()]).optional(), }); -export type VacayToggleEntryRequest = z.infer; +export type VacayToggleEntryRequest = z.infer< + typeof vacayToggleEntryRequestSchema +>; export const vacayCompanyHolidayRequestSchema = z.object({ date: z.string(), note: z.string().optional(), }); -export type VacayCompanyHolidayRequest = z.infer; +export type VacayCompanyHolidayRequest = z.infer< + typeof vacayCompanyHolidayRequestSchema +>; export const vacayUpdateStatsRequestSchema = z.object({ vacation_days: z.number().optional(), target_user_id: z.union([z.number(), z.string()]).optional(), }); -export type VacayUpdateStatsRequest = z.infer; +export type VacayUpdateStatsRequest = z.infer< + typeof vacayUpdateStatsRequestSchema +>; /** Plan / entries / stats payloads are wide and DB-derived; kept open. */ export const vacayPlanDataSchema = open;