├── .eslintrc.cjs ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── drizzle.config.ts ├── drizzle └── migrations │ └── .gitkeep ├── package.json ├── src ├── index.ts └── schema.ts ├── test ├── api.test.ts ├── apply-migrations.ts └── env.d.ts ├── tsconfig.json ├── vitest.config.ts └── wrangler.example.toml /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@hono/eslint-config'] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | 6 | # Change them to your taste: 7 | package-lock.json 8 | yarn.lock 9 | pnpm-lock.yaml 10 | bun.lockb 11 | 12 | wrangler.toml 13 | drizzle/migrations/* 14 | !drizzle/migrations/.gitkeep -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | }, 11 | "typescript.tsdk": "node_modules/typescript/lib" 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing a D1 Application with Types 2 | 3 | This project shows how we test an application built on Cloudflare Workers with Cloudflare D1. 4 | 5 | ## Stack 6 | 7 | - Hono - Web framework 8 | - Cloudflare D1 - SQL database 9 | - Drizzle ORM - ORM Mapper 10 | - Zod - Validator 11 | - Vitest - Test runner 12 | - `@cloudflare/vitest-pool-workers` - Simulator for Cloudflare Environment 13 | 14 | ## Demo 15 | 16 | https://github.com/yusukebe/testing-d1-app-with-types/assets/10682/7589e8b6-9035-4714-9de7-6af15303de4d 17 | 18 | ## Author 19 | 20 | Yusuke Wada 21 | 22 | ## License 23 | 24 | MIT 25 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit' 2 | 3 | export default { 4 | schema: './src/schema.ts', 5 | out: './drizzle/migrations', 6 | driver: 'd1', 7 | dbCredentials: { 8 | wranglerConfigPath: 'wrangler.toml', 9 | dbName: 'd1-app' 10 | } 11 | } satisfies Config 12 | -------------------------------------------------------------------------------- /drizzle/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yusukebe/testing-d1-app-with-types/da9a37cd6bd065a77aa6e1ac71763c48d9a5cbfa/drizzle/migrations/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-d1-app-with-types", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "wrangler dev src/index.ts", 6 | "test": "vitest", 7 | "migration:generate": "drizzle-kit generate:sqlite", 8 | "migration:apply": "wrangler d1 migrations apply d1-app --local", 9 | "migration:apply:remote": "wrangler d1 migrations apply d1-app --remote", 10 | "deploy": "wrangler deploy src/index.ts" 11 | }, 12 | "dependencies": { 13 | "drizzle-orm": "^0.30.8", 14 | "drizzle-zod": "^0.5.1", 15 | "hono": "^4.2.5", 16 | "zod": "^3.22.4" 17 | }, 18 | "devDependencies": { 19 | "@cloudflare/vitest-pool-workers": "^0.2.0", 20 | "@cloudflare/workers-types": "^4.20240403.0", 21 | "@hono/eslint-config": "^0.0.4", 22 | "@hono/zod-validator": "^0.2.1", 23 | "drizzle-kit": "^0.20.14", 24 | "vitest": "1.3.0", 25 | "wrangler": "^3.47.0" 26 | } 27 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { zValidator } from '@hono/zod-validator' 2 | import { eq } from 'drizzle-orm' 3 | import { drizzle, type DrizzleD1Database } from 'drizzle-orm/d1' 4 | import { createInsertSchema } from 'drizzle-zod' 5 | import { Hono } from 'hono' 6 | import { showRoutes } from 'hono/dev' 7 | import { z } from 'zod' 8 | import { posts } from './schema' 9 | 10 | type Env = { 11 | Bindings: { 12 | DB: D1Database 13 | } 14 | Variables: { 15 | db: DrizzleD1Database 16 | } 17 | } 18 | 19 | const app = new Hono().basePath('/api') 20 | 21 | app.use(async (c, next) => { 22 | c.set('db', drizzle(c.env.DB)) 23 | await next() 24 | }) 25 | 26 | const insertSchema = createInsertSchema(posts) 27 | 28 | const routes = app 29 | .get('/', async (c) => { 30 | const results = await c.var.db.select().from(posts).all() 31 | return c.json(results) 32 | }) 33 | .post('/', zValidator('form', insertSchema), async (c) => { 34 | const data = c.req.valid('form') 35 | const results = await c.var.db.insert(posts).values(data).returning() 36 | return c.json(results[0]) 37 | }) 38 | .get( 39 | '/:id', 40 | zValidator( 41 | 'param', 42 | z.object({ 43 | id: z.string() 44 | }) 45 | ), 46 | async (c) => { 47 | const { id } = c.req.valid('param') 48 | const results = await c.var.db 49 | .select() 50 | .from(posts) 51 | .where(eq(posts.id, Number(id))) 52 | return c.json(results[0]) 53 | } 54 | ) 55 | 56 | showRoutes(app) 57 | 58 | export default routes 59 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm' 2 | import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core' 3 | 4 | export const posts = sqliteTable('posts', { 5 | id: integer('id').primaryKey({ autoIncrement: true }), 6 | text: text('title').notNull(), 7 | createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(strftime('%s', 'now'))`) 8 | }) 9 | -------------------------------------------------------------------------------- /test/api.test.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'cloudflare:test' 2 | import { testClient } from 'hono/testing' 3 | import api from '../src' 4 | 5 | describe('Test the D1 application', () => { 6 | const client = testClient(api, env) 7 | 8 | const text = 'My first Post' 9 | let id = '' 10 | 11 | it('Should create a new post - POST /', async () => { 12 | const res = await client.api.$post({ 13 | form: { 14 | text 15 | } 16 | }) 17 | expect(res.status).toBe(200) 18 | const data = await res.json() 19 | expect(data.id).not.toBeUndefined() 20 | id = data.id 21 | }) 22 | 23 | it('Should return a single post - GET /:id', async () => { 24 | const res = await client.api[':id'].$get({ 25 | param: { 26 | id 27 | } 28 | }) 29 | expect(res.status).toBe(200) 30 | const data = await res.json() 31 | expect(data.id).toBe(id) 32 | expect(data.text).toBe(text) 33 | expect(data.createdAt).not.toBeUndefined() 34 | }) 35 | 36 | it('Should return all posts - GET /', async () => { 37 | const res = await client.api.$get() 38 | expect(res.status).toBe(200) 39 | const data = await res.json() 40 | expect(data.length).toBe(1) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/apply-migrations.ts: -------------------------------------------------------------------------------- 1 | import { applyD1Migrations, env } from 'cloudflare:test' 2 | 3 | await applyD1Migrations(env.DB, env.TEST_MIGRATIONS) 4 | -------------------------------------------------------------------------------- /test/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cloudflare:test' { 2 | interface ProvidedEnv { 3 | DB: D1Database 4 | TEST_MIGRATIONS: D1Migration[] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "lib": [ 8 | "ESNext", 9 | "DOM" 10 | ], 11 | "types": [ 12 | "vite/client", 13 | "vitest/globals", 14 | "@cloudflare/workers-types", 15 | "@cloudflare/vitest-pool-workers" 16 | ], 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "hono/jsx", 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": [ 22 | "./src/*" 23 | ] 24 | } 25 | }, 26 | "include": [ 27 | "src/**/*.ts", 28 | "src/**/*.tsx", 29 | "test/**/*.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineWorkersProject, readD1Migrations } from '@cloudflare/vitest-pool-workers/config' 3 | 4 | export default defineWorkersProject(async () => { 5 | const migrationsPath = path.join(__dirname, 'drizzle/migrations') 6 | const migrations = await readD1Migrations(migrationsPath) 7 | return { 8 | test: { 9 | setupFiles: ['./test/apply-migrations.ts'], 10 | globals: true, 11 | poolOptions: { 12 | workers: { 13 | singleWorker: true, 14 | isolatedStorage: false, 15 | miniflare: { 16 | compatibilityFlags: ['nodejs_compat'], 17 | compatibilityDate: '2024-04-01', 18 | d1Databases: ['DB'], 19 | bindings: { TEST_MIGRATIONS: migrations } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /wrangler.example.toml: -------------------------------------------------------------------------------- 1 | name = "testing-d1-app-with-types" 2 | compatibility_date = "2024-04-01" 3 | compatibility_flags = ["nodejs_compat"] 4 | 5 | [[d1_databases]] 6 | binding = "DB" 7 | database_name = "d1-app" 8 | database_id = "your-database-id" 9 | migrations_dir = "drizzle/migrations" 10 | --------------------------------------------------------------------------------