@takaro/http 0.0.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.
Files changed (50) hide show
  1. package/README.md +7 -0
  2. package/dist/app.d.ts +21 -0
  3. package/dist/app.js +87 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/controllers/meta.d.ts +15 -0
  6. package/dist/controllers/meta.js +186 -0
  7. package/dist/controllers/meta.js.map +1 -0
  8. package/dist/main.d.ts +6 -0
  9. package/dist/main.js +7 -0
  10. package/dist/main.js.map +1 -0
  11. package/dist/middleware/adminAuth.d.ts +2 -0
  12. package/dist/middleware/adminAuth.js +27 -0
  13. package/dist/middleware/adminAuth.js.map +1 -0
  14. package/dist/middleware/errorHandler.d.ts +2 -0
  15. package/dist/middleware/errorHandler.js +51 -0
  16. package/dist/middleware/errorHandler.js.map +1 -0
  17. package/dist/middleware/logger.d.ts +5 -0
  18. package/dist/middleware/logger.js +51 -0
  19. package/dist/middleware/logger.js.map +1 -0
  20. package/dist/middleware/metrics.d.ts +2 -0
  21. package/dist/middleware/metrics.js +38 -0
  22. package/dist/middleware/metrics.js.map +1 -0
  23. package/dist/middleware/paginationMiddleware.d.ts +2 -0
  24. package/dist/middleware/paginationMiddleware.js +26 -0
  25. package/dist/middleware/paginationMiddleware.js.map +1 -0
  26. package/dist/middleware/rateLimit.d.ts +8 -0
  27. package/dist/middleware/rateLimit.js +65 -0
  28. package/dist/middleware/rateLimit.js.map +1 -0
  29. package/dist/util/apiResponse.d.ts +36 -0
  30. package/dist/util/apiResponse.js +100 -0
  31. package/dist/util/apiResponse.js.map +1 -0
  32. package/package.json +37 -0
  33. package/src/app.ts +104 -0
  34. package/src/controllers/__tests__/meta.integration.test.ts +29 -0
  35. package/src/controllers/defaultMetadatastorage.d.ts +5 -0
  36. package/src/controllers/meta.ts +170 -0
  37. package/src/main.ts +7 -0
  38. package/src/middleware/__tests__/adminAuth.unit.test.ts +65 -0
  39. package/src/middleware/__tests__/paginationMiddleware.unit.test.ts +48 -0
  40. package/src/middleware/__tests__/rateLimit.integration.test.ts +125 -0
  41. package/src/middleware/adminAuth.ts +33 -0
  42. package/src/middleware/errorHandler.ts +67 -0
  43. package/src/middleware/logger.ts +62 -0
  44. package/src/middleware/metrics.ts +44 -0
  45. package/src/middleware/paginationMiddleware.ts +29 -0
  46. package/src/middleware/rateLimit.ts +78 -0
  47. package/src/util/apiResponse.ts +106 -0
  48. package/tsconfig.build.json +9 -0
  49. package/tsconfig.json +8 -0
  50. package/typedoc.json +3 -0
@@ -0,0 +1,48 @@
1
+ import { sandbox, expect } from '@takaro/test';
2
+ import { NextFunction, Request, Response } from 'express';
3
+ import { errors } from '@takaro/util';
4
+ import { paginationMiddleware } from '../paginationMiddleware.js';
5
+
6
+ async function runPagination(page?: number, limit?: number) {
7
+ const req = { query: { page, limit } } as unknown as Request;
8
+ const res = { locals: {} } as Response;
9
+ const next = sandbox.stub<errors.ValidationError[]>();
10
+ await paginationMiddleware(req, res, next as unknown as NextFunction);
11
+ return { req, res, next };
12
+ }
13
+
14
+ describe('pagination middleware', () => {
15
+ it('Handles setting defaults', async () => {
16
+ const { res, next } = await runPagination();
17
+ expect(res.locals.page).to.equal(0);
18
+ expect(res.locals.limit).to.equal(100);
19
+ expect(next).to.have.been.calledOnce;
20
+ });
21
+ it('Works when pagination is passed', async () => {
22
+ const { res, next } = await runPagination(2, 20);
23
+ expect(res.locals.page).to.equal(2);
24
+ expect(res.locals.limit).to.equal(20);
25
+ expect(next).to.have.been.calledOnce;
26
+ });
27
+ it('Handles negative limit', async () => {
28
+ const { next } = await runPagination(1, -1);
29
+ expect(next).to.have.been.calledOnce;
30
+ const callArg = next.getCalls()[0].args[0];
31
+ expect(callArg).to.be.an.instanceOf(Error);
32
+ expect(callArg.message).to.equal('Invalid pagination: limit must be greater than or equal to 1');
33
+ });
34
+ it('Handles negative page', async () => {
35
+ const { next } = await runPagination(-1, 5);
36
+ expect(next).to.have.been.calledOnce;
37
+ const callArg = next.getCalls()[0].args[0];
38
+ expect(callArg).to.be.an.instanceOf(Error);
39
+ expect(callArg.message).to.equal('Invalid pagination: page must be greater than or equal to 0');
40
+ });
41
+ it('Handles limit too high', async () => {
42
+ const { next } = await runPagination(1, 1001);
43
+ expect(next).to.have.been.calledOnce;
44
+ const callArg = next.getCalls()[0].args[0];
45
+ expect(callArg).to.be.an.instanceOf(Error);
46
+ expect(callArg.message).to.equal('Invalid pagination: limit must be less than or equal to 1000');
47
+ });
48
+ });
@@ -0,0 +1,125 @@
1
+ import { Response, NextFunction, Request } from 'express';
2
+ import { Redis } from '@takaro/db';
3
+ import { expect } from '@takaro/test';
4
+ import { Controller, UseBefore, Get } from 'routing-controllers';
5
+ import { HTTP } from '../../main.js';
6
+ import { createRateLimitMiddleware } from '../rateLimit.js';
7
+ import { ctx } from '@takaro/util';
8
+ import supertest from 'supertest';
9
+
10
+ describe('rateLimit middleware', () => {
11
+ let http: HTTP;
12
+ beforeEach(async () => {
13
+ @Controller()
14
+ class TestController {
15
+ @Get('/low-limit')
16
+ @UseBefore(
17
+ await createRateLimitMiddleware({
18
+ max: 5,
19
+ windowSeconds: 5,
20
+ useInMemory: false,
21
+ })
22
+ )
23
+ getLow() {
24
+ return 'Hello World';
25
+ }
26
+
27
+ @Get('/high-limit')
28
+ @UseBefore(
29
+ await createRateLimitMiddleware({
30
+ max: 15,
31
+ windowSeconds: 5,
32
+ useInMemory: false,
33
+ })
34
+ )
35
+ getHigh() {
36
+ return 'Hello World';
37
+ }
38
+
39
+ @Get('/authenticated')
40
+ @UseBefore((req: Request, _res: Response, next: NextFunction) => {
41
+ ctx.addData({ user: req.query.user as string });
42
+ next();
43
+ }, await createRateLimitMiddleware({ max: 5, windowSeconds: 5, useInMemory: false }))
44
+ getAuthenticated() {
45
+ return 'Hello World';
46
+ }
47
+ }
48
+
49
+ http = new HTTP(
50
+ {
51
+ controllers: [TestController],
52
+ },
53
+ { port: undefined }
54
+ );
55
+
56
+ await http.start();
57
+ });
58
+
59
+ afterEach(async () => {
60
+ await http.stop();
61
+
62
+ const redis = await Redis.getClient('http:rateLimit');
63
+ await redis.flushAll();
64
+ });
65
+
66
+ after(async () => {
67
+ await Redis.destroy();
68
+ });
69
+
70
+ it('should limit requests', async () => {
71
+ const agent = supertest(http.expressInstance);
72
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
73
+ // @ts-ignore
74
+ agent.get('/low-limit').set('X-Forwarded-For', '127.0.0.2');
75
+
76
+ for (let i = 0; i < 4; i++) {
77
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
78
+ // @ts-ignore
79
+ await agent.get('/low-limit').expect(200);
80
+ }
81
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
82
+ // @ts-ignore
83
+ await agent.get('/low-limit').expect(429);
84
+ });
85
+
86
+ it('should apply distinct limits per user', async () => {
87
+ const agent = supertest(http.expressInstance);
88
+
89
+ for (let i = 0; i < 4; i++) {
90
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
91
+ // @ts-ignore
92
+ await agent.get('/authenticated?user=1').expect(200);
93
+ }
94
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
95
+ // @ts-ignore
96
+ await agent.get('/authenticated?user=1').expect(429);
97
+
98
+ for (let i = 0; i < 4; i++) {
99
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
100
+ // @ts-ignore
101
+ await agent.get('/authenticated?user=2').expect(200);
102
+ }
103
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
104
+ // @ts-ignore
105
+ await agent.get('/authenticated?user=2').expect(429);
106
+ });
107
+
108
+ it('Should accurately report metadata info via HTTP headers', async () => {
109
+ const agent = supertest(http.expressInstance);
110
+
111
+ for (let i = 1; i < 5; i++) {
112
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
113
+ // @ts-ignore
114
+ const res = await agent.get('/low-limit').expect(200);
115
+ expect(res.header['x-ratelimit-remaining']).to.equal((5 - i).toString());
116
+ expect(res.header['x-ratelimit-limit']).to.equal('5');
117
+ expect(res.header['x-ratelimit-reset']).to.be.a('string');
118
+ }
119
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
+ // @ts-ignore
121
+ const res = await agent.get('/low-limit').expect(429);
122
+ expect(res.header['x-ratelimit-remaining']).to.equal('0');
123
+ expect(res.header['x-ratelimit-limit']).to.equal('5');
124
+ });
125
+ });
@@ -0,0 +1,33 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { errors, logger } from '@takaro/util';
3
+ import { ory, AUDIENCES } from '@takaro/auth';
4
+
5
+ const log = logger('http:middleware:adminAuth');
6
+
7
+ export async function adminAuthMiddleware(request: Request, response: Response, next: NextFunction) {
8
+ try {
9
+ const rawToken = request.headers['authorization']?.replace('Bearer ', '');
10
+
11
+ if (!rawToken) {
12
+ log.warn('No token provided');
13
+ return next(new errors.UnauthorizedError());
14
+ }
15
+
16
+ const token = await ory.introspectToken(rawToken);
17
+
18
+ if (!token.active) {
19
+ log.warn('Token is not active');
20
+ return next(new errors.ForbiddenError());
21
+ }
22
+
23
+ if (!token.aud.includes(AUDIENCES.TAKARO_API_ADMIN)) {
24
+ log.warn('Token is not for admin API', { token });
25
+ return next(new errors.ForbiddenError());
26
+ }
27
+
28
+ return next();
29
+ } catch (error) {
30
+ log.error('Unexpected error', { error });
31
+ next(new errors.ForbiddenError());
32
+ }
33
+ }
@@ -0,0 +1,67 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { HttpError } from 'routing-controllers';
3
+ import { logger, errors } from '@takaro/util';
4
+ import { apiResponse } from '../util/apiResponse.js';
5
+ import { ValidationError } from 'class-validator';
6
+
7
+ const log = logger('errorHandler');
8
+
9
+ export async function ErrorHandler(
10
+ originalError: Error,
11
+ req: Request,
12
+ res: Response,
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ _next: NextFunction
15
+ ) {
16
+ let status = 500;
17
+ let parsedError = new errors.InternalServerError();
18
+
19
+ if (originalError.name === 'BadRequestError') {
20
+ if (originalError.hasOwnProperty('errors')) {
21
+ // @ts-expect-error Error typing is weird in ts... but we validate during runtime so should be OK
22
+ const validationErrors = originalError['errors'] as ValidationError[];
23
+ parsedError = new errors.ValidationError('Validation error', validationErrors);
24
+ log.warn('⚠️ Validation errror', { details: validationErrors.map((e) => JSON.stringify(e.target, null, 2)) });
25
+ }
26
+ }
27
+
28
+ if (originalError instanceof HttpError) {
29
+ status = originalError.httpCode;
30
+ }
31
+
32
+ if (originalError.name === 'UniqueViolationError') {
33
+ status = 409;
34
+ parsedError = new errors.ConflictError('Unique constraint violation');
35
+ }
36
+
37
+ if (originalError.name === 'NotNullViolationError') {
38
+ status = 400;
39
+ parsedError = new errors.BadRequestError('Missing required field');
40
+ }
41
+
42
+ if (originalError instanceof errors.TakaroError) {
43
+ status = originalError.http;
44
+ parsedError = originalError;
45
+ }
46
+
47
+ // If error is a JSON.parse error
48
+ if (originalError instanceof SyntaxError) {
49
+ if (
50
+ originalError.message.includes('Unexpected token') ||
51
+ originalError.message.includes('Unexpected end of JSON input')
52
+ ) {
53
+ status = 400;
54
+ parsedError = new errors.BadRequestError('Invalid JSON');
55
+ }
56
+ }
57
+
58
+ log.error(originalError);
59
+ if (status >= 500) {
60
+ log.error(`🔴 FAIL ${req.method} ${req.originalUrl}`, parsedError);
61
+ } else {
62
+ log.warn(`⚠️ FAIL ${req.method} ${req.originalUrl}`, parsedError);
63
+ }
64
+
65
+ res.status(status).json(apiResponse({}, { error: parsedError, req, res }));
66
+ return res.end();
67
+ }
@@ -0,0 +1,62 @@
1
+ import { NextFunction, Request, Response } from 'express';
2
+ import { logger, ctx } from '@takaro/util';
3
+
4
+ const SUPPRESS_BODY_KEYWORDS = ['password', 'newPassword'];
5
+ const HIDDEN_ROUTES = ['/metrics', '/health', '/healthz', '/ready', '/readyz', '/queues/api/queues'];
6
+ import { context, trace } from '@opentelemetry/api';
7
+ const log = logger('http');
8
+
9
+ /**
10
+ * This middleware is called very early in the request lifecycle, so it's
11
+ * we leverage this fact to inject the context tracking at this stage
12
+ */
13
+ export const LoggingMiddleware = ctx.wrap('HTTP', loggingMiddleware);
14
+
15
+ async function loggingMiddleware(req: Request, res: Response, next: NextFunction) {
16
+ if (HIDDEN_ROUTES.some((route) => req.originalUrl.startsWith(route))) {
17
+ return next();
18
+ }
19
+
20
+ const requestStartMs = Date.now();
21
+
22
+ const hideData = SUPPRESS_BODY_KEYWORDS.some((keyword) => (JSON.stringify(req.body) || '').includes(keyword));
23
+
24
+ log.debug(`⬇️ ${req.method} ${req.originalUrl}`, {
25
+ ip: req.ip,
26
+ method: req.method,
27
+ path: req.originalUrl,
28
+ body: hideData ? { suppressed_output: true } : req.body,
29
+ });
30
+
31
+ const span = trace.getSpan(context.active());
32
+
33
+ if (span) {
34
+ // get the trace ID from the span and set it in the headers
35
+ const traceId = span.spanContext().traceId;
36
+ res.header('X-Trace-Id', traceId);
37
+ }
38
+
39
+ // Log on API Call Finish to add responseTime
40
+ res.once('finish', () => {
41
+ const responseTime = Date.now() - requestStartMs;
42
+
43
+ log.info(`⬆️ ${req.method} ${req.originalUrl}`, {
44
+ responseTime,
45
+ requestMethod: req.method,
46
+ requestUrl: req.originalUrl,
47
+ requestSize: req.headers['content-length'],
48
+ status: res.statusCode,
49
+ responseSize: res.getHeader('Content-Length'),
50
+ userAgent: req.get('User-Agent'),
51
+ remoteIp: req.ip,
52
+ serverIp: '127.0.0.1',
53
+ referer: req.get('Referer'),
54
+ cacheLookup: false,
55
+ cacheHit: false,
56
+ cacheValidatedWithOriginServer: false,
57
+ protocol: req.protocol,
58
+ });
59
+ });
60
+
61
+ next();
62
+ }
@@ -0,0 +1,44 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { Counter, Histogram } from 'prom-client';
3
+
4
+ const counter = new Counter({
5
+ name: 'http_requests_total',
6
+ help: 'Total number of HTTP requests made',
7
+ labelNames: ['path', 'method', 'status'],
8
+ });
9
+
10
+ const histogram = new Histogram({
11
+ name: 'http_request_duration_seconds',
12
+ help: 'Duration of HTTP requests in seconds',
13
+ labelNames: ['path', 'method', 'status'],
14
+ buckets: [0.1, 0.5, 1, 2, 5, 10],
15
+ });
16
+
17
+ export async function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
18
+ const rawPath = req.path;
19
+ const method = req.method;
20
+ // Filter out anything that looks like a UUID from path
21
+ const path = rawPath.replace(/\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, '/:id');
22
+
23
+ const start = Date.now();
24
+
25
+ try {
26
+ await next();
27
+ } catch (error) {
28
+ throw error;
29
+ } finally {
30
+ counter.inc({
31
+ path,
32
+ method,
33
+ status: res.statusCode.toString(),
34
+ });
35
+ histogram.observe(
36
+ {
37
+ path,
38
+ method,
39
+ status: res.statusCode.toString(),
40
+ },
41
+ (Date.now() - start) / 1000
42
+ );
43
+ }
44
+ }
@@ -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,78 @@
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
+ // Legacy mode is required for rate-limiter-flexible which isn't updated
17
+ // to use the new v4 redis API.
18
+ // See: https://github.com/animir/node-rate-limiter-flexible/wiki/Redis#usage
19
+ legacyMode: true,
20
+ });
21
+
22
+ // We create a randomHash to use in Redis keys
23
+ // This makes sure that each endpoint can get different rate limits without too much hassle
24
+ const randomHash = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
25
+
26
+ let rateLimiter: RateLimiterMemory | RateLimiterRedis;
27
+
28
+ if (opts.useInMemory) {
29
+ rateLimiter = new RateLimiterMemory({
30
+ points: opts.max,
31
+ duration: opts.windowSeconds,
32
+ keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
33
+ });
34
+ } else {
35
+ rateLimiter = new RateLimiterRedis({
36
+ points: opts.max,
37
+ duration: opts.windowSeconds,
38
+ storeClient: redis,
39
+ keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
40
+ });
41
+ }
42
+
43
+ return async (req: Request, res: Response, next: NextFunction) => {
44
+ const ctxData = ctx.data;
45
+ let limitedKey = null;
46
+ if (ctxData.user) {
47
+ limitedKey = ctxData.user;
48
+ } else {
49
+ // TODO: should handle case ip is undefined
50
+ limitedKey = req.ip!;
51
+ }
52
+
53
+ let rateLimiterRes: RateLimiterRes | null = null;
54
+
55
+ try {
56
+ rateLimiterRes = await rateLimiter.consume(limitedKey);
57
+ } catch (err) {
58
+ if (err instanceof RateLimiterRes) {
59
+ rateLimiterRes = err;
60
+ log.warn(`rate limited, try again in ${err.msBeforeNext}ms`);
61
+ } else {
62
+ throw err;
63
+ }
64
+ }
65
+
66
+ if (rateLimiterRes) {
67
+ res.set('X-RateLimit-Limit', opts.max.toString());
68
+ res.set('X-RateLimit-Remaining', rateLimiterRes.remainingPoints.toString());
69
+ res.set('X-RateLimit-Reset', rateLimiterRes.msBeforeNext.toString());
70
+
71
+ if (rateLimiterRes.remainingPoints === 0) {
72
+ next(new errors.TooManyRequestsError());
73
+ }
74
+ }
75
+
76
+ return next();
77
+ };
78
+ }
@@ -0,0 +1,106 @@
1
+ import { errors, isTakaroDTO, TakaroDTO } from '@takaro/util';
2
+ import { Type } from 'class-transformer';
3
+ import { IsISO8601, IsNumber, IsOptional, ValidateNested } from 'class-validator';
4
+ import { IsString, ValidationError as ClassValidatorError } from 'class-validator';
5
+ import { Request, Response } from 'express';
6
+
7
+ class ErrorOutput {
8
+ @IsString()
9
+ code?: string;
10
+
11
+ @IsString()
12
+ message?: string;
13
+
14
+ @IsString()
15
+ details?: string | ClassValidatorError[];
16
+ }
17
+
18
+ class MetadataOutput {
19
+ @IsString()
20
+ @IsISO8601()
21
+ serverTime!: string;
22
+
23
+ @Type(() => ErrorOutput)
24
+ @ValidateNested()
25
+ error?: ErrorOutput;
26
+
27
+ /**
28
+ * The page number of the response
29
+ */
30
+ @IsNumber()
31
+ @IsOptional()
32
+ page?: number;
33
+
34
+ /**
35
+ * The number of items returned in the response (aka page size)
36
+ */
37
+ @IsNumber()
38
+ @IsOptional()
39
+ limit?: number;
40
+
41
+ /**
42
+ * The total number of items in the collection
43
+ */
44
+ @IsNumber()
45
+ @IsOptional()
46
+ total?: number;
47
+ }
48
+ export class APIOutput<T> extends TakaroDTO<APIOutput<T>> {
49
+ @Type(() => MetadataOutput)
50
+ @ValidateNested()
51
+ meta!: MetadataOutput;
52
+
53
+ data!: T;
54
+ }
55
+
56
+ interface IApiResponseOptions {
57
+ error?: Error;
58
+ meta?: Record<string, string | number>;
59
+ req: Request;
60
+ res: Response;
61
+ }
62
+
63
+ export function apiResponse(data: unknown = {}, opts?: IApiResponseOptions): APIOutput<unknown> {
64
+ const returnVal = new APIOutput<unknown>();
65
+
66
+ returnVal.meta = new MetadataOutput();
67
+ returnVal.data = {};
68
+
69
+ if (opts?.error) {
70
+ returnVal.meta.error = new ErrorOutput();
71
+
72
+ returnVal.meta.error.code = String(opts.error.name);
73
+
74
+ if ('details' in opts.error) {
75
+ if (opts.error instanceof errors.ValidationError) {
76
+ returnVal.meta.error.details = opts.error.details as ClassValidatorError[];
77
+ } else {
78
+ returnVal.meta.error.details = opts.error.details as string;
79
+ }
80
+ }
81
+
82
+ returnVal.meta.error.message = String(opts.error.message);
83
+ }
84
+
85
+ if (opts?.meta) {
86
+ returnVal.meta.page = opts?.res.locals.page;
87
+ returnVal.meta.limit = opts?.res.locals.limit;
88
+ returnVal.meta.total = opts?.meta.total as number;
89
+ }
90
+
91
+ if (isTakaroDTO(data)) {
92
+ returnVal.data = data.toJSON();
93
+ } else if (Array.isArray(data)) {
94
+ returnVal.data = data.map((item) => {
95
+ if (isTakaroDTO(item)) {
96
+ return item.toJSON();
97
+ }
98
+
99
+ return item;
100
+ });
101
+ } else {
102
+ returnVal.data = data;
103
+ }
104
+
105
+ return returnVal;
106
+ }
@@ -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.esm.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
+ }