@takaro/http 0.0.0-next.0da151e

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.
Files changed (59) hide show
  1. package/README.md +7 -0
  2. package/dist/app.d.ts +21 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +86 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/controllers/meta.d.ts +18 -0
  7. package/dist/controllers/meta.d.ts.map +1 -0
  8. package/dist/controllers/meta.js +284 -0
  9. package/dist/controllers/meta.js.map +1 -0
  10. package/dist/main.d.ts +8 -0
  11. package/dist/main.d.ts.map +1 -0
  12. package/dist/main.js +8 -0
  13. package/dist/main.js.map +1 -0
  14. package/dist/middleware/basicAuth.d.ts +3 -0
  15. package/dist/middleware/basicAuth.d.ts.map +1 -0
  16. package/dist/middleware/basicAuth.js +27 -0
  17. package/dist/middleware/basicAuth.js.map +1 -0
  18. package/dist/middleware/errorHandler.d.ts +3 -0
  19. package/dist/middleware/errorHandler.d.ts.map +1 -0
  20. package/dist/middleware/errorHandler.js +73 -0
  21. package/dist/middleware/errorHandler.js.map +1 -0
  22. package/dist/middleware/logger.d.ts +9 -0
  23. package/dist/middleware/logger.d.ts.map +1 -0
  24. package/dist/middleware/logger.js +51 -0
  25. package/dist/middleware/logger.js.map +1 -0
  26. package/dist/middleware/metrics.d.ts +3 -0
  27. package/dist/middleware/metrics.d.ts.map +1 -0
  28. package/dist/middleware/metrics.js +35 -0
  29. package/dist/middleware/metrics.js.map +1 -0
  30. package/dist/middleware/paginationMiddleware.d.ts +3 -0
  31. package/dist/middleware/paginationMiddleware.d.ts.map +1 -0
  32. package/dist/middleware/paginationMiddleware.js +26 -0
  33. package/dist/middleware/paginationMiddleware.js.map +1 -0
  34. package/dist/middleware/rateLimit.d.ts +9 -0
  35. package/dist/middleware/rateLimit.d.ts.map +1 -0
  36. package/dist/middleware/rateLimit.js +60 -0
  37. package/dist/middleware/rateLimit.js.map +1 -0
  38. package/dist/util/apiResponse.d.ts +37 -0
  39. package/dist/util/apiResponse.d.ts.map +1 -0
  40. package/dist/util/apiResponse.js +99 -0
  41. package/dist/util/apiResponse.js.map +1 -0
  42. package/package.json +15 -0
  43. package/src/app.ts +105 -0
  44. package/src/controllers/__tests__/meta.integration.test.ts +23 -0
  45. package/src/controllers/defaultMetadatastorage.d.ts +5 -0
  46. package/src/controllers/meta.ts +268 -0
  47. package/src/main.ts +8 -0
  48. package/src/middleware/__tests__/paginationMiddleware.unit.test.ts +49 -0
  49. package/src/middleware/__tests__/rateLimit.integration.test.ts +130 -0
  50. package/src/middleware/basicAuth.ts +34 -0
  51. package/src/middleware/errorHandler.ts +95 -0
  52. package/src/middleware/logger.ts +62 -0
  53. package/src/middleware/metrics.ts +42 -0
  54. package/src/middleware/paginationMiddleware.ts +29 -0
  55. package/src/middleware/rateLimit.ts +73 -0
  56. package/src/util/apiResponse.ts +112 -0
  57. package/tsconfig.build.json +9 -0
  58. package/tsconfig.json +8 -0
  59. package/typedoc.json +3 -0
@@ -0,0 +1,29 @@
1
+ import { NextFunction, Request, Response } from 'express';
2
+ import * as yup from 'yup';
3
+ import { errors, logger } from '@takaro/util';
4
+
5
+ const log = logger('http:pagination');
6
+
7
+ const paginationSchema = yup.object({
8
+ page: yup.number().default(0).min(0),
9
+ limit: yup.number().default(100).min(1).max(1000),
10
+ });
11
+
12
+ export async function paginationMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
13
+ const merged = { ...req.query, ...req.body };
14
+ try {
15
+ const result = await paginationSchema.validate(merged);
16
+
17
+ res.locals.page = result.page;
18
+ res.locals.limit = result.limit;
19
+
20
+ next();
21
+ } catch (error) {
22
+ if (error instanceof yup.ValidationError) {
23
+ next(new errors.ValidationError('Invalid pagination', [error]));
24
+ } else {
25
+ log.error('Unexpected error', error);
26
+ next(new errors.InternalServerError());
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,73 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { Redis } from '@takaro/db';
3
+ import { logger, errors, ctx } from '@takaro/util';
4
+ import { RateLimiterRes, RateLimiterRedis, RateLimiterMemory } from 'rate-limiter-flexible';
5
+
6
+ export interface IRateLimitMiddlewareOptions {
7
+ max: number;
8
+ windowSeconds: number;
9
+ keyPrefix?: string;
10
+ useInMemory?: boolean;
11
+ }
12
+
13
+ export async function createRateLimitMiddleware(opts: IRateLimitMiddlewareOptions) {
14
+ const log = logger('http:rateLimit');
15
+ const redis = await Redis.getClient('http:rateLimit');
16
+
17
+ // We create a randomHash to use in Redis keys
18
+ // This makes sure that each endpoint can get different rate limits without too much hassle
19
+ const randomHash = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
20
+
21
+ let rateLimiter: RateLimiterMemory | RateLimiterRedis;
22
+
23
+ if (opts.useInMemory) {
24
+ rateLimiter = new RateLimiterMemory({
25
+ points: opts.max,
26
+ duration: opts.windowSeconds,
27
+ keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
28
+ });
29
+ } else {
30
+ rateLimiter = new RateLimiterRedis({
31
+ points: opts.max,
32
+ duration: opts.windowSeconds,
33
+ storeClient: redis,
34
+ keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
35
+ });
36
+ }
37
+
38
+ return async (req: Request, res: Response, next: NextFunction) => {
39
+ const ctxData = ctx.data;
40
+ let limitedKey = null;
41
+ if (ctxData.user) {
42
+ limitedKey = ctxData.user;
43
+ } else {
44
+ // TODO: should handle case ip is undefined
45
+ limitedKey = req.ip!;
46
+ }
47
+
48
+ let rateLimiterRes: RateLimiterRes | null = null;
49
+
50
+ try {
51
+ rateLimiterRes = await rateLimiter.consume(limitedKey);
52
+ } catch (err) {
53
+ if (err instanceof RateLimiterRes) {
54
+ rateLimiterRes = err;
55
+ log.warn(`rate limited, try again in ${err.msBeforeNext}ms`);
56
+ } else {
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ if (rateLimiterRes) {
62
+ res.set('X-RateLimit-Limit', opts.max.toString());
63
+ res.set('X-RateLimit-Remaining', rateLimiterRes.remainingPoints.toString());
64
+ res.set('X-RateLimit-Reset', rateLimiterRes.msBeforeNext.toString());
65
+
66
+ if (rateLimiterRes.remainingPoints === 0) {
67
+ next(new errors.TooManyRequestsError());
68
+ }
69
+ }
70
+
71
+ return next();
72
+ };
73
+ }
@@ -0,0 +1,112 @@
1
+ import { errors, isTakaroDTO, TakaroDTO } from '@takaro/util';
2
+ import { Type } from 'class-transformer';
3
+ import {
4
+ IsISO8601,
5
+ IsNumber,
6
+ IsOptional,
7
+ ValidateNested,
8
+ IsString,
9
+ ValidationError as ClassValidatorError,
10
+ } from 'class-validator';
11
+ import { Request, Response } from 'express';
12
+
13
+ class ErrorOutput {
14
+ @IsString()
15
+ code?: string;
16
+
17
+ @IsString()
18
+ message?: string;
19
+
20
+ @IsString()
21
+ details?: string | ClassValidatorError[];
22
+ }
23
+
24
+ class MetadataOutput {
25
+ @IsString()
26
+ @IsISO8601()
27
+ serverTime!: string;
28
+
29
+ @Type(() => ErrorOutput)
30
+ @ValidateNested()
31
+ error?: ErrorOutput;
32
+
33
+ /**
34
+ * The page number of the response
35
+ */
36
+ @IsNumber()
37
+ @IsOptional()
38
+ page?: number;
39
+
40
+ /**
41
+ * The number of items returned in the response (aka page size)
42
+ */
43
+ @IsNumber()
44
+ @IsOptional()
45
+ limit?: number;
46
+
47
+ /**
48
+ * The total number of items in the collection
49
+ */
50
+ @IsNumber()
51
+ @IsOptional()
52
+ total?: number;
53
+ }
54
+ export class APIOutput<T> extends TakaroDTO<APIOutput<T>> {
55
+ @Type(() => MetadataOutput)
56
+ @ValidateNested()
57
+ meta!: MetadataOutput;
58
+
59
+ data!: T;
60
+ }
61
+
62
+ interface IApiResponseOptions {
63
+ error?: Error;
64
+ meta?: Record<string, string | number>;
65
+ req: Request;
66
+ res: Response;
67
+ }
68
+
69
+ export function apiResponse(data: unknown = {}, opts?: IApiResponseOptions): APIOutput<unknown> {
70
+ const returnVal = new APIOutput<unknown>();
71
+
72
+ returnVal.meta = new MetadataOutput();
73
+ returnVal.data = {};
74
+
75
+ if (opts?.error) {
76
+ returnVal.meta.error = new ErrorOutput();
77
+
78
+ returnVal.meta.error.code = String(opts.error.name);
79
+
80
+ if ('details' in opts.error) {
81
+ if (opts.error instanceof errors.ValidationError) {
82
+ returnVal.meta.error.details = opts.error.details as ClassValidatorError[];
83
+ } else {
84
+ returnVal.meta.error.details = opts.error.details as string;
85
+ }
86
+ }
87
+
88
+ returnVal.meta.error.message = String(opts.error.message);
89
+ }
90
+
91
+ if (opts?.meta) {
92
+ returnVal.meta.page = opts?.res.locals.page;
93
+ returnVal.meta.limit = opts?.res.locals.limit;
94
+ returnVal.meta.total = opts?.meta.total as number;
95
+ }
96
+
97
+ if (isTakaroDTO(data)) {
98
+ returnVal.data = data.toJSON();
99
+ } else if (Array.isArray(data)) {
100
+ returnVal.data = data.map((item) => {
101
+ if (isTakaroDTO(item)) {
102
+ return item.toJSON();
103
+ }
104
+
105
+ return item;
106
+ });
107
+ } else {
108
+ returnVal.data = data;
109
+ }
110
+
111
+ return returnVal;
112
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["src/**/*.test.ts"]
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
package/typedoc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "entryPoints": ["src/main.ts"]
3
+ }