@shimehituzi - 3M
main.ts
1import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'2import { swaggerUI } from '@hono/swagger-ui'34const Todo = z.object({5 id: z.number().int().positive(),6 title: z.string().min(1),7 completed: z.boolean(),8})910type Todo = z.infer<typeof Todo>1112const kv = await Deno.openKv(':memory:')1314const CreateTodoSchema = z.object({15 title: z.string().min(1),16})1718const UpdateTodoSchema = z.object({19 title: z.string().min(1).optional(),20 completed: z.boolean().optional(),21})2223const ParamsSchema = z.object({24 id: z.string().regex(/^\d+$/).transform(Number).pipe(25 z.number().int().positive(),26 ),27})2829const ErrorSchema = z.object({30 error: z.string(),31})3233async function createTodo(title: string): Promise<Todo> {34 while (true) {35 const counterKey = ['todo_counter']36 const { value: currentId, versionstamp } = await kv.get<number>(counterKey)37 const nextId = (currentId ?? 0) + 13839 const newTodo: Todo = {40 id: nextId,41 title,42 completed: false,43 }4445 const res = await kv.atomic()46 .check({ key: counterKey, versionstamp })47 .set(counterKey, nextId)48 .set(['todo', nextId], newTodo)49 .commit()5051 if (res.ok) {52 return newTodo53 }54 // If the transaction fails, the loop will retry55 }56}5758async function getAllTodos(): Promise<Todo[]> {59 const todos: Todo[] = []60 const iter = kv.list<Todo>({ prefix: ['todo'] })61 for await (const entry of iter) {62 if (typeof entry.key[1] === 'number') { // Ensure we're only getting todo entries63 todos.push(entry.value)64 }65 }66 return todos67}6869async function updateTodo(70 id: number,71 updates: Partial<Todo>,72): Promise<Todo | null> {73 while (true) {74 const { value: existingTodo, versionstamp } = await kv.get<Todo>([75 'todo',76 id,77 ])78 if (!existingTodo) return null7980 const updatedTodo: Todo = {81 ...existingTodo,82 ...updates,83 }8485 const res = await kv.atomic()86 .check({ key: ['todo', id], versionstamp })87 .set(['todo', id], updatedTodo)88 .commit()8990 if (res.ok) {91 return updatedTodo92 }93 // If the transaction fails, the loop will retry94 }95}9697async function deleteTodo(id: number): Promise<boolean> {98 while (true) {99 const { versionstamp } = await kv.get(['todo', id])100101 if (!versionstamp) {102 // Todo doesn't exist103 return false104 }105106 const res = await kv.atomic()107 .check({ key: ['todo', id], versionstamp })108 .delete(['todo', id])109 .commit()110111 if (res.ok) {112 return true113 }114 // If the transaction fails, the loop will retry115 }116}117118export const app = new OpenAPIHono()119 .openapi(120 createRoute({121 method: 'get',122 path: '/',123 responses: {124 200: {125 content: {126 'text/plain': {127 schema: z.string(),128 },129 },130 description: 'Welcome message',131 },132 },133 }),134 (c) => {135 return c.text('Hono + Deno TODO API')136 },137 )138 .openapi(139 createRoute({140 method: 'get',141 path: '/todos',142 responses: {143 200: {144 content: {145 'application/json': {146 schema: z.array(Todo),147 },148 },149 description: 'List of todos',150 },151 },152 }),153 async (c) => {154 const todos = await getAllTodos()155 return c.json(todos, 200)156 },157 )158 .openapi(159 createRoute({160 method: 'post',161 path: '/todos',162 request: {163 body: {164 content: {165 'application/json': {166 schema: CreateTodoSchema,167 },168 },169 },170 },171 responses: {172 201: {173 content: {174 'application/json': {175 schema: Todo,176 },177 },178 description: 'Create a new todo',179 },180 },181 }),182 async (c) => {183 const { title } = c.req.valid('json')184 const newTodo = await createTodo(title)185 return c.json(newTodo, 201)186 },187 )188 .openapi(189 createRoute({190 method: 'put',191 path: '/todos/{id}',192 request: {193 params: ParamsSchema,194 body: {195 content: {196 'application/json': {197 schema: UpdateTodoSchema,198 },199 },200 },201 },202 responses: {203 200: {204 content: {205 'application/json': {206 schema: Todo,207 },208 },209 description: 'Update a todo',210 },211 404: {212 content: {213 'application/json': {214 schema: ErrorSchema,215 },216 },217 description: 'Todo not found',218 },219 },220 }),221 async (c) => {222 const { id } = c.req.valid('param')223 const updates = c.req.valid('json')224 const updatedTodo = await updateTodo(id, updates)225 if (!updatedTodo) {226 return c.json({ error: 'Todo not found' }, 404)227 }228 return c.json(updatedTodo, 200)229 },230 )231 .openapi(232 createRoute({233 method: 'delete',234 path: '/todos/{id}',235 request: {236 params: ParamsSchema,237 },238 responses: {239 200: {240 content: {241 'application/json': {242 schema: z.object({243 message: z.string(),244 }),245 },246 },247 description: 'Delete a todo',248 },249 404: {250 content: {251 'application/json': {252 schema: ErrorSchema,253 },254 },255 description: 'Todo not found',256 },257 },258 }),259 async (c) => {260 const { id } = c.req.valid('param')261 const deleted = await deleteTodo(id)262 if (!deleted) {263 return c.json({ error: 'Todo not found' }, 404)264 }265 return c.json({ message: 'Todo deleted successfully' }, 200)266 },267 )268 .doc('/doc', {269 openapi: '3.0.0',270 info: {271 version: '1.0.0',272 title: 'My API',273 },274 })275 .get('/ui', swaggerUI({ url: '/doc' }))276277export type AppType = typeof app278279Deno.serve(app.fetch)
main_test.ts
1import { assert, assertEquals, assertNotEquals } from '@std/assert'2import { app } from './main.ts'3import { testClient } from 'hono/testing'45const client = testClient(app)67const createTodo = async (title: string) => {8 const res = await client.todos.$post({9 json: {10 title: title,11 },12 })13 return await res.json()14}1516const resetTodos = async () => {17 const res = await client.todos.$get()18 const todos = await res.json()19 const deletePromises = todos.map(({ id }) => {20 return client.todos[':id'].$delete({21 param: {22 id: `${id}`,23 },24 })25 })26 await Promise.all(deletePromises)27}2829Deno.test('GET / returns welcome message', async () => {30 const res = await client.index.$get()31 assertEquals(res.status, 200)32 assertEquals(await res.text(), 'Hono + Deno TODO API')33})3435Deno.test('Post /todos creates a new todo', async () => {36 await resetTodos()37 const res = await client.todos.$post({38 json: {39 title: 'Test Todo',40 },41 })42 assertEquals(res.status, 201)43 const todo = await res.json()44 assertEquals(todo.title, 'Test Todo')45 assertEquals(todo.completed, false)46 assertEquals(typeof todo.id, 'number')47})4849Deno.test('GET /todos returns all todos', async () => {50 await resetTodos()51 await createTodo('Todo 1')52 await createTodo('Todo 2')5354 const res = await client.todos.$get()55 assertEquals(res.status, 200)56 const todos = await res.json()57 assertEquals(todos.length, 2)58 assertEquals(todos[0].title, 'Todo 1')59 assertEquals(todos[1].title, 'Todo 2')60})6162Deno.test('PUT /todos/:id updates a todo', async () => {63 await resetTodos()64 const todo = await createTodo('Update Me')6566 const updateRes = await client.todos[':id'].$put({67 param: {68 id: `${todo.id}`,69 },70 json: {71 title: 'Updated',72 completed: true,73 },74 })75 assertEquals(updateRes.status, 200)7677 const updatedTodo = await updateRes.json()78 assert(!('error' in updatedTodo), 'Unexpected error response')79 assertEquals(updatedTodo.id, todo.id)80 assertEquals(updatedTodo.title, 'Updated')81 assertEquals(updatedTodo.completed, true)8283 const getRes = await client.todos.$get()84 assertEquals(getRes.status, 200)85 const fetchedTodos = await getRes.json()8687 const fetchedTodo = fetchedTodos.find((t) => t.id === todo.id)88 assert(fetchedTodo, 'Todo not found')8990 assertEquals(fetchedTodo.title, 'Updated')91 assertEquals(fetchedTodo.completed, true)92})9394Deno.test('DELETE /todos/:id deletes a todo', async () => {95 await resetTodos()96 const todo = await createTodo('Delete Me')9798 const res = await client.todos[':id'].$delete({99 param: {100 id: `${todo.id}`,101 },102 })103104 assertEquals(res.status, 200)105 const result = await res.json()106 assert(!(('error') in result), 'Unexpected error response')107108 assertEquals(result.message, 'Todo deleted successfully')109 const getRes = await client.todos.$get()110 const todos = await getRes.json()111 assertEquals(todos.length, 0)112})113114Deno.test('POST /todos with invalid data returns 400', async () => {115 await resetTodos()116117 const res = await client.todos.$post({118 json: {119 title: '',120 },121 })122123 assertEquals(res.status, 400)124})125126Deno.test('PUT /todos/:id with invalid id returns 404', async () => {127 await resetTodos()128129 const res = await client.todos[':id'].$put({130 param: {131 id: '999',132 },133 json: {134 title: 'Updated',135 },136 })137138 assertEquals(res.status, 404)139})140141Deno.test('DELETE /todos/:id with invalid id returns 404', async () => {142 await resetTodos()143144 const res = await client.todos[':id'].$delete({145 param: {146 id: '999',147 },148 })149150 assertEquals(res.status, 404)151})152153Deno.test('PUT /todos/:id with no changes returns the same todo', async () => {154 await resetTodos()155 const todo = await createTodo('No Change')156157 const res = await client.todos[':id'].$put({158 param: {159 id: `${todo.id}`,160 },161 json: {},162 })163164 assertEquals(res.status, 200)165 const updatedTodo = await res.json()166 assertEquals(updatedTodo, todo)167})168169Deno.test('Todos maintain order after updates and deletions', async () => {170 await resetTodos()171 const todo1 = await createTodo('First')172 const todo2 = await createTodo('Second')173 await createTodo('Third')174175 await client.todos[':id'].$put({176 param: {177 id: `${todo2.id}`,178 },179 json: {180 title: 'Updated Second',181 },182 })183 await client.todos[':id'].$delete({184 param: {185 id: `${todo1.id}`,186 },187 })188189 const res = await app.request('/todos')190 const todos = await res.json()191192 assertEquals(todos.length, 2)193 assertEquals(todos[0].title, 'Updated Second')194 assertEquals(todos[1].title, 'Third')195})196197Deno.test('Todo IDs are unique and incremental', async () => {198 await resetTodos()199 const todo1 = await createTodo('First')200 const todo2 = await createTodo('Second')201202 assertNotEquals(todo1.id, todo2.id)203 assertEquals(todo2.id, todo1.id + 1)204})