-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
249 lines (200 loc) · 8.95 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/**
** TOC:
* - setup .................... #L15
* - endpoints ................ #L25
* - without parameters ..... #L31
* - with parameters ........ #L69
* - with wildcard .......... #L85
* - middleware ............... #L98
* - general middleware ..... #L106
* - specific middleware .... #L114
* - types .................... #L126
* - utility functions ........ #L177
*/
/**
** ================ Setup ===================
*/
import { LOG } from '@peter-schweitzer/ez-utils';
import { App, buildRes, getBodyJSON, MIME, serveFromFS, throw404 } from '../index.js';
const app = new App();
app.listen(65535);
/**
** ================ Endpoints ===================
*/
// An Endpoint resolves the specified URI.
/* ================ without param =================== */
// Endpoints without parameters have the highest specificity, so they get matched first
// these are the most specific, as they only resolve a specific URI, requested via a specific method (also supports head, connect and trace)
app.get('/get', (_req, res, _params) => {
buildRes(res, 'get');
});
app.put('/put', (_req, res, _params) => {
buildRes(res, 'put');
});
app.post('/post', (_req, res, _params) => {
buildRes(res, 'post');
});
app.delete('/delete', (_req, res, _params) => {
buildRes(res, 'delete');
});
app.patch('/patch', (_req, res, _params) => {
buildRes(res, 'patch');
});
app.options('/options', (_req, res, _params) => {
buildRes(res, 'options');
});
// add() ignores the method, hence it's less specific than the examples above
app.add('/', (_req, res, _params) => {
serveFromFS(res, './html/home.html');
});
app.add('/favicon.ico', (_req, res, _params) => {
buildRes(res, null, { code: 404 });
});
/* ================ with parameters =================== */
//params are prefixed with ':' and can be registered with all above mentioned functions
// endpoints with parameters are less specific than endpoints without
// for more info on the Params class look at the types-section
// going to '/echo-route-params/first-param/second-param' will return 'first-param second-param'
app.add('/echo-route-params/:param1/:param2', (_req, res, params) => {
buildRes(res, `${params.route('param1')} ${params.route('param2')}`);
});
// going to '/sum/351/69' will return '420'
app.add('/sum/:a/:b', (_req, res, params) => {
buildRes(res, `${params.routeNumber('a', 0) + params.routeNumber('b', 0)}`);
});
/* ================ with wildcard =================== */
// by having '/:*' as the last part of the URI the endpoint will resolve all requests that start with the URI
// params will include the rest of the requested URI in route
// params.route('*') will return an array of strings (the rest split on every '/')
// going to '/wildcard/a/b/c' will return '3 (a-b-c)'
// going to '/wildcard' will return '0 ()'
app.add('/wildcard/:*', (_req, res, params) => {
const rest = params.route('*');
buildRes(res, `${rest.length} (${rest.join('-')})`);
});
/**
** ================ middleware ===================
*/
// Middleware gets called in order of adding them
// Middleware gets passed the underlying 'query' and 'route' LUT
// When the request gets resolved (i.e. by calling buildRes()) within middleware the rest of the middleware stack and the ResFunction (or 404 function) won't get called
/* ================ general middleware =================== */
// To use middleware that gets called on every request call app.use() with a Middleware
// Calling app.use() also returns app so you can chain .use() calls and/or register middleware directly after instantiating
// you can see the below debug output in the terminal on every request
app.use({ handle: (req, res, query, route) => LOG(`handeling request for '${req.url}', with method '${req.method}'`) });
/* ================ specific middleware =================== */
// By calling use() on MiddlewareCurry you can add middleware to a specific ResFunction
// going to '/middleware/echo' will return "method: 'GET', url: '/middleware/echo', uri: '/middleware/echo'"
app
.add('/middleware/echo', (req, res, params) => buildRes(res, 'this should not be seen'))
.use({ handle: (req, res, query, route) => buildRes(res, `method: '${req.method}', url: '${req.url}', uri: '${req.uri}', query: '${JSON.stringify(query)}'`) });
// going to '/middleware/error' will return 'middleware error'
app.add('/middleware/error', (req, res, params) => buildRes(res, 'this should not be seen')).use({ handle: (req, res, query, params) => 'middleware error' });
/**
** ================ Types ===================
*/
/**
** MIME
* MIME is an Object that provides nice auto completion for the most commonly used mime types.
* Instead of writing them out every time you can just use STD_MIME and auto completion.
* This aim is to:
* - decreases the amount of magic strings in your code
* - avoid errors tue to miss typing
* - spend less time repetitively typing the same few strings over and over
*/
/**
** Methods
* functions that expect a HTTP method use this type
* it is a list of all accepted strings i.e. {"GET"|"HEAD"|"POST"|"PUT"|"DELETE"|"CONNECT"|"OPTIONS"|"TRACE"|"PATCH"}
*/
/**
** ErrorOr<T>
* the ErrorOr type is used to propagate errors in a predictable and consistent way
* it has two properties: 'err' and 'data'
* if an error occurs 'err' is a string and 'data' null, otherwise 'err' is null and data 'T' ('T' is a template and can be any type)
*/
/**
** Params
* every resFunction gets passed a params object as the third argument
* params include all query parameters, as well as route parameters when specified
* route parameters are a component of the route and prefixed with ':' (e.g. '/:' signifies a route parameter)
*/
// going to '/echo-msg?msg=hello&msg2=world' will return 'hello world'
app.add('/echo-msg', (_req, res, params) => {
buildRes(res, `${params.query('msg', '')} ${params.query('msg2', '')}`);
});
/**
** EZIncomingMessage
* EZIncoming is like IncomingMessage from node:http but has the property 'uri', which is like 'url' but without the query parameters
* The query parameters are in the params object
*/
/**
** MiddlewareCurry
* When registering a ResFunction the underlying data structure gets curried into the .use() function.
* Calling use() on a MiddlewareCurry returns the MiddlewareCurry object, so you can chain .use() calls to add multiple middlewares to the same ResFunction.
*/
/**
** ================ Utility Functions ===================
*/
/**
** getBodyJSON(req)
*
* getBodyJSON() takes a EZIncomingMessage and returns the JSON from the request body asynchronously (doesn't reject)
*
* req is passed by EZServer as the first argument to every resFunction
* filePath is the path to the file that should be send to resolve the request
* statusCode is the HTTP-status that should be send as part of the response (default 200 when omitted)
*/
app.get('/echo-json', async (req, res, _params) => {
const { err, data } = await getBodyJSON(req);
if (err !== null) {
console.error(err);
buildRes(res, "couldn't get JSON from request", { code: 500 });
} else {
buildRes(res, data, { mime: MIME.JSON });
}
});
/**
** buildRes(res, data, { code, mime, headers })
*
* buildRes() takes a ServerResponse, an optional string or Buffer (default is null) and optionally an object
*
* res is passed by EZServer as the second argument to every resFunction
* data can be a string or Buffer, that should be send to resolve the request
* the last argument is an optional object containing the properties 'code', 'mime' and 'headers'
* code is the HTTP-status that gets send as part of the response (defaults to 200)
* mime is the mime-type that gets send as part of the response (defaults to "text/plain;charset=UTF-8")
* headers is an object representing key (string) value (string or number) pairs that represent additional headers of the response (defaults to empty Object)
*/
app.get('/hello', (_req, res, _params) => {
buildRes(res, 'Hello, World', { code: 200, mime: MIME.TEXT, headers: {} });
});
/**
** serveFromFS(res, filePath, statusCode)
*
* serveFromFS takes a ServerResponse, a string and optionally a number (default is 200) and returns void
*
* res is passed by EZServer as the second argument to every resFunction
* filePath is the path to the file that should be send to resolve the request
* statusCode is the HTTP-status that should be send as part of the response (defaults to 200 when omitted)
*
* Note: serveFromFS uses buildRes() internally
*/
app.get('/example', (_req, res, _params) => {
serveFromFS(res, './example/index.js');
});
/**
** throw404(req, res)
*
* throw404 takes an EZIncomingMessage and a ServerResponse and returns void
*
* req is passed by EZServer as the first argument to every resFunction
* res is passed by EZServer as the second argument to every resFunction
* throw404() sends a default 404 page to the requester with code 404 and mime 'text/html'
*
* Note: throw404() uses buildRes() internally
*/
app.add('/404', (req, res, _params) => {
throw404(req, res);
});