From 4e1c0558d5a64381bcc26627f639da2953fb57d2 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 03:54:59 +0900 Subject: [PATCH 1/8] engineType=client --- app/lib/prisma.ts | 23 ++++- package-lock.json | 213 ++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + prisma/schema.prisma | 1 + 4 files changed, 236 insertions(+), 3 deletions(-) diff --git a/app/lib/prisma.ts b/app/lib/prisma.ts index 72dd8ef..4fb258d 100644 --- a/app/lib/prisma.ts +++ b/app/lib/prisma.ts @@ -1,5 +1,24 @@ import { PrismaClient } from "../generated/prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaNeon } from "@prisma/adapter-neon"; +import { getCloudflareContext } from "@opennextjs/cloudflare"; -const prisma = new PrismaClient(); -export default prisma; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let cloudflareEnv: any; +try { + cloudflareEnv = getCloudflareContext().env; +} catch { + // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている + cloudflareEnv = {}; +} +const DATABASE_URL = + process.env.DATABASE_URL ?? cloudflareEnv.DATABASE_URL ?? ""; +let adapter; +if (DATABASE_URL.includes("neon")) { + adapter = new PrismaNeon({ connectionString: DATABASE_URL }); +} else { + adapter = new PrismaPg({ connectionString: DATABASE_URL }); +} +const prisma = new PrismaClient({ adapter }); +export default prisma; diff --git a/package-lock.json b/package-lock.json index e6c04fc..1df0fd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@fontsource-variable/noto-sans-jp": "^5.2.6", "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.7.1", + "@prisma/adapter-neon": "^6.18.0", + "@prisma/adapter-pg": "^6.18.0", "@prisma/client": "^6.18.0", "@xterm/addon-fit": "^0.11.0-beta.115", "@xterm/xterm": "^5.6.0-beta.115", @@ -9117,6 +9119,28 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@neondatabase/serverless": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.0.2.tgz", + "integrity": "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.15.30", + "@types/pg": "^8.8.0" + }, + "engines": { + "node": ">=19.0.0" + } + }, + "node_modules/@neondatabase/serverless/node_modules/@types/node": { + "version": "22.18.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", + "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@next/env": { "version": "15.4.7", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.7.tgz", @@ -9724,6 +9748,28 @@ "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", "license": "MIT" }, + "node_modules/@prisma/adapter-neon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-6.18.0.tgz", + "integrity": "sha512-U97AnsRVEj7nHBujGM2GFD8QZUafdSdC98FItu4TRywzazWb5JaHi/tyfxTW5PthDfXZEyjzE8FaD/9HMi6h8g==", + "license": "Apache-2.0", + "dependencies": { + "@neondatabase/serverless": ">0.6.0 <2", + "@prisma/driver-adapter-utils": "6.18.0", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-6.18.0.tgz", + "integrity": "sha512-eBtBOL1z2Sh0Rc2hwa4dmwoNeFs5T69tsA62AkhBvnzNfTcr/YfLeJae/wjNmvRttYDPmdiuhqZCRPNY1+8fOA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "6.18.0", + "pg": "^8.11.3", + "postgres-array": "3.0.4" + } + }, "node_modules/@prisma/client": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", @@ -9763,9 +9809,17 @@ "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", - "devOptional": true, "license": "Apache-2.0" }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-6.18.0.tgz", + "integrity": "sha512-9wgSriEKs4j1ePxlv1/RNfJV9Gu5rzG37Neshg+DfrCcUY3amroERvTjyR04w5J1THdGdOTgGL9VdJcVaKRMmQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.18.0" + } + }, "node_modules/@prisma/engines": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", @@ -11710,6 +11764,17 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/pg": { + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", @@ -18861,6 +18926,104 @@ "devOptional": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -18931,6 +19094,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -19998,6 +20200,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", diff --git a/package.json b/package.json index 6a5c18e..0a0e497 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@fontsource-variable/noto-sans-jp": "^5.2.6", "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.7.1", + "@prisma/adapter-neon": "^6.18.0", + "@prisma/adapter-pg": "^6.18.0", "@prisma/client": "^6.18.0", "@xterm/addon-fit": "^0.11.0-beta.115", "@xterm/xterm": "^5.6.0-beta.115", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index adba390..52082c4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,6 +7,7 @@ generator client { provider = "prisma-client" output = "../app/generated/prisma" + engineType = "client" } datasource db { From 508a93f874dee396c46a0bfb9ad876a06f28c63f Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 18:26:29 +0900 Subject: [PATCH 2/8] =?UTF-8?q?worker=E3=81=A7=E5=8B=95=E3=81=8F=E3=82=88?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 -- app/api/auth/[...all]/route.ts | 8 +++-- app/lib/auth.ts | 66 ++++++++++++++++++---------------- app/lib/chatHistory.ts | 9 +++-- app/lib/prisma.ts | 32 +++++++---------- next.config.ts | 1 + package.json | 2 +- prisma/schema.prisma | 7 ++-- 8 files changed, 67 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 485c203..efb8a61 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,3 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts - -/app/generated/prisma diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts index fcb1ef0..7f0db11 100644 --- a/app/api/auth/[...all]/route.ts +++ b/app/api/auth/[...all]/route.ts @@ -1,4 +1,8 @@ -import { auth } from "@/lib/auth"; // path to your auth file +import { getAuthServer } from "@/lib/auth"; +import { getPrismaClient } from "@/lib/prisma"; import { toNextJsHandler } from "better-auth/next-js"; -export const { POST, GET } = toNextJsHandler(auth); +export const { POST, GET } = toNextJsHandler({ + handler: async (req) => + (await getAuthServer(await getPrismaClient())).handler(req), +}); diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 1b91e16..48cef12 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -2,37 +2,43 @@ import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { getCloudflareContext } from "@opennextjs/cloudflare"; import { anonymous } from "better-auth/plugins"; -import prisma from "./prisma"; import { migrateChatUser } from "./chatHistory"; +import { PrismaClient } from "@prisma/client"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let cloudflareEnv: any; -try { - cloudflareEnv = getCloudflareContext().env; -} catch { - // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている - cloudflareEnv = {}; -} -export const auth = betterAuth({ - database: prismaAdapter(prisma, { - provider: "postgresql", - }), - plugins: [ - anonymous({ - onLinkAccount: ({ anonymousUser, newUser }) => - migrateChatUser(anonymousUser.user.id, newUser.user.id), +export async function getAuthServer(prisma: PrismaClient) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let cloudflareEnv: any; + try { + cloudflareEnv = getCloudflareContext().env; + } catch { + // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている + cloudflareEnv = {}; + } + return betterAuth({ + database: prismaAdapter(prisma, { + provider: "sqlite", }), - ], - socialProviders: { - github: { - clientId: process.env.GITHUB_CLIENT_ID ?? cloudflareEnv.GITHUB_CLIENT_ID, - clientSecret: - process.env.GITHUB_CLIENT_SECRET ?? cloudflareEnv.GITHUB_CLIENT_SECRET, - }, - google: { - clientId: process.env.GOOGLE_CLIENT_ID ?? cloudflareEnv.GOOGLE_CLIENT_ID, - clientSecret: - process.env.GOOGLE_CLIENT_SECRET ?? cloudflareEnv.GOOGLE_CLIENT_SECRET, + plugins: [ + anonymous({ + onLinkAccount: ({ anonymousUser, newUser }) => + migrateChatUser(anonymousUser.user.id, newUser.user.id), + }), + ], + socialProviders: { + github: { + clientId: + process.env.GITHUB_CLIENT_ID ?? cloudflareEnv.GITHUB_CLIENT_ID, + clientSecret: + process.env.GITHUB_CLIENT_SECRET ?? + cloudflareEnv.GITHUB_CLIENT_SECRET, + }, + google: { + clientId: + process.env.GOOGLE_CLIENT_ID ?? cloudflareEnv.GOOGLE_CLIENT_ID, + clientSecret: + process.env.GOOGLE_CLIENT_SECRET ?? + cloudflareEnv.GOOGLE_CLIENT_SECRET, + }, }, - }, -}); + }); +} diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts index a8faf0b..92a7a02 100644 --- a/app/lib/chatHistory.ts +++ b/app/lib/chatHistory.ts @@ -1,6 +1,6 @@ import { headers } from "next/headers"; -import { auth } from "./auth"; -import prisma from "./prisma"; +import { getAuthServer } from "./auth"; +import { getPrismaClient } from "./prisma"; export interface CreateChatMessage { role: "user" | "ai" | "error"; @@ -12,6 +12,8 @@ export async function addChat( sectionId: string, messages: CreateChatMessage[] ) { + const prisma = await getPrismaClient(); + const auth = await getAuthServer(prisma); const session = await auth.api.getSession({ headers: await headers() }); if (!session) { throw new Error("Not authenticated"); @@ -37,6 +39,8 @@ export async function addChat( export type ChatWithMessages = Awaited>; export async function getChat(docsId: string) { + const prisma = await getPrismaClient(); + const auth = await getAuthServer(prisma); const session = await auth.api.getSession({ headers: await headers() }); if (!session) { return []; @@ -61,6 +65,7 @@ export async function getChat(docsId: string) { } export async function migrateChatUser(oldUserId: string, newUserId: string) { + const prisma = await getPrismaClient(); await prisma.chat.updateMany({ where: { userId: oldUserId, diff --git a/app/lib/prisma.ts b/app/lib/prisma.ts index 4fb258d..6ae1703 100644 --- a/app/lib/prisma.ts +++ b/app/lib/prisma.ts @@ -1,24 +1,16 @@ -import { PrismaClient } from "../generated/prisma/client"; -import { PrismaPg } from "@prisma/adapter-pg"; -import { PrismaNeon } from "@prisma/adapter-neon"; import { getCloudflareContext } from "@opennextjs/cloudflare"; +import { PrismaClient } from "@prisma/client"; +import { PrismaD1 } from "@prisma/adapter-d1"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let cloudflareEnv: any; -try { - cloudflareEnv = getCloudflareContext().env; -} catch { - // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている - cloudflareEnv = {}; -} -const DATABASE_URL = - process.env.DATABASE_URL ?? cloudflareEnv.DATABASE_URL ?? ""; +export async function getPrismaClient() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let cloudflareEnv: any; + try { + cloudflareEnv = (await getCloudflareContext({ async: true })).env; + } catch { + // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている + cloudflareEnv = {}; + } -let adapter; -if (DATABASE_URL.includes("neon")) { - adapter = new PrismaNeon({ connectionString: DATABASE_URL }); -} else { - adapter = new PrismaPg({ connectionString: DATABASE_URL }); + return new PrismaClient({ adapter: new PrismaD1(cloudflareEnv.my_code_db) }); } -const prisma = new PrismaClient({ adapter }); -export default prisma; diff --git a/next.config.ts b/next.config.ts index 8757724..069c8e8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,6 +15,7 @@ const nextConfig: NextConfig = { env: { PYODIDE_VERSION: pyodideVersion, }, + serverExternalPackages: ["@prisma/client", ".prisma/client"], }; export default nextConfig; diff --git a/package.json b/package.json index 0a0e497..54db015 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "prisma generate && npm run cf-typegen && next lint", "tsc": "prisma generate && npm run cf-typegen && tsc", "format": "prettier --write app/", - "cf-preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", + "cf-preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview --port 3000", "cf-deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 52082c4..80e474d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -5,9 +5,10 @@ // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { - provider = "prisma-client" - output = "../app/generated/prisma" - engineType = "client" + provider = "prisma-client-js" + // outputを指定すると動作しない: https://opennext.js.org/cloudflare/howtos/db#schemaprisma + // output = "../app/generated/prisma" + // engineType = "client" } datasource db { From 26a3d360cfede5516c6df00492154bf439f8243e Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 19:50:14 +0900 Subject: [PATCH 3/8] better-auth+drizzle+pg --- README.md | 37 +-- app/api/auth/[...all]/route.ts | 4 +- app/lib/auth.ts | 13 +- app/lib/chatHistory.ts | 1 - app/lib/drizzle.ts | 29 ++ app/schema/auth.ts | 62 +++++ drizzle.config.ts | 10 + drizzle/0000_classy_thunderbolt_ross.sql | 51 ++++ drizzle/meta/0000_snapshot.json | 332 +++++++++++++++++++++++ drizzle/meta/_journal.json | 13 + package.json | 12 +- 11 files changed, 534 insertions(+), 30 deletions(-) create mode 100644 app/lib/drizzle.ts create mode 100644 app/schema/auth.ts create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_classy_thunderbolt_ross.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json diff --git a/README.md b/README.md index d58b8a2..0a73c0a 100644 --- a/README.md +++ b/README.md @@ -3,36 +3,30 @@ https://my-code.utcode.net ## インストール + ```bash npm ci ``` +## 開発環境 + +```bash +npx prisma dev +``` +を実行し、`t` キーを押して表示される DATABASE_URL をコピー + ルートディレクトリに .env.local という名前のファイルを作成し、以下の内容を記述 ```dotenv API_KEY=GeminiAPIキー BETTER_AUTH_URL=http://localhost:3000 +DATABASE_URL="postgres://... (prisma devの出力)" ``` -prismaの開発環境を起動 -(.env にDATABASE_URLが自動的に追加される) +別のターミナルで、 ```bash -npx prisma dev -``` -別ターミナルで -```bash -npx prisma db push +npx drizzle-kit migrate ``` - -### 本番環境の場合 - -上記の環境変数以外に、 -* BETTER_AUTH_SECRET に任意の文字列 -* DATABASE_URL に本番用のPostgreSQLデータベースURL -* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google -* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github - - -## 開発環境 +でデータベースを初期化 ```bash npm run dev @@ -49,6 +43,13 @@ npm run lint ``` でコードをチェックします。出てくるwarningやerrorはできるだけ直しましょう。 +### 本番環境の場合 + +上記の環境変数以外に、 +* BETTER_AUTH_SECRET に任意の文字列 +* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google +* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github + ## markdown仕様 ```` diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts index 7f0db11..36bb229 100644 --- a/app/api/auth/[...all]/route.ts +++ b/app/api/auth/[...all]/route.ts @@ -1,8 +1,8 @@ import { getAuthServer } from "@/lib/auth"; -import { getPrismaClient } from "@/lib/prisma"; +import { getDrizzle } from "@/lib/drizzle"; import { toNextJsHandler } from "better-auth/next-js"; export const { POST, GET } = toNextJsHandler({ handler: async (req) => - (await getAuthServer(await getPrismaClient())).handler(req), + (await getAuthServer(await getDrizzle())).handler(req), }); diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 48cef12..3daaf48 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -1,11 +1,11 @@ import { betterAuth } from "better-auth"; -import { prismaAdapter } from "better-auth/adapters/prisma"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { getCloudflareContext } from "@opennextjs/cloudflare"; import { anonymous } from "better-auth/plugins"; import { migrateChatUser } from "./chatHistory"; -import { PrismaClient } from "@prisma/client"; +import { getDrizzle } from "./drizzle"; -export async function getAuthServer(prisma: PrismaClient) { +export async function getAuthServer(drizzle: ReturnType) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let cloudflareEnv: any; try { @@ -15,8 +15,8 @@ export async function getAuthServer(prisma: PrismaClient) { cloudflareEnv = {}; } return betterAuth({ - database: prismaAdapter(prisma, { - provider: "sqlite", + database: drizzleAdapter(drizzle, { + provider: "pg", }), plugins: [ anonymous({ @@ -42,3 +42,6 @@ export async function getAuthServer(prisma: PrismaClient) { }, }); } + +// @better-auth/cli を実行するときだけ以下のコメントアウトを解除 +// export const auth = await getAuthServer(await getDrizzle()); diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts index 92a7a02..470de82 100644 --- a/app/lib/chatHistory.ts +++ b/app/lib/chatHistory.ts @@ -1,6 +1,5 @@ import { headers } from "next/headers"; import { getAuthServer } from "./auth"; -import { getPrismaClient } from "./prisma"; export interface CreateChatMessage { role: "user" | "ai" | "error"; diff --git a/app/lib/drizzle.ts b/app/lib/drizzle.ts new file mode 100644 index 0000000..d657292 --- /dev/null +++ b/app/lib/drizzle.ts @@ -0,0 +1,29 @@ +import { getCloudflareContext } from "@opennextjs/cloudflare"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as authSchema from "../schema/auth"; + +export async function getDrizzle() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let cloudflareEnv: any; + try { + cloudflareEnv = (await getCloudflareContext({ async: true })).env; + } catch { + // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている + cloudflareEnv = {}; + } + const DATABASE_URL = + process.env.DATABASE_URL ?? cloudflareEnv.DATABASE_URL ?? ""; + + const pool = new Pool({ + connectionString: DATABASE_URL, + // You don't want to reuse the same connection for multiple requests + maxUses: 1, + }); + return drizzle({ + client: pool, + schema: { + ...authSchema, + }, + }); +} diff --git a/app/schema/auth.ts b/app/schema/auth.ts new file mode 100644 index 0000000..e0e1f52 --- /dev/null +++ b/app/schema/auth.ts @@ -0,0 +1,62 @@ +import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"; + +export const user = pgTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false).notNull(), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + isAnonymous: boolean("is_anonymous"), +}); + +export const session = pgTable("session", { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), +}); + +export const account = pgTable("account", { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), +}); + +export const verification = pgTable("verification", { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), +}); diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..95c7dea --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import "dotenv/config"; +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", // 'mysql' | 'sqlite' | 'turso' + schema: "./app/schema", + dbCredentials: { + url: process.env.DATABASE_URL || "", + }, +}); diff --git a/drizzle/0000_classy_thunderbolt_ross.sql b/drizzle/0000_classy_thunderbolt_ross.sql new file mode 100644 index 0000000..8d01524 --- /dev/null +++ b/drizzle/0000_classy_thunderbolt_ross.sql @@ -0,0 +1,51 @@ +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "is_anonymous" boolean, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..4b57d00 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,332 @@ +{ + "id": "160bc5eb-e56b-48c4-92d7-2247c63401fc", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..1d9c768 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1761475266670, + "tag": "0000_classy_thunderbolt_ross", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 54db015..7d5f529 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "prisma generate && npm run cf-typegen && next dev", - "build": "prisma generate && npm run cf-typegen && next build", + "dev": "npm run cf-typegen && next dev", + "build": "npm run cf-typegen && next build", "start": "next start", - "lint": "prisma generate && npm run cf-typegen && next lint", - "tsc": "prisma generate && npm run cf-typegen && tsc", + "lint": "npm run cf-typegen && next lint", + "tsc": "npm run cf-typegen && tsc", "format": "prettier --write app/", "cf-preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview --port 3000", "cf-deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", @@ -29,7 +29,9 @@ "chalk": "^5.5.0", "clsx": "^2.1.1", "dotenv": "^17.2.3", + "drizzle-orm": "^0.44.7", "next": "<15.5", + "pg": "^8.16.3", "prismjs": "^1.30.0", "pyodide": "^0.28.1", "react": "19.1.0", @@ -45,11 +47,13 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.15.5", "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", "daisyui": "^5.0.50", + "drizzle-kit": "^0.31.5", "eslint": "^9", "eslint-config-next": "<15.4", "prettier": "^3.6.2", From 8d84c4db3ab7187fda54cbe770c7c327c2430900 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:23:26 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E3=82=82drizzle=E3=81=A7=E6=89=B1?= =?UTF-8?q?=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/auth.ts | 4 +- app/lib/chatHistory.ts | 77 +++--- app/lib/drizzle.ts | 2 + app/schema/chat.ts | 29 +++ drizzle/0001_stale_reaper.sql | 15 ++ drizzle/meta/0001_snapshot.json | 422 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + prisma/schema.prisma | 103 -------- 8 files changed, 518 insertions(+), 141 deletions(-) create mode 100644 app/schema/chat.ts create mode 100644 drizzle/0001_stale_reaper.sql create mode 100644 drizzle/meta/0001_snapshot.json delete mode 100644 prisma/schema.prisma diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 3daaf48..2bee1c7 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -5,7 +5,9 @@ import { anonymous } from "better-auth/plugins"; import { migrateChatUser } from "./chatHistory"; import { getDrizzle } from "./drizzle"; -export async function getAuthServer(drizzle: ReturnType) { +export async function getAuthServer( + drizzle: Awaited> +) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let cloudflareEnv: any; try { diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts index 470de82..5e37c07 100644 --- a/app/lib/chatHistory.ts +++ b/app/lib/chatHistory.ts @@ -1,5 +1,8 @@ import { headers } from "next/headers"; import { getAuthServer } from "./auth"; +import { getDrizzle } from "./drizzle"; +import { chat, message } from "@/schema/chat"; +import { and, asc, eq } from "drizzle-orm"; export interface CreateChatMessage { role: "user" | "ai" | "error"; @@ -11,66 +14,66 @@ export async function addChat( sectionId: string, messages: CreateChatMessage[] ) { - const prisma = await getPrismaClient(); - const auth = await getAuthServer(prisma); + const drizzle = await getDrizzle(); + const auth = await getAuthServer(drizzle); const session = await auth.api.getSession({ headers: await headers() }); if (!session) { throw new Error("Not authenticated"); } - return await prisma.chat.create({ - data: { + const [newChat] = await drizzle + .insert(chat) + .values({ userId: session.user.id, docsId, sectionId, - messages: { - createMany: { - data: messages, - }, - }, - }, - include: { - messages: true, - }, - }); + }) + .returning(); + + const chatMessages = await drizzle + .insert(message) + .values( + messages.map((msg) => ({ + chatId: newChat.chatId, + role: msg.role, + content: msg.content, + })) + ) + .returning(); + + return { + ...newChat, + messages: chatMessages, + }; } export type ChatWithMessages = Awaited>; export async function getChat(docsId: string) { - const prisma = await getPrismaClient(); - const auth = await getAuthServer(prisma); + const drizzle = await getDrizzle(); + const auth = await getAuthServer(drizzle); const session = await auth.api.getSession({ headers: await headers() }); if (!session) { return []; } - return await prisma.chat.findMany({ - where: { - userId: session.user.id, - docsId, - }, - include: { + const chats = await drizzle.query.chat.findMany({ + where: and(eq(chat.userId, session.user.id), eq(chat.docsId, docsId)), + with: { messages: { - orderBy: { - createdAt: "asc", - }, + orderBy: [asc(message.createdAt)], }, }, - orderBy: { - createdAt: "asc", - }, + orderBy: [asc(chat.createdAt)], }); + + return chats; } export async function migrateChatUser(oldUserId: string, newUserId: string) { - const prisma = await getPrismaClient(); - await prisma.chat.updateMany({ - where: { - userId: oldUserId, - }, - data: { - userId: newUserId, - }, - }); + const drizzle = await getDrizzle(); + await drizzle + .update(chat) + .set({ userId: newUserId }) + .where(eq(chat.userId, oldUserId)); } diff --git a/app/lib/drizzle.ts b/app/lib/drizzle.ts index d657292..8718088 100644 --- a/app/lib/drizzle.ts +++ b/app/lib/drizzle.ts @@ -2,6 +2,7 @@ import { getCloudflareContext } from "@opennextjs/cloudflare"; import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; import * as authSchema from "../schema/auth"; +import * as chatSchema from "../schema/chat"; export async function getDrizzle() { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -24,6 +25,7 @@ export async function getDrizzle() { client: pool, schema: { ...authSchema, + ...chatSchema, }, }); } diff --git a/app/schema/chat.ts b/app/schema/chat.ts new file mode 100644 index 0000000..722071d --- /dev/null +++ b/app/schema/chat.ts @@ -0,0 +1,29 @@ +import { relations } from "drizzle-orm"; +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; + +export const chat = pgTable("chat", { + chatId: uuid("chatId").primaryKey().defaultRandom(), + userId: text("userId").notNull(), + docsId: text("docsId").notNull(), + sectionId: text("sectionId").notNull(), + createdAt: timestamp("createdAt").notNull().defaultNow(), +}); + +export const message = pgTable("message", { + id: uuid("id").primaryKey().defaultRandom(), + chatId: uuid("chatId").notNull(), + role: text("role").notNull(), + content: text("content").notNull(), + createdAt: timestamp("createdAt").notNull().defaultNow(), +}); + +export const chatRelations = relations(chat, ({ many }) => ({ + messages: many(message), +})); + +export const messageRelations = relations(message, ({ one }) => ({ + chat: one(chat, { + fields: [message.chatId], + references: [chat.chatId], + }), +})); diff --git a/drizzle/0001_stale_reaper.sql b/drizzle/0001_stale_reaper.sql new file mode 100644 index 0000000..f57e46b --- /dev/null +++ b/drizzle/0001_stale_reaper.sql @@ -0,0 +1,15 @@ +CREATE TABLE "chat" ( + "chatId" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "userId" text NOT NULL, + "docsId" text NOT NULL, + "sectionId" text NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "message" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "chatId" uuid NOT NULL, + "role" text NOT NULL, + "content" text NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL +); diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..0a0d275 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,422 @@ +{ + "id": "25214b31-4d61-4cdf-8463-9ea838a4a573", + "prevId": "160bc5eb-e56b-48c4-92d7-2247c63401fc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "docsId": { + "name": "docsId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sectionId": { + "name": "sectionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1d9c768..53e3ecb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1761475266670, "tag": "0000_classy_thunderbolt_ross", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1761476713185, + "tag": "0001_stale_reaper", + "breakpoints": true } ] } \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 80e474d..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,103 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - -generator client { - provider = "prisma-client-js" - // outputを指定すると動作しない: https://opennext.js.org/cloudflare/howtos/db#schemaprisma - // output = "../app/generated/prisma" - // engineType = "client" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id - name String - email String - emailVerified Boolean @default(false) - image String? - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - sessions Session[] - accounts Account[] - - isAnonymous Boolean? - - Chat Chat[] - - @@unique([email]) - @@map("user") -} - -model Session { - id String @id - expiresAt DateTime - token String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - ipAddress String? - userAgent String? - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([token]) - @@map("session") -} - -model Account { - id String @id - accountId String - providerId String - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - accessToken String? - refreshToken String? - idToken String? - accessTokenExpiresAt DateTime? - refreshTokenExpiresAt DateTime? - scope String? - password String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@map("account") -} - -model Verification { - id String @id - identifier String - value String - expiresAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@map("verification") -} - -// ここまでbetter-authが使う - -model Chat { - chatId String @id @default(uuid()) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - docsId String - sectionId String - createdAt DateTime @default(now()) - - messages Message[] -} - -model Message { - id String @id @default(uuid()) - chatId String - chat Chat @relation(fields: [chatId], references: [chatId], onDelete: Cascade) - role String - content String - createdAt DateTime @default(now()) -} From f2a9980a3ff6ae21abf30690b5450dae838c1df0 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:31:52 +0900 Subject: [PATCH 5/8] =?UTF-8?q?prisma=E3=81=AE=E3=83=91=E3=83=83=E3=82=B1?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E5=89=8A=E9=99=A4=20&=20package-loc?= =?UTF-8?q?k=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 652 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 3 - 2 files changed, 609 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1df0fd0..ca69af9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,6 @@ "@fontsource-variable/noto-sans-jp": "^5.2.6", "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.7.1", - "@prisma/adapter-neon": "^6.18.0", - "@prisma/adapter-pg": "^6.18.0", - "@prisma/client": "^6.18.0", "@xterm/addon-fit": "^0.11.0-beta.115", "@xterm/xterm": "^5.6.0-beta.115", "ace-builds": "^1.43.2", @@ -23,7 +20,9 @@ "chalk": "^5.5.0", "clsx": "^2.1.1", "dotenv": "^17.2.3", + "drizzle-orm": "^0.44.7", "next": "<15.5", + "pg": "^8.16.3", "prismjs": "^1.30.0", "pyodide": "^0.28.1", "react": "19.1.0", @@ -39,11 +38,13 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.15.5", "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", "daisyui": "^5.0.50", + "drizzle-kit": "^0.31.5", "eslint": "^9", "eslint-config-next": "<15.4", "prettier": "^3.6.2", @@ -7860,6 +7861,13 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@ecies/ciphers": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.4.tgz", @@ -7907,6 +7915,442 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -9124,6 +9568,8 @@ "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.0.2.tgz", "integrity": "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" @@ -9137,6 +9583,8 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -9748,34 +10196,14 @@ "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", "license": "MIT" }, - "node_modules/@prisma/adapter-neon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-6.18.0.tgz", - "integrity": "sha512-U97AnsRVEj7nHBujGM2GFD8QZUafdSdC98FItu4TRywzazWb5JaHi/tyfxTW5PthDfXZEyjzE8FaD/9HMi6h8g==", - "license": "Apache-2.0", - "dependencies": { - "@neondatabase/serverless": ">0.6.0 <2", - "@prisma/driver-adapter-utils": "6.18.0", - "postgres-array": "3.0.4" - } - }, - "node_modules/@prisma/adapter-pg": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-6.18.0.tgz", - "integrity": "sha512-eBtBOL1z2Sh0Rc2hwa4dmwoNeFs5T69tsA62AkhBvnzNfTcr/YfLeJae/wjNmvRttYDPmdiuhqZCRPNY1+8fOA==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/driver-adapter-utils": "6.18.0", - "pg": "^8.11.3", - "postgres-array": "3.0.4" - } - }, "node_modules/@prisma/client": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=18.18" }, @@ -9809,17 +10237,9 @@ "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", + "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/driver-adapter-utils": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-6.18.0.tgz", - "integrity": "sha512-9wgSriEKs4j1ePxlv1/RNfJV9Gu5rzG37Neshg+DfrCcUY3amroERvTjyR04w5J1THdGdOTgGL9VdJcVaKRMmQ==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0" - } - }, "node_modules/@prisma/engines": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", @@ -11768,6 +12188,7 @@ "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -13778,6 +14199,147 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.5", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.5.tgz", + "integrity": "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.7", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz", + "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -14121,6 +14683,19 @@ "@esbuild/win32-x64": "0.25.4" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -19094,15 +19669,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/postgres-bytea": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", diff --git a/package.json b/package.json index 7d5f529..6019c18 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,6 @@ "@fontsource-variable/noto-sans-jp": "^5.2.6", "@google/genai": "^1.21.0", "@opennextjs/cloudflare": "^1.7.1", - "@prisma/adapter-neon": "^6.18.0", - "@prisma/adapter-pg": "^6.18.0", - "@prisma/client": "^6.18.0", "@xterm/addon-fit": "^0.11.0-beta.115", "@xterm/xterm": "^5.6.0-beta.115", "ace-builds": "^1.43.2", From 210501c75616f763db82d9886cd68997e0ef3105 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:38:54 +0900 Subject: [PATCH 6/8] update readme --- README.md | 5 +++++ app/lib/prisma.ts | 16 ---------------- 2 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 app/lib/prisma.ts diff --git a/README.md b/README.md index 0a73c0a..9b77a38 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,11 @@ npm run lint ``` でコードをチェックします。出てくるwarningやerrorはできるだけ直しましょう。 +* データベースのスキーマ(./app/schema/hoge.ts)を編集した場合、 `npx drizzle-kit generate` でmigrationファイルを作成し、 `npx drizzle-kit migrate` でデータベースに反映します。 + * また、mainにマージする際に本番環境のデータベースにもmigrateをする必要があります +* スキーマのファイルを追加した場合は app/lib/drizzle.ts でimportを追加する必要があります(たぶん) +* `npx prisma dev` で立ち上げたデータベースは `npx prisma dev ls` でデータベース名の確認・ `npx prisma dev rm default` で削除ができるらしい + ### 本番環境の場合 上記の環境変数以外に、 diff --git a/app/lib/prisma.ts b/app/lib/prisma.ts deleted file mode 100644 index 6ae1703..0000000 --- a/app/lib/prisma.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getCloudflareContext } from "@opennextjs/cloudflare"; -import { PrismaClient } from "@prisma/client"; -import { PrismaD1 } from "@prisma/adapter-d1"; - -export async function getPrismaClient() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let cloudflareEnv: any; - try { - cloudflareEnv = (await getCloudflareContext({ async: true })).env; - } catch { - // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている - cloudflareEnv = {}; - } - - return new PrismaClient({ adapter: new PrismaD1(cloudflareEnv.my_code_db) }); -} From 34db91f598bdac3ed32ba05d456a1536f2795792 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:39:28 +0900 Subject: [PATCH 7/8] Revert "Revert "Merge pull request #71 from ut-code/auth"" This reverts commit 72fc0f6ae1eaade198ec9e7e73d103c5fbf288d5. --- .gitignore | 2 + README.md | 24 +- app/[docs_id]/chatForm.tsx | 12 +- app/[docs_id]/chatHistory.tsx | 81 +--- app/[docs_id]/page.tsx | 5 +- app/[docs_id]/pageContent.tsx | 14 +- app/accountMenu.tsx | 136 ++++++ app/actions/chatActions.ts | 40 +- app/api/auth/[...all]/route.ts | 4 + app/layout.tsx | 2 + app/lib/auth-client.ts | 7 + app/lib/auth.ts | 38 ++ app/lib/chatHistory.ts | 72 +++ app/lib/prisma.ts | 5 + app/navbar.tsx | 2 + app/sidebar.tsx | 4 +- package-lock.json | 830 ++++++++++++++++++++++++++++++++- package.json | 12 +- prisma.config.ts | 13 + prisma/schema.prisma | 101 ++++ tsconfig.json | 2 +- 21 files changed, 1294 insertions(+), 112 deletions(-) create mode 100644 app/accountMenu.tsx create mode 100644 app/api/auth/[...all]/route.ts create mode 100644 app/lib/auth-client.ts create mode 100644 app/lib/auth.ts create mode 100644 app/lib/chatHistory.ts create mode 100644 app/lib/prisma.ts create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma diff --git a/.gitignore b/.gitignore index efb8a61..485c203 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/app/generated/prisma diff --git a/README.md b/README.md index b4246e8..d58b8a2 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,31 @@ https://my-code.utcode.net npm ci ``` -ルートディレクトリに .env.local という名前のファイルを作成し、Gemini APIキーを設定してください +ルートディレクトリに .env.local という名前のファイルを作成し、以下の内容を記述 ```dotenv -API_KEY="XXXXXXXX" +API_KEY=GeminiAPIキー +BETTER_AUTH_URL=http://localhost:3000 ``` +prismaの開発環境を起動 +(.env にDATABASE_URLが自動的に追加される) +```bash +npx prisma dev +``` +別ターミナルで +```bash +npx prisma db push +``` + +### 本番環境の場合 + +上記の環境変数以外に、 +* BETTER_AUTH_SECRET に任意の文字列 +* DATABASE_URL に本番用のPostgreSQLデータベースURL +* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google +* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github + + ## 開発環境 ```bash diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index 932be11..381e014 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -1,7 +1,6 @@ "use client"; import { useState, FormEvent, useEffect } from "react"; -import { askAI } from "@/app/actions/chatActions"; import useSWR from "swr"; import { getQuestionExample, @@ -10,7 +9,8 @@ import { import { getLanguageName } from "../pagesList"; import { DynamicMarkdownSection } from "./pageContent"; import { useEmbedContext } from "../terminal/embedContext"; -import { ChatMessage, useChatHistoryContext } from "./chatHistory"; +import { useChatHistoryContext } from "./chatHistory"; +import { askAI } from "@/actions/chatActions"; interface ChatFormProps { docs_id: string; @@ -71,8 +71,6 @@ export function ChatForm({ setIsLoading(true); setErrorMessage(null); // Clear previous error message - const userMessage: ChatMessage = { sender: "user", text: inputValue }; - let userQuestion = inputValue; if (!userQuestion && exampleData) { // 質問が空欄なら、質問例を使用 @@ -83,6 +81,7 @@ export function ChatForm({ const result = await askAI({ userQuestion, + docsId: docs_id, documentContent, sectionContent, replOutputs, @@ -90,12 +89,11 @@ export function ChatForm({ execResults, }); - if (result.error) { + if (result.error !== null) { setErrorMessage(result.error); console.log(result.error); } else { - const aiMessage: ChatMessage = { sender: "ai", text: result.response }; - const chatId = addChat(result.targetSectionId, [userMessage, aiMessage]); + addChat(result.chat); // TODO: chatIdが指す対象の回答にフォーカス setInputValue(""); close(); diff --git a/app/[docs_id]/chatHistory.tsx b/app/[docs_id]/chatHistory.tsx index 65d95ce..fc92e5d 100644 --- a/app/[docs_id]/chatHistory.tsx +++ b/app/[docs_id]/chatHistory.tsx @@ -1,5 +1,6 @@ "use client"; +import { ChatWithMessages } from "@/lib/chatHistory"; import { createContext, ReactNode, @@ -8,15 +9,10 @@ import { useState, } from "react"; -export interface ChatMessage { - sender: "user" | "ai" | "error"; - text: string; -} - export interface IChatHistoryContext { - chatHistories: Record>; - addChat: (sectionId: string, messages: ChatMessage[]) => string; - updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void; + chatHistories: ChatWithMessages[]; + addChat: (chat: ChatWithMessages) => void; + // updateChat: (sectionId: string, chatId: string, message: ChatMessage) => void; } const ChatHistoryContext = createContext(null); export function useChatHistoryContext() { @@ -29,65 +25,26 @@ export function useChatHistoryContext() { return context; } -export function ChatHistoryProvider({ children }: { children: ReactNode }) { - const [chatHistories, setChatHistories] = useState< - Record> - >({}); +export function ChatHistoryProvider({ + children, + initialChatHistories, +}: { + children: ReactNode; + initialChatHistories: ChatWithMessages[]; +}) { + const [chatHistories, setChatHistories] = + useState(initialChatHistories); useEffect(() => { - // Load chat histories from localStorage on mount - const chatHistories: Record> = {}; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.startsWith("chat/") && key.split("/").length === 3) { - const savedHistory = localStorage.getItem(key); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, sectionId, chatId] = key.split("/"); - if (savedHistory) { - if (!chatHistories[sectionId]) { - chatHistories[sectionId] = {}; - } - chatHistories[sectionId][chatId] = JSON.parse(savedHistory); - } - } - } - setChatHistories(chatHistories); - }, []); + setChatHistories(initialChatHistories); + }, [initialChatHistories]); - const addChat = (sectionId: string, messages: ChatMessage[]): string => { - const chatId = Date.now().toString(); - const newChatHistories = { ...chatHistories }; - if (!newChatHistories[sectionId]) { - newChatHistories[sectionId] = {}; - } - newChatHistories[sectionId][chatId] = messages; - setChatHistories(newChatHistories); - localStorage.setItem( - `chat/${sectionId}/${chatId}`, - JSON.stringify(messages) - ); - return chatId; - }; - const updateChat = ( - sectionId: string, - chatId: string, - message: ChatMessage - ) => { - const newChatHistories = { ...chatHistories }; - if (newChatHistories[sectionId] && newChatHistories[sectionId][chatId]) { - newChatHistories[sectionId][chatId] = [ - ...newChatHistories[sectionId][chatId], - message, - ]; - setChatHistories(newChatHistories); - localStorage.setItem( - `chat/${sectionId}/${chatId}`, - JSON.stringify(newChatHistories[sectionId][chatId]) - ); - } + const addChat = (chat: ChatWithMessages) => { + // サーバー側で追加された新しいchatをクライアント側にも反映する + setChatHistories([...chatHistories, chat]); }; return ( - + {children} ); diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index 9c89033..7cf15dc 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -6,6 +6,7 @@ import { MarkdownSection, splitMarkdown } from "./splitMarkdown"; import pyodideLock from "pyodide/pyodide-lock.json"; import { PageContent } from "./pageContent"; import { ChatHistoryProvider } from "./chatHistory"; +import { getChat } from "@/lib/chatHistory"; export default async function Page({ params, @@ -44,8 +45,10 @@ export default async function Page({ const splitMdContent: MarkdownSection[] = splitMarkdown(mdContent); + const initialChatHistories = await getChat(docs_id); + return ( - +
{/* 右側に表示するチャット履歴欄 */} - {Object.entries(chatHistories[section.sectionId] ?? {}).map( - ([chatId, messages]) => ( + {chatHistories.filter((c) => c.sectionId === section.sectionId).map( + ({chatId, messages}) => (
(
- +
))} diff --git a/app/accountMenu.tsx b/app/accountMenu.tsx new file mode 100644 index 0000000..c5d0f4c --- /dev/null +++ b/app/accountMenu.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { authClient } from "@/lib/auth-client"; +import { usePathname } from "next/navigation"; +import { useEffect } from "react"; + +export function AutoAnonymousLogin() { + const { data: session, isPending } = authClient.useSession(); + useEffect(() => { + if (!isPending && !session) { + authClient.signIn.anonymous(); + } + }, [isPending, session]); + + return null; +} + +export function AccountMenu() { + const { data: session, isPending } = authClient.useSession(); + const pathname = usePathname(); + + const signout = () => { + if ( + window.confirm( + "ログアウトしますか?\nチャット履歴はこの端末上で見られなくなりますが、再度ログインすることでアクセスできます。" + ) + ) { + authClient.signOut({ + fetchOptions: { + onSuccess: () => window.location.reload(), + }, + }); + } + }; + const signoutFromAnonymous = () => { + if (window.confirm("チャット履歴は削除され、アクセスできなくなります。")) { + authClient.signOut({ + fetchOptions: { + onSuccess: () => window.location.reload(), + }, + }); + } + }; + + if (isPending) { + return
; + } + + if (session && !session.user.isAnonymous) { + return ( +
+ + +
+ ); + } + + return ( +
+ +
    +
  • + ログインすると、チャット履歴を保存し別のデバイスからもアクセスできるようになります。 +
  • +
  • + +
  • +
  • + +
  • + {session?.user && ( + <> +
    +
  • + +
  • + + )} +
+
+ ); +} diff --git a/app/actions/chatActions.ts b/app/actions/chatActions.ts index 7adf58e..87dfa18 100644 --- a/app/actions/chatActions.ts +++ b/app/actions/chatActions.ts @@ -4,15 +4,21 @@ import { generateContent } from "./gemini"; import { DynamicMarkdownSection } from "../[docs_id]/pageContent"; import { ReplCommand, ReplOutput } from "../terminal/repl"; +import { addChat, ChatWithMessages } from "@/lib/chatHistory"; -interface FormState { - response: string; - error: string | null; - targetSectionId: string; -} +type ChatResult = + | { + error: string; + } + | { + error: null; + // サーバー側でデータベースに新しく追加されたチャットデータ + chat: ChatWithMessages; + }; type ChatParams = { userQuestion: string; + docsId: string; documentContent: string; sectionContent: DynamicMarkdownSection[]; replOutputs: Record; @@ -20,7 +26,7 @@ type ChatParams = { execResults: Record; }; -export async function askAI(params: ChatParams): Promise { +export async function askAI(params: ChatParams): Promise { // const parseResult = ChatSchema.safeParse(params); // if (!parseResult.success) { @@ -141,25 +147,21 @@ export async function askAI(params: ChatParams): Promise { if (!text) { throw new Error("AIからの応答が空でした"); } + // TODO: どのセクションへの回答にするかをAIに決めさせる + const targetSectionId = + sectionContent.find((s) => s.inView)?.sectionId || ""; + const newChat = await addChat(params.docsId, targetSectionId, [ + { role: "user", content: userQuestion }, + { role: "ai", content: text }, + ]); return { - response: text, error: null, - // TODO: どのセクションへの回答にするかをAIに決めさせる - targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "", + chat: newChat, }; } catch (error: unknown) { console.error("Error calling Generative AI:", error); - if (error instanceof Error) { - return { - response: "", - error: `AIへのリクエスト中にエラーが発生しました: ${error.message}`, - targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "", - }; - } return { - response: "", - error: "予期せぬエラーが発生しました。", - targetSectionId: sectionContent.find((s) => s.inView)?.sectionId || "", + error: String(error), }; } } diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..fcb1ef0 --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth"; // path to your auth file +import { toNextJsHandler } from "better-auth/next-js"; + +export const { POST, GET } = toNextJsHandler(auth); diff --git a/app/layout.tsx b/app/layout.tsx index 365472f..2589c54 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,6 +8,7 @@ import { ReactNode } from "react"; import { PyodideProvider } from "./terminal/python/pyodide"; import { WandboxProvider } from "./terminal/wandbox/wandbox"; import { EmbedContextProvider } from "./terminal/embedContext"; +import { AutoAnonymousLogin } from "./accountMenu"; export const metadata: Metadata = { title: "Create Next App", @@ -20,6 +21,7 @@ export default function RootLayout({ return ( +
diff --git a/app/lib/auth-client.ts b/app/lib/auth-client.ts new file mode 100644 index 0000000..811590c --- /dev/null +++ b/app/lib/auth-client.ts @@ -0,0 +1,7 @@ +import { anonymousClient } from "better-auth/client/plugins"; +import { createAuthClient } from "better-auth/react"; +export const authClient = createAuthClient({ + /** The base URL of the server (optional if you're using the same domain) */ + // baseURL: "http://localhost:3000" + plugins: [anonymousClient()], +}); diff --git a/app/lib/auth.ts b/app/lib/auth.ts new file mode 100644 index 0000000..1b91e16 --- /dev/null +++ b/app/lib/auth.ts @@ -0,0 +1,38 @@ +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { getCloudflareContext } from "@opennextjs/cloudflare"; +import { anonymous } from "better-auth/plugins"; +import prisma from "./prisma"; +import { migrateChatUser } from "./chatHistory"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let cloudflareEnv: any; +try { + cloudflareEnv = getCloudflareContext().env; +} catch { + // @better-auth/cli generate を実行する際には initOpenNextCloudflareForDev がセットアップされていない環境になっている + cloudflareEnv = {}; +} +export const auth = betterAuth({ + database: prismaAdapter(prisma, { + provider: "postgresql", + }), + plugins: [ + anonymous({ + onLinkAccount: ({ anonymousUser, newUser }) => + migrateChatUser(anonymousUser.user.id, newUser.user.id), + }), + ], + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID ?? cloudflareEnv.GITHUB_CLIENT_ID, + clientSecret: + process.env.GITHUB_CLIENT_SECRET ?? cloudflareEnv.GITHUB_CLIENT_SECRET, + }, + google: { + clientId: process.env.GOOGLE_CLIENT_ID ?? cloudflareEnv.GOOGLE_CLIENT_ID, + clientSecret: + process.env.GOOGLE_CLIENT_SECRET ?? cloudflareEnv.GOOGLE_CLIENT_SECRET, + }, + }, +}); diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts new file mode 100644 index 0000000..a8faf0b --- /dev/null +++ b/app/lib/chatHistory.ts @@ -0,0 +1,72 @@ +import { headers } from "next/headers"; +import { auth } from "./auth"; +import prisma from "./prisma"; + +export interface CreateChatMessage { + role: "user" | "ai" | "error"; + content: string; +} + +export async function addChat( + docsId: string, + sectionId: string, + messages: CreateChatMessage[] +) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session) { + throw new Error("Not authenticated"); + } + + return await prisma.chat.create({ + data: { + userId: session.user.id, + docsId, + sectionId, + messages: { + createMany: { + data: messages, + }, + }, + }, + include: { + messages: true, + }, + }); +} + +export type ChatWithMessages = Awaited>; + +export async function getChat(docsId: string) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session) { + return []; + } + + return await prisma.chat.findMany({ + where: { + userId: session.user.id, + docsId, + }, + include: { + messages: { + orderBy: { + createdAt: "asc", + }, + }, + }, + orderBy: { + createdAt: "asc", + }, + }); +} + +export async function migrateChatUser(oldUserId: string, newUserId: string) { + await prisma.chat.updateMany({ + where: { + userId: oldUserId, + }, + data: { + userId: newUserId, + }, + }); +} diff --git a/app/lib/prisma.ts b/app/lib/prisma.ts new file mode 100644 index 0000000..72dd8ef --- /dev/null +++ b/app/lib/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "../generated/prisma/client"; + +const prisma = new PrismaClient(); +export default prisma; + diff --git a/app/navbar.tsx b/app/navbar.tsx index b599410..2955d59 100644 --- a/app/navbar.tsx +++ b/app/navbar.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { AccountMenu } from "./accountMenu"; import { ThemeToggle } from "./[docs_id]/themeToggle"; export function Navbar() { return ( @@ -32,6 +33,7 @@ export function Navbar() { {/* サイドバーが常時表示されている場合のみ */} my.code(); +
); diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 8e263ab..1afa75b 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -4,6 +4,7 @@ import { usePathname } from "next/navigation"; import useSWR, { Fetcher } from "swr"; import { splitMarkdown } from "./[docs_id]/splitMarkdown"; import { pagesList } from "./pagesList"; +import { AccountMenu } from "./accountMenu"; import { ThemeToggle } from "./[docs_id]/themeToggle"; const fetcher: Fetcher = (url) => @@ -20,12 +21,13 @@ export function Sidebar() { return (
{/* todo: 背景色ほんとにこれでいい? */} -

+

{/* サイドバーが常時表示されている場合のみ */} my.code(); +