@esportsplus/routing 0.6.0

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.
@@ -0,0 +1,294 @@
1
+ import { ON_DELETE, ON_GET, ON_POST, ON_PUT, PACKAGE_NAME } from '../constants';
2
+ import { Route, Name, Options, PathParamsTuple, Request, RouteOptions, RouteRegistry } from '../types';
3
+ import { Node } from './node';
4
+ import pipeline from '@esportsplus/pipeline';
5
+
6
+
7
+ function key(method: string, subdomain?: string | null) {
8
+ return (method + (subdomain ? ' ' + subdomain : '')).toUpperCase();
9
+ }
10
+
11
+ function normalize(path: string) {
12
+ if (path) {
13
+ if (path[0] !== '/') {
14
+ path = '/' + path;
15
+ }
16
+
17
+ if (path.length > 1 && path[path.length - 1] === '/') {
18
+ path = path.slice(0, -1);
19
+ }
20
+ }
21
+
22
+ return path || '/';
23
+ }
24
+
25
+ function set<T>(route: Route<T>, options: Options<T> | RouteOptions<T>) {
26
+ let pipeline = route.pipeline;
27
+
28
+ for (let key in options) {
29
+ let value = options[key as keyof typeof options] as any;
30
+
31
+ if (key === 'middleware') {
32
+ for (let i = 0, n = value.length; i < n; i++) {
33
+ pipeline.add(value[i]);
34
+ }
35
+ }
36
+ else if (key === 'responder') {
37
+ pipeline.add(value);
38
+ }
39
+ else {
40
+ // @ts-ignore
41
+ route[key] = (route[key] || '') + value;
42
+ }
43
+ }
44
+ }
45
+
46
+
47
+ class Router<T, TRoutes extends RouteRegistry = {}> {
48
+ bucket: Record<ReturnType<typeof key>, { root: Node<T>, static: Record<string, Route<T>> }> = {};
49
+ groups: Options<T>[] = [];
50
+ routes: Record<Name, Route<T>> = {};
51
+ subdomains: string[] | null = null;
52
+
53
+
54
+ private add(method: string, path: string, route: Route<T>) {
55
+ let bucket = this.bucket[ key(method, route.subdomain) ] ??= {
56
+ root: new Node(),
57
+ static: {}
58
+ };
59
+
60
+ if (path.indexOf(':') === -1) {
61
+ if (path in bucket.static) {
62
+ throw new Error(`${PACKAGE_NAME}: static path '${path}' is already in use`);
63
+ }
64
+
65
+ bucket.static[path] = route;
66
+ }
67
+ else {
68
+ bucket.root.add(path, route);
69
+ }
70
+
71
+ return this;
72
+ }
73
+
74
+ private create(options: RouteOptions<T>) {
75
+ let groups = this.groups,
76
+ route: Route<T> = {
77
+ name: null,
78
+ path: null,
79
+ pipeline: pipeline<Request<T>, T>(),
80
+ subdomain: null
81
+ };
82
+
83
+ for (let i = 0, n = groups.length; i < n; i++) {
84
+ set(route, groups[i]);
85
+ }
86
+
87
+ set(route, options);
88
+
89
+ if (route.path) {
90
+ route.path = normalize(route.path);
91
+ }
92
+
93
+ if (route.subdomain === 'www') {
94
+ route.subdomain = '';
95
+ }
96
+
97
+ return route;
98
+ }
99
+
100
+
101
+ delete<RouteName extends string = string, RoutePath extends string = string>(
102
+ options: RouteOptions<T> & { name?: RouteName; path?: RoutePath }
103
+ ): Router<
104
+ T,
105
+ TRoutes & (
106
+ RouteName extends string
107
+ ? RoutePath extends string
108
+ ? { [K in RouteName]: { path: RoutePath } }
109
+ : TRoutes
110
+ : TRoutes
111
+ )
112
+ > {
113
+ this.on(ON_DELETE, options);
114
+ return this as any;
115
+ }
116
+
117
+ get<RouteName extends string = string, RoutePath extends string = string>(
118
+ options: RouteOptions<T> & { name?: RouteName; path?: RoutePath }
119
+ ): Router<
120
+ T,
121
+ TRoutes & (
122
+ RouteName extends string
123
+ ? RoutePath extends string
124
+ ? { [K in RouteName]: { path: RoutePath } }
125
+ : TRoutes
126
+ : TRoutes
127
+ )
128
+ > {
129
+ this.on(ON_GET, options);
130
+ return this as any;
131
+ }
132
+
133
+ group(options: Options<T>): {
134
+ routes: (fn: (router: Router<T, TRoutes>) => void) => Router<T, TRoutes>
135
+ } {
136
+ return {
137
+ routes: (fn: (router: Router<T, TRoutes>) => void) => {
138
+ this.groups.push(options);
139
+ fn(this);
140
+ this.groups.pop();
141
+ return this;
142
+ }
143
+ };
144
+ }
145
+
146
+ match(method: string, path: string, subdomain?: string | null) {
147
+ let bucket = this.bucket[ key(method, subdomain) ];
148
+
149
+ if (!bucket) {
150
+ return {};
151
+ }
152
+
153
+ path = normalize(path);
154
+
155
+ if (path in bucket.static) {
156
+ return { route: bucket.static[path] };
157
+ }
158
+
159
+ return bucket.root.find(path);
160
+ }
161
+
162
+ on<RouteName extends string = string, RoutePath extends string = string>(
163
+ methods: string[],
164
+ options: RouteOptions<T> & { name?: RouteName; path?: RoutePath }
165
+ ): Router<
166
+ T,
167
+ TRoutes & (
168
+ RouteName extends string
169
+ ? RoutePath extends string
170
+ ? { [K in RouteName]: { path: RoutePath } }
171
+ : TRoutes
172
+ : TRoutes
173
+ )
174
+ > {
175
+ let route = this.create(options);
176
+
177
+ let name = route.name,
178
+ path = route.path,
179
+ subdomain = route.subdomain;
180
+
181
+ if (name) {
182
+ if (this.routes[name]) {
183
+ throw new Error(`${PACKAGE_NAME}: '${name}' is already in use`);
184
+ }
185
+
186
+ this.routes[name] = route;
187
+ }
188
+
189
+ if (path) {
190
+ for (let i = 0, n = methods.length; i < n; i++) {
191
+ let method = methods[i];
192
+
193
+ if (path.indexOf('?:') !== -1) {
194
+ let segments = path.split('?:'),
195
+ url = segments[0];
196
+
197
+ this.add(method, url, route);
198
+
199
+ for (let i = 1; i < segments.length; i++) {
200
+ url += '/:' + segments[i];
201
+ this.add(method, url, route);
202
+ }
203
+ }
204
+ else {
205
+ this.add(method, path, route);
206
+ }
207
+ }
208
+ }
209
+
210
+ if (subdomain) {
211
+ (this.subdomains ??= []).push( subdomain.toLowerCase() );
212
+ }
213
+
214
+ return this as any;
215
+ }
216
+
217
+ post<RouteName extends string = string, RoutePath extends string = string>(
218
+ options: RouteOptions<T> & { name?: RouteName; path?: RoutePath }
219
+ ): Router<
220
+ T,
221
+ TRoutes & (
222
+ RouteName extends string
223
+ ? RoutePath extends string
224
+ ? { [K in RouteName]: { path: RoutePath } }
225
+ : TRoutes
226
+ : TRoutes
227
+ )
228
+ > {
229
+ this.on(ON_POST, options);
230
+ return this as any;
231
+ }
232
+
233
+ put<RouteName extends string = string, RoutePath extends string = string>(
234
+ options: RouteOptions<T> & { name?: RouteName; path?: RoutePath }
235
+ ): Router<
236
+ T,
237
+ TRoutes & (
238
+ RouteName extends string
239
+ ? RoutePath extends string
240
+ ? { [K in RouteName]: { path: RoutePath } }
241
+ : TRoutes
242
+ : TRoutes
243
+ )
244
+ > {
245
+ this.on(ON_PUT, options);
246
+ return this as any;
247
+ }
248
+
249
+ uri<RouteName extends keyof TRoutes & string>(
250
+ name: RouteName,
251
+ values: PathParamsTuple<TRoutes[RouteName]['path']> = [] as any
252
+ ): string {
253
+ let path = this.routes[name]?.path;
254
+
255
+ if (!path) {
256
+ throw new Error(`${PACKAGE_NAME}: route name '${name}' does not exist or it does not provide a path`);
257
+ }
258
+
259
+ let resolved: (string | number)[] = [],
260
+ segments = path.split('/'),
261
+ v = 0;
262
+
263
+ for (let i = 0, n = segments.length; i < n; i++) {
264
+ let segment = segments[i],
265
+ symbol = segment[0];
266
+
267
+ if (symbol === ':') {
268
+ resolved.push((values as (string | number)[])[v++]);
269
+ }
270
+ else if (symbol === '?') {
271
+ if ((values as (string | number)[])[v] === undefined) {
272
+ break;
273
+ }
274
+
275
+ resolved.push((values as (string | number)[])[v++]);
276
+ }
277
+ else if (symbol === '*') {
278
+ for (let n = values.length; v < n; v++) {
279
+ resolved.push((values as (string | number)[])[v]);
280
+ }
281
+ break;
282
+ }
283
+ else {
284
+ resolved.push(segment);
285
+ }
286
+ }
287
+
288
+ return resolved.join('/');
289
+ }
290
+ }
291
+
292
+
293
+ export { Router };
294
+ export type { Route };
@@ -0,0 +1,122 @@
1
+ import { PARAMETER, STATIC, WILDCARD } from '../constants';
2
+ import { Route } from './index';
3
+
4
+
5
+ class Node<T> {
6
+ parent: Node<T> | null = null;
7
+ path: string | null = null;
8
+ route: Route<T> | null = null;
9
+ static: Map<string | number, Node<T>> | null = null;
10
+ type: number | null = null;
11
+
12
+ // Parameter or Wildcard parameter name
13
+ name: string | null = null;
14
+ parameter: Node<T> | null = null;
15
+ wildcard: Node<T> | null = null;
16
+
17
+
18
+ constructor(parent: Node<T>['parent'] = null) {
19
+ this.parent = parent;
20
+ }
21
+
22
+
23
+ add(path: string, route: Route<T>) {
24
+ let node: Node<T> | undefined = this,
25
+ segments = path.split('/'),
26
+ type: Node<T>['type'] = STATIC,
27
+ unnamed = 0;
28
+
29
+ for (let i = 0, n = segments.length; i < n; i++) {
30
+ let segment = segments[i],
31
+ symbol = segment[0];
32
+
33
+ // Parameter
34
+ if (symbol === ':') {
35
+ if (!node.parameter) {
36
+ node.parameter = new Node<T>(node);
37
+ node.parameter.name = (segment.slice(1) || unnamed++).toString();
38
+ }
39
+
40
+ node = node.parameter;
41
+ type = PARAMETER;
42
+ }
43
+ // "*:" Wildcard
44
+ else if (symbol === '*') {
45
+ if (!node.wildcard) {
46
+ node.wildcard = new Node<T>(node);
47
+ node.wildcard.name = (segment.slice(2) || unnamed++).toString();
48
+ }
49
+
50
+ node = node.wildcard;
51
+ type = WILDCARD;
52
+ }
53
+ // Static name
54
+ else {
55
+ let next: Node<T> | undefined = node.static?.get(segment);
56
+
57
+ if (!next) {
58
+ next = new Node<T>(node);
59
+ (node.static ??= new Map()).set(segment, next);
60
+ }
61
+
62
+ node = next;
63
+ }
64
+ }
65
+
66
+ node.path = path;
67
+ node.route = route;
68
+ node.type = type;
69
+
70
+ return node;
71
+ }
72
+
73
+ find(path: string): {
74
+ parameters?: Readonly<Record<PropertyKey, unknown>>;
75
+ route?: Readonly<Route<T>>;
76
+ } {
77
+ let node: Node<T> | undefined = this,
78
+ parameters: Record<PropertyKey, unknown> | undefined,
79
+ segments = path.split('/'),
80
+ wildcard: { node: Node<T>, start: number } | undefined;
81
+
82
+ for (let i = 0, n = segments.length; i < n; i++) {
83
+ let segment = segments[i];
84
+
85
+ if (node.wildcard) {
86
+ wildcard = {
87
+ node: node.wildcard,
88
+ start: i
89
+ };
90
+ }
91
+
92
+ // Exact matches take precedence over parameters
93
+ let next: Node<T> | undefined = node.static?.get(segment) as Node<T> | undefined;
94
+
95
+ if (next) {
96
+ node = next;
97
+ continue;
98
+ }
99
+
100
+ if (!node.parameter) {
101
+ node = undefined;
102
+ break;
103
+ }
104
+
105
+ node = node.parameter;
106
+ (parameters ??= {})[node.name!] = segment;
107
+ }
108
+
109
+ if ((node === undefined || node.route === null) && wildcard) {
110
+ node = wildcard.node;
111
+ (parameters ??= {})[ node.name! ] = segments.slice(wildcard.start).join('/');
112
+ }
113
+
114
+ return {
115
+ parameters,
116
+ route: node?.route || undefined
117
+ };
118
+ }
119
+ }
120
+
121
+
122
+ export { Node };
@@ -0,0 +1,105 @@
1
+ import { NeverAsync } from '@esportsplus/utilities';
2
+ import { Router } from './router';
3
+ import pipeline from '@esportsplus/pipeline';
4
+
5
+
6
+ type AccumulateRoutes<T extends readonly RouteFactory<any>[]> =
7
+ T extends readonly [infer F extends RouteFactory<any>, ...infer Rest extends readonly RouteFactory<any>[]]
8
+ ? ExtractRoutes<F> & AccumulateRoutes<Rest>
9
+ : {};
10
+
11
+ type ExtractOptionalParams<Path extends string> =
12
+ Path extends `/${infer Segment}/${infer Rest}`
13
+ ? (Segment extends `?:${infer Param}` ? Param : never) | ExtractOptionalParams<`/${Rest}`>
14
+ : Path extends `/${infer Segment}`
15
+ ? (Segment extends `?:${infer Param}` ? Param : never)
16
+ : never;
17
+
18
+ type ExtractParamsTuple<Path extends string> =
19
+ Path extends `${infer _Start}:${infer Param}/${infer Rest}`
20
+ ? [Param, ...ExtractParamsTuple<`/${Rest}`>]
21
+ : Path extends `${infer _Start}:${infer Param}`
22
+ ? [Param]
23
+ : [];
24
+
25
+ type ExtractRequiredParams<Path extends string> =
26
+ Path extends `/${infer Segment}/${infer Rest}`
27
+ ? (Segment extends `:${infer Param}` ? Param : never) | ExtractRequiredParams<`/${Rest}`>
28
+ : Path extends `/${infer Segment}`
29
+ ? (Segment extends `:${infer Param}` ? Param : never)
30
+ : never;
31
+
32
+ type ExtractRoutes<F> =
33
+ F extends (r: Router<any, any>) => Router<any, infer Routes extends RouteRegistry>
34
+ ? Routes
35
+ : never;
36
+
37
+ type InferOutput<F> = F extends RouteFactory<infer T> ? T : never;
38
+
39
+ type LabeledParamsTuple<Params extends string[]> =
40
+ Params extends [infer _First extends string, ...infer Rest extends string[]]
41
+ ? [_First: string | number, ...LabeledParamsTuple<Rest>]
42
+ : [];
43
+
44
+ type Middleware<T> = NeverAsync<(input: Request<T>, next: Next<T>) => T>;
45
+
46
+ type Name = string;
47
+
48
+ type Next<T> = NeverAsync<(input: Request<T>) => T>;
49
+
50
+ type Options<T> = {
51
+ middleware?: Middleware<T>[];
52
+ name?: string;
53
+ path?: string;
54
+ subdomain?: string;
55
+ };
56
+
57
+ type PathParamsObject<Path extends string> =
58
+ { [K in ExtractRequiredParams<Path>]: string | number } &
59
+ { [K in ExtractOptionalParams<Path>]?: string | number };
60
+
61
+ type PathParamsTuple<Path extends string> =
62
+ LabeledParamsTuple<ExtractParamsTuple<Path>>;
63
+
64
+ type Request<T> = {
65
+ data: Record<PropertyKey, unknown> & ReturnType<Router<T>['match']>;
66
+ hostname: string;
67
+ href: string;
68
+ method: string;
69
+ origin: string;
70
+ path: string;
71
+ port: string;
72
+ protocol: string;
73
+ query: Record<string, unknown>;
74
+ subdomain?: string;
75
+ };
76
+
77
+ type Route<T> = {
78
+ name: Name | null;
79
+ path: string | null;
80
+ pipeline: ReturnType<typeof pipeline<Request<T>, T>>,
81
+ subdomain: string | null;
82
+ };
83
+
84
+ type RouteFactory<T> = (router: Router<T, any>) => Router<T, RouteRegistry>;
85
+
86
+ type RouteOptions<T> = Options<T> & {
87
+ responder: Next<T>;
88
+ };
89
+
90
+ type RoutePath<Routes, K extends keyof Routes> =
91
+ Routes[K] extends { path: infer P extends string } ? P : string;
92
+
93
+ type RouteRegistry = Record<string, { path: string }>;
94
+
95
+
96
+ export type {
97
+ AccumulateRoutes,
98
+ ExtractOptionalParams, ExtractRequiredParams,
99
+ InferOutput,
100
+ Middleware,
101
+ Name, Next,
102
+ Options,
103
+ PathParamsObject, PathParamsTuple,
104
+ Request, Router, Route, RouteFactory, RouteOptions, RoutePath, RouteRegistry
105
+ };
@@ -0,0 +1,4 @@
1
+ const PACKAGE_NAME = '@esportsplus/routing';
2
+
3
+
4
+ export { PACKAGE_NAME };
package/src/index.ts ADDED
File without changes