Merge pull request #6 from ThisIsBenny/feature/1-Add-Create-Update-and-Delete-Endpoint-to-the-API

Feature/#1 add create update and delete endpoint to the api
This commit is contained in:
Benny 2022-02-06 13:28:38 +01:00 committed by GitHub
commit 73f53616db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 474 additions and 96 deletions

1
.gitignore vendored
View file

@ -30,5 +30,4 @@ dist/
coverage
.env
data
prisma/seed.ts
public/*.jpeg

73
examples.http Normal file
View file

@ -0,0 +1,73 @@
@BASE_URL=http://localhost:5000/api
###
# @name createWishlistFirst
POST {{BASE_URL}}/wishlist
Content-Type: application/json
{
"title": "Junior",
"imageSrc": "https://unsplash.com/photos/JZ51o_-UOY8/download?force=true&w=200",
"slugUrlText": "junior"
}
###
# @name createWishlistSecond
POST {{BASE_URL}}/wishlist
Content-Type: application/json
{
"title": "Wedding",
"imageSrc": "https://unsplash.com/photos/8vaQKYnawHw/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjQ0MDQ4MTIy&force=true&w=200",
"description": "We are getting married",
"slugUrlText": "wedding"
}
###
# @name getWishlists
GET {{BASE_URL}}/wishlist
###
# @name getFirstWishlist
GET {{BASE_URL}}/wishlist/{{getWishlists.response.body.0.slugUrlText}}
###
# @name updateFirstWishlist
PUT {{BASE_URL}}/wishlist/{{getWishlists.response.body.0.id}}
Content-Type: application/json
{
"title": "Junior",
"imageSrc": "https://unsplash.com/photos/JZ51o_-UOY8/download?force=true&w=200",
"description": "Juniors Wishlist",
"slugUrlText": "junior"
}
###
# @name addItemToFirstWishlist
POST {{BASE_URL}}/wishlist/{{getWishlists.response.body.0.id}}/item
Content-Type: application/json
{
"title": "Goldfish 40442 | BrickHeadz",
"url": "https://www.lego.com/en-de/product/goldfish-40442",
"imageSrc": "https://www.lego.com/cdn/cs/set/assets/blt1fc37afef51cfa9f/40442.jpg?fit=bounds&format=jpg&quality=80&width=1500&height=1500&dpr=1",
"description": "Cute goldfish and fry, build-and-display BrickHeadz™ model"
}
###
# @name updateItemOnFirstWishlist
PUT {{BASE_URL}}/wishlist/{{getWishlists.response.body.0.id}}/item/1
Content-Type: application/json
{
"title": "Goldfish | BrickHeadz",
"url": "https://www.lego.com/en-de/product/goldfish-40442",
"imageSrc": "https://www.lego.com/cdn/cs/set/assets/blt1fc37afef51cfa9f/40442.jpg?fit=bounds&format=jpg&quality=80&width=1500&height=1500&dpr=1",
"description": "Cute goldfish and fry, build-and-display BrickHeadz™ model"
}
###
# @name deleteItemToFirstWishlist
DELETE {{BASE_URL}}/wishlist/{{getWishlists.response.body.0.id}}/item/2

View file

@ -4,17 +4,17 @@ CREATE TABLE "Wishlist" (
"title" TEXT NOT NULL,
"imageSrc" TEXT NOT NULL,
"slugUrlText" TEXT NOT NULL,
"description" TEXT
"description" TEXT NOT NULL DEFAULT ''
);
-- CreateTable
CREATE TABLE "Item" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"url" TEXT,
"imageSrc" TEXT,
"url" TEXT NOT NULL DEFAULT '',
"imageSrc" TEXT NOT NULL DEFAULT '',
"description" TEXT NOT NULL,
"comment" TEXT,
"comment" TEXT NOT NULL DEFAULT '',
"bought" BOOLEAN NOT NULL DEFAULT false,
"wishlistId" TEXT NOT NULL,
CONSTRAINT "Item_wishlistId_fkey" FOREIGN KEY ("wishlistId") REFERENCES "Wishlist" ("id") ON DELETE RESTRICT ON UPDATE CASCADE

View file

@ -15,17 +15,17 @@ model Wishlist {
title String
imageSrc String
slugUrlText String @unique
description String?
description String @default("")
items Item[]
}
model Item {
id Int @id @default(autoincrement())
title String
url String?
imageSrc String?
url String @default("")
imageSrc String @default("")
description String
comment String?
comment String @default("")
bought Boolean @default(false)
wishlist Wishlist @relation(fields: [wishlistId], references: [id])
wishlistId String

60
prisma/seed.ts Normal file
View file

@ -0,0 +1,60 @@
import { PrismaClient, Prisma } from '@prisma/client'
const prisma = new PrismaClient()
const wishlistData: Prisma.WishlistCreateInput[] = [
{
title: 'Junior',
imageSrc:
'https://unsplash.com/photos/JZ51o_-UOY8/download?force=true&w=200',
description: '',
slugUrlText: 'junior',
items: {
create: [
{
title: 'Goldfish 40442 | BrickHeadz',
url: 'https://www.lego.com/en-de/product/goldfish-40442',
imageSrc:
'https://www.lego.com/cdn/cs/set/assets/blt1fc37afef51cfa9f/40442.jpg?fit=bounds&format=jpg&quality=80&width=1500&height=1500&dpr=1',
description:
'Cute goldfish and fry, build-and-display BrickHeadz™ model',
comment: '',
},
],
},
},
{
title: 'Wedding',
imageSrc:
'https://unsplash.com/photos/8vaQKYnawHw/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjQ0MDQ4MTIy&force=true&w=200',
description: 'We are getting married',
slugUrlText: 'wedding',
},
{
title: '40th birthday',
imageSrc:
'https://unsplash.com/photos/poH6OvcEeXE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8NHx8YmlydGhkYXl8fDB8fHx8MTY0NDA1NDEzNA&force=true&w=200',
description: 'We are getting married',
slugUrlText: '40th-birthday',
},
]
async function main() {
console.log(`Start seeding ...`)
for (const u of wishlistData) {
const wishlist = await prisma.wishlist.create({
data: u,
})
console.log(`Created wishlist with id: ${wishlist.id}`)
}
console.log(`Seeding finished.`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View file

@ -0,0 +1,56 @@
import { FastifyRequest, FastifyReply, FastifyError } from 'fastify'
import { Prisma } from '@prisma/client'
const errorIs = (e: unknown, c: string) =>
e instanceof Prisma.PrismaClientKnownRequestError && e.code === c
export const defaultErrorHandler = (
error: FastifyError,
request: FastifyRequest,
reply: FastifyReply
) => {
if (error.validation) {
error.code = '400'
reply.send(error)
} else if (errorIs(error, 'P2002')) {
reply.send(
uniqueKeyError(
// @ts-expect-error: Object is possibly 'undefined'
`${error.meta.target[0] || 'One of the fields'} has to be unique`
)
)
} else if (errorIs(error, 'P2025')) {
reply.callNotFound()
} else {
request.log.error(error)
const e = new httpError('unexpected error', 500, '500')
reply.send(e)
}
}
export const notFoundHandler = (
request: FastifyRequest,
reply: FastifyReply
) => {
reply.send(notFoundError())
}
class httpError extends Error {
code: string
statusCode: number
constructor(message: string, statusCode: number, code: string) {
super(message)
this.name = this.constructor.name
Error.captureStackTrace(this, this.constructor)
this.statusCode = statusCode
this.code = code
}
}
const notFoundError = () => {
return new httpError('Not Found', 404, '404')
}
const uniqueKeyError = (msg: string, code = '4001') => {
return new httpError(msg, 422, code)
}

View file

@ -0,0 +1 @@
export * from './wishlist'

View file

@ -0,0 +1,53 @@
export const wishlistItemRequestSchema = {
type: 'object',
additionalProperties: false,
required: ['title', 'description'],
properties: {
title: { type: 'string' },
url: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
comment: { type: 'string' },
bought: { type: 'boolean' },
},
}
export const wishlistItemResponseSchema = {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
url: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
comment: { type: 'string' },
bought: { type: 'boolean' },
wishlistId: { type: 'string' },
},
}
export const wishlistRequestSchema = {
type: 'object',
additionalProperties: false,
required: ['title', 'imageSrc', 'slugUrlText'],
properties: {
title: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
slugUrlText: { type: 'string' },
},
}
export const wishlistResponseSchema = {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
slugUrlText: { type: 'string' },
items: {
type: 'array',
items: wishlistItemResponseSchema,
},
},
}

View file

@ -1,15 +1,13 @@
import { prisma } from '../../services'
import { Wishlist, WishlistItem } from '@/types'
export default {
getAll: async (): Promise<any> => {
return await prisma.client.wishlist.findMany({
getAll: async (): Promise<Wishlist[]> => {
return (await prisma.client.wishlist.findMany({
include: { items: false },
})
})) as Wishlist[]
},
getBySlugUrlText: async (
value: string,
includeItems = false
): Promise<any> => {
getBySlugUrlText: async (value: string, includeItems = false) => {
return await prisma.client.wishlist.findUnique({
where: {
slugUrlText: value,
@ -17,6 +15,44 @@ export default {
include: { items: includeItems },
})
},
create: async (payload: Wishlist) => {
return await prisma.client.wishlist.create({
data: payload,
})
},
update: async (id: string, payload: Wishlist) => {
return await prisma.client.wishlist.update({
where: {
id: id,
},
data: {
...payload,
},
})
},
delete: async (id: string) => {
return await prisma.client.wishlist.delete({
where: {
id: id,
},
})
},
createItem: async (wishlistId: string, payload: WishlistItem) => {
const wishlist = await prisma.client.wishlist.update({
where: {
id: wishlistId,
},
data: {
items: {
create: {
...payload,
},
},
},
include: { items: true },
})
return wishlist.items.pop()
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateItem: async (itemId: number, payload: any) => {
return await prisma.client.item.update({
@ -28,4 +64,11 @@ export default {
},
})
},
deleteItem: async (itemId: number) => {
return await prisma.client.item.delete({
where: {
id: itemId,
},
})
},
}

View file

@ -1,10 +1,13 @@
import { FastifyInstance } from 'fastify'
import { default as wishlistRoute } from './wishlist/'
import { defaultErrorHandler, notFoundHandler } from '../config/errors'
export default {
register: (app: FastifyInstance) => {
return app.register(
async (app) => {
await app.setNotFoundHandler(notFoundHandler)
await app.setErrorHandler(defaultErrorHandler)
await app.register(wishlistRoute, { prefix: '/wishlist' })
},
{ prefix: '/api' }

View file

@ -0,0 +1,56 @@
import { Wishlist, WishlistItem } from '@/types'
import { FastifyRequest, FastifyReply, RouteOptions } from 'fastify'
import { wishlist } from '../../models'
import {
wishlistItemRequestSchema,
wishlistItemResponseSchema,
wishlistRequestSchema,
wishlistResponseSchema,
} from '../../config/schemas'
interface createItemRequest extends FastifyRequest {
params: {
wishlistId: string
}
}
export const createList = <RouteOptions>{
method: 'POST',
url: '/',
schema: {
body: wishlistRequestSchema,
response: {
201: wishlistResponseSchema,
},
},
handler: async (request: FastifyRequest, reply: FastifyReply) => {
request.log.debug(request.body)
const item = await wishlist.create(request.body as Wishlist)
reply.code(201).send(item)
},
}
export const createItem = <RouteOptions>{
method: 'POST',
url: '/:wishlistId/item',
schema: {
body: wishlistItemRequestSchema,
params: {
type: 'object',
properties: {
wishlistId: { type: 'string' },
},
},
response: {
201: wishlistItemResponseSchema,
},
},
handler: async (request: createItemRequest, reply: FastifyReply) => {
request.log.debug(request.body)
const item = await wishlist.createItem(
request.params.wishlistId,
request.body as WishlistItem
)
reply.code(201).send(item)
},
}

View file

@ -0,0 +1,50 @@
import { FastifyRequest, FastifyReply, RouteOptions } from 'fastify'
import { wishlist } from '../../models'
interface deleteRequest extends FastifyRequest {
params: {
wishlistId: string
}
}
interface deleteItemRequest extends FastifyRequest {
params: {
wishlistId: string
itemId: number
}
}
export const deleteList = <RouteOptions>{
method: 'DELETE',
url: '/:wishlistId',
schema: {
params: {
type: 'object',
properties: {
wishlistId: { type: 'string' },
},
},
},
handler: async (request: deleteRequest, reply: FastifyReply) => {
await wishlist.delete(request.params.wishlistId)
reply.code(204).send()
},
}
export const deleteItem = <RouteOptions>{
method: 'DELETE',
url: '/:wishlistId/item/:itemId',
schema: {
params: {
type: 'object',
properties: {
wishlistId: { type: 'string' },
itemId: { type: 'number' },
},
},
},
handler: async (request: deleteItemRequest, reply: FastifyReply) => {
await wishlist.deleteItem(request.params.itemId)
reply.code(204).send()
},
}

View file

@ -1,9 +1,16 @@
import { FastifyInstance } from 'fastify'
import { getAll, getBySlugUrl } from './read'
import { updateItem } from './update'
import { updateList, updateItem } from './update'
import { createList, createItem } from './create'
import { deleteList, deleteItem } from './delete'
export default async (app: FastifyInstance) => {
await app.route(getAll)
await app.route(getBySlugUrl)
await app.route(createList)
await app.route(createItem)
await app.route(updateList)
await app.route(updateItem)
await app.route(deleteList)
await app.route(deleteItem)
}

View file

@ -1,22 +1,15 @@
import { FastifyRequest, FastifyReply, RouteOptions } from 'fastify'
import { wishlist } from '../../models'
import { wishlistResponseSchema } from '../../config/schemas'
export const getAll = <any>{
export const getAll = <RouteOptions>{
method: 'GET',
url: '/',
schema: {
response: {
200: {
type: 'array',
items: {
properties: {
id: { type: 'string' },
title: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
slugUrlText: { type: 'string' },
},
},
items: wishlistResponseSchema,
},
},
},
@ -36,31 +29,7 @@ export const getBySlugUrl = <RouteOptions>{
url: '/:slugText',
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
slugUrlText: { type: 'string' },
items: {
type: 'array',
items: {
properties: {
id: { type: 'number' },
title: { type: 'string' },
url: { type: 'string' },
imageSrc: { type: 'string' },
description: { type: 'string' },
comment: { type: 'string' },
bought: { type: 'boolean' },
wishlistId: { type: 'string' },
},
},
},
},
},
200: wishlistResponseSchema,
},
},
handler: async (request: GetBySlugUrlTextRequest, reply: FastifyReply) => {
@ -68,10 +37,7 @@ export const getBySlugUrl = <RouteOptions>{
if (list) {
return list
} else {
return reply.code(404).send({
error: 'notFound',
http: 404,
})
return reply.callNotFound()
}
},
}

View file

@ -1,58 +1,69 @@
import { Wishlist } from '@/types'
import { FastifyRequest, FastifyReply, RouteOptions } from 'fastify'
import { wishlist } from '../../models'
import {
wishlistRequestSchema,
wishlistResponseSchema,
wishlistItemRequestSchema,
wishlistItemResponseSchema,
} from '../../config/schemas'
interface GetBySlugUrlTextRequest extends FastifyRequest {
interface updateRequest extends FastifyRequest {
params: {
wishlistId: string
}
}
interface updateItemRequest extends FastifyRequest {
params: {
wishlistId: string
itemId: number
}
}
export const updateList = <RouteOptions>{
method: 'PUT',
url: '/:wishlistId',
schema: {
body: wishlistRequestSchema,
params: {
type: 'object',
properties: {
wishlistId: { type: 'string' },
},
},
response: {
200: wishlistResponseSchema,
},
},
handler: async (request: updateRequest, reply: FastifyReply) => {
request.log.debug(request.body)
const item = await wishlist.update(
request.params.wishlistId,
request.body as Wishlist
)
reply.code(201).send(item)
},
}
export const updateItem = <RouteOptions>{
method: 'PUT',
url: '/:wishlistId/item/:itemId',
schema: {
body: {
body: wishlistItemRequestSchema,
params: {
type: 'object',
additionalProperties: false,
properties: {
title: { type: 'string' },
url: { type: 'string' },
image: { type: 'string' },
description: { type: 'string' },
comment: { type: 'string' },
bought: { type: 'boolean' },
wishlistId: { type: 'string' },
itemId: { type: 'number' },
},
},
response: {
204: {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
url: { type: 'string' },
image: { type: 'string' },
description: { type: 'string' },
comment: { type: 'string' },
bought: { type: 'boolean' },
wishlistId: { type: 'string' },
200: wishlistItemResponseSchema,
},
},
},
},
handler: async (request: GetBySlugUrlTextRequest, reply: FastifyReply) => {
handler: async (request: updateItemRequest, reply: FastifyReply) => {
request.log.debug(request.body)
const item = await wishlist.updateItem(
Number(request.params.itemId),
request.body
)
if (item) {
return item
} else {
return reply.code(404).send({
error: 'notFound',
http: 404,
})
}
reply.send(await wishlist.updateItem(request.params.itemId, request.body))
},
}

View file

@ -9,12 +9,12 @@ export interface WishlistItem {
wishlistId: boolean
}
export interface Wishlist {
id: string
id?: string
title: string
description: string
imageSrc: string
slugUrlText: string
items: WishlistItem[]
items?: WishlistItem[]
}
export interface TileProp {
title: string

View file

@ -16,7 +16,7 @@ const { list, fetch, updateItem } = useWishlistStore()
await fetch(route.params.slug as string)
const notBoughtItems = computed(() => {
return list.value?.items.filter(
return list.value?.items?.filter(
(item: WishlistItemType) => item.bought === false
)
})