0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-22 11:17:33 +01:00

Feature/example blog (#39)

* Add blog CRUD example

* Fixed some

* Exclude example test
This commit is contained in:
Yusuke Wada 2022-01-10 23:30:19 +09:00 committed by GitHub
parent 4a795dab88
commit 511c0ebd7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 287 additions and 4 deletions

5
example/blog/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
dist
node_modules
worker
package-lock.json
yarn.lock

13
example/blog/README.md Normal file
View File

@ -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`

View File

@ -0,0 +1,7 @@
module.exports = {
testEnvironment: 'miniflare', // ✨
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
}

30
example/blog/package.json Normal file
View File

@ -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 <yusuke@kamawada.com> (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"
}
}

View File

@ -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)
})
})

View File

@ -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 })
}

14
example/blog/src/index.ts Normal file
View File

@ -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()

53
example/blog/src/model.ts Normal file
View File

@ -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
}

View File

@ -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",
]
}

View File

@ -0,0 +1,4 @@
module.exports = {
target: 'webworker',
entry: './dist/index.js',
}

View File

@ -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"

View File

@ -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'],
}