claude drizzle integration
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -4,6 +4,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "ical-pwa",
|
"name": "ical-pwa",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@auth/drizzle-adapter": "^1.10.0",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"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/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=="],
|
"@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=="],
|
"@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=="],
|
"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-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
"use server";
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
import { config } from "dotenv";
|
dotenv.config({ path: '.env.local' });
|
||||||
import { type Config } from "drizzle-kit";
|
|
||||||
|
|
||||||
config({ path: ".env" });
|
export default defineConfig({
|
||||||
|
dialect: 'postgresql',
|
||||||
export default {
|
schema: './src/db/schema.ts',
|
||||||
out: "./drizzle",
|
out: './drizzle',
|
||||||
schema: "./src/db/schema.ts",
|
|
||||||
dialect: "postgresql",
|
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.DATABASE_URL!,
|
url: process.env.DATABASE_URL!,
|
||||||
},
|
},
|
||||||
verbose: true,
|
verbose: true,
|
||||||
strict: true,
|
strict: true,
|
||||||
} satisfies Config;
|
});
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@auth/drizzle-adapter": "^1.10.0",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Card } from '@/components/ui/card'
|
|||||||
import { RecurrencePicker } from '@/components/recurrence-picker'
|
import { RecurrencePicker } from '@/components/recurrence-picker'
|
||||||
import { IcsFilePicker } from '@/components/ics-file-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 { parseICS, generateICS } from '@/lib/ical'
|
||||||
import type { CalendarEvent } from '@/lib/types'
|
import type { CalendarEvent } from '@/lib/types'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
@@ -74,11 +74,8 @@ export default function HomePage() {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
const db = await getDB()
|
await updateEvent(eventData)
|
||||||
if (db) {
|
|
||||||
await db.put('events', eventData)
|
|
||||||
setEvents(prev => prev.map(e => (e.id === editingId ? eventData : e)))
|
setEvents(prev => prev.map(e => (e.id === editingId ? eventData : e)))
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await addEvent(eventData)
|
await addEvent(eventData)
|
||||||
setEvents(prev => [...prev, eventData])
|
setEvents(prev => [...prev, eventData])
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import NextAuth, { NextAuthConfig, NextAuthResult } from "next-auth";
|
import NextAuth, { NextAuthConfig, NextAuthResult } from "next-auth";
|
||||||
import Authentik from "next-auth/providers/authentik";
|
import Authentik from "next-auth/providers/authentik";
|
||||||
import type { Provider } from "next-auth/providers";
|
import type { Provider } from "next-auth/providers";
|
||||||
|
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
Authentik({
|
Authentik({
|
||||||
@@ -20,6 +22,7 @@ export const providerMap = providers.map((provider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
adapter: DrizzleAdapter(db),
|
||||||
providers,
|
providers,
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/signin",
|
signIn: "/signin",
|
||||||
|
|||||||
10
src/db.ts
10
src/db.ts
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
12
src/db/index.ts
Normal file
12
src/db/index.ts
Normal file
@@ -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 });
|
||||||
@@ -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", {
|
export const users = pgTable('users', {
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
name: varchar({ length: 255 }).notNull(),
|
email: text('email').notNull().unique(),
|
||||||
email: varchar({ length: 255 }).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(),
|
||||||
});
|
});
|
||||||
@@ -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<IDBPDatabase<ICalDB>> | null = null;
|
|
||||||
|
|
||||||
async function initDB() {
|
|
||||||
return openDB<ICalDB>("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");
|
|
||||||
}
|
|
||||||
62
src/lib/events-db.ts
Normal file
62
src/lib/events-db.ts
Normal file
@@ -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<IDBPDatabase> | 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<void> {
|
||||||
|
const db = await getDB();
|
||||||
|
await db.put(EVENTS_STORE, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEvents(): Promise<CalendarEvent[]> {
|
||||||
|
const db = await getDB();
|
||||||
|
return db.getAll(EVENTS_STORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEvent(id: string): Promise<CalendarEvent | undefined> {
|
||||||
|
const db = await getDB();
|
||||||
|
return db.get(EVENTS_STORE, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteEvent(id: string): Promise<void> {
|
||||||
|
const db = await getDB();
|
||||||
|
await db.delete(EVENTS_STORE, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateEvent(event: CalendarEvent): Promise<void> {
|
||||||
|
const db = await getDB();
|
||||||
|
await db.put(EVENTS_STORE, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEventsByDateRange(startDate: string, endDate: string): Promise<CalendarEvent[]> {
|
||||||
|
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<void> {
|
||||||
|
const db = await getDB();
|
||||||
|
await db.clear(EVENTS_STORE);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user