From 3d815afc829c5836b406a903d520a1cff0e43e21 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Tue, 19 Aug 2025 01:48:23 -0400 Subject: [PATCH] claude drizzle integration --- bun.lock | 6 +++++ drizzle.config.ts | 28 ++++++++++---------- package.json | 2 ++ src/app/page.tsx | 9 +++---- src/auth.ts | 3 +++ src/db.ts | 10 ------- src/db/index.ts | 12 +++++++++ src/db/schema.ts | 39 ++++++++++++++++++++++++---- src/lib/db.ts | 55 --------------------------------------- src/lib/events-db.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 135 insertions(+), 91 deletions(-) delete mode 100644 src/db.ts create mode 100644 src/db/index.ts delete mode 100644 src/lib/db.ts create mode 100644 src/lib/events-db.ts diff --git a/bun.lock b/bun.lock index e1d55a9..b3fdda0 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "ical-pwa", "dependencies": { + "@auth/drizzle-adapter": "^1.10.0", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -22,6 +23,7 @@ "next-auth": "^5.0.0-beta.29", "next-themes": "^0.4.6", "pg": "^8.16.3", + "postgres": "^3.4.7", "react": "19.1.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1", @@ -51,6 +53,8 @@ "@auth/core": ["@auth/core@0.40.0", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw=="], + "@auth/drizzle-adapter": ["@auth/drizzle-adapter@1.10.0", "", { "dependencies": { "@auth/core": "0.40.0" } }, "sha512-3MKsdAINTfvV4QKev8PFMNG93HJEUHh9sggDXnmUmriFogRf8qLvgqnPsTlfUyWcLwTzzrrYjeu8CGM+4IxHwQ=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], @@ -869,6 +873,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], diff --git a/drizzle.config.ts b/drizzle.config.ts index 99a6eae..7502c55 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,17 +1,15 @@ -"use server"; +import { defineConfig } from 'drizzle-kit'; +import * as dotenv from 'dotenv'; -import { config } from "dotenv"; -import { type Config } from "drizzle-kit"; +dotenv.config({ path: '.env.local' }); -config({ path: ".env" }); - -export default { - out: "./drizzle", - schema: "./src/db/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: process.env.DATABASE_URL!, - }, - verbose: true, - strict: true, -} satisfies Config; +export default defineConfig({ + dialect: 'postgresql', + schema: './src/db/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, + verbose: true, + strict: true, +}); \ No newline at end of file diff --git a/package.json b/package.json index 7ea4d45..ee6131d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@auth/drizzle-adapter": "^1.10.0", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -27,6 +28,7 @@ "next-auth": "^5.0.0-beta.29", "next-themes": "^0.4.6", "pg": "^8.16.3", + "postgres": "^3.4.7", "react": "19.1.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1" diff --git a/src/app/page.tsx b/src/app/page.tsx index 794cb56..d03ca69 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,7 +9,7 @@ import { Card } from '@/components/ui/card' import { RecurrencePicker } from '@/components/recurrence-picker' import { IcsFilePicker } from '@/components/ics-file-picker' -import { addEvent, deleteEvent, getAllEvents, clearEvents, getDB } from '@/lib/db' +import { saveEvent as addEvent, deleteEvent, getEvents as getAllEvents, clearEvents, updateEvent } from '@/lib/events-db' import { parseICS, generateICS } from '@/lib/ical' import type { CalendarEvent } from '@/lib/types' import { Textarea } from '@/components/ui/textarea' @@ -74,11 +74,8 @@ export default function HomePage() { lastModified: new Date().toISOString(), } if (editingId) { - const db = await getDB() - if (db) { - await db.put('events', eventData) - setEvents(prev => prev.map(e => (e.id === editingId ? eventData : e))) - } + await updateEvent(eventData) + setEvents(prev => prev.map(e => (e.id === editingId ? eventData : e))) } else { await addEvent(eventData) setEvents(prev => [...prev, eventData]) diff --git a/src/auth.ts b/src/auth.ts index e3daa56..74b1faa 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,6 +1,8 @@ import NextAuth, { NextAuthConfig, NextAuthResult } from "next-auth"; import Authentik from "next-auth/providers/authentik"; import type { Provider } from "next-auth/providers"; +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { db } from "@/db/index"; const providers: Provider[] = [ Authentik({ @@ -20,6 +22,7 @@ export const providerMap = providers.map((provider) => { }); const config = { + adapter: DrizzleAdapter(db), providers, pages: { signIn: "/signin", diff --git a/src/db.ts b/src/db.ts deleted file mode 100644 index 842cf89..0000000 --- a/src/db.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "dotenv/config"; -import { drizzle } from "drizzle-orm/node-postgres"; - -// You can specify any property from the node-postgres connection options -const db = drizzle({ - connection: { - connectionString: process.env.DATABASE_URL!, - ssl: true, - }, -}); diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..ecc23cf --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,12 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; + +const connectionString = process.env.DATABASE_URL!; + +const client = postgres(connectionString, { + prepare: false, + connect_timeout: 30, + idle_timeout: 30, +}); +export const db = drizzle(client, { schema }); \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index e66b799..ed1a815 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,7 +1,36 @@ -import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; -export const usersTable = pgTable("users", { - id: integer().primaryKey().generatedAlwaysAsIdentity(), - name: varchar({ length: 255 }).notNull(), - email: varchar({ length: 255 }).notNull().unique(), +export const users = pgTable('users', { + id: uuid('id').primaryKey().defaultRandom(), + email: text('email').notNull().unique(), + name: text('name'), + image: text('image'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), }); + +export const accounts = pgTable('accounts', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + type: text('type').notNull(), + provider: text('provider').notNull(), + providerAccountId: text('provider_account_id').notNull(), + refreshToken: text('refresh_token'), + accessToken: text('access_token'), + expiresAt: timestamp('expires_at'), + tokenType: text('token_type'), + scope: text('scope'), + idToken: text('id_token'), + sessionState: text('session_state'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +export const sessions = pgTable('sessions', { + id: uuid('id').primaryKey().defaultRandom(), + sessionToken: text('session_token').notNull().unique(), + userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + expires: timestamp('expires').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); \ No newline at end of file diff --git a/src/lib/db.ts b/src/lib/db.ts deleted file mode 100644 index a81dc04..0000000 --- a/src/lib/db.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { openDB, DBSchema, IDBPDatabase } from "idb"; -import { type CalendarEvent } from "./types"; - -interface ICalDB extends DBSchema { - events: { - key: string; - value: CalendarEvent; - }; -} - -let dbPromise: Promise> | null = null; - -async function initDB() { - return openDB("icalPWA", 1, { - upgrade(db) { - if (!db.objectStoreNames.contains("events")) { - db.createObjectStore("events", { keyPath: "id" }); - } - }, - }); -} - -// Get the database in a browser-safe way -export async function getDB() { - if (typeof window === "undefined") return null; - if (!dbPromise) { - dbPromise = initDB(); - } - return dbPromise; -} - -// CRUD operations — all SSR-safe -export async function getAllEvents() { - const db = await getDB(); - if (!db) return []; - return db.getAll("events"); -} - -export async function addEvent(event: ICalDB["events"]["value"]) { - const db = await getDB(); - if (!db) return; - return db.put("events", event); -} - -export async function deleteEvent(id: string) { - const db = await getDB(); - if (!db) return; - return db.delete("events", id); -} - -export async function clearEvents() { - const db = await getDB(); - if (!db) return; - return db.clear("events"); -} diff --git a/src/lib/events-db.ts b/src/lib/events-db.ts new file mode 100644 index 0000000..7094213 --- /dev/null +++ b/src/lib/events-db.ts @@ -0,0 +1,62 @@ +import { openDB, type IDBPDatabase } from 'idb'; +import type { CalendarEvent } from '@/lib/types'; + +const DB_NAME = 'LocalCalEvents'; +const DB_VERSION = 1; +const EVENTS_STORE = 'events'; + +let dbPromise: Promise | null = null; + +function getDB() { + if (!dbPromise) { + dbPromise = openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(EVENTS_STORE)) { + const store = db.createObjectStore(EVENTS_STORE, { keyPath: 'id' }); + store.createIndex('start', 'start'); + store.createIndex('title', 'title'); + } + }, + }); + } + return dbPromise; +} + +export async function saveEvent(event: CalendarEvent): Promise { + const db = await getDB(); + await db.put(EVENTS_STORE, event); +} + +export async function getEvents(): Promise { + const db = await getDB(); + return db.getAll(EVENTS_STORE); +} + +export async function getEvent(id: string): Promise { + const db = await getDB(); + return db.get(EVENTS_STORE, id); +} + +export async function deleteEvent(id: string): Promise { + const db = await getDB(); + await db.delete(EVENTS_STORE, id); +} + +export async function updateEvent(event: CalendarEvent): Promise { + const db = await getDB(); + await db.put(EVENTS_STORE, event); +} + +export async function getEventsByDateRange(startDate: string, endDate: string): Promise { + const db = await getDB(); + const tx = db.transaction(EVENTS_STORE, 'readonly'); + const index = tx.store.index('start'); + const events = await index.getAll(IDBKeyRange.bound(startDate, endDate)); + await tx.done; + return events; +} + +export async function clearEvents(): Promise { + const db = await getDB(); + await db.clear(EVENTS_STORE); +} \ No newline at end of file