bootpress 2.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,62 +1,90 @@
1
- <h1 align="center" style="margin-bottom: 0" >bootpress</h1>
2
- <p align=center>Express but SpringBoot like</p>
1
+ <h1 align="center" style="margin-bottom: 0" >
2
+ <img src="bootpress.svg" height=120 alt="bootpress">
3
+ </h1>
4
+ <p align=center>Express but Spring Boot like</p>
3
5
 
4
6
  ## Methods
5
- ### **RestService**: Converts all methods to Express RequestHandlers
7
+ ### **<u>RestService</u>**: Converts all methods to Express RequestHandlers
6
8
  #### Basic usage:
7
9
  ```ts
10
+ import express from "express";
11
+ import bodyparser from "body-parser";
12
+ import { HttpError, PassParams, RestService } from "bootpress";
13
+ import { asInteger, getOrThrow } from "bootpress/helpers";
14
+
15
+ const app = express();
16
+ app.use(bodyparser.json());
17
+
8
18
  const UserServiceImpl = {
9
19
  users: [1, 2, 3, 4],
10
20
  findAllUsers(): number[] {
11
21
  return this.users;
12
22
  },
13
- findUserById(id: number) {
14
- return this.users.find(user => user == id);
23
+ findUserById(idInParams: string) {
24
+ const id = asInteger(idInParams);
25
+ return getOrThrow(this.users.find(user => user == id), new HttpError(404, "Not Found"));
15
26
  }
16
27
  };
17
28
 
18
29
  const UserService = RestService(UserServiceImpl);
19
30
 
20
- app.get("/users", UserService.findAllUsers);
21
- app.get("/users/:id", (req) => UserService.findUserById(+req.params.id));
31
+ app.get("/users", UserService.findAllUsers());
32
+ app.get("/users/:id", PassParams("id")(UserService.findUserById));
22
33
  ```
23
34
 
24
35
  #### Advanced usage:
25
36
  ```ts
37
+ import { HttpError, HttpResponse, PassBody, PassParams, PassQueries, RestService } from "bootpress";
38
+ import { asInteger, asSchema, getOrThrow } from "bootpress/helpers";
39
+
26
40
  class PostServiceImpl {
27
41
  posts = [1, 2, 3, 4, 5];
28
42
  findById(id: number | string) {
43
+ console.log("looking for " + id);
29
44
  return getOrThrow(
30
45
  this.posts.find(p => p == id),
31
46
  new HttpError(404, "Post is not found")
32
47
  );
33
48
  }
34
- add(id: number) {
35
- this.posts.push(id);
36
- return new HttpResponse(201, id);
49
+ add(body: any) {
50
+ let casted = asSchema(body, {
51
+ "id": "number"
52
+ });
53
+ this.posts.push(casted.id);
54
+ return new HttpResponse(201, casted.id);
37
55
  }
38
- delete(id: number) {
39
- const idx = this.posts.indexOf(id);
56
+ delete(deleteInQuery: string, idInQuery: string) {
57
+ const idx = deleteInQuery === "yes" ? this.posts.indexOf(asInteger(idInQuery)) : -1;
40
58
  if (idx > -1) {
41
59
  this.posts.splice(idx, 1);
42
- this.#printDeleted(id);
60
+ this.#printDeleted(idInQuery);
43
61
  }
44
62
  }
45
63
  // use private methods to
46
64
  #printDeleted(id: number | string) {
47
65
  console.warn(`post ${id} is deleted`)
48
66
  }
67
+ findAll() {
68
+ return this.posts;
69
+ }
49
70
  }
50
71
 
51
- const PostService = RestService(new PostServiceImpl());
72
+ const PostService = RestService(PostServiceImpl);
73
+ // this is valid too:
74
+ // const PostService = RestService(new PostServiceImpl());
52
75
 
53
- app.get("/posts/:id", (req) => PostService.findById(req.params.id));
54
- app.post("/posts/:id", (req) => PostService.add(+req.params.id));
76
+ app.get("/posts", PostService.findAll())
77
+ app.post("/posts", PassBody(PostService.add));
78
+ app.delete("/posts", PassQueries("delete", "id")(PostService.delete));
79
+ app.get("/posts/:id", PassParams("id")(PostService.findById));
55
80
  ```
56
81
 
57
- ### **RestMethod**: Converts single method to RequestHandler
82
+ ### **<u>RestMethod</u>**: Converts single method to RequestHandler
58
83
  #### Usage:
59
84
  ```ts
85
+ import { HttpError, RestMethod } from "bootpress";
86
+ import { getOrThrow } from "bootpress/helpers";
87
+
60
88
  class UserService {
61
89
  users = [1, 2, 3, 4];
62
90
  findAll() {
@@ -77,10 +105,13 @@ app.get("/users", userService.findAll())
77
105
  app.get("/users/:id", (req) => userService.findById(+req.params.id))
78
106
  ```
79
107
 
80
- ### **Restify**: Decorator to convert a single method to RequestHandler
108
+ ### **<u>Restify</u>**: Decorator to convert a single method to RequestHandler
81
109
  #### Note that currently decorators in Typescript doesn't support changing the return type of applied method. So you have to provide RequestHandler as an "or type":
82
110
 
83
111
  ```ts
112
+ import { Restify } from "bootpress";
113
+ import { RequestHandler } from "express";
114
+
84
115
  class LogServiceImpl {
85
116
  logs = ["log1", "log2", "log3"];
86
117
 
@@ -1,3 +1,23 @@
1
1
  import { HttpError } from "..";
2
2
 
3
+ type TypeMap = {
4
+ "string": string,
5
+ "string?": string | null,
6
+ "boolean": boolean,
7
+ "boolean?": boolean | null,
8
+ "number": number,
9
+ "number?": number | null,
10
+ }
11
+
12
+ type JsSchema = {
13
+ [key: string]: keyof TypeMap | JsSchema
14
+ }
15
+
16
+ type SchemadRecord<T> = { [E in keyof T]: T[E] extends string ? TypeMap[T[E]] : SchemadRecord<T[E]> };
17
+
3
18
  export function getOrThrow<T, E extends HttpError>(data: T, error: E): T;
19
+ export function getOrElse<T, E>(data: T, defaultValue: E): T | E;
20
+ export function asBoolean(o: any): boolean;
21
+ export function asNumber(o: any): number;
22
+ export function asInteger(o: any): number;
23
+ export function asSchema<T extends JsSchema>(o: any, jsSchema: T): SchemadRecord<T>
package/helpers/index.js CHANGED
@@ -1,11 +1,100 @@
1
- function getOrThrow(data, error){
2
- if(data === null || data === undefined){
1
+ const { HttpError } = require("..");
2
+
3
+ function getOrThrow(data, error) {
4
+ if (data === null || data === undefined) {
3
5
  throw error;
4
- }else{
6
+ } else {
7
+ return data;
8
+ }
9
+ }
10
+
11
+ function getOrElse(data, defaultValue) {
12
+ if (data === null || data === undefined) {
13
+ return defaultValue;
14
+ } else {
5
15
  return data;
6
16
  }
7
17
  }
8
18
 
19
+ function asBoolean(o, errorMessage = undefined, errorStatus = 400) {
20
+ errorMessage = errorMessage ?? `Value ${o} should have been a boolean but it's not`;
21
+ if (typeof o === "string") {
22
+ const lowercased = o.toLowerCase();
23
+ const validBooleanStrings = new Map(Object.entries({
24
+ "true": true,
25
+ "false": false,
26
+ "1": true,
27
+ "0": false,
28
+ "yes": true,
29
+ "no": false
30
+ }));
31
+ if (!validBooleanStrings.has(lowercased)) {
32
+ throw new HttpError(errorStatus, errorMessage);
33
+ }
34
+ return validBooleanStrings.get(lowercased);
35
+ } else if (typeof o === "boolean") {
36
+ return o;
37
+ }
38
+ throw new HttpError(errorStatus, errorMessage);
39
+ }
40
+
41
+ function asNumber(o, errorMessage = undefined, errorStatus = 400) {
42
+ errorMessage = errorMessage ?? `Value ${o} should have been a number but it's not`
43
+ if (typeof o === "number") {
44
+ return o;
45
+ }
46
+ else if (typeof o === "string") {
47
+ const castedValue = Number(o);
48
+ if (isNaN(castedValue)) {
49
+ throw new HttpError(errorStatus, errorMessage);
50
+ }
51
+ }
52
+ throw new HttpError(errorStatus, errorMessage);
53
+ }
54
+
55
+ function asInteger(o, errorMessage = undefined, errorStatus = 400) {
56
+ errorMessage = errorMessage ?? `Value ${o} should have been a integer but it's not`;
57
+ let value = o;
58
+ if (typeof o === "string") {
59
+ value = Number(o);
60
+ }
61
+ if (!Number.isInteger(value)) {
62
+ throw new HttpError(errorStatus, errorMessage);
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function asSchema(o, schema){
68
+ const schemaKeyValues = Object.entries(schema);
69
+ for(let i = 0; i < schemaKeyValues.length; i ++){
70
+ const key = schemaKeyValues[i][0];
71
+ const expectedType = schemaKeyValues[i][1];
72
+ const errorMessage = `Value of ${key} should have been a ${expectedType} but it's a ${typeof o[key]}`;
73
+
74
+ if(typeof expectedType === "object"){
75
+ asSchema(o[key], expectedType);
76
+ }
77
+ else if(typeof expectedType === "string"){
78
+ if(expectedType.endsWith("?") && o[key] == null){
79
+ continue;
80
+ }
81
+ expectedType.replace("?", "");
82
+ if(typeof o[key] !== expectedType){
83
+ throw new HttpError(400, errorMessage);
84
+ }
85
+ }
86
+ else {
87
+ throw new HttpError(500, `Type of a schema key should be a primitive type or another schema`);
88
+ }
89
+ }
90
+ return o;
91
+ }
92
+
9
93
  module.exports = {
10
- getOrThrow
94
+ getOrThrow,
95
+ getOrElse,
96
+ asBoolean,
97
+ asNumber,
98
+ asInteger,
99
+ asSchema
11
100
  }
package/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { RequestHandler } from "express"
1
+ import { Response } from "express"
2
+ import { Request } from "express"
3
+
4
+ type RequestHandler = (req: Request, res: Response) => void
5
+ type RequsetHandlerWithArgs = (...args: any[]) => RequestHandler
2
6
 
3
7
  declare class HttpError extends Error {
4
8
  status: number
@@ -14,18 +18,36 @@ declare class HttpResponse<T> {
14
18
 
15
19
  type RestedService<T extends Record<string, any>> = { [K in keyof T]:
16
20
  T[K] extends Function
17
- ? (...args: Parameters<T[K]>) => ((req: Request, res: Response) => void)
21
+ ? (...args: Parameters<T[K]>) => RequestHandler
18
22
  : T[K]
19
23
  }
20
24
 
25
+ declare function RestService<T extends Record<string, any>>(clazz: new () => T): RestedService<T>;
21
26
  declare function RestService<T extends Record<string, any>>(service: T): RestedService<T>;
22
27
  declare function RestMethod<T>(callback: () => T): RequestHandler;
23
28
  declare function Restify(target: any, key: string, desc: PropertyDescriptor): PropertyDescriptor;
24
29
 
30
+ declare function PassBody(serviceFunction: RequestHandler | RequsetHandlerWithArgs): RequestHandler
31
+ declare function PassRequest(serviceFunction: RequestHandler | RequsetHandlerWithArgs): RequestHandler
32
+ declare function PassAllParams(serviceFunction: RequestHandler | RequsetHandlerWithArgs): RequestHandler
33
+ declare function PassAllQueries(serviceFunction: RequestHandler | RequsetHandlerWithArgs): RequestHandler
34
+ declare function PassAllCookies(serviceFunction: RequestHandler | RequsetHandlerWithArgs): RequestHandler
35
+ declare function PassParams(...paramNames: string[]): (serviceFunction: RequestHandler | RequsetHandlerWithArgs) => RequestHandler
36
+ declare function PassQueries(...queryNames: string[]): (serviceFunction: RequestHandler | RequsetHandlerWithArgs) => RequestHandler
37
+ declare function PassCookies(...cookieNames: string[]): (serviceFunction: RequestHandler |RequsetHandlerWithArgs) => RequestHandler
38
+
25
39
  export {
26
40
  HttpError,
27
41
  HttpResponse,
28
42
  RestService,
29
43
  RestMethod,
30
- Restify
44
+ Restify,
45
+ PassParams,
46
+ PassAllParams,
47
+ PassQueries,
48
+ PassAllQueries,
49
+ PassCookies,
50
+ PassAllCookies,
51
+ PassBody,
52
+ PassRequest
31
53
  }
package/index.js CHANGED
@@ -14,27 +14,57 @@ class HttpResponse {
14
14
  }
15
15
  }
16
16
 
17
+ const protectedProperties = [
18
+ "toString",
19
+ "toJSON",
20
+ "valueOf",
21
+ "toLocaleString"
22
+ ]
23
+
17
24
  function RestService(service) {
18
- return Object.fromEntries(
19
- Object.entries(service).map(keyvalue => {
20
- if (typeof keyvalue[1] == "function") {
21
- return [
22
- keyvalue[0],
23
- (...args) =>
25
+ if (typeof service == "function") {
26
+ try {
27
+ service = service();
28
+ } catch (e) {
29
+ service = new service();
30
+ }
31
+ }
32
+ const descriptors = {
33
+ ...Object.getOwnPropertyDescriptors(service),
34
+ ...Object.getOwnPropertyDescriptors(service.__proto__ || {})
35
+ };
36
+ const alteredDescriptors = Object.fromEntries(Object.entries(descriptors).filter(keyvalue => !protectedProperties.includes(keyvalue[0])).map(keyvalue => {
37
+ const propertyName = keyvalue[0];
38
+ const value = keyvalue[1].value;
39
+ if (typeof value == "function" && !propertyName.startsWith("#")) {
40
+ return [
41
+ propertyName,
42
+ {
43
+ value: ((...args) =>
24
44
  (req, res) => {
25
45
  try {
26
- const result = keyvalue[1](...args);
46
+ const result = value.bind(service)(...args);
47
+ if (result === undefined) {
48
+ throw new HttpError(200, "Your method is executed but it returned undefined. Please avoid using 'void' methods as service methods.");
49
+ } else if (result === null) {
50
+ throw new HttpError(200, "Your method is executed but it returned null. At least a value is expected to be returned.");
51
+ }
27
52
  res.status(result.status || 200).json(result.data || result);
28
53
  } catch (e) {
29
- res.status(e.status || 500).json(e.message || e);
54
+ res.status(e.status || 500).send(e.message || e);
30
55
  }
31
- }
32
- ]
33
- } else {
34
- return keyvalue;
35
- }
36
- })
37
- );
56
+ }),
57
+ configurable: keyvalue[1].configurable,
58
+ writable: keyvalue[1].writable,
59
+ enumerable: false
60
+ }
61
+ ]
62
+ } else {
63
+ return keyvalue;
64
+ }
65
+ }));
66
+ Object.defineProperties(service, alteredDescriptors);
67
+ return service;
38
68
  }
39
69
 
40
70
  function RestMethod(callback) {
@@ -53,7 +83,7 @@ function Restify(target, key, desc) {
53
83
  const oldFunc = desc.value;
54
84
  return {
55
85
  ...desc,
56
- value: (...args) => {
86
+ value: ((...args) => {
57
87
  return (req, res) => {
58
88
  try {
59
89
  const result = oldFunc(...args);
@@ -63,6 +93,116 @@ function Restify(target, key, desc) {
63
93
  res.status(e.status || 500).json(e.message || e);
64
94
  }
65
95
  }
96
+ }).bind(target)
97
+ }
98
+ }
99
+
100
+
101
+ function isResponse(o) {
102
+ return o instanceof Object && "socket" in o && "parser" in o.socket && "_httpMessage" in o.socket && o.socket._httpMessage.writable == true;
103
+ }
104
+ function isRequest(o) {
105
+ return o instanceof Object && "socket" in o && "url" in o && "body" in o && "params" in o && "query" in o && "res" in o;
106
+ }
107
+
108
+ function isRequstHandlerArgs(args) {
109
+ const [last1, last2, last3, ...others] = [...args].reverse();
110
+ return isResponse(last2) && isRequest(last3);
111
+ }
112
+
113
+ function PassParams(...paramNames) {
114
+ return (actualHandler) => {
115
+ return (...args) => {
116
+ if (isRequstHandlerArgs(args)) {
117
+ const req = args.at(-3); const res = args.at(-2);
118
+ const paramsToPass = paramNames.map(paramName => req.params[paramName]);
119
+ return actualHandler(...paramsToPass)(req, res);
120
+ } else {
121
+ return (req, res) => { const paramsToPass = paramNames.map(paramName => req.params[paramName]); return actualHandler(...args, ...paramsToPass)(req, res); };
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ function PassQueries(...searchQueries) {
128
+ return (actualHandler) => {
129
+ return (...args) => {
130
+ if (isRequstHandlerArgs(args)) {
131
+ const req = args.at(-3); const res = args.at(-2);
132
+ const paramsToPass = searchQueries.map(query => req.query[query]);
133
+ return actualHandler(...paramsToPass)(req, res);
134
+ } else {
135
+ return (req, res) => { const paramsToPass = searchQueries.map(query => req.query[query]); return actualHandler(...args, ...paramsToPass)(req, res); };
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ function PassAllParams(actualHandler) {
142
+ return (...args) => {
143
+ if (isRequstHandlerArgs(args)) {
144
+ const req = args.at(-3); const res = args.at(-2);
145
+ return actualHandler(req.params)(req, res);
146
+ } else {
147
+ return (req, res) => actualHandler(...args, req.params)(req, res);
148
+ }
149
+ }
150
+ }
151
+
152
+ function PassAllQueries(actualHandler) {
153
+ return (...args) => {
154
+ if (isRequstHandlerArgs(args)) {
155
+ const req = args.at(-3); const res = args.at(-2);
156
+ return actualHandler(req.query)(req, res);
157
+ } else {
158
+ return (req, res) => actualHandler(...args, req.query)(req, res);
159
+ }
160
+ }
161
+ }
162
+
163
+ function PassBody(actualHandler) {
164
+ return (...args) => {
165
+ if (isRequstHandlerArgs(args)) {
166
+ const req = args.at(-3); const res = args.at(-2);
167
+ return actualHandler(req.body)(req, res);
168
+ } else {
169
+ return (req, res) => actualHandler(...args, req.body)(req, res)
170
+ }
171
+ }
172
+ }
173
+
174
+ function PassRequest(actualHandler) {
175
+ return (...args) => {
176
+ if (isRequstHandlerArgs(args)) {
177
+ const req = args.at(-3); const res = args.at(-2);
178
+ return actualHandler(req)(req, res);
179
+ } else {
180
+ return (req, res) => actualHandler(...args, req)(req, res)
181
+ }
182
+ }
183
+ }
184
+
185
+ function PassAllCookies(actualHandler) {
186
+ return (...args) => {
187
+ if (isRequstHandlerArgs(args)) {
188
+ const req = args.at(-3); const res = args.at(-2);
189
+ return actualHandler(req.cookies)(req, res);
190
+ } else {
191
+ return (req, res) => actualHandler(...args, req.cookies)(req, res);
192
+ }
193
+ }
194
+ }
195
+
196
+ function PassCookies(...cookieNames) {
197
+ return (actualHandler) => {
198
+ return (...args) => {
199
+ if (isRequstHandlerArgs(args)) {
200
+ const req = args.at(-3); const res = args.at(-2);
201
+ const paramsToPass = cookieNames.map(cookie => req.cookies[cookie]);
202
+ return actualHandler(...paramsToPass)(req, res);
203
+ } else {
204
+ return (req, res) => { const paramsToPass = cookieNames.map(cookie => req.cookies[cookie]); return actualHandler(...args, ...paramsToPass)(req, res) };
205
+ }
66
206
  }
67
207
  }
68
208
  }
@@ -72,5 +212,13 @@ module.exports = {
72
212
  HttpResponse,
73
213
  RestService,
74
214
  RestMethod,
75
- Restify
215
+ Restify,
216
+ PassParams,
217
+ PassAllParams,
218
+ PassQueries,
219
+ PassAllQueries,
220
+ PassAllCookies,
221
+ PassCookies,
222
+ PassBody,
223
+ PassRequest
76
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bootpress",
3
- "version": "2.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "REST service methods for express",
5
5
  "main": "index.js",
6
6
  "scripts": {},
@@ -24,10 +24,10 @@
24
24
  "url": "https://github.com/ufukbakan/bootpress/issues"
25
25
  },
26
26
  "homepage": "https://github.com/ufukbakan/bootpress#readme",
27
- "devDependencies": {
28
- "@types/express": "^4.17.17"
29
- },
30
27
  "dependencies": {
28
+ "@types/body-parser": "^1.19.2",
29
+ "@types/express": "^4.17.17",
30
+ "body-parser": "^1.20.2",
31
31
  "express": "^4.18.2"
32
32
  }
33
33
  }