diff --git a/example/blog/.gitignore b/example/blog/.gitignore new file mode 100644 index 00000000..1a5d11b4 --- /dev/null +++ b/example/blog/.gitignore @@ -0,0 +1,5 @@ +dist +node_modules +worker +package-lock.json +yarn.lock \ No newline at end of file diff --git a/example/blog/README.md b/example/blog/README.md new file mode 100644 index 00000000..b43d37b4 --- /dev/null +++ b/example/blog/README.md @@ -0,0 +1,13 @@ +# hono-example-blog + +- CRUD +- Using TypeScript +- Test with Jest `miniflare environment` + +## Endpoints + +- `GET /posts` +- `POST /posts` +- `GET /posts/:id` +- `PUT /posts/:id` +- `DELETE /posts/:id` diff --git a/example/blog/jest.config.js b/example/blog/jest.config.js new file mode 100644 index 00000000..831b933b --- /dev/null +++ b/example/blog/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testEnvironment: 'miniflare', // ✨ + testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'], + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, +} diff --git a/example/blog/package.json b/example/blog/package.json new file mode 100644 index 00000000..c716d952 --- /dev/null +++ b/example/blog/package.json @@ -0,0 +1,30 @@ +{ + "name": "hono-example-blog", + "version": "0.0.1", + "description": "", + "main": "dist/worker.js", + "scripts": { + "test": "jest --verbose", + "build": "rimraf tsc && wrangler build", + "dev": "tsc && npm-run-all --parallel dev:*", + "dev:tsc": "tsc --watch", + "dev:wrangler": "wrangler dev" + }, + "author": "Yusuke Wada (https://github.com/yusukebe)", + "license": "MIT", + "dependencies": { + "hono": "^0.0.12" + }, + "devDependencies": { + "@cloudflare/workers-types": "^3.3.0", + "@cloudflare/wrangler": "^1.19.7", + "@types/jest": "^27.4.0", + "jest": "^27.4.7", + "jest-environment-miniflare": "^2.0.0", + "npm-run-all": "^4.1.5", + "ts-jest": "^27.1.2", + "typescript": "^4.5.4", + "webpack": "^5.65.0", + "webpack-cli": "^4.9.1" + } +} \ No newline at end of file diff --git a/example/blog/src/controller.test.ts b/example/blog/src/controller.test.ts new file mode 100644 index 00000000..dad39ed9 --- /dev/null +++ b/example/blog/src/controller.test.ts @@ -0,0 +1,76 @@ +import { app } from './index' +import type { Post } from './model' + +describe('Root', () => { + it('GET /', async () => { + const req = new Request('http://localhost/') + const res = await app.dispatch(req) + expect(res.status).toBe(200) + const body = (await res.json()) as any + expect(body['message']).toBe('Hello') + }) +}) + +describe('Blog API', () => { + it('List', async () => { + const req = new Request('http://localhost/posts') + const res = await app.dispatch(req) + expect(res.status).toBe(200) + const body = (await res.json()) as any + expect(body['posts']).not.toBeUndefined() + expect(body['posts'].length).toBe(0) + }) + + it('CRUD', async () => { + let payload = JSON.stringify({ title: 'Morning', body: 'Good Morning' }) + let req = new Request('http://localhost/posts', { method: 'POST', body: payload }) + let res = await app.dispatch(req) + expect(res.status).toBe(201) + let body = (await res.json()) as any + const newPost = body['post'] as Post + expect(newPost.title).toBe('Morning') + expect(newPost.body).toBe('Good Morning') + + req = new Request('https://localhost/posts') + res = await app.dispatch(req) + expect(res.status).toBe(200) + body = (await res.json()) as any + expect(body['posts'].length).toBe(1) + + req = new Request(`https://localhost/posts/${newPost.id}`) + res = await app.dispatch(req) + expect(res.status).toBe(200) + body = (await res.json()) as any + let post = body['post'] as Post + expect(post.id).toBe(newPost.id) + expect(post.title).toBe('Morning') + + payload = JSON.stringify({ title: 'Night', body: 'Good Night' }) + req = new Request(`https://localhost/posts/${post.id}`, { + method: 'PUT', + body: payload, + }) + res = await app.dispatch(req) + expect(res.status).toBe(200) + body = (await res.json()) as any + expect(body['ok']).toBeTruthy() + + req = new Request(`https://localhost/posts/${post.id}`) + res = await app.dispatch(req) + expect(res.status).toBe(200) + body = (await res.json()) as any + post = body['post'] as Post + expect(post.title).toBe('Night') + expect(post.body).toBe('Good Night') + + req = new Request(`https://localhost/posts/${post.id}`, { method: 'DELETE' }) + res = await app.dispatch(req) + expect(res.status).toBe(200) + body = (await res.json()) as any + expect(body['ok']).toBeTruthy() + + req = new Request(`https://localhost/posts/${post.id}`) + res = await app.dispatch(req) + expect(res.status).toBe(404) + }) +}) diff --git a/example/blog/src/controller.ts b/example/blog/src/controller.ts new file mode 100644 index 00000000..6d9b8c5e --- /dev/null +++ b/example/blog/src/controller.ts @@ -0,0 +1,51 @@ +import type { Handler } from '../../../dist' +import * as Model from './model' + +export const root: Handler = (c) => { + return c.json({ message: 'Hello' }) +} + +export const list: Handler = (c) => { + const posts = Model.getPosts() + return c.json({ posts: posts, ok: true }) +} + +export const create: Handler = async (c) => { + const param = (await c.req.json()) as Model.Param + const newPost = Model.createPost(param) + if (!newPost) { + // Is 200 suitable? + return c.json({ error: 'Can not create new post', ok: false }, 200) + } + return c.json({ post: newPost, ok: true }, 201) +} + +export const show: Handler = async (c) => { + const id = c.req.params('id') + const post = Model.getPost(id) + if (!post) { + return c.json({ error: 'Not Found', ok: false }, 404) + } + return c.json({ post: post, ok: true }) +} + +export const update: Handler = async (c) => { + const id = c.req.params('id') + if (!Model.getPost(id)) { + // 204 No Content + return c.json({ ok: false }, 204) + } + const param = (await c.req.json()) as Model.Param + const success = Model.updatePost(id, param) + return c.json({ ok: success }) +} + +export const destroy: Handler = async (c) => { + const id = c.req.params('id') + if (!Model.getPost(id)) { + // 204 No Content + return c.json({ ok: false }, 204) + } + const success = Model.deletePost(id) + return c.json({ ok: success }) +} diff --git a/example/blog/src/index.ts b/example/blog/src/index.ts new file mode 100644 index 00000000..3c6401d7 --- /dev/null +++ b/example/blog/src/index.ts @@ -0,0 +1,14 @@ +import { Hono } from '../../../dist' +import * as Controller from './controller' + +export const app = new Hono() + +app.get('/', Controller.root) + +app.get('/posts', Controller.list) +app.post('/posts', Controller.create) +app.get('/posts/:id', Controller.show) +app.put('/posts/:id', Controller.update) +app.delete('/posts/:id', Controller.destroy) + +app.fire() diff --git a/example/blog/src/model.ts b/example/blog/src/model.ts new file mode 100644 index 00000000..604b50a1 --- /dev/null +++ b/example/blog/src/model.ts @@ -0,0 +1,53 @@ +declare global { + interface Crypto { + randomUUID(): string + } +} + +export interface Post { + id: string + title: string + body: string +} + +export type Param = { + title: string + body: string +} + +const posts: { [key: string]: Post } = {} + +export const getPosts = (): Post[] => { + return Object.values(posts) +} + +export const getPost = (id: string): Post | undefined => { + return posts[id] +} + +export const createPost = (param: Param): Post | undefined => { + if (!(param.title && param.body)) return + const id = crypto.randomUUID() + const newPost: Post = { id: id, title: param.title, body: param.body } + posts[id] = newPost + return newPost +} + +export const updatePost = (id: string, param: Param): boolean => { + if (!(param.title && param.body)) return false + const post = posts[id] + if (post) { + post.title = param.title + post.body = param.body + return true + } + return false +} + +export const deletePost = (id: string): boolean => { + if (posts[id]) { + delete posts[id] + return true + } + return false +} diff --git a/example/blog/tsconfig.json b/example/blog/tsconfig.json new file mode 100644 index 00000000..b7e84ea3 --- /dev/null +++ b/example/blog/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "rootDir": "src", + "outDir": "./dist", + "allowJs": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [ + "@cloudflare/workers-types", + "@types/jest", + "@types/service-worker-mock" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/example/blog/webpack.config.js b/example/blog/webpack.config.js new file mode 100644 index 00000000..9a634463 --- /dev/null +++ b/example/blog/webpack.config.js @@ -0,0 +1,4 @@ +module.exports = { + target: 'webworker', + entry: './dist/index.js', +} diff --git a/example/blog/wrangler.toml b/example/blog/wrangler.toml new file mode 100644 index 00000000..1fdae065 --- /dev/null +++ b/example/blog/wrangler.toml @@ -0,0 +1,9 @@ +name = "hono-example-blog" +type = "webpack" +route = '' +zone_id = '' +usage_model = '' +compatibility_flags = [] +workers_dev = true +compatibility_date = "2022-01-09" +webpack_config = "webpack.config.js" \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 59dbbe9f..eda6924d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,7 @@ module.exports = { - testMatch: [ - '**/test/**/*.+(ts|tsx|js)', - '**/src/**/(*.)+(spec|test).+(ts|tsx|js)', - ], + testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, + testPathIgnorePatterns: ['./example'], }