feat: complete v0.2 source and sync management
This commit is contained in:
@@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS sources (
|
||||
id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL, base_url TEXT NOT NULL, token_encrypted TEXT NOT NULL, remote_user TEXT,
|
||||
webhook_supported INTEGER NOT NULL DEFAULT 0, sync_status TEXT NOT NULL DEFAULT 'pending', last_synced_at TEXT, last_error TEXT,
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1, disabled_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, base_url)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
@@ -44,7 +45,8 @@ CREATE TABLE IF NOT EXISTS reports (
|
||||
CREATE TABLE IF NOT EXISTS sync_jobs (
|
||||
id INTEGER PRIMARY KEY, source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL, payload_json TEXT, status TEXT NOT NULL DEFAULT 'queued', attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, run_after TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
trigger TEXT NOT NULL DEFAULT 'manual', last_error TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
started_at TEXT, finished_at TEXT, run_after TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS source_members (
|
||||
source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
||||
@@ -55,18 +57,27 @@ CREATE TABLE IF NOT EXISTS source_members (
|
||||
CREATE INDEX IF NOT EXISTS posts_public_idx ON posts(visibility, hidden, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS sync_jobs_idx ON sync_jobs(status, run_after);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS source_remote_identity_unique ON sources(base_url, remote_user) WHERE remote_user IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec("INSERT OR IGNORE INTO source_members(source_id,user_id,role) SELECT id,user_id,'owner' FROM sources");
|
||||
|
||||
const sourceColumns = db.prepare("PRAGMA table_info(sources)").all() as { name: string }[];
|
||||
if (!sourceColumns.some((column) => column.name === "webhook_secret_hash")) {
|
||||
db.exec("ALTER TABLE sources ADD COLUMN webhook_secret_hash TEXT");
|
||||
}
|
||||
if (!sourceColumns.some((column) => column.name === "last_webhook_at")) {
|
||||
db.exec("ALTER TABLE sources ADD COLUMN last_webhook_at TEXT");
|
||||
function applyColumnMigration(version: number, table: string, column: string, sql: string) {
|
||||
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
||||
if (!columns.some((item) => item.name === column)) db.exec(sql);
|
||||
db.prepare("INSERT OR IGNORE INTO schema_migrations(version) VALUES(?)").run(version);
|
||||
}
|
||||
|
||||
applyColumnMigration(1, "sources", "webhook_secret_hash", "ALTER TABLE sources ADD COLUMN webhook_secret_hash TEXT");
|
||||
applyColumnMigration(2, "sources", "last_webhook_at", "ALTER TABLE sources ADD COLUMN last_webhook_at TEXT");
|
||||
applyColumnMigration(3, "sources", "is_enabled", "ALTER TABLE sources ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1");
|
||||
applyColumnMigration(4, "sources", "disabled_at", "ALTER TABLE sources ADD COLUMN disabled_at TEXT");
|
||||
applyColumnMigration(5, "sync_jobs", "trigger", "ALTER TABLE sync_jobs ADD COLUMN trigger TEXT NOT NULL DEFAULT 'manual'");
|
||||
applyColumnMigration(6, "sync_jobs", "started_at", "ALTER TABLE sync_jobs ADD COLUMN started_at TEXT");
|
||||
applyColumnMigration(7, "sync_jobs", "finished_at", "ALTER TABLE sync_jobs ADD COLUMN finished_at TEXT");
|
||||
|
||||
const admin = process.env.ADMIN_USERNAME;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
if (admin && adminPassword) {
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export type SyncTrigger = "manual" | "webhook" | "scheduled" | "source-created";
|
||||
|
||||
/** Queue one pull per source at a time. Returns true only when a new job was created. */
|
||||
export function queuePull(sourceId: number, trigger: SyncTrigger, payload: unknown = {}) {
|
||||
const result = db.prepare(`
|
||||
INSERT INTO sync_jobs(source_id,kind,payload_json,trigger)
|
||||
SELECT ?, 'pull', ?, ?
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sync_jobs
|
||||
WHERE source_id=? AND kind='pull' AND status IN ('queued','running')
|
||||
)
|
||||
`).run(sourceId, JSON.stringify(payload), trigger, sourceId);
|
||||
return result.changes === 1;
|
||||
}
|
||||
Reference in New Issue
Block a user