Skip to content

Commit

Permalink
add route conditionally (#8)
Browse files Browse the repository at this point in the history
* add route conditionally
* test routes
* impl errorHandler

closes #3
  • Loading branch information
kedoska authored Sep 17, 2020
1 parent b5ca79c commit f9e7086
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 94 deletions.
87 changes: 77 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,54 @@ npm install @kedoska/resty
- Define CRUD operations on the data-adapter.
- Built-in extractors for _pagination_ and _query_.

#### Specify Version and Resource
The first operation is to define the REST resource, passing the **version** and the **resource name**.<br/>
Both values are used to create the API endpoint, having the version as the root and the resource name as the second path.
#### Define your resources
> Example: './resources/users'
```typescript
const users = resty({
import resty, { Pagination, Query } from '@kedoska/resty'

export interface User {
email: string
username: string
}

const selectMany = (pagination: Pagination, query?: Query): Promise<User[]> =>
new Promise((resolve, reject) => {
try {
resolve([])
} catch ({message}) {
reject(Error(`could not "select" the resources, ${message}`))
}
})

export default () => {
return resty({
version: 'v1',
resource: 'users',
dataAdapter: {},
dataAdapter: {
// createOne,
selectMany,
// selectOne,
// updateOne,
// deleteOne,
// deleteAll,
},
})
}
```

#### Consume the resource
> Example: '.server.ts'
```typescript
// in your server
import express from 'express'
import users from './resources/users'

const app = express()
app.use(users())
app.listen(8080)

const app = express()
app.use(users)
```

#### The Data Adapter
Expand Down Expand Up @@ -86,10 +121,42 @@ Consider the below examples, the default pagination is very straightforward, the
* `curl https://localhost:8080?limit=10&page=2` becomes `{ limit: 10 page: 2 }`
* ...

### selectMany with custom pagination
### Examples
- **(TS)** Copy/Paste Data Adapter Skeleton [gits](https://gist.github.com/kedoska/eab2179c0532df77892a59a158da77ef)
- **(JS)** How to build a CRUD REST API using Express, resty and Sqlite3 [examples/sqllite3](https://github.com/kedoska/resty/tree/master/examples/sqlite3)

## Error Handling

The below example implements the `errorHandler` middleware from `'@kedoska/resty'` to catch the error sent by the `createOne` function.<br/>
The function handles eventual rejections coming from the data-adapter.<br/>

### Examples
```typescript
// in your server
import express from 'express'
import { errorHandler } from '@kedoska/resty'

const app = express()
app.use(
resty({
version: 'v1',
resource: 'users',
dataAdapter: {
createOne: (resource: any) => new Promise((resolve, reject) => {
reject(Error('Not Yet Implemented'))
}),
},
})
)

app.use(errorHandler)
app.listen(8080)
```
The `post` endpoint created by `createOne` is `/v1/users/`.<br/>
It will fail, returning status `200 OK`, having the following body:<br/>
- How to build a CRUD REST API using Express, resty and Sqlite3 [exmaples/sqllite3](https://github.com/kedoska/resty/tree/master/examples/sqlite3)?
```json
{
"message": "createOne not yet implemented"
}
```
31 changes: 30 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"express": "^4.17.1",
"jest": "^26.0.1",
"jest-fetch-mock": "^3.0.3",
"method-override": "^3.0.0",
"prettier": "^2.0.5",
"supertest": "^4.0.2",
"ts-jest": "^26.0.0",
Expand All @@ -37,6 +38,7 @@
"whatwg-fetch": "^3.0.0"
},
"peerDependencies": {
"express": "^4.17.1"
"express": "^4.17.1",
"method-override": "^3.0.0"
}
}
188 changes: 107 additions & 81 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,92 @@ export interface RestOptions {
dataAdapter: DataAdapter
}

export const createOneHandler = (options: RestOptions) => async (req: Request, res: Response, next: NextFunction) => {
if (!options.dataAdapter.createOne) {
next(new Error(`createOne not yet implemented`))
return
}

try {
res.send(await options.dataAdapter.createOne(req.parsedResource || req.body))
} catch ({ message }) {
next(new Error(`could not create the new resource, ${message}`))
}
}

export const selectManyHandler = (options: RestOptions) => async (req: Request, res: Response, next: NextFunction) => {
if (!options.dataAdapter.selectMany) {
next(new Error(`selectMany not yet implemented`))
return
}

try {
res.send(await options.dataAdapter.selectMany(req.dbPagination, req.dbQuery))
} catch ({ message }) {
next(new Error(`could not get the resources, ${message}`))
}
}

export const selectOneHandler = (options: RestOptions) => async (req: Request, res: Response, next: NextFunction) => {
if (!options.dataAdapter.selectOne) {
next(new Error(`selectOne not yet implemented`))
return
}
try {
res.send(res.send(await options.dataAdapter.selectOne(req.params.id)))
} catch ({ message }) {
next(new Error(`could not get the resources, ${message}`))
}
}

export const updateOneHandler = (options: RestOptions) => async (req: Request, res: Response, next: NextFunction) => {
if (!options.dataAdapter.updateOne) {
next(new Error(`updateOne not yet implemented`))
return
}
try {
res.send(await options.dataAdapter.updateOne(req.params.id, req.parsedResource || req.body))
} catch ({ message }) {
next(new Error(`could not update the resource "${req.params.id}", ${message}`))
}
}

export const deleteOneHandler = (options: RestOptions) => async (req: Request, res: Response, next: NextFunction) => {
if (!options.dataAdapter.deleteOne) {
next(new Error(`deleteOne not yet implemented`))
return
}
try {
await options.dataAdapter.deleteOne(req.params.id)
res.send()
} catch ({ message }) {
next(new Error(`could not delete the resource "${req.params.id}", ${message}`))
}
}

export const deleteAllHandler = (options: RestOptions) => async (req: Request, res: Response, next: NextFunction) => {
if (!options.dataAdapter.deleteAll) {
next(new Error(`deleteAll not yet implemented`))
return
}

try {
res.send(await options.dataAdapter.deleteAll())
} catch ({ message }) {
next(new Error(`could not get the resources, ${message}`))
}
}

export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
if (err) {
res.status(200).send({
message: err.message,
})
return
}
next()
}

export default (options: RestOptions): Router => {
const router = Router()
router.use(json())
Expand All @@ -125,96 +211,36 @@ export default (options: RestOptions): Router => {
req.parsedResource = options.parser.extractor(req[options.parser.source])
}
next()
} catch (error) {
next(error)
} catch (err) {
next(err)
}
})

router.get(`${path}`, async (req, res, next) => {
if (!options.dataAdapter.selectMany) {
next(new Error(`selectMany not yet implemented`))
return
}

try {
res.send(await options.dataAdapter.selectMany(req.dbPagination, req.dbQuery))
} catch ({ message }) {
next(new Error(`could not get the resources, ${message}`))
}
})

router.delete(`${path}`, async (req, res, next) => {
if (!options.dataAdapter.deleteAll) {
next(new Error(`deleteAll not yet implemented`))
return
}

try {
res.send(await options.dataAdapter.deleteAll())
} catch ({ message }) {
next(new Error(`could not get the resources, ${message}`))
}
})
if (options.dataAdapter.createOne) {
router.post(`${path}`, createOneHandler(options))
}

router.get(`${path}/:id`, async (req, res, next) => {
if (!options.dataAdapter.selectOne) {
next(new Error(`selectOne not yet implemented`))
return
}
try {
res.send(res.send(await options.dataAdapter.selectOne(req.params.id)))
} catch ({ message }) {
next(new Error(`could not get the resources, ${message}`))
}
})
if (options.dataAdapter.selectMany) {
router.get(`${path}`, selectManyHandler(options))
}

router.post(`${path}`, async (req, res, next) => {
if (!options.dataAdapter.createOne) {
next(new Error(`createOne not yet implemented`))
return
}
if (options.dataAdapter.selectOne) {
router.get(`${path}/:id`, selectOneHandler(options))
}

try {
res.send(await options.dataAdapter.createOne(req.parsedResource || req.body))
} catch ({ message }) {
next(new Error(`could not create the new resource, ${message}`))
}
})
if (options.dataAdapter.updateOne) {
router.put(`${path}/:id`, updateOneHandler(options))
}

router.delete(`${path}/:id`, async (req, res, next) => {
if (!options.dataAdapter.deleteOne) {
next(new Error(`deleteOne not yet implemented`))
return
}
try {
await options.dataAdapter.deleteOne(req.params.id)
res.send()
} catch ({ message }) {
next(new Error(`could not delete the resource "${req.params.id}", ${message}`))
}
})
if (options.dataAdapter.deleteAll) {
router.delete(`${path}`, deleteAllHandler(options))
}

router.put(`${path}/:id`, async (req, res, next) => {
if (!options.dataAdapter.updateOne) {
next(new Error(`updateOne not yet implemented`))
return
}
try {
res.send(await options.dataAdapter.updateOne(req.params.id, req.parsedResource || req.body))
} catch ({ message }) {
next(new Error(`could not update the resource "${req.params.id}", ${message}`))
}
})
if (options.dataAdapter.deleteOne) {
router.delete(`${path}/:id`, deleteOneHandler(options))
}

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err) {
res.status(500).send({
message: err.message,
})
return
}
next()
})
router.use(errorHandler)

return router
}
2 changes: 1 addition & 1 deletion tests/GET.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as express from 'express'
import * as request from 'supertest'
import resty, { DataAdapter, Pagination, Query, RequestDataSource } from '../src'
import resty, { DataAdapter } from '../src'

interface User {
name: string
Expand Down
Loading

0 comments on commit f9e7086

Please sign in to comment.