UNPKG

@squirrel-scheduler/drizzle-adapter

Version:
450 lines (413 loc) 15.1 kB
import { BaseSQLiteDatabase, integer, SQLiteColumn, sqliteTable, SQLiteTableWithColumns, text, } from "drizzle-orm/sqlite-core" import {and, desc, eq, gte, inArray, lt} from "drizzle-orm"; // Example domain types from your core package import {PruneTasksParams, SDBAdapter, STask, TaskAttemptResult} from "@squirrel-scheduler/core" import { ListTasksParams } from "@squirrel-scheduler/core"; // 1. Define a reusable "DefaultSQLiteColumn" type type DefaultSQLiteColumn< T extends { data: string | number | boolean | Date dataType: "string" | "number" | "boolean" | "date" notNull: boolean isPrimaryKey?: boolean columnType: "SQLiteText" | "SQLiteInteger" | "SQLiteBoolean" | "SQLiteTimestamp" }, > = SQLiteColumn<{ name: string isAutoincrement: boolean isPrimaryKey: T["isPrimaryKey"] extends true ? true : false hasRuntimeDefault: boolean generated: unknown // can refine if needed columnType: T["columnType"] data: T["data"] driverParam: string | number | boolean notNull: T["notNull"] hasDefault: boolean enumValues: any dataType: T["dataType"] tableName: string }> // 2. Define typed interfaces for each table with columns export type DefaultSQLiteTasksTable = SQLiteTableWithColumns<{ name: string columns: { id: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: true isPrimaryKey: true }> payload: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: true }> scheduledAt: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: true }> status: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: STask["status"] notNull: true }> retryCount: DefaultSQLiteColumn<{ dataType: "number" columnType: "SQLiteInteger" data: number notNull: true }> maxRetries: DefaultSQLiteColumn<{ dataType: "number" columnType: "SQLiteInteger" data: number notNull: true }> lastAttemptAt: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: false }> nextAttemptAt: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: false }> createdAt: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: true }> updatedAt: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: true }> metadata: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: false }> } dialect: "sqlite" schema: string | undefined }> export type DefaultSQLiteTaskResultsTable = SQLiteTableWithColumns<{ name: string columns: { id: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: true isPrimaryKey: true }> taskId: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: true }> attemptedAt: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: true }> statusCode: DefaultSQLiteColumn<{ dataType: "number" columnType: "SQLiteInteger" data: number notNull: true }> response: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: false }> error: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: false }> duration: DefaultSQLiteColumn<{ dataType: "number" columnType: "SQLiteInteger" data: number notNull: true }> } dialect: "sqlite" schema: string | undefined }> export type DefaultSQLiteSyncHistoryTable = SQLiteTableWithColumns<{ name: string columns: { id: DefaultSQLiteColumn<{ dataType: "string" columnType: "SQLiteText" data: string notNull: true isPrimaryKey: true }> timestamp: DefaultSQLiteColumn<{ dataType: "date" columnType: "SQLiteTimestamp" data: Date | number notNull: true }> totalTasks: DefaultSQLiteColumn<{ dataType: "number" columnType: "SQLiteInteger" data: number notNull: true }> } dialect: "sqlite" schema: string | undefined }> // 3. Combine them in a single schema interface export type DefaultSQLiteSchema = { tasksTable?: DefaultSQLiteTasksTable taskResultsTable?: DefaultSQLiteTaskResultsTable syncHistoryTable?: DefaultSQLiteSyncHistoryTable } // 4. Provide default table definitions export const defaultTasksTable = sqliteTable("squirrel_task", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), payload: text("payload").notNull(), scheduledAt: integer("scheduledAt", {mode: "timestamp_ms"}).notNull(), status: text("status").$type<STask["status"]>().notNull(), retryCount: integer("retryCount").notNull().default(0), maxRetries: integer("maxRetries").notNull(), lastAttemptAt: integer("lastAttemptAt", {mode: "timestamp_ms"}), nextAttemptAt: integer("nextAttemptAt", {mode: "timestamp_ms"}), createdAt: integer("createdAt", {mode: "timestamp_ms"}) .notNull() .$defaultFn(() => new Date()), updatedAt: integer("updatedAt", {mode: "timestamp_ms"}) .notNull() .$defaultFn(() => new Date()), metadata: text("metadata"), }) satisfies DefaultSQLiteTasksTable export const defaultTaskResultsTable = sqliteTable("squirrel_task_result", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), taskId: text("taskId") .notNull() .references(() => defaultTasksTable.id, {onDelete: "cascade"}), attemptedAt: integer("attemptedAt", {mode: "timestamp_ms"}).notNull(), statusCode: integer("statusCode").notNull(), response: text("response"), error: text("error"), duration: integer("duration").notNull(), }) satisfies DefaultSQLiteTaskResultsTable export const defaultSyncHistoryTable = sqliteTable("squirrel_sync_history", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), timestamp: integer("timestamp", {mode: "timestamp_ms"}).notNull(), totalTasks: integer("totalTasks").notNull(), }) satisfies DefaultSQLiteSyncHistoryTable // 5. defineTables() merges user overrides, returning typed tables export function defineTables( schema: DefaultSQLiteSchema = {} ): { tasksTable: DefaultSQLiteTasksTable taskResultsTable: DefaultSQLiteTaskResultsTable, syncHistoryTable: DefaultSQLiteSyncHistoryTable } { return { tasksTable: schema.tasksTable ?? defaultTasksTable, taskResultsTable: schema.taskResultsTable ?? defaultTaskResultsTable, syncHistoryTable: schema.syncHistoryTable ?? defaultSyncHistoryTable, } } // sqlite.ts (continued) export function SQLiteDrizzleAdapter( client: BaseSQLiteDatabase<"sync" | "async", any, any>, schema?: DefaultSQLiteSchema ): SDBAdapter { const { tasksTable, taskResultsTable, syncHistoryTable } = defineTables(schema); return { async claimTasks(tasks: STask[]) { const ids = tasks.map((task) => task.id); await client .update(tasksTable) .set({ status: "in_progress" }) .where(and(inArray(tasksTable.id, ids), eq(tasksTable.status, "pending"))); return await client .select() .from(tasksTable) .where(and(inArray(tasksTable.id, ids), eq(tasksTable.status, "in_progress"))) .execute(); }, async createTask( data: Omit<STask, "id" | "status" | "retryCount" | "createdAt" | "updatedAt"> ) { const result = await client .insert(tasksTable) .values({ payload: JSON.stringify(data.payload), scheduledAt: data.scheduledAt, maxRetries: data.maxRetries, // ... add nextTaskId or metadata if needed nextTaskId: data.nextTaskId, metadata: data.metadata ? JSON.stringify(data.metadata) : null, status: "pending", retryCount: 0, createdAt: new Date(), updatedAt: new Date(), }) .returning(); return result[0]; }, async createTasks( data: Array<Omit<STask, "id" | "status" | "retryCount" | "createdAt" | "updatedAt">> ) { const results = await client .insert(tasksTable) .values( data.map((task) => ({ // Removed: url: task.url, payload: JSON.stringify(task.payload), scheduledAt: task.scheduledAt, maxRetries: task.maxRetries, nextTaskId: task.nextTaskId, metadata: task.metadata ? JSON.stringify(task.metadata) : null, status: "pending", retryCount: 0, createdAt: new Date(), updatedAt: new Date(), })) ) .returning(); return results; }, async getLastSync() { const result = await client .select() .from(syncHistoryTable) .orderBy(desc(syncHistoryTable.timestamp)) .limit(1) .execute(); if (Array.isArray(result) && result.length > 0) { return result[0] as any; } if (result) { return result; } return { timestamp: 0, totalTasks: 0, }; }, async getTask(id: string) { const task = await client .select() .from(tasksTable) .where(eq(tasksTable.id, id)) .limit(1) .execute(); return task[0]; }, async listTasks(params: ListTasksParams) { const { limit, offset, status, from, to } = params; const conditions = []; if (status) conditions.push(eq(tasksTable.status, status)); if (from) { conditions.push(gte(tasksTable.scheduledAt, from)); } if (to) conditions.push(lt(tasksTable.scheduledAt, to)); return await client .select() .from(tasksTable) .where(and(...conditions)) .orderBy(desc(tasksTable.scheduledAt)) .limit(limit) .offset(offset) .execute(); }, async pruneTasks(params: PruneTasksParams) { const { olderThan, status } = params; const conditions = []; if (status) conditions.push(eq(tasksTable.status, status)); if (olderThan) conditions.push(lt(tasksTable.updatedAt, olderThan)); const result = await client.delete(tasksTable).where(and(...conditions)).returning(); return result.length; }, async setLastSync(at?: Date, args?: { totalTasks: number }) { return client.insert(syncHistoryTable).values({ id: crypto.randomUUID(), timestamp: at ?? new Date(), totalTasks: args?.totalTasks ?? 0, }); }, async recordTaskAttempt(taskId: string, result: TaskAttemptResult) { return client.insert(taskResultsTable).values({ id: crypto.randomUUID(), taskId, attemptedAt: new Date(), statusCode: result.statusCode, response: result.response || null, error: result.error || null, duration: result.duration, }); }, async updateTask(taskId: string, update: Partial<Omit<STask, "id">>) { // If your payload changed, ensure JSON stringifying if needed. const updatePayload: Partial<Record<string, unknown>> = { updatedAt: new Date() }; if (typeof update.payload !== "undefined") { updatePayload.payload = JSON.stringify(update.payload); } if (typeof update.metadata !== "undefined") { updatePayload.metadata = update.metadata !== null ? JSON.stringify(update.metadata) : null; } // Add other fields (like status, nextAttemptAt, etc.) if (typeof update.status !== "undefined") { updatePayload.status = update.status; } if (typeof update.retryCount !== "undefined") { updatePayload.retryCount = update.retryCount; } if (typeof update.lastAttemptAt !== "undefined") { updatePayload.lastAttemptAt = update.lastAttemptAt; } if (typeof update.nextAttemptAt !== "undefined") { updatePayload.nextAttemptAt = update.nextAttemptAt; } if (typeof update.maxRetries !== "undefined") { updatePayload.maxRetries = update.maxRetries; } if (typeof update.nextTaskId !== "undefined") { updatePayload.nextTaskId = update.nextTaskId; } await client.update(tasksTable).set(updatePayload).where(eq(tasksTable.id, taskId)); const updatedTask = await client .select() .from(tasksTable) .where(eq(tasksTable.id, taskId)) .limit(1) .execute(); return updatedTask[0]; }, }; } // 02-11