CloudflareD1へNode.jsのPrismaからアクセスする
Prisma の D1 対応
下記の通り Prisma で Cloudflare の D1 がサポートされました。
https://www.prisma.io/blog/build-applications-at-the-edge-with-prisma-orm-and-cloudflare-d1-preview
ということで早速試してみます。
ただ、Prisma を使って 普通に Cloudflare 上から D1 にアクセスするのは誰でも簡単にできるので、世界で誰もやってなさそうな Node.js からアクセスする方法紹介します。
Worker の作成
まずは Cloudflare 上に D1 にアクセスするための Worker を作成します。これは Prisma をリモートアクセスさせるための Proxy です。QueryEngine だけ Workers 上で動かし、その他の部分は Node.js で行います。
https://github.com/SoraKumo001/prisma-accelerate-workers-d1
このプログラムを Workers にデプロイし、d1_databases をバインディング、シークレットを設定します。その後、以下のコマンドで API キーを作成します。これで準備完了です。
1npx prisma-accelerate-local -s SECRET -m DB
ここで作った API キーと Workers の URL を使えば、Node.js 上の Prisma から D1 にアクセス出来るようになります。
余談ですが、prisma@5.12.0の D1 などの Adapter には以下の PR で混入した Node.js のutilがバンドルされてしまう問題があって、本来いらないはずの Node.js のランタイムを必要とします。
https://github.com/prisma/prisma/pull/23013
これがどういうことかというと、最初の記事で紹介されている Workers の設定のnodejs_compat(Workers 上のランタイムで 0 サイズ)では動かず、node_compat(Polyfill で容量を食う)の方が必要となります。これを回避する方法は前述の github の README に記載しています。
Node.js からのアクセス
https://github.com/SoraKumo001/prisma-d1-test
prisma/schema.prisma
directUrl はマイグレーションファイルの生成に必要なのでダミーで指定しておきますが、実際に中にデータを入れることはありません。
1generator client {2 provider = "prisma-client-js"3}45datasource db {6 provider = "sqlite"7 url = env("DATABASE_URL")8 directUrl = "file:./dev.db"9}1011model Role{12 id String @id @default(uuid())13 name String @unique14 users User[]15 createdAt DateTime @default(now())16 updatedAt DateTime @updatedAt17}1819model User {20 id String @id @default(uuid())21 email String @unique22 name String @default("User")23 posts Post[]24 roles Role[]25 createdAt DateTime @default(now())26 updatedAt DateTime @updatedAt27}2829model Post {30 id String @id @default(uuid())31 published Boolean @default(false)32 title String @default("New Post")33 content String @default("")34 author User? @relation(fields: [authorId], references: [id])35 authorId String?36 categories Category[]37 createdAt DateTime @default(now())38 updatedAt DateTime @updatedAt39 publishedAt DateTime @default(now())40}4142model Category {43 id String @id @default(uuid())44 name String45 posts Post[]46 createdAt DateTime @default(now())47 updatedAt DateTime @updatedAt48}
tools/migrate.ts
D1 に対して、リモートでマイグレーションを行うために適当に作ったコードです。Prisma のマイグレーションファイルを一つのフォルダに集約し、DB 名から UUID を取得して、その UUID を使ってマイグレーションを行います。手動でやると面倒なので、これを使って自動化します。
1import fs from "fs";2import { exec } from "child_process";3import { promisify } from "util";4import path from "path";5import os from "os";67const srcPath = "prisma/migrations";8const distPath = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-migrations"));9const DB = process.env.DB_NAME!;1011const execAsync = promisify(exec);1213type D1List = {14 uuid: string;15 name: string;16 version: string;17 created_at: string;18};1920export const getD1List = async () => {21 const list = await execAsync("wrangler d1 list --json").then(22 ({ stdout }) => JSON.parse(stdout) as D1List[]23 );24 return list;25};2627export const migrationD1 = async (dbName: string, dir: string) => {28 return execAsync(29 `wrangler d1 migrations apply --remote ${dbName} -c ${dir}/wrangler.toml`30 ).then(({ stdout, stderr }) => stderr || stdout);31};3233const main = async () => {34 if (fs.existsSync(distPath)) {35 fs.rmSync(distPath, { recursive: true });36 }3738 fs.mkdirSync(distPath, { recursive: true });39 const migrations = fs.readdirSync(srcPath, { withFileTypes: true });40 migrations41 .filter((v) => {42 return v.isDirectory();43 })44 .sort((a, b) => (a.name < b.name ? -1 : 1))45 .forEach(({ name }) => {46 const sql = fs.readFileSync(`${srcPath}/${name}/migration.sql`, "utf8");47 fs.writeFileSync(`${distPath}/${name}.sql`, sql);48 });4950 const list = await getD1List();51 const uuid = list.find((v) => v.name === DB)?.uuid;52 if (uuid) {53 fs.writeFileSync(54 `${distPath}/wrangler.toml`,55 `[[d1_databases]]56binding = "DB"57database_name = "${DB}"58database_id ="${uuid}"59migrations_dir = "./"`60 );61 }62 const result = await migrationD1(DB, distPath);63 console.log(result);64 fs.rmSync(distPath, { recursive: true });65};6667main();
.env
Workers の URL と API キーを設定します。また、マイグレーションを行うための DB 名を設定します。こちらは先程のスクリプトで利用します。
1# Address of installed Workers2DATABASE_URL=prisma://xxxxx.workers.dev?api_key=xxxxxx3# For Migration4DB_NAME=xxxx
package.json
yarn prisma:migrateを実行すると、Prisma がマイグレーションファイルを作成し、それを Workers に送信してマイグレーションを行います。next-execは環境変数を設定してから実行するためのツールです。
1{2 "name": "prisma-d1-test",3 "version": "1.0.0",4 "main": "index.js",5 "license": "MIT",6 "scripts": {7 "start": "next-exec -- tsx src",8 "prisma:migrate": "prisma migrate dev && next-exec -- tsx tools/migrate.ts && prisma generate --no-engine"9 },10 "dependencies": {11 "@prisma/client": "^5.12.0"12 },13 "devDependencies": {14 "@types/node": "^20.12.3",15 "next-exec": "^1.0.0",16 "prisma": "^5.12.0",17 "tsx": "^4.7.1",18 "typescript": "^5.4.3",19 "wrangler": "^3.44.0"20 }21}
src/index.ts
こちらがメインのコードです。Prisma を使って D1 にアクセスしデータの読み書きをしています。通常の Node.js のコードと全く代わりません。DATABASE_URL にprisma://xxxxを指定している時点で、D1 の設定作業は終わっています。
1import { PrismaClient } from "@prisma/client";23const formatNumber = (num: number) => {4 return num.toString().padStart(2, "0");5};67const main = async () => {8 const prisma = new PrismaClient();9 const roles = await prisma.role.count().then(async (count) => {10 if (!count) {11 return Promise.all(12 [13 {14 name: "ADMIN",15 },16 { name: "USER" },17 ].map((data) => {18 return prisma.role.create({19 data,20 });21 })22 );23 }24 return prisma.role.findMany();25 });2627 if (roles === undefined) {28 throw new Error("roles is undefined");29 }30 const ROLES = Object.fromEntries(roles.map((v) => [v.name, v.id] as const));3132 const users = await prisma.user.count().then(async (count) => {33 if (!count) {34 return Promise.all(35 [36 {37 name: "admin",38 email: "admin@example.com",39 roles: {40 connect: [41 {42 id: ROLES["ADMIN"],43 },44 { id: ROLES["USER"] },45 ],46 },47 },48 {49 name: "example",50 email: "example@example.com",51 roles: { connect: [{ id: ROLES["USER"] }] },52 },53 ].map((data) => {54 return prisma.user.create({55 data,56 });57 })58 );59 }60 return prisma.user.findMany();61 });6263 // add category64 const categories = await prisma.category.count().then(async (count) => {65 if (!count) {66 return Promise.all(67 Array(10)68 .fill(0)69 .map((_, i) => ({ name: `Category${formatNumber(i + 1)}` }))70 .map((data) =>71 prisma.category.create({72 data,73 })74 )75 );76 }77 return prisma.category.findMany();78 });7980 // add post81 await prisma.post.count().then(async (count) => {82 if (!count) {83 for (let i = 0; i < 30; i++) {84 await prisma.post.create({85 data: {86 title: `Post${formatNumber(i + 1)}`,87 content: `Post${formatNumber(i + 1)} content`,88 authorId: users[1].id,89 published: i % 4 !== 0,90 categories: {91 connect: [92 { id: categories[i % 2].id },93 { id: categories[i % 10].id },94 ],95 },96 },97 });98 }99 }100 });101102 console.log(103 JSON.stringify(104 await prisma.post.findMany({105 include: { author: true, categories: true },106 }),107 undefined,108 2109 )110 );111};112main();
まとめ
Prisma で D1 がサポートされたので、Node.js からアクセスする方法を紹介しました。これによって Cloudflare の D1 が、サーバーレスな DB となります。