feat: migrate from PocketBase to PostgreSQL with Drizzle ORM
- Add @project-e/db package with Drizzle schema and migrations - Replace PocketBase client with PostgreSQL-based database client - Migrate auth from custom to NextAuth.js - Add Docker Compose with PostgreSQL container - Update worker to use new database client - Remove PocketBase-specific files and migrations - Add drizzle config and initial migration
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { db, records } from '@project-e/db';
|
||||
|
||||
type RecordData = Record<string, any>;
|
||||
|
||||
function serialize(record: typeof records.$inferSelect): RecordData {
|
||||
return {
|
||||
...record.data,
|
||||
id: record.id,
|
||||
created: record.createdAt.toISOString(),
|
||||
updated: record.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function matchesFilter(record: RecordData, filter?: string): boolean {
|
||||
if (!filter) return true;
|
||||
return filter.split('&&').every((term) => {
|
||||
const match = term.trim().match(/^([a-zA-Z_][\w]*)\s*(=|<=|<)\s*(.+)$/);
|
||||
if (!match) return false;
|
||||
const [, field, operator, rawExpected] = match;
|
||||
const expected = rawExpected.trim().replace(/^"|"$/g, '');
|
||||
const actual = record[field];
|
||||
if (operator === '=') return String(actual) === expected;
|
||||
if (operator === '<=') return String(actual ?? '') <= expected;
|
||||
return String(actual ?? '') < expected;
|
||||
});
|
||||
}
|
||||
|
||||
export function createDatabaseClient() {
|
||||
return {
|
||||
collection(collection: string) {
|
||||
return {
|
||||
async getList(page = 1, perPage = 50, options: { filter?: string; sort?: string } = {}) {
|
||||
const rows = (await db.select().from(records).where(eq(records.collection, collection)))
|
||||
.map(serialize)
|
||||
.filter((record) => matchesFilter(record, options.filter));
|
||||
return {
|
||||
items: rows.slice((page - 1) * perPage, page * perPage),
|
||||
totalItems: rows.length,
|
||||
totalPages: Math.max(1, Math.ceil(rows.length / perPage)),
|
||||
page,
|
||||
perPage,
|
||||
};
|
||||
},
|
||||
async create(data: RecordData) {
|
||||
const [record] = await db.insert(records).values({ collection, data }).returning();
|
||||
return serialize(record);
|
||||
},
|
||||
async update(id: string, data: RecordData) {
|
||||
const [existing] = await db.select().from(records).where(and(eq(records.id, id), eq(records.collection, collection))).limit(1);
|
||||
if (!existing) throw new Error(`Record ${id} not found`);
|
||||
const [record] = await db.update(records)
|
||||
.set({ data: { ...existing.data, ...data }, updatedAt: new Date() })
|
||||
.where(and(eq(records.id, id), eq(records.collection, collection)))
|
||||
.returning();
|
||||
return serialize(record);
|
||||
},
|
||||
async delete(id: string) {
|
||||
await db.delete(records).where(and(eq(records.id, id), eq(records.collection, collection)));
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+5
-12
@@ -1,7 +1,4 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const POCKETBASE_URL = process.env.POCKETBASE_URL || 'http://localhost:8090';
|
||||
const ADMIN_TOKEN = process.env.POCKETBASE_ADMIN_TOKEN || '';
|
||||
import { createDatabaseClient } from './database.js';
|
||||
const POLL_INTERVAL_BASE = 5000; // 5 seconds base
|
||||
const POLL_INTERVAL_MAX = 60000; // 60 seconds max
|
||||
|
||||
@@ -20,12 +17,8 @@ interface QueueJob {
|
||||
let currentPollInterval = POLL_INTERVAL_BASE;
|
||||
let isProcessing = false;
|
||||
|
||||
function createAdminClient(): PocketBase {
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
if (ADMIN_TOKEN) {
|
||||
pb.authStore.save(ADMIN_TOKEN, null);
|
||||
}
|
||||
return pb;
|
||||
function createAdminClient() {
|
||||
return createDatabaseClient();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +36,7 @@ async function poll(): Promise<void> {
|
||||
const jobs = await pb.collection('queue_jobs').getList(1, 10, {
|
||||
filter: `status = "pending" && scheduled_at <= "${now}"`,
|
||||
sort: 'created',
|
||||
}) as { items: QueueJob[] };
|
||||
}) as unknown as { items: QueueJob[] };
|
||||
|
||||
if (jobs.items.length > 0) {
|
||||
console.log(`[Worker] Processing ${jobs.items.length} job(s)`);
|
||||
@@ -335,7 +328,7 @@ async function scheduleCleanup(): Promise<void> {
|
||||
|
||||
// Start the worker
|
||||
console.log('[Worker] Starting Project E worker...');
|
||||
console.log(`[Worker] PocketBase URL: ${POCKETBASE_URL}`);
|
||||
console.log('[Worker] PostgreSQL queue enabled');
|
||||
console.log(`[Worker] Poll interval: ${POLL_INTERVAL_BASE}ms (base), ${POLL_INTERVAL_MAX}ms (max)`);
|
||||
|
||||
// Initial cleanup schedule
|
||||
|
||||
+7
-4
@@ -9,11 +9,14 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pocketbase": "^0.25.0",
|
||||
"@project-e/shared": "*"
|
||||
"@project-e/db": "^0.1.0",
|
||||
"@project-e/shared": "*",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"postgres": "^3.4.9",
|
||||
"tsx": "^4.23.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^22.19.0"
|
||||
"@types/node": "^22.19.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user