Skip to content

Commit 53fdd2f

Browse files
committed
Add before hooks, rename from createApi to createService
1 parent 911b5cf commit 53fdd2f

File tree

10 files changed

+192
-154
lines changed

10 files changed

+192
-154
lines changed

README.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Hooks are just middlewares that run before each of the handlers.
1010
```ts
1111
import getConfig from 'next/config'
1212
import * as mongoose from 'mongoose'
13-
import { createApi, hook, NotFoundError } from 'api-next'
13+
import { createService, hook, NotFoundError } from 'api-next'
1414

1515
export interface PostAttrs {
1616
title: string
@@ -51,24 +51,26 @@ const config = getConfig()
5151

5252
// Concept from Feathersjs: https://feathersjs.com/
5353
const hooks = {
54-
all: [
55-
hook.connectToDatabase({
56-
name: 'posts-db',
57-
connect: () =>
58-
mongoose.connect(config.serverRuntimeConfig.MONGO_URI, {
59-
useNewUrlParser: true,
60-
useUnifiedTopology: true,
61-
useCreateIndex: true,
62-
}),
63-
}),
64-
],
65-
create: [validateBody],
66-
update: [validateBody],
54+
before: {
55+
all: [
56+
hook.connectToDatabase({
57+
name: 'posts-db',
58+
connect: () =>
59+
mongoose.connect(config.serverRuntimeConfig.MONGO_URI, {
60+
useNewUrlParser: true,
61+
useUnifiedTopology: true,
62+
useCreateIndex: true,
63+
}),
64+
}),
65+
],
66+
create: [validateBody],
67+
update: [validateBody],
68+
},
6769
}
6870

6971
// All the keys are optional
7072
// Pick what you need
71-
export default createApi({
73+
export default createService({
7274
hooks,
7375
find: async () => Post.find(),
7476
create: async (body: PostAttrs) => Post.build(body),
@@ -93,11 +95,11 @@ export default createApi({
9395
### Alternative
9496

9597
```ts
96-
import { createMongooseApi } from 'api-next'
98+
import { createMongooseService } from 'api-next'
9799

98-
const { find, create, get, update, remove } = createMongooseApi(Post)
100+
const { find, create, get, update, remove } = createMongooseService(Post)
99101

100-
export default createApi({
102+
export default createService({
101103
hooks,
102104
find,
103105
create,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "api-next",
3-
"version": "1.0.0-beta.2",
3+
"version": "1.0.0",
44
"repository": "https://github.com/imflavio/api-next",
55
"author": "Flávio Carvalho <hey@flavio.uk>",
66
"license": "MIT",

src/api.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as hook from './hooks'
22
export { hook }
33

4-
export * from './api'
4+
export * from 'services.ts'
55
export * from './mongoose'
66
export * from './types'
77
export { BadRequestError } from './errors/bad-request-error'

src/mongoose.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import * as mongoose from 'mongoose'
22

3-
import { Services } from './types'
3+
import { ServiceMethods } from './types'
44
import { NotFoundError } from './errors/not-found-error'
55

6-
export const createMongooseApi = (
6+
export const createMongooseService = (
77
Model: ReturnType<typeof mongoose.model>,
8-
): Services => ({
8+
): ServiceMethods => ({
99
find: async () => Model.find(),
1010
create: async (body) => Model.create(body),
1111
get: async (pk) => Model.findById(pk),
1212
update: async (pk, body) => {
13-
const post = await Model.findById(pk)
14-
if (!post) throw new NotFoundError()
13+
const data = await Model.findById(pk)
14+
if (!data) throw new NotFoundError()
1515

16-
post.set(body)
17-
await post.save()
18-
return post
16+
data.set(body)
17+
await data.save()
18+
return data
1919
},
2020
remove: async (pk) => {
21-
const post = await Model.findById(pk)
22-
if (!post) throw new NotFoundError()
23-
await post.remove()
24-
return { success: true }
21+
const data = await Model.findById(pk)
22+
if (!data) throw new NotFoundError()
23+
await data.remove()
24+
return { success: true, data }
2525
},
2626
})

src/services.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { NextApiRequest, NextApiResponse } from 'next'
2+
import morgan from 'morgan'
3+
import { Method, Status } from 'simple-http-status'
4+
5+
import { CustomError } from './errors/custom-error'
6+
import { NotFoundError } from './errors/not-found-error'
7+
import { Hook, ServiceMethods } from './types'
8+
9+
interface Pk {
10+
name?: string
11+
cast?: (pk: string) => string
12+
}
13+
14+
interface Hooks {
15+
all?: Hook[]
16+
find?: Hook[]
17+
create?: Hook[]
18+
get?: Hook[]
19+
update?: Hook[]
20+
patch?: Hook[]
21+
remove?: Hook[]
22+
}
23+
24+
interface ServiceOptions extends ServiceMethods {
25+
pk?: Pk
26+
hooks?: {
27+
before?: Hooks
28+
after?: Hooks
29+
}
30+
}
31+
32+
const logger = morgan(
33+
':method :url :status :res[content-length] - :response-time ms',
34+
)
35+
36+
const getPk = (pk: Pk = {}, query: NextApiRequest['query']): string | null => {
37+
const queryValue = pk?.name ? query[pk?.name] : query?.id || query?.pk
38+
const [value] = Array.isArray(queryValue) ? queryValue : [queryValue]
39+
return pk?.cast && value ? pk.cast(value!) : value
40+
}
41+
42+
const getStatusCode = (type: keyof ServiceMethods) => {
43+
switch (type) {
44+
case 'create':
45+
return Status.HTTP_201_CREATED
46+
default:
47+
return Status.HTTP_200_OK
48+
}
49+
}
50+
51+
const runHandler = async (
52+
type: keyof ServiceMethods,
53+
options: ServiceOptions,
54+
req: NextApiRequest,
55+
pk: string,
56+
) => {
57+
switch (type) {
58+
case 'find':
59+
return options.find!(req.query)
60+
case 'create':
61+
return options.create!(req.body)
62+
case 'get':
63+
return options.get!(pk, req.query)
64+
case 'update':
65+
return options.update!(pk, req.body, req.query)
66+
case 'patch':
67+
return options.patch!(pk, req.body, req.query)
68+
case 'remove':
69+
return options.remove!(pk)
70+
}
71+
}
72+
73+
const handleService = async (
74+
type: keyof ServiceMethods,
75+
options: ServiceOptions,
76+
req: NextApiRequest,
77+
res: NextApiResponse,
78+
) => {
79+
const pk = getPk(options.pk, req.query)
80+
81+
const runHooks = async (hooks: Hook[]) => {
82+
for (let index = 0; index < hooks.length; index++) {
83+
await hooks[index](req, res, () => {})
84+
}
85+
}
86+
87+
const allBeforeHooks = options.hooks?.before?.all ?? []
88+
const beforeHooks = options.hooks?.before?.[type] ?? []
89+
await runHooks([...allBeforeHooks, ...beforeHooks])
90+
91+
const result = await runHandler(type, options, req, pk!)
92+
93+
const allAfterHooks = options.hooks?.after?.all ?? []
94+
const afterHooks = options.hooks?.after?.[type] ?? []
95+
await runHooks([...allAfterHooks, ...afterHooks])
96+
97+
const statusCode = getStatusCode(type)
98+
99+
return res.status(statusCode).json(result)
100+
}
101+
102+
export const createService = (options: ServiceOptions) => async (
103+
req: NextApiRequest,
104+
res: NextApiResponse,
105+
) => {
106+
logger(req, res, () => {})
107+
108+
try {
109+
const pk = getPk(options.pk, req.query)
110+
const { method } = req
111+
112+
switch (true) {
113+
case options.get && pk && method === Method.GET:
114+
return handleService('get', options, req, res)
115+
116+
case options.update && pk && method === Method.PUT:
117+
return handleService('update', options, req, res)
118+
119+
case options.patch && pk && method === Method.PATCH:
120+
return handleService('patch', options, req, res)
121+
122+
case options.remove && pk && method === Method.DELETE:
123+
return handleService('remove', options, req, res)
124+
125+
case options.find && !pk && method === Method.GET:
126+
return handleService('find', options, req, res)
127+
128+
case options.create && !pk && method === Method.POST:
129+
return handleService('create', options, req, res)
130+
131+
default:
132+
throw new NotFoundError()
133+
}
134+
} catch (err) {
135+
if (err instanceof CustomError) {
136+
return res.status(err.statusCode).json({
137+
errors: err.serializeErrors(),
138+
})
139+
}
140+
141+
return res.status(Status.HTTP_500_INTERNAL_SERVER_ERROR).json({
142+
errors: [{ message: 'Something went wrong' }],
143+
})
144+
}
145+
}

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface DatabaseConnection {
88
export type ApiNextQuery = NextApiRequest['query']
99
export type ApiNextBody = NextApiRequest['body']
1010

11-
export interface Services {
11+
export interface ServiceMethods {
1212
find?(query: ApiNextQuery): Promise<any>
1313
create?: (body: ApiNextBody) => Promise<any>
1414
get?: (pk: string, query: ApiNextQuery) => Promise<any>

0 commit comments

Comments
 (0)