mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
## 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>
10 KiB
10 KiB
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):
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):
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)
// 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
// 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
// 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
// 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
// PocketBase
await locals.pb.collection('links').delete(linkId);
// Drizzle
await locals.db
.delete(links)
.where(eq(links.id, linkId));
6. COUNT
// 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)
// 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
// 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
// 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
// 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:
expandparameter for relations
Drizzle:
- Explicit
leftJoin/innerJoin - More control over query structure
4. Timestamps
PocketBase:
- Auto-managed
createdandupdated
Drizzle:
- Manual:
createdAt: timestamp('created_at').defaultNow() - Must update
updatedAtmanually
🚀 Migration Checklist for Each Route
-
Import Drizzle Schema & Operators
import { users, links, clicks } from '$lib/db/schema'; import { eq, and, desc, count } from 'drizzle-orm'; -
Replace
locals.pbwithlocals.db// Before: locals.pb.collection('users') // After: locals.db.select().from(users) -
Convert Filter Syntax
// Before: filter: `user_id="${userId}" && is_active=true` // After: where(and(eq(links.userId, userId), eq(links.isActive, true))) -
Handle Empty Results
// Before: try/catch for not found // After: if (!result || result.length === 0) -
Update Naming Convention
// Before: short_code, user_id, is_active // After: shortCode, userId, isActive -
Add
.returning()for Insertsconst [newRecord] = await locals.db .insert(links) .values({...}) .returning(); // ← Important! -
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 trackingsrc/routes/(app)/my/links/+page.server.ts- Link management CRUDsrc/routes/(app)/my/tags/+page.server.ts- Tag managementsrc/routes/(app)/settings/+page.server.ts- User settingssrc/routes/p/[username]/+page.server.ts- Public profile
Medium Priority (Features)
src/routes/(app)/settings/team/+page.server.ts- Team managementsrc/routes/(app)/settings/workspaces/+page.server.ts- Workspace managementsrc/routes/api/vote/+server.ts- Feature votingsrc/routes/register/+page.server.ts- User registrationsrc/routes/login/+page.server.ts- User login
Low Priority (Admin/Testing)
src/routes/api/test-pb/+server.ts- Can be removedsrc/routes/api/verify/+server.ts- Email verification
🧪 Testing Strategy
1. Unit Tests
Create test files for each migrated route:
// 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
-- 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
- Migrate High Priority Routes - Start with link redirect and management
- Implement External Auth - Replace PocketBase auth with external provider
- Remove PocketBase Code - Delete
backend/,pb_hooks/,pocketbasebinary - Update Documentation - Update CLAUDE.md and README
- Deploy to Production - Push to Coolify on Hetzner VPS
📞 Support
If you encounter issues during migration:
- Check this guide for common patterns
- Review the test route:
/api/check-username - Consult Drizzle ORM docs: https://orm.drizzle.team/
- Check PostgreSQL logs:
npm run docker:logs
Last Updated: 2025-11-19 Migration Progress: ~5% Complete (1 of ~25 routes)