diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..2de52f85 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL=postgresql://perd:perd@db.localtest.me:5432/perd +LOCAL_DATABASE=1 +SESSION_SECRET=a165925602027f51ecd748965fd7d494 diff --git a/.github/skills/code-style/SKILL.md b/.github/skills/code-style/SKILL.md deleted file mode 100644 index bf55b02d..00000000 --- a/.github/skills/code-style/SKILL.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: code-style -description: Code style patterns including function declarations, async/await patterns, and error handling for both client and server. Use when writing or reviewing code, implementing functions, handling errors, formatting code, or when user mentions style, patterns, async, await, error handling, try/catch, comparisons, or naming conventions. -license: Unlicense ---- - -# Code Style - -Core code style patterns for consistent codebase. - -## Quick Reference - -- **Functions**: See [functions.md](references/functions.md) - named functions over arrow functions for declarations -- **Async/Await**: See [async.md](references/async.md) - always async/await, never .then() -- **Error Handling**: See [errors.md](references/errors.md) - try/catch/finally with type guards -- **Comparisons**: See [comparisons.md](references/comparisons.md) - explicit comparisons, no implicit coercion -- **Formatting**: See [formatting.md](references/formatting.md) - code formatting rules (Drizzle schemas, Vue files, spacing) -- **Naming**: See [naming.md](references/naming.md) - no abbreviations, descriptive names - -## Core Principles - -- Named functions for exports and declarations -- Arrow functions only for inline callbacks -- async/await always, never `.then()` -- try/catch/finally for proper cleanup -- Type guards for client-side errors -- createError for server-side errors -- Explicit return types on exported functions -- Explicit comparisons (`=== undefined`, `=== null`, `=== false` `!==`) over implicit (`!value`) diff --git a/.github/skills/code-style/references/async.md b/.github/skills/code-style/references/async.md deleted file mode 100644 index 8e5bbcaf..00000000 --- a/.github/skills/code-style/references/async.md +++ /dev/null @@ -1,86 +0,0 @@ -# Async/Await Patterns - -**Always use async/await, never `.then()` chains.** - -## Client-side Async Operations - -```typescript -// ✅ Preferred pattern with try/catch/finally -async function deleteChecklist() { - try { - isDeleting.value = true - - await $fetch(`/api/checklists/${checklistId}`, { method: 'DELETE' }) - - navigateTo('/checklists') - } catch (error) { - console.error(error) - // Handle error (see Error Handling section) - } finally { - isDeleting.value = false - } -} - -// ✅ Multiple sequential operations -async function loadData() { - const user = await $fetch('/api/user') - const equipment = await $fetch(`/api/users/${user.id}/equipment`) - - return { user, equipment } -} - -// ✅ Parallel operations with Promise.all -async function loadMultiple() { - const [brands, types] = await Promise.all([ - $fetch('/api/brands'), - $fetch('/api/equipment-types') - ]) - - return { brands, types } -} -``` - -## Server-side Async Operations - -```typescript -// ✅ API route handlers -export default defineEventHandler(async (event) => { - const { db } = event.context - - const items = await db - .select() - .from(tables.items) - .where(eq(tables.items.userId, userId)) - - return items -}) - -// ✅ Database transactions -async function createWithRelation() { - await db.transaction(async (tx) => { - const [parent] = await tx.insert(tables.parents).values({ name }).returning() - - await tx.insert(tables.children).values({ parentId: parent.id }) - }) -} -``` - -## Anti-pattern: .then() Chains - -```typescript -// ❌ Never use .then() chains -$fetch('/api/endpoint') - .then((data) => processData(data)) - .then(() => navigateTo('/success')) - .catch((error) => console.error(error)) - -// ✅ Use async/await instead -try { - const data = await $fetch('/api/endpoint') - - processData(data) - navigateTo('/success') -} catch (error) { - console.error(error) -} -``` diff --git a/.github/skills/code-style/references/comparisons.md b/.github/skills/code-style/references/comparisons.md deleted file mode 100644 index c930b93a..00000000 --- a/.github/skills/code-style/references/comparisons.md +++ /dev/null @@ -1,120 +0,0 @@ -# Explicit Comparisons - -**Always use explicit comparisons** instead of implicit boolean coercion to avoid type conversion issues. - -## Explicit vs Implicit Comparisons - -```typescript -// ❌ Avoid implicit boolean coercion -if (!value) { } -if (!user) { } -if (!items.length) { } - -// ✅ Use explicit comparisons -if (value === undefined) { } -if (value === null) { } -if (user === undefined) { } -if (items.length === 0) { } -``` - -## Checking for null/undefined - -```typescript -// ❌ Avoid negation -if (!user) { - return null -} - -// ✅ Explicit check -if (user === undefined || user === null) { - return null -} - -// ✅ Or if only checking undefined -if (user === undefined) { - return null -} -``` - -## Checking boolean values - -```typescript -// ❌ Implicit -if (!isActive) { } -if (!!flag) { } - -// ✅ Explicit -if (isActive === false) { } -if (flag === true) { } -``` - -## Array length checks - -```typescript -// ❌ Implicit -if (!items.length) { - return [] -} - -// ✅ Explicit -if (items.length === 0) { - return [] -} -``` - -## Optional values - -```typescript -// ❌ Implicit negation -const brand = await findBrand(id) - -if (!brand) { - throw createError({ status: 404 }) -} - -// ✅ Explicit undefined check -const brand = await findBrand(id) - -if (brand === undefined) { - throw createError({ status: 404 }) -} -``` - -## Inequality checks - -```typescript -// ❌ Using negation -if (!(index === -1)) { } - -// ✅ Use !== operator -if (index !== -1) { } - -// ✅ Ownership check -if (resource.creatorId !== userId) { - throw createError({ status: 403 }) -} -``` - -## Special cases - -```typescript -// ✅ For NaN checks -if (isNaN(value) === false) { } - -// ✅ For null checks in object properties -if (value !== null && typeof value === 'object') { } - -// ✅ For array find results -const item = items.find(item => item.id === id) - -if (item === undefined) { - console.log('Not found') -} -``` - -## Why Explicit Comparisons? - -1. **No type coercion** - Avoids JavaScript's implicit conversions (0, '', [], etc.) -2. **Clear intent** - Obvious what value you're checking for -3. **Type safety** - Works better with TypeScript's strict mode -4. **Predictable** - No surprises with falsy values diff --git a/.github/skills/code-style/references/errors.md b/.github/skills/code-style/references/errors.md deleted file mode 100644 index feee6ead..00000000 --- a/.github/skills/code-style/references/errors.md +++ /dev/null @@ -1,92 +0,0 @@ -# Error Handling - -## Client-side Error Handling - -```typescript -// ✅ Basic try/catch -try { - await $fetch('/api/endpoint') -} catch (error) { - console.error(error) -} - -// ✅ Type-safe error handling with type guards -try { - await $fetch('/api/endpoint') -} catch (error) { - console.error(error) - - // Check for FetchError - if (error instanceof FetchError) { - return error.data.message - } - - // Check for specific error types - if (isRecord(error) && 'statusCode' in error) { - if (error.statusCode === 404) { - navigateTo('/not-found') - } - } -} - -// ✅ Using composable for toast notifications -const { showErrorToast } = useApiErrorToast() - -try { - await $fetch('/api/endpoint') -} catch (error) { - showErrorToast(error, 'Failed to load data') -} -``` - -## Server-side Error Handling - -```typescript -// ✅ Use h3's createError -export default defineEventHandler(async (event) => { - const item = await findItem(id) - - if (item === undefined) { - throw createError({ - status: 404, - message: `Item with ID ${id} not found` - }) - } - - return item -}) - -// ✅ Guard clauses for authentication -export default defineEventHandler(async (event) => { - const userId = event.context.session?.userId - - if (userId === undefined) { - throw createError({ status: 401 }) - } - - // Continue with authenticated logic -}) - -// ✅ Validation errors with specific messages -if (name.length > 100) { - throw createError({ - status: 400, - message: 'Name must not exceed 100 characters' - }) -} - -// ✅ Authorization checks -if (resource.creatorId !== userId) { - throw createError({ - status: 403, - message: 'You do not have permission to modify this resource' - }) -} -``` - -## Key Principles - -- Use try/catch/finally for proper cleanup -- Type guards for client-side error handling -- createError for server-side errors -- Guard clauses early in functions diff --git a/.github/skills/code-style/references/formatting.md b/.github/skills/code-style/references/formatting.md deleted file mode 100644 index ebefb419..00000000 --- a/.github/skills/code-style/references/formatting.md +++ /dev/null @@ -1,99 +0,0 @@ -# Code Formatting - -General formatting rules for consistent code style across all file types. - -## Code Block Spacing - -Proper spacing between code blocks improves readability. - -### Variable Declarations - -Single-line declarations grouped together with blank line after: - -```typescript -// ✅ Correct - grouped single-line declarations -const name = 'John' -const age = 30 -const isActive = true - -// Blank line after group -console.log(name) -``` - -Multi-line declarations separated by blank lines: - -```typescript -// ✅ Correct - multi-line declarations separated -const user = { - name: 'John', - age: 30 -} - -const settings = { - theme: 'dark', - language: 'en' -} - -// Function after blank line -function process() { - // ... -} -``` - -### Between Blocks - -All code blocks separated by blank lines: - -```typescript -// ✅ Correct spacing -function fetchData() { - const url = '/api/data' - const options = { method: 'GET' } - - return fetch(url, options) -} - -function processData(data: unknown) { - if (data === null || data === undefined) { - return null - } - - return transform(data) -} - -const result = await fetchData() -``` - -### Inside Blocks - -```typescript -// ✅ Correct - blank lines inside blocks -async function handleSubmit() { - // Group declarations - const name = input.value - const email = emailInput.value - - // Blank line before block - if (name === '') { - showError('Name required') - return - } - - // Blank line between blocks - try { - await saveUser({ name, email }) - - showSuccess() - } catch (error) { - showError(error) - } -} -``` - -### Key Rules - -1. **Single-line declarations**: Group together, blank line after group -2. **Multi-line declarations**: Blank line before (unless block start) and after (unless block end) -3. **Between functions**: Always blank line -4. **Between blocks** (if, try, for, etc.): Always blank line -5. **Inside blocks**: Blank line to separate logical groups diff --git a/.github/skills/code-style/references/functions.md b/.github/skills/code-style/references/functions.md deleted file mode 100644 index fdf218d7..00000000 --- a/.github/skills/code-style/references/functions.md +++ /dev/null @@ -1,53 +0,0 @@ -# Function Declarations - -**Prefer named functions over arrow functions** for exports and declarations. -**Always use explicit comparisons** instead of implicit boolean coercion. - -## Named Functions (Preferred) - -```typescript -// ✅ Preferred for composables -export default function useApiErrorToast() { - function showErrorToast(error: unknown, title: string) { - // Implementation - } - - return { showErrorToast } -} - -// ✅ Preferred for standalone functions -export function formatDate(date: Date) : string { - return date.toISOString() -} - -// ✅ Preferred for utilities -export function calculateTotal(items: number[]) : number { - return items.reduce((sum, item) => sum + item, 0) -} -``` - -## Arrow Functions (Avoid for Declarations) - -```typescript -// ❌ Avoid for exports and named declarations -const showErrorToast = (error: unknown) => { - // ... -} - -// ❌ Avoid for composable exports -export default () => { - const toast = useToaster() - - return { toast } -} -``` - -## Arrow Functions (OK for Callbacks) - -```typescript -// ✅ OK for inline callbacks -items.map((item) => ({ id: item.id, name: item.name })) - -// ✅ OK for array methods -const active = items.filter((item) => item.active) -``` diff --git a/.github/skills/code-style/references/naming.md b/.github/skills/code-style/references/naming.md deleted file mode 100644 index d20e6552..00000000 --- a/.github/skills/code-style/references/naming.md +++ /dev/null @@ -1,82 +0,0 @@ -# Naming Conventions - -## No Abbreviations - -Always use full, descriptive names. Avoid abbreviations. - -### ❌ Wrong - -```typescript -// Single-letter or abbreviated names -const e = new Error() -const v = getValue() -const val = input.value -const i = items[0] -const err = handleError() -``` - -### ✅ Correct - -```typescript -// Full descriptive names -const error = new Error() -const value = getValue() -const itemValue = input.value -const item = items[0] -const error = handleError() -``` - -## Array Methods - -Use descriptive names in array methods: - -```typescript -// ❌ Wrong -items.findIndex((i) => i.id === id) - -// ✅ Correct -items.findIndex((item) => item.id === id) -``` - -## Function Parameters - -Event handlers and callbacks should use descriptive parameter names: - -```typescript -// ❌ Wrong -function handleInput(e: Event) { - const target = e.target -} - -// ✅ Correct -function handleInput(event: Event) { - const target = event.target -} -``` - -## Catch Block Variables - -Use descriptive names in catch blocks: - -```typescript -// ❌ Wrong -try { - await fetchData() -} catch (e) { - console.error(e) -} - -// ✅ Correct -try { - await fetchData() -} catch (error) { - console.error(error) -} - -// Or with shadowing prevention -try { - await fetchData() -} catch (caughtError) { - console.error(caughtError) -} -``` diff --git a/.github/skills/database/SKILL.md b/.github/skills/database/SKILL.md deleted file mode 100644 index 1819162f..00000000 --- a/.github/skills/database/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: database -description: Database patterns using Drizzle ORM with PostgreSQL. Includes schema definitions, relations, migrations workflow, and querying patterns in API routes. Use when working with database schema, queries, migrations, debugging database issues, or when user mentions Drizzle, PostgreSQL, tables, relations, migrations, schema, SQL, database queries, transactions, DB operations, database errors, migration failures, or connection issues. -license: Unlicense ---- - -# Database Patterns - -Drizzle ORM with PostgreSQL (Neon serverless). All database code lives in `server/database/`. - -## Quick Reference - -- **Schema**: See [schema-patterns.md](references/schema-patterns.md) for table definitions, foreign keys, enums, and JSON fields -- **Formatting**: See [formatting.md](references/formatting.md) for Drizzle schema formatting rules -- **Relations**: See [relations.md](references/relations.md) for one-to-many and many-to-one patterns -- **Queries**: See [queries.md](references/queries.md) for select, insert, update, delete, and transactions -- **Migrations**: See [migrations.md](references/migrations.md) for workflow and commands - -## Core Principles - -- Always use indexes on foreign keys -- ULID primary keys with `ulid()` helper -- Timestamps with timezone and auto-update -- Use `db` from `event.context` in API routes -- Validate with Valibot before DB operations -- Guard clauses for auth checks -- Transactions for multi-table operations diff --git a/.github/skills/database/references/formatting.md b/.github/skills/database/references/formatting.md deleted file mode 100644 index f8647b41..00000000 --- a/.github/skills/database/references/formatting.md +++ /dev/null @@ -1,88 +0,0 @@ -# Drizzle Schema Formatting - -Each table field on separate lines with proper indentation and line breaks between method calls. - -## Field Declaration Pattern - -```typescript -export const users = pgTable('users', { - id: - ulid() - .notNull() - .default(sql`gen_ulid()`) - .primaryKey(), - - name: varchar({ - length: limits.maxUserNameLength - }), - - email: - varchar({ - length: 255 - }) - .notNull() - .unique(), - - createdAt: - timestamp({ - withTimezone: true - }) - .notNull() - .defaultNow() -}) -``` - -## Formatting Rules - -1. **Long chains**: Break after field name, each method on new line with indentation -2. **Simple fields**: Single-line if no chaining (e.g., `name: varchar({ length: 100 })`) -3. **Object params**: Multi-line if complex, single-line if simple -4. **Blank lines**: Between fields for readability -5. **Constraints**: Index definitions after closing brace in callback - -## Why This Format - -- Clear visual separation between fields -- Easy to scan and understand field definitions -- Consistent with project schema patterns -- Better diffs when modifying fields - -## Examples - -### Simple Field - -```typescript -name: varchar({ - length: 100 -}) -``` - -### Field with Chaining - -```typescript -email: - varchar({ - length: 255 - }) - .notNull() - .unique() -``` - -### Indexes - -```typescript -export const users = pgTable('users', { - id: ulid() - .notNull() - .primaryKey(), - - email: - varchar({ length: 255 }) - .notNull(), - - name: varchar({ length: 100 }) -}, (table) => [ - index().on(table.email), - index().on(table.name) -]) -``` diff --git a/.github/skills/database/references/migrations.md b/.github/skills/database/references/migrations.md deleted file mode 100644 index c47685ab..00000000 --- a/.github/skills/database/references/migrations.md +++ /dev/null @@ -1,37 +0,0 @@ -# Migrations Workflow - -## Standard Workflow - -1. **Modify schema** in `server/database/schema.ts` -2. **Generate migration**: `pnpm db:generate` -3. **Review migration** in `server/database/migrations/` -4. **Apply locally**: `pnpm db:migrate:local` -5. **Commit migration files** to git -6. **Deploy**: Migrations run automatically via GitHub Actions - -## Commands - -```bash -# Generate migration from schema changes -pnpm db:generate - -# Apply migrations locally (uses .env) -pnpm db:migrate:local - -# Apply migrations to production/staging -pnpm db:migrate - -# Open Drizzle Studio (GUI) -pnpm db:studio - -# Seed database -pnpm db:seed:local # Local -pnpm db:seed # Production/staging -``` - -## Important Notes - -- Always review generated migrations before committing -- Migrations run automatically on deployment -- Use `pnpm db:studio` to inspect database state -- Seed scripts are in `tools/seed.ts` diff --git a/.github/skills/database/references/queries.md b/.github/skills/database/references/queries.md deleted file mode 100644 index 80208874..00000000 --- a/.github/skills/database/references/queries.md +++ /dev/null @@ -1,240 +0,0 @@ -# Database Queries - -Use the `db` instance from `event.context` (injected by middleware) in API routes. - -Import `tables` for query builder or use `db.query` for relational queries. - -```typescript -import { tables } from '#server/utils/database' -``` - -## Simple Queries (Relational API) - -For simple queries without complex joins, use `db.query` relational API: - -```typescript -export default defineEventHandler(async (event) => { - const { db } = event.context - const brandId = getRouterParam(event, 'brandId') - - // Find first (returns single record or undefined) - const brand = await db.query.brands.findFirst({ - where: { - id: Number(brandId) - } - }) - - if (brand === undefined) { - throw createError({ - status: 404, - message: `Brand with ID ${brandId} not found` - }) - } - - return brand -}) -``` - -```typescript -// Find many (returns array) -export default defineEventHandler(async (event) => { - const { db } = event.context - const userId = event.context.session?.userId - - if (userId === undefined) { - throw createError({ status: 401 }) - } - - const equipment = await db.query.equipment.findMany({ - where: { - creatorId: userId - }, - - orderBy: { createdAt: 'desc' }, - limit: 50, - - columns: { - id: true, - name: true, - createdAt: true - }, - - with: { - brand: { - columns: { - id: true, - name: true - } - } - } - }) - - return equipment -}) -``` - -## Query Builder (for complex queries) - -For complex queries with custom joins, aggregations, or specific SQL needs, use query builder: - -```typescript -import { eq, and, desc } from 'drizzle-orm' -import { tables } from '#server/utils/database' - -export default defineEventHandler(async (event) => { - const { db } = event.context - - // Complex join with custom selection - const result = await db - .select({ - equipmentId: tables.equipment.id, - equipmentName: tables.equipment.name, - brandName: tables.brands.name, - typeName: tables.equipmentTypes.name - }) - .from(tables.equipment) - .leftJoin( - tables.brands, - eq(tables.equipment.brandId, tables.brands.id)) - .leftJoin( - tables.equipmentTypes, - eq(tables.equipment.equipmentTypeId, tables.equipmentTypes.id)) - .where( - and( - eq(tables.equipment.creatorId, userId), - eq(tables.brands.verified, true) - ) - ) - .orderBy(desc(tables.equipment.createdAt)) - .limit(50) - - return result -}) -``` - -## Insert Operations - -```typescript -export default defineEventHandler(async (event) => { - const { db } = event.context - const body = await readValidatedBody(event, bodySchema) - const userId = event.context.session?.userId - - if (userId === undefined) { - throw createError({ status: 401 }) - } - - const [newEquipment] = await db - .insert(tables.equipment) - .values({ - name: body.name, - brandId: body.brandId, - creatorId: userId - }) - .returning() - - return newEquipment -}) -``` - -## Update Operations - -```typescript -export default defineEventHandler(async (event) => { - const { db } = event.context - const equipmentId = getRouterParam(event, 'id') - const body = await readValidatedBody(event, bodySchema) - - const [updated] = await db - .update(tables.equipment) - .set({ - name: body.name, - updatedAt: new Date() - }) - .where( - eq(tables.equipment.id, equipmentId) - ) - .returning() - - if (updated === undefined) { - throw createError({ status: 404 }) - } - - return updated -}) -``` - -## Delete Operations - -```typescript -export default defineEventHandler(async (event) => { - const { db } = event.context - const brandId = getRouterParam(event, 'brandId') - const userId = event.context.session?.userId - - // Check ownership before delete - const [brand] = await db - .select() - .from(tables.brands) - .where( - and( - eq(tables.brands.id, Number(brandId)), - eq(tables.brands.creatorId, userId) - ) - ) - - if (brand === undefined) { - throw createError({ status: 404 }) - } - - await db - .delete(tables.brands) - .where(eq(tables.brands.id, Number(brandId))) - - return { success: true } -}) -``` - -## Transactions - -```typescript -export default defineEventHandler(async (event) => { - const { db } = event.context - const body = await readValidatedBody(event, bodySchema) - - await db.transaction(async (tx) => { - // Insert parent - const [checklist] = await tx - .insert(tables.checklists) - .values({ - name: body.name, - userId: body.userId - }) - .returning() - - // Insert children - await tx - .insert(tables.checklistItems) - .values( - body.items.map((item) => ({ - checklistId: checklist.id, - name: item.name - })) - ) - }) - - return { success: true } -}) -``` - -## Best Practices - -- **Prefer `db.query` API** for simple queries with relations - cleaner syntax and better DX -- **Use query builder** for complex joins, aggregations, or custom SQL -- Always check for empty results and throw `createError` -- Guard clauses for authentication/authorization before queries -- Use `findFirst()` when expecting single result (adds LIMIT 1) -- Use `findMany()` with explicit `limit` for lists -- Specify `columns` to select only needed fields -- Use `with` to include relations instead of manual joins -- Use transactions for multi-table operations that must succeed/fail together diff --git a/.github/skills/database/references/relations.md b/.github/skills/database/references/relations.md deleted file mode 100644 index 522738cd..00000000 --- a/.github/skills/database/references/relations.md +++ /dev/null @@ -1,51 +0,0 @@ -# Database Relations - -Define relations in `server/database/relations.ts` using Drizzle relations API. - -**Official Docs**: [Drizzle Relations Schema Declaration](https://orm.drizzle.team/docs/relations-schema-declaration) - -## One-to-Many Relations - -```typescript -import { defineRelations } from 'drizzle-orm' -import * as schema from './schema' - -export const relations = defineRelations(schema, (r) => ({ - users: { - equipment: r.many.equipment({ - from: r.users.id, - to: r.equipment.creatorId - }), - - checklists: r.many.checklists({ - from: r.users.id, - to: r.checklists.userId - }) - }, - - brands: { - equipment: r.many.equipment({ - from: r.brands.id, - to: r.equipment.brandId - }) - } -})) -``` - -## Many-to-One Relations - -```typescript -export const relations = defineRelations(schema, (r) => ({ - equipment: { - creator: r.one.users({ - from: r.equipment.creatorId, - to: r.users.id - }), - - brand: r.one.brands({ - from: r.equipment.brandId, - to: r.brands.id - }) - } -})) -``` diff --git a/.github/skills/database/references/schema-patterns.md b/.github/skills/database/references/schema-patterns.md deleted file mode 100644 index 28bd1643..00000000 --- a/.github/skills/database/references/schema-patterns.md +++ /dev/null @@ -1,151 +0,0 @@ -# Database Schema Patterns - -Define schemas in `server/database/schema.ts`. Import tables in API routes from `#server/utils/database`. - -**Official Docs**: [Drizzle SQL Schema Declaration](https://orm.drizzle.team/docs/sql-schema-declaration) - -## Basic Table Schema - -```typescript -import { pgTable, varchar, timestamp, index } from 'drizzle-orm/pg-core' -import { sql } from 'drizzle-orm' -import { ulid } from '#server/utils/ulid' -import { limits } from '~~/constants' - -export const users = pgTable('users', { - id: - ulid() - .notNull() - .default(sql`gen_ulid()`) - .primaryKey(), - - name: varchar({ - length: limits.maxUserNameLength - }), - - email: - varchar({ - length: 255 - }) - .notNull() - .unique(), - - createdAt: - timestamp({ - withTimezone: true - }) - .notNull() - .defaultNow(), - - updatedAt: - timestamp({ - withTimezone: true - }) - .notNull() - .defaultNow() - .$onUpdate(() => sql`now()`) -}, (table) => [ - index().on(table.name), - index().on(table.email) -]) -``` - -## Table with Foreign Keys - -```typescript -export const equipment = pgTable('equipment', { - id: - ulid() - .notNull() - .default(sql`gen_ulid()`) - .primaryKey(), - - name: - varchar({ - length: limits.maxEquipmentNameLength - }) - .notNull(), - - brandId: - integer() - .references(() => brands.id, { - onDelete: 'set null' - }), - - creatorId: - ulid() - .notNull() - .references(() => users.id, { - onDelete: 'cascade' - }), - - createdAt: - timestamp({ - withTimezone: true - }) - .notNull() - .defaultNow(), - - updatedAt: - timestamp({ - withTimezone: true - }) - .notNull() - .defaultNow() - .$onUpdate(() => sql`now()`) -}, (table) => [ - index().on(table.brandId), - index().on(table.creatorId) -]) -``` - -## Table with Enums and JSON - -```typescript -import { pgEnum, jsonb } from 'drizzle-orm/pg-core' - -export const equipmentStatusEnum = pgEnum('equipment_status', [ - 'active', - 'retired', - 'lost' -]) - -export const equipment = pgTable('equipment', { - id: - ulid() - .notNull() - .default(sql`gen_ulid()`) - .primaryKey(), - - name: - varchar({ - length: 100 - }) - .notNull(), - - status: - equipmentStatusEnum() - .notNull() - .default('active'), - - metadata: jsonb().$type<{ - tags: string[]; - notes: string; - }>(), - - createdAt: - timestamp({ - withTimezone: true - }) - .notNull() - .defaultNow() -}) -``` - -## Key Principles - -- Always use indexes on foreign keys -- Use `timestamp({ withTimezone: true })` for dates -- Auto-update timestamps with `.$onUpdate(() => sql`now()`)` -- ULID for primary keys via `ulid()` helper -- Define length limits from `constants.ts` diff --git a/.github/skills/typescript-patterns/SKILL.md b/.github/skills/typescript-patterns/SKILL.md deleted file mode 100644 index 2af612be..00000000 --- a/.github/skills/typescript-patterns/SKILL.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: typescript-patterns -description: TypeScript patterns including path aliases, strict typing, type guards, and validation patterns. Use when working with TypeScript code, imports, interfaces, type definitions, or when user mentions types, interfaces, type guards, validation, Valibot, imports, path aliases, type safety, or type assertions. -license: Unlicense ---- - -# TypeScript Patterns - -TypeScript patterns and strict typing rules. - -## Quick Reference - -- **Imports**: See [imports.md](references/imports.md) for path aliases (~/@, ~~/, #shared, #server) -- **Types**: See [types.md](references/types.md) for interfaces and when to use interface vs type -- **Type Guards**: See [guards.md](references/guards.md) for type predicates and runtime type checking -- **Type Assertions**: See [type-assertions.md](references/type-assertions.md) for avoiding `as` keyword -- **Validation**: See [validation.md](references/validation.md) for Valibot validation patterns - -## Strict Typing Rules - -- **NO `any` types** - always use proper types or `unknown` -- **Explicit return types** on exported functions -- **Use `unknown`** for uncertain types, then narrow with type guards -- **Import types separately**: `import type { ... }` when only importing types diff --git a/.github/skills/typescript-patterns/references/guards.md b/.github/skills/typescript-patterns/references/guards.md deleted file mode 100644 index a5254df9..00000000 --- a/.github/skills/typescript-patterns/references/guards.md +++ /dev/null @@ -1,111 +0,0 @@ -# Type Guards - -Type guards enable runtime type checking and type narrowing in TypeScript. - -## Type Predicates - -```typescript -// ✅ Custom type guards for common checks -export function isRecord(value: unknown) : value is Record { - return typeof value === 'object' && value !== null -} - -export function isString(value: unknown) : value is string { - return typeof value === 'string' -} - -export function isNumber(value: unknown) : value is number { - return typeof value === 'number' && isNaN(value) === false -} -``` - -## Built-in Type Guards - -```typescript -// ✅ instanceof for class/DOM instances -function handleInput(event: Event) { - const target = event.target - - if (target instanceof HTMLInputElement) { - console.log(target.value) // TypeScript knows this is HTMLInputElement - } -} - -// ✅ typeof for primitives -function processValue(value: unknown) { - if (typeof value === 'string') { - console.log(value.toUpperCase()) - } - - if (typeof value === 'number') { - console.log(value.toFixed(2)) - } -} - -// ✅ in operator for property checks -function handleResponse(response: unknown) { - if ( - typeof response === 'object' && - response !== null && - 'data' in response - ) { - console.log(response.data) - } -} -``` - -## Custom Type Guards for Models - -```typescript -// ✅ Type guard for API responses -function isBrandResponse(data: unknown): data is BrandModel { - return ( - typeof data === 'object' && - data !== null && - 'id' in data && - 'name' in data && - typeof data.id === 'number' && - typeof data.name === 'string' - ) -} - -// Usage -const response = await $fetch('/api/brands/1') - -if (isBrandResponse(response)) { - console.log(response.name) // Safely typed as BrandModel -} -``` - -## Type Narrowing - -```typescript -// ✅ Narrowing with null/undefined checks -function getDisplayName(name: string | null | undefined) : string { - if (name === null || name === undefined) { - return 'Unknown' - } - - return name // TypeScript knows this is string -} - -// ✅ Array.isArray for arrays -function processItems(data: unknown) { - if (Array.isArray(data)) { - data.forEach((item) => console.log(item)) // TypeScript knows this is array - } -} -``` - -## Best Practices - -- Use type guards to narrow `unknown` types safely -- Prefer built-in type guards (`instanceof`, `typeof`, `in`) when possible -- Create custom type predicates for complex model validation -- Combine multiple checks for thorough validation -- Type guards help narrow `unknown` to specific types - -## When to Use Type Guards vs Validation - -- **Type guards**: Quick runtime type checks, client-side narrowing -- **Valibot validation**: Complex server-side validation with detailed errors (see validation.md) diff --git a/.github/skills/typescript-patterns/references/imports.md b/.github/skills/typescript-patterns/references/imports.md deleted file mode 100644 index 75264da9..00000000 --- a/.github/skills/typescript-patterns/references/imports.md +++ /dev/null @@ -1,34 +0,0 @@ -# TypeScript Path Aliases - -Use these import path aliases throughout the project: - -- `~` or `~/` or `@/` - App directory (`/app/`) -- `~~/` - Project root -- `#shared` - Shared code directory (`/shared/`) -- `#server` - Server code directory (`/server/`) - -## Import Examples - -```typescript -// Components (~ and @ are equivalent) -import PageContent from '~/components/layout/PageContent.vue' -import FidgetSpinner from '@/components/FidgetSpinner.vue' - -// Root constants -import { limits } from '~~/constants' - -// Models -import type { ChecklistItemModel } from "~/models/checklist" - -// Shared utilities -import { someSharedUtil } from '#shared/utils' - -// Server-side code -import { db } from '#server/database/connection' -``` - -## Best Practices - -- Always use `import type { ... }` when only importing types -- Use aliases consistently across the codebase -- Prefer shorter aliases (`~` over `~/`) when both work diff --git a/.github/skills/typescript-patterns/references/type-assertions.md b/.github/skills/typescript-patterns/references/type-assertions.md deleted file mode 100644 index 422f31eb..00000000 --- a/.github/skills/typescript-patterns/references/type-assertions.md +++ /dev/null @@ -1,82 +0,0 @@ -# Type Assertions - -## No Type Assertions (`as`) - -Type assertions with `as` bypass TypeScript's type checking and should be avoided. - -### ❌ Wrong - -```typescript -// Using 'as' type casting -function handleInput(event: Event) { - const target = event.target as HTMLInputElement - - console.log(target.value) -} - -// Forcing type conversion -const data = response as UserData - -// Casting to any then to specific type -const element = document.getElementById('id') as any as CustomElement -``` - -### ✅ Correct - -Use type guards and runtime checks instead: - -```typescript -// Using instanceof type guard -function handleInput(event: Event) { - const target = event.target - - if (target instanceof HTMLInputElement) { - console.log(target.value) // TypeScript knows this is HTMLInputElement - } -} - -// Using custom type guard -function isUserData(data: unknown) : data is UserData { - return ( - typeof data === 'object' && - data !== null && - 'id' in data && - 'name' in data - ) -} - -const response = await fetch('/api/user') -const data = await response.json() - -if (isUserData(data)) { - console.log(data.name) // TypeScript knows this is UserData -} - -// Using element type checks -const element = document.getElementById('myInput') - -if (element instanceof HTMLInputElement) { - element.value = 'new value' -} -``` - -### Exception: Const Assertions - -The only acceptable use of `as` is for const assertions: - -```typescript -// ✅ Correct - const assertion -const config = { - status: 'active', - priority: 'high' -} as const - -// This makes the object deeply readonly and literal types -``` - -## Why Avoid `as` - -1. **Bypasses type checking**: Compiler trusts you blindly -2. **Runtime errors**: No guarantee the assertion is correct -3. **Maintenance issues**: Hard to track when types change -4. **Better alternatives**: Type guards provide runtime safety diff --git a/.github/skills/typescript-patterns/references/types.md b/.github/skills/typescript-patterns/references/types.md deleted file mode 100644 index dd252ec6..00000000 --- a/.github/skills/typescript-patterns/references/types.md +++ /dev/null @@ -1,61 +0,0 @@ -# TypeScript Types & Interfaces - -## Models - -```typescript -// ✅ Preferred for data models -export interface BrandModel { - id: number; - name: string; - websiteUrl: string | null; -} - -export interface ChecklistItemModel { - id: string; - name: string; - checked: boolean; - createdAt: Date; -} -``` - -## Component Props - -```typescript -// ✅ Component props -interface Props { - item: ChecklistItemModel; - checkMode: boolean; -} - -const { checkMode, item } = defineProps() -``` - -## When to Use Interface vs Type - -- **Interface**: For object shapes, especially models and component props -- **Type**: For unions, intersections, or mapped types - -## Examples - -```typescript -// ✅ Interface for object shapes -interface UserModel { - id: string; - email: string; -} - -// ✅ Type for unions -type Status = 'active' | 'retired' | 'lost' - -// ✅ Type for intersections -type UserWithRole = UserModel & { role: string } - -// ✅ Type for mapped types -type Nullable = { [K in keyof T]: T[K] | null } -``` - -## Best Practices - -- Use const assertions for literal types: `as const` -- Prefer union types over enums when possible -- Use branded types for IDs if needed (e.g., `type BrandId = string & { __brand: 'BrandId' }`) diff --git a/.github/skills/typescript-patterns/references/validation.md b/.github/skills/typescript-patterns/references/validation.md deleted file mode 100644 index 233ba1e9..00000000 --- a/.github/skills/typescript-patterns/references/validation.md +++ /dev/null @@ -1,123 +0,0 @@ -# Valibot Validation - -Use Valibot for server-side validation with detailed error messages. - -## Request Body Validation - -```typescript -import * as v from 'valibot' -import { limits } from '~~/constants' - -// ✅ Define schema -const bodySchema = v.object({ - name: v.pipe( - v.string(), - v.nonEmpty(), - v.maxLength(limits.maxBrandNameLength) - ), - - websiteUrl: v.optional( - v.pipe( - v.string(), - v.url() - ) - ), - - description: v.optional( - v.string() - ) -}) - -// ✅ Extract validator function -function validateBody(body: unknown) { - return v.parse(bodySchema, body) -} - -// ✅ Use in event handler -export default defineEventHandler(async (event) => { - const { name, websiteUrl } = await readValidatedBody(event, validateBody) - // ... -}) -``` - -## Query Parameter Validation - -```typescript -import * as v from 'valibot' - -const querySchema = v.object({ - page: v.optional( - v.pipe( - v.string(), - v.transform(Number), - v.number(), - v.minValue(1) - ) - ), - - limit: v.optional( - v.pipe( - v.string(), - v.transform(Number), - v.number(), - v.minValue(1), - v.maxValue(100) - ) - ) -}) - -function validateQuery(query: unknown) { - return v.parse(querySchema, query) -} - -export default defineEventHandler(async (event) => { - const { page, limit } = await getValidatedQuery(event, validateQuery) - // ... -}) -``` - -## Complex Validation - -```typescript -import * as v from 'valibot' - -// Define schema with nested objects -const BrandSchema = v.object({ - id: v.pipe(v.number(), v.integer(), v.minValue(1)), - name: v.pipe(v.string(), v.nonEmpty(), v.maxLength(100)), - website: v.optional(v.pipe(v.string(), v.url())), - createdAt: v.pipe(v.string(), v.isoTimestamp()) -}) - -// Validator function with safeParse for detailed errors -function validateBrand(data: unknown) { - return v.safeParse(BrandSchema, data) -} - -// Usage -const response = await $fetch('/api/brands/1') -const result = validateBrand(response) - -if (result.success) { - console.log(result.output.name) // Fully typed and validated -} else { - console.error('Validation failed:', result.issues) -} -``` - -## Best Practices - -- Use Valibot for server-side validation only -- Extract validator function separately (don't inline `v.parse`) -- Use `v.nonEmpty()` instead of `v.minLength(1)` for clarity -- Use `v.pipe()` for chaining validations -- Reference `limits` from constants for max lengths -- Never trust input data - always validate - -## When to Use Valibot - -- Complex nested objects -- Multiple fields with constraints (length, range, format) -- Need detailed error messages -- API response validation -- User input validation on server diff --git a/.github/skills/vue-patterns/SKILL.md b/.github/skills/vue-patterns/SKILL.md deleted file mode 100644 index baece713..00000000 --- a/.github/skills/vue-patterns/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: vue-patterns -description: Vue and Nuxt patterns including script setup with TypeScript, CSS modules, component props, and composable patterns. Use when creating or modifying Vue components, composables, Nuxt features, or when user mentions Vue, Nuxt, components, props, emits, composables, CSS modules, v-model, reactivity, or styling. -license: Unlicense ---- - -# Vue/Nuxt Patterns - -Vue 3 and Nuxt 4 patterns with TypeScript. - -## Quick Reference - -- **Components**: See [components.md](references/components.md) for props, emits, composables usage, and v-model -- **Formatting**: See [formatting.md](references/formatting.md) for Vue SFC formatting rules -- **Styling**: See [styling.md](references/styling.md) for CSS modules, dynamic classes, and SCSS utilities -- **Composables**: See [composables.md](references/composables.md) for state management and store patterns - -## Core Principles - -- Always use ` -``` - -## Component Emits - -Multiple events with different signatures: - -```vue - -``` - -Multiple events with same signature: - -```vue - -``` - -## Using Composables in Components - -```vue - -``` - -## v-model Pattern - -```vue - - - -``` diff --git a/.github/skills/vue-patterns/references/composables.md b/.github/skills/vue-patterns/references/composables.md deleted file mode 100644 index a565e763..00000000 --- a/.github/skills/vue-patterns/references/composables.md +++ /dev/null @@ -1,85 +0,0 @@ -# Composables - -## Basic Composable Structure - -```typescript -// composables/use-brands.ts -import type { BrandModel } from '~/models/brand' - -export default function useBrands() { - const brands = useState('brands', () => []) - const isLoading = ref(false) - const error = ref(null) - - async function fetchBrands() { - try { - isLoading.value = true - error.value = null - - const data = await $fetch('/api/brands') - - brands.value = data - } catch (caughtError) { - if (caughtError instanceof Error) { - error.value = caughtError - } - - console.error(caughtError) - } finally { - isLoading.value = false - } - } - - return { - brands, - isLoading, - error, - fetchBrands - } -} -``` - -## Composable with Store Pattern - -```typescript -// composables/use-checklist-store.ts -import type { ChecklistItemModel } from '~/models/checklist' - -export default function useChecklistStore() { - const items = useState('checklist-items', () => []) - - function addItem(item: ChecklistItemModel) { - items.value.push(item) - } - - function removeItem(id: string) { - const index = items.value.findIndex((item) => item.id === id) - - if (index !== -1) { - items.value.splice(index, 1) - } - } - - function updateItem(id: string, updates: Partial) { - const index = items.value.findIndex((item) => item.id === id) - - if (index !== -1) { - items.value[index] = { ...items.value[index], ...updates } - } - } - - return { - items: readonly(items), - addItem, - removeItem, - updateItem - } -} -``` - -## Best Practices - -- Named functions (not arrow functions) for composable exports -- Use `useState` for cross-component state -- Use `readonly()` from Vue for runtime immutability when needed -- Explicit return types on exported functions diff --git a/.github/skills/vue-patterns/references/formatting.md b/.github/skills/vue-patterns/references/formatting.md deleted file mode 100644 index ae994342..00000000 --- a/.github/skills/vue-patterns/references/formatting.md +++ /dev/null @@ -1,50 +0,0 @@ -# Vue Files Formatting - -Vue single-file components follow strict indentation rules. - -## Block Order and Indentation - -```vue - - - - - -``` - -## Formatting Rules - -1. **Block order**: ` - - diff --git a/app/components/PerdPaginator.vue b/app/components/PerdPaginator.vue deleted file mode 100644 index 8c1b7f10..00000000 --- a/app/components/PerdPaginator.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - diff --git a/app/components/PerdSearch/DefaultOption.vue b/app/components/PerdSearch/DefaultOption.vue deleted file mode 100644 index 8232ddcf..00000000 --- a/app/components/PerdSearch/DefaultOption.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/app/components/PerdSearch/EmptyOption.vue b/app/components/PerdSearch/EmptyOption.vue deleted file mode 100644 index c141f4e6..00000000 --- a/app/components/PerdSearch/EmptyOption.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/app/components/PerdSearch/PerdSearch.vue b/app/components/PerdSearch/PerdSearch.vue deleted file mode 100644 index 8488e65e..00000000 --- a/app/components/PerdSearch/PerdSearch.vue +++ /dev/null @@ -1,292 +0,0 @@ - - - - - diff --git a/app/components/PerdSearch/SearchOptionAdd.vue b/app/components/PerdSearch/SearchOptionAdd.vue deleted file mode 100644 index 95e10547..00000000 --- a/app/components/PerdSearch/SearchOptionAdd.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/app/components/PerdSelect.vue b/app/components/PerdSelect.vue deleted file mode 100644 index 0cb6118e..00000000 --- a/app/components/PerdSelect.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - - - diff --git a/app/components/PerdSidebar/PerdSidebar.vue b/app/components/PerdSidebar/PerdSidebar.vue index c591c1a7..763c6755 100644 --- a/app/components/PerdSidebar/PerdSidebar.vue +++ b/app/components/PerdSidebar/PerdSidebar.vue @@ -8,49 +8,12 @@ />
-
- - Inventory - - - - Checklists - - - - Equipment database - - - - Brands - - - - Equipment manager - -
+