diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7273ae5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Rules + +AGENTS.md \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b513262 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/EXAMPLE-AGENTS.md b/EXAMPLE-AGENTS.md new file mode 100644 index 0000000..36b36a8 --- /dev/null +++ b/EXAMPLE-AGENTS.md @@ -0,0 +1,1251 @@ +# Appwrite Development Rules + +> You are an expert developer focused on building apps with Appwrite's JavaScript/TypeScript SDK. + +## Overview + +This file provides AI coding assistants with Appwrite-specific development instructions, best practices, and code patterns for the JavaScript/TypeScript SDK with nextjs. + +## SDK Installation + +Install the Appwrite Node.js Server SDK using npm: + +```bash +npm install node-appwrite +``` + +You can also use yarn, pnpm, or bun instead. + +**Framework Documentation:** +- [Next.js App Router Docs](https://nextjs.org/docs/app) +- [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/nextjs) + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications + +**SSR Authentication Pattern:** + +Server-side rendering requires using the Server SDK instead of the client SDK. + +**Authentication Flow:** +1. User credentials are sent from browser to your server +2. Your server authenticates with Appwrite using the Server SDK +3. Appwrite returns a session object +4. Store the session secret in an httpOnly cookie +5. Subsequent requests include the session cookie +6. Your server makes authenticated requests on behalf of the user + +**Key Implementation Details:** + +**Initialize Two Clients:** +- **Admin Client**: Uses API key for unauthenticated requests and session creation +- **Session Client**: Uses session cookie for user-specific requests + +**Best Practices:** +- Use httpOnly, secure, and sameSite cookie flags +- Create new session client per request +- Never share clients between requests +- Use API key for admin client to bypass rate limits +- Set forwarded user agent for better session tracking + +**See full SSR auth guide:** https://appwrite.io/docs/products/auth/server-side-rendering + +**Creating Sessions:** +```javascript +import { Client, Account } from "node-appwrite"; + +// In your login endpoint: +const account = new Account(adminClient); +const session = await account.createEmailPasswordSession(email, password); + +// Set httpOnly cookie with session secret +res.cookie('a_session_', session.secret, { + httpOnly: true, + secure: true, + sameSite: 'strict', + expires: new Date(session.expire), + path: '/' +}); +``` + +**Making Authenticated Requests:** +```javascript +// Read session from cookie +const session = req.cookies['a_session_']; + +// Create session client +const sessionClient = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setSession(session); + +const account = new Account(sessionClient); +const user = await account.get(); +``` + +**OAuth2 Flow:** +1. Redirect to OAuth provider using createOAuth2Token +2. Handle callback with userId and secret parameters +3. Call createSession to exchange for session object +4. Store session secret in cookie + + +## 🚨 Absolute Rules (Non-Negotiable) + +### Authoritative Access Pattern + +- **NEVER** import or invoke the Appwrite SDK directly from feature/component code +- **ALWAYS** use centralized wrapper functions for all data access +- **ALWAYS** authenticate before any data operation +- **NEVER** expose API keys to client-side code + +--- + +## Error Handling + +- Let errors bubble by default for consistent error handling +- Catch only when adding context or performing cleanup +- Always rethrow with clear error messages +- Never swallow errors silently + +--- + +## Type Safety + +- Avoid untyped/dynamic types where possible +- Define models/interfaces/structs for all data structures +- Use your language's type system to enforce constraints +- Validate inputs with appropriate validation libraries for your language + + + +## Next.js Server Action Pattern + +### Mandatory Structure (App Router) + +Every server action must: +1. Be marked with `'use server'` +2. Authenticate immediately +3. Validate input +4. Use centralized db/storage helpers only +5. Return plain serializable objects + +### Canonical Example + +```typescript +// app/actions/items.ts +'use server' + +import { auth } from '@/lib/auth' +import { db } from '@/lib/db' + +export async function createItem(formData: FormData) { + // 1. Authenticate first + const session = await auth() + if (!session?.user) throw new Error('Unauthorized') + + // 2. Validate input + const title = formData.get('title')?.toString() + const teamId = formData.get('teamId')?.toString() + + if (!title || title.length === 0 || title.length > 120) { + throw new Error('Invalid title') + } + + // 3. Use centralized db helper + const item = await db.items.create({ + title: title.trim(), + description: null, + createdBy: session.user.id, + teamId: teamId ?? null, + }) + + // 4. Revalidate and return + revalidatePath('/items') + return { item } +} +``` + +### Route Handler Pattern + +```typescript +// app/api/items/route.ts +import { NextResponse } from 'next/server' +import { auth } from '@/lib/auth' +import { db } from '@/lib/db' + +export async function GET() { + const session = await auth() + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const items = await db.items.listByOwner(session.user.id) + return NextResponse.json({ items }) +} + +export async function POST(request: Request) { + const session = await auth() + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const item = await db.items.create({ + ...body, + createdBy: session.user.id, + }) + + return NextResponse.json({ item }, { status: 201 }) +} +``` + +### Server Component Data Fetching + +```typescript +// app/items/page.tsx +import { auth } from '@/lib/auth' +import { db } from '@/lib/db' + +export default async function ItemsPage() { + const session = await auth() + if (!session?.user) redirect('/login') + + const items = await db.items.listByOwner(session.user.id) + + return +} +``` + +### Environment Variables + +Required in `.env.local`: +- `APPWRITE_ENDPOINT` +- `APPWRITE_PROJECT_ID` +- `APPWRITE_API_KEY` +- `APPWRITE_DATABASE_ID` +- `APPWRITE_BUCKET_ID` (if using storage) + + + +## Database Implementation Rules + +### Database Wrapper Requirements + +Create centralized database helpers that: +- Configure the admin client with proper credentials +- Handle project, database, and table IDs +- Manage permissions automatically +- Return typed/structured objects (never raw Appwrite rows) + +- **NEVER** use TablesDB SDK directly for app data +- **ALWAYS** use centralized wrapper functions for database access + +--- + +## Ownership Enforcement (Critical) + +### User-Owned Entities (`createdBy`) + +Every user-owned table must include a `createdBy` column. + +| Operation | Rule | +|-----------|------| +| **Create** | Set `createdBy` to authenticated user's `$id` | +| **List** | Filter with `Query.equal('createdBy', [userId])` | +| **Read** | Verify ownership before returning data | +| **Update** | Confirm ownership; NEVER allow `createdBy` to change | +| **Delete** | Confirm ownership before deletion | + +### Team-Owned Entities (`teamId`) + +For shared workspaces and organization data: + +| Operation | Rule | +|-----------|------| +| **Create** | Set `teamId`; verify user is team member | +| **List** | Filter with `Query.equal('teamId', [teamId])` | +| **Read** | Verify team match AND user membership | +| **Update** | Confirm membership; NEVER allow `teamId` to change | +| **Delete** | Confirm membership before deletion | + +--- + +## Database Usage Rules + +- Import database helpers from centralized location only +- Operate on **tables**, expect **rows** +- Create payloads: exclude system columns (`$id`, `$createdAt`, `$updatedAt`) +- Update payloads: partial data, exclude system and ownership columns +- NEVER modify: `$id`, `$createdAt`, `$updatedAt`, `createdBy`, `teamId` +- Use `Query.equal()` for ownership filtering +- Return serialized/structured objects only (no raw SDK responses) + +--- + +## Sanitization & Payload Rules + +- Trim all user-provided strings +- Convert empty optional text to `null` (or language equivalent) +- Creates: require full payload (minus system fields) +- Updates: accept partial payload +- Ownership fields (`createdBy`, `teamId`) are IMMUTABLE after creation + + + +## Database Wrapper Template + +Create a centralized database helper at your designated location (e.g., `lib/db.ts` or `server/lib/db.ts`): + +```typescript +import { Client, TablesDB, Query, ID } from 'node-appwrite' + +// Initialize admin client (server-side only) +const client = new Client() + .setEndpoint(process.env.APPWRITE_ENDPOINT!) + .setProject(process.env.APPWRITE_PROJECT_ID!) + .setKey(process.env.APPWRITE_API_KEY!) + +const tablesDB = new TablesDB(client) +const DATABASE_ID = process.env.APPWRITE_DATABASE_ID! + +// Generic CRUD helper factory +function createTable(tableId: string) { + return { + async create(data: Omit) { + const doc = await tablesDB.createRow( + DATABASE_ID, + tableId, + ID.unique(), + data + ) + return doc as T + }, + + async get(id: string) { + try { + const doc = await tablesDB.getRow(DATABASE_ID, tableId, id) + return doc as T + } catch { + return null + } + }, + + async listByOwner(userId: string) { + const response = await tablesDB.listRows(DATABASE_ID, tableId, [ + Query.equal('createdBy', [userId]), + Query.orderDesc('$createdAt'), + ]) + return response.rows as T[] + }, + + async listByTeam(teamId: string) { + const response = await tablesDB.listRows(DATABASE_ID, tableId, [ + Query.equal('teamId', [teamId]), + Query.orderDesc('$createdAt'), + ]) + return response.rows as T[] + }, + + async update(id: string, data: Partial) { + // Remove immutable columns + const { $id, $createdAt, $updatedAt, createdBy, teamId, ...updateData } = data as any + const doc = await tablesDB.updateRow(DATABASE_ID, tableId, id, updateData) + return doc as T + }, + + async delete(id: string) { + await tablesDB.deleteRow(DATABASE_ID, tableId, id) + }, + } +} + +// Export typed tables +export const db = { + items: createTable('items'), + projects: createTable('projects'), + // Add more tables as needed +} +``` + + + +## Storage Implementation Rules + +### Storage Wrapper Requirements + +Create centralized storage helpers that: +- Initialize storage with proper bucket configuration +- Handle file upload/download conversions +- Manage file permissions +- Return only file IDs or data URLs (never raw binary data to client) + +- **NEVER** use Storage SDK directly for app data +- **ALWAYS** use centralized wrapper functions for storage access + +--- + +## Storage Usage Rules + +### Upload Flow (Mandatory Conversion) + +``` +Client → Server: base64 string (or file bytes) +Server: Decode base64 → bytes/buffer → InputFile +Server → Storage: InputFile +Storage → Server: File metadata +Server → Client: file ID only +``` + +Rules: +- Strip `data:...;base64,` prefix before processing +- Decode base64 to bytes using your language's standard library +- Use the SDK's InputFile helper for uploads +- NEVER store base64 strings in database +- Store **file IDs only** in database fields + +### Read Flow + +``` +Server: Fetch file from storage (binary/bytes) +Server: Convert bytes → base64 → data URL +Server → Client: data URL string (or secure download URL) +``` + +- NEVER expose raw binary data to client +- Always return URL strings or base64 data URLs + +--- + +## File References in Rows + +- Store file references as string arrays (`fileIds` field) +- On row load, fetch file URLs and inject into response +- Handle missing files gracefully +- Implement orphaned file cleanup when rows are deleted + + + +## Storage Wrapper Template + +Create a centralized storage helper: + +```typescript +import { Client, Storage, ID } from 'node-appwrite' +import { InputFile } from 'node-appwrite/file' + +const client = new Client() + .setEndpoint(process.env.APPWRITE_ENDPOINT!) + .setProject(process.env.APPWRITE_PROJECT_ID!) + .setKey(process.env.APPWRITE_API_KEY!) + +const storage = new Storage(client) +const BUCKET_ID = process.env.APPWRITE_BUCKET_ID! + +export const fileStorage = { + /** + * Upload file from base64 string + * @returns File ID only (never store base64 in database) + */ + async upload(base64Data: string, fileName: string, mimeType: string) { + // Strip data URL prefix if present + const base64Clean = base64Data.replace(/^data:[^;]+;base64,/, '') + + // Convert to buffer + const buffer = Buffer.from(base64Clean, 'base64') + + // Create InputFile + const inputFile = InputFile.fromBuffer(buffer, fileName) + + // Upload to storage + const file = await storage.createFile(BUCKET_ID, ID.unique(), inputFile) + + return file.$id // Return ID only + }, + + /** + * Get file as data URL for client consumption + */ + async getAsDataUrl(fileId: string, mimeType: string) { + const arrayBuffer = await storage.getFileDownload(BUCKET_ID, fileId) + const base64 = Buffer.from(arrayBuffer).toString('base64') + return `data:${mimeType};base64,${base64}` + }, + + /** + * Get file preview URL + */ + getPreviewUrl(fileId: string, width?: number, height?: number) { + return storage.getFilePreview(BUCKET_ID, fileId, width, height) + }, + + /** + * Delete file + */ + async delete(fileId: string) { + await storage.deleteFile(BUCKET_ID, fileId) + }, + + /** + * Delete multiple files (for cleanup) + */ + async deleteMany(fileIds: string[]) { + await Promise.allSettled( + fileIds.map(id => storage.deleteFile(BUCKET_ID, id)) + ) + }, +} +``` + + + +## Functions Integration Pattern + +**Documentation:** [Functions Quick Start](https://appwrite.io/docs/products/functions/quick-start) + +### When to Use Appwrite Functions + +- **Scheduled tasks**: Cron jobs, periodic cleanup, report generation +- **Event-driven processing**: Triggered by database changes, file uploads, user events +- **Background jobs**: Long-running operations, email sending, data processing +- **Webhooks**: Third-party integrations, payment processing +- **Server-side logic**: Operations requiring elevated permissions + +### Execution Patterns + +| Pattern | Use Case | Method | +|---------|----------|--------| +| **Synchronous** | Immediate response needed | `createExecution(functionId, body, async=false)` | +| **Asynchronous** | Fire-and-forget, long tasks | `createExecution(functionId, body, async=true)` | +| **Scheduled** | Cron-based triggers | Configure in Console/appwrite.json | +| **Event-driven** | Database/storage triggers | Configure event subscriptions | + +### Implementation Steps + +1. **Initialize Functions service** with your SDK's client +2. **Call `createExecution()`** with: + - `functionId`: The function's unique ID + - `body`: JSON string of data to pass + - `async`: Boolean for sync/async execution + - `path`: Optional route path (default: "/") + - `method`: HTTP method (GET, POST, etc.) +3. **Handle response**: Check `status` field for success/failure +4. **Parse `responseBody`** as JSON for the result + +### Function Development Best Practices + +- Use [Appwrite Function Templates](https://github.com/appwrite/templates) as starting points +- Store secrets in function environment variables, not in code +- Return JSON responses for easy parsing +- Implement proper error handling and logging +- Use typed request/response structures for your language + +### Resources + +- [Develop Functions](https://appwrite.io/docs/products/functions/develop) +- [Execute Functions](https://appwrite.io/docs/products/functions/execute) +- [Function Runtimes](https://appwrite.io/docs/products/functions/runtimes) +- [Event Triggers](https://appwrite.io/docs/advanced/platform/events) + + + +## Messaging Integration Pattern + +**Documentation:** [Messaging Overview](https://appwrite.io/docs/products/messaging) + +### Message Types + +| Type | Method | Use Case | +|------|--------|----------| +| **Email** | `createEmail()` | Transactional emails, notifications | +| **Push** | `createPush()` | Mobile/web push notifications | +| **SMS** | `createSms()` | Text messages, verification codes | + +### Targeting Options + +Messages can be sent to: +- **Users**: Array of user IDs (`users` parameter) +- **Targets**: Specific device/endpoint IDs (`targets` parameter) +- **Topics**: Broadcast to subscribers (`topics` parameter) + +### Implementation Steps + +1. **Initialize Messaging service** with your SDK's client (requires API key) +2. **Create message** using the appropriate method: + - `createEmail(messageId, subject, content, topics, users, targets, ...)` + - `createPush(messageId, title, body, topics, users, targets, data)` + - `createSms(messageId, content, topics, users, targets)` +3. **Handle delivery status** by checking the returned message object + +### Topic Subscriptions + +- **Subscribe**: `createSubscriber(topicId, subscriberId, targetId)` +- **Unsubscribe**: `deleteSubscriber(topicId, subscriberId)` + +### Provider Configuration Required + +| Channel | Providers | +|---------|-----------| +| **Email** | SMTP, Mailgun, SendGrid, Mailchimp | +| **Push** | FCM (Android), APNS (iOS) | +| **SMS** | Twilio, Vonage, Textmagic, Telesign | + +Configure providers in Appwrite Console → Messaging → Providers + +### Best Practices + +- Generate unique message IDs for each send +- Handle message failures gracefully +- Use topics for broadcast messages, targets for specific devices +- Store provider credentials securely in Console +- Implement retry logic for failed deliveries + +### Resources + +- [Send Email](https://appwrite.io/docs/products/messaging/send-email-messages) +- [Send Push](https://appwrite.io/docs/products/messaging/send-push-notifications) +- [Send SMS](https://appwrite.io/docs/products/messaging/send-sms-messages) +- [Topics](https://appwrite.io/docs/products/messaging/topics) + + + +## Realtime Integration Pattern + +**Documentation:** [Realtime Overview](https://appwrite.io/docs/products/realtime) + +### Overview + +Realtime subscriptions allow your app to receive live updates when data changes. Subscriptions run on the **client side** using the client SDK. + +### Subscription Flow + +1. **Initialize client** with endpoint and project ID +2. **Subscribe to channel(s)** using `client.subscribe(channels, callback)` +3. **Handle events** in the callback (create, update, delete) +4. **Unsubscribe** when component unmounts or no longer needed + +### Realtime Channels Reference + +| Channel Pattern | Description | +|-----------------|-------------| +| `databases.[DB_ID].tables.[TABLE_ID].rows` | All rows in table | +| `databases.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]` | Specific row | +| `buckets.[BUCKET_ID].files` | All files in bucket | +| `buckets.[BUCKET_ID].files.[FILE_ID]` | Specific file | +| `account` | Current user's account changes | +| `teams` | Team membership changes | +| `teams.[TEAM_ID]` | Specific team changes | + +### Event Types + +The callback receives an event object with: +- `events`: Array of event strings (e.g., `databases.*.tables.*.rows.*.create`) +- `payload`: The row/file/account data that changed + +| Event | Triggered When | +|-------|----------------| +| `*.create` | New row/file created | +| `*.update` | Existing row/file updated | +| `*.delete` | Row/file deleted | + +### Implementation Pattern + +``` +// Pseudocode for any SDK +subscription = client.subscribe( + ["databases.DB_ID.tables.TABLE_ID.rows"], + function(event) { + if event.type contains "create": + add event.payload to local state + else if event.type contains "update": + update matching item in local state + else if event.type contains "delete": + remove matching item from local state + } +) + +// On cleanup/unmount: +subscription.close() // or unsubscribe() +``` + +### Best Practices + +- **Always unsubscribe** in cleanup/dispose functions +- Use **client SDK** for realtime (not server SDK) +- Filter events client-side if you only need specific event types +- Implement **reconnection logic** for dropped connections +- Consider **optimistic updates** combined with realtime for better UX +- Use **initial data from server** then enhance with realtime updates +- Handle the case where connection drops and reconnects + +### Resources + +- [Subscribe to Databases](https://appwrite.io/docs/products/realtime/subscribe-to-databases) +- [Subscribe to Storage](https://appwrite.io/docs/products/realtime/subscribe-to-storage) +- [Subscribe to Account](https://appwrite.io/docs/products/realtime/subscribe-to-account) +- [Channels Reference](https://appwrite.io/docs/products/realtime/channels) + + + +## Sites Deployment Pattern + +Appwrite Sites is a hosting platform for static and SSR applications. Configuration is primarily done through the Appwrite Console or CLI. + +### Deployment Methods + +1. **Git Integration** (Recommended) + - Connect your repository in Appwrite Console + - Automatic deployments on push to configured branch + - See: [Deploy from Git](https://appwrite.io/docs/products/sites/deploy-from-git) + +2. **CLI Deployment** + - Use `appwrite deploy` command + - See: [Deploy from CLI](https://appwrite.io/docs/products/sites/deploy-from-cli) + +3. **Manual Upload** + - Upload built assets directly + - See: [Deploy Manually](https://appwrite.io/docs/products/sites/deploy-manually) + +### Environment Variables + +Configure in Appwrite Console under Site settings: +- `APPWRITE_ENDPOINT` - Your Appwrite endpoint +- `APPWRITE_PROJECT_ID` - Your project ID +- Build-time vs runtime variables as needed + +### Framework-Specific Notes + +- **Static Sites**: Build output uploaded as-is +- **SSR Apps**: Require Appwrite Functions runtime +- Check [Frameworks Documentation](https://appwrite.io/docs/products/sites/frameworks) for your specific framework + +### Rollbacks + +Appwrite Sites supports instant rollbacks: +- View deployment history in Console +- Click "Activate" on any previous deployment +- See: [Instant Rollbacks](https://appwrite.io/docs/products/sites/instant-rollbacks) + + +## Next.js-Specific Best Practices + +### Rendering Strategy +- Default to Server Components for all data fetching +- Use `'use server'` for all mutation functions +- Only use Client Components when explicitly needed for interactivity +- Never import Appwrite SDK in Client Components + +### Data Fetching Pattern +```typescript +// In Server Component - direct async/await +async function ItemsPage() { + const items = await db.items.listByOwner(userId) + return +} +``` + +### Revalidation +```typescript +// After mutations, revalidate the path +import { revalidatePath } from 'next/cache' + +export async function createItem(data) { + 'use server' + const item = await db.items.create(data) + revalidatePath('/items') + return { item } +} +``` + +### File Organization +``` +app/ +├── actions/ # Server Actions +│ └── items.ts +├── lib/ +│ ├── db.ts # Database wrapper (server-only) +│ ├── storage.ts # Storage wrapper (server-only) +│ └── auth.ts # Auth helpers +└── (routes)/ + └── items/ + └── page.tsx # Server Component +``` + +## Authentication & Teams + +**Authentication Documentation:** + +- [Authentication Quick Start](https://appwrite.io/docs/products/auth/quick-start) - Getting started with authentication +- [Email & Password](https://appwrite.io/docs/products/auth/email-password) - Email/password authentication +- [OAuth2 Providers](https://appwrite.io/docs/products/auth/oauth2) - Social authentication (Google, GitHub, etc.) +- [Magic URL](https://appwrite.io/docs/products/auth/magic-url) - Passwordless authentication via email +- [Phone (SMS)](https://appwrite.io/docs/products/auth/phone-sms) - Phone number authentication +- [Anonymous Sessions](https://appwrite.io/docs/products/auth/anonymous) - Guest/anonymous users +- [JWT Tokens](https://appwrite.io/docs/products/auth/jwt) - JSON Web Token authentication +- [MFA/2FA](https://appwrite.io/docs/products/auth/mfa) - Multi-factor authentication +- [SSR Authentication](https://appwrite.io/docs/products/auth/server-side-rendering) - Server-side rendering auth patterns +- [Teams](https://appwrite.io/docs/products/auth/teams) - Team management and team-based permissions +- [Team Invites](https://appwrite.io/docs/products/auth/team-invites) - Inviting members to teams +- [Multi-tenancy](https://appwrite.io/docs/products/auth/multi-tenancy) - Building multi-tenant applications with teams + +### Best Practices for Authentication & Teams + +- **Session Security**: Always use HttpOnly cookies for session storage in SSR applications +- **API Keys**: Never expose API keys to client-side code - use environment variables +- **Session Validation**: Always validate sessions on the server before trusting them +- **Team-Based Architecture**: ALWAYS prefer team/member-based roles over user-specific roles for any application requiring shared access or multi-tenancy +- **Multi-Tenant Applications**: Use teams as the primary mechanism for tenant isolation and resource sharing +- **OAuth Redirects**: Handle OAuth redirects properly with success and failure URLs +- **Password Security**: Use strong password requirements and consider implementing MFA +- **Session Expiry**: Configure appropriate session expiry times based on your security requirements + +### Team & Member Management Fundamentals + +When building applications that involve multiple users or tenants: + +1. **Always Start with Teams**: For any feature requiring shared access, create a team first, then add members with roles +2. **Role-Based Access**: Assign roles (e.g., "owner", "admin", "member", "viewer") to team members rather than setting individual user permissions +3. **Team Isolation**: Use teams as the boundary for data isolation in multi-tenant applications +4. **Member Invitations**: Implement team invitation workflows for onboarding new members +5. **Role Management**: Build role management UIs that allow team owners/admins to manage member roles dynamically + +## Permissions & Multi-Tenancy + +This section is CRITICAL for building secure, scalable applications with Appwrite. Multi-tenancy is one of the most important architectural patterns in modern applications, and Appwrite's team-based permission system is designed specifically for this. + +**Permissions Documentation:** + +- [Permissions Overview](https://appwrite.io/docs/advanced/platform/permissions) - Understanding Appwrite's permission system +- [Role Types](https://appwrite.io/docs/advanced/platform/permissions#role-types) - Available permission roles (any, users, guests, team, member, label) +- [Permission Types](https://appwrite.io/docs/advanced/platform/permissions#permission-types) - Read, create, update, delete permissions +- [Teams](https://appwrite.io/docs/products/auth/teams) - Team-based access control +- [Multi-tenancy](https://appwrite.io/docs/products/auth/multi-tenancy) - Tenant isolation patterns + +### Why Multi-Tenancy Matters + +Multi-tenancy allows a single application instance to serve multiple isolated groups of users (tenants) while maintaining complete data isolation and security. Almost every modern SaaS application requires multi-tenancy to scale efficiently. + +### The Critical Pattern: Team-Based Permissions + +**ALWAYS PREFER TEAM/MEMBER-BASED ROLES over user-specific roles.** This is a fundamental architectural decision: + +#### Avoid: User-Specific Permissions +```javascript +// DON'T: User-specific permissions don't scale +Permission.read(Role.user('')) +Permission.write(Role.user('')) +``` + +**Problems with user-specific permissions:** +- Hard to scale when users need to share resources +- Difficult to add/remove access without updating every row +- No way to represent organizational hierarchies +- Poor support for collaborative features + +#### Prefer: Team/Member-Based Roles +```javascript +// DO: Team-based permissions scale with your organization +Permission.read(Role.team('', 'member')) +Permission.update(Role.team('', 'admin')) +Permission.delete(Role.team('', 'owner')) +``` + +**Benefits of team/member-based roles:** +- Automatic access for all team members based on their role +- Easy to add/remove members without touching rows +- Scales naturally as teams grow +- Supports organizational hierarchies and complex permissions + +### Query Isolation Pattern + +**CRITICAL**: Always filter queries by `teamId` to ensure tenant isolation: + +```javascript +// ALWAYS filter by teamId for tenant isolation +const response = await tablesDB.listRows( + '', + '', + [ + Query.equal('teamId', ''), // Critical for isolation + Query.orderDesc('$createdAt'), + Query.limit(25) + ] +); +``` + +### Role Verification Pattern + +Before allowing sensitive operations, always verify the user's role: + +```javascript +// Verify permissions before sensitive operations +const memberships = await teams.listMemberships(''); +const membership = memberships.memberships.find(m => m.userId === userId); + +if (!membership?.roles.includes('admin')) { + throw new Error('Insufficient permissions'); +} +``` + +### Multi-Tenancy Implementation Guide + +#### Step 1: Create Teams Structure + +Teams in Appwrite represent tenants. Each team should map to a business entity (company, organization, workspace). + +See: [Teams Documentation](https://appwrite.io/docs/products/auth/teams) + +#### Step 2: Define Custom Roles + +Common role hierarchy: +- **owner**: Full control, can manage team settings and members +- **admin**: Can manage resources and most settings +- **member**: Can create/edit resources with limited permissions +- **viewer**: Read-only access + +#### Step 3: Member Management + +For team invitations and membership management, see [Team Invites Guide](https://appwrite.io/docs/products/auth/team-invites) + +#### Step 4: Apply Permissions Consistently + +Use team roles for all resources: +- **Database rows**: Apply `Role.team('', 'role')` permissions +- **Storage files**: Same team-based permission pattern +- **Always include `teamId`** as a field in your rows for query filtering + +### Permission Best Practices + +1. **Always Store teamId**: Every row in a multi-tenant app should have a `teamId` field +2. **Default Deny**: Don't grant permissions unless explicitly needed +3. **Role Hierarchy**: Design roles to reflect natural hierarchies (owner > admin > member > viewer) +4. **Server-Side Validation**: Always validate team membership server-side +5. **Query Isolation**: Every multi-tenant query MUST include a `teamId` filter + +### Common Multi-Tenancy Patterns + +| Pattern | Example Apps | Structure | +|---------|--------------|-----------| +| Workspace-Based | Notion, Slack | Each workspace = 1 team, users can belong to multiple teams | +| Organization-Based | GitHub, GitLab | Each org = 1 team, resources scoped to org | +| Project-Based | Linear, Asana | Each project = 1 team, members invited per project | + +### Debugging Permission Issues + +1. **Check Team Membership**: Verify user is actually a member of the team +2. **Verify Roles**: Check `membership.roles` contains the required role +3. **Check Permission Strings**: Permission strings are case-sensitive +4. **Query Filters**: Ensure `teamId` filters are applied correctly +5. **Server vs Client**: Some operations require the Server SDK + +### Additional Resources + +**Authentication Documentation:** + +- [Authentication Quick Start](https://appwrite.io/docs/products/auth/quick-start) - Getting started with authentication +- [Email & Password](https://appwrite.io/docs/products/auth/email-password) - Email/password authentication +- [OAuth2 Providers](https://appwrite.io/docs/products/auth/oauth2) - Social authentication (Google, GitHub, etc.) +- [Magic URL](https://appwrite.io/docs/products/auth/magic-url) - Passwordless authentication via email +- [Phone (SMS)](https://appwrite.io/docs/products/auth/phone-sms) - Phone number authentication +- [Anonymous Sessions](https://appwrite.io/docs/products/auth/anonymous) - Guest/anonymous users +- [JWT Tokens](https://appwrite.io/docs/products/auth/jwt) - JSON Web Token authentication +- [MFA/2FA](https://appwrite.io/docs/products/auth/mfa) - Multi-factor authentication +- [SSR Authentication](https://appwrite.io/docs/products/auth/server-side-rendering) - Server-side rendering auth patterns +- [Teams](https://appwrite.io/docs/products/auth/teams) - Team management and team-based permissions +- [Team Invites](https://appwrite.io/docs/products/auth/team-invites) - Inviting members to teams +- [Multi-tenancy](https://appwrite.io/docs/products/auth/multi-tenancy) - Building multi-tenant applications with teams + +## Database Operations + +**Database Documentation:** + +- [Database Quick Start](https://appwrite.io/docs/products/databases/quick-start) - Getting started with TablesDB +- [Tables](https://appwrite.io/docs/products/databases/tables) - Creating and managing tables +- [Rows](https://appwrite.io/docs/products/databases/rows) - CRUD operations on rows +- [Queries](https://appwrite.io/docs/products/databases/queries) - Filtering, sorting, and querying data +- [Pagination](https://appwrite.io/docs/products/databases/pagination) - Paginating large datasets +- [Relationships](https://appwrite.io/docs/products/databases/relationships) - One-to-one, one-to-many, many-to-many relationships +- [Permissions](https://appwrite.io/docs/products/databases/permissions) - Row and table-level permissions +- [Transactions](https://appwrite.io/docs/products/databases/transactions) - Atomic operations + +### Database Setup Scripts + +**ALWAYS create a database setup script using the Server SDK and API key** to initialize your database schema. This script should be version-controlled and run during deployment or initial setup. + +**Why Use Setup Scripts:** +- **Infrastructure as Code**: Database schema becomes part of your codebase, not manual console clicks +- **Reproducibility**: Easy to recreate database structure across different environments (dev, staging, production) +- **Version Control**: Track schema changes over time with Git +- **Team Collaboration**: All developers can sync database structure automatically +- **CI/CD Integration**: Automate database setup in deployment pipelines +- **Documentation**: The script serves as living documentation of your database structure + +**What Your Setup Script Should Include:** + +1. **Table Creation**: All tables with proper naming and IDs +2. **Column Definitions**: All columns with correct data types (string, integer, boolean, datetime, email, url, etc.) +3. **Indexes**: Performance-critical indexes on frequently queried fields (especially `teamId`, foreign keys, search fields) +4. **Relationships**: All table relationships and foreign key constraints +5. **Default Permissions**: Table-level permissions using team roles +6. **Column Constraints**: Required fields, string lengths, number ranges, enum values, default values + +**Setup Script Requirements:** + +- **Use Server SDK**: Must use the Server SDK (node-appwrite, appwrite/appwrite for PHP, etc.), NOT the client SDK +- **Require API Key**: The script must use an API key with appropriate scopes (`databases.write`, `tables.write`, `columns.write`) +- **Idempotent**: Script should safely handle re-runs (check if tables exist before creating) +- **Environment Variables**: Store API key, endpoint, project ID, and database ID in environment variables +- **Error Handling**: Proper error handling with clear error messages +- **Logging**: Log progress and errors for debugging + +**Example Setup Script Structure:** + +```javascript +// scripts/setup-database.js (Node.js example) +import { Client, TablesDB, Permission, Role } from 'node-appwrite'; + +const client = new Client() + .setEndpoint(process.env.APPWRITE_ENDPOINT) + .setProject(process.env.APPWRITE_PROJECT_ID) + .setKey(process.env.APPWRITE_API_KEY); + +const tablesDB = new TablesDB(client); +const databaseId = process.env.APPWRITE_DATABASE_ID; + +async function setupDatabase() { + try { + // Create Users table + await tablesDB.createTable(databaseId, 'users', 'Users', [ + Permission.read(Role.users()), + Permission.write(Role.users()) + ]); + + // Add columns to Users table + await tablesDB.createStringColumn(databaseId, 'users', 'name', 255, true); + await tablesDB.createEmailColumn(databaseId, 'users', 'email', true); + await tablesDB.createStringColumn(databaseId, 'users', 'teamId', 255, true); + + // Create index on teamId for query performance + await tablesDB.createIndex(databaseId, 'users', 'idx_team', 'key', ['teamId']); + + // Create Projects table with team-based permissions + await tablesDB.createTable(databaseId, 'projects', 'Projects', [ + Permission.read(Role.team('[TEAM_ID]')), + Permission.create(Role.team('[TEAM_ID]', 'member')), + Permission.update(Role.team('[TEAM_ID]', 'admin')), + Permission.delete(Role.team('[TEAM_ID]', 'owner')), + ]); + + // Add columns to Projects table + await tablesDB.createStringColumn(databaseId, 'projects', 'name', 255, true); + await tablesDB.createStringColumn(databaseId, 'projects', 'description', 5000, false); + await tablesDB.createStringColumn(databaseId, 'projects', 'teamId', 255, true); + await tablesDB.createStringColumn(databaseId, 'projects', 'ownerId', 255, true); + await tablesDB.createDatetimeColumn(databaseId, 'projects', 'createdAt', true); + + // Create indexes + await tablesDB.createIndex(databaseId, 'projects', 'idx_team', 'key', ['teamId']); + await tablesDB.createIndex(databaseId, 'projects', 'idx_owner', 'key', ['ownerId']); + + // Create relationship between projects and users + await tablesDB.createRelationshipColumn( + databaseId, + 'projects', + 'users', + 'oneToMany', + false, // twoWay + 'owner', // key in projects + 'projects', // key in users + 'cascade' // onDelete + ); + + console.log('Database setup completed successfully!'); + } catch (error) { + // Handle "already exists" errors gracefully for idempotency + if (error.code !== 409) { + console.error('Database setup failed:', error); + throw error; + } else { + console.log('Tables already exist, skipping creation'); + } + } +} + +setupDatabase(); +``` + +**Running the Setup Script:** + +```bash +# Set environment variables +export APPWRITE_ENDPOINT="https://cloud.appwrite.io/v1" +export APPWRITE_PROJECT_ID="your-project-id" +export APPWRITE_API_KEY="your-api-key" +export APPWRITE_DATABASE_ID="your-database-id" + +# Run the setup script +node scripts/setup-database.js +``` + +**Best Practices for Setup Scripts:** + +1. **Separate File**: Keep setup scripts in a `scripts/` directory +2. **Documentation**: Add comments explaining each table's purpose and relationships +3. **Testing**: Test the script on a separate development database before production +4. **Migration Strategy**: For schema changes, create new migration scripts instead of modifying the original setup +5. **Backup First**: Always backup production data before running schema changes +6. **Team ID Fields**: Always include `teamId` fields in multi-tenant tables +7. **Timestamp Fields**: Include `createdAt` and `updatedAt` fields for auditing +8. **Foreign Keys**: Use relationship columns to enforce referential integrity + +### Best Practices for TablesDB + +- **SDK Usage**: Use the `TablesDB` service (formerly `Databases`) for all database operations +- **Permissions & Multi-Tenancy**: ALWAYS use team/member-based roles for permissions (see Permissions & Multi-Tenancy section above). Never use user-specific permissions in multi-tenant applications +- **Tenant Isolation**: Always include `teamId` fields in your rows and filter queries by `teamId` to ensure complete data isolation between tenants +- **Permission Patterns**: Apply team roles (owner, admin, member, viewer) consistently across all tables. Use Role.team() for all permission checks +- **Query Security**: Every multi-tenant query MUST include a `teamId` filter to prevent cross-tenant data access +- **Table Permissions**: Set table-level permissions using team roles, then override at row level when needed +- **Query Optimization**: Use indexes for frequently queried fields, especially on `teamId` and commonly filtered fields +- **Data Validation**: Validate data before creating or updating rows, including team membership validation +- **Transactions**: Use transactions for operations that must succeed or fail together, ensuring atomicity across tenant boundaries +- **Pagination**: Always implement pagination for large datasets to improve performance and reduce response sizes +- **Type Safety**: Use type-safe models when available in your SDK for better code quality and fewer runtime errors + +## Storage Operations + +**Storage Documentation:** + +- [Storage Quick Start](https://appwrite.io/docs/products/storage/quick-start) - Getting started with storage +- [Buckets](https://appwrite.io/docs/products/storage/buckets) - Creating and managing storage buckets +- [Upload & Download](https://appwrite.io/docs/products/storage/upload-download) - File upload and download operations +- [Permissions](https://appwrite.io/docs/products/storage/permissions) - Bucket and file-level permissions +- [Images](https://appwrite.io/docs/products/storage/images) - Image manipulation and transformations + +### Best Practices for Storage + +- **Permissions & Multi-Tenancy**: ALWAYS use team/member-based roles for storage permissions (see Permissions & Multi-Tenancy section above). Apply Role.team() permissions to buckets and files for proper tenant isolation +- **Bucket Organization**: Consider organizing files by team/tenant using folder structures or bucket naming conventions for easier management +- **Tenant Isolation**: When querying files, always filter by metadata (e.g., `teamId`) to ensure users only access files from their teams +- **File Size Limits**: Set appropriate file size limits to prevent abuse and manage costs +- **File Types**: Validate file types before upload to ensure security and prevent malicious uploads +- **Permission Patterns**: Use team roles (owner, admin, member, viewer) consistently for bucket and file permissions, matching your database permission model +- **Cleanup**: Implement cleanup strategies for unused or temporary files, especially when teams are deleted +- **Virus Scanning**: Consider implementing virus scanning for uploaded files to protect all tenants +- **Access Control**: Validate team membership before allowing file uploads/downloads, even if permissions are set correctly + +## Functions + +**Functions Documentation:** + +- [Functions Quick Start](https://appwrite.io/docs/products/functions/quick-start) - Getting started with serverless functions +- [Develop Functions](https://appwrite.io/docs/products/functions/develop) - Writing and developing functions +- [Execute Functions](https://appwrite.io/docs/products/functions/execute) - Triggering function executions +- [Deployments](https://appwrite.io/docs/products/functions/deployments) - Deploying function code +- [Domains](https://appwrite.io/docs/products/functions/domains) - Custom domains for functions +- [Events](https://appwrite.io/docs/advanced/platform/events) - Event-driven function execution +- [Scheduled Executions](https://appwrite.io/docs/products/functions/execute#schedule) - Cron-based scheduling + +### Starter Templates + +For getting started with Appwrite Functions, use the official starter template for your runtime: + +- **JavaScript/TypeScript Starter**: [View Template](https://github.com/appwrite/templates/tree/main/node/starter) + +For more templates and examples, see the [Appwrite Templates Repository](https://github.com/appwrite/templates). + +### When to Use Starter Templates + +**ALWAYS use starter templates from the [Appwrite Templates Repository](https://github.com/appwrite/templates) when building functions for:** + +- **Scheduled Tasks**: Functions that run on a schedule (cron jobs, periodic cleanup, etc.) +- **Event-Driven Tasks**: Functions triggered by Appwrite events (database changes, storage uploads, user events, etc.) +- **Background Processing**: Long-running or resource-intensive operations +- **Integration Functions**: Functions that integrate with third-party services (APIs, webhooks, etc.) +- **Complex Functions**: Any function that requires specific runtime configuration or dependencies + +**Why use templates?** Starter templates provide the correct project structure, dependencies, and configuration needed for functions to build and execute successfully. They ensure proper handling of environment variables, logging, error handling, and Appwrite SDK initialization. + +### Best Practices for Functions + +- **Error Handling**: Implement comprehensive error handling in your functions +- **Timeouts**: Be aware of function execution timeouts and optimize accordingly +- **Environment Variables**: Use environment variables for configuration, not hardcoded values +- **Logging**: Implement proper logging for debugging and monitoring +- **Security**: Validate all inputs and never trust user-provided data +- **Resource Limits**: Be mindful of memory and CPU limits for function executions +- **Template Usage**: Start with official templates for scheduled and event-driven functions to ensure proper setup + +## Messaging + +**Messaging Documentation:** + +- [Messaging Overview](https://appwrite.io/docs/products/messaging) - Getting started with messaging +- [Push Notifications](https://appwrite.io/docs/products/messaging/send-push-notifications) - Sending push notifications +- [Email Messages](https://appwrite.io/docs/products/messaging/send-email-messages) - Sending emails +- [SMS Messages](https://appwrite.io/docs/products/messaging/send-sms-messages) - Sending SMS +- [Topics](https://appwrite.io/docs/products/messaging/topics) - Managing message topics +- [Targets](https://appwrite.io/docs/products/messaging/targets) - Managing message targets +- [Providers](https://appwrite.io/docs/products/messaging/providers) - Configuring messaging providers + +### Best Practices for Messaging + +- **Provider Selection**: Choose the right messaging provider based on your needs (FCM, APNS, Mailgun, Twilio, etc.) +- **Message Content**: Keep push notifications concise and actionable +- **Scheduling**: Use scheduled messages for better user engagement timing +- **Personalization**: Personalize messages to increase engagement +- **Rate Limiting**: Be mindful of rate limits when sending bulk messages +- **Error Handling**: Implement retry logic for failed message deliveries + +## Sites + +**Sites Documentation:** + +- [Sites Quick Start](https://appwrite.io/docs/products/sites/quick-start) - Getting started with Sites +- [Deploy from Git](https://appwrite.io/docs/products/sites/deploy-from-git) - Git-based deployments +- [Deploy from CLI](https://appwrite.io/docs/products/sites/deploy-from-cli) - CLI deployments +- [Deploy Manually](https://appwrite.io/docs/products/sites/deploy-manually) - Manual file uploads +- [Deployments](https://appwrite.io/docs/products/sites/deployments) - Managing deployments +- [Domains](https://appwrite.io/docs/products/sites/domains) - Custom domain configuration +- [Rendering](https://appwrite.io/docs/products/sites/rendering) - Static vs SSR rendering +- [Frameworks](https://appwrite.io/docs/products/sites/frameworks) - Supported frameworks +- [Rollbacks](https://appwrite.io/docs/products/sites/instant-rollbacks) - Instant rollbacks +- [Previews](https://appwrite.io/docs/products/sites/previews) - Deployment previews + +### Best Practices for Sites + +- **Environment Variables**: Use environment variables for configuration, not hardcoded values +- **Custom Domains**: Configure custom domains for production sites for better branding +- **Rendering Strategy**: Choose between static and SSR based on your content needs and SEO requirements +- **Deployment Strategy**: Use Git deployments for automatic builds on commits +- **Rollback Plan**: Keep previous deployments ready for instant rollbacks if needed + +## Realtime Subscriptions + +**Realtime Documentation:** + +- [Realtime Overview](https://appwrite.io/docs/products/realtime) - Getting started with realtime +- [Database Subscriptions](https://appwrite.io/docs/products/realtime/subscribe-to-databases) - Subscribe to database changes +- [Storage Subscriptions](https://appwrite.io/docs/products/realtime/subscribe-to-storage) - Subscribe to storage changes +- [Account Subscriptions](https://appwrite.io/docs/products/realtime/subscribe-to-account) - Subscribe to account changes +- [Channels](https://appwrite.io/docs/products/realtime/channels) - Available subscription channels +- [Events](https://appwrite.io/docs/products/realtime/events) - Event types and payloads + +### Best Practices for Realtime Subscriptions + +- **Connection Management**: Always unsubscribe from channels when components unmount or pages are closed to prevent memory leaks +- **Error Handling**: Implement reconnection logic for dropped connections and handle network errors gracefully +- **Event Filtering**: Filter events on the client side to only process relevant updates for better performance +- **Channel Selection**: Subscribe only to the specific channels you need to minimize bandwidth and improve performance +- **Payload Validation**: Always validate payload data before processing to ensure data integrity +- **Rate Limiting**: Be mindful of the number of subscriptions and events to avoid overwhelming the client +- **State Synchronization**: Use realtime updates to keep local state in sync with server state, but handle conflicts appropriately +- **Authentication**: Ensure proper authentication is in place before subscribing to protected channels +- **Testing**: Test realtime functionality with network interruptions and reconnection scenarios +- **Cleanup**: Store unsubscribe functions and call them in cleanup hooks (useEffect cleanup, componentWillUnmount, etc.) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 466695b..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Appwrite - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..91c2578 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# Appwrite AGENTS.md Generator + +A web application for generating comprehensive AGENTS.md files for Appwrite development across multiple SDKs and frameworks. This tool helps developers create customized AI coding assistant instructions that include best practices, code examples, and guidance for building applications with Appwrite. + +## Features + +- **Multi-SDK Support**: Generate rules for JavaScript/TypeScript, Python, PHP, Go, Flutter/Dart, Apple, Android, Swift, Kotlin, Ruby, .NET, and React Native +- **Framework-Specific Rules**: Get tailored rules for popular frameworks like Next.js, React, Vue, Svelte, Angular, Astro, Nuxt, Qwik, Solid, and more +- **Feature Selection**: Choose which Appwrite features to include: + - Authentication & Teams + - Database Operations + - Storage Operations + - Functions + - Messaging + - Sites + - Realtime Subscriptions +- **Export Options**: Copy to clipboard or download as `AGENTS.md` file +- **Best Practices**: Generated rules include comprehensive best practices, multi-tenancy patterns, and security guidelines + +## Supported SDKs and Frameworks + +| SDK | Frameworks | +|-----|------------| +| JavaScript/TypeScript | Next.js, React, Vue, Svelte, Angular, Astro, Nuxt, Qwik, Solid, TanStack, Node.js, Vanilla | +| React Native | React Native, Vanilla | +| Python | Flask, Django, FastAPI, Server | +| Flutter/Dart | Flutter, Server | +| Apple | Vanilla | +| Android | Vanilla | +| Swift | Server, Vanilla | +| Kotlin | Server, Vanilla | +| PHP | Laravel, Symfony, Server | +| Go | Gin, Fiber, Server | +| Ruby | Rails, Server | +| .NET | ASP.NET, Server, Vanilla | + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- pnpm + +### Installation + +1. Clone the repository: +```bash +git clone +cd appwrite-cursor-rules +``` + +2. Install dependencies: +```bash +pnpm install +``` + +3. Start the development server: +```bash +pnpm dev +``` + +4. Open your browser and navigate to `http://localhost:5173` (or the port shown in the terminal) + +## Usage + +1. **Select SDK**: Choose your preferred Appwrite SDK from the dropdown +2. **Select Framework**: Pick the framework you're using (options depend on the selected SDK) +3. **Choose Features**: Check the boxes for the Appwrite features you want to include in your rules +4. **Generate Rules**: Click the "Generate Rules" button +5. **Export**: Copy the rules to your clipboard or download as a `AGENTS.md` file + +The generated AGENTS.md file can be used with AI coding assistants (Cursor, GitHub Copilot, OpenAI Codex, etc.) to provide AI-assisted development guidance specific to your Appwrite setup. + +## API Endpoints + +The application exposes REST API endpoints for programmatic access to rule generation. + +### Get Available SDKs and Frameworks + +**GET** `/api/sdks` + +Returns a list of all available SDKs, their frameworks, and available features. + +**Response:** +```json +{ + "sdks": [ + { + "id": "javascript", + "name": "JavaScript/TypeScript", + "frameworks": ["nextjs", "react", "vue", ...], + "importSyntax": "import", + "exportSyntax": "export", + "asyncSyntax": "async/await" + }, + ... + ], + "availableFeatures": ["auth", "database", "storage", "functions", "messaging", "sites", "realtime"] +} +``` + +### Generate Rules + +**GET** `/api/rules` + +Generate rules using query parameters. + +**Query Parameters:** +- `sdk` (required): SDK identifier (e.g., `javascript`, `python`, `go`) +- `framework` (required): Framework identifier (e.g., `nextjs`, `react`, `flask`) +- `features` (optional): Comma-separated list of features (default: `auth`) + - Available: `auth`, `database`, `storage`, `functions`, `messaging`, `sites`, `realtime` + - Special: `all` - includes all available features +- `format` (optional): Response format (`text` or `json`, default: `text`) + +**Example:** +```bash +# Get rules as markdown text +curl "http://localhost:5173/api/rules?sdk=javascript&framework=nextjs&features=auth,database&format=text" + +# Get all features +curl "http://localhost:5173/api/rules?sdk=javascript&framework=nextjs&features=all" + +# Get rules as JSON +curl "http://localhost:5173/api/rules?sdk=python&framework=flask&features=auth,storage&format=json" +``` + +**Response (format=text):** +- Content-Type: `text/markdown; charset=utf-8` +- Returns the generated rules as markdown text +- Includes `Content-Disposition` header for file download + +**Response (format=json):** +```json +{ + "sdk": "javascript", + "framework": "nextjs", + "features": ["auth", "database"], + "rules": "# Appwrite Development Rules\n\n> You are an expert developer...\n\n## Overview\n..." +} +``` + +**Error Responses:** +- `400 Bad Request`: Invalid SDK or framework +- `500 Internal Server Error`: Server error during rule generation + +**Example Usage:** + +```javascript +// Fetch rules using fetch API +const response = await fetch('/api/rules?sdk=javascript&framework=nextjs&features=auth,database'); +const rules = await response.text(); + +// Get all features as JSON +const response = await fetch('/api/rules?sdk=javascript&framework=nextjs&features=all&format=json'); +const data = await response.json(); +console.log(data.rules); +``` + +## Development + +### Available Scripts + +- `pnpm dev` - Start development server +- `pnpm build` - Build for production +- `pnpm preview` - Preview production build +- `pnpm check` - Run Svelte type checking +- `pnpm lint` - Run ESLint and Prettier +- `pnpm format` - Format code with Prettier +- `pnpm generate:nextjs` - Generate Next.js rules file (example script) + +### Project Structure + +``` +appwrite-cursor-rules/ +├── src/ +│ ├── lib/ +│ │ ├── languages/ # SDK and framework-specific code examples +│ │ │ ├── js/ # JavaScript/TypeScript frameworks +│ │ │ ├── python/ # Python frameworks +│ │ │ ├── common/ # Shared rules (products, permissions, etc.) +│ │ │ └── ... # Other SDKs +│ │ ├── rules-generator.js # Main rules generation logic +│ │ └── utils/ # Utility functions +│ └── routes/ +│ ├── api/ +│ │ ├── rules/ +│ │ │ └── +server.js # API endpoint for generating rules +│ │ └── sdks/ +│ │ └── +server.js # API endpoint for listing SDKs +│ ├── +page.svelte # Main application page +│ └── +layout.svelte # Layout component +├── scripts/ +│ ├── generate-nextjs-rules.js # Example script for generating rules +│ └── lib-loader.js # Module loader for scripts +└── static/ # Static assets +``` + +### Adding New SDKs or Frameworks + +1. Create a new file in `src/lib/languages/[sdk-name]/index.js` (or add to existing SDK directory) +2. Export framework-specific initialization code +3. Add the SDK configuration to `SDK_OPTIONS` in `src/lib/rules-generator.js` +4. Export the SDK module in `src/lib/languages/index.js` + +### Adding New Features + +1. Create a new section generator function in `src/lib/rules-generator.js` (e.g., `generateNewFeatureSection`) +2. Add the feature to the features array in `src/routes/+page.svelte` +3. Include the feature in the `generateRules` function's Promise.all array + +## Generated AGENTS.md Format + +The generated AGENTS.md file follows the [AGENTS.md standard](https://agents.md/) and includes: + +- **Title and Description**: Project context and overview for AI coding assistants +- **SDK Initialization**: Framework-specific code examples for setting up Appwrite +- **Feature Sections**: Best practices and guidance for selected features +- **Multi-Tenancy Guide**: Comprehensive guide on using teams and permissions +- **Product Links**: Links to official Appwrite documentation + +The AGENTS.md format is compatible with multiple AI coding assistants including Cursor, GitHub Copilot, OpenAI Codex, and others. + + diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..134b1b2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig } from 'eslint/config'; +import globals from 'globals'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + "no-undef": 'off' } + }, + { + files: [ + '**/*.svelte', + '**/*.svelte.ts', + '**/*.svelte.js' + ], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..de0addf --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "appwrite-cursor-rules", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "generate:nextjs": "node ./scripts/generate-nextjs-rules.js" + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.39.1", + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^22", + "eslint": "^9.39.1", + "eslint-plugin-svelte": "^3.13.0", + "globals": "^16.5.0", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^5.43.8", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.47.0", + "vite": "^7.2.2" + }, + "dependencies": { + "@appwrite.io/pink": "^1.0.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3db13e6 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2054 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@appwrite.io/pink': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + '@eslint/compat': + specifier: ^1.4.0 + version: 1.4.1(eslint@9.39.1) + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@sveltejs/adapter-auto': + specifier: ^7.0.0 + version: 7.0.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1))) + '@sveltejs/kit': + specifier: ^2.48.5 + version: 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.1 + version: 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)) + '@types/node': + specifier: ^22 + version: 22.19.1 + eslint: + specifier: ^9.39.1 + version: 9.39.1 + eslint-plugin-svelte: + specifier: ^3.13.0 + version: 3.13.0(eslint@9.39.1)(svelte@5.44.0) + globals: + specifier: ^16.5.0 + version: 16.5.0 + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + svelte: + specifier: ^5.43.8 + version: 5.44.0 + svelte-check: + specifier: ^4.3.4 + version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.47.0 + version: 8.48.0(eslint@9.39.1)(typescript@5.9.3) + vite: + specifier: ^7.2.2 + version: 7.2.4(@types/node@22.19.1) + +packages: + + '@appwrite.io/pink-icons@1.0.0': + resolution: {integrity: sha512-+zpksP07MvOYwhx9AZDFW0pxXQNC2juKwyOQVRAwAOkN1ACSQKPlyytkI1u2ci6CQPWjJe20CzbvBBuRNXOKjA==} + + '@appwrite.io/pink@1.0.0': + resolution: {integrity: sha512-q/RFlKjtGL7yBfjYkPkHYnhPt9jSMT3zoLRlEIu+G2lt59QWjybELQW17cS8gm1ZBTcncuzoPtVd6x1vrEo5uw==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@1.4.1': + resolution: {integrity: sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@sveltejs/acorn-typescript@1.0.7': + resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-auto@7.0.0': + resolution: {integrity: sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/kit@2.49.0': + resolution: {integrity: sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1': + resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.1': + resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.19.1': + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + + '@typescript-eslint/eslint-plugin@8.48.0': + resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.48.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.48.0': + resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.48.0': + resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.48.0': + resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.48.0': + resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.48.0': + resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.48.0': + resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.48.0': + resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.48.0': + resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.48.0': + resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + devalue@5.5.0: + resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-svelte@3.13.0: + resolution: {integrity: sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrap@2.2.0: + resolution: {integrity: sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + normalize.css@8.0.1: + resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.4.0: + resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svelte-check@4.3.4: + resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.4.0: + resolution: {integrity: sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.18.3} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte@5.44.0: + resolution: {integrity: sha512-R7387No2zEGw4CtYtI2rgsui6BqjFARzoZFGLiLN5OPla0Pq4Ra2WwcP/zBomP3MYalhSNvF1fzDMuU0P0zPJw==} + engines: {node: '>=18'} + + the-new-css-reset@1.11.3: + resolution: {integrity: sha512-61SB81vu9foUyEIqoU1CeqxrdlsVjJojj/CBXoG8BdvlKFsllB0Rza63DblnRqH+3uttPj3FGWo7+c9nu7MT+A==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.48.0: + resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@7.2.4: + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@appwrite.io/pink-icons@1.0.0': {} + + '@appwrite.io/pink@1.0.0': + dependencies: + '@appwrite.io/pink-icons': 1.0.0 + normalize.css: 8.0.1 + the-new-css-reset: 1.11.3 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': + dependencies: + eslint: 9.39.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@1.4.1(eslint@9.39.1)': + dependencies: + '@eslint/core': 0.17.0 + optionalDependencies: + eslint: 9.39.1 + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.1': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@polka/url@1.0.0-next.29': {} + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@standard-schema/spec@1.0.0': {} + + '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))': + dependencies: + '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)) + + '@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.5.0 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.2 + sirv: 3.0.2 + svelte: 5.44.0 + vite: 7.2.4(@types/node@22.19.1) + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)) + debug: 4.4.3 + svelte: 5.44.0 + vite: 7.2.4(@types/node@22.19.1) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)) + debug: 4.4.3 + deepmerge: 4.3.1 + magic-string: 0.30.21 + svelte: 5.44.0 + vite: 7.2.4(@types/node@22.19.1) + vitefu: 1.1.1(vite@7.2.4(@types/node@22.19.1)) + transitivePeerDependencies: + - supports-color + + '@types/cookie@0.6.0': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.19.1': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.0 + eslint: 9.39.1 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.0 + debug: 4.4.3 + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.48.0': + dependencies: + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 + + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.1 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.48.0': {} + + '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.48.0': + dependencies: + '@typescript-eslint/types': 8.48.0 + eslint-visitor-keys: 4.2.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cookie@0.6.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + devalue@5.5.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escape-string-regexp@4.0.0: {} + + eslint-plugin-svelte@3.13.0(eslint@9.39.1)(svelte@5.44.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 9.39.1 + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-safe-parser: 7.0.1(postcss@8.5.6) + semver: 7.7.3 + svelte-eslint-parser: 1.4.0(svelte@5.44.0) + optionalDependencies: + svelte: 5.44.0 + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + isexe@2.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + known-css-properties@0.37.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + + locate-character@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + normalize.css@8.0.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss-load-config@3.1.4(postcss@8.5.6): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.6 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0): + dependencies: + prettier: 3.6.2 + svelte: 5.44.0 + + prettier@3.6.2: {} + + punycode@2.3.1: {} + + readdirp@4.1.2: {} + + resolve-from@4.0.0: {} + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + semver@7.7.3: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.44.0 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.4.0(svelte@5.44.0): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + postcss-selector-parser: 7.1.0 + optionalDependencies: + svelte: 5.44.0 + + svelte@5.44.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.5.0 + esm-env: 1.2.2 + esrap: 2.2.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + the-new-css-reset@1.11.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + totalist@3.0.1: {} + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.48.0(eslint@9.39.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@7.2.4(@types/node@22.19.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.1 + fsevents: 2.3.3 + + vitefu@1.1.1(vite@7.2.4(@types/node@22.19.1)): + optionalDependencies: + vite: 7.2.4(@types/node@22.19.1) + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yaml@1.10.2: {} + + yocto-queue@0.1.0: {} + + zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/scripts/generate-nextjs-rules.js b/scripts/generate-nextjs-rules.js new file mode 100644 index 0000000..d191173 --- /dev/null +++ b/scripts/generate-nextjs-rules.js @@ -0,0 +1,31 @@ +import { generateRules } from '../src/lib/rules-generator.js'; +import { writeFile } from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function generateNextJSRules() { + try { + console.log('Generating Next.js rules with all products enabled...'); + + const rules = await generateRules({ + sdk: 'javascript', + framework: 'nextjs', + features: ['auth', 'database', 'storage', 'functions', 'messaging', 'sites', 'realtime'] + }); + + const outputPath = join(__dirname, '..', 'AGENTS.md'); + await writeFile(outputPath, rules, 'utf-8'); + + console.log(`Rules generated successfully!`); + console.log(`Output file: ${outputPath}`); + } catch (error) { + console.error('Error generating rules:', error); + process.exit(1); + } +} + +generateNextJSRules(); + diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..0bcd3ce --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/appwrite.js b/src/lib/appwrite.js new file mode 100644 index 0000000..7b905d2 --- /dev/null +++ b/src/lib/appwrite.js @@ -0,0 +1,14 @@ +import { Client, Account } from 'appwrite'; +// @ts-ignore - SvelteKit env variables +import { PUBLIC_APPWRITE_ENDPOINT, PUBLIC_APPWRITE_PROJECT_ID } from '$env/static/public'; + +const endpoint = PUBLIC_APPWRITE_ENDPOINT || 'https://cloud.appwrite.io/v1'; +const projectId = PUBLIC_APPWRITE_PROJECT_ID || ''; + +const client = new Client() + .setEndpoint(endpoint) + .setProject(projectId); + +export const account = new Account(client); +export { client }; + diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..bd847aa --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/lib/languages/android/index.js b/src/lib/languages/android/index.js new file mode 100644 index 0000000..e446c29 --- /dev/null +++ b/src/lib/languages/android/index.js @@ -0,0 +1,72 @@ +import { getSDKVersion } from '../../utils/versions.js'; +import { clientSecurity } from '../common/security.js'; +import { getMobileImplementationGuide } from '../common/implementation-patterns.js'; + +/** + * Generates the Android SDK installation template with the latest version + * @param {string} version - The SDK version to use + * @returns {string} + */ +function generateInstallationTemplate(version) { + return `## SDK Installation + +Add the Appwrite Android SDK to your \`build.gradle.kts\`: + +**Recommended: Specify exact version for stability** + +\`\`\`kotlin +dependencies { + implementation("io.appwrite:sdk-for-android:${version}") +} +\`\`\` + +Or for Maven, add to \`pom.xml\`: + +**Recommended: Specify exact version** + +\`\`\`xml + + io.appwrite + sdk-for-android + ${version} + +\`\`\` +`; +} + +/** + * Gets the Android SDK installation template with the latest version from Appwrite's API + * This is the main export used by the rules generator + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const vanilla = async (features = []) => { + const version = await getSDKVersion('client-android'); + const installation = generateInstallationTemplate(version); + const androidImplementation = getMobileImplementationGuide('android', features); + + return `${installation} + +**Framework Documentation:** +- [Account API](https://appwrite.io/docs/references/cloud/client-web/account) - Authentication and user management +- [Databases API](https://appwrite.io/docs/references/cloud/client-web/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/client-web/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/client-web/functions) - Serverless functions execution +- [Messaging API](https://appwrite.io/docs/references/cloud/client-web/messaging) - Push notifications and messaging +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/android) + +${clientSecurity} + +${androidImplementation} + +## Android-Specific Best Practices + +- Initialize AppwriteService in Application class +- Use Hilt or Koin for dependency injection +- Use ViewModel + StateFlow for UI state +- Store session in EncryptedSharedPreferences +- Handle configuration changes properly +- Use WorkManager for background operations +- Follow Material Design guidelines +`; +}; diff --git a/src/lib/languages/apple/index.js b/src/lib/languages/apple/index.js new file mode 100644 index 0000000..504bc46 --- /dev/null +++ b/src/lib/languages/apple/index.js @@ -0,0 +1,62 @@ +import { getSDKVersion } from '../../utils/versions.js'; +import { clientSecurity } from '../common/security.js'; +import { getMobileImplementationGuide } from '../common/implementation-patterns.js'; + +/** + * Generates the Apple SDK installation template with the latest version + * @param {string} version - The SDK version to use + * @returns {string} + */ +function generateInstallationTemplate(version) { + return `## SDK Installation + +Add the Appwrite Apple SDK to your \`Package.swift\`: + +\`\`\`swift +dependencies: [ + .package(url: "https://github.com/appwrite/sdk-for-apple", from: "${version}") +] +\`\`\` + +Or add it via Xcode: +1. File → Add Packages... +2. Enter: \`https://github.com/appwrite/sdk-for-apple\` +3. Select version: \`${version}\` or later`; +} + +/** + * Gets the Apple SDK installation template with the latest version from Appwrite's API + * This is the main export used by the rules generator + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const vanilla = async (features = []) => { + const version = await getSDKVersion('client-apple'); + const installation = generateInstallationTemplate(version); + const appleImplementation = getMobileImplementationGuide('apple', features); + + return `${installation} + +**Framework Documentation:** +- [Account API](https://appwrite.io/docs/references/cloud/client-web/account) - Authentication and user management +- [Databases API](https://appwrite.io/docs/references/cloud/client-web/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/client-web/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/client-web/functions) - Serverless functions execution +- [Messaging API](https://appwrite.io/docs/references/cloud/client-web/messaging) - Push notifications and messaging +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/apple) + +${clientSecurity} + +${appleImplementation} + +## Apple Platform Best Practices + +- Use @MainActor for UI-related async operations +- Store configuration in Info.plist or xcconfig files +- Use Keychain for storing session tokens +- Use Combine or async/await for reactive programming +- Follow Human Interface Guidelines +- Support Dark Mode and Dynamic Type +- Test on multiple device sizes +`; +}; diff --git a/src/lib/languages/common/implementation-patterns.js b/src/lib/languages/common/implementation-patterns.js new file mode 100644 index 0000000..af2f4af --- /dev/null +++ b/src/lib/languages/common/implementation-patterns.js @@ -0,0 +1,5893 @@ +/** + * Comprehensive implementation patterns for Appwrite integration + * These patterns ensure secure, typed, ownership-enforced data access + */ + +/** + * General implementation rules that apply to all Appwrite apps + */ +export const generalImplementationRules = ` +## 🚨 Absolute Rules (Non-Negotiable) + +### Authoritative Access Pattern + +- **NEVER** import or invoke the Appwrite SDK directly from feature/component code +- **ALWAYS** use centralized wrapper functions for all data access +- **ALWAYS** authenticate before any data operation +- **NEVER** expose API keys to client-side code + +--- + +## Error Handling + +- Let errors bubble by default for consistent error handling +- Catch only when adding context or performing cleanup +- Always rethrow with clear error messages +- Never swallow errors silently + +--- + +## Type Safety + +- Avoid untyped/dynamic types where possible +- Define models/interfaces/structs for all data structures +- Use your language's type system to enforce constraints +- Validate inputs with appropriate validation libraries for your language +`; + +/** + * Database-specific implementation rules + */ +export const databaseImplementationRules = ` +## Database Implementation Rules + +### Database Wrapper Requirements + +Create centralized database helpers that: +- Configure the admin client with proper credentials +- Handle project, database, and table IDs +- Manage permissions automatically +- Return typed/structured objects (never raw Appwrite rows) + +- **NEVER** use TablesDB SDK directly for app data +- **ALWAYS** use centralized wrapper functions for database access + +--- + +## Ownership Enforcement (Critical) + +### User-Owned Entities (\`createdBy\`) + +Every user-owned table must include a \`createdBy\` column. + +| Operation | Rule | +|-----------|------| +| **Create** | Set \`createdBy\` to authenticated user's \`$id\` | +| **List** | Filter with \`Query.equal('createdBy', [userId])\` | +| **Read** | Verify ownership before returning data | +| **Update** | Confirm ownership; NEVER allow \`createdBy\` to change | +| **Delete** | Confirm ownership before deletion | + +### Team-Owned Entities (\`teamId\`) + +For shared workspaces and organization data: + +| Operation | Rule | +|-----------|------| +| **Create** | Set \`teamId\`; verify user is team member | +| **List** | Filter with \`Query.equal('teamId', [teamId])\` | +| **Read** | Verify team match AND user membership | +| **Update** | Confirm membership; NEVER allow \`teamId\` to change | +| **Delete** | Confirm membership before deletion | + +--- + +## Database Usage Rules + +- Import database helpers from centralized location only +- Operate on **tables**, expect **rows** +- Create payloads: exclude system columns (\`$id\`, \`$createdAt\`, \`$updatedAt\`) +- Update payloads: partial data, exclude system and ownership columns +- NEVER modify: \`$id\`, \`$createdAt\`, \`$updatedAt\`, \`createdBy\`, \`teamId\` +- Use \`Query.equal()\` for ownership filtering +- Return serialized/structured objects only (no raw SDK responses) + +--- + +## Sanitization & Payload Rules + +- Trim all user-provided strings +- Convert empty optional text to \`null\` (or language equivalent) +- Creates: require full payload (minus system fields) +- Updates: accept partial payload +- Ownership fields (\`createdBy\`, \`teamId\`) are IMMUTABLE after creation +`; + +/** + * Storage-specific implementation rules + */ +export const storageImplementationRules = ` +## Storage Implementation Rules + +### Storage Wrapper Requirements + +Create centralized storage helpers that: +- Initialize storage with proper bucket configuration +- Handle file upload/download conversions +- Manage file permissions +- Return only file IDs or data URLs (never raw binary data to client) + +- **NEVER** use Storage SDK directly for app data +- **ALWAYS** use centralized wrapper functions for storage access + +--- + +## Storage Usage Rules + +### Upload Flow (Mandatory Conversion) + +\`\`\` +Client → Server: base64 string (or file bytes) +Server: Decode base64 → bytes/buffer → InputFile +Server → Storage: InputFile +Storage → Server: File metadata +Server → Client: file ID only +\`\`\` + +Rules: +- Strip \`data:...;base64,\` prefix before processing +- Decode base64 to bytes using your language's standard library +- Use the SDK's InputFile helper for uploads +- NEVER store base64 strings in database +- Store **file IDs only** in database fields + +### Read Flow + +\`\`\` +Server: Fetch file from storage (binary/bytes) +Server: Convert bytes → base64 → data URL +Server → Client: data URL string (or secure download URL) +\`\`\` + +- NEVER expose raw binary data to client +- Always return URL strings or base64 data URLs + +--- + +## File References in Rows + +- Store file references as string arrays (\`fileIds\` field) +- On row load, fetch file URLs and inject into response +- Handle missing files gracefully +- Implement orphaned file cleanup when rows are deleted +`; + +/** + * TanStack Start specific implementation pattern + */ +export const tanstackStartPattern = ` +## TanStack Start Server Function Pattern + +### Mandatory Structure + +Every endpoint must: +1. Use \`createServerFn\` wrapper +2. Attach \`.validator(...)\` for input validation +3. Authenticate immediately in handler +4. Call ONLY centralized db/storage helpers +5. Return plain serializable objects + +### Canonical Example + +\`\`\`typescript +import { createServerFn } from '@tanstack/start' + +export const createItemFn = createServerFn({ method: 'POST' }) + .handler(async ({ data }) => { + // 1. Authenticate first + const { currentUser } = await authMiddleware() + if (!currentUser) throw new Error('Unauthorized') + + // 2. Use centralized db helper + const item = await db.items.create({ + title: data.title.trim(), + description: null, + createdBy: currentUser.$id, + teamId: data.teamId ?? null, + }) + + // 3. Return serialized object + return { item } + }) +\`\`\` + +### Route Integration + +\`\`\`typescript +// In route loader +export const Route = createFileRoute('/items')({ + loader: async () => { + const { items } = await listItemsFn() + return { items } + }, +}) + +// In component +function ItemsPage() { + const { items } = Route.useLoaderData() + const router = useRouter() + + const handleCreate = async (data) => { + await createItemFn({ data }) + router.invalidate() // Refresh data + } +} +\`\`\` + +### Environment Variables + +Required in \`@/server/lib/appwrite.ts\`: +- \`APPWRITE_ENDPOINT\` +- \`APPWRITE_PROJECT_ID\` +- \`APPWRITE_API_KEY\` +- \`APPWRITE_DATABASE_ID\` +- \`APPWRITE_BUCKET_ID\` (if using storage) +`; + +/** + * Next.js specific implementation pattern + */ +export const nextjsPattern = ` +## Next.js Server Action Pattern + +### Mandatory Structure (App Router) + +Every server action must: +1. Be marked with \`'use server'\` +2. Authenticate immediately +3. Validate input +4. Use centralized db/storage helpers only +5. Return plain serializable objects + +### Canonical Example + +\`\`\`typescript +// app/actions/items.ts +'use server' + +import { auth } from '@/lib/auth' +import { db } from '@/lib/db' + +export async function createItem(formData: FormData) { + // 1. Authenticate first + const session = await auth() + if (!session?.user) throw new Error('Unauthorized') + + // 2. Validate input + const title = formData.get('title')?.toString() + const teamId = formData.get('teamId')?.toString() + + if (!title || title.length === 0 || title.length > 120) { + throw new Error('Invalid title') + } + + // 3. Use centralized db helper + const item = await db.items.create({ + title: title.trim(), + description: null, + createdBy: session.user.id, + teamId: teamId ?? null, + }) + + // 4. Revalidate and return + revalidatePath('/items') + return { item } +} +\`\`\` + +### Route Handler Pattern + +\`\`\`typescript +// app/api/items/route.ts +import { NextResponse } from 'next/server' +import { auth } from '@/lib/auth' +import { db } from '@/lib/db' + +export async function GET() { + const session = await auth() + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const items = await db.items.listByOwner(session.user.id) + return NextResponse.json({ items }) +} + +export async function POST(request: Request) { + const session = await auth() + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const item = await db.items.create({ + ...body, + createdBy: session.user.id, + }) + + return NextResponse.json({ item }, { status: 201 }) +} +\`\`\` + +### Server Component Data Fetching + +\`\`\`typescript +// app/items/page.tsx +import { auth } from '@/lib/auth' +import { db } from '@/lib/db' + +export default async function ItemsPage() { + const session = await auth() + if (!session?.user) redirect('/login') + + const items = await db.items.listByOwner(session.user.id) + + return +} +\`\`\` + +### Environment Variables + +Required in \`.env.local\`: +- \`APPWRITE_ENDPOINT\` +- \`APPWRITE_PROJECT_ID\` +- \`APPWRITE_API_KEY\` +- \`APPWRITE_DATABASE_ID\` +- \`APPWRITE_BUCKET_ID\` (if using storage) +`; + +/** + * SvelteKit specific implementation pattern + */ +export const sveltekitPattern = ` +## SvelteKit Server Pattern + +### Mandatory Structure + +Every server operation must: +1. Use \`+server.ts\` for API routes or \`+page.server.ts\` for page data +2. Authenticate via \`locals\` or session check +3. Validate input +4. Use centralized db/storage helpers only +5. Return plain serializable objects + +### Form Actions Pattern + +\`\`\`typescript +// routes/items/+page.server.ts +import { fail, redirect } from '@sveltejs/kit' +import { db } from '$lib/server/db' + +export const actions = { + create: async ({ request, locals }) => { + // 1. Authenticate first + if (!locals.user) throw redirect(303, '/login') + + // 2. Validate input + const formData = await request.formData() + const title = formData.get('title')?.toString() + const teamId = formData.get('teamId')?.toString() + + if (!title || title.length === 0 || title.length > 120) { + return fail(400, { error: 'Invalid title' }) + } + + // 3. Use centralized db helper + const item = await db.items.create({ + title: title.trim(), + description: null, + createdBy: locals.user.id, + teamId: teamId ?? null, + }) + + return { success: true, item } + }, + + delete: async ({ request, locals }) => { + if (!locals.user) throw redirect(303, '/login') + + const formData = await request.formData() + const id = formData.get('id') as string + + // Verify ownership before delete + const item = await db.items.get(id) + if (item?.createdBy !== locals.user.id) { + return fail(403, { error: 'Forbidden' }) + } + + await db.items.delete(id) + return { success: true } + } +} +\`\`\` + +### Load Function Pattern + +\`\`\`typescript +// routes/items/+page.server.ts +import type { PageServerLoad } from './$types' +import { redirect } from '@sveltejs/kit' +import { db } from '$lib/server/db' + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) throw redirect(303, '/login') + + const items = await db.items.listByOwner(locals.user.id) + + return { items } +} +\`\`\` + +### API Route Pattern + +\`\`\`typescript +// routes/api/items/+server.ts +import { json, error } from '@sveltejs/kit' +import { db } from '$lib/server/db' + +export async function GET({ locals }) { + if (!locals.user) throw error(401, 'Unauthorized') + + const items = await db.items.listByOwner(locals.user.id) + return json({ items }) +} + +export async function POST({ request, locals }) { + if (!locals.user) throw error(401, 'Unauthorized') + + const body = await request.json() + const item = await db.items.create({ + ...body, + createdBy: locals.user.id, + }) + + return json({ item }, { status: 201 }) +} +\`\`\` + +### Environment Variables + +Required in \`.env\`: +- \`APPWRITE_ENDPOINT\` +- \`APPWRITE_PROJECT_ID\` +- \`APPWRITE_API_KEY\` +- \`APPWRITE_DATABASE_ID\` +- \`APPWRITE_BUCKET_ID\` (if using storage) +`; + +/** + * Nuxt specific implementation pattern + */ +export const nuxtPattern = ` +## Nuxt Server Pattern + +### Mandatory Structure + +Every server operation must: +1. Use \`server/api/\` routes or \`server/routes/\` +2. Authenticate via \`event.context\` or session +3. Validate input +4. Use centralized db/storage helpers only +5. Return plain serializable objects + +### API Route Pattern + +\`\`\`typescript +// server/api/items/index.get.ts +import { db } from '~/server/lib/db' + +export default defineEventHandler(async (event) => { + // 1. Authenticate first + const user = event.context.user + if (!user) { + throw createError({ statusCode: 401, message: 'Unauthorized' }) + } + + // 2. Use centralized db helper + const items = await db.items.listByOwner(user.id) + + return { items } +}) +\`\`\` + +\`\`\`typescript +// server/api/items/index.post.ts +import { db } from '~/server/lib/db' + +export default defineEventHandler(async (event) => { + // 1. Authenticate first + const user = event.context.user + if (!user) { + throw createError({ statusCode: 401, message: 'Unauthorized' }) + } + + // 2. Validate input + const body = await readBody(event) + const { title, teamId } = body + + if (!title || title.length === 0 || title.length > 120) { + throw createError({ statusCode: 400, message: 'Invalid title' }) + } + + // 3. Use centralized db helper + const item = await db.items.create({ + title: title.trim(), + description: null, + createdBy: user.id, + teamId: teamId ?? null, + }) + + return { item } +}) +\`\`\` + +### Ownership Verification + +\`\`\`typescript +// server/api/items/[id].delete.ts +import { db } from '~/server/lib/db' + +export default defineEventHandler(async (event) => { + const user = event.context.user + if (!user) { + throw createError({ statusCode: 401, message: 'Unauthorized' }) + } + + const id = getRouterParam(event, 'id') + + // Verify ownership before delete + const item = await db.items.get(id) + if (!item || item.createdBy !== user.id) { + throw createError({ statusCode: 403, message: 'Forbidden' }) + } + + await db.items.delete(id) + return { success: true } +}) +\`\`\` + +### Composables Pattern + +\`\`\`typescript +// composables/useItems.ts +export function useItems() { + const items = ref([]) + + async function fetchItems() { + const { data } = await useFetch('/api/items') + items.value = data.value?.items ?? [] + } + + async function createItem(title: string) { + await $fetch('/api/items', { + method: 'POST', + body: { title } + }) + await fetchItems() // Refresh + } + + return { items, fetchItems, createItem } +} +\`\`\` + +### Environment Variables + +Required in \`.env\` or \`nuxt.config.ts\`: +- \`APPWRITE_ENDPOINT\` +- \`APPWRITE_PROJECT_ID\` +- \`APPWRITE_API_KEY\` +- \`APPWRITE_DATABASE_ID\` +- \`APPWRITE_BUCKET_ID\` (if using storage) +`; + +/** + * Astro specific implementation pattern + */ +export const astroPattern = ` +## Astro Server Pattern + +### Mandatory Structure + +Every server operation must: +1. Use API routes in \`src/pages/api/\` +2. Authenticate via request context or session +3. Validate input +4. Use centralized db/storage helpers only +5. Return plain serializable objects + +### API Route Pattern + +\`\`\`typescript +// src/pages/api/items/index.ts +import type { APIRoute } from 'astro' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export const GET: APIRoute = async ({ request }) => { + // 1. Authenticate first + const session = await getSession(request) + if (!session?.user) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }) + } + + // 2. Use centralized db helper + const items = await db.items.listByOwner(session.user.id) + + return new Response(JSON.stringify({ items }), { + headers: { 'Content-Type': 'application/json' } + }) +} + +export const POST: APIRoute = async ({ request }) => { + const session = await getSession(request) + if (!session?.user) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }) + } + + const body = await request.json() + const { title } = body + + if (!title || title.length === 0 || title.length > 120) { + return new Response(JSON.stringify({ error: 'Invalid title' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } + + const item = await db.items.create({ + title: title.trim(), + createdBy: session.user.id, + }) + + return new Response(JSON.stringify({ item }), { + status: 201, + headers: { 'Content-Type': 'application/json' } + }) +} +\`\`\` + +### Server-Side Data Fetching + +\`\`\`astro +--- +// src/pages/items.astro +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' +import ItemsList from '@/components/ItemsList' + +const session = await getSession(Astro.request) +if (!session?.user) { + return Astro.redirect('/login') +} + +const items = await db.items.listByOwner(session.user.id) +--- + + + + +\`\`\` + +### Environment Variables + +Required in \`.env\`: +- \`APPWRITE_ENDPOINT\` +- \`APPWRITE_PROJECT_ID\` +- \`APPWRITE_API_KEY\` +- \`APPWRITE_DATABASE_ID\` +- \`APPWRITE_BUCKET_ID\` (if using storage) +`; + +/** + * Database wrapper templates for each language + */ +const databaseWrapperTemplates = { + javascript: ` +## Database Wrapper Template + +Create a centralized database helper at your designated location (e.g., \`lib/db.ts\` or \`server/lib/db.ts\`): + +\`\`\`typescript +import { Client, TablesDB, Query, ID } from 'node-appwrite' + +// Initialize admin client (server-side only) +const client = new Client() + .setEndpoint(process.env.APPWRITE_ENDPOINT!) + .setProject(process.env.APPWRITE_PROJECT_ID!) + .setKey(process.env.APPWRITE_API_KEY!) + +const tablesDB = new TablesDB(client) +const DATABASE_ID = process.env.APPWRITE_DATABASE_ID! + +// Generic CRUD helper factory +function createTable(tableId: string) { + return { + async create(data: Omit) { + const doc = await tablesDB.createRow( + DATABASE_ID, + tableId, + ID.unique(), + data + ) + return doc as T + }, + + async get(id: string) { + try { + const doc = await tablesDB.getRow(DATABASE_ID, tableId, id) + return doc as T + } catch { + return null + } + }, + + async listByOwner(userId: string) { + const response = await tablesDB.listRows(DATABASE_ID, tableId, [ + Query.equal('createdBy', [userId]), + Query.orderDesc('$createdAt'), + ]) + return response.rows as T[] + }, + + async listByTeam(teamId: string) { + const response = await tablesDB.listRows(DATABASE_ID, tableId, [ + Query.equal('teamId', [teamId]), + Query.orderDesc('$createdAt'), + ]) + return response.rows as T[] + }, + + async update(id: string, data: Partial) { + // Remove immutable columns + const { $id, $createdAt, $updatedAt, createdBy, teamId, ...updateData } = data as any + const doc = await tablesDB.updateRow(DATABASE_ID, tableId, id, updateData) + return doc as T + }, + + async delete(id: string) { + await tablesDB.deleteRow(DATABASE_ID, tableId, id) + }, + } +} + +// Export typed tables +export const db = { + items: createTable('items'), + projects: createTable('projects'), + // Add more tables as needed +} +\`\`\` +`, + + python: ` +## Database Wrapper Template + +Create a centralized database helper at \`services/db.py\`: + +\`\`\`python +import os +from typing import TypeVar, Generic, Optional, List, Dict, Any +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.query import Query +from appwrite.id import ID + +# Initialize admin client (server-side only) +client = Client() +client.set_endpoint(os.environ['APPWRITE_ENDPOINT']) +client.set_project(os.environ['APPWRITE_PROJECT_ID']) +client.set_key(os.environ['APPWRITE_API_KEY']) + +tables_db = TablesDB(client) +DATABASE_ID = os.environ['APPWRITE_DATABASE_ID'] + +T = TypeVar('T') + +class Table(Generic[T]): + def __init__(self, table_id: str): + self.table_id = table_id + + def create(self, data: Dict[str, Any]) -> T: + """Create row with auto-generated ID""" + doc = tables_db.create_row( + DATABASE_ID, + self.table_id, + ID.unique(), + data + ) + return doc + + def get(self, row_id: str) -> Optional[T]: + """Get row by ID""" + try: + return tables_db.get_row( + DATABASE_ID, + self.table_id, + row_id + ) + except Exception: + return None + + def list_by_owner(self, user_id: str) -> List[T]: + """List rows by owner""" + result = tables_db.list_rows( + DATABASE_ID, + self.table_id, + [ + Query.equal('createdBy', user_id), + Query.order_desc('$createdAt') + ] + ) + return result['rows'] + + def list_by_team(self, team_id: str) -> List[T]: + """List rows by team""" + result = tables_db.list_rows( + DATABASE_ID, + self.table_id, + [ + Query.equal('teamId', team_id), + Query.order_desc('$createdAt') + ] + ) + return result['rows'] + + def update(self, row_id: str, data: Dict[str, Any]) -> T: + """Update row (removes immutable columns)""" + safe_data = {k: v for k, v in data.items() + if k not in ('$id', '$createdAt', '$updatedAt', 'createdBy', 'teamId')} + return tables_db.update_row( + DATABASE_ID, + self.table_id, + row_id, + safe_data + ) + + def delete(self, row_id: str) -> None: + """Delete row""" + tables_db.delete_row(DATABASE_ID, self.table_id, row_id) + +# Export typed tables +items = Table('items') +projects = Table('projects') +\`\`\` +`, + + php: ` +## Database Wrapper Template + +Create a centralized database helper at \`src/Services/DatabaseService.php\`: + +\`\`\`php +tableId = $tableId; + $this->databaseId = $_ENV['APPWRITE_DATABASE_ID']; + } + + private static function getClient(): Client + { + if (self::$client === null) { + self::$client = new Client(); + self::$client + ->setEndpoint($_ENV['APPWRITE_ENDPOINT']) + ->setProject($_ENV['APPWRITE_PROJECT_ID']) + ->setKey($_ENV['APPWRITE_API_KEY']); + } + return self::$client; + } + + private static function getTablesDB(): TablesDB + { + if (self::$tablesDB === null) { + self::$tablesDB = new TablesDB(self::getClient()); + } + return self::$tablesDB; + } + + public function create(array $data): array + { + return self::getTablesDB()->createRow( + $this->databaseId, + $this->tableId, + ID::unique(), + $data + ); + } + + public function get(string $rowId): ?array + { + try { + return self::getTablesDB()->getRow( + $this->databaseId, + $this->tableId, + $rowId + ); + } catch (\\Exception $e) { + return null; + } + } + + public function listByOwner(string $userId): array + { + $result = self::getTablesDB()->listRows( + $this->databaseId, + $this->tableId, + [ + Query::equal('createdBy', [$userId]), + Query::orderDesc('\\$createdAt') + ] + ); + return $result['rows']; + } + + public function listByTeam(string $teamId): array + { + $result = self::getTablesDB()->listRows( + $this->databaseId, + $this->tableId, + [ + Query::equal('teamId', [$teamId]), + Query::orderDesc('\\$createdAt') + ] + ); + return $result['rows']; + } + + public function update(string $rowId, array $data): array + { + // Remove immutable columns + unset($data['\\$id'], $data['\\$createdAt'], $data['\\$updatedAt'], + $data['createdBy'], $data['teamId']); + + return self::getTablesDB()->updateRow( + $this->databaseId, + $this->tableId, + $rowId, + $data + ); + } + + public function delete(string $rowId): void + { + self::getTablesDB()->deleteRow( + $this->databaseId, + $this->tableId, + $rowId + ); + } +} + +// Usage: $items = new DatabaseService('items'); +\`\`\` +`, + + go: ` +## Database Wrapper Template + +Create a centralized database helper at \`internal/db/db.go\`: + +\`\`\`go +package db + +import ( + "os" + "sync" + + "github.com/appwrite/sdk-for-go/appwrite" + "github.com/appwrite/sdk-for-go/id" + "github.com/appwrite/sdk-for-go/query" +) + +var ( + client *appwrite.Client + tablesDB *appwrite.TablesDB + once sync.Once + DatabaseID string +) + +func init() { + once.Do(func() { + client = appwrite.NewClient() + client.SetEndpoint(os.Getenv("APPWRITE_ENDPOINT")) + client.SetProject(os.Getenv("APPWRITE_PROJECT_ID")) + client.SetKey(os.Getenv("APPWRITE_API_KEY")) + + tablesDB = appwrite.NewTablesDB(client) + DatabaseID = os.Getenv("APPWRITE_DATABASE_ID") + }) +} + +type Table struct { + ID string +} + +func NewTable(tableID string) *Table { + return &Table{ID: tableID} +} + +func (c *Table) Create(data map[string]interface{}) (map[string]interface{}, error) { + doc, err := tablesDB.CreateRow(DatabaseID, c.ID, id.Unique(), data) + if err != nil { + return nil, err + } + return doc.ToMap(), nil +} + +func (c *Table) Get(rowID string) (map[string]interface{}, error) { + doc, err := tablesDB.GetRow(DatabaseID, c.ID, rowID) + if err != nil { + return nil, err + } + return doc.ToMap(), nil +} + +func (c *Table) ListByOwner(userID string) ([]map[string]interface{}, error) { + result, err := tablesDB.ListRows( + DatabaseID, + c.ID, + tablesDB.WithListRowsQueries([]string{ + query.Equal("createdBy", userID), + query.OrderDesc("$createdAt"), + }), + ) + if err != nil { + return nil, err + } + + docs := make([]map[string]interface{}, len(result.Rows)) + for i, doc := range result.Rows { + docs[i] = doc.ToMap() + } + return docs, nil +} + +func (c *Table) ListByTeam(teamID string) ([]map[string]interface{}, error) { + result, err := tablesDB.ListRows( + DatabaseID, + c.ID, + tablesDB.WithListRowsQueries([]string{ + query.Equal("teamId", teamID), + query.OrderDesc("$createdAt"), + }), + ) + if err != nil { + return nil, err + } + + docs := make([]map[string]interface{}, len(result.Rows)) + for i, doc := range result.Rows { + docs[i] = doc.ToMap() + } + return docs, nil +} + +func (c *Table) Update(rowID string, data map[string]interface{}) (map[string]interface{}, error) { + // Remove immutable columns + delete(data, "$id") + delete(data, "$createdAt") + delete(data, "$updatedAt") + delete(data, "createdBy") + delete(data, "teamId") + + doc, err := tablesDB.UpdateRow(DatabaseID, c.ID, rowID, data) + if err != nil { + return nil, err + } + return doc.ToMap(), nil +} + +func (c *Table) Delete(rowID string) error { + _, err := tablesDB.DeleteRow(DatabaseID, c.ID, rowID) + return err +} + +// Exported tables +var ( + Items = NewTable("items") + Projects = NewTable("projects") +) +\`\`\` +`, + + ruby: ` +## Database Wrapper Template + +Create a centralized database helper at \`lib/database_service.rb\`: + +\`\`\`ruby +require 'appwrite' + +module DatabaseService + class << self + def client + @client ||= begin + client = Appwrite::Client.new + client + .set_endpoint(ENV['APPWRITE_ENDPOINT']) + .set_project(ENV['APPWRITE_PROJECT_ID']) + .set_key(ENV['APPWRITE_API_KEY']) + client + end + end + + def tables_db + @tables_db ||= Appwrite::TablesDB.new(client) + end + + def database_id + ENV['APPWRITE_DATABASE_ID'] + end + end + + class Table + def initialize(table_id) + @table_id = table_id + end + + def create(data) + DatabaseService.tables_db.create_row( + database_id: DatabaseService.database_id, + table_id: @table_id, + row_id: Appwrite::ID.unique, + data: data + ) + end + + def get(row_id) + DatabaseService.tables_db.get_row( + database_id: DatabaseService.database_id, + table_id: @table_id, + row_id: row_id + ) + rescue Appwrite::Exception + nil + end + + def list_by_owner(user_id) + result = DatabaseService.tables_db.list_rows( + database_id: DatabaseService.database_id, + table_id: @table_id, + queries: [ + Appwrite::Query.equal('createdBy', [user_id]), + Appwrite::Query.order_desc('$createdAt') + ] + ) + result.rows + end + + def list_by_team(team_id) + result = DatabaseService.tables_db.list_rows( + database_id: DatabaseService.database_id, + table_id: @table_id, + queries: [ + Appwrite::Query.equal('teamId', [team_id]), + Appwrite::Query.order_desc('$createdAt') + ] + ) + result.rows + end + + def update(row_id, data) + # Remove immutable fields + safe_data = data.except('$id', '$createdAt', '$updatedAt', 'createdBy', 'teamId') + + DatabaseService.tables_db.update_row( + database_id: DatabaseService.database_id, + table_id: @table_id, + row_id: row_id, + data: safe_data + ) + end + + def delete(row_id) + DatabaseService.tables_db.delete_row( + database_id: DatabaseService.database_id, + table_id: @table_id, + row_id: row_id + ) + end + end +end + +# Convenience accessors +module DB + def self.items + @items ||= DatabaseService::Table.new('items') + end + + def self.projects + @projects ||= DatabaseService::Table.new('projects') + end +end +\`\`\` +`, + + dotnet: ` +## Database Wrapper Template + +Create a centralized database helper at \`Services/DatabaseService.cs\`: + +\`\`\`csharp +using Appwrite; +using Appwrite.Services; +using Appwrite.Models; + +public class DatabaseService where T : class +{ + private static Client? _client; + private static TablesDB? _tablesDB; + private readonly string _tableId; + + public static string DatabaseId => Environment.GetEnvironmentVariable("APPWRITE_DATABASE_ID")!; + + private static Client GetClient() + { + if (_client == null) + { + _client = new Client() + .SetEndpoint(Environment.GetEnvironmentVariable("APPWRITE_ENDPOINT")!) + .SetProject(Environment.GetEnvironmentVariable("APPWRITE_PROJECT_ID")!) + .SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY")!); + } + return _client; + } + + private static TablesDB GetTablesDB() + { + return _tablesDB ??= new TablesDB(GetClient()); + } + + public DatabaseService(string tableId) + { + _tableId = tableId; + } + + public async Task CreateAsync(Dictionary data) + { + return await GetTablesDB().CreateRow( + databaseId: DatabaseId, + tableId: _tableId, + rowId: ID.Unique(), + data: data + ); + } + + public async Task GetAsync(string rowId) + { + try + { + return await GetTablesDB().GetRow( + databaseId: DatabaseId, + tableId: _tableId, + rowId: rowId + ); + } + catch (AppwriteException) + { + return null; + } + } + + public async Task> ListByOwnerAsync(string userId) + { + var result = await GetTablesDB().ListRows( + databaseId: DatabaseId, + tableId: _tableId, + queries: new List + { + Query.Equal("createdBy", new List { userId }), + Query.OrderDesc("$createdAt") + } + ); + return result.Rows; + } + + public async Task> ListByTeamAsync(string teamId) + { + var result = await GetTablesDB().ListRows( + databaseId: DatabaseId, + tableId: _tableId, + queries: new List + { + Query.Equal("teamId", new List { teamId }), + Query.OrderDesc("$createdAt") + } + ); + return result.Rows; + } + + public async Task UpdateAsync(string rowId, Dictionary data) + { + // Remove immutable columns + data.Remove("$id"); + data.Remove("$createdAt"); + data.Remove("$updatedAt"); + data.Remove("createdBy"); + data.Remove("teamId"); + + return await GetTablesDB().UpdateRow( + databaseId: DatabaseId, + tableId: _tableId, + rowId: rowId, + data: data + ); + } + + public async Task DeleteAsync(string rowId) + { + await GetTablesDB().DeleteRow( + databaseId: DatabaseId, + tableId: _tableId, + rowId: rowId + ); + } +} + +// Static accessors +public static class DB +{ + public static DatabaseService Items { get; } = new("items"); + public static DatabaseService Projects { get; } = new("projects"); +} +\`\`\` +`, + + dart: ` +## Database Wrapper Template + +Create a centralized database helper at \`lib/services/database.dart\`: + +\`\`\`dart +import 'dart:io'; +import 'package:dart_appwrite/dart_appwrite.dart'; + +class AppwriteService { + static Client? _client; + static TablesDB? _tablesDB; + + static String get databaseId => Platform.environment['APPWRITE_DATABASE_ID']!; + + static Client get client { + _client ??= Client() + .setEndpoint(Platform.environment['APPWRITE_ENDPOINT']!) + .setProject(Platform.environment['APPWRITE_PROJECT_ID']!) + .setKey(Platform.environment['APPWRITE_API_KEY']!); + return _client!; + } + + static TablesDB get tablesDB { + _tablesDB ??= TablesDB(client); + return _tablesDB!; + } +} + +class Table { + final String tableId; + + Table(this.tableId); + + Future create(Map data) async { + return await AppwriteService.tablesDB.createRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: ID.unique(), + data: data, + ); + } + + Future get(String rowId) async { + try { + return await AppwriteService.tablesDB.getRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: rowId, + ); + } on AppwriteException { + return null; + } + } + + Future> listByOwner(String userId) async { + final result = await AppwriteService.tablesDB.listRows( + databaseId: AppwriteService.databaseId, + tableId: tableId, + queries: [ + Query.equal('createdBy', userId), + Query.orderDesc(r'$createdAt'), + ], + ); + return result.rows; + } + + Future> listByTeam(String teamId) async { + final result = await AppwriteService.tablesDB.listRows( + databaseId: AppwriteService.databaseId, + tableId: tableId, + queries: [ + Query.equal('teamId', teamId), + Query.orderDesc(r'$createdAt'), + ], + ); + return result.rows; + } + + Future update(String rowId, Map data) async { + // Remove immutable columns + data.remove(r'$id'); + data.remove(r'$createdAt'); + data.remove(r'$updatedAt'); + data.remove('createdBy'); + data.remove('teamId'); + + return await AppwriteService.tablesDB.updateRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: rowId, + data: data, + ); + } + + Future delete(String rowId) async { + await AppwriteService.tablesDB.deleteRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: rowId, + ); + } +} + +// Exported tables +final items = Table('items'); +final projects = Table('projects'); +\`\`\` +`, + + kotlin: ` +## Database Wrapper Template + +Create a centralized database helper at \`services/DatabaseService.kt\`: + +\`\`\`kotlin +package com.example.services + +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.Query +import io.appwrite.models.Row +import io.appwrite.services.TablesDB + +object AppwriteService { + val databaseId: String = System.getenv("APPWRITE_DATABASE_ID") + + val client: Client by lazy { + Client() + .setEndpoint(System.getenv("APPWRITE_ENDPOINT")) + .setProject(System.getenv("APPWRITE_PROJECT_ID")) + .setKey(System.getenv("APPWRITE_API_KEY")) + } + + val tablesDB: TablesDB by lazy { TablesDB(client) } +} + +class Table(private val tableId: String) { + private val tablesDB = AppwriteService.tablesDB + private val databaseId = AppwriteService.databaseId + + suspend fun create(data: Map): Row> { + return tablesDB.createRow( + databaseId = databaseId, + tableId = tableId, + rowId = ID.unique(), + data = data + ) + } + + suspend fun get(rowId: String): Row>? { + return try { + tablesDB.getRow( + databaseId = databaseId, + tableId = tableId, + rowId = rowId + ) + } catch (e: Exception) { + null + } + } + + suspend fun listByOwner(userId: String): List>> { + val result = tablesDB.listRows( + databaseId = databaseId, + tableId = tableId, + queries = listOf( + Query.equal("createdBy", listOf(userId)), + Query.orderDesc("\$createdAt") + ) + ) + return result.rows + } + + suspend fun listByTeam(teamId: String): List>> { + val result = tablesDB.listRows( + databaseId = databaseId, + tableId = tableId, + queries = listOf( + Query.equal("teamId", listOf(teamId)), + Query.orderDesc("\$createdAt") + ) + ) + return result.rows + } + + suspend fun update(rowId: String, data: Map): Row> { + // Remove immutable columns + val safeData = data.filterKeys { + it !in listOf("\$id", "\$createdAt", "\$updatedAt", "createdBy", "teamId") + } + + return tablesDB.updateRow( + databaseId = databaseId, + tableId = tableId, + rowId = rowId, + data = safeData + ) + } + + suspend fun delete(rowId: String) { + tablesDB.deleteRow( + databaseId = databaseId, + tableId = tableId, + rowId = rowId + ) + } +} + +// Exported tables +object DB { + val items = Table("items") + val projects = Table("projects") +} +\`\`\` +`, + + swift: ` +## Database Wrapper Template + +Create a centralized database helper at \`Services/DatabaseService.swift\`: + +\`\`\`swift +import Appwrite +import Foundation + +class AppwriteService { + static let shared = AppwriteService() + + let client: Client + let tablesDB: TablesDB + let databaseId: String + + private init() { + client = Client() + .setEndpoint(ProcessInfo.processInfo.environment["APPWRITE_ENDPOINT"]!) + .setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!) + .setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!) + + tablesDB = TablesDB(client) + databaseId = ProcessInfo.processInfo.environment["APPWRITE_DATABASE_ID"]! + } +} + +class Table { + let tableId: String + private let tablesDB: TablesDB + private let databaseId: String + + init(_ tableId: String) { + self.tableId = tableId + self.tablesDB = AppwriteService.shared.tablesDB + self.databaseId = AppwriteService.shared.databaseId + } + + func create(data: [String: Any]) async throws -> Row<[String: AnyCodable]> { + return try await tablesDB.createRow( + databaseId: databaseId, + tableId: tableId, + rowId: ID.unique(), + data: data + ) + } + + func get(rowId: String) async throws -> Row<[String: AnyCodable]>? { + do { + return try await tablesDB.getRow( + databaseId: databaseId, + tableId: tableId, + rowId: rowId + ) + } catch { + return nil + } + } + + func listByOwner(userId: String) async throws -> [Row<[String: AnyCodable]>] { + let result = try await tablesDB.listRows( + databaseId: databaseId, + tableId: tableId, + queries: [ + Query.equal("createdBy", value: userId), + Query.orderDesc("$createdAt") + ] + ) + return result.rows + } + + func listByTeam(teamId: String) async throws -> [Row<[String: AnyCodable]>] { + let result = try await tablesDB.listRows( + databaseId: databaseId, + tableId: tableId, + queries: [ + Query.equal("teamId", value: teamId), + Query.orderDesc("$createdAt") + ] + ) + return result.rows + } + + func update(rowId: String, data: [String: Any]) async throws -> Row<[String: AnyCodable]> { + // Remove immutable columns + var safeData = data + safeData.removeValue(forKey: "$id") + safeData.removeValue(forKey: "$createdAt") + safeData.removeValue(forKey: "$updatedAt") + safeData.removeValue(forKey: "createdBy") + safeData.removeValue(forKey: "teamId") + + return try await tablesDB.updateRow( + databaseId: databaseId, + tableId: tableId, + rowId: rowId, + data: safeData + ) + } + + func delete(rowId: String) async throws { + _ = try await tablesDB.deleteRow( + databaseId: databaseId, + tableId: tableId, + rowId: rowId + ) + } +} + +// Exported tables +struct DB { + static let items = Table("items") + static let projects = Table("projects") +} +\`\`\` +` +}; + +/** + * Get database wrapper template for a specific SDK + * @param {string} sdk - SDK name (javascript, python, php, etc.) + * @returns {string} Database wrapper template + */ +export function getDatabaseWrapperTemplate(sdk) { + // Map SDK names to template keys + /** @type {Record} */ + const sdkMap = { + javascript: 'javascript', + 'react-native': 'javascript', + python: 'python', + php: 'php', + go: 'go', + flutter: 'dart', + dart: 'dart', + apple: 'swift', + android: 'kotlin', + swift: 'swift', + kotlin: 'kotlin', + ruby: 'ruby', + dotnet: 'dotnet' + }; + + const templateKey = sdkMap[sdk] || 'javascript'; + /** @type {Record} */ + const templates = databaseWrapperTemplates; + return templates[templateKey] || templates.javascript; +} + +// Legacy export for backwards compatibility +export const databaseWrapperTemplate = databaseWrapperTemplates.javascript; + +/** + * Storage wrapper templates for each language + */ +const storageWrapperTemplates = { + javascript: ` +## Storage Wrapper Template + +Create a centralized storage helper: + +\`\`\`typescript +import { Client, Storage, ID } from 'node-appwrite' +import { InputFile } from 'node-appwrite/file' + +const client = new Client() + .setEndpoint(process.env.APPWRITE_ENDPOINT!) + .setProject(process.env.APPWRITE_PROJECT_ID!) + .setKey(process.env.APPWRITE_API_KEY!) + +const storage = new Storage(client) +const BUCKET_ID = process.env.APPWRITE_BUCKET_ID! + +export const fileStorage = { + /** + * Upload file from base64 string + * @returns File ID only (never store base64 in database) + */ + async upload(base64Data: string, fileName: string, mimeType: string) { + // Strip data URL prefix if present + const base64Clean = base64Data.replace(/^data:[^;]+;base64,/, '') + + // Convert to buffer + const buffer = Buffer.from(base64Clean, 'base64') + + // Create InputFile + const inputFile = InputFile.fromBuffer(buffer, fileName) + + // Upload to storage + const file = await storage.createFile(BUCKET_ID, ID.unique(), inputFile) + + return file.$id // Return ID only + }, + + /** + * Get file as data URL for client consumption + */ + async getAsDataUrl(fileId: string, mimeType: string) { + const arrayBuffer = await storage.getFileDownload(BUCKET_ID, fileId) + const base64 = Buffer.from(arrayBuffer).toString('base64') + return \`data:\${mimeType};base64,\${base64}\` + }, + + /** + * Get file preview URL + */ + getPreviewUrl(fileId: string, width?: number, height?: number) { + return storage.getFilePreview(BUCKET_ID, fileId, width, height) + }, + + /** + * Delete file + */ + async delete(fileId: string) { + await storage.deleteFile(BUCKET_ID, fileId) + }, + + /** + * Delete multiple files (for cleanup) + */ + async deleteMany(fileIds: string[]) { + await Promise.allSettled( + fileIds.map(id => storage.deleteFile(BUCKET_ID, id)) + ) + }, +} +\`\`\` +`, + + python: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`services/storage.py\`: + +\`\`\`python +import os +import base64 +from typing import Optional +from appwrite.client import Client +from appwrite.services.storage import Storage +from appwrite.id import ID +from appwrite.input_file import InputFile + +# Initialize admin client (server-side only) +client = Client() +client.set_endpoint(os.environ['APPWRITE_ENDPOINT']) +client.set_project(os.environ['APPWRITE_PROJECT_ID']) +client.set_key(os.environ['APPWRITE_API_KEY']) + +storage = Storage(client) +BUCKET_ID = os.environ.get('APPWRITE_BUCKET_ID', '') + +class FileStorage: + @staticmethod + def upload(base64_data: str, file_name: str, mime_type: str) -> str: + """ + Upload file from base64 string + Returns: File ID only (never store base64 in database) + """ + # Strip data URL prefix if present + if ';base64,' in base64_data: + base64_data = base64_data.split(';base64,')[1] + + # Decode base64 to bytes + file_bytes = base64.b64decode(base64_data) + + # Create InputFile from bytes + input_file = InputFile.from_bytes(file_bytes, file_name, mime_type) + + # Upload to storage + result = storage.create_file(BUCKET_ID, ID.unique(), input_file) + + return result['$id'] + + @staticmethod + def get_as_data_url(file_id: str, mime_type: str) -> str: + """Get file as data URL for client consumption""" + file_bytes = storage.get_file_download(BUCKET_ID, file_id) + base64_str = base64.b64encode(file_bytes).decode('utf-8') + return f"data:{mime_type};base64,{base64_str}" + + @staticmethod + def get_preview_url(file_id: str, width: Optional[int] = None, height: Optional[int] = None) -> str: + """Get file preview URL""" + return storage.get_file_preview(BUCKET_ID, file_id, width=width, height=height) + + @staticmethod + def delete(file_id: str) -> None: + """Delete file""" + storage.delete_file(BUCKET_ID, file_id) + + @staticmethod + def delete_many(file_ids: list) -> None: + """Delete multiple files (for cleanup)""" + for file_id in file_ids: + try: + storage.delete_file(BUCKET_ID, file_id) + except Exception: + pass # Continue even if some files fail + +file_storage = FileStorage() +\`\`\` +`, + + php: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`src/Services/StorageService.php\`: + +\`\`\`php +bucketId = $_ENV['APPWRITE_BUCKET_ID']; + } + + private static function getClient(): Client + { + if (self::$client === null) { + self::$client = new Client(); + self::$client + ->setEndpoint($_ENV['APPWRITE_ENDPOINT']) + ->setProject($_ENV['APPWRITE_PROJECT_ID']) + ->setKey($_ENV['APPWRITE_API_KEY']); + } + return self::$client; + } + + private static function getStorage(): Storage + { + if (self::$storage === null) { + self::$storage = new Storage(self::getClient()); + } + return self::$storage; + } + + /** + * Upload file from base64 string + * @return string File ID only (never store base64 in database) + */ + public function upload(string $base64Data, string $fileName, string $mimeType): string + { + // Strip data URL prefix if present + if (strpos($base64Data, ';base64,') !== false) { + $base64Data = explode(';base64,', $base64Data)[1]; + } + + // Decode base64 to binary + $fileData = base64_decode($base64Data); + + // Create InputFile + $inputFile = InputFile::withData($fileData, $fileName, $mimeType); + + // Upload to storage + $file = self::getStorage()->createFile($this->bucketId, ID::unique(), $inputFile); + + return $file['\\$id']; + } + + /** + * Get file as data URL for client consumption + */ + public function getAsDataUrl(string $fileId, string $mimeType): string + { + $fileData = self::getStorage()->getFileDownload($this->bucketId, $fileId); + $base64 = base64_encode($fileData); + return "data:{$mimeType};base64,{$base64}"; + } + + /** + * Get file preview URL + */ + public function getPreviewUrl(string $fileId, ?int $width = null, ?int $height = null): string + { + return self::getStorage()->getFilePreview($this->bucketId, $fileId, $width, $height); + } + + /** + * Delete file + */ + public function delete(string $fileId): void + { + self::getStorage()->deleteFile($this->bucketId, $fileId); + } + + /** + * Delete multiple files (for cleanup) + */ + public function deleteMany(array $fileIds): void + { + foreach ($fileIds as $fileId) { + try { + self::getStorage()->deleteFile($this->bucketId, $fileId); + } catch (\\Exception $e) { + // Continue even if some files fail + } + } + } +} +\`\`\` +`, + + go: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`internal/storage/storage.go\`: + +\`\`\`go +package storage + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + + "github.com/appwrite/sdk-for-go/appwrite" + "github.com/appwrite/sdk-for-go/file" + "github.com/appwrite/sdk-for-go/id" +) + +var ( + client *appwrite.Client + storage *appwrite.Storage + BucketID string +) + +func init() { + client = appwrite.NewClient() + client.SetEndpoint(os.Getenv("APPWRITE_ENDPOINT")) + client.SetProject(os.Getenv("APPWRITE_PROJECT_ID")) + client.SetKey(os.Getenv("APPWRITE_API_KEY")) + + storage = appwrite.NewStorage(client) + BucketID = os.Getenv("APPWRITE_BUCKET_ID") +} + +// Upload file from base64 string, returns File ID only +func Upload(base64Data, fileName, mimeType string) (string, error) { + // Strip data URL prefix if present + if idx := strings.Index(base64Data, ";base64,"); idx != -1 { + base64Data = base64Data[idx+8:] + } + + // Decode base64 to bytes + fileBytes, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + return "", err + } + + // Create InputFile + inputFile := file.NewInputFile(fileBytes, fileName) + + // Upload to storage + result, err := storage.CreateFile(BucketID, id.Unique(), inputFile) + if err != nil { + return "", err + } + + return result.Id, nil +} + +// GetAsDataUrl returns file as data URL for client consumption +func GetAsDataUrl(fileId, mimeType string) (string, error) { + fileBytes, err := storage.GetFileDownload(BucketID, fileId) + if err != nil { + return "", err + } + + base64Str := base64.StdEncoding.EncodeToString(fileBytes) + return fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str), nil +} + +// GetPreviewUrl returns file preview URL +func GetPreviewUrl(fileId string, width, height int) string { + return storage.GetFilePreview(BucketID, fileId, + storage.WithGetFilePreviewWidth(width), + storage.WithGetFilePreviewHeight(height)) +} + +// Delete removes a file +func Delete(fileId string) error { + _, err := storage.DeleteFile(BucketID, fileId) + return err +} + +// DeleteMany removes multiple files (for cleanup) +func DeleteMany(fileIds []string) { + for _, fileId := range fileIds { + _ = Delete(fileId) // Continue even if some fail + } +} +\`\`\` +`, + + ruby: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`lib/storage_service.rb\`: + +\`\`\`ruby +require 'appwrite' +require 'base64' + +module StorageService + class << self + def client + @client ||= begin + client = Appwrite::Client.new + client + .set_endpoint(ENV['APPWRITE_ENDPOINT']) + .set_project(ENV['APPWRITE_PROJECT_ID']) + .set_key(ENV['APPWRITE_API_KEY']) + client + end + end + + def storage + @storage ||= Appwrite::Storage.new(client) + end + + def bucket_id + ENV['APPWRITE_BUCKET_ID'] + end + end + + class FileStorage + # Upload file from base64 string, returns File ID only + def upload(base64_data, file_name, mime_type) + # Strip data URL prefix if present + if base64_data.include?(';base64,') + base64_data = base64_data.split(';base64,').last + end + + # Decode base64 to bytes + file_bytes = Base64.decode64(base64_data) + + # Create InputFile + input_file = Appwrite::InputFile.from_bytes(file_bytes, file_name, mime_type) + + # Upload to storage + result = StorageService.storage.create_file( + bucket_id: StorageService.bucket_id, + file_id: Appwrite::ID.unique, + file: input_file + ) + + result['$id'] + end + + # Get file as data URL for client consumption + def get_as_data_url(file_id, mime_type) + file_bytes = StorageService.storage.get_file_download( + bucket_id: StorageService.bucket_id, + file_id: file_id + ) + base64_str = Base64.strict_encode64(file_bytes) + "data:#{mime_type};base64,#{base64_str}" + end + + # Get file preview URL + def get_preview_url(file_id, width: nil, height: nil) + StorageService.storage.get_file_preview( + bucket_id: StorageService.bucket_id, + file_id: file_id, + width: width, + height: height + ) + end + + # Delete file + def delete(file_id) + StorageService.storage.delete_file( + bucket_id: StorageService.bucket_id, + file_id: file_id + ) + end + + # Delete multiple files (for cleanup) + def delete_many(file_ids) + file_ids.each do |file_id| + delete(file_id) + rescue StandardError + # Continue even if some files fail + end + end + end +end + +# Convenience accessor +def file_storage + @file_storage ||= StorageService::FileStorage.new +end +\`\`\` +`, + + dotnet: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`Services/StorageService.cs\`: + +\`\`\`csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Appwrite; +using Appwrite.Services; + +public class StorageService +{ + private static Client? _client; + private static Storage? _storage; + private static string BucketId => Environment.GetEnvironmentVariable("APPWRITE_BUCKET_ID")!; + + private static Client GetClient() + { + if (_client == null) + { + _client = new Client() + .SetEndpoint(Environment.GetEnvironmentVariable("APPWRITE_ENDPOINT")!) + .SetProject(Environment.GetEnvironmentVariable("APPWRITE_PROJECT_ID")!) + .SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY")!); + } + return _client; + } + + private static Storage GetStorage() + { + return _storage ??= new Storage(GetClient()); + } + + /// + /// Upload file from base64 string + /// + /// File ID only (never store base64 in database) + public static async Task UploadAsync(string base64Data, string fileName, string mimeType) + { + // Strip data URL prefix if present + if (base64Data.Contains(";base64,")) + { + base64Data = base64Data.Split(";base64,")[1]; + } + + // Decode base64 to bytes + var fileBytes = Convert.FromBase64String(base64Data); + + // Create InputFile + var inputFile = InputFile.FromBytes(fileBytes, fileName, mimeType); + + // Upload to storage + var file = await GetStorage().CreateFile( + bucketId: BucketId, + fileId: ID.Unique(), + file: inputFile + ); + + return file.Id; + } + + /// + /// Get file as data URL for client consumption + /// + public static async Task GetAsDataUrlAsync(string fileId, string mimeType) + { + var fileBytes = await GetStorage().GetFileDownload(BucketId, fileId); + var base64Str = Convert.ToBase64String(fileBytes); + return $"data:{mimeType};base64,{base64Str}"; + } + + /// + /// Get file preview URL + /// + public static string GetPreviewUrl(string fileId, int? width = null, int? height = null) + { + return GetStorage().GetFilePreview(BucketId, fileId, width: width, height: height); + } + + /// + /// Delete file + /// + public static async Task DeleteAsync(string fileId) + { + await GetStorage().DeleteFile(BucketId, fileId); + } + + /// + /// Delete multiple files (for cleanup) + /// + public static async Task DeleteManyAsync(IEnumerable fileIds) + { + foreach (var fileId in fileIds) + { + try + { + await GetStorage().DeleteFile(BucketId, fileId); + } + catch + { + // Continue even if some files fail + } + } + } +} +\`\`\` +`, + + dart: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`lib/services/storage.dart\`: + +\`\`\`dart +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:dart_appwrite/dart_appwrite.dart'; + +class AppwriteStorage { + static Client? _client; + static Storage? _storage; + + static String get bucketId => Platform.environment['APPWRITE_BUCKET_ID']!; + + static Client get client { + _client ??= Client() + .setEndpoint(Platform.environment['APPWRITE_ENDPOINT']!) + .setProject(Platform.environment['APPWRITE_PROJECT_ID']!) + .setKey(Platform.environment['APPWRITE_API_KEY']!); + return _client!; + } + + static Storage get storage { + _storage ??= Storage(client); + return _storage!; + } +} + +class FileStorageService { + /// Upload file from base64 string + /// Returns: File ID only (never store base64 in database) + Future upload(String base64Data, String fileName, String mimeType) async { + // Strip data URL prefix if present + if (base64Data.contains(';base64,')) { + base64Data = base64Data.split(';base64,').last; + } + + // Decode base64 to bytes + final Uint8List fileBytes = base64Decode(base64Data); + + // Create InputFile + final inputFile = InputFile.fromBytes( + bytes: fileBytes, + filename: fileName, + contentType: mimeType, + ); + + // Upload to storage + final file = await AppwriteStorage.storage.createFile( + bucketId: AppwriteStorage.bucketId, + fileId: ID.unique(), + file: inputFile, + ); + + return file.\$id; + } + + /// Get file as data URL for client consumption + Future getAsDataUrl(String fileId, String mimeType) async { + final fileBytes = await AppwriteStorage.storage.getFileDownload( + bucketId: AppwriteStorage.bucketId, + fileId: fileId, + ); + final base64Str = base64Encode(fileBytes); + return 'data:$mimeType;base64,$base64Str'; + } + + /// Get file preview URL + String getPreviewUrl(String fileId, {int? width, int? height}) { + return AppwriteStorage.storage.getFilePreview( + bucketId: AppwriteStorage.bucketId, + fileId: fileId, + width: width, + height: height, + ).toString(); + } + + /// Delete file + Future delete(String fileId) async { + await AppwriteStorage.storage.deleteFile( + bucketId: AppwriteStorage.bucketId, + fileId: fileId, + ); + } + + /// Delete multiple files (for cleanup) + Future deleteMany(List fileIds) async { + for (final fileId in fileIds) { + try { + await delete(fileId); + } catch (_) { + // Continue even if some files fail + } + } + } +} + +final fileStorage = FileStorageService(); +\`\`\` +`, + + kotlin: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`services/StorageService.kt\`: + +\`\`\`kotlin +package com.example.services + +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.models.File +import io.appwrite.services.Storage +import java.util.Base64 + +object AppwriteStorageService { + val bucketId: String = System.getenv("APPWRITE_BUCKET_ID") + + val client: Client by lazy { + Client() + .setEndpoint(System.getenv("APPWRITE_ENDPOINT")) + .setProject(System.getenv("APPWRITE_PROJECT_ID")) + .setKey(System.getenv("APPWRITE_API_KEY")) + } + + val storage: Storage by lazy { Storage(client) } +} + +class FileStorageService { + private val storage = AppwriteStorageService.storage + private val bucketId = AppwriteStorageService.bucketId + + /** + * Upload file from base64 string + * @return File ID only (never store base64 in database) + */ + suspend fun upload(base64Data: String, fileName: String, mimeType: String): String { + // Strip data URL prefix if present + val cleanBase64 = if (base64Data.contains(";base64,")) { + base64Data.substringAfter(";base64,") + } else { + base64Data + } + + // Decode base64 to bytes + val fileBytes = Base64.getDecoder().decode(cleanBase64) + + // Upload to storage + val file = storage.createFile( + bucketId = bucketId, + fileId = ID.unique(), + file = io.appwrite.models.InputFile.fromBytes(fileBytes, fileName, mimeType) + ) + + return file.id + } + + /** + * Get file as data URL for client consumption + */ + suspend fun getAsDataUrl(fileId: String, mimeType: String): String { + val fileBytes = storage.getFileDownload(bucketId, fileId) + val base64Str = Base64.getEncoder().encodeToString(fileBytes) + return "data:\$mimeType;base64,\$base64Str" + } + + /** + * Get file preview URL + */ + fun getPreviewUrl(fileId: String, width: Int? = null, height: Int? = null): String { + return storage.getFilePreview(bucketId, fileId, width = width, height = height) + } + + /** + * Delete file + */ + suspend fun delete(fileId: String) { + storage.deleteFile(bucketId, fileId) + } + + /** + * Delete multiple files (for cleanup) + */ + suspend fun deleteMany(fileIds: List) { + fileIds.forEach { fileId -> + try { + delete(fileId) + } catch (_: Exception) { + // Continue even if some files fail + } + } + } +} + +val fileStorage = FileStorageService() +\`\`\` +`, + + swift: ` +## Storage Wrapper Template + +Create a centralized storage helper at \`Services/StorageService.swift\`: + +\`\`\`swift +import Appwrite +import Foundation + +class AppwriteStorageService { + static let shared = AppwriteStorageService() + + let client: Client + let storage: Storage + let bucketId: String + + private init() { + client = Client() + .setEndpoint(ProcessInfo.processInfo.environment["APPWRITE_ENDPOINT"]!) + .setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!) + .setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!) + + storage = Storage(client) + bucketId = ProcessInfo.processInfo.environment["APPWRITE_BUCKET_ID"] ?? "" + } +} + +class FileStorageService { + private let storage = AppwriteStorageService.shared.storage + private let bucketId = AppwriteStorageService.shared.bucketId + + /// Upload file from base64 string + /// Returns: File ID only (never store base64 in database) + func upload(base64Data: String, fileName: String, mimeType: String) async throws -> String { + // Strip data URL prefix if present + var cleanBase64 = base64Data + if let range = base64Data.range(of: ";base64,") { + cleanBase64 = String(base64Data[range.upperBound...]) + } + + // Decode base64 to bytes + guard let fileData = Data(base64Encoded: cleanBase64) else { + throw NSError(domain: "StorageService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 data"]) + } + + // Create InputFile + let inputFile = InputFile.fromData(fileData, filename: fileName, mimeType: mimeType) + + // Upload to storage + let file = try await storage.createFile( + bucketId: bucketId, + fileId: ID.unique(), + file: inputFile + ) + + return file.id + } + + /// Get file as data URL for client consumption + func getAsDataUrl(fileId: String, mimeType: String) async throws -> String { + let fileData = try await storage.getFileDownload(bucketId: bucketId, fileId: fileId) + let base64Str = fileData.base64EncodedString() + return "data:\\(mimeType);base64,\\(base64Str)" + } + + /// Get file preview URL + func getPreviewUrl(fileId: String, width: Int? = nil, height: Int? = nil) -> String { + return storage.getFilePreview( + bucketId: bucketId, + fileId: fileId, + width: width, + height: height + ) + } + + /// Delete file + func delete(fileId: String) async throws { + _ = try await storage.deleteFile(bucketId: bucketId, fileId: fileId) + } + + /// Delete multiple files (for cleanup) + func deleteMany(fileIds: [String]) async { + for fileId in fileIds { + do { + try await delete(fileId: fileId) + } catch { + // Continue even if some files fail + } + } + } +} + +let fileStorage = FileStorageService() +\`\`\` +` +}; + +/** + * Get storage wrapper template for a specific SDK + * @param {string} sdk - SDK name (javascript, python, php, etc.) + * @returns {string} Storage wrapper template + */ +export function getStorageWrapperTemplate(sdk) { + // Map SDK names to template keys + /** @type {Record} */ + const sdkMap = { + javascript: 'javascript', + 'react-native': 'javascript', + python: 'python', + php: 'php', + go: 'go', + flutter: 'dart', + dart: 'dart', + apple: 'swift', + android: 'kotlin', + swift: 'swift', + kotlin: 'kotlin', + ruby: 'ruby', + dotnet: 'dotnet' + }; + + const templateKey = sdkMap[sdk] || 'javascript'; + /** @type {Record} */ + const templates = storageWrapperTemplates; + return templates[templateKey] || templates.javascript; +} + +// Legacy export for backwards compatibility +export const storageWrapperTemplate = storageWrapperTemplates.javascript; + +/** + * Functions implementation pattern (language-agnostic) + */ +export const functionsPattern = ` +## Functions Integration Pattern + +**Documentation:** [Functions Quick Start](https://appwrite.io/docs/products/functions/quick-start) + +### When to Use Appwrite Functions + +- **Scheduled tasks**: Cron jobs, periodic cleanup, report generation +- **Event-driven processing**: Triggered by database changes, file uploads, user events +- **Background jobs**: Long-running operations, email sending, data processing +- **Webhooks**: Third-party integrations, payment processing +- **Server-side logic**: Operations requiring elevated permissions + +### Execution Patterns + +| Pattern | Use Case | Method | +|---------|----------|--------| +| **Synchronous** | Immediate response needed | \`createExecution(functionId, body, async=false)\` | +| **Asynchronous** | Fire-and-forget, long tasks | \`createExecution(functionId, body, async=true)\` | +| **Scheduled** | Cron-based triggers | Configure in Console/appwrite.json | +| **Event-driven** | Database/storage triggers | Configure event subscriptions | + +### Implementation Steps + +1. **Initialize Functions service** with your SDK's client +2. **Call \`createExecution()\`** with: + - \`functionId\`: The function's unique ID + - \`body\`: JSON string of data to pass + - \`async\`: Boolean for sync/async execution + - \`path\`: Optional route path (default: "/") + - \`method\`: HTTP method (GET, POST, etc.) +3. **Handle response**: Check \`status\` field for success/failure +4. **Parse \`responseBody\`** as JSON for the result + +### Function Development Best Practices + +- Use [Appwrite Function Templates](https://github.com/appwrite/templates) as starting points +- Store secrets in function environment variables, not in code +- Return JSON responses for easy parsing +- Implement proper error handling and logging +- Use typed request/response structures for your language + +### Resources + +- [Develop Functions](https://appwrite.io/docs/products/functions/develop) +- [Execute Functions](https://appwrite.io/docs/products/functions/execute) +- [Function Runtimes](https://appwrite.io/docs/products/functions/runtimes) +- [Event Triggers](https://appwrite.io/docs/advanced/platform/events) +`; + +/** + * Messaging implementation pattern (language-agnostic) + */ +export const messagingPattern = ` +## Messaging Integration Pattern + +**Documentation:** [Messaging Overview](https://appwrite.io/docs/products/messaging) + +### Message Types + +| Type | Method | Use Case | +|------|--------|----------| +| **Email** | \`createEmail()\` | Transactional emails, notifications | +| **Push** | \`createPush()\` | Mobile/web push notifications | +| **SMS** | \`createSms()\` | Text messages, verification codes | + +### Targeting Options + +Messages can be sent to: +- **Users**: Array of user IDs (\`users\` parameter) +- **Targets**: Specific device/endpoint IDs (\`targets\` parameter) +- **Topics**: Broadcast to subscribers (\`topics\` parameter) + +### Implementation Steps + +1. **Initialize Messaging service** with your SDK's client (requires API key) +2. **Create message** using the appropriate method: + - \`createEmail(messageId, subject, content, topics, users, targets, ...)\` + - \`createPush(messageId, title, body, topics, users, targets, data)\` + - \`createSms(messageId, content, topics, users, targets)\` +3. **Handle delivery status** by checking the returned message object + +### Topic Subscriptions + +- **Subscribe**: \`createSubscriber(topicId, subscriberId, targetId)\` +- **Unsubscribe**: \`deleteSubscriber(topicId, subscriberId)\` + +### Provider Configuration Required + +| Channel | Providers | +|---------|-----------| +| **Email** | SMTP, Mailgun, SendGrid, Mailchimp | +| **Push** | FCM (Android), APNS (iOS) | +| **SMS** | Twilio, Vonage, Textmagic, Telesign | + +Configure providers in Appwrite Console → Messaging → Providers + +### Best Practices + +- Generate unique message IDs for each send +- Handle message failures gracefully +- Use topics for broadcast messages, targets for specific devices +- Store provider credentials securely in Console +- Implement retry logic for failed deliveries + +### Resources + +- [Send Email](https://appwrite.io/docs/products/messaging/send-email-messages) +- [Send Push](https://appwrite.io/docs/products/messaging/send-push-notifications) +- [Send SMS](https://appwrite.io/docs/products/messaging/send-sms-messages) +- [Topics](https://appwrite.io/docs/products/messaging/topics) +`; + +/** + * Realtime implementation pattern (language-agnostic) + */ +export const realtimePattern = ` +## Realtime Integration Pattern + +**Documentation:** [Realtime Overview](https://appwrite.io/docs/products/realtime) + +### Overview + +Realtime subscriptions allow your app to receive live updates when data changes. Subscriptions run on the **client side** using the client SDK. + +### Subscription Flow + +1. **Initialize client** with endpoint and project ID +2. **Subscribe to channel(s)** using \`client.subscribe(channels, callback)\` +3. **Handle events** in the callback (create, update, delete) +4. **Unsubscribe** when component unmounts or no longer needed + +### Realtime Channels Reference + +| Channel Pattern | Description | +|-----------------|-------------| +| \`databases.[DB_ID].tables.[TABLE_ID].rows\` | All rows in table | +| \`databases.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]\` | Specific row | +| \`buckets.[BUCKET_ID].files\` | All files in bucket | +| \`buckets.[BUCKET_ID].files.[FILE_ID]\` | Specific file | +| \`account\` | Current user's account changes | +| \`teams\` | Team membership changes | +| \`teams.[TEAM_ID]\` | Specific team changes | + +### Event Types + +The callback receives an event object with: +- \`events\`: Array of event strings (e.g., \`databases.*.tables.*.rows.*.create\`) +- \`payload\`: The row/file/account data that changed + +| Event | Triggered When | +|-------|----------------| +| \`*.create\` | New row/file created | +| \`*.update\` | Existing row/file updated | +| \`*.delete\` | Row/file deleted | + +### Implementation Pattern + +\`\`\` +// Pseudocode for any SDK +subscription = client.subscribe( + ["databases.DB_ID.tables.TABLE_ID.rows"], + function(event) { + if event.type contains "create": + add event.payload to local state + else if event.type contains "update": + update matching item in local state + else if event.type contains "delete": + remove matching item from local state + } +) + +// On cleanup/unmount: +subscription.close() // or unsubscribe() +\`\`\` + +### Best Practices + +- **Always unsubscribe** in cleanup/dispose functions +- Use **client SDK** for realtime (not server SDK) +- Filter events client-side if you only need specific event types +- Implement **reconnection logic** for dropped connections +- Consider **optimistic updates** combined with realtime for better UX +- Use **initial data from server** then enhance with realtime updates +- Handle the case where connection drops and reconnects + +### Resources + +- [Subscribe to Databases](https://appwrite.io/docs/products/realtime/subscribe-to-databases) +- [Subscribe to Storage](https://appwrite.io/docs/products/realtime/subscribe-to-storage) +- [Subscribe to Account](https://appwrite.io/docs/products/realtime/subscribe-to-account) +- [Channels Reference](https://appwrite.io/docs/products/realtime/channels) +`; + +/** + * Sites deployment notes (primarily documentation reference) + */ +export const sitesPattern = ` +## Sites Deployment Pattern + +Appwrite Sites is a hosting platform for static and SSR applications. Configuration is primarily done through the Appwrite Console or CLI. + +### Deployment Methods + +1. **Git Integration** (Recommended) + - Connect your repository in Appwrite Console + - Automatic deployments on push to configured branch + - See: [Deploy from Git](https://appwrite.io/docs/products/sites/deploy-from-git) + +2. **CLI Deployment** + - Use \`appwrite deploy\` command + - See: [Deploy from CLI](https://appwrite.io/docs/products/sites/deploy-from-cli) + +3. **Manual Upload** + - Upload built assets directly + - See: [Deploy Manually](https://appwrite.io/docs/products/sites/deploy-manually) + +### Environment Variables + +Configure in Appwrite Console under Site settings: +- \`APPWRITE_ENDPOINT\` - Your Appwrite endpoint +- \`APPWRITE_PROJECT_ID\` - Your project ID +- Build-time vs runtime variables as needed + +### Framework-Specific Notes + +- **Static Sites**: Build output uploaded as-is +- **SSR Apps**: Require Appwrite Functions runtime +- Check [Frameworks Documentation](https://appwrite.io/docs/products/sites/frameworks) for your specific framework + +### Rollbacks + +Appwrite Sites supports instant rollbacks: +- View deployment history in Console +- Click "Activate" on any previous deployment +- See: [Instant Rollbacks](https://appwrite.io/docs/products/sites/instant-rollbacks) +`; + +// ============================================================ +// SERVER SDK PATTERNS +// ============================================================ + +/** + * Python Server SDK implementation pattern + */ +export const pythonServerPattern = ` +## Python Implementation Pattern + +### Project Structure + +\`\`\` +project/ +├── app/ +│ ├── __init__.py +│ ├── main.py # Application entry point +│ ├── config.py # Configuration +│ └── services/ +│ ├── __init__.py +│ ├── appwrite.py # Appwrite client singleton +│ ├── db.py # Database wrapper +│ └── storage.py # Storage wrapper +├── requirements.txt +└── .env +\`\`\` + +### Appwrite Client Singleton + +\`\`\`python +# app/services/appwrite.py +import os +from appwrite.client import Client +from appwrite.services.tables_db import TablesDB +from appwrite.services.storage import Storage +from appwrite.services.users import Users + +client = Client() +client.set_endpoint(os.environ['APPWRITE_ENDPOINT']) +client.set_project(os.environ['APPWRITE_PROJECT_ID']) +client.set_key(os.environ['APPWRITE_API_KEY']) + +tables_db = TablesDB(client) +storage = Storage(client) +users = Users(client) + +DATABASE_ID = os.environ['APPWRITE_DATABASE_ID'] +BUCKET_ID = os.environ.get('APPWRITE_BUCKET_ID', '') +\`\`\` + +### Database Wrapper + +\`\`\`python +# app/services/db.py +from typing import TypeVar, Generic, Optional, List +from appwrite.query import Query +from appwrite.id import ID +from .appwrite import databases, DATABASE_ID + +T = TypeVar('T') + +class Table(Generic[T]): + def __init__(self, table_id: str): + self.table_id = table_id + + def create(self, data: dict) -> T: + """Create row with auto-generated ID""" + doc = tablesDB.create_row( + DATABASE_ID, + self.table_id, + ID.unique(), + data + ) + return doc + + def get(self, row_id: str) -> Optional[T]: + """Get row by ID""" + try: + return tablesDB.get_row( + DATABASE_ID, + self.table_id, + row_id + ) + except Exception: + return None + + def list_by_owner(self, user_id: str) -> List[T]: + """List rows by owner""" + result = tablesDB.list_rows( + DATABASE_ID, + self.table_id, + [ + Query.equal('createdBy', user_id), + Query.order_desc('$createdAt') + ] + ) + return result['rows'] + + def list_by_team(self, team_id: str) -> List[T]: + """List rows by team""" + result = tablesDB.list_rows( + DATABASE_ID, + self.table_id, + [ + Query.equal('teamId', team_id), + Query.order_desc('$createdAt') + ] + ) + return result['rows'] + + def update(self, row_id: str, data: dict) -> T: + """Update row (removes immutable columns)""" + safe_data = {k: v for k, v in data.items() + if k not in ('$id', '$createdAt', '$updatedAt', 'createdBy', 'teamId')} + return tablesDB.update_row( + DATABASE_ID, + self.table_id, + row_id, + safe_data + ) + + def delete(self, row_id: str) -> None: + """Delete row""" + tablesDB.delete_row(DATABASE_ID, self.table_id, row_id) + + +# Export typed tables +items = Table('items') +projects = Table('projects') +\`\`\` + +### Flask Integration Example + +\`\`\`python +# app/main.py +from flask import Flask, request, jsonify, g +from functools import wraps +from app.services import db +from app.services.appwrite import users + +app = Flask(__name__) + +def require_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + session = request.cookies.get('session') + if not session: + return jsonify({'error': 'Unauthorized'}), 401 + + try: + # Verify session and get user + user = users.get(session) # Or use session client + g.user = user + except Exception: + return jsonify({'error': 'Invalid session'}), 401 + + return f(*args, **kwargs) + return decorated + +@app.route('/api/items', methods=['GET']) +@require_auth +def list_items(): + items_list = db.items.list_by_owner(g.user['$id']) + return jsonify({'items': items_list}) + +@app.route('/api/items', methods=['POST']) +@require_auth +def create_item(): + data = request.get_json() + + item = db.items.create({ + 'title': data['title'].strip(), + 'description': data.get('description'), + 'createdBy': g.user['$id'], + 'teamId': data.get('teamId') + }) + + return jsonify({'item': item}), 201 +\`\`\` + +### Environment Variables + +Required in \`.env\`: +\`\`\` +APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 +APPWRITE_PROJECT_ID=your-project-id +APPWRITE_API_KEY=your-api-key +APPWRITE_DATABASE_ID=your-database-id +APPWRITE_BUCKET_ID=your-bucket-id +\`\`\` +`; + +/** + * PHP Server SDK implementation pattern + */ +export const phpServerPattern = ` +## PHP Implementation Pattern + +### Project Structure + +\`\`\` +project/ +├── src/ +│ ├── Services/ +│ │ ├── AppwriteService.php # Client singleton +│ │ ├── DatabaseService.php # Database wrapper +│ │ └── StorageService.php # Storage wrapper +│ └── Middleware/ +│ └── AuthMiddleware.php +├── public/ +│ └── index.php +├── composer.json +└── .env +\`\`\` + +### Appwrite Client Service + +\`\`\`php +setEndpoint($_ENV['APPWRITE_ENDPOINT']) + ->setProject($_ENV['APPWRITE_PROJECT_ID']) + ->setKey($_ENV['APPWRITE_API_KEY']); + } + return self::$client; + } + + public static function getTablesDB(): TablesDB + { + if (self::$tablesDB === null) { + self::$tablesDB = new TablesDB(self::getClient()); + } + return self::$tablesDB; + } + + public static function getStorage(): Storage + { + if (self::$storage === null) { + self::$storage = new Storage(self::getClient()); + } + return self::$storage; + } +} +\`\`\` + +### Database Wrapper + +\`\`\`php +databaseId = $_ENV['APPWRITE_DATABASE_ID']; + $this->tablesDB = AppwriteService::getTablesDB(); + } + + public function create(string $tableId, array $data): array + { + return $this->tablesDB->createRow( + $this->databaseId, + $tableId, + ID::unique(), + $data + ); + } + + public function get(string $tableId, string $rowId): ?array + { + try { + return $this->tablesDB->getRow( + $this->databaseId, + $tableId, + $rowId + ); + } catch (\\Exception $e) { + return null; + } + } + + public function listByOwner(string $tableId, string $userId): array + { + $result = $this->tablesDB->listRows( + $this->databaseId, + $tableId, + [ + Query::equal('createdBy', [$userId]), + Query::orderDesc('\\$createdAt') + ] + ); + return $result['rows']; + } + + public function listByTeam(string $tableId, string $teamId): array + { + $result = $this->tablesDB->listRows( + $this->databaseId, + $tableId, + [ + Query::equal('teamId', [$teamId]), + Query::orderDesc('\\$createdAt') + ] + ); + return $result['rows']; + } + + public function update(string $tableId, string $rowId, array $data): array + { + // Remove immutable columns + unset($data['\\$id'], $data['\\$createdAt'], $data['\\$updatedAt'], + $data['createdBy'], $data['teamId']); + + return $this->tablesDB->updateRow( + $this->databaseId, + $tableId, + $rowId, + $data + ); + } + + public function delete(string $tableId, string $rowId): void + { + $this->tablesDB->deleteRow( + $this->databaseId, + $tableId, + $rowId + ); + } +} +\`\`\` + +### Laravel Integration Example + +\`\`\`php +db = $db; + } + + public function index(Request $request) + { + $userId = $request->user()->id; + $items = $this->db->listByOwner('items', $userId); + + return response()->json(['items' => $items]); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'title' => 'required|string|max:120', + 'teamId' => 'nullable|string' + ]); + + $item = $this->db->create('items', [ + 'title' => trim($validated['title']), + 'description' => null, + 'createdBy' => $request->user()->id, + 'teamId' => $validated['teamId'] ?? null + ]); + + return response()->json(['item' => $item], 201); + } + + public function destroy(Request $request, string $id) + { + $item = $this->db->get('items', $id); + + if (!$item || $item['createdBy'] !== $request->user()->id) { + return response()->json(['error' => 'Forbidden'], 403); + } + + $this->db->delete('items', $id); + + return response()->json(['success' => true]); + } +} +\`\`\` +`; + +/** + * Go Server SDK implementation pattern + */ +export const goServerPattern = ` +## Go Implementation Pattern + +### Project Structure + +\`\`\` +project/ +├── cmd/ +│ └── server/ +│ └── main.go +├── internal/ +│ ├── appwrite/ +│ │ ├── client.go # Client singleton +│ │ ├── db.go # Database wrapper +│ │ └── storage.go # Storage wrapper +│ ├── handlers/ +│ │ └── items.go +│ └── middleware/ +│ └── auth.go +├── go.mod +└── .env +\`\`\` + +### Appwrite Client + +\`\`\`go +// internal/appwrite/client.go +package appwrite + +import ( + "os" + "sync" + + "github.com/appwrite/sdk-for-go/appwrite" +) + +var ( + client *appwrite.Client + once sync.Once + + DatabaseID string + BucketID string +) + +func GetClient() *appwrite.Client { + once.Do(func() { + client = appwrite.NewClient() + client.SetEndpoint(os.Getenv("APPWRITE_ENDPOINT")) + client.SetProject(os.Getenv("APPWRITE_PROJECT_ID")) + client.SetKey(os.Getenv("APPWRITE_API_KEY")) + + DatabaseID = os.Getenv("APPWRITE_DATABASE_ID") + BucketID = os.Getenv("APPWRITE_BUCKET_ID") + }) + return client +} + +func GetTablesDB() *appwrite.TablesDB { + return appwrite.NewTablesDB(GetClient()) +} + +func GetStorage() *appwrite.Storage { + return appwrite.NewStorage(GetClient()) +} +\`\`\` + +### Database Wrapper + +\`\`\`go +// internal/appwrite/db.go +package appwrite + +import ( + "github.com/appwrite/sdk-for-go/appwrite" + "github.com/appwrite/sdk-for-go/id" + "github.com/appwrite/sdk-for-go/query" +) + +type Table struct { + ID string + db *appwrite.TablesDB +} + +func NewTable(tableID string) *Table { + return &Table{ + ID: tableID, + db: GetTablesDB(), + } +} + +func (c *Table) Create(data map[string]interface{}) (map[string]interface{}, error) { + doc, err := c.db.CreateRow( + DatabaseID, + c.ID, + id.Unique(), + data, + ) + if err != nil { + return nil, err + } + return doc.ToMap(), nil +} + +func (c *Table) Get(rowID string) (map[string]interface{}, error) { + doc, err := c.db.GetRow(DatabaseID, c.ID, rowID) + if err != nil { + return nil, err + } + return doc.ToMap(), nil +} + +func (c *Table) ListByOwner(userID string) ([]map[string]interface{}, error) { + result, err := c.db.ListRows( + DatabaseID, + c.ID, + c.db.WithListRowsQueries([]string{ + query.Equal("createdBy", userID), + query.OrderDesc("$createdAt"), + }), + ) + if err != nil { + return nil, err + } + + docs := make([]map[string]interface{}, len(result.Rows)) + for i, doc := range result.Rows { + docs[i] = doc.ToMap() + } + return docs, nil +} + +func (c *Table) ListByTeam(teamID string) ([]map[string]interface{}, error) { + result, err := c.db.ListRows( + DatabaseID, + c.ID, + c.db.WithListRowsQueries([]string{ + query.Equal("teamId", teamID), + query.OrderDesc("$createdAt"), + }), + ) + if err != nil { + return nil, err + } + + docs := make([]map[string]interface{}, len(result.Rows)) + for i, doc := range result.Rows { + docs[i] = doc.ToMap() + } + return docs, nil +} + +func (c *Table) Update(rowID string, data map[string]interface{}) (map[string]interface{}, error) { + // Remove immutable columns + delete(data, "$id") + delete(data, "$createdAt") + delete(data, "$updatedAt") + delete(data, "createdBy") + delete(data, "teamId") + + doc, err := c.db.UpdateRow(DatabaseID, c.ID, rowID, data) + if err != nil { + return nil, err + } + return doc.ToMap(), nil +} + +func (c *Table) Delete(rowID string) error { + _, err := c.db.DeleteRow(DatabaseID, c.ID, rowID) + return err +} + +// Exported tables +var ( + Items = NewTable("items") + Projects = NewTable("projects") +) +\`\`\` + +### HTTP Handler Example + +\`\`\`go +// internal/handlers/items.go +package handlers + +import ( + "encoding/json" + "net/http" + "strings" + + "yourproject/internal/appwrite" + "yourproject/internal/middleware" +) + +func ListItems(w http.ResponseWriter, r *http.Request) { + user := middleware.GetUser(r.Context()) + + items, err := appwrite.Items.ListByOwner(user.ID) + if err != nil { + http.Error(w, "Failed to fetch items", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{"items": items}) +} + +func CreateItem(w http.ResponseWriter, r *http.Request) { + user := middleware.GetUser(r.Context()) + + var input struct { + Title string \`json:"title"\` + TeamID *string \`json:"teamId"\` + } + + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + item, err := appwrite.Items.Create(map[string]interface{}{ + "title": strings.TrimSpace(input.Title), + "description": nil, + "createdBy": user.ID, + "teamId": input.TeamID, + }) + if err != nil { + http.Error(w, "Failed to create item", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{"item": item}) +} +\`\`\` +`; + +/** + * Ruby Server SDK implementation pattern + */ +export const rubyServerPattern = ` +## Ruby Implementation Pattern + +### Project Structure + +\`\`\` +project/ +├── lib/ +│ ├── appwrite_client.rb # Client singleton +│ ├── database_service.rb # Database wrapper +│ └── storage_service.rb # Storage wrapper +├── app/ +│ └── controllers/ +│ └── items_controller.rb +├── Gemfile +└── .env +\`\`\` + +### Appwrite Client + +\`\`\`ruby +# lib/appwrite_client.rb +require 'appwrite' + +module AppwriteClient + class << self + def client + @client ||= begin + client = Appwrite::Client.new + client + .set_endpoint(ENV['APPWRITE_ENDPOINT']) + .set_project(ENV['APPWRITE_PROJECT_ID']) + .set_key(ENV['APPWRITE_API_KEY']) + client + end + end + + def tables_db + @tables_db ||= Appwrite::TablesDB.new(client) + end + + def storage + @storage ||= Appwrite::Storage.new(client) + end + + def database_id + ENV['APPWRITE_DATABASE_ID'] + end + + def bucket_id + ENV['APPWRITE_BUCKET_ID'] + end + end +end +\`\`\` + +### Database Service + +\`\`\`ruby +# lib/database_service.rb +require_relative 'appwrite_client' + +class DatabaseService + def initialize(table_id) + @table_id = table_id + @tables_db = AppwriteClient.tables_db + @database_id = AppwriteClient.database_id + end + + def create(data) + @tables_db.create_row( + database_id: @database_id, + table_id: @table_id, + row_id: Appwrite::ID.unique, + data: data + ) + end + + def get(row_id) + @tables_db.et_row( + database_id: @database_id, + table_id: @table_id, + row_id: row_id + ) + rescue Appwrite::Exception + nil + end + + def list_by_owner(user_id) + result = @tables_db.list_rows( + database_id: @database_id, + table_id: @table_id, + queries: [ + Appwrite::Query.equal('createdBy', [user_id]), + Appwrite::Query.order_desc('$createdAt') + ] + ) + result.rows + end + + def list_by_team(team_id) + result = @tables_db.list_rows( + database_id: @database_id, + table_id: @table_id, + queries: [ + Appwrite::Query.equal('teamId', [team_id]), + Appwrite::Query.order_desc('$createdAt') + ] + ) + result.rows + end + + def update(row_id, data) + # Remove immutable fields + safe_data = data.except('$id', '$createdAt', '$updatedAt', 'createdBy', 'teamId') + + @tables_db.update_row( + database_id: @database_id, + table_id: @table_id, + row_id: row_id, + data: safe_data + ) + end + + def delete(row_id) + @tables_db.delete_row( + database_id: @database_id, + table_id: @table_id, + row_id: row_id + ) + end +end + +# Convenience accessors +module DB + def self.items + @items ||= DatabaseService.new('items') + end + + def self.projects + @projects ||= DatabaseService.new('projects') + end +end +\`\`\` + +### Rails Controller Example + +\`\`\`ruby +# app/controllers/items_controller.rb +class ItemsController < ApplicationController + before_action :authenticate_user! + + def index + items = DB.items.list_by_owner(current_user.id) + render json: { items: items } + end + + def create + item = DB.items.create( + title: params[:title].strip, + description: nil, + createdBy: current_user.id, + teamId: params[:team_id] + ) + + render json: { item: item }, status: :created + end + + def destroy + item = DB.items.get(params[:id]) + + if item.nil? || item['createdBy'] != current_user.id + return render json: { error: 'Forbidden' }, status: :forbidden + end + + DB.items.delete(params[:id]) + render json: { success: true } + end +end +\`\`\` +`; + +/** + * .NET Server SDK implementation pattern + */ +export const dotnetServerPattern = ` +## .NET Implementation Pattern + +### Project Structure + +\`\`\` +Project/ +├── Services/ +│ ├── AppwriteService.cs # Client singleton +│ ├── DatabaseService.cs # Database wrapper +│ └── StorageService.cs # Storage wrapper +├── Controllers/ +│ └── ItemsController.cs +├── Models/ +│ └── Item.cs +├── Program.cs +├── appsettings.json +└── .env +\`\`\` + +### Appwrite Service + +\`\`\`csharp +// Services/AppwriteService.cs +using Appwrite; +using Appwrite.Services; + +public class AppwriteService +{ + private static Client? _client; + private static TablesDB? _tablesDB; + private static Storage? _storage; + + public static string DatabaseId => Environment.GetEnvironmentVariable("APPWRITE_DATABASE_ID")!; + public static string BucketId => Environment.GetEnvironmentVariable("APPWRITE_BUCKET_ID")!; + + public static Client GetClient() + { + if (_client == null) + { + _client = new Client() + .SetEndpoint(Environment.GetEnvironmentVariable("APPWRITE_ENDPOINT")!) + .SetProject(Environment.GetEnvironmentVariable("APPWRITE_PROJECT_ID")!) + .SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY")!); + } + return _client; + } + + public static TablesDB GetTablesDB() + { + return _tablesDB ??= new TablesDB(GetClient()); + } + + public static Storage GetStorage() + { + return _storage ??= new Storage(GetClient()); + } +} +\`\`\` + +### Database Service + +\`\`\`csharp +// Services/DatabaseService.cs +using Appwrite; +using Appwrite.Services; +using Appwrite.Models; +using System.Text.Json; + +public class DatabaseService where T : class +{ +private readonly string _tableId; + private readonly TablesDB _tablesDB; + + public DatabaseService(string tableId) + { + _tableId = tableId; + _tablesDB = AppwriteService.GetTablesDB(); + } + + public async Task CreateAsync(Dictionary data) + { + return await _tablesDB.CreateRow( + databaseId: AppwriteService.DatabaseId, + tableId: _tableId, + rowId: ID.Unique(), + data: data + ); + } + + public async Task GetAsync(string rowId) + { + try + { + return await _tablesDB.GetRow( + databaseId: AppwriteService.DatabaseId, + tableId: _tableId, + rowId: rowId + ); + } + catch (AppwriteException) + { + return null; + } + } + + public async Task> ListByOwnerAsync(string userId) + { + var result = await _tablesDB.ListRows( + databaseId: AppwriteService.DatabaseId, + tableId: _tableId, + queries: new List + { + Query.Equal("createdBy", new List { userId }), + Query.OrderDesc("$createdAt") + } + ); + return result.Rows; + } + + public async Task> ListByTeamAsync(string teamId) + { + var result = await _tablesDB.ListRows( + databaseId: AppwriteService.DatabaseId, + tableId: _tableId, + queries: new List + { + Query.Equal("teamId", new List { teamId }), + Query.OrderDesc("$createdAt") + } + ); + return result.Rows; + } + + public async Task UpdateAsync(string rowId, Dictionary data) + { + // Remove immutable columns + data.Remove("$id"); + data.Remove("$createdAt"); + data.Remove("$updatedAt"); + data.Remove("createdBy"); + data.Remove("teamId"); + + return await _tablesDB.UpdateRow( + databaseId: AppwriteService.DatabaseId, + tableId: _tableId, + rowId: rowId, + data: data + ); + } + + public async Task DeleteAsync(string rowId) + { + await _tablesDB.DeleteRow( + databaseId: AppwriteService.DatabaseId, + tableId: _tableId, + rowId: rowId + ); + } +} + +// Static accessors +public static class DB +{ + public static DatabaseService Items { get; } = new("items"); + public static DatabaseService Projects { get; } = new("projects"); +} +\`\`\` + +### Controller Example + +\`\`\`csharp +// Controllers/ItemsController.cs +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class ItemsController : ControllerBase +{ + [HttpGet] + public async Task GetItems() + { + var userId = User.FindFirst("sub")?.Value!; + var items = await DB.Items.ListByOwnerAsync(userId); + return Ok(new { items }); + } + + [HttpPost] + public async Task CreateItem([FromBody] CreateItemRequest request) + { + var userId = User.FindFirst("sub")?.Value!; + + var item = await DB.Items.CreateAsync(new Dictionary + { + { "title", request.Title.Trim() }, + { "description", null! }, + { "createdBy", userId }, + { "teamId", request.TeamId ?? null! } + }); + + return CreatedAtAction(nameof(GetItems), new { item }); + } + + [HttpDelete("{id}")] + public async Task DeleteItem(string id) + { + var userId = User.FindFirst("sub")?.Value!; + var item = await DB.Items.GetAsync(id); + + if (item == null || item.Data["createdBy"].ToString() != userId) + { + return Forbid(); + } + + await DB.Items.DeleteAsync(id); + return Ok(new { success = true }); + } +} + +public record CreateItemRequest(string Title, string? TeamId); +\`\`\` +`; + +/** + * Dart Server SDK implementation pattern + */ +export const dartServerPattern = ` +## Dart Server Implementation Pattern + +### Project Structure + +\`\`\` +project/ +├── bin/ +│ └── server.dart # Entry point +├── lib/ +│ ├── services/ +│ │ ├── appwrite.dart # Client singleton +│ │ ├── database.dart # Database wrapper +│ │ └── storage.dart # Storage wrapper +│ └── handlers/ +│ └── items.dart +├── pubspec.yaml +└── .env +\`\`\` + +### Appwrite Client + +\`\`\`dart +// lib/services/appwrite.dart +import 'dart:io'; +import 'package:dart_appwrite/dart_appwrite.dart'; + +class AppwriteService { + static Client? _client; + static TablesDB? _tablesDB; + static Storage? _storage; + + static String get databaseId => Platform.environment['APPWRITE_DATABASE_ID']!; + static String get bucketId => Platform.environment['APPWRITE_BUCKET_ID']!; + + static Client get client { + _client ??= Client() + .setEndpoint(Platform.environment['APPWRITE_ENDPOINT']!) + .setProject(Platform.environment['APPWRITE_PROJECT_ID']!) + .setKey(Platform.environment['APPWRITE_API_KEY']!); + return _client!; + } + + static TablesDB get tablesDB { + _tablesDB ??= TablesDB(client); + return _tablesDB!; + } + + static Storage get storage { + _storage ??= Storage(client); + return _storage!; + } +} +\`\`\` + +### Database Wrapper + +\`\`\`dart +// lib/services/database.dart +import 'package:dart_appwrite/dart_appwrite.dart'; +import 'appwrite.dart'; + +class Table { + final String tableId; + + Table(this.tableId); + + Future create(Map data) async { + return await AppwriteService.tablesDB.createRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: ID.unique(), + data: data, + ); + } + + Future get(String rowId) async { + try { + return await AppwriteService.tablesDB.getRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: rowId, + ); + } on AppwriteException { + return null; + } + } + + Future> listByOwner(String userId) async { + final result = await AppwriteService.tablesDB.listRows( + databaseId: AppwriteService.databaseId, + tableId: tableId, + queries: [ + Query.equal('createdBy', userId), + Query.orderDesc(r'$createdAt'), + ], + ); + return result.rows; + } + + Future> listByTeam(String teamId) async { + final result = await AppwriteService.tablesDB.ws( + databaseId: AppwriteService.databaseId, + tableId: tableId, + queries: [ + Query.equal('teamId', teamId), + Query.orderDesc(r'$createdAt'), + ], + ); + return result.rows; + } + + Future update(String rowId, Map data) async { + // Remove immutable columns + data.remove(r'$id'); + data.remove(r'$createdAt'); + data.remove(r'$updatedAt'); + data.remove('createdBy'); + data.remove('teamId'); + + return await AppwriteService.tablesDB.w( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: rowId, + data: data, + ); + } + + Future delete(String rowId) async { + await AppwriteService.tablesDB.eteRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: rowId, + ); + } +} + +// Exported tables +final items = Table('items'); +final projects = Table('projects'); +\`\`\` + +### Shelf Handler Example + +\`\`\`dart +// lib/handlers/items.dart +import 'dart:convert'; +import 'package:shelf/shelf.dart'; +import '../services/database.dart'; + +Response jsonResponse(Object data, {int statusCode = 200}) { + return Response( + statusCode, + body: jsonEncode(data), + headers: {'content-type': 'application/json'}, + ); +} + +Future listItems(Request request) async { + final userId = request.context['userId'] as String; + + final itemsList = await items.listByOwner(userId); + final itemsData = itemsList.map((doc) => doc.data).toList(); + + return jsonResponse({'items': itemsData}); +} + +Future createItem(Request request) async { + final userId = request.context['userId'] as String; + final body = jsonDecode(await request.readAsString()) as Map; + + final item = await items.create({ + 'title': (body['title'] as String).trim(), + 'description': null, + 'createdBy': userId, + 'teamId': body['teamId'], + }); + + return jsonResponse({'item': item.data}, statusCode: 201); +} + +Future deleteItem(Request request, String id) async { + final userId = request.context['userId'] as String; + + final item = await items.get(id); + if (item == null || item.data['createdBy'] != userId) { + return jsonResponse({'error': 'Forbidden'}, statusCode: 403); + } + + await items.delete(id); + return jsonResponse({'success': true}); +} +\`\`\` +`; + +/** + * Kotlin Server SDK implementation pattern + */ +export const kotlinServerPattern = ` +## Kotlin Server Implementation Pattern + +### Project Structure + +\`\`\` +project/ +├── src/main/kotlin/ +│ ├── Application.kt +│ ├── services/ +│ │ ├── AppwriteService.kt +│ │ ├── DatabaseService.kt +│ │ └── StorageService.kt +│ └── routes/ +│ └── ItemRoutes.kt +├── build.gradle.kts +└── .env +\`\`\` + +### Appwrite Service + +\`\`\`kotlin +// services/AppwriteService.kt +package com.example.services + +import io.appwrite.Client +import io.appwrite.services.TablesDB +import io.appwrite.services.Storage + +object AppwriteService { + val databaseId: String = System.getenv("APPWRITE_DATABASE_ID") + val bucketId: String = System.getenv("APPWRITE_BUCKET_ID") + + val client: Client by lazy { + Client() + .setEndpoint(System.getenv("APPWRITE_ENDPOINT")) + .setProject(System.getenv("APPWRITE_PROJECT_ID")) + .setKey(System.getenv("APPWRITE_API_KEY")) + } + + val tablesDB: TablesDB by lazy { TablesDB(client) } + val storage: Storage by lazy { Storage(client) } +} +\`\`\` + +### Database Service + +\`\`\`kotlin +// services/DatabaseService.kt +package com.example.services + +import io.appwrite.ID +import io.appwrite.Query +import io.appwrite.models.Row + +class Table(private val tableId: String) { + private val tablesDB = AppwriteService.tablesDB + private val databaseId = AppwriteService.databaseId + + suspend fun create(data: Map): Row> { + return tablesDB.createRow( + databaseId = databaseId, + tableId = tableId, + rowId = ID.unique(), + data = data + ) + } + + suspend fun get(rowId: String): Row>? { + return try { + tablesDB.etRow( + databaseId = databaseId, + tableId = tableId, + rowId = rowId + ) + } catch (e: Exception) { + null + } + } + + suspend fun listByOwner(userId: String): List>> { + val result = tablesDB.tRows( + databaseId = databaseId, + tableId = tableId, + queries = listOf( + Query.equal("createdBy", listOf(userId)), + Query.orderDesc("\$createdAt") + ) + ) + return result.rows + } + + suspend fun listByTeam(teamId: String): List>> { + val result = tablesDB.listRows( + databaseId = databaseId, + tableId = tableId, + queries = listOf( + Query.equal("teamId", listOf(teamId)), + Query.orderDesc("\$createdAt") + ) + ) + return result.rows + } + + suspend fun update(rowId: String, data: Map): Row> { + // Remove immutable columns + val safeData = data.filterKeys { + it !in listOf("\$id", "\$createdAt", "\$updatedAt", "createdBy", "teamId") + } + + return tablesDB.updateRow( + databaseId = databaseId, + tableId = tableId, + rowId = rowId, + data = safeData + ) + } + + suspend fun delete(rowId: String) { + tablesDB.deleteRow( + databaseId = databaseId, + tableId = tableId, + rowId = rowId + ) + } +} + +// Exported tables +object DB { + val items = Table("items") + val projects = Table("projects") +} +\`\`\` + +### Ktor Routes Example + +\`\`\`kotlin +// routes/ItemRoutes.kt +package com.example.routes + +import com.example.services.DB +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.itemRoutes() { + authenticate { + route("/api/items") { + get { + val userId = call.principal()!!.id + val items = DB.items.listByOwner(userId) + call.respond(mapOf("items" to items.map { it.data })) + } + + post { + val userId = call.principal()!!.id + val request = call.receive() + + val item = DB.items.create(mapOf( + "title" to request.title.trim(), + "description" to null, + "createdBy" to userId, + "teamId" to request.teamId + )) + + call.respond(HttpStatusCode.Created, mapOf("item" to item.data)) + } + + delete("/{id}") { + val userId = call.principal()!!.id + val id = call.parameters["id"]!! + + val item = DB.items.get(id) + if (item == null || item.data["createdBy"] != userId) { + call.respond(HttpStatusCode.Forbidden, mapOf("error" to "Forbidden")) + return@delete + } + + DB.items.delete(id) + call.respond(mapOf("success" to true)) + } + } + } +} + +data class CreateItemRequest(val title: String, val teamId: String?) +\`\`\` +`; + +/** + * Swift Server SDK implementation pattern + */ +export const swiftServerPattern = ` +## Swift Server Implementation Pattern + +### Project Structure + +\`\`\` +Project/ +├── Sources/ +│ └── App/ +│ ├── main.swift +│ ├── Services/ +│ │ ├── AppwriteService.swift +│ │ ├── DatabaseService.swift +│ │ └── StorageService.swift +│ └── Controllers/ +│ └── ItemController.swift +├── Package.swift +└── .env +\`\`\` + +### Appwrite Service + +\`\`\`swift +// Services/AppwriteService.swift +import Appwrite +import Foundation + +class AppwriteService { + static let shared = AppwriteService() + + let client: Client + let tablesDB: TablesDB + let storage: Storage + + let databaseId: String + let bucketId: String + + private init() { + client = Client() + .setEndpoint(ProcessInfo.processInfo.environment["APPWRITE_ENDPOINT"]!) + .setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!) +.setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!) + + tablesDB = TablesDB(client) + storage = Storage(client) + + databaseId = ProcessInfo.processInfo.environment["APPWRITE_DATABASE_ID"]! + bucketId = ProcessInfo.processInfo.environment["APPWRITE_BUCKET_ID"] ?? "" + } +} +\`\`\` + +### Database Service + +\`\`\`swift +// Services/DatabaseService.swift +import Appwrite +import Foundation + +class Table { + let tableId: String + private let tablesDB: TablesDB + private let databaseId: String + + init(_ tableId: String) { + self.tableId = tableId + self.tablesDB = AppwriteService.shared.tablesDB + self.databaseId = AppwriteService.shared.databaseId + } + + func create(data: [String: Any]) async throws -> Row<[String: AnyCodable]> { + return try await tablesDB.createRow( + databaseId: databaseId, + tableId: tableId, + rowId: ID.unique(), + data: data + ) + } + + func get(rowId: String) async throws -> Row<[String: AnyCodable]>? { + do { + return try await tablesDB.getRow( + databaseId: databaseId, + tableId: tableId, + rowId: rowId + ) + } catch { + return nil + } + } + + func listByOwner(userId: String) async throws -> [Row<[String: AnyCodable]>] { + let result = try await tablesDB.listRows( + databaseId: databaseId, + tableId: tableId, + queries: [ + Query.equal("createdBy", value: userId), + Query.orderDesc("$createdAt") + ] + ) + return result.rows + } + + func listByTeam(teamId: String) async throws -> [Row<[String: AnyCodable]>] { + let result = try await tablesDB.listRows( + databaseId: databaseId, + tableId: tableId, + queries: [ + Query.equal("teamId", value: teamId), + Query.orderDesc("$createdAt") + ] + ) + return result.rows + } + + func update(rowId: String, data: [String: Any]) async throws -> Row<[String: AnyCodable]> { + // Remove immutable columns + var safeData = data + safeData.removeValue(forKey: "$id") + safeData.removeValue(forKey: "$createdAt") + safeData.removeValue(forKey: "$updatedAt") + safeData.removeValue(forKey: "createdBy") + safeData.removeValue(forKey: "teamId") + + return try await tablesDB.updateRow( + databaseId: databaseId, + tableId: tableId, + rowId: rowId, + data: safeData + ) + } + + func delete(rowId: String) async throws { + _ = try await tablesDB.deleteRow( + databaseId: databaseId, + tableId: tableId, + rowId: rowId + ) + } +} + +// Exported tables +struct DB { + static let items = Table("items") + static let projects = Table("projects") +} +\`\`\` + +### Vapor Controller Example + +\`\`\`swift +// Controllers/ItemController.swift +import Vapor + +struct ItemController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let items = routes.grouped("api", "items") + + let protected = items.grouped(AuthMiddleware()) + protected.get(use: index) + protected.post(use: create) + protected.delete(":id", use: delete) + } + + func index(req: Request) async throws -> Response { + let user = try req.auth.require(User.self) + let items = try await DB.items.listByOwner(userId: user.id) + + return try Response( + status: .ok, + body: .init(data: JSONEncoder().encode(["items": items.map { $0.data }])) + ) + } + + func create(req: Request) async throws -> Response { + let user = try req.auth.require(User.self) + let input = try req.content.decode(CreateItemRequest.self) + + let item = try await DB.items.create(data: [ + "title": input.title.trimmingCharacters(in: .whitespaces), + "description": nil as Any, + "createdBy": user.id, + "teamId": input.teamId as Any + ]) + + return try Response( + status: .created, + body: .init(data: JSONEncoder().encode(["item": item.data])) + ) + } + + func delete(req: Request) async throws -> Response { + let user = try req.auth.require(User.self) + let id = req.parameters.get("id")! + + guard let item = try await DB.items.get(rowId: id), + item.data["createdBy"]?.value as? String == user.id else { + throw Abort(.forbidden) + } + + try await DB.items.delete(rowId: id) + return Response(status: .ok) + } +} + +struct CreateItemRequest: Content { + let title: String + let teamId: String? +} +\`\`\` +`; + +// ============================================================ +// MOBILE/CLIENT SDK PATTERNS +// ============================================================ + +/** + * Android SDK implementation pattern + */ +export const androidPattern = ` +## Android Implementation Pattern + +### Project Structure + +\`\`\` +app/ +├── src/main/java/com/example/app/ +│ ├── AppwriteService.kt # Client singleton +│ ├── data/ +│ │ ├── repository/ +│ │ │ ├── ItemRepository.kt # Data repository +│ │ │ └── AuthRepository.kt +│ │ └── model/ +│ │ └── Item.kt +│ ├── ui/ +│ │ └── items/ +│ │ ├── ItemsViewModel.kt +│ │ └── ItemsScreen.kt +│ └── di/ +│ └── AppModule.kt # Dependency injection +├── build.gradle.kts +└── local.properties +\`\`\` + +### Appwrite Client + +\`\`\`kotlin +// AppwriteService.kt +package com.example.app + +import android.content.Context +import io.appwrite.Client +import io.appwrite.services.Account +import io.appwrite.services.TablesDB +import io.appwrite.services.Storage +import io.appwrite.services.Realtime + +object AppwriteService { + private lateinit var client: Client + + lateinit var account: Account + lateinit var tablesDB: TablesDB + lateinit var storage: Storage + lateinit var realtime: Realtime + + const val DATABASE_ID = "your-database-id" + const val BUCKET_ID = "your-bucket-id" + + fun init(context: Context) { + client = Client(context) + .setEndpoint(BuildConfig.APPWRITE_ENDPOINT) + .setProject(BuildConfig.APPWRITE_PROJECT_ID) + + account = Account(client) + tablesDB = TablesDB(client) + storage = Storage(client) + realtime = Realtime(client) + } +} +\`\`\` + +### Repository Pattern + +\`\`\`kotlin +// data/repository/ItemRepository.kt +package com.example.app.data.repository + +import io.appwrite.ID +import io.appwrite.Query +import io.appwrite.models.Row +import com.example.app.AppwriteService +import com.example.app.data.model.Item +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class ItemRepository { + private val tablesDB = AppwriteService.tablesDB + private val tableId = "items" + + suspend fun create(title: String, userId: String, teamId: String? = null): Item { + val doc = tablesDB.createRow( + databaseId = AppwriteService.DATABASE_ID, + tableId = tableId, + rowId = ID.unique(), + data = mapOf( + "title" to title.trim(), + "description" to null, + "createdBy" to userId, + "teamId" to teamId + ) + ) + return doc.toItem() + } + + suspend fun listByOwner(userId: String): List { + val result = tablesDB.listRows( + databaseId = AppwriteService.DATABASE_ID, + tableId = tableId, + queries = listOf( + Query.equal("createdBy", listOf(userId)), + Query.orderDesc("\$createdAt"), + Query.limit(100) + ) + ) + return result.rows.map { it.toItem() } + } + + suspend fun get(id: String): Item? { + return try { + tablesDB.getRow( + databaseId = AppwriteService.DATABASE_ID, + tableId = tableId, + rowId = id + ).toItem() + } catch (e: Exception) { + null + } + } + + suspend fun update(id: String, title: String): Item { + val doc = tablesDB.updateRow( + databaseId = AppwriteService.DATABASE_ID, + tableId = tableId, + rowId = id, + data = mapOf("title" to title.trim()) + ) + return doc.toItem() + } + + suspend fun delete(id: String) { + tablesDB.deleteRow( + databaseId = AppwriteService.DATABASE_ID, + tableId = tableId, + rowId = id + ) + } + + private fun Row>.toItem(): Item { + return Item( + id = this.id, + title = this.data["title"] as String, + description = this.data["description"] as? String, + createdBy = this.data["createdBy"] as String, + teamId = this.data["teamId"] as? String, + createdAt = this.createdAt, + updatedAt = this.updatedAt + ) + } +} +\`\`\` + +### ViewModel Pattern + +\`\`\`kotlin +// ui/items/ItemsViewModel.kt +package com.example.app.ui.items + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app.data.repository.ItemRepository +import com.example.app.data.repository.AuthRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class ItemsViewModel( + private val itemRepository: ItemRepository, + private val authRepository: AuthRepository +) : ViewModel() { + + private val _items = MutableStateFlow>(emptyList()) + val items: StateFlow> = _items + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading + + fun loadItems() { + viewModelScope.launch { + _isLoading.value = true + try { + val user = authRepository.getCurrentUser() + _items.value = itemRepository.listByOwner(user.id) + } catch (e: Exception) { + // Handle error + } finally { + _isLoading.value = false + } + } + } + + fun createItem(title: String, teamId: String? = null) { + viewModelScope.launch { + try { + val user = authRepository.getCurrentUser() + itemRepository.create(title, user.id, teamId) + loadItems() // Refresh list + } catch (e: Exception) { + // Handle error + } + } + } + + fun deleteItem(id: String) { + viewModelScope.launch { + try { + itemRepository.delete(id) + loadItems() + } catch (e: Exception) { + // Handle error + } + } + } +} +\`\`\` + +### Realtime Subscription + +\`\`\`kotlin +// Subscribe to table changes +fun subscribeToItems(onUpdate: (List) -> Unit): RealtimeSubscription { + return AppwriteService.realtime.subscribe( + "databases.\${AppwriteService.DATABASE_ID}.tables.items.rows" + ) { response -> + // Refresh items on any change + viewModelScope.launch { + loadItems() + } + } +} + +// Remember to unsubscribe in onCleared() +override fun onCleared() { + super.onCleared() + subscription?.close() +} +\`\`\` + +### Best Practices + +- Initialize AppwriteService in Application class +- Use Repository pattern for data access +- Use ViewModel for UI state management +- Handle offline/network errors gracefully +- Store session in encrypted SharedPreferences +- Never hardcode API keys in source code +`; + +/** + * Apple (iOS/macOS) SDK implementation pattern + */ +export const applePattern = ` +## Apple (iOS/macOS) Implementation Pattern + +### Project Structure + +\`\`\` +App/ +├── AppwriteService.swift # Client singleton +├── Repositories/ +│ ├── ItemRepository.swift +│ └── AuthRepository.swift +├── Models/ +│ └── Item.swift +├── ViewModels/ +│ └── ItemsViewModel.swift +├── Views/ +│ └── ItemsView.swift +└── App.swift +\`\`\` + +### Appwrite Client + +\`\`\`swift +// AppwriteService.swift +import Appwrite +import Foundation + +class AppwriteService { + static let shared = AppwriteService() + + let client: Client + let account: Account + let tablesDB: TablesDB + let storage: Storage + let realtime: Realtime + + static let databaseId = "your-database-id" + static let bucketId = "your-bucket-id" + + private init() { + client = Client() + .setEndpoint(Bundle.main.infoDictionary?["APPWRITE_ENDPOINT"] as? String ?? "") + .setProject(Bundle.main.infoDictionary?["APPWRITE_PROJECT_ID"] as? String ?? "") + + account = Account(client) + tablesDB = TablesDB(client) + storage = Storage(client) + realtime = Realtime(client) + } +} +\`\`\` + +### Model + +\`\`\`swift +// Models/Item.swift +import Foundation + +struct Item: Identifiable, Codable { + let id: String + var title: String + var description: String? + let createdBy: String + let teamId: String? + let createdAt: String + let updatedAt: String + + enum CodingKeys: String, CodingKey { + case id = "$id" + case title, description, createdBy, teamId + case createdAt = "$createdAt" + case updatedAt = "$updatedAt" + } +} +\`\`\` + +### Repository Pattern + +\`\`\`swift +// Repositories/ItemRepository.swift +import Appwrite +import Foundation + +class ItemRepository { + private let tablesDB = AppwriteService.shared.tablesDB + private let tableId = "items" + + func create(title: String, userId: String, teamId: String? = nil) async throws -> Item { + let doc = try await tablesDB.createRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: ID.unique(), + data: [ + "title": title.trimmingCharacters(in: .whitespaces), + "description": nil as Any, + "createdBy": userId, + "teamId": teamId as Any + ] + ) + return try JSONDecoder().decode(Item.self, from: JSONSerialization.data(withJSONObject: doc.data)) + } + + func listByOwner(userId: String) async throws -> [Item] { + let result = try await tablesDB.listRows( + databaseId: AppwriteService.databaseId, + tableId: tableId, + queries: [ + Query.equal("createdBy", value: userId), + Query.orderDesc("$createdAt"), + Query.limit(100) + ] + ) + + return try result.rows.map { doc in + try JSONDecoder().decode(Item.self, from: JSONSerialization.data(withJSONObject: doc.data)) + } + } + + func get(id: String) async throws -> Item? { + do { + let doc = try await tablesDB.getRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: id + ) + return try JSONDecoder().decode(Item.self, from: JSONSerialization.data(withJSONObject: doc.data)) + } catch { + return nil + } + } + + func update(id: String, title: String) async throws -> Item { + let doc = try await tablesDB.updateRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: id, + data: ["title": title.trimmingCharacters(in: .whitespaces)] + ) + return try JSONDecoder().decode(Item.self, from: JSONSerialization.data(withJSONObject: doc.data)) + } + + func delete(id: String) async throws { + _ = try await tablesDB.deleteRow( + databaseId: AppwriteService.databaseId, + tableId: tableId, + rowId: id + ) + } +} +\`\`\` + +### ViewModel (ObservableObject) + +\`\`\`swift +// ViewModels/ItemsViewModel.swift +import SwiftUI + +@MainActor +class ItemsViewModel: ObservableObject { + @Published var items: [Item] = [] + @Published var isLoading = false + @Published var error: Error? + + private let itemRepository = ItemRepository() + private let authRepository = AuthRepository() + private var realtimeSubscription: RealtimeSubscription? + + func loadItems() async { + isLoading = true + error = nil + + do { + let user = try await authRepository.getCurrentUser() + items = try await itemRepository.listByOwner(userId: user.id) + } catch { + self.error = error + } + + isLoading = false + } + + func createItem(title: String, teamId: String? = nil) async { + do { + let user = try await authRepository.getCurrentUser() + _ = try await itemRepository.create(title: title, userId: user.id, teamId: teamId) + await loadItems() + } catch { + self.error = error + } + } + + func deleteItem(id: String) async { + do { + try await itemRepository.delete(id: id) + await loadItems() + } catch { + self.error = error + } + } + + func subscribeToChanges() { + realtimeSubscription = AppwriteService.shared.realtime.subscribe( + channels: ["databases.\\(AppwriteService.databaseId).tables.items.rows"] + ) { [weak self] event in + Task { @MainActor in + await self?.loadItems() + } + } + } + + func unsubscribe() { + realtimeSubscription?.close() + } +} +\`\`\` + +### SwiftUI View + +\`\`\`swift +// Views/ItemsView.swift +import SwiftUI + +struct ItemsView: View { + @StateObject private var viewModel = ItemsViewModel() + @State private var newItemTitle = "" + + var body: some View { + NavigationView { + List { + ForEach(viewModel.items) { item in + Text(item.title) + } + .onDelete { indexSet in + for index in indexSet { + Task { + await viewModel.deleteItem(id: viewModel.items[index].id) + } + } + } + } + .navigationTitle("Items") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Add") { + Task { + await viewModel.createItem(title: newItemTitle) + } + } + } + } + .refreshable { + await viewModel.loadItems() + } + } + .task { + await viewModel.loadItems() + viewModel.subscribeToChanges() + } + .onDisappear { + viewModel.unsubscribe() + } + } +} +\`\`\` + +### Best Practices + +- Use @MainActor for UI-related async operations +- Store configuration in Info.plist or xcconfig files +- Use Keychain for storing session tokens +- Handle network errors with retry logic +- Unsubscribe from realtime when view disappears +`; + +/** + * Flutter SDK implementation pattern + */ +export const flutterPattern = ` +## Flutter Implementation Pattern + +### Project Structure + +\`\`\` +lib/ +├── main.dart +├── services/ +│ └── appwrite_service.dart +├── repositories/ +│ ├── item_repository.dart +│ └── auth_repository.dart +├── models/ +│ └── item.dart +├── providers/ +│ └── items_provider.dart +└── screens/ + └── items_screen.dart +\`\`\` + +### Appwrite Service + +\`\`\`dart +// services/appwrite_service.dart +import 'package:appwrite/appwrite.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class AppwriteService { + static final AppwriteService _instance = AppwriteService._internal(); + factory AppwriteService() => _instance; + + late final Client client; + late final Account account; + late final TablesDB tablesDB; + late final Storage storage; + late final Realtime realtime; + + static String get databaseId => dotenv.env['APPWRITE_DATABASE_ID']!; + static String get bucketId => dotenv.env['APPWRITE_BUCKET_ID'] ?? ''; + + AppwriteService._internal() { + client = Client() + .setEndpoint(dotenv.env['APPWRITE_ENDPOINT']!) + .setProject(dotenv.env['APPWRITE_PROJECT_ID']!); + + account = Account(client); + tablesDB = TablesDB(client); + storage = Storage(client); + realtime = Realtime(client); + } +} +\`\`\` + +### Model + +\`\`\`dart +// models/item.dart +class Item { + final String id; + String title; + String? description; + final String createdBy; + final String? teamId; + final DateTime createdAt; + final DateTime updatedAt; + + Item({ + required this.id, + required this.title, + this.description, + required this.createdBy, + this.teamId, + required this.createdAt, + required this.updatedAt, + }); + + factory Item.fromRow(Row doc) { + return Item( + id: doc.$id, + title: doc.data['title'], + description: doc.data['description'], + createdBy: doc.data['createdBy'], + teamId: doc.data['teamId'], + createdAt: DateTime.parse(doc.$createdAt), + updatedAt: DateTime.parse(doc.$updatedAt), + ); + } +} +\`\`\` + +### Repository + +\`\`\`dart +// repositories/item_repository.dart +import 'package:appwrite/appwrite.dart'; +import '../services/appwrite_service.dart'; +import '../models/item.dart'; + +class ItemRepository { + final _tablesDB = AppwriteService().tablesDB; + final _tableId = 'items'; + + Future create({ + required String title, + required String userId, + String? teamId, + }) async { + final doc = await _tablesDB.createRow( + databaseId: AppwriteService.databaseId, + tableId: _tableId, + rowId: ID.unique(), + data: { + 'title': title.trim(), + 'description': null, + 'createdBy': userId, + 'teamId': teamId, + }, + ); + return Item.fromRow(doc); + } + + Future> listByOwner(String userId) async { + final result = await _tablesDB.listRows( + databaseId: AppwriteService.databaseId, + tableId: _tableId, + queries: [ + Query.equal('createdBy', userId), + Query.orderDesc(r'$createdAt'), + Query.limit(100), + ], + ); + return result.rows.map(Item.fromRow).toList(); + } + + Future get(String id) async { + try { + final doc = await _tablesDB.getRow( + databaseId: AppwriteService.databaseId, + tableId: _tableId, + rowId: id, + ); + return Item.fromRow(doc); + } on AppwriteException { + return null; + } + } + + Future update(String id, {required String title}) async { + final doc = await _tablesDB.updateRow( + databaseId: AppwriteService.databaseId, + tableId: _tableId, + rowId: id, + data: {'title': title.trim()}, + ); + return Item.fromRow(doc); + } + + Future delete(String id) async { + await _tablesDB.deleteRow( + databaseId: AppwriteService.databaseId, + tableId: _tableId, + rowId: id, + ); + } +} +\`\`\` + +### Provider (Riverpod Example) + +\`\`\`dart +// providers/items_provider.dart +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../repositories/item_repository.dart'; +import '../repositories/auth_repository.dart'; +import '../models/item.dart'; + +final itemRepositoryProvider = Provider((ref) => ItemRepository()); +final authRepositoryProvider = Provider((ref) => AuthRepository()); + +final itemsProvider = StateNotifierProvider>>((ref) { + return ItemsNotifier( + ref.watch(itemRepositoryProvider), + ref.watch(authRepositoryProvider), + ); +}); + +class ItemsNotifier extends StateNotifier>> { + final ItemRepository _itemRepo; + final AuthRepository _authRepo; + RealtimeSubscription? _subscription; + + ItemsNotifier(this._itemRepo, this._authRepo) : super(const AsyncValue.loading()); + + Future loadItems() async { + state = const AsyncValue.loading(); + try { + final user = await _authRepo.getCurrentUser(); + final items = await _itemRepo.listByOwner(user.$id); + state = AsyncValue.data(items); + } catch (e, st) { + state = AsyncValue.error(e, st); + } + } + + Future createItem(String title, {String? teamId}) async { + try { + final user = await _authRepo.getCurrentUser(); + await _itemRepo.create(title: title, userId: user.$id, teamId: teamId); + await loadItems(); + } catch (e) { + // Handle error + } + } + + Future deleteItem(String id) async { + try { + await _itemRepo.delete(id); + await loadItems(); + } catch (e) { + // Handle error + } + } + + void subscribeToChanges() { + final channel = 'databases.\${AppwriteService.databaseId}.tables.items.rows'; + _subscription = AppwriteService().realtime.subscribe([channel]); + _subscription!.stream.listen((event) => loadItems()); + } + + void unsubscribe() { + _subscription?.close(); + } + + @override + void dispose() { + unsubscribe(); + super.dispose(); + } +} +\`\`\` + +### Screen Widget + +\`\`\`dart +// screens/items_screen.dart +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../providers/items_provider.dart'; + +class ItemsScreen extends ConsumerStatefulWidget { + const ItemsScreen({super.key}); + + @override + ConsumerState createState() => _ItemsScreenState(); +} + +class _ItemsScreenState extends ConsumerState { + @override + void initState() { + super.initState(); + ref.read(itemsProvider.notifier).loadItems(); + ref.read(itemsProvider.notifier).subscribeToChanges(); + } + + @override + void dispose() { + ref.read(itemsProvider.notifier).unsubscribe(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final itemsAsync = ref.watch(itemsProvider); + + return Scaffold( + appBar: AppBar(title: const Text('Items')), + body: itemsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, _) => Center(child: Text('Error: \$err')), + data: (items) => ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return ListTile( + title: Text(item.title), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () => ref.read(itemsProvider.notifier).deleteItem(item.id), + ), + ); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showCreateDialog(context), + child: const Icon(Icons.add), + ), + ); + } + + void _showCreateDialog(BuildContext context) { + // Show dialog to create new item + } +} +\`\`\` + +### Best Practices + +- Initialize AppwriteService before runApp() +- Use flutter_dotenv for environment variables +- Use state management (Riverpod, Provider, Bloc) +- Handle loading/error states in UI +- Dispose realtime subscriptions properly +- Use offline-first approach with local caching +`; + +/** + * React Native SDK implementation pattern + */ +export const reactNativePattern = ` +## React Native Implementation Pattern + +### Project Structure + +\`\`\` +src/ +├── services/ +│ └── appwrite.ts +├── repositories/ +│ ├── itemRepository.ts +│ └── authRepository.ts +├── hooks/ +│ ├── useItems.ts +│ └── useAuth.ts +├── types/ +│ └── index.ts +└── screens/ + └── ItemsScreen.tsx +\`\`\` + +### Appwrite Service + +\`\`\`typescript +// services/appwrite.ts +import { Client, Account, TablesDB, Storage } from 'react-native-appwrite' +import Config from 'react-native-config' + +const client = new Client() + .setEndpoint(Config.APPWRITE_ENDPOINT!) + .setProject(Config.APPWRITE_PROJECT_ID!) + +export const account = new Account(client) +export const tablesDB = new TablesDB(client) +export const storage = new Storage(client) +export { client } + +export const DATABASE_ID = Config.APPWRITE_DATABASE_ID! +export const BUCKET_ID = Config.APPWRITE_BUCKET_ID ?? '' +\`\`\` + +### Types + +\`\`\`typescript +// types/index.ts +export interface Item { + $id: string + title: string + description: string | null + createdBy: string + teamId: string | null + $createdAt: string + $updatedAt: string +} + +export interface User { + $id: string + email: string + name: string +} +\`\`\` + +### Repository + +\`\`\`typescript +// repositories/itemRepository.ts +import { ID, Query } from 'react-native-appwrite' +import { databases, DATABASE_ID } from '../services/appwrite' +import type { Item } from '../types' + +const TABLE_ID = 'items' + +export const itemRepository = { + async create(data: { title: string; userId: string; teamId?: string }): Promise { + return tablesDB.createRow( + DATABASE_ID, + TABLE_ID, + ID.unique(), + { + title: data.title.trim(), + description: null, + createdBy: data.userId, + teamId: data.teamId ?? null, + } + ) + }, + + async listByOwner(userId: string): Promise { + const result = await tablesDB.listRows( + DATABASE_ID, + TABLE_ID, + [ + Query.equal('createdBy', userId), + Query.orderDesc('$createdAt'), + Query.limit(100), + ] + ) + return result.rows + }, + + async get(id: string): Promise { + try { + return await tablesDB.getRow(DATABASE_ID, TABLE_ID, id) + } catch { + return null + } + }, + + async update(id: string, data: { title?: string }): Promise { + const updateData: Record = {} + if (data.title) updateData.title = data.title.trim() + + return tablesDB.updateRow(DATABASE_ID, TABLE_ID, id, updateData) + }, + + async delete(id: string): Promise { + await tablesDB.deleteRow(DATABASE_ID, TABLE_ID, id) + }, +} +\`\`\` + +### Custom Hook + +\`\`\`typescript +// hooks/useItems.ts +import { useState, useEffect, useCallback } from 'react' +import { RealtimeResponseEvent } from 'react-native-appwrite' +import { client, DATABASE_ID } from '../services/appwrite' +import { itemRepository } from '../repositories/itemRepository' +import { useAuth } from './useAuth' +import type { Item } from '../types' + +export function useItems() { + const [items, setItems] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const { user } = useAuth() + + const loadItems = useCallback(async () => { + if (!user) return + + setIsLoading(true) + setError(null) + + try { + const data = await itemRepository.listByOwner(user.$id) + setItems(data) + } catch (e) { + setError(e as Error) + } finally { + setIsLoading(false) + } + }, [user]) + + const createItem = useCallback(async (title: string, teamId?: string) => { + if (!user) throw new Error('Not authenticated') + + await itemRepository.create({ title, userId: user.$id, teamId }) + await loadItems() + }, [user, loadItems]) + + const deleteItem = useCallback(async (id: string) => { + await itemRepository.delete(id) + await loadItems() + }, [loadItems]) + + // Initial load + useEffect(() => { + loadItems() + }, [loadItems]) + + // Realtime subscription + useEffect(() => { + if (!user) return + + const channel = \`databases.\${DATABASE_ID}.tables.items.rows\` + const unsubscribe = client.subscribe(channel, (response: RealtimeResponseEvent) => { + const event = response.events[0] + + if (event.includes('.create')) { + setItems(prev => [response.payload, ...prev]) + } else if (event.includes('.update')) { + setItems(prev => prev.map(item => + item.$id === response.payload.$id ? response.payload : item + )) + } else if (event.includes('.delete')) { + setItems(prev => prev.filter(item => item.$id !== response.payload.$id)) + } + }) + + return () => unsubscribe() + }, [user]) + + return { + items, + isLoading, + error, + refresh: loadItems, + createItem, + deleteItem, + } +} +\`\`\` + +### Screen Component + +\`\`\`typescript +// screens/ItemsScreen.tsx +import React, { useState } from 'react' +import { + View, + Text, + FlatList, + TextInput, + TouchableOpacity, + ActivityIndicator, + RefreshControl, + StyleSheet, +} from 'react-native' +import { useItems } from '../hooks/useItems' + +export function ItemsScreen() { + const { items, isLoading, error, refresh, createItem, deleteItem } = useItems() + const [newTitle, setNewTitle] = useState('') + + const handleCreate = async () => { + if (!newTitle.trim()) return + await createItem(newTitle) + setNewTitle('') + } + + if (error) { + return ( + + Error: {error.message} + + Retry + + + ) + } + + return ( + + + + + Add + + + + item.$id} + renderItem={({ item }) => ( + + {item.title} + deleteItem(item.$id)}> + Delete + + + )} + refreshControl={ + + } + ListEmptyComponent={ + !isLoading ? No items yet : null + } + /> + + ) +} + +const styles = StyleSheet.create({ + container: { flex: 1, padding: 16 }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + inputRow: { flexDirection: 'row', marginBottom: 16 }, + input: { flex: 1, borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12 }, + addButton: { marginLeft: 8, backgroundColor: '#007AFF', borderRadius: 8, padding: 12 }, + addButtonText: { color: 'white', fontWeight: 'bold' }, + itemRow: { flexDirection: 'row', justifyContent: 'space-between', padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' }, + itemTitle: { fontSize: 16 }, + deleteText: { color: 'red' }, + empty: { textAlign: 'center', marginTop: 32, color: '#999' }, +}) +\`\`\` + +### Best Practices + +- Use react-native-config for environment variables +- Store session in SecureStore or encrypted storage +- Handle app state changes (background/foreground) +- Implement proper error boundaries +- Use custom hooks for data fetching logic +- Unsubscribe from realtime in cleanup functions +- Handle offline state gracefully +`; + +/** + * Get implementation pattern for a specific framework + * @param {string} framework - Framework name + * @returns {string} Implementation pattern + */ +export function getImplementationPattern(framework) { + /** @type {Record} */ + const patterns = { + // JS SSR frameworks + tanstack: tanstackStartPattern, + nextjs: nextjsPattern, + svelte: sveltekitPattern, + nuxt: nuxtPattern, + astro: astroPattern, + // Server SDKs + python: pythonServerPattern, + php: phpServerPattern, + go: goServerPattern, + ruby: rubyServerPattern, + dotnet: dotnetServerPattern, + dart: dartServerPattern, + kotlin: kotlinServerPattern, + swift: swiftServerPattern, + // Mobile/Client SDKs + android: androidPattern, + apple: applePattern, + flutter: flutterPattern, + 'react-native': reactNativePattern, + }; + + return patterns[framework] || ''; +} + +/** + * Check if framework is a server SDK + * @param {string} framework - Framework name + * @returns {boolean} + */ +export function isServerSDK(framework) { + const serverSDKs = ['python', 'php', 'go', 'ruby', 'dotnet', 'dart', 'kotlin', 'swift']; + return serverSDKs.includes(framework); +} + +/** + * Check if framework is a mobile/client SDK + * @param {string} framework - Framework name + * @returns {boolean} + */ +export function isMobileSDK(framework) { + const mobileSDKs = ['android', 'apple', 'flutter', 'react-native']; + return mobileSDKs.includes(framework); +} + +/** + * Get full implementation guide for a JS SSR framework + * @param {string} framework - Framework name + * @param {string} [sdk='javascript'] - SDK name for template selection + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {string} Complete implementation guide + */ +export function getFullImplementationGuide(framework, sdk = 'javascript', features = []) { + const frameworkPattern = getImplementationPattern(framework); + + if (!frameworkPattern) { + return generalImplementationRules; + } + + // Build sections based on selected features + const sections = [generalImplementationRules, frameworkPattern]; + + // Include database-specific rules if database is selected + if (features.includes('database')) { + sections.push(databaseImplementationRules); + sections.push(getDatabaseWrapperTemplate(sdk)); + } + + // Include storage-specific rules if storage is selected + if (features.includes('storage')) { + sections.push(storageImplementationRules); + sections.push(getStorageWrapperTemplate(sdk)); + } + + // Include other service patterns + if (features.includes('functions')) { + sections.push(functionsPattern); + } + + if (features.includes('messaging')) { + sections.push(messagingPattern); + } + + if (features.includes('realtime')) { + sections.push(realtimePattern); + } + + if (features.includes('sites')) { + sections.push(sitesPattern); + } + + return sections.join('\n\n'); +} + +/** + * Get implementation guide for server SDKs + * @param {string} sdk - SDK/Framework name (python, php, go, ruby, dotnet, dart, kotlin, swift) + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {string} Server SDK implementation guide + */ +export function getServerImplementationGuide(sdk, features = []) { + const frameworkPattern = getImplementationPattern(sdk); + + if (!frameworkPattern) { + return ''; + } + + // Build sections based on selected features + const sections = [generalImplementationRules, frameworkPattern]; + + // Include database-specific rules if database is selected + if (features.includes('database')) { + sections.push(databaseImplementationRules); + sections.push(getDatabaseWrapperTemplate(sdk)); + } + + // Include storage-specific rules if storage is selected + if (features.includes('storage')) { + sections.push(storageImplementationRules); + sections.push(getStorageWrapperTemplate(sdk)); + } + + // Include other service patterns + if (features.includes('functions')) { + sections.push(functionsPattern); + } + + if (features.includes('messaging')) { + sections.push(messagingPattern); + } + + return sections.join('\n\n'); +} + +/** + * Get implementation guide for mobile/client SDKs + * @param {string} sdk - SDK/Framework name (android, apple, flutter, react-native) + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {string} Mobile SDK implementation guide + */ +export function getMobileImplementationGuide(sdk, features = []) { + const frameworkPattern = getImplementationPattern(sdk); + + if (!frameworkPattern) { + return ''; + } + + // Build sections based on selected features + const sections = [generalImplementationRules, frameworkPattern]; + + // Include database-specific rules if database is selected + if (features.includes('database')) { + sections.push(databaseImplementationRules); + sections.push(getDatabaseWrapperTemplate(sdk)); + } + + // Include storage-specific rules if storage is selected + if (features.includes('storage')) { + sections.push(storageImplementationRules); + sections.push(getStorageWrapperTemplate(sdk)); + } + + // Include other service patterns + if (features.includes('functions')) { + sections.push(functionsPattern); + } + + if (features.includes('messaging')) { + sections.push(messagingPattern); + } + + if (features.includes('realtime')) { + sections.push(realtimePattern); + } + + return sections.join('\n\n'); +} diff --git a/src/lib/languages/common/install.js b/src/lib/languages/common/install.js new file mode 100644 index 0000000..f00d2d7 --- /dev/null +++ b/src/lib/languages/common/install.js @@ -0,0 +1,132 @@ +/** + * Common installation instructions by package manager/language + */ + +export const npmInstall = `npm install appwrite`; + +/** + * JavaScript/TypeScript installation section + * @param {string} [packageName] - Package name (default: 'appwrite') + * @param {string} [title] - Installation title (default: 'Install the Appwrite JavaScript SDK') + */ +export function jsInstall(packageName = 'appwrite', title = 'Install the Appwrite JavaScript SDK') { + return `## SDK Installation + +${title} using npm: + +\`\`\`bash +npm install ${packageName} +\`\`\` + +You can also use yarn, pnpm, or bun instead.`; +} + +/** + * Default JavaScript installation (export as constant for backwards compatibility) + */ +export const jsInstallDefault = jsInstall(); + +/** + * Node.js Server SDK installation section for SSR frameworks + */ +export const nodeAppwriteInstall = jsInstall('node-appwrite', 'Install the Appwrite Node.js Server SDK'); + +/** + * Python installation section + */ +export const pythonInstall = `## SDK Installation + +Install the Appwrite Python SDK using pip: + +\`\`\`bash +pip install appwrite +\`\`\` + +Or using poetry: + +\`\`\`bash +poetry add appwrite +\`\`\``; + +/** + * PHP installation section + */ +export const phpInstall = `## SDK Installation + +Install the Appwrite PHP SDK using Composer: + +\`\`\`bash +composer require appwrite/appwrite +\`\`\``; + +/** + * Go installation section + */ +export const goInstall = `## SDK Installation + +Install the Appwrite Go SDK: + +\`\`\`bash +go get github.com/appwrite/sdk-for-go +\`\`\``; + +/** + * Ruby installation section + */ +export const rubyInstall = `## SDK Installation + +Install the Appwrite Ruby SDK using Bundler: + +\`\`\`bash +gem install appwrite +\`\`\` + +Or add to your \`Gemfile\`: + +\`\`\`ruby +gem 'appwrite' +\`\`\``; + +/** + * .NET installation section + */ +export const dotnetInstall = `## SDK Installation + +Install the Appwrite .NET SDK using NuGet: + +\`\`\`bash +dotnet add package Appwrite +\`\`\` + +Or using Package Manager: + +\`\`\`powershell +Install-Package Appwrite +\`\`\``; + +/** + * Dart/Flutter installation section (async - requires version lookup) + * @param {string} version - SDK version + * @param {boolean} isServer - Whether this is for server SDK + */ +export const dartInstall = (version, isServer = false) => { + const sdkName = isServer ? 'Dart Server SDK' : 'Flutter SDK'; + const pubCommand = isServer ? 'dart pub get' : 'flutter pub get'; + const packageName = isServer ? 'dart_appwrite' : 'appwrite'; + + return `## SDK Installation + +Add the Appwrite ${sdkName} to your \`pubspec.yaml\`: + +\`\`\`yaml +dependencies: + ${packageName}: ^${version} +\`\`\` + +Then install it: + +\`\`\`bash +${pubCommand} +\`\`\``; +}; + diff --git a/src/lib/languages/common/permissions-examples.js b/src/lib/languages/common/permissions-examples.js new file mode 100644 index 0000000..392081e --- /dev/null +++ b/src/lib/languages/common/permissions-examples.js @@ -0,0 +1,318 @@ +/** + * Essential permission patterns for Appwrite multi-tenancy + * + * These examples demonstrate the critical patterns that differentiate + * good multi-tenant architecture from poor practices. For full API + * documentation, see the official Appwrite docs. + */ + +/** + * Permission patterns for each SDK + * Only includes the essential anti-pattern vs correct pattern examples + */ +const permissionPatterns = { + javascript: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +Permission.read(Role.user('')) +Permission.write(Role.user(''))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +Permission.read(Role.team('', 'member')) +Permission.update(Role.team('', 'admin')) +Permission.delete(Role.team('', 'owner'))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +const response = await tablesDB.listRows( + '', + '', + [ + Query.equal('teamId', ''), // Critical for isolation + Query.orderDesc('$createdAt'), + Query.limit(25) + ] +);`, + + roleCheck: `// Verify permissions before sensitive operations +const memberships = await teams.listMemberships(''); +const membership = memberships.memberships.find(m => m.userId === userId); + +if (!membership?.roles.includes('admin')) { + throw new Error('Insufficient permissions'); +}` + }, + + python: { + avoidUserPermissions: `# DON'T: User-specific permissions don't scale +Permission.read(Role.user('')) +Permission.write(Role.user(''))`, + + preferTeamPermissions: `# DO: Team-based permissions scale with your organization +Permission.read(Role.team('', 'member')) +Permission.update(Role.team('', 'admin')) +Permission.delete(Role.team('', 'owner'))`, + + queryWithTeamId: `# ALWAYS filter by teamId for tenant isolation +response = tables_db.list_rows( + database_id='', + table_id='', + queries=[ + Query.equal('teamId', ''), # Critical for isolation + Query.order_desc('$createdAt'), + Query.limit(25) + ] +)`, + + roleCheck: `# Verify permissions before sensitive operations +memberships = teams.list_memberships('') +membership = next((m for m in memberships.memberships if m.user_id == user_id), None) + +if not membership or 'admin' not in membership.roles: + raise Exception('Insufficient permissions')` + }, + + php: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +Permission::read(Role::user('')) +Permission::write(Role::user(''))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +Permission::read(Role::team('', 'member')) +Permission::update(Role::team('', 'admin')) +Permission::delete(Role::team('', 'owner'))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +$response = $tablesDB->listRows( + databaseId: '', + tableId: '', + queries: [ + Query::equal('teamId', ''), // Critical for isolation + Query::orderDesc('$createdAt'), + Query::limit(25) + ] +);`, + + roleCheck: `// Verify permissions before sensitive operations +$memberships = $teams->listMemberships(''); +$membership = array_filter($memberships->memberships, fn($m) => $m->userId === $userId); + +if (empty($membership) || !in_array('admin', current($membership)->roles)) { + throw new Exception('Insufficient permissions'); +}` + }, + + kotlin: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +Permission.read(Role.user("")) +Permission.write(Role.user(""))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +Permission.read(Role.team("", "member")) +Permission.update(Role.team("", "admin")) +Permission.delete(Role.team("", "owner"))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +val response = tablesDB.listRows( + databaseId = "", + tableId = "", + queries = listOf( + Query.equal("teamId", ""), // Critical for isolation + Query.orderDesc("\$createdAt"), + Query.limit(25) + ) +)`, + + roleCheck: `// Verify permissions before sensitive operations +val memberships = teams.listMemberships("") +val membership = memberships.memberships.find { it.userId == userId } + +if (membership == null || "admin" !in membership.roles) { + throw Exception("Insufficient permissions") +}` + }, + + swift: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +Permission.read(Role.user("")) +Permission.write(Role.user(""))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +Permission.read(Role.team("", "member")) +Permission.update(Role.team("", "admin")) +Permission.delete(Role.team("", "owner"))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +let response = try await tablesDB.listRows( + databaseId: "", + tableId: "", + queries: [ + Query.equal("teamId", ""), // Critical for isolation + Query.orderDesc("$createdAt"), + Query.limit(25) + ] +)`, + + roleCheck: `// Verify permissions before sensitive operations +let memberships = try await teams.listMemberships("") +guard let membership = memberships.memberships.first(where: { $0.userId == userId }), + membership.roles.contains("admin") else { + throw AppError.insufficientPermissions +}` + }, + + dart: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +Permission.read(Role.user('')) +Permission.write(Role.user(''))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +Permission.read(Role.team('', 'member')) +Permission.update(Role.team('', 'admin')) +Permission.delete(Role.team('', 'owner'))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +final response = await tablesDB.listRows( + databaseId: '', + tableId: '', + queries: [ + Query.equal('teamId', ''), // Critical for isolation + Query.orderDesc('\$createdAt'), + Query.limit(25), + ], +);`, + + roleCheck: `// Verify permissions before sensitive operations +final memberships = await teams.listMemberships(''); +final membership = memberships.memberships.firstWhere( + (m) => m.userId == userId, + orElse: () => null, +); + +if (membership == null || !membership.roles.contains('admin')) { + throw Exception('Insufficient permissions'); +}` + }, + + go: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +permission.Read(role.User("")) +permission.Write(role.User(""))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +permission.Read(role.Team("", "member")) +permission.Update(role.Team("", "admin")) +permission.Delete(role.Team("", "owner"))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +response, err := tablesDB.ListRows( + "", + "", + tablesDB.WithListRowsQueries([]string{ + query.Equal("teamId", ""), // Critical for isolation + query.OrderDesc("$createdAt"), + query.Limit(25), + }), +)`, + + roleCheck: `// Verify permissions before sensitive operations +memberships, _ := teams.ListMemberships("") +var membership *models.Membership +for _, m := range memberships.Memberships { + if m.UserId == userId { + membership = &m + break + } +} + +if membership == nil || !contains(membership.Roles, "admin") { + return errors.New("insufficient permissions") +}` + }, + + ruby: { + avoidUserPermissions: `# DON'T: User-specific permissions don't scale +Permission.read(Role.user('')) +Permission.write(Role.user(''))`, + + preferTeamPermissions: `# DO: Team-based permissions scale with your organization +Permission.read(Role.team('', 'member')) +Permission.update(Role.team('', 'admin')) +Permission.delete(Role.team('', 'owner'))`, + + queryWithTeamId: `# ALWAYS filter by teamId for tenant isolation +response = tables_db.list_rows( + database_id: '', + table_id: '', + queries: [ + Query.equal('teamId', ''), # Critical for isolation + Query.order_desc('$createdAt'), + Query.limit(25) + ] +)`, + + roleCheck: `# Verify permissions before sensitive operations +memberships = teams.list_memberships('') +membership = memberships.memberships.find { |m| m.user_id == user_id } + +unless membership&.roles&.include?('admin') + raise 'Insufficient permissions' +end` + }, + + dotnet: { + avoidUserPermissions: `// DON'T: User-specific permissions don't scale +Permission.Read(Role.User("")) +Permission.Write(Role.User(""))`, + + preferTeamPermissions: `// DO: Team-based permissions scale with your organization +Permission.Read(Role.Team("", "member")) +Permission.Update(Role.Team("", "admin")) +Permission.Delete(Role.Team("", "owner"))`, + + queryWithTeamId: `// ALWAYS filter by teamId for tenant isolation +var response = await tablesDB.ListRows( + databaseId: "", + tableId: "", + queries: new List { + Query.Equal("teamId", ""), // Critical for isolation + Query.OrderDesc("$createdAt"), + Query.Limit(25) + } +);`, + + roleCheck: `// Verify permissions before sensitive operations +var memberships = await teams.ListMemberships(""); +var membership = memberships.Memberships.FirstOrDefault(m => m.UserId == userId); + +if (membership == null || !membership.Roles.Contains("admin")) +{ + throw new UnauthorizedAccessException("Insufficient permissions"); +}` + } +}; + +/** + * Get permission examples for a specific SDK + * @param {string} sdk - The SDK name + * @returns {Object} Permission pattern examples + */ +export function getPermissionExamples(sdk) { + // Map SDK names to their pattern keys + const sdkMap = { + javascript: 'javascript', + 'react-native': 'javascript', + python: 'python', + php: 'php', + go: 'go', + ruby: 'ruby', + dotnet: 'dotnet', + swift: 'swift', + kotlin: 'kotlin', + apple: 'swift', + android: 'kotlin', + flutter: 'dart', + dart: 'dart' + }; + + const patternKey = sdkMap[sdk] || 'javascript'; + return permissionPatterns[patternKey] || permissionPatterns.javascript; +} diff --git a/src/lib/languages/common/products.js b/src/lib/languages/common/products.js new file mode 100644 index 0000000..cf610bd --- /dev/null +++ b/src/lib/languages/common/products.js @@ -0,0 +1,113 @@ +/** + * Common product documentation links and descriptions + * These are reused across all SDKs and frameworks + */ + +/** + * Authentication product documentation + */ +export const authProductLinks = `**Authentication Documentation:** + +- [Authentication Quick Start](https://appwrite.io/docs/products/auth/quick-start) - Getting started with authentication +- [Email & Password](https://appwrite.io/docs/products/auth/email-password) - Email/password authentication +- [OAuth2 Providers](https://appwrite.io/docs/products/auth/oauth2) - Social authentication (Google, GitHub, etc.) +- [Magic URL](https://appwrite.io/docs/products/auth/magic-url) - Passwordless authentication via email +- [Phone (SMS)](https://appwrite.io/docs/products/auth/phone-sms) - Phone number authentication +- [Anonymous Sessions](https://appwrite.io/docs/products/auth/anonymous) - Guest/anonymous users +- [JWT Tokens](https://appwrite.io/docs/products/auth/jwt) - JSON Web Token authentication +- [MFA/2FA](https://appwrite.io/docs/products/auth/mfa) - Multi-factor authentication +- [SSR Authentication](https://appwrite.io/docs/products/auth/server-side-rendering) - Server-side rendering auth patterns +- [Teams](https://appwrite.io/docs/products/auth/teams) - Team management and team-based permissions +- [Team Invites](https://appwrite.io/docs/products/auth/team-invites) - Inviting members to teams +- [Multi-tenancy](https://appwrite.io/docs/products/auth/multi-tenancy) - Building multi-tenant applications with teams`; + +/** + * Permissions product documentation + */ +export const permissionsProductLinks = `**Permissions Documentation:** + +- [Permissions Overview](https://appwrite.io/docs/advanced/platform/permissions) - Understanding Appwrite's permission system +- [Role Types](https://appwrite.io/docs/advanced/platform/permissions#role-types) - Available permission roles (any, users, guests, team, member, label) +- [Permission Types](https://appwrite.io/docs/advanced/platform/permissions#permission-types) - Read, create, update, delete permissions +- [Teams](https://appwrite.io/docs/products/auth/teams) - Team-based access control +- [Multi-tenancy](https://appwrite.io/docs/products/auth/multi-tenancy) - Tenant isolation patterns`; + +/** + * Database product documentation + */ +export const databaseProductLinks = `**Database Documentation:** + +- [Database Quick Start](https://appwrite.io/docs/products/databases/quick-start) - Getting started with TablesDB +- [Tables](https://appwrite.io/docs/products/databases/tables) - Creating and managing tables +- [Rows](https://appwrite.io/docs/products/databases/rows) - CRUD operations on rows +- [Queries](https://appwrite.io/docs/products/databases/queries) - Filtering, sorting, and querying data +- [Pagination](https://appwrite.io/docs/products/databases/pagination) - Paginating large datasets +- [Relationships](https://appwrite.io/docs/products/databases/relationships) - One-to-one, one-to-many, many-to-many relationships +- [Permissions](https://appwrite.io/docs/products/databases/permissions) - Row and table-level permissions +- [Transactions](https://appwrite.io/docs/products/databases/transactions) - Atomic operations`; + +/** + * Storage product documentation + */ +export const storageProductLinks = `**Storage Documentation:** + +- [Storage Quick Start](https://appwrite.io/docs/products/storage/quick-start) - Getting started with storage +- [Buckets](https://appwrite.io/docs/products/storage/buckets) - Creating and managing storage buckets +- [Upload & Download](https://appwrite.io/docs/products/storage/upload-download) - File upload and download operations +- [Permissions](https://appwrite.io/docs/products/storage/permissions) - Bucket and file-level permissions +- [Images](https://appwrite.io/docs/products/storage/images) - Image manipulation and transformations`; + +/** + * Functions product documentation + */ +export const functionsProductLinks = `**Functions Documentation:** + +- [Functions Quick Start](https://appwrite.io/docs/products/functions/quick-start) - Getting started with serverless functions +- [Develop Functions](https://appwrite.io/docs/products/functions/develop) - Writing and developing functions +- [Execute Functions](https://appwrite.io/docs/products/functions/execute) - Triggering function executions +- [Deployments](https://appwrite.io/docs/products/functions/deployments) - Deploying function code +- [Domains](https://appwrite.io/docs/products/functions/domains) - Custom domains for functions +- [Events](https://appwrite.io/docs/advanced/platform/events) - Event-driven function execution +- [Scheduled Executions](https://appwrite.io/docs/products/functions/execute#schedule) - Cron-based scheduling`; + +/** + * Messaging product documentation + */ +export const messagingProductLinks = `**Messaging Documentation:** + +- [Messaging Overview](https://appwrite.io/docs/products/messaging) - Getting started with messaging +- [Push Notifications](https://appwrite.io/docs/products/messaging/send-push-notifications) - Sending push notifications +- [Email Messages](https://appwrite.io/docs/products/messaging/send-email-messages) - Sending emails +- [SMS Messages](https://appwrite.io/docs/products/messaging/send-sms-messages) - Sending SMS +- [Topics](https://appwrite.io/docs/products/messaging/topics) - Managing message topics +- [Targets](https://appwrite.io/docs/products/messaging/targets) - Managing message targets +- [Providers](https://appwrite.io/docs/products/messaging/providers) - Configuring messaging providers`; + +/** + * Sites product documentation + */ +export const sitesProductLinks = `**Sites Documentation:** + +- [Sites Quick Start](https://appwrite.io/docs/products/sites/quick-start) - Getting started with Sites +- [Deploy from Git](https://appwrite.io/docs/products/sites/deploy-from-git) - Git-based deployments +- [Deploy from CLI](https://appwrite.io/docs/products/sites/deploy-from-cli) - CLI deployments +- [Deploy Manually](https://appwrite.io/docs/products/sites/deploy-manually) - Manual file uploads +- [Deployments](https://appwrite.io/docs/products/sites/deployments) - Managing deployments +- [Domains](https://appwrite.io/docs/products/sites/domains) - Custom domain configuration +- [Rendering](https://appwrite.io/docs/products/sites/rendering) - Static vs SSR rendering +- [Frameworks](https://appwrite.io/docs/products/sites/frameworks) - Supported frameworks +- [Rollbacks](https://appwrite.io/docs/products/sites/instant-rollbacks) - Instant rollbacks +- [Previews](https://appwrite.io/docs/products/sites/previews) - Deployment previews`; + +/** + * Realtime product documentation + */ +export const realtimeProductLinks = `**Realtime Documentation:** + +- [Realtime Overview](https://appwrite.io/docs/products/realtime) - Getting started with realtime +- [Database Subscriptions](https://appwrite.io/docs/products/realtime/subscribe-to-databases) - Subscribe to database changes +- [Storage Subscriptions](https://appwrite.io/docs/products/realtime/subscribe-to-storage) - Subscribe to storage changes +- [Account Subscriptions](https://appwrite.io/docs/products/realtime/subscribe-to-account) - Subscribe to account changes +- [Channels](https://appwrite.io/docs/products/realtime/channels) - Available subscription channels +- [Events](https://appwrite.io/docs/products/realtime/events) - Event types and payloads`; + diff --git a/src/lib/languages/common/security.js b/src/lib/languages/common/security.js new file mode 100644 index 0000000..440223b --- /dev/null +++ b/src/lib/languages/common/security.js @@ -0,0 +1,424 @@ +/** + * Common security best practices and guidelines + */ + +/** + * Client-side security best practices (for web frameworks) + */ +export const clientSecurity = `**Best Practices:** +- Store endpoint and project ID in environment variables +- Never commit API keys to version control +- Initialize services once and export as singletons`; + +/** + * Client-side security with environment variable note + * @param {string} envFile - Environment file name (e.g., '.env.local', '.env') + */ +export const clientSecurityWithEnv = (envFile = '.env.local') => `**Best Practices:** +- Store endpoint and project ID in environment variables (${envFile}) +- Never commit API keys to version control +- Initialize services once and export as singletons`; + +/** + * Server-side security best practices + */ +export const serverSecurity = `**Security:** +- API keys should NEVER be exposed to client-side code +- Use environment variables for all sensitive configuration +- API keys grant admin access - use with extreme caution`; + +/** + * Server-side security with framework-specific config note + * @param {string} configMethod - Configuration method (e.g., "configuration files or environment variables", "Rails credentials") + */ +export const serverSecurityWithConfig = (configMethod = 'environment variables') => `**Security:** +- API keys should NEVER be exposed to client-side code +- Use ${configMethod} for configuration +- API keys grant admin access - use with extreme caution`; + +/** + * Authentication-specific notes + */ +export const authNote = `**Authentication:** +- Prefer SSR auth for better security and performance`; + +/** + * SSR Authentication pattern (language-agnostic explanation) + * Use this for the conceptual overview, then pair with language-specific examples + */ +export const ssrAuthPatternExplanation = `**SSR Authentication Pattern:** + +Server-side rendering requires using the Server SDK instead of the client SDK. + +**Authentication Flow:** +1. User credentials are sent from browser to your server +2. Your server authenticates with Appwrite using the Server SDK +3. Appwrite returns a session object +4. Store the session secret in an httpOnly cookie +5. Subsequent requests include the session cookie +6. Your server makes authenticated requests on behalf of the user + +**Key Implementation Details:** + +**Initialize Two Clients:** +- **Admin Client**: Uses API key for unauthenticated requests and session creation +- **Session Client**: Uses session cookie for user-specific requests + +**Best Practices:** +- Use httpOnly, secure, and sameSite cookie flags +- Create new session client per request +- Never share clients between requests +- Use API key for admin client to bypass rate limits +- Set forwarded user agent for better session tracking + +**See full SSR auth guide:** https://appwrite.io/docs/products/auth/server-side-rendering`; + +/** + * SSR Auth code examples by language + */ +const ssrAuthExamples = { + javascript: { + createSession: `import { Client, Account } from "node-appwrite"; + +// In your login endpoint: +const account = new Account(adminClient); +const session = await account.createEmailPasswordSession(email, password); + +// Set httpOnly cookie with session secret +res.cookie('a_session_', session.secret, { + httpOnly: true, + secure: true, + sameSite: 'strict', + expires: new Date(session.expire), + path: '/' +});`, + useSession: `// Read session from cookie +const session = req.cookies['a_session_']; + +// Create session client +const sessionClient = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setSession(session); + +const account = new Account(sessionClient); +const user = await account.get();` + }, + python: { + createSession: `from appwrite.client import Client +from appwrite.services.account import Account + +# In your login endpoint: +account = Account(admin_client) +session = account.create_email_password_session(email, password) + +# Set httpOnly cookie with session secret +response.set_cookie( + 'a_session_', + session['secret'], + httponly=True, + secure=True, + samesite='strict', + expires=session['expire'], + path='/' +)`, + useSession: `# Read session from cookie +session = request.cookies.get('a_session_') + +# Create session client +session_client = Client() +session_client.set_endpoint('https://cloud.appwrite.io/v1') +session_client.set_project('') +session_client.set_session(session) + +account = Account(session_client) +user = account.get()` + }, + php: { + createSession: `use Appwrite\\Client; +use Appwrite\\Services\\Account; + +// In your login endpoint: +$account = new Account($adminClient); +$session = $account->createEmailPasswordSession($email, $password); + +// Set httpOnly cookie with session secret +setcookie( + 'a_session_', + $session['secret'], + [ + 'httponly' => true, + 'secure' => true, + 'samesite' => 'Strict', + 'expires' => strtotime($session['expire']), + 'path' => '/' + ] +);`, + useSession: `// Read session from cookie +$session = $_COOKIE['a_session_'] ?? null; + +// Create session client +$sessionClient = new Client(); +$sessionClient + ->setEndpoint('https://cloud.appwrite.io/v1') + ->setProject('') + ->setSession($session); + +$account = new Account($sessionClient); +$user = $account->get();` + }, + ruby: { + createSession: `require 'appwrite' + +# In your login endpoint: +account = Appwrite::Account.new(admin_client) +session = account.create_email_password_session(email: email, password: password) + +# Set httpOnly cookie with session secret +cookies['a_session_'] = { + value: session['secret'], + httponly: true, + secure: true, + same_site: :strict, + expires: Time.parse(session['expire']), + path: '/' +}`, + useSession: `# Read session from cookie +session = cookies['a_session_'] + +# Create session client +session_client = Appwrite::Client.new +session_client + .set_endpoint('https://cloud.appwrite.io/v1') + .set_project('') + .set_session(session) + +account = Appwrite::Account.new(session_client) +user = account.get` + }, + go: { + createSession: `import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +// In your login endpoint: +account := appwrite.NewAccount(adminClient) +session, _ := account.CreateEmailPasswordSession(email, password) + +// Set httpOnly cookie with session secret +http.SetCookie(w, &http.Cookie{ + Name: "a_session_", + Value: session.Secret, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + Expires: session.Expire, + Path: "/", +})`, + useSession: `// Read session from cookie +cookie, _ := r.Cookie("a_session_") + +// Create session client +sessionClient := appwrite.NewClient() +sessionClient.SetEndpoint("https://cloud.appwrite.io/v1") +sessionClient.SetProject("") +sessionClient.SetSession(cookie.Value) + +account := appwrite.NewAccount(sessionClient) +user, _ := account.Get()` + }, + dotnet: { + createSession: `using Appwrite; +using Appwrite.Services; + +// In your login endpoint: +var account = new Account(adminClient); +var session = await account.CreateEmailPasswordSession(email, password); + +// Set httpOnly cookie with session secret +Response.Cookies.Append("a_session_", session.Secret, new CookieOptions +{ + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + Expires = DateTimeOffset.Parse(session.Expire), + Path = "/" +});`, + useSession: `// Read session from cookie +var session = Request.Cookies["a_session_"]; + +// Create session client +var sessionClient = new Client() + .SetEndpoint("https://cloud.appwrite.io/v1") + .SetProject("") + .SetSession(session); + +var account = new Account(sessionClient); +var user = await account.Get();` + }, + kotlin: { + createSession: `import io.appwrite.Client +import io.appwrite.services.Account + +// In your login endpoint: +val account = Account(adminClient) +val session = account.createEmailPasswordSession(email, password) + +// Set httpOnly cookie with session secret +call.response.cookies.append( + name = "a_session_", + value = session.secret, + httpOnly = true, + secure = true, + extensions = mapOf("SameSite" to "Strict"), + expires = GMTDate(session.expire), + path = "/" +)`, + useSession: `// Read session from cookie +val session = call.request.cookies["a_session_"] + +// Create session client +val sessionClient = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setSession(session) + +val account = Account(sessionClient) +val user = account.get()` + }, + swift: { + createSession: `import Appwrite + +// In your login endpoint: +let account = Account(adminClient) +let session = try await account.createEmailPasswordSession(email: email, password: password) + +// Set httpOnly cookie with session secret +response.cookies["a_session_"] = HTTPCookie( + name: "a_session_", + value: session.secret, + httpOnly: true, + secure: true, + sameSite: .strict, + expires: session.expire, + path: "/" +)`, + useSession: `// Read session from cookie +let session = request.cookies["a_session_"] + +// Create session client +let sessionClient = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setSession(session) + +let account = Account(sessionClient) +let user = try await account.get()` + }, + dart: { + createSession: `import 'package:dart_appwrite/dart_appwrite.dart'; + +// In your login endpoint: +final account = Account(adminClient); +final session = await account.createEmailPasswordSession( + email: email, + password: password, +); + +// Set httpOnly cookie with session secret +response.headers.add( + 'Set-Cookie', + 'a_session_=\${session.secret}; HttpOnly; Secure; SameSite=Strict; Path=/', +);`, + useSession: `// Read session from cookie +final session = request.headers['Cookie'] + ?.split('; ') + .firstWhere((c) => c.startsWith('a_session_=')) + ?.split('=')[1]; + +// Create session client +final sessionClient = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setSession(session); + +final account = Account(sessionClient); +final user = await account.get();` + } +}; + +/** + * Get SSR auth examples for a specific language + * @param {string} language - The language/SDK name + * @returns {Object} SSR auth code examples + */ +export function getSSRAuthExamples(language) { + const languageMap = { + javascript: 'javascript', + nodejs: 'javascript', + 'react-native': 'javascript', + python: 'python', + php: 'php', + go: 'go', + ruby: 'ruby', + dotnet: 'dotnet', + swift: 'swift', + kotlin: 'kotlin', + dart: 'dart', + flutter: 'dart' + }; + + const key = languageMap[language] || 'javascript'; + return ssrAuthExamples[key] || ssrAuthExamples.javascript; +} + +/** + * Get full SSR auth pattern with language-specific examples + * @param {string} language - The language/SDK name + * @returns {string} Complete SSR auth pattern with examples + */ +export function getSSRAuthPattern(language = 'javascript') { + const examples = getSSRAuthExamples(language); + + return `${ssrAuthPatternExplanation} + +**Creating Sessions:** +\`\`\`${language === 'javascript' || language === 'nodejs' ? 'javascript' : language} +${examples.createSession} +\`\`\` + +**Making Authenticated Requests:** +\`\`\`${language === 'javascript' || language === 'nodejs' ? 'javascript' : language} +${examples.useSession} +\`\`\` + +**OAuth2 Flow:** +1. Redirect to OAuth provider using createOAuth2Token +2. Handle callback with userId and secret parameters +3. Call createSession to exchange for session object +4. Store session secret in cookie`; +} + +/** + * SSR Authentication pattern for Node.js/JavaScript SSR frameworks + * @deprecated Use getSSRAuthPattern('javascript') instead + */ +export const ssrAuthPattern = getSSRAuthPattern('javascript'); + +/** + * Framework-specific notes + */ +export const frameworkNotes = { + angular: `**Best Practices:** +- Store endpoint and project ID in environment variables +- Never commit API keys to version control +- Use Angular services for dependency injection + +${authNote}`, + + nodejs: `**Best Practices:** +- Store endpoint and project ID in environment variables +- Never commit API keys to version control +- Use environment variables for configuration +- API keys grant admin access - use with extreme caution` +}; diff --git a/src/lib/languages/common/utils.js b/src/lib/languages/common/utils.js new file mode 100644 index 0000000..effa417 --- /dev/null +++ b/src/lib/languages/common/utils.js @@ -0,0 +1,112 @@ +/** + * Utility functions for composing SDK initialization templates + */ + +/** + * Creates a security/best practices section (SDK Initialization section removed) + * @param {Object} options + * @param {string} options.securityNotes - Security/best practices section + * @param {string} [options.additionalNotes] - Additional framework-specific notes + * @returns {string} + */ +export function createSecuritySection({ + securityNotes, + additionalNotes = '' +}) { + return securityNotes + (additionalNotes ? `\n\n${additionalNotes}` : ''); +} + +/** + * Client-side API references for JavaScript frameworks + */ +const clientAPIReferences = ` +**API References:** +- [Account API](https://appwrite.io/docs/references/cloud/client-web/account) - Authentication and user management +- [Databases API](https://appwrite.io/docs/references/cloud/client-web/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/client-web/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/client-web/functions) - Serverless functions execution +- [Messaging API](https://appwrite.io/docs/references/cloud/client-web/messaging) - Push notifications and messaging +`; + +/** + * Creates a complete framework template by combining installation and security notes + * @param {Object} options + * @param {string} options.installation - Installation section + * @param {string} options.securityNotes - Security/best practices section + * @param {string} [options.additionalNotes] - Additional framework-specific notes + * @param {boolean} [options.includeAPIReferences=true] - Whether to include API references + * @returns {string} + */ +export function createFrameworkTemplate({ installation, securityNotes, additionalNotes = '', includeAPIReferences = true }) { + const securitySection = createSecuritySection({ securityNotes, additionalNotes }); + const apiSection = includeAPIReferences ? clientAPIReferences : ''; + return `${installation} +${apiSection} +${securitySection}`; +} + +/** + * Quick Start Guide URLs by framework + */ +export const quickStartUrls = { + // JavaScript frameworks + react: 'https://appwrite.io/docs/quick-starts/react', + nextjs: 'https://appwrite.io/docs/quick-starts/nextjs', + vue: 'https://appwrite.io/docs/quick-starts/vue', + svelte: 'https://appwrite.io/docs/quick-starts/sveltekit', + angular: 'https://appwrite.io/docs/quick-starts/angular', + astro: 'https://appwrite.io/docs/quick-starts/astro', + nuxt: 'https://appwrite.io/docs/quick-starts/nuxt', + qwik: 'https://appwrite.io/docs/quick-starts/qwik', + solid: 'https://appwrite.io/docs/quick-starts/solid', + tanstack: 'https://appwrite.io/docs/quick-starts/tanstack', + nodejs: 'https://appwrite.io/docs/quick-starts/nodejs', + vanilla: 'https://appwrite.io/docs/quick-starts/web', + + // Mobile Client SDKs + apple: 'https://appwrite.io/docs/quick-starts/apple', + android: 'https://appwrite.io/docs/quick-starts/android', + flutter: 'https://appwrite.io/docs/quick-starts/flutter', + 'react-native': 'https://appwrite.io/docs/quick-starts/react-native', + + // Server SDKs + python: 'https://appwrite.io/docs/quick-starts/python', + php: 'https://appwrite.io/docs/quick-starts/php', + go: 'https://appwrite.io/docs/quick-starts/go', + ruby: 'https://appwrite.io/docs/quick-starts/ruby', + dotnet: 'https://appwrite.io/docs/quick-starts/dotnet', + dart: 'https://appwrite.io/docs/quick-starts/dart', + kotlin: 'https://appwrite.io/docs/quick-starts/kotlin', + swift: 'https://appwrite.io/docs/quick-starts/swift' +}; + +/** + * Framework display names + */ +export const frameworkNames = { + react: 'React', + nextjs: 'Next.js', + vue: 'Vue', + svelte: 'SvelteKit', + angular: 'Angular', + astro: 'Astro', + nuxt: 'Nuxt', + qwik: 'Qwik', + solid: 'Solid', + tanstack: 'TanStack', + nodejs: 'Node.js', + vanilla: 'Web', + apple: 'Apple', + android: 'Android', + flutter: 'Flutter', + 'react-native': 'React Native', + python: 'Python', + php: 'PHP', + go: 'Go', + ruby: 'Ruby', + dotnet: '.NET', + dart: 'Dart', + kotlin: 'Kotlin', + swift: 'Swift' +}; + diff --git a/src/lib/languages/dart/index.js b/src/lib/languages/dart/index.js new file mode 100644 index 0000000..8a9856b --- /dev/null +++ b/src/lib/languages/dart/index.js @@ -0,0 +1,50 @@ +import { getSDKVersion } from '../../utils/versions.js'; +import { dartInstall } from '../common/install.js'; +import { getMobileImplementationGuide } from '../common/implementation-patterns.js'; + +/** + * Gets the Flutter SDK installation template with the latest version from Appwrite's API + * This is the main export used by the rules generator for Flutter + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const flutter = async (features = []) => { + const version = await getSDKVersion('client-flutter'); + const installation = dartInstall(version, false); + const flutterImplementation = getMobileImplementationGuide('flutter', features); + + return `${installation} + +**Framework Documentation:** +- [Account API](https://appwrite.io/docs/references/cloud/client-web/account) - Authentication and user management +- [Databases API](https://appwrite.io/docs/references/cloud/client-web/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/client-web/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/client-web/functions) - Serverless functions execution +- [Messaging API](https://appwrite.io/docs/references/cloud/client-web/messaging) - Push notifications and messaging +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/flutter) + +${flutterImplementation} + +## Flutter-Specific Best Practices + +- Initialize AppwriteService in main() before runApp() +- Use flutter_dotenv for environment variables +- Use Riverpod, Provider, or Bloc for state management +- Handle loading/error states in UI with AsyncValue +- Dispose realtime subscriptions in dispose() +- Use flutter_secure_storage for session tokens +- Implement offline-first with local caching (Hive, Isar) +`; +}; + +/** + * Export vanilla as an alias to flutter for backwards compatibility + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const vanilla = flutter; + +/** + * Export server from server.js + */ +export { server } from './server.js'; diff --git a/src/lib/languages/dart/server.js b/src/lib/languages/dart/server.js new file mode 100644 index 0000000..650d1c8 --- /dev/null +++ b/src/lib/languages/dart/server.js @@ -0,0 +1,39 @@ +import { getSDKVersion } from '../../utils/versions.js'; +import { dartInstall } from '../common/install.js'; +import { serverSecurity } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +/** + * Gets the Dart Server SDK installation template with the latest version from Appwrite's API + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const server = async (features = []) => { + const version = await getSDKVersion('server-dart'); + const installation = dartInstall(version, true); + const dartImplementation = getServerImplementationGuide('dart', features); + + return `${installation} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/dart) + +${serverSecurity} + +${dartImplementation} + +## Dart Server Best Practices + +- Use shelf or dart_frog for HTTP servers +- Use environment variables for configuration +- Implement proper error handling with try/catch +- Use null safety features (Dart 2.12+) +- Create factory constructors for models +- Use streams for realtime data +`; +}; diff --git a/src/lib/languages/dotnet/index.js b/src/lib/languages/dotnet/index.js new file mode 100644 index 0000000..fac460d --- /dev/null +++ b/src/lib/languages/dotnet/index.js @@ -0,0 +1,3 @@ +export { server } from './server.js'; +export { vanilla } from './vanilla.js'; + diff --git a/src/lib/languages/dotnet/server.js b/src/lib/languages/dotnet/server.js new file mode 100644 index 0000000..0060448 --- /dev/null +++ b/src/lib/languages/dotnet/server.js @@ -0,0 +1,32 @@ +import { dotnetInstall } from '../common/install.js'; +import { serverSecurityWithConfig } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +export async function server(features = []) { + const dotnetImplementation = getServerImplementationGuide('dotnet', features); + + return `${dotnetInstall} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/dotnet) + +${serverSecurityWithConfig('configuration files (appsettings.json) or environment variables')} + +${dotnetImplementation} + +## .NET-Specific Best Practices + +- Use dependency injection (built-in or Autofac) +- Store secrets in User Secrets (dev) or Azure Key Vault (prod) +- Use async/await for all I/O operations +- Use IOptions pattern for configuration +- Implement IDisposable for cleanup +- Use records for immutable data models +- Follow C# naming conventions (PascalCase for public members) +`; +} diff --git a/src/lib/languages/dotnet/vanilla.js b/src/lib/languages/dotnet/vanilla.js new file mode 100644 index 0000000..ae558d0 --- /dev/null +++ b/src/lib/languages/dotnet/vanilla.js @@ -0,0 +1,22 @@ +import { dotnetInstall } from '../common/install.js'; +import { clientSecurity } from '../common/security.js'; + +export const vanilla = `${dotnetInstall} + +**Framework Documentation:** +- [Account API](https://appwrite.io/docs/references/cloud/client-web/account) - Authentication and user management +- [Databases API](https://appwrite.io/docs/references/cloud/client-web/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/client-web/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/client-web/functions) - Serverless functions execution +- [Messaging API](https://appwrite.io/docs/references/cloud/client-web/messaging) - Push notifications and messaging +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/dotnet) + +${clientSecurity} + +## .NET Client Best Practices + +- Use the SDK in console apps, desktop apps (WPF, WinForms, MAUI) +- Store configuration in appsettings.json or user secrets +- Use async/await for all SDK operations +- Implement proper error handling with try/catch +`; diff --git a/src/lib/languages/go/index.js b/src/lib/languages/go/index.js new file mode 100644 index 0000000..5a95a78 --- /dev/null +++ b/src/lib/languages/go/index.js @@ -0,0 +1,2 @@ +export { server } from './server.js'; + diff --git a/src/lib/languages/go/server.js b/src/lib/languages/go/server.js new file mode 100644 index 0000000..aeab15e --- /dev/null +++ b/src/lib/languages/go/server.js @@ -0,0 +1,32 @@ +import { goInstall } from '../common/install.js'; +import { serverSecurity } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +export async function server(features = []) { + const goImplementation = getServerImplementationGuide('go', features); + + return `${goInstall} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/go) + +${serverSecurity} + +${goImplementation} + +## Go-Specific Best Practices + +- Use Go modules for dependency management +- Use godotenv or similar for environment variables +- Create interfaces for testability +- Use context.Context for request cancellation +- Handle errors explicitly (don't ignore returned errors) +- Use goroutines carefully with proper synchronization +- Implement graceful shutdown for servers +`; +} diff --git a/src/lib/languages/index.js b/src/lib/languages/index.js new file mode 100644 index 0000000..060ee08 --- /dev/null +++ b/src/lib/languages/index.js @@ -0,0 +1,13 @@ +export * as js from './js/index.js'; +export * as python from './python/index.js'; +export * as php from './php/index.js'; +export * as go from './go/index.js'; +export * as dart from './dart/index.js'; +export * as swift from './swift/index.js'; +export * as kotlin from './kotlin/index.js'; +export * as reactNative from './react-native/index.js'; +export * as ruby from './ruby/index.js'; +export * as dotnet from './dotnet/index.js'; +export * as apple from './apple/index.js'; +export * as android from './android/index.js'; + diff --git a/src/lib/languages/js/angular.js b/src/lib/languages/js/angular.js new file mode 100644 index 0000000..ba24fad --- /dev/null +++ b/src/lib/languages/js/angular.js @@ -0,0 +1,9 @@ +import { jsInstallDefault as jsInstall } from '../common/install.js'; +import { createFrameworkTemplate } from '../common/utils.js'; +import { frameworkNotes } from '../common/security.js'; + +export const angular = createFrameworkTemplate({ + installation: jsInstall, + securityNotes: frameworkNotes.angular +}); + diff --git a/src/lib/languages/js/astro.js b/src/lib/languages/js/astro.js new file mode 100644 index 0000000..888985d --- /dev/null +++ b/src/lib/languages/js/astro.js @@ -0,0 +1,90 @@ +import { nodeAppwriteInstall } from '../common/install.js'; +import { ssrAuthPattern } from '../common/security.js'; +import { getFullImplementationGuide } from '../common/implementation-patterns.js'; + +export async function astro(features = []) { + const astroImplementation = getFullImplementationGuide('astro', 'javascript', features); + const authSection = features.includes('auth') ? `\n${ssrAuthPattern}\n` : ''; + + return `${nodeAppwriteInstall} + +**Framework Documentation:** +- [Astro Server Endpoints](https://docs.astro.build/en/guides/endpoints/) +- [Astro Middleware](https://docs.astro.build/en/guides/middleware/) +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/astro) + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +${authSection} +${astroImplementation} + +## Astro-Specific Best Practices + +### File Organization +\`\`\` +src/ +├── lib/ +│ ├── db.ts # Database wrapper (server-only) +│ ├── storage.ts # Storage wrapper (server-only) +│ └── auth.ts # Auth helpers +├── pages/ +│ ├── api/ +│ │ ├── items/ +│ │ │ ├── index.ts # GET/POST /api/items +│ │ │ └── [id].ts # GET/PUT/DELETE /api/items/:id +│ │ └── auth/ +│ │ └── session.ts +│ └── items.astro # Server-rendered page +├── components/ +│ └── ItemsList.tsx # Interactive component +└── middleware.ts # Auth middleware +\`\`\` + +### Middleware Pattern +\`\`\`typescript +// src/middleware.ts +import { defineMiddleware } from 'astro:middleware' +import { getSession } from '@/lib/auth' + +export const onRequest = defineMiddleware(async (context, next) => { + const session = await getSession(context.request) + context.locals.user = session?.user ?? null + return next() +}) +\`\`\` + +### Hybrid Rendering +\`\`\`astro +--- +// Force server rendering for this page +export const prerender = false + +import { db } from '@/lib/db' +const user = Astro.locals.user +if (!user) return Astro.redirect('/login') + +const items = await db.items.listByOwner(user.id) +--- + + + +

Your Items

+ + + +
+\`\`\` + +### Client Directive Guidelines +- \`client:load\` - Hydrate immediately (for critical interactivity) +- \`client:idle\` - Hydrate when browser is idle +- \`client:visible\` - Hydrate when component enters viewport +- Never use Appwrite SDK in client components +`; +} + diff --git a/src/lib/languages/js/index.js b/src/lib/languages/js/index.js new file mode 100644 index 0000000..ce81dcd --- /dev/null +++ b/src/lib/languages/js/index.js @@ -0,0 +1,13 @@ +export { react } from './react.js'; +export { nextjs } from './nextjs.js'; +export { svelte } from './svelte.js'; +export { nodejs } from './nodejs.js'; +export { vanilla } from './vanilla.js'; +export { vue } from './vue.js'; +export { angular } from './angular.js'; +export { astro } from './astro.js'; +export { nuxt } from './nuxt.js'; +export { qwik } from './qwik.js'; +export { solid } from './solid.js'; +export { tanstack } from './tanstack.js'; + diff --git a/src/lib/languages/js/nextjs.js b/src/lib/languages/js/nextjs.js new file mode 100644 index 0000000..d0960f1 --- /dev/null +++ b/src/lib/languages/js/nextjs.js @@ -0,0 +1,71 @@ +import { nodeAppwriteInstall } from '../common/install.js'; +import { ssrAuthPattern } from '../common/security.js'; +import { getFullImplementationGuide } from '../common/implementation-patterns.js'; + +export async function nextjs(features = []) { + const nextjsImplementation = getFullImplementationGuide('nextjs', 'javascript', features); + const authSection = features.includes('auth') ? `\n${ssrAuthPattern}\n` : ''; + + return `${nodeAppwriteInstall} + +**Framework Documentation:** +- [Next.js App Router Docs](https://nextjs.org/docs/app) +- [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/nextjs) + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +${authSection} +${nextjsImplementation} + +## Next.js-Specific Best Practices + +### Rendering Strategy +- Default to Server Components for all data fetching +- Use \`'use server'\` for all mutation functions +- Only use Client Components when explicitly needed for interactivity +- Never import Appwrite SDK in Client Components + +### Data Fetching Pattern +\`\`\`typescript +// In Server Component - direct async/await +async function ItemsPage() { + const items = await db.items.listByOwner(userId) + return +} +\`\`\` + +### Revalidation +\`\`\`typescript +// After mutations, revalidate the path +import { revalidatePath } from 'next/cache' + +export async function createItem(data) { + 'use server' + const item = await db.items.create(data) + revalidatePath('/items') + return { item } +} +\`\`\` + +### File Organization +\`\`\` +app/ +├── actions/ # Server Actions +│ └── items.ts +├── lib/ +│ ├── db.ts # Database wrapper (server-only) +│ ├── storage.ts # Storage wrapper (server-only) +│ └── auth.ts # Auth helpers +└── (routes)/ + └── items/ + └── page.tsx # Server Component +\`\`\` +`; +} + diff --git a/src/lib/languages/js/nodejs.js b/src/lib/languages/js/nodejs.js new file mode 100644 index 0000000..ef3e6bc --- /dev/null +++ b/src/lib/languages/js/nodejs.js @@ -0,0 +1,16 @@ +import { jsInstall } from '../common/install.js'; +import { serverSecurity } from '../common/security.js'; + +export const nodejs = `${jsInstall('node-appwrite', 'Install the Appwrite Node.js SDK')} + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications + +${serverSecurity} +- Never log or expose API keys in error messages`; + diff --git a/src/lib/languages/js/nuxt.js b/src/lib/languages/js/nuxt.js new file mode 100644 index 0000000..c323a2b --- /dev/null +++ b/src/lib/languages/js/nuxt.js @@ -0,0 +1,92 @@ +import { nodeAppwriteInstall } from '../common/install.js'; +import { ssrAuthPattern } from '../common/security.js'; +import { getFullImplementationGuide } from '../common/implementation-patterns.js'; + +export async function nuxt(features = []) { + const nuxtImplementation = getFullImplementationGuide('nuxt', 'javascript', features); + const authSection = features.includes('auth') ? `\n${ssrAuthPattern}\n` : ''; + + return `${nodeAppwriteInstall} + +**Framework Documentation:** +- [Nuxt Server Routes](https://nuxt.com/docs/guide/directory-structure/server) +- [Nuxt Middleware](https://nuxt.com/docs/guide/directory-structure/middleware) +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/nuxt) + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +${authSection} +${nuxtImplementation} + +## Nuxt-Specific Best Practices + +### Server Route Organization +\`\`\` +server/ +├── api/ +│ ├── items/ +│ │ ├── index.get.ts # GET /api/items +│ │ ├── index.post.ts # POST /api/items +│ │ └── [id].delete.ts # DELETE /api/items/:id +│ └── auth/ +│ └── session.get.ts +├── lib/ +│ ├── db.ts # Database wrapper +│ └── storage.ts # Storage wrapper +└── middleware/ + └── auth.ts # Auth middleware +\`\`\` + +### Auth Middleware Pattern +\`\`\`typescript +// server/middleware/auth.ts +export default defineEventHandler(async (event) => { + // Skip auth for public routes + if (event.path.startsWith('/api/public')) return + + const session = await getSession(event) + event.context.user = session?.user ?? null +}) +\`\`\` + +### Composables for Client +\`\`\`typescript +// composables/useItems.ts +export function useItems() { + const { data: items, refresh } = useFetch('/api/items') + + async function createItem(title: string) { + await $fetch('/api/items', { + method: 'POST', + body: { title } + }) + await refresh() + } + + return { items, createItem, refresh } +} +\`\`\` + +### Environment Configuration +\`\`\`typescript +// nuxt.config.ts +export default defineNuxtConfig({ + runtimeConfig: { + // Server-only (never exposed to client) + appwriteApiKey: process.env.APPWRITE_API_KEY, + // Can be overridden by NUXT_PUBLIC_* env vars + public: { + appwriteEndpoint: process.env.APPWRITE_ENDPOINT, + appwriteProjectId: process.env.APPWRITE_PROJECT_ID, + } + } +}) +\`\`\` +`; +} + diff --git a/src/lib/languages/js/qwik.js b/src/lib/languages/js/qwik.js new file mode 100644 index 0000000..4da9355 --- /dev/null +++ b/src/lib/languages/js/qwik.js @@ -0,0 +1,17 @@ +import { jsInstallDefault as jsInstall } from '../common/install.js'; +import { createFrameworkTemplate } from '../common/utils.js'; +import { clientSecurityWithEnv, authNote } from '../common/security.js'; + +export const qwik = createFrameworkTemplate({ + installation: jsInstall, + securityNotes: `${clientSecurityWithEnv('.env')} + +**Server-Side Security (Critical for Qwik):** +- API keys should NEVER be exposed to client-side code +- Separate client and server code: protect API keys on server, never expose them to client +- Use route loaders (\`routeLoader$\`) or server endpoints (\`server$\`) for secret operations +- For operations requiring API keys, use server actions or route loaders that run only on the server +- Client-side code should only use public endpoint and project ID`, + additionalNotes: authNote +}); + diff --git a/src/lib/languages/js/react.js b/src/lib/languages/js/react.js new file mode 100644 index 0000000..ea92424 --- /dev/null +++ b/src/lib/languages/js/react.js @@ -0,0 +1,9 @@ +import { jsInstallDefault as jsInstall } from '../common/install.js'; +import { createFrameworkTemplate } from '../common/utils.js'; +import { clientSecurity } from '../common/security.js'; + +export const react = createFrameworkTemplate({ + installation: jsInstall, + securityNotes: clientSecurity +}); + diff --git a/src/lib/languages/js/solid.js b/src/lib/languages/js/solid.js new file mode 100644 index 0000000..006df4e --- /dev/null +++ b/src/lib/languages/js/solid.js @@ -0,0 +1,10 @@ +import { jsInstallDefault as jsInstall } from '../common/install.js'; +import { createFrameworkTemplate } from '../common/utils.js'; +import { clientSecurityWithEnv, authNote } from '../common/security.js'; + +export const solid = createFrameworkTemplate({ + installation: jsInstall, + securityNotes: clientSecurityWithEnv('.env'), + additionalNotes: authNote +}); + diff --git a/src/lib/languages/js/svelte.js b/src/lib/languages/js/svelte.js new file mode 100644 index 0000000..f730a14 --- /dev/null +++ b/src/lib/languages/js/svelte.js @@ -0,0 +1,77 @@ +import { nodeAppwriteInstall } from '../common/install.js'; +import { ssrAuthPattern } from '../common/security.js'; +import { getFullImplementationGuide } from '../common/implementation-patterns.js'; + +export async function svelte(features = []) { + const sveltekitImplementation = getFullImplementationGuide('svelte', 'javascript', features); + const authSection = features.includes('auth') ? `\n${ssrAuthPattern}\n` : ''; + + return `${nodeAppwriteInstall} + +**Framework Documentation:** +- [SvelteKit Load Functions](https://kit.svelte.dev/docs/load) +- [SvelteKit Form Actions](https://kit.svelte.dev/docs/form-actions) +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/sveltekit) + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +${authSection} +${sveltekitImplementation} + +## SvelteKit-Specific Best Practices + +### File Organization +\`\`\` +src/ +├── lib/ +│ └── server/ +│ ├── db.ts # Database wrapper (server-only) +│ ├── storage.ts # Storage wrapper (server-only) +│ └── auth.ts # Auth helpers +├── routes/ +│ ├── items/ +│ │ ├── +page.svelte # Client component +│ │ ├── +page.server.ts # Load + Actions +│ │ └── +server.ts # API endpoints +│ └── +layout.server.ts # Root auth check +└── hooks.server.ts # Auth middleware +\`\`\` + +### Auth Hook Pattern +\`\`\`typescript +// src/hooks.server.ts +import type { Handle } from '@sveltejs/kit' +import { getSession } from '$lib/server/auth' + +export const handle: Handle = async ({ event, resolve }) => { + const session = await getSession(event.cookies) + event.locals.user = session?.user ?? null + return resolve(event) +} +\`\`\` + +### Form Actions Pattern +\`\`\`svelte + + + +
+ + +
+\`\`\` + +### Progressive Enhancement +- Use \`use:enhance\` for form submissions to enable JS-enhanced UX +- Forms work without JavaScript enabled +- Server handles all validation and ownership checks +`; +} + diff --git a/src/lib/languages/js/tanstack.js b/src/lib/languages/js/tanstack.js new file mode 100644 index 0000000..a014edd --- /dev/null +++ b/src/lib/languages/js/tanstack.js @@ -0,0 +1,52 @@ +import { nodeAppwriteInstall } from '../common/install.js'; +import { ssrAuthPattern } from '../common/security.js'; +import { getFullImplementationGuide } from '../common/implementation-patterns.js'; + +export async function tanstack(features = []) { + const tanstackImplementation = getFullImplementationGuide('tanstack', 'javascript', features); + const authSection = features.includes('auth') ? `\n${ssrAuthPattern}\n` : ''; + + return `${nodeAppwriteInstall} + +**Framework Documentation:** +- [TanStack Start Docs](https://tanstack.com/start/latest) +- [TanStack Query Docs](https://tanstack.com/query/latest) +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/tanstack) + +**API References:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Account API](https://appwrite.io/docs/references/cloud/server-nodejs/account) - Session management and account operations +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations and queries +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File upload, download, and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +${authSection} +${tanstackImplementation} + +## TanStack-Specific Best Practices + +- Use \`createServerFn\` for ALL data operations - never call SDK from components +- Validate inputs in server functions before processing +- Use \`router.invalidate()\` after mutations to refresh cached data +- Leverage TanStack Query for client-side caching when needed +- Keep server functions in dedicated files (e.g., \`server/functions/\`) +- Export typed return types from server functions + +## Query Invalidation Pattern + +\`\`\`typescript +// After mutation, invalidate relevant queries +const router = useRouter() + +async function handleCreate(data) { + await createItemFn({ data }) + router.invalidate() // Invalidates all route data +} + +// Or use TanStack Query for fine-grained control +const queryClient = useQueryClient() +queryClient.invalidateQueries({ queryKey: ['items'] }) +\`\`\` +`; +} + diff --git a/src/lib/languages/js/vanilla.js b/src/lib/languages/js/vanilla.js new file mode 100644 index 0000000..9cf3bd1 --- /dev/null +++ b/src/lib/languages/js/vanilla.js @@ -0,0 +1,9 @@ +import { jsInstallDefault as jsInstall } from '../common/install.js'; +import { createFrameworkTemplate } from '../common/utils.js'; +import { clientSecurity } from '../common/security.js'; + +export const vanilla = createFrameworkTemplate({ + installation: jsInstall, + securityNotes: clientSecurity.replace('- Initialize services once and export as singletons', '').trim() +}); + diff --git a/src/lib/languages/js/vue.js b/src/lib/languages/js/vue.js new file mode 100644 index 0000000..c8d5d8d --- /dev/null +++ b/src/lib/languages/js/vue.js @@ -0,0 +1,9 @@ +import { jsInstallDefault as jsInstall } from '../common/install.js'; +import { createFrameworkTemplate } from '../common/utils.js'; +import { clientSecurityWithEnv } from '../common/security.js'; + +export const vue = createFrameworkTemplate({ + installation: jsInstall, + securityNotes: clientSecurityWithEnv('.env') +}); + diff --git a/src/lib/languages/kotlin/index.js b/src/lib/languages/kotlin/index.js new file mode 100644 index 0000000..ba835ce --- /dev/null +++ b/src/lib/languages/kotlin/index.js @@ -0,0 +1,72 @@ +import { getSDKVersion } from '../../utils/versions.js'; +import { serverSecurity } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +/** + * Generates the Kotlin SDK installation template with the latest version + * @param {string} version - The SDK version to use + * @returns {string} + */ +function generateInstallationTemplate(version) { + return `## SDK Installation + +Add the Appwrite Kotlin SDK to your \`build.gradle.kts\`: + +**Recommended: Specify exact version for stability** + +\`\`\`kotlin +dependencies { + implementation("io.appwrite:sdk-for-kotlin:${version}") +} +\`\`\` + +Or for Maven, add to \`pom.xml\`: + +**Recommended: Specify exact version** + +\`\`\`xml + + io.appwrite + sdk-for-kotlin + ${version} + +\`\`\` +`; +} + +/** + * Gets the Kotlin SDK installation template with the latest version from Appwrite's API + * This is the main export used by the rules generator + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const vanilla = async (features = []) => { + const version = await getSDKVersion('server-kotlin'); + const installation = generateInstallationTemplate(version); + const kotlinImplementation = getServerImplementationGuide('kotlin', features); + + return `${installation} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/kotlin) + +${serverSecurity} + +${kotlinImplementation} + +## Kotlin-Specific Best Practices + +- Use Ktor or Spring Boot for HTTP servers +- Use coroutines for async operations +- Use Kotlin Flow for reactive streams +- Use data classes for models +- Use object declarations for singletons +- Handle errors with Result or sealed classes +- Use Koin or Hilt for dependency injection +`; +}; diff --git a/src/lib/languages/php/index.js b/src/lib/languages/php/index.js new file mode 100644 index 0000000..5a95a78 --- /dev/null +++ b/src/lib/languages/php/index.js @@ -0,0 +1,2 @@ +export { server } from './server.js'; + diff --git a/src/lib/languages/php/server.js b/src/lib/languages/php/server.js new file mode 100644 index 0000000..ee1f120 --- /dev/null +++ b/src/lib/languages/php/server.js @@ -0,0 +1,32 @@ +import { phpInstall } from '../common/install.js'; +import { serverSecurityWithConfig } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +export async function server(features = []) { + const phpImplementation = getServerImplementationGuide('php', features); + + return `${phpInstall} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/php) + +${serverSecurityWithConfig('environment variables or .env files')} + +${phpImplementation} + +## PHP-Specific Best Practices + +- Use Composer for dependency management +- Use PHP 8.1+ for better type support and enums +- Use vlucas/phpdotenv for environment variables +- Implement PSR-4 autoloading +- Use dependency injection containers (Laravel, Symfony) +- Handle exceptions with try/catch blocks +- Use strict typing with \`declare(strict_types=1)\` +`; +} diff --git a/src/lib/languages/python/flask.js b/src/lib/languages/python/flask.js new file mode 100644 index 0000000..9c93b59 --- /dev/null +++ b/src/lib/languages/python/flask.js @@ -0,0 +1,71 @@ +import { pythonInstall } from '../common/install.js'; +import { serverSecurity } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +export async function flask(features = []) { + const pythonImplementation = getServerImplementationGuide('python', features); + + return `${pythonInstall} + +**Framework Documentation:** +- [Flask Documentation](https://flask.palletsprojects.com/) +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/python) + +${serverSecurity} + +${pythonImplementation} + +## Flask-Specific Best Practices + +### Application Factory Pattern + +\`\`\`python +# app/__init__.py +from flask import Flask +from app.services.appwrite import init_appwrite + +def create_app(): + app = Flask(__name__) + app.config.from_object('config.Config') + + # Initialize Appwrite client + init_appwrite(app) + + # Register blueprints + from app.routes import items + app.register_blueprint(items.bp) + + return app +\`\`\` + +### Request Context + +\`\`\`python +# Use Flask's g object for request-scoped data +from flask import g + +@app.before_request +def load_user(): + session_token = request.cookies.get('session') + if session_token: + g.user = verify_session(session_token) + else: + g.user = None +\`\`\` + +### Error Handling + +\`\`\`python +from appwrite.exception import AppwriteException + +@app.errorhandler(AppwriteException) +def handle_appwrite_error(error): + return jsonify({'error': error.message}), error.code +\`\`\` +`; +} diff --git a/src/lib/languages/python/index.js b/src/lib/languages/python/index.js new file mode 100644 index 0000000..3ab6ffe --- /dev/null +++ b/src/lib/languages/python/index.js @@ -0,0 +1,3 @@ +export { flask } from './flask.js'; +export { server } from './server.js'; + diff --git a/src/lib/languages/python/server.js b/src/lib/languages/python/server.js new file mode 100644 index 0000000..678f3f0 --- /dev/null +++ b/src/lib/languages/python/server.js @@ -0,0 +1,31 @@ +import { pythonInstall } from '../common/install.js'; +import { serverSecurity } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +export async function server(features = []) { + const pythonImplementation = getServerImplementationGuide('python', features); + + return `${pythonInstall} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/python) + +${serverSecurity} + +${pythonImplementation} + +## Python-Specific Best Practices + +- Use virtual environments (venv, poetry, pipenv) +- Type hints for better IDE support and documentation +- Use async/await with asyncio for concurrent operations +- Store configuration in environment variables (.env with python-dotenv) +- Use dataclasses or Pydantic for models +- Implement proper exception handling with try/except +`; +} diff --git a/src/lib/languages/react-native/index.js b/src/lib/languages/react-native/index.js new file mode 100644 index 0000000..57da198 --- /dev/null +++ b/src/lib/languages/react-native/index.js @@ -0,0 +1,2 @@ +export { vanilla } from './vanilla.js'; + diff --git a/src/lib/languages/react-native/vanilla.js b/src/lib/languages/react-native/vanilla.js new file mode 100644 index 0000000..cf6fc74 --- /dev/null +++ b/src/lib/languages/react-native/vanilla.js @@ -0,0 +1,34 @@ +import { jsInstall } from '../common/install.js'; +import { getMobileImplementationGuide } from '../common/implementation-patterns.js'; + +export async function vanilla(features = []) { + const reactNativeImplementation = getMobileImplementationGuide('react-native', features); + + return `${jsInstall('react-native-appwrite', 'Install the Appwrite React Native SDK')} + +**Framework Documentation:** +- [Account API](https://appwrite.io/docs/references/cloud/client-web/account) - Authentication and user management +- [Databases API](https://appwrite.io/docs/references/cloud/client-web/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/client-web/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/client-web/functions) - Serverless functions execution +- [Messaging API](https://appwrite.io/docs/references/cloud/client-web/messaging) - Push notifications and messaging +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/react-native) + +**Best Practices:** +- Store endpoint and project ID in react-native-config +- Never commit API keys to version control +- Initialize services once and export as singletons + +${reactNativeImplementation} + +## React Native-Specific Best Practices + +- Use react-native-config for environment variables +- Store session in expo-secure-store or react-native-keychain +- Use React Navigation for routing +- Handle app state changes (AppState API) +- Implement proper error boundaries +- Use custom hooks for data fetching logic +- Test on both iOS and Android +`; +} diff --git a/src/lib/languages/ruby/index.js b/src/lib/languages/ruby/index.js new file mode 100644 index 0000000..5a95a78 --- /dev/null +++ b/src/lib/languages/ruby/index.js @@ -0,0 +1,2 @@ +export { server } from './server.js'; + diff --git a/src/lib/languages/ruby/server.js b/src/lib/languages/ruby/server.js new file mode 100644 index 0000000..2dc6293 --- /dev/null +++ b/src/lib/languages/ruby/server.js @@ -0,0 +1,32 @@ +import { rubyInstall } from '../common/install.js'; +import { serverSecurityWithConfig } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +export async function server(features = []) { + const rubyImplementation = getServerImplementationGuide('ruby', features); + + return `${rubyInstall} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/ruby) + +${serverSecurityWithConfig('environment variables or Rails credentials')} + +${rubyImplementation} + +## Ruby-Specific Best Practices + +- Use Bundler for dependency management +- Use dotenv-rails or Rails credentials for secrets +- Follow Ruby naming conventions (snake_case) +- Use modules for namespacing services +- Implement proper exception handling with begin/rescue +- Use symbols for hash keys +- Consider using Sorbet or RBS for type checking +`; +} diff --git a/src/lib/languages/swift/index.js b/src/lib/languages/swift/index.js new file mode 100644 index 0000000..7093c15 --- /dev/null +++ b/src/lib/languages/swift/index.js @@ -0,0 +1,62 @@ +import { getSDKVersion } from '../../utils/versions.js'; +import { serverSecurity } from '../common/security.js'; +import { getServerImplementationGuide } from '../common/implementation-patterns.js'; + +/** + * Generates the Swift SDK installation template with the latest version + * @param {string} version - The SDK version to use + * @returns {string} + */ +function generateInstallationTemplate(version) { + return `## SDK Installation + +Add the Appwrite Swift Server SDK to your \`Package.swift\`: + +\`\`\`swift +dependencies: [ + .package(url: "https://github.com/appwrite/sdk-for-swift", from: "${version}") +] +\`\`\` + +Or add it via Xcode: +1. File → Add Packages... +2. Enter: \`https://github.com/appwrite/sdk-for-swift\` +3. Select version: \`${version}\` or later`; +} + +/** + * Gets the Swift SDK installation template with the latest version from Appwrite's API + * This is the main export used by the rules generator + * @param {string[]} [features=[]] - Selected features to include patterns for + * @returns {Promise} + */ +export const vanilla = async (features = []) => { + const version = await getSDKVersion('server-swift'); + const installation = generateInstallationTemplate(version); + const swiftImplementation = getServerImplementationGuide('swift', features); + + return `${installation} + +**Framework Documentation:** +- [Users API](https://appwrite.io/docs/references/cloud/server-nodejs/users) - User management and administration +- [Databases API](https://appwrite.io/docs/references/cloud/server-nodejs/databases) - Database operations +- [Storage API](https://appwrite.io/docs/references/cloud/server-nodejs/storage) - File storage and management +- [Functions API](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Serverless functions management +- [Messaging API](https://appwrite.io/docs/references/cloud/server-nodejs/messaging) - Email, SMS, and push notifications +- [Appwrite Quick Start](https://appwrite.io/docs/quick-starts/swift) + +${serverSecurity} + +${swiftImplementation} + +## Swift Server Best Practices + +- Use Vapor or Hummingbird for HTTP servers +- Use async/await for all SDK operations +- Use Codable for JSON serialization +- Use actors for thread-safe singletons +- Handle errors with do/catch +- Use Swift Package Manager for dependencies +- Store configuration in environment variables +`; +}; diff --git a/src/lib/rules-generator.js b/src/lib/rules-generator.js new file mode 100644 index 0000000..0e085e3 --- /dev/null +++ b/src/lib/rules-generator.js @@ -0,0 +1,683 @@ +// Rules generator for different Appwrite SDKs and frameworks +import * as codeExamples from './languages/index.js'; + +/** @typedef {Object} SDKConfig + * @property {string} name + * @property {string[]} frameworks + * @property {string} importSyntax + * @property {string} exportSyntax + * @property {string} asyncSyntax + */ + +/** @typedef {Object} GeneratorConfig + * @property {string} sdk + * @property {string} framework + * @property {string[]} features + */ + +/** @type {Record} */ +export const SDK_OPTIONS = { + javascript: { + name: 'JavaScript/TypeScript', + frameworks: ['nextjs', 'react', 'vue', 'svelte', 'angular', 'astro', 'nuxt', 'qwik', 'solid', 'tanstack', 'nodejs', 'vanilla'], + importSyntax: 'import', + exportSyntax: 'export', + asyncSyntax: 'async/await' + }, + 'react-native': { + name: 'React Native', + frameworks: ['react-native', 'vanilla'], + importSyntax: 'import', + exportSyntax: 'export', + asyncSyntax: 'async/await' + }, + python: { + name: 'Python', + frameworks: ['flask', 'django', 'fastapi', 'server'], + importSyntax: 'from', + exportSyntax: 'def', + asyncSyntax: 'async def' + }, + flutter: { + name: 'Flutter/Dart', + frameworks: ['flutter', 'server'], + importSyntax: 'import', + exportSyntax: 'class', + asyncSyntax: 'Future' + }, + apple: { + name: 'Apple', + frameworks: ['vanilla'], + importSyntax: 'import', + exportSyntax: 'func', + asyncSyntax: 'async' + }, + android: { + name: 'Android', + frameworks: ['vanilla'], + importSyntax: 'import', + exportSyntax: 'fun', + asyncSyntax: 'suspend' + }, + swift: { + name: 'Swift', + frameworks: ['server', 'vanilla'], + importSyntax: 'import', + exportSyntax: 'func', + asyncSyntax: 'async' + }, + kotlin: { + name: 'Kotlin', + frameworks: ['server', 'vanilla'], + importSyntax: 'import', + exportSyntax: 'fun', + asyncSyntax: 'suspend' + }, + php: { + name: 'PHP', + frameworks: ['laravel', 'symfony', 'server'], + importSyntax: 'use', + exportSyntax: 'function', + asyncSyntax: 'async' + }, + go: { + name: 'Go', + frameworks: ['gin', 'fiber', 'server'], + importSyntax: 'import', + exportSyntax: 'func', + asyncSyntax: 'goroutine' + }, + ruby: { + name: 'Ruby', + frameworks: ['rails', 'server'], + importSyntax: 'require', + exportSyntax: 'def', + asyncSyntax: 'async' + }, + dotnet: { + name: '.NET', + frameworks: ['aspnet', 'server', 'vanilla'], + importSyntax: 'using', + exportSyntax: 'public', + asyncSyntax: 'async Task' + } +}; + +/** + * @param {GeneratorConfig} config + * @returns {Promise} + */ +export async function generateRules(config) { + const { sdk, framework, features } = config; + const sdkInfo = SDK_OPTIONS[sdk]; + + const sdkInit = await generateSDKInitialization(sdk, framework, features); + + // Generate all sections in parallel + // Permissions section is mandatory for all products + const sections = await Promise.all([ + features.includes('auth') ? generateAuthSection() : Promise.resolve(''), + generatePermissionsSection(sdk), + features.includes('database') ? generateDatabaseSection() : Promise.resolve(''), + features.includes('storage') ? generateStorageSection() : Promise.resolve(''), + features.includes('functions') ? generateFunctionsSection(sdk) : Promise.resolve(''), + features.includes('messaging') ? generateMessagingSection() : Promise.resolve(''), + features.includes('sites') ? generateSitesSection() : Promise.resolve(''), + features.includes('realtime') ? generateRealtimeSection() : Promise.resolve('') + ]); + + let rules = `# Appwrite Development Rules + +> You are an expert developer focused on building apps with Appwrite's ${sdkInfo?.name || sdk} SDK. + +## Overview + +This file provides AI coding assistants with Appwrite-specific development instructions, best practices, and code patterns for the ${sdkInfo?.name || sdk} SDK${framework !== 'vanilla' ? ` with ${framework}` : ''}. + +${sdkInit} +${sections.join('\n\n')} +`; + + return rules; +} + + +/** + * @param {string} sdk + * @param {string} framework + * @param {string[]} features + * @returns {Promise} + */ +async function generateSDKInitialization(sdk, framework, features) { + /** @type {Record Promise)>>} */ + const templates = { + javascript: codeExamples.js, + 'react-native': codeExamples.reactNative, + python: codeExamples.python, + php: codeExamples.php, + go: codeExamples.go, + flutter: codeExamples.dart, + apple: codeExamples.apple, + android: codeExamples.android, + swift: codeExamples.swift, + kotlin: codeExamples.kotlin, + ruby: codeExamples.ruby, + dotnet: codeExamples.dotnet + }; + + const sdkTemplates = templates[sdk]; + if (sdkTemplates && sdkTemplates[framework]) { + const template = sdkTemplates[framework]; + // Check if it's an async function + if (typeof template === 'function') { + return await template(features); + } + return template; + } + + // Fallback to vanilla if available + if (sdkTemplates && sdkTemplates.vanilla) { + const template = sdkTemplates.vanilla; + // Check if it's an async function + if (typeof template === 'function') { + return await template(features); + } + return template; + } + + // Final fallback + return `## SDK Initialization + +Configure your Appwrite client for ${SDK_OPTIONS[sdk]?.name || sdk}.`; +} + +/** + * @returns {Promise} + */ +async function generateAuthSection() { + const { authProductLinks } = await import('./languages/common/products.js'); + return `## Authentication & Teams + +${authProductLinks} + +### Best Practices for Authentication & Teams + +- **Session Security**: Always use HttpOnly cookies for session storage in SSR applications +- **API Keys**: Never expose API keys to client-side code - use environment variables +- **Session Validation**: Always validate sessions on the server before trusting them +- **Team-Based Architecture**: ALWAYS prefer team/member-based roles over user-specific roles for any application requiring shared access or multi-tenancy +- **Multi-Tenant Applications**: Use teams as the primary mechanism for tenant isolation and resource sharing +- **OAuth Redirects**: Handle OAuth redirects properly with success and failure URLs +- **Password Security**: Use strong password requirements and consider implementing MFA +- **Session Expiry**: Configure appropriate session expiry times based on your security requirements + +### Team & Member Management Fundamentals + +When building applications that involve multiple users or tenants: + +1. **Always Start with Teams**: For any feature requiring shared access, create a team first, then add members with roles +2. **Role-Based Access**: Assign roles (e.g., "owner", "admin", "member", "viewer") to team members rather than setting individual user permissions +3. **Team Isolation**: Use teams as the boundary for data isolation in multi-tenant applications +4. **Member Invitations**: Implement team invitation workflows for onboarding new members +5. **Role Management**: Build role management UIs that allow team owners/admins to manage member roles dynamically`; +} + +/** + * @param {string} sdk + * @returns {Promise} + */ +async function generatePermissionsSection(sdk) { + const { authProductLinks, permissionsProductLinks } = await import('./languages/common/products.js'); + const { getPermissionExamples } = await import('./languages/common/permissions-examples.js'); + const examples = getPermissionExamples(sdk); + + return `## Permissions & Multi-Tenancy + +This section is CRITICAL for building secure, scalable applications with Appwrite. Multi-tenancy is one of the most important architectural patterns in modern applications, and Appwrite's team-based permission system is designed specifically for this. + +${permissionsProductLinks} + +### Why Multi-Tenancy Matters + +Multi-tenancy allows a single application instance to serve multiple isolated groups of users (tenants) while maintaining complete data isolation and security. Almost every modern SaaS application requires multi-tenancy to scale efficiently. + +### The Critical Pattern: Team-Based Permissions + +**ALWAYS PREFER TEAM/MEMBER-BASED ROLES over user-specific roles.** This is a fundamental architectural decision: + +#### Avoid: User-Specific Permissions +\`\`\`${getLanguageFromSdk(sdk)} +${examples.avoidUserPermissions} +\`\`\` + +**Problems with user-specific permissions:** +- Hard to scale when users need to share resources +- Difficult to add/remove access without updating every row +- No way to represent organizational hierarchies +- Poor support for collaborative features + +#### Prefer: Team/Member-Based Roles +\`\`\`${getLanguageFromSdk(sdk)} +${examples.preferTeamPermissions} +\`\`\` + +**Benefits of team/member-based roles:** +- Automatic access for all team members based on their role +- Easy to add/remove members without touching rows +- Scales naturally as teams grow +- Supports organizational hierarchies and complex permissions + +### Query Isolation Pattern + +**CRITICAL**: Always filter queries by \`teamId\` to ensure tenant isolation: + +\`\`\`${getLanguageFromSdk(sdk)} +${examples.queryWithTeamId} +\`\`\` + +### Role Verification Pattern + +Before allowing sensitive operations, always verify the user's role: + +\`\`\`${getLanguageFromSdk(sdk)} +${examples.roleCheck} +\`\`\` + +### Multi-Tenancy Implementation Guide + +#### Step 1: Create Teams Structure + +Teams in Appwrite represent tenants. Each team should map to a business entity (company, organization, workspace). + +See: [Teams Documentation](https://appwrite.io/docs/products/auth/teams) + +#### Step 2: Define Custom Roles + +Common role hierarchy: +- **owner**: Full control, can manage team settings and members +- **admin**: Can manage resources and most settings +- **member**: Can create/edit resources with limited permissions +- **viewer**: Read-only access + +#### Step 3: Member Management + +For team invitations and membership management, see [Team Invites Guide](https://appwrite.io/docs/products/auth/team-invites) + +#### Step 4: Apply Permissions Consistently + +Use team roles for all resources: +- **Database rows**: Apply \`Role.team('', 'role')\` permissions +- **Storage files**: Same team-based permission pattern +- **Always include \`teamId\`** as a field in your rows for query filtering + +### Permission Best Practices + +1. **Always Store teamId**: Every row in a multi-tenant app should have a \`teamId\` field +2. **Default Deny**: Don't grant permissions unless explicitly needed +3. **Role Hierarchy**: Design roles to reflect natural hierarchies (owner > admin > member > viewer) +4. **Server-Side Validation**: Always validate team membership server-side +5. **Query Isolation**: Every multi-tenant query MUST include a \`teamId\` filter + +### Common Multi-Tenancy Patterns + +| Pattern | Example Apps | Structure | +|---------|--------------|-----------| +| Workspace-Based | Notion, Slack | Each workspace = 1 team, users can belong to multiple teams | +| Organization-Based | GitHub, GitLab | Each org = 1 team, resources scoped to org | +| Project-Based | Linear, Asana | Each project = 1 team, members invited per project | + +### Debugging Permission Issues + +1. **Check Team Membership**: Verify user is actually a member of the team +2. **Verify Roles**: Check \`membership.roles\` contains the required role +3. **Check Permission Strings**: Permission strings are case-sensitive +4. **Query Filters**: Ensure \`teamId\` filters are applied correctly +5. **Server vs Client**: Some operations require the Server SDK + +### Additional Resources + +${authProductLinks}`; +} + +/** + * Get language identifier for code blocks based on SDK + * @param {string} sdk + * @returns {string} + */ +function getLanguageFromSdk(sdk) { + const languageMap = { + javascript: 'javascript', + 'react-native': 'javascript', + python: 'python', + php: 'php', + go: 'go', + flutter: 'dart', + dart: 'dart', + apple: 'swift', + android: 'kotlin', + swift: 'swift', + kotlin: 'kotlin', + ruby: 'ruby', + dotnet: 'csharp' + }; + return languageMap[sdk] || 'javascript'; +} + +/** + * @returns {Promise} + */ +async function generateDatabaseSection() { + const { databaseProductLinks } = await import('./languages/common/products.js'); + return `## Database Operations + +${databaseProductLinks} + +### Database Setup Scripts + +**ALWAYS create a database setup script using the Server SDK and API key** to initialize your database schema. This script should be version-controlled and run during deployment or initial setup. + +**Why Use Setup Scripts:** +- **Infrastructure as Code**: Database schema becomes part of your codebase, not manual console clicks +- **Reproducibility**: Easy to recreate database structure across different environments (dev, staging, production) +- **Version Control**: Track schema changes over time with Git +- **Team Collaboration**: All developers can sync database structure automatically +- **CI/CD Integration**: Automate database setup in deployment pipelines +- **Documentation**: The script serves as living documentation of your database structure + +**What Your Setup Script Should Include:** + +1. **Table Creation**: All tables with proper naming and IDs +2. **Column Definitions**: All columns with correct data types (string, integer, boolean, datetime, email, url, etc.) +3. **Indexes**: Performance-critical indexes on frequently queried fields (especially \`teamId\`, foreign keys, search fields) +4. **Relationships**: All table relationships and foreign key constraints +5. **Default Permissions**: Table-level permissions using team roles +6. **Column Constraints**: Required fields, string lengths, number ranges, enum values, default values + +**Setup Script Requirements:** + +- **Use Server SDK**: Must use the Server SDK (node-appwrite, appwrite/appwrite for PHP, etc.), NOT the client SDK +- **Require API Key**: The script must use an API key with appropriate scopes (\`databases.write\`, \`tables.write\`, \`columns.write\`) +- **Idempotent**: Script should safely handle re-runs (check if tables exist before creating) +- **Environment Variables**: Store API key, endpoint, project ID, and database ID in environment variables +- **Error Handling**: Proper error handling with clear error messages +- **Logging**: Log progress and errors for debugging + +**Example Setup Script Structure:** + +\`\`\`${getLanguageFromSdk('javascript')} +// scripts/setup-database.js (Node.js example) +import { Client, TablesDB, Permission, Role } from 'node-appwrite'; + +const client = new Client() + .setEndpoint(process.env.APPWRITE_ENDPOINT) + .setProject(process.env.APPWRITE_PROJECT_ID) + .setKey(process.env.APPWRITE_API_KEY); + +const tablesDB = new TablesDB(client); +const databaseId = process.env.APPWRITE_DATABASE_ID; + +async function setupDatabase() { + try { + // Create Users table + await tablesDB.createTable(databaseId, 'users', 'Users', [ + Permission.read(Role.users()), + Permission.write(Role.users()) + ]); + + // Add columns to Users table + await tablesDB.createStringColumn(databaseId, 'users', 'name', 255, true); + await tablesDB.createEmailColumn(databaseId, 'users', 'email', true); + await tablesDB.createStringColumn(databaseId, 'users', 'teamId', 255, true); + + // Create index on teamId for query performance + await tablesDB.createIndex(databaseId, 'users', 'idx_team', 'key', ['teamId']); + + // Create Projects table with team-based permissions + await tablesDB.createTable(databaseId, 'projects', 'Projects', [ + Permission.read(Role.team('[TEAM_ID]')), + Permission.create(Role.team('[TEAM_ID]', 'member')), + Permission.update(Role.team('[TEAM_ID]', 'admin')), + Permission.delete(Role.team('[TEAM_ID]', 'owner')), + ]); + + // Add columns to Projects table + await tablesDB.createStringColumn(databaseId, 'projects', 'name', 255, true); + await tablesDB.createStringColumn(databaseId, 'projects', 'description', 5000, false); + await tablesDB.createStringColumn(databaseId, 'projects', 'teamId', 255, true); + await tablesDB.createStringColumn(databaseId, 'projects', 'ownerId', 255, true); + await tablesDB.createDatetimeColumn(databaseId, 'projects', 'createdAt', true); + + // Create indexes + await tablesDB.createIndex(databaseId, 'projects', 'idx_team', 'key', ['teamId']); + await tablesDB.createIndex(databaseId, 'projects', 'idx_owner', 'key', ['ownerId']); + + // Create relationship between projects and users + await tablesDB.createRelationshipColumn( + databaseId, + 'projects', + 'users', + 'oneToMany', + false, // twoWay + 'owner', // key in projects + 'projects', // key in users + 'cascade' // onDelete + ); + + console.log('Database setup completed successfully!'); + } catch (error) { + // Handle "already exists" errors gracefully for idempotency + if (error.code !== 409) { + console.error('Database setup failed:', error); + throw error; + } else { + console.log('Tables already exist, skipping creation'); + } + } +} + +setupDatabase(); +\`\`\` + +**Running the Setup Script:** + +\`\`\`bash +# Set environment variables +export APPWRITE_ENDPOINT="https://cloud.appwrite.io/v1" +export APPWRITE_PROJECT_ID="your-project-id" +export APPWRITE_API_KEY="your-api-key" +export APPWRITE_DATABASE_ID="your-database-id" + +# Run the setup script +node scripts/setup-database.js +\`\`\` + +**Best Practices for Setup Scripts:** + +1. **Separate File**: Keep setup scripts in a \`scripts/\` directory +2. **Documentation**: Add comments explaining each table's purpose and relationships +3. **Testing**: Test the script on a separate development database before production +4. **Migration Strategy**: For schema changes, create new migration scripts instead of modifying the original setup +5. **Backup First**: Always backup production data before running schema changes +6. **Team ID Fields**: Always include \`teamId\` fields in multi-tenant tables +7. **Timestamp Fields**: Include \`createdAt\` and \`updatedAt\` fields for auditing +8. **Foreign Keys**: Use relationship columns to enforce referential integrity + +### Best Practices for TablesDB + +- **SDK Usage**: Use the \`TablesDB\` service (formerly \`Databases\`) for all database operations +- **Permissions & Multi-Tenancy**: ALWAYS use team/member-based roles for permissions (see Permissions & Multi-Tenancy section above). Never use user-specific permissions in multi-tenant applications +- **Tenant Isolation**: Always include \`teamId\` fields in your rows and filter queries by \`teamId\` to ensure complete data isolation between tenants +- **Permission Patterns**: Apply team roles (owner, admin, member, viewer) consistently across all tables. Use Role.team() for all permission checks +- **Query Security**: Every multi-tenant query MUST include a \`teamId\` filter to prevent cross-tenant data access +- **Table Permissions**: Set table-level permissions using team roles, then override at row level when needed +- **Query Optimization**: Use indexes for frequently queried fields, especially on \`teamId\` and commonly filtered fields +- **Data Validation**: Validate data before creating or updating rows, including team membership validation +- **Transactions**: Use transactions for operations that must succeed or fail together, ensuring atomicity across tenant boundaries +- **Pagination**: Always implement pagination for large datasets to improve performance and reduce response sizes +- **Type Safety**: Use type-safe models when available in your SDK for better code quality and fewer runtime errors`; +} + +/** + * @returns {Promise} + */ +async function generateStorageSection() { + const { storageProductLinks } = await import('./languages/common/products.js'); + return `## Storage Operations + +${storageProductLinks} + +### Best Practices for Storage + +- **Permissions & Multi-Tenancy**: ALWAYS use team/member-based roles for storage permissions (see Permissions & Multi-Tenancy section above). Apply Role.team() permissions to buckets and files for proper tenant isolation +- **Bucket Organization**: Consider organizing files by team/tenant using folder structures or bucket naming conventions for easier management +- **Tenant Isolation**: When querying files, always filter by metadata (e.g., \`teamId\`) to ensure users only access files from their teams +- **File Size Limits**: Set appropriate file size limits to prevent abuse and manage costs +- **File Types**: Validate file types before upload to ensure security and prevent malicious uploads +- **Permission Patterns**: Use team roles (owner, admin, member, viewer) consistently for bucket and file permissions, matching your database permission model +- **Cleanup**: Implement cleanup strategies for unused or temporary files, especially when teams are deleted +- **Virus Scanning**: Consider implementing virus scanning for uploaded files to protect all tenants +- **Access Control**: Validate team membership before allowing file uploads/downloads, even if permissions are set correctly`; +} + +/** + * Maps SDK names to their corresponding template paths in the Appwrite templates repository + * @param {string} sdk + * @returns {string|null} Template path or null if no template available + */ +function getFunctionTemplatePath(sdk) { + /** @type {Record} */ + const templateMap = { + javascript: 'node/starter', + 'react-native': 'node/starter', + python: 'python/starter', + php: 'php/starter', + go: 'go/starter', + flutter: 'dart/starter', + swift: 'swift/starter', + kotlin: 'kotlin/starter', + ruby: 'ruby/starter', + dotnet: 'dotnet/starter' + }; + return templateMap[sdk] || null; +} + +/** + * Generates template links section for functions + * @param {string} sdk + * @returns {string} + */ +function generateFunctionTemplateLinks(sdk) { + const templatePath = getFunctionTemplatePath(sdk); + if (!templatePath) { + return ''; + } + + const templateUrl = `https://github.com/appwrite/templates/tree/main/${templatePath}`; + const templatesBaseUrl = 'https://github.com/appwrite/templates'; + + return `### Starter Templates + +For getting started with Appwrite Functions, use the official starter template for your runtime: + +- **${SDK_OPTIONS[sdk]?.name || sdk} Starter**: [View Template](${templateUrl}) + +For more templates and examples, see the [Appwrite Templates Repository](${templatesBaseUrl}).`; +} + +/** + * @param {string} sdk + * @returns {Promise} + */ +async function generateFunctionsSection(sdk) { + const { functionsProductLinks } = await import('./languages/common/products.js'); + const templateLinks = generateFunctionTemplateLinks(sdk); + + return `## Functions + +${functionsProductLinks} + +${templateLinks} + +### When to Use Starter Templates + +**ALWAYS use starter templates from the [Appwrite Templates Repository](https://github.com/appwrite/templates) when building functions for:** + +- **Scheduled Tasks**: Functions that run on a schedule (cron jobs, periodic cleanup, etc.) +- **Event-Driven Tasks**: Functions triggered by Appwrite events (database changes, storage uploads, user events, etc.) +- **Background Processing**: Long-running or resource-intensive operations +- **Integration Functions**: Functions that integrate with third-party services (APIs, webhooks, etc.) +- **Complex Functions**: Any function that requires specific runtime configuration or dependencies + +**Why use templates?** Starter templates provide the correct project structure, dependencies, and configuration needed for functions to build and execute successfully. They ensure proper handling of environment variables, logging, error handling, and Appwrite SDK initialization. + +### Best Practices for Functions + +- **Error Handling**: Implement comprehensive error handling in your functions +- **Timeouts**: Be aware of function execution timeouts and optimize accordingly +- **Environment Variables**: Use environment variables for configuration, not hardcoded values +- **Logging**: Implement proper logging for debugging and monitoring +- **Security**: Validate all inputs and never trust user-provided data +- **Resource Limits**: Be mindful of memory and CPU limits for function executions +- **Template Usage**: Start with official templates for scheduled and event-driven functions to ensure proper setup`; +} + +/** + * @returns {Promise} + */ +async function generateMessagingSection() { + const { messagingProductLinks } = await import('./languages/common/products.js'); + return `## Messaging + +${messagingProductLinks} + +### Best Practices for Messaging + +- **Provider Selection**: Choose the right messaging provider based on your needs (FCM, APNS, Mailgun, Twilio, etc.) +- **Message Content**: Keep push notifications concise and actionable +- **Scheduling**: Use scheduled messages for better user engagement timing +- **Personalization**: Personalize messages to increase engagement +- **Rate Limiting**: Be mindful of rate limits when sending bulk messages +- **Error Handling**: Implement retry logic for failed message deliveries`; +} + +/** + * @returns {Promise} + */ +async function generateSitesSection() { + const { sitesProductLinks } = await import('./languages/common/products.js'); + return `## Sites + +${sitesProductLinks} + +### Best Practices for Sites + +- **Environment Variables**: Use environment variables for configuration, not hardcoded values +- **Custom Domains**: Configure custom domains for production sites for better branding +- **Rendering Strategy**: Choose between static and SSR based on your content needs and SEO requirements +- **Deployment Strategy**: Use Git deployments for automatic builds on commits +- **Rollback Plan**: Keep previous deployments ready for instant rollbacks if needed`; +} + +/** + * @returns {Promise} + */ +async function generateRealtimeSection() { + const { realtimeProductLinks } = await import('./languages/common/products.js'); + return `## Realtime Subscriptions + +${realtimeProductLinks} + +### Best Practices for Realtime Subscriptions + +- **Connection Management**: Always unsubscribe from channels when components unmount or pages are closed to prevent memory leaks +- **Error Handling**: Implement reconnection logic for dropped connections and handle network errors gracefully +- **Event Filtering**: Filter events on the client side to only process relevant updates for better performance +- **Channel Selection**: Subscribe only to the specific channels you need to minimize bandwidth and improve performance +- **Payload Validation**: Always validate payload data before processing to ensure data integrity +- **Rate Limiting**: Be mindful of the number of subscriptions and events to avoid overwhelming the client +- **State Synchronization**: Use realtime updates to keep local state in sync with server state, but handle conflicts appropriately +- **Authentication**: Ensure proper authentication is in place before subscribing to protected channels +- **Testing**: Test realtime functionality with network interruptions and reconnection scenarios +- **Cleanup**: Store unsubscribe functions and call them in cleanup hooks (useEffect cleanup, componentWillUnmount, etc.)`; +} + diff --git a/src/lib/utils/versions.js b/src/lib/utils/versions.js new file mode 100644 index 0000000..9771863 --- /dev/null +++ b/src/lib/utils/versions.js @@ -0,0 +1,61 @@ +/** + * Fetches the latest SDK versions from Appwrite's versions API + * @returns {Promise>} + */ +export async function fetchSDKVersions() { + try { + const response = await fetch('https://cloud.appwrite.io/versions'); + if (!response.ok) { + throw new Error(`Failed to fetch versions: ${response.statusText}`); + } + return await response.json(); + } catch (error) { + console.error('Error fetching SDK versions:', error); + // Return fallback versions if API is unavailable + return { + 'server-kotlin': '13.0.0', + 'client-flutter': '20.3.2', + 'client-apple': '13.4.0', + 'client-android': '11.3.0', + 'client-react-native': '0.18.0', + 'server-nodejs': '26.0.0', + 'server-php': '20.0.0', + 'server-python': '15.0.0', + 'server-ruby': '19.4.0', + 'server-go': 'v0.15.0', + 'server-dotnet': '0.23.0', + 'server-dart': '20.0.1', + 'server-swift': '14.0.0', + 'client-web': '21.4.0' + }; + } +} + +/** + * Gets the version for a specific SDK + * @param {string} sdkKey - The SDK key (e.g., 'server-kotlin', 'client-flutter') + * @returns {Promise} + */ +export async function getSDKVersion(sdkKey) { + const versions = await fetchSDKVersions(); + return versions[sdkKey] || 'latest'; +} + +/** + * Version cache to avoid multiple API calls + * @type {Record | null} + */ +let versionCache = null; + +/** + * Gets cached or fetches SDK versions + * @returns {Promise>} + */ +export async function getCachedVersions() { + if (versionCache) { + return versionCache; + } + versionCache = await fetchSDKVersions(); + return versionCache; +} + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..5cdf9e2 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + + + + + + +{@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..64958e6 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,453 @@ + + + + Appwrite Rules Generator + + +
+ + +
+ {#if previewVisible && generatedRules} +
+
+

Generated Rules

+
+ + +
+
+
+
{generatedRules}
+
+
+ {:else} +
+
+

Select your options and click "Generate Rules" to see the output here.

+
+
+ {/if} +
+
+ + + + diff --git a/src/routes/api/rules/+server.js b/src/routes/api/rules/+server.js new file mode 100644 index 0000000..6652dee --- /dev/null +++ b/src/routes/api/rules/+server.js @@ -0,0 +1,82 @@ +import { generateRules, SDK_OPTIONS } from '$lib/rules-generator.js'; +import { json, text } from '@sveltejs/kit'; + +// All available features +const ALL_FEATURES = ['auth', 'database', 'storage', 'functions', 'messaging', 'sites', 'realtime']; + +/** + * Normalize features array - replace 'all' with all available features + * @param {string[]} features + * @returns {string[]} + */ +function normalizeFeatures(features) { + if (features.includes('all')) { + return ALL_FEATURES; + } + return features; +} + +/** + * @param {{ url: URL }} event + */ +export async function GET({ url }) { + try { + const sdk = url.searchParams.get('sdk') || 'javascript'; + const framework = url.searchParams.get('framework') || 'nextjs'; + const featuresParam = url.searchParams.get('features'); + const format = url.searchParams.get('format') || 'text'; // 'text' or 'json' + + // Validate SDK + if (!SDK_OPTIONS[sdk]) { + return json( + { error: `Invalid SDK: ${sdk}. Available SDKs: ${Object.keys(SDK_OPTIONS).join(', ')}` }, + { status: 400 } + ); + } + + // Validate framework + const sdkInfo = SDK_OPTIONS[sdk]; + if (!sdkInfo.frameworks.includes(framework)) { + return json( + { + error: `Invalid framework: ${framework} for SDK: ${sdk}. Available frameworks: ${sdkInfo.frameworks.join(', ')}` + }, + { status: 400 } + ); + } + + // Parse and normalize features + const features = normalizeFeatures( + featuresParam ? featuresParam.split(',').filter(Boolean) : ['auth'] + ); + + // Generate rules + const rules = await generateRules({ + sdk, + framework, + features + }); + + // Return based on format + if (format === 'json') { + return json({ + sdk, + framework, + features, + rules + }); + } + + return text(rules, { + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Content-Disposition': 'attachment; filename="AGENTS.md"' + } + }); + } catch (error) { + console.error('Error generating rules:', error); + const errorMessage = error instanceof Error ? error.message : 'Failed to generate rules'; + return json({ error: errorMessage }, { status: 500 }); + } +} + diff --git a/src/routes/api/sdks/+server.js b/src/routes/api/sdks/+server.js new file mode 100644 index 0000000..ce9dd19 --- /dev/null +++ b/src/routes/api/sdks/+server.js @@ -0,0 +1,33 @@ +import { SDK_OPTIONS } from '$lib/rules-generator.js'; +import { json } from '@sveltejs/kit'; + +export async function GET() { + try { + const sdks = Object.entries(SDK_OPTIONS).map(([key, value]) => ({ + id: key, + name: value.name, + frameworks: value.frameworks, + importSyntax: value.importSyntax, + exportSyntax: value.exportSyntax, + asyncSyntax: value.asyncSyntax + })); + + return json({ + sdks, + availableFeatures: [ + 'auth', + 'database', + 'storage', + 'functions', + 'messaging', + 'sites', + 'realtime', + 'all' + ] + }); + } catch (error) { + console.error('Error fetching SDKs:', error); + return json({ error: error.message || 'Failed to fetch SDKs' }, { status: 500 }); + } +} + diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..1295460 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});