Skip to content

Commit

Permalink
more explicit naming of ids and uuids
Browse files Browse the repository at this point in the history
  • Loading branch information
karl-run committed Feb 3, 2024
1 parent e2bbc0f commit 635d20c
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 48 deletions.
Binary file modified bun.lockb
Binary file not shown.
10 changes: 5 additions & 5 deletions db/schema.sql
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
CREATE TABLE authors
(
id SERIAL PRIMARY KEY,
author_id uuid DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
bio TEXT
id SERIAL PRIMARY KEY,
uuid uuid DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
bio TEXT
);

CREATE TABLE books
(
book_id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
uuid uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INT NOT NULL,
published_date DATE,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"pg": "^8.11.3"
},
"devDependencies": {
"@types/bun": "^1.0.4",
"@types/pg": "^8.10.9",
"bun-types": "^1.0.25",
"prettier": "^3.2.4"
},
"peerDependencies": {
Expand Down
14 changes: 14 additions & 0 deletions src/db-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type DbBook = {
uuid: string
title: string
published_date: Date
isbn: string
author_id: number
}

export type DbAuthor = {
id: number
uuid: string
name: string
bio: string
}
117 changes: 117 additions & 0 deletions src/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { test, describe, expect, beforeAll, beforeEach } from 'bun:test'

describe('API sanity checks', () => {
beforeEach(async () => {
await fetch('http://localhost:6969/dev/re-seed', { method: 'POST' })
})

test('GET /authors', async () => {
const response = await fetch('http://localhost:6969/authors')
const data = await response.json()

expect(data).toHaveLength(8)
})

test('GET /books', async () => {
const response = await fetch('http://localhost:6969/books')
const data = await response.json()

expect(data).toHaveLength(129)
})

test('GET /author/:uuid', async () => {
const response = await fetch('http://localhost:6969/authors')
const data = (await response.json()) as { uuid: string }[]
const authorUuid = data[0].uuid

const authorResponse = await fetch(`http://localhost:6969/author/${authorUuid}`)
const authorData = await authorResponse.json()

expect(authorData).toEqual({
name: 'J.K. Rowling',
bio: 'British author, best known for the Harry Potter series.',
published_books: 20,
uuid: authorUuid,
})
})

test('GET /author/:uuid/books', async () => {
const response = await fetch('http://localhost:6969/authors')
const data = (await response.json()) as { uuid: string }[]
const authorUuid = data[0].uuid

const authorBooksResponse = await fetch(`http://localhost:6969/author/${authorUuid}/books`)
const authorBooksData = await authorBooksResponse.json()

expect(authorBooksData).toHaveLength(20)
})

test('POST /author', async () => {
const response = await fetch('http://localhost:6969/author', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'New Author',
bio: 'New author bio',
}),
})

expect(response.status).toBe(201)
})

test('POST /book', async () => {
const response = await fetch('http://localhost:6969/authors')
const data = (await response.json()) as { uuid: string }[]
const authorUuid = data[0].uuid

const bookResponse = await fetch('http://localhost:6969/book', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'New Book',
isbn: '1234567890',
published_date: '2022-01-01',
author_uuid: authorUuid,
}),
})

expect(bookResponse.status).toBe(201)
})

test('POST /author, POST /book on author, GET /books from author', async () => {
const authorResponse = await fetch('http://localhost:6969/author', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Author with book',
bio: 'Author with book bio',
}),
})
const authorData = (await authorResponse.json()) as { uuid: string }
const authorUuid = authorData.uuid

const bookResponse = await fetch('http://localhost:6969/book', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Book from author',
isbn: '1234567890',
published_date: '2022-01-01',
author_uuid: authorUuid,
}),
})
expect(bookResponse.status).toBe(201)

const authorBooksResponse = await fetch(`http://localhost:6969/author/${authorUuid}/books`)
const authorBooksData = await authorBooksResponse.json()
expect(authorBooksData).toHaveLength(1)
})
})
86 changes: 51 additions & 35 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Elysia, t } from 'elysia'
import { client, initDb } from './db.ts'
import swagger from '@elysiajs/swagger'
import { DbAuthor, DbBook } from './db-types.ts'
import path from 'path'
import { cwd } from 'node:process'

await initDb()

Expand All @@ -19,18 +22,18 @@ const app = new Elysia()
.get(
'authors',
async () => {
const result = await client.query('SELECT * FROM authors')
const result = await client.query<DbAuthor>('SELECT * FROM authors')

return result.rows.map((it) => ({
author_id: it.author_id,
uuid: it.uuid,
name: it.name,
bio: it.bio,
}))
},
{
response: t.Array(
t.Object({
author_id: t.String(),
uuid: t.String(),
name: t.String(),
bio: t.String(),
}),
Expand All @@ -39,9 +42,9 @@ const app = new Elysia()
},
)
.get(
'author/:id',
async ({ params: { id } }) => {
const result = await client.query('SELECT * FROM authors WHERE author_id = $1', [id])
'author/:uuid',
async ({ params: { uuid } }) => {
const result = await client.query<DbAuthor>('SELECT * FROM authors WHERE uuid = $1', [uuid])

if (result.rows.length === 0) {
return null
Expand All @@ -50,7 +53,7 @@ const app = new Elysia()
const author = result.rows[0]
const publishedBooks = await client.query('SELECT COUNT(*) FROM books WHERE author_id = $1', [author.id])
return {
author_id: author.author_id,
uuid: author.uuid,
name: author.name,
bio: author.bio,
published_books: +publishedBooks.rows[0].count,
Expand All @@ -60,7 +63,7 @@ const app = new Elysia()
response: t.Nullable(
t.Object(
{
author_id: t.String(),
uuid: t.String(),
name: t.String(),
bio: t.String(),
published_books: t.Number({ description: 'Number of published books' }),
Expand All @@ -78,13 +81,15 @@ const app = new Elysia()
return null
}

const newAuthor = await client.query('INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING author_id', [
body.name,
body.bio,
])
const newAuthor = await client.query<{ uuid: string }>(
'INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING uuid',
[body.name, body.bio],
)

set.status = 'Created'

const newAuthorId: string = newAuthor.rows[0].author_id
return { author_id: newAuthorId }
const newAuthorId: string = newAuthor.rows[0].uuid
return { uuid: newAuthorId }
},
{
body: t.Object({
Expand All @@ -93,25 +98,25 @@ const app = new Elysia()
}),
response: t.Nullable(
t.Object({
author_id: t.String(),
uuid: t.String(),
}),
),
},
)
.delete(
'author/:id',
async ({ params: { id }, set }) => {
if (await client.query('SELECT * FROM authors WHERE author_id = $1', [id]).then((it) => it.rows.length === 0)) {
'author/:uuid',
async ({ params: { uuid }, set }) => {
if (await client.query('SELECT * FROM authors WHERE uuid = $1', [uuid]).then((it) => it.rows.length === 0)) {
set.status = 'Not Found'
return null
}

await client.query('BEGIN')
const author = await client.query('SELECT * FROM authors WHERE author_id = $1', [id])
const authorId = author.rows[0].id
const author = await client.query('SELECT * FROM authors WHERE uuid = $1', [uuid])
const id = author.rows[0].id

const deletedBooks = await client.query('DELETE FROM books WHERE author_id = $1', [authorId])
const deletedAuthor = await client.query('DELETE FROM authors WHERE id = $1', [authorId])
const deletedBooks = await client.query('DELETE FROM books WHERE author_id = $1', [id])
const deletedAuthor = await client.query('DELETE FROM authors WHERE id = $1', [id])
await client.query('COMMIT')

console.info(`Deleted ${deletedBooks.rowCount} books and ${deletedAuthor.rowCount} author`)
Expand All @@ -123,14 +128,13 @@ const app = new Elysia()
},
)
.get(
'author/:id/books',
async ({ params: { id } }) => {
const result = await client.query(
`SELECT *
FROM books
'author/:uuid/books',
async ({ params: { uuid } }) => {
const result = await client.query<Pick<DbBook, 'title' | 'published_date' | 'isbn'>>(
`SELECT title, published_date, isbn FROM books
LEFT JOIN authors a on a.id = books.author_id
WHERE a.author_id = $1`,
[id],
WHERE a.uuid = $1`,
[uuid],
)

return result.rows.map((it) => ({
Expand All @@ -153,7 +157,7 @@ const app = new Elysia()
.get(
'books',
async () => {
const result = await client.query('SELECT * FROM books')
const result = await client.query<DbBook>('SELECT * FROM books')

return result.rows.map((it) => ({
title: it.title,
Expand All @@ -180,21 +184,23 @@ const app = new Elysia()
return null
}

const author = await client.query('SELECT * FROM authors WHERE author_id = $1', [body.author_id])
const newBook = await client.query(
'INSERT INTO books (isbn, title, published_date, author_id) VALUES ($1, $2, $3, $4) RETURNING book_id',
const author = await client.query<DbAuthor>('SELECT * FROM authors WHERE uuid = $1', [body.author_uuid])
const newBook = await client.query<Pick<DbBook, 'uuid'>>(
'INSERT INTO books (isbn, title, published_date, author_id) VALUES ($1, $2, $3, $4) RETURNING uuid',
[body.isbn, body.title, body.published_date, author.rows[0].id],
)

const newBookId: string = newBook.rows[0].book_id
set.status = 'Created'

const newBookId: string = newBook.rows[0].uuid
return { book_id: newBookId }
},
{
body: t.Object({
isbn: t.String(),
title: t.String(),
published_date: t.String({ description: 'ISO8601 Date' }),
author_id: t.String({ description: 'UUID of author' }),
author_uuid: t.String({ description: 'UUID of author' }),
}),
response: t.Nullable(
t.Object({
Expand All @@ -203,6 +209,16 @@ const app = new Elysia()
),
},
)
.post('dev/re-seed', async () => {
await client.query('BEGIN')
await client.query('DELETE FROM books')
await client.query('DELETE FROM authors')
await client.query('ALTER SEQUENCE authors_id_seq RESTART WITH 1')
await client.query(await Bun.file(path.join(cwd(), 'db', 'seed.sql')).text())
await client.query('COMMIT')

return { success: true }
})
.onError((err) => {
console.error(err)
})
Expand Down
10 changes: 3 additions & 7 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
{
"compilerOptions": {
"lib": [
"ESNext"
],
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
Expand All @@ -17,8 +15,6 @@
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": [
"@types/bun"
]
}
"types": ["bun-types"],
},
}

0 comments on commit 635d20c

Please sign in to comment.