managarten/apps-archived/uload/docs/MIGRATION_GUIDE.md
Till-JS ee42b6cc76 feat: major update with network graphs, themes, todo extensions, and more
## New Features

### Network Graph Visualization (Contacts, Calendar, Todo)
- D3.js force simulation for physics-based layout
- Zoom & pan with mouse/touchpad
- Keyboard shortcuts: +/- zoom, 0 reset, Esc deselect, / search, F focus
- Filtering by tags, company/location/project, connection strength
- Shared components in @manacore/shared-ui

### Central Tags API (mana-core-auth)
- CRUD endpoints for tags
- Schema: tags table with userId, name, color, app
- Shared tag components in @manacore/shared-ui

### Custom Themes System
- Theme editor with live preview and color picker
- Community theme gallery
- Theme sharing (public, unlisted, private)
- Backend API in mana-core-auth

### Todo App Extensions
- Glass-pill design for task input and items
- Settings page with 20+ preferences
- Task edit modal with inline editing
- Statistics page with visualizations
- PWA support with offline capabilities
- Multiple kanban boards

### Contacts App Features
- Duplicate detection
- Photo upload
- Batch operations
- Enhanced favorites page with multiple view modes
- Alphabet view improvements
- Search modal

### Help System
- @manacore/shared-help-content
- @manacore/shared-help-ui
- @manacore/shared-help-types

### Other Features
- Themes page for all apps
- Referral system frontend
- CommandBar (global search)
- Skeleton loaders
- Settings page improvements

## Bug Fixes
- Network graph simulation initialization
- Database schema TEXT for user_id columns (Better Auth compatibility)
- Various styling fixes

## Documentation
- Daily report for 2025-12-10
- CI/CD deployment guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 02:37:46 +01:00

452 lines
10 KiB
Markdown

# PocketBase → PostgreSQL + Drizzle ORM Migration Guide
## ✅ Migration Status
- **Infrastructure:** ✅ Complete
- **Database Schema:** ✅ Complete (13 tables)
- **Test Route:** ✅ Complete (`/api/check-username`)
- **Remaining Routes:** 🔄 In Progress (~25 files)
---
## 📊 Database Schema
All 13 tables successfully migrated:
```
✅ users (21 columns) - User profiles with external auth support
✅ links (21 columns) - Short links with analytics
✅ clicks (15 columns) - Click analytics tracking
✅ accounts (8 columns) - Business/team accounts
✅ workspaces (7 columns) - Team workspaces
✅ tags (10 columns) - Link categorization
✅ link_tags (4 columns) - Link-to-tag junction table
✅ notifications (10 columns) - In-app notifications
✅ shared_access (8 columns) - Team member access
✅ pending_invitations (9 columns) - Email invitations
✅ feature_requests (8 columns) - Feature voting
✅ feature_votes (4 columns) - Vote tracking
✅ folders (5 columns) - Link organization
```
---
## 🔄 Migration Pattern: PocketBase → Drizzle
### Example: `/api/check-username`
#### **Before (PocketBase):**
```typescript
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, locals }) => {
const username = url.searchParams.get('username');
try {
// PocketBase query
const existingUser = await locals.pb
.collection('users')
.getFirstListItem(`username="${username}"`);
return json({ available: false });
} catch (err) {
// Not found = available
return json({ available: true });
}
};
```
#### **After (Drizzle ORM):**
```typescript
import { json } from '@sveltejs/kit';
import { users } from '$lib/db/schema';
import { eq } from 'drizzle-orm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, locals }) => {
const username = url.searchParams.get('username');
// Drizzle query
const [existingUser] = await locals.db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);
if (!existingUser) {
return json({ available: true });
}
return json({ available: false });
};
```
---
## 📚 Common Query Patterns
### 1. **Simple SELECT (Find One)**
```typescript
// PocketBase
const user = await locals.pb.collection('users').getFirstListItem(`email="${email}"`);
// Drizzle
const [user] = await locals.db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
```
### 2. **SELECT with Multiple Conditions**
```typescript
// PocketBase
const links = await locals.pb.collection('links').getList(1, 50, {
filter: `user_id="${userId}" && is_active=true`,
sort: '-created'
});
// Drizzle
import { and, desc } from 'drizzle-orm';
const linksData = await locals.db
.select()
.from(links)
.where(and(
eq(links.userId, userId),
eq(links.isActive, true)
))
.orderBy(desc(links.createdAt))
.limit(50);
```
### 3. **INSERT**
```typescript
// PocketBase
const newLink = await locals.pb.collection('links').create({
short_code: 'abc123',
original_url: 'https://example.com',
user_id: userId
});
// Drizzle
const [newLink] = await locals.db
.insert(links)
.values({
shortCode: 'abc123',
originalUrl: 'https://example.com',
userId: userId
})
.returning();
```
### 4. **UPDATE**
```typescript
// PocketBase
await locals.pb.collection('links').update(linkId, {
is_active: false
});
// Drizzle
await locals.db
.update(links)
.set({
isActive: false,
updatedAt: new Date()
})
.where(eq(links.id, linkId));
```
### 5. **DELETE**
```typescript
// PocketBase
await locals.pb.collection('links').delete(linkId);
// Drizzle
await locals.db
.delete(links)
.where(eq(links.id, linkId));
```
### 6. **COUNT**
```typescript
// PocketBase
const result = await locals.pb.collection('links').getList(1, 1, {
filter: `user_id="${userId}"`
});
const count = result.totalItems;
// Drizzle
import { count } from 'drizzle-orm';
const [{ count: linkCount }] = await locals.db
.select({ count: count() })
.from(links)
.where(eq(links.userId, userId));
```
### 7. **JOIN (Relations)**
```typescript
// PocketBase
const links = await locals.pb.collection('links').getList(1, 50, {
expand: 'user'
});
// Drizzle
const linksWithUsers = await locals.db
.select({
link: links,
user: users
})
.from(links)
.leftJoin(users, eq(links.userId, users.id))
.limit(50);
```
### 8. **Transactions**
```typescript
// PocketBase - No native transactions
// Drizzle
await locals.db.transaction(async (tx) => {
// Insert click
await tx.insert(clicks).values({
linkId: link.id,
ipHash: hashIp(ip)
});
// Increment click count
await tx
.update(links)
.set({ clickCount: sql`${links.clickCount} + 1` })
.where(eq(links.id, link.id));
});
```
### 9. **LIKE Search**
```typescript
// PocketBase
const links = await locals.pb.collection('links').getList(1, 50, {
filter: `title~"${searchTerm}"`
});
// Drizzle
import { ilike } from 'drizzle-orm';
const linksData = await locals.db
.select()
.from(links)
.where(ilike(links.title, `%${searchTerm}%`))
.limit(50);
```
### 10. **Aggregation**
```typescript
// PocketBase - Limited aggregation support
// Drizzle
import { count, sum, avg } from 'drizzle-orm';
const stats = await locals.db
.select({
totalClicks: count(clicks.id),
uniqueCountries: sql<number>`count(DISTINCT ${clicks.country})`,
avgClicksPerLink: avg(links.clickCount)
})
.from(clicks)
.leftJoin(links, eq(clicks.linkId, links.id))
.where(eq(links.userId, userId));
```
---
## 🔑 Key Differences
### **1. Error Handling**
**PocketBase:**
- Throws on not found (requires try/catch)
- Limited error types
**Drizzle:**
- Returns empty array `[]` on not found
- More granular error handling
### **2. Naming Conventions**
**PocketBase:**
- Snake case: `user_id`, `is_active`, `created`
**Drizzle:**
- Camel case: `userId`, `isActive`, `createdAt`
### **3. Relations**
**PocketBase:**
- `expand` parameter for relations
**Drizzle:**
- Explicit `leftJoin` / `innerJoin`
- More control over query structure
### **4. Timestamps**
**PocketBase:**
- Auto-managed `created` and `updated`
**Drizzle:**
- Manual: `createdAt: timestamp('created_at').defaultNow()`
- Must update `updatedAt` manually
---
## 🚀 Migration Checklist for Each Route
1. **Import Drizzle Schema & Operators**
```typescript
import { users, links, clicks } from '$lib/db/schema';
import { eq, and, desc, count } from 'drizzle-orm';
```
2. **Replace `locals.pb` with `locals.db`**
```typescript
// Before: locals.pb.collection('users')
// After: locals.db.select().from(users)
```
3. **Convert Filter Syntax**
```typescript
// Before: filter: `user_id="${userId}" && is_active=true`
// After: where(and(eq(links.userId, userId), eq(links.isActive, true)))
```
4. **Handle Empty Results**
```typescript
// Before: try/catch for not found
// After: if (!result || result.length === 0)
```
5. **Update Naming Convention**
```typescript
// Before: short_code, user_id, is_active
// After: shortCode, userId, isActive
```
6. **Add `.returning()` for Inserts**
```typescript
const [newRecord] = await locals.db
.insert(links)
.values({...})
.returning(); // ← Important!
```
7. **Test Thoroughly**
- Test with existing data
- Test with missing data
- Test edge cases
---
## 📝 Routes to Migrate
### **High Priority (Core Functionality)**
- [ ] `src/routes/w/[workspace]/[...code]/+page.server.ts` - Link redirect with click tracking
- [ ] `src/routes/(app)/my/links/+page.server.ts` - Link management CRUD
- [ ] `src/routes/(app)/my/tags/+page.server.ts` - Tag management
- [ ] `src/routes/(app)/settings/+page.server.ts` - User settings
- [ ] `src/routes/p/[username]/+page.server.ts` - Public profile
### **Medium Priority (Features)**
- [ ] `src/routes/(app)/settings/team/+page.server.ts` - Team management
- [ ] `src/routes/(app)/settings/workspaces/+page.server.ts` - Workspace management
- [ ] `src/routes/api/vote/+server.ts` - Feature voting
- [ ] `src/routes/register/+page.server.ts` - User registration
- [ ] `src/routes/login/+page.server.ts` - User login
### **Low Priority (Admin/Testing)**
- [ ] `src/routes/api/test-pb/+server.ts` - Can be removed
- [ ] `src/routes/api/verify/+server.ts` - Email verification
---
## 🧪 Testing Strategy
### **1. Unit Tests**
Create test files for each migrated route:
```typescript
// src/routes/api/check-username/+server.test.ts
import { describe, it, expect } from 'vitest';
import { GET } from './+server';
describe('/api/check-username', () => {
it('returns available for new usernames', async () => {
// Test implementation
});
});
```
### **2. Database Seeding**
```sql
-- Insert test data
INSERT INTO users (email, username, name) VALUES
('test1@example.com', 'testuser1', 'Test User 1'),
('test2@example.com', 'testuser2', 'Test User 2');
INSERT INTO links (short_code, original_url, user_id) VALUES
('test123', 'https://example.com', (SELECT id FROM users WHERE username='testuser1'));
```
### **3. Manual Testing Checklist**
- [ ] Create new link
- [ ] Update existing link
- [ ] Delete link
- [ ] Click tracking
- [ ] Tag management
- [ ] User profile updates
- [ ] Team invitations
---
## 🎯 Next Steps
1. **Migrate High Priority Routes** - Start with link redirect and management
2. **Implement External Auth** - Replace PocketBase auth with external provider
3. **Remove PocketBase Code** - Delete `backend/`, `pb_hooks/`, `pocketbase` binary
4. **Update Documentation** - Update CLAUDE.md and README
5. **Deploy to Production** - Push to Coolify on Hetzner VPS
---
## 📞 Support
If you encounter issues during migration:
1. Check this guide for common patterns
2. Review the test route: `/api/check-username`
3. Consult Drizzle ORM docs: https://orm.drizzle.team/
4. Check PostgreSQL logs: `npm run docker:logs`
---
**Last Updated:** 2025-11-19
**Migration Progress:** ~5% Complete (1 of ~25 routes)