Skip to content

Commit 580f15c

Browse files
committed
added factory, utils, errors and types
1 parent bcba789 commit 580f15c

File tree

26 files changed

+633
-20
lines changed

26 files changed

+633
-20
lines changed

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@
88

99
express.js implementation of js-items
1010

11-
There is a fantastic alternative to this project (and @js-items/express is based on it):
11+
There is a fantastic alternative to this project (and @js-items/express is based on it):
1212
[js-entity-repos/express](https://github.com/js-entity-repos/express).
1313

1414
The main differences to the @js-entity-repos/express:
15+
1516
- different naming convention: using `item` instead of `entity`
1617
- cursor based pagination operates using `before` and `after` instead of `cursor` and `direction`
17-
- enveloping response (GET /items) which allows client to more easily discover the pagination and metadata
18-
- transactions handlers for each request handler - this could be used for i.e. permissions checks
19-
- possibility to override each request handler
20-
- no /count route as not restful
18+
- `enveloping` response (GET /items) which allows client to more easily discover the pagination and metadata
19+
- `granular transactions handlers` for each `request handler` - this could be used for i.e. `authentication` or `permissions` checks
20+
- possibility to `override` each `request handler`
21+
- ability to disable json body parser middleware if already present in the stack (`enableJsonBodyParser: false`)
22+
- replaced /count path with optional header available on get request
2123

2224
## Installation:
25+
2326
`npm i @js-items/express --save`
2427

2528
Credits:
26-
- [ryansmith94](https://github.com/ryansmith94)
29+
30+
- [ryansmith94](https://github.com/ryansmith94)
31+
- [best-practices-for-a-pragmatic-restful-api](https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api)

package-lock.json

Lines changed: 36 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"@types/express": "^4.16.1",
3131
"@types/jest": "24.0.11",
3232
"@types/node": "11.12.1",
33+
"@types/ramda": "^0.26.6",
34+
"@types/uuid": "^3.4.4",
3335
"jest": "24.5.0",
3436
"rimraf": "2.6.3",
3537
"ts-jest": "24.0.0",
@@ -41,11 +43,16 @@
4143
"access": "public"
4244
},
4345
"dependencies": {
46+
"body-parser": "^1.18.3",
47+
"boolean": "^1.0.0",
4448
"codecov": "^3.2.0",
4549
"dotenv": "^7.0.0",
4650
"express": "^4.16.4",
51+
"http-status-codes": "^1.3.2",
4752
"jscpd": "^2.0.11",
4853
"jscpd-badge-reporter": "^1.1.3",
49-
"make-error": "^1.3.5"
54+
"make-error": "^1.3.5",
55+
"ramda": "^0.26.1",
56+
"uuid": "^3.3.2"
5057
}
5158
}

src/FacadeConfig.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
import { Item } from '@js-items/foundation';
22
import Facade from '@js-items/foundation/dist/Facade';
3+
import CreateFilter from './types/CreateFilter';
4+
import CreatePatch from './types/CreatePatch';
5+
import DocumentIntoItem from './types/DocumentIntoItem';
6+
import ItemIntoDocument from './types/ItemIntoDocument';
7+
import TransactionHandler from './types/TransactionHandler';
38

4-
// tslint:disable-next-line:no-empty-interface
59
export default interface FacadeConfig<I extends Item> {
10+
readonly totalHeaderName: string;
11+
readonly beforeHeaderName: string;
12+
readonly hasAfterHeaderName: string;
13+
readonly hasBeforeHeaderName: string;
14+
readonly afterHeaderName: string;
15+
readonly envelopeParamName: string;
16+
readonly prettyParamName: string;
17+
readonly createFilter: CreateFilter<I>;
18+
readonly createPatch: CreatePatch<I>;
19+
readonly convertDocumentIntoItem: DocumentIntoItem<I>;
20+
readonly convertItemIntoDocument: ItemIntoDocument<I>;
21+
readonly beforeGetItem?: TransactionHandler;
22+
readonly beforeUpdateItem?: TransactionHandler;
23+
readonly beforeReplaceItem?: TransactionHandler;
24+
readonly beforeDeleteItem?: TransactionHandler;
25+
readonly beforeGetItems?: TransactionHandler;
26+
readonly beforeCreateItem?: TransactionHandler;
27+
readonly beforeDeleteItems?: TransactionHandler;
28+
readonly defaultTransactionHandler: TransactionHandler;
29+
readonly defaultPaginationLimit: number;
630
readonly service: Facade<I>;
731
}

src/FactoryConfig.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import TransactionHandler from './types/TransactionHandler';
88
import RequestHandlerFactory from './types/RequestHandlerFactory';
99

1010
export default interface FactoryConfig<I extends Item> {
11+
readonly enableJsonBodyParser?: boolean;
12+
readonly totalHeaderName?: string;
13+
readonly hasAfterHeaderName?: string;
14+
readonly afterHeaderName?: string;
15+
readonly hasBeforeHeaderName?: string;
16+
readonly beforeHeaderName?: string;
17+
readonly envelopParamName?: string;
18+
readonly prettyParamName?: string;
1119
readonly createFilter?: CreateFilter<I>;
1220
readonly createPatch?: CreatePatch<I>;
1321
readonly convertDocumentIntoItem?: DocumentIntoItem<I>;

src/errors/JsonError.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// tslint:disable:no-any
2+
import { BaseError } from 'make-error';
3+
4+
export default class JsonError extends BaseError {
5+
6+
public data: any;
7+
public path: string[];
8+
9+
constructor(data: any, path: string[]) {
10+
super();
11+
this.data = data;
12+
this.path = path;
13+
}
14+
}

src/errors/NumberError.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// tslint:disable:no-any
2+
import { BaseError } from 'make-error';
3+
4+
export default class NumberError extends BaseError {
5+
6+
public data: any;
7+
public path: string[];
8+
9+
constructor(data: any, path: string[]) {
10+
super();
11+
this.data = data;
12+
this.path = path;
13+
}
14+
}

src/factory.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Item } from '@js-items/foundation';
2+
import { json } from 'body-parser';
3+
import { Router } from 'express';
4+
import _defaultTo from 'ramda/src/defaultTo';
5+
import FacadeConfig from './FacadeConfig';
6+
import FactoryConfig from './FactoryConfig';
7+
import defaultCreateItem from './functions/createItem';
8+
import defaultDeleteItem from './functions/deleteItem';
9+
// import defaultDeleteItems from './functions/deleteItems';
10+
import defaultGetItem from './functions/getItem';
11+
// import defaultGetItems from './functions/getItems';
12+
// import defaultReplaceItem from './functions/replaceItem';
13+
// import defaultUpdateItem from './functions/updateItem';
14+
import defaultTransactionHandler from './utils/defaultTransactionHandler';
15+
16+
export default <I extends Item>({
17+
deleteItem,
18+
deleteItems,
19+
getItem,
20+
getItems,
21+
updateItem,
22+
replaceItem,
23+
createItem,
24+
totalHeaderName,
25+
hasAfterHeaderName,
26+
afterHeaderName,
27+
hasBeforeHeaderName,
28+
beforeHeaderName,
29+
envelopParamName,
30+
prettyParamName,
31+
...config
32+
}: FactoryConfig<I>): Router => {
33+
const customTotalHeaderName = _defaultTo('x-total-count')(totalHeaderName);
34+
const customHasBeforeHeaderName = _defaultTo('x-has-before')(hasBeforeHeaderName);
35+
const customBeforeHeaderName = _defaultTo('x-before-cursor')(beforeHeaderName);
36+
const customHasAfterHeaderName = _defaultTo('x-has-after')(hasAfterHeaderName);
37+
const customAfterHeaderName = _defaultTo('x-after-cursor')(afterHeaderName);
38+
const customEnvelopParamName = _defaultTo('envelope')(envelopParamName);
39+
const customPrettyParamName = _defaultTo('pretty')(prettyParamName);
40+
41+
const facadeConfig: FacadeConfig<I> = {
42+
afterHeaderName: customAfterHeaderName,
43+
beforeHeaderName: customBeforeHeaderName,
44+
convertDocumentIntoItem: ({ document }) => document,
45+
convertItemIntoDocument: ({ item }) => item,
46+
createFilter: ({ filter }) => filter,
47+
createPatch: ({ document }) => document,
48+
defaultPaginationLimit: 10,
49+
defaultTransactionHandler,
50+
envelopeParamName: customEnvelopParamName,
51+
hasAfterHeaderName: customHasAfterHeaderName,
52+
hasBeforeHeaderName: customHasBeforeHeaderName,
53+
prettyParamName: customPrettyParamName,
54+
totalHeaderName: customTotalHeaderName,
55+
...config,
56+
};
57+
58+
const router = Router();
59+
const bodyParserEnabled = _defaultTo(true)(config.enableJsonBodyParser);
60+
61+
if (bodyParserEnabled) {
62+
router.use(json());
63+
}
64+
65+
const deleteItemFactory = _defaultTo(defaultDeleteItem)(deleteItem);
66+
const getItemFactory = _defaultTo(defaultGetItem)(getItem);
67+
// const updateItemFactory = _defaultTo(defaultUpdateItem)(updateItem);
68+
// const replaceItemFactory = _defaultTo(defaultReplaceItem)(replaceItem);
69+
// const deleteItemsFactory = _defaultTo(defaultDeleteItems)(deleteItems);
70+
// const getItemsFactory = _defaultTo(defaultGetItems)(getItems);
71+
const createItemFactory = _defaultTo(defaultCreateItem)(createItem);
72+
73+
router.delete('/:id', deleteItemFactory(facadeConfig));
74+
router.get('/:id', getItemFactory(facadeConfig));
75+
// router.patch('/:id', updateItemFactory(facadeConfig));
76+
// router.put('/:id', replaceItemFactory(facadeConfig));
77+
// router.delete('', deleteItemsFactory(facadeConfig));
78+
// router.get('', getItemsFactory(facadeConfig));
79+
router.post('', createItemFactory(facadeConfig));
80+
81+
return router;
82+
};

src/functions/createItem/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Item } from '@js-items/foundation';
2+
import { Request, Response } from 'express';
3+
import { OK } from 'http-status-codes';
4+
import _defaultTo from 'ramda/src/defaultTo';
5+
import FacadeConfig from '../../FacadeConfig';
6+
import RequestHandlerFactory from '../../types/RequestHandlerFactory';
7+
8+
const createItem: RequestHandlerFactory = <I extends Item>(config: FacadeConfig<I>) => async (
9+
req: Request,
10+
res: Response
11+
) => {
12+
const transactionHandler = _defaultTo(config.defaultTransactionHandler)(config.beforeCreateItem);
13+
14+
await transactionHandler({ req, res }, async () => {
15+
16+
const { item } = await config.service.createItem({
17+
id: req.body.id,
18+
item: config.convertDocumentIntoItem({ document: req.body, req, res }),
19+
});
20+
21+
res.status(OK).json(config.convertItemIntoDocument({ item, req, res }));
22+
});
23+
};
24+
25+
export default createItem;

src/functions/deleteItem/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Item } from '@js-items/foundation';
2+
import { Request, Response } from 'express';
3+
import { NO_CONTENT } from 'http-status-codes';
4+
import _defaultTo from 'ramda/src/defaultTo';
5+
import FacadeConfig from '../../FacadeConfig';
6+
import RequestHandlerFactory from '../../types/RequestHandlerFactory';
7+
import getJsonQueryParam from '../utils/getJsonQueryParam';
8+
9+
const deleteItem: RequestHandlerFactory = <I extends Item>(
10+
config: FacadeConfig<I>
11+
) => async (req: Request, res: Response) => {
12+
const transactionHandler = _defaultTo(config.defaultTransactionHandler)(
13+
config.beforeDeleteItem
14+
);
15+
16+
await transactionHandler({ req, res }, async () => {
17+
const filter = getJsonQueryParam(req.query, 'filter');
18+
19+
await config.service.deleteItem({
20+
filter: config.createFilter({ filter, req, res }),
21+
id: req.params.id,
22+
});
23+
24+
res.status(NO_CONTENT).send();
25+
});
26+
};
27+
28+
export default deleteItem;

0 commit comments

Comments
 (0)