From f9e7086826ad777254aa93d55a6535f29bfc1c41 Mon Sep 17 00:00:00 2001 From: Marco Casula Date: Thu, 17 Sep 2020 09:47:12 -0600 Subject: [PATCH] add route conditionally (#8) * add route conditionally * test routes * impl errorHandler closes https://github.com/kedoska/resty/issues/3 --- README.md | 87 ++++++++++++++++--- package-lock.json | 31 ++++++- package.json | 4 +- src/index.ts | 188 ++++++++++++++++++++++++------------------ tests/GET.test.ts | 2 +- tests/config.test.ts | 38 +++++++++ tests/handler.test.ts | 31 +++++++ 7 files changed, 287 insertions(+), 94 deletions(-) create mode 100644 tests/config.test.ts create mode 100644 tests/handler.test.ts diff --git a/README.md b/README.md index 8bd0498..e6f3661 100644 --- a/README.md +++ b/README.md @@ -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**.
-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 => + 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 @@ -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.
+The function handles eventual rejections coming from the data-adapter.
-### 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/`.
+It will fail, returning status `200 OK`, having the following body:
- - 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" +} +``` diff --git a/package-lock.json b/package-lock.json index 2ccb875..42f9b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@kedoska/resty", - "version": "1.0.29", + "version": "0.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4693,6 +4693,35 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "dev": true, + "requires": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", diff --git a/package.json b/package.json index a8f1e8a..498fd3d 100644 --- a/package.json +++ b/package.json @@ -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", @@ -37,6 +38,7 @@ "whatwg-fetch": "^3.0.0" }, "peerDependencies": { - "express": "^4.17.1" + "express": "^4.17.1", + "method-override": "^3.0.0" } } diff --git a/src/index.ts b/src/index.ts index a8402fa..001772f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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()) @@ -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 } diff --git a/tests/GET.test.ts b/tests/GET.test.ts index 40c8e08..235d356 100644 --- a/tests/GET.test.ts +++ b/tests/GET.test.ts @@ -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 diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 0000000..45bccec --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,38 @@ +import * as express from 'express' +import * as request from 'supertest' +import resty, { DataAdapter } from '../src' + +const dataAdapter: DataAdapter = {} + +const emptyRouter = resty({ + version: 'v1', + resource: 'users', + dataAdapter, +}) + +const app = express() +app.use(emptyRouter) + +test('createOneHandler - error', function (done) { + request(app).get('/v1/users').expect(404).end(done) +}) + +test('selectManyHandler - error', function (done) { + request(app).post('/v1/users').expect(404).end(done) +}) + +test('selectOneHandler - error', function (done) { + request(app).get('/v1/users/1').expect(404).end(done) +}) + +test('updateOneHandler - error', function (done) { + request(app).put('/v1/users/1').expect(404).end(done) +}) + +test('deleteOneHandler - error', function (done) { + request(app).delete('/v1/users/1').expect(404).end(done) +}) + +test('deleteAllHandler - error', function (done) { + request(app).delete('/v1/users').expect(404).end(done) +}) \ No newline at end of file diff --git a/tests/handler.test.ts b/tests/handler.test.ts new file mode 100644 index 0000000..3e234e8 --- /dev/null +++ b/tests/handler.test.ts @@ -0,0 +1,31 @@ +import * as express from 'express' +import * as request from 'supertest' +import { createOneHandler, errorHandler, RestOptions } from '../src' + +const options: RestOptions = { + version: 'v1', + resource: 'test', + dataAdapter: {}, +} + +const app = express() +app.use(express.json()) +app.use(express.urlencoded({ extended: false })) + + +test('GET ALL', function (done) { + + app.get('/', createOneHandler(options)) + app.use(errorHandler) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) return done(err) + expect(res.body.message).toBe('createOne not yet implemented') + done() + }) + +})