style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
## Naming Patterns
|
||||
|
||||
**Files:**
|
||||
|
||||
- Backend route handlers: `camelCase.ts` — `events.ts`, `me.ts`, `health.ts` (`apps/api/src/routes/`)
|
||||
- Broker modules: `camelCase.ts` — `poller.ts`, `sync.ts`, `write.ts`, `expand.ts` (`apps/api/src/broker/`)
|
||||
- Frontend components: `PascalCase.tsx` — `EventForm.tsx`, `CalendarShell.tsx`, `InstallPrompt.tsx` (`apps/pwa/src/components/`)
|
||||
@@ -12,12 +13,14 @@
|
||||
- Tests: `{filename}.test.ts` or `.test.tsx` co-located with source
|
||||
|
||||
**Functions:**
|
||||
|
||||
- Private helpers (not exported): `camelCase` — `claimStr()`, `getBreakpointGroup()`, `viewStorageKey()`, `resolveUserId()`
|
||||
- Exported async handlers: `camelCase` — `fetchMe()`, `createEvent()`, `expandOccurrences()`, `upsertUser()`
|
||||
- React hooks (Zustand): `useCalendarStore`, `useXxxx` pattern — follows React convention
|
||||
- Type guard / coercion functions: `camelCase` — `deriveDisplayName()`, `claimStr()`
|
||||
|
||||
**Variables:**
|
||||
|
||||
- Constants (module-level): `SCREAMING_SNAKE_CASE` — `MAX_WINDOW_DAYS`, `SHARED_FAMILY_COLOR`, `COLOR_PALETTE`, `FIXTURES`
|
||||
- Local state: `camelCase` — `currentUserId`, `targetCalendarUrl`, `windowStartDate`, `eventRow`
|
||||
- Zustand store methods: `camelCase` setters — `setSelectedView()`, `setEventForm()`, `setLastSyncedUid()`
|
||||
@@ -26,6 +29,7 @@
|
||||
- Database column mappings: `snake_case` in schema → `camelCase` in TypeScript (Drizzle handles mapping)
|
||||
|
||||
**Types/Interfaces:**
|
||||
|
||||
- TypeScript interfaces: `PascalCase` — `MeUser`, `MeResponse`, `CalendarOccurrence`, `WritableCalendar`, `CalendarStore`, `SyncStatus`
|
||||
- Zod schemas: `camelCase` + `Schema` suffix — `eventsQuerySchema`, `eventFieldsSchema`, `syncStatusQuerySchema`
|
||||
- Union types (enums): `PascalCase` or quoted literals in types — `'create' | 'update' | 'delete'`, `'pending' | 'done' | 'failed' | 'dead'`
|
||||
@@ -33,6 +37,7 @@
|
||||
- DB column names: `snake_case` — `dtstart_utc`, `dtstart_date`, `oidc_iss`, `oidc_sub`
|
||||
|
||||
**Drizzle ORM tables:**
|
||||
|
||||
- Table function: `mysqlTable('table_name', {...})`
|
||||
- Column names in schema def: use snake_case strings — `int('user_id')`, `varchar('oidc_iss', ...)`
|
||||
- TypeScript field names (destructured queries): auto-convert to camelCase via Drizzle's default mode
|
||||
@@ -44,6 +49,7 @@
|
||||
## Code Style
|
||||
|
||||
**Formatting:**
|
||||
|
||||
- No explicit ESLint or Prettier config files in the codebase (uses project defaults)
|
||||
- 2-space indentation (inferred from source code)
|
||||
- Single quotes for strings (`'string'`, not `"string"`)
|
||||
@@ -51,21 +57,23 @@
|
||||
- No trailing commas in function calls; trailing commas in object/array literals (modern style)
|
||||
|
||||
**Linting:**
|
||||
|
||||
- TypeScript: `strict: true` in both backend and frontend `tsconfig.json`
|
||||
- Module resolution: `NodeNext` (backend), `Bundler` (frontend)
|
||||
- No `any` types — use `Context` from Hono where typing is available
|
||||
|
||||
**Example formatting (from `routes/events.ts` line 64):**
|
||||
|
||||
```typescript
|
||||
async function resolveUserId(c: Context): Promise<number | null> {
|
||||
const devUser = c.get('user') as { id: number } | undefined
|
||||
if (devUser) return devUser.id
|
||||
const devUser = c.get('user') as { id: number } | undefined;
|
||||
if (devUser) return devUser.id;
|
||||
|
||||
const auth = await getAuth(c)
|
||||
if (!auth) return null
|
||||
const auth = await getAuth(c);
|
||||
if (!auth) return null;
|
||||
|
||||
const iss = (auth.iss as string | undefined) ?? ''
|
||||
const sub = auth.sub ?? ''
|
||||
const iss = (auth.iss as string | undefined) ?? '';
|
||||
const sub = auth.sub ?? '';
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -73,6 +81,7 @@ async function resolveUserId(c: Context): Promise<number | null> {
|
||||
## Import Organization
|
||||
|
||||
**Order:**
|
||||
|
||||
1. Node.js built-ins (`import { ... } from 'node:...'`)
|
||||
2. Third-party packages (`import { ... } from 'hono'`, `import { ... } from 'drizzle-orm'`)
|
||||
3. Local absolute imports (backend: none; frontend: none visible — no path aliases configured)
|
||||
@@ -80,10 +89,12 @@ async function resolveUserId(c: Context): Promise<number | null> {
|
||||
5. Side-effect imports (import without destructuring, placed last) — `import '../auth/devBypass.js'`
|
||||
|
||||
**Path extensions:**
|
||||
|
||||
- All imports use explicit `.js` extensions — `from './index.js'`, `from '../db/client.js'`
|
||||
- Applies to both backend and frontend (ESM module resolution)
|
||||
|
||||
**Example (from `routes/events.ts` lines 24–38):**
|
||||
|
||||
```typescript
|
||||
import { randomUUID } from 'node:crypto' // Node.js built-in
|
||||
import { Hono } from 'hono' // Third-party
|
||||
@@ -105,6 +116,7 @@ import '../auth/devBypass.js' // Side-effect import (last)
|
||||
**Patterns:**
|
||||
|
||||
**Backend (Hono routes):**
|
||||
|
||||
- Early return with typed `c.json(...)` on validation or auth failure — `return c.json({ error: 'message' }, statusCode)`
|
||||
- Try-catch blocks wrap DB/external I/O, catch logs error + returns 503 Service Unavailable
|
||||
- No unhandled rejections — every async operation has explicit error handling
|
||||
@@ -112,6 +124,7 @@ import '../auth/devBypass.js' // Side-effect import (last)
|
||||
- Validation failures: return 400 Bad Request with error envelope
|
||||
|
||||
**Example (from `routes/events.ts` lines 126–225):**
|
||||
|
||||
```typescript
|
||||
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
||||
const currentUserId = await resolveUserId(c)
|
||||
@@ -136,28 +149,30 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
||||
```
|
||||
|
||||
**Frontend (React + TanStack Query):**
|
||||
|
||||
- Fetch client throws on non-ok response; caller handles redirect logic (`maybeRedirectToLogin()`)
|
||||
- API client checks `res.type === 'opaqueredirect'` and `res.status === 401` to detect auth failure (CORS-safe 302 handling)
|
||||
- Component state via Zustand; server state via React Query
|
||||
- No inline try-catch in components — defer to query error states
|
||||
|
||||
**Example (from `api/client.ts` lines 28–53):**
|
||||
|
||||
```typescript
|
||||
export async function fetchMe(): Promise<MeResponse> {
|
||||
const res = await fetch('/api/me', {
|
||||
credentials: 'include',
|
||||
redirect: 'manual',
|
||||
})
|
||||
});
|
||||
|
||||
if (res.type === 'opaqueredirect' || res.status === 401) {
|
||||
throw new Error('GET /api/me: authentication required')
|
||||
throw new Error('GET /api/me: authentication required');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET /api/me failed: ${res.status}`)
|
||||
throw new Error(`GET /api/me failed: ${res.status}`);
|
||||
}
|
||||
|
||||
return res.json() as Promise<MeResponse>
|
||||
return res.json() as Promise<MeResponse>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -166,6 +181,7 @@ export async function fetchMe(): Promise<MeResponse> {
|
||||
**Framework:** Console methods only (`console.log`, `console.error`, `console.warn`)
|
||||
|
||||
**Patterns:**
|
||||
|
||||
- Errors logged with context prefix in square brackets — `console.error('[events]', message)`, `console.error('[broker/sync]', message)`
|
||||
- Startup messages logged at info level — `console.log('FamilySync API running on ...')`
|
||||
- Dev-mode warnings prefixed with warning emoji-ish symbol — `console.warn('⚠ DEV_AUTH_BYPASS active ...')`
|
||||
@@ -173,19 +189,21 @@ export async function fetchMe(): Promise<MeResponse> {
|
||||
- Errors include the full exception object for stack trace — `console.error('[events] DB query failed:', err)`
|
||||
|
||||
**Example (from `index.ts` lines 23, 111):**
|
||||
|
||||
```typescript
|
||||
if (devBypassActive) {
|
||||
console.warn('⚠ DEV_AUTH_BYPASS active — OIDC guard DISABLED. Never use in production.')
|
||||
console.warn('⚠ DEV_AUTH_BYPASS active — OIDC guard DISABLED. Never use in production.');
|
||||
}
|
||||
// ...
|
||||
serve({ fetch: app.fetch, port: 3000 }, (info) => {
|
||||
console.log(`FamilySync API running on http://localhost:${info.port}`)
|
||||
})
|
||||
console.log(`FamilySync API running on http://localhost:${info.port}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
**When to Comment:**
|
||||
|
||||
- Complex algorithms or non-obvious business logic — e.g., window date filtering in `routes/events.ts` (lines 142–151)
|
||||
- Security assertions or threat-model references — e.g., ownership checks (T-03-06), CSRF-token patterns
|
||||
- Architectural invariants — e.g., "broker boundary: this route reads ONLY from cache" (routes/events.ts:4)
|
||||
@@ -193,11 +211,13 @@ serve({ fetch: app.fetch, port: 3000 }, (info) => {
|
||||
- Workarounds and why they exist — e.g., "WR-04: carrier/groupId for edit-as-move txn" (routes/events.ts:373)
|
||||
|
||||
**JSDoc/TSDoc:**
|
||||
|
||||
- Used for public exported functions, not for every function
|
||||
- Single-line for simple functions; multi-line with `@param` and `@returns` for complex signatures
|
||||
- Comments on types (interfaces) to document contract — e.g., `CalendarOccurrence` interface (api/client.ts:71–87)
|
||||
|
||||
**Example (from `auth/user.ts` lines 25–32):**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Accessible, visually-distinct palette for per-member color assignment.
|
||||
@@ -217,16 +237,19 @@ export const COLOR_PALETTE: string[] = [...]
|
||||
**Size:** Prefer short, single-responsibility functions. Route handlers are the exception — they bundle validation, ownership check, and response assembly (pragmatism for Hono idiom).
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- Use Hono's `Context` type rather than destructuring everything — `async (c: Context)`
|
||||
- Explicit parameters for helper functions; Hono context passed implicitly where possible
|
||||
- Zod validators return typed objects via `c.req.valid('json')` or `c.req.valid('query')`
|
||||
|
||||
**Return Values:**
|
||||
|
||||
- Async functions return typed values or throw — `Promise<T>` or `Promise<void>`
|
||||
- Error responses returned explicitly (not thrown) — callers handle 4xx/5xx in same try-catch
|
||||
- Database queries return typed Drizzle result objects; destructure as needed
|
||||
|
||||
**Example (from `auth/user.ts` lines 79–142):**
|
||||
|
||||
```typescript
|
||||
export async function upsertUser(
|
||||
oidcIss: string,
|
||||
@@ -253,22 +276,26 @@ export async function upsertUser(
|
||||
## Module Design
|
||||
|
||||
**Exports:**
|
||||
|
||||
- Named exports for functions and types — `export const TABLE`, `export function handler()`, `export interface Type`
|
||||
- No default exports (exception: SPA app shell `App.tsx` uses default export)
|
||||
- Re-export from middleware modules for convenience — `auth/middleware.ts` re-exports `@hono/oidc-auth` functions
|
||||
|
||||
**Barrel Files:**
|
||||
|
||||
- No wildcard re-exports (`export * from ...`) — explicit named exports only
|
||||
- Top-level index files not used (each module imported directly)
|
||||
|
||||
**Example (from `auth/middleware.ts` lines 24–26):**
|
||||
|
||||
```typescript
|
||||
export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-auth'
|
||||
export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-auth';
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
**Drizzle conventions (critical):**
|
||||
|
||||
- Schema definition: `mysqlTable('name', { id: int().primaryKey().autoincrement(), ... }, (t) => [...])`
|
||||
- Foreign keys: ALWAYS include `{ onDelete: 'cascade' }` to propagate deletes cleanly
|
||||
- Indexes: Explicit index names with `idx_` prefix on frequently filtered columns
|
||||
@@ -276,12 +303,14 @@ export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-au
|
||||
- Never use `db:push` on populated MariaDB (false destructive diffs) — ALWAYS use `generate + migrate`
|
||||
|
||||
**Query patterns:**
|
||||
|
||||
- Use Drizzle's type-safe query builder: `db.select(...).from(table).where(...).limit(...)`
|
||||
- Raw SQL via `` sql`...` `` for complex predicates (e.g., multi-condition OR chains in events.ts:167–201)
|
||||
- Parameterized values via `sql` template tag prevent SQL injection
|
||||
- Joins: explicitly `innerJoin()` or `leftJoin()` with `.on(eq(...))` conditions
|
||||
|
||||
**Example (from `db/schema.ts` lines 96–123):**
|
||||
|
||||
```typescript
|
||||
export const calendarEvents = mysqlTable(
|
||||
'calendar_events',
|
||||
@@ -298,24 +327,27 @@ export const calendarEvents = mysqlTable(
|
||||
index('idx_calendar_events_has_rrule').on(t.hasRrule),
|
||||
unique('uniq_calendar_uid').on(t.calendarId, t.uid),
|
||||
],
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Reactive State (Frontend)
|
||||
|
||||
**TanStack Query (Server State):**
|
||||
|
||||
- All calendar events, lists, user profile live in React Query
|
||||
- Queries keyed by API endpoint + windowing params — `['events', { start, end }]`
|
||||
- Mutations handle POST/PATCH/DELETE; invalidate cache on success
|
||||
- Use `useQuery` for reads, `useMutation` for writes; never mix server state into Zustand
|
||||
|
||||
**Zustand (UI State):**
|
||||
|
||||
- Owns only UI-shape state: `selectedView`, `openEventId`, `eventFormOpen`, `deleteDialogOpen`, etc.
|
||||
- Persists breakpoint-scoped `selectedView` to `localStorage`
|
||||
- Never store server data (user profile, events) — keep it in React Query
|
||||
- Setters are synchronous; no side effects (except localStorage in `setSelectedView`)
|
||||
|
||||
**Example (from `store/calendarStore.ts` lines 1–25):**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Zustand UI-state store for the calendar shell.
|
||||
@@ -327,4 +359,4 @@ export const calendarEvents = mysqlTable(
|
||||
|
||||
---
|
||||
|
||||
*Convention analysis: 2026-06-09*
|
||||
_Convention analysis: 2026-06-09_
|
||||
|
||||
Reference in New Issue
Block a user