@forklaunch/core 0.1.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.
- package/dist/cache/interfaces/ttlCache.interface.d.ts +47 -0
- package/dist/cache/interfaces/ttlCache.interface.js +3 -0
- package/dist/cache/interfaces/ttlCache.interface.js.map +1 -0
- package/dist/cache/redisTtlCache.d.ts +64 -0
- package/dist/cache/redisTtlCache.js +105 -0
- package/dist/cache/redisTtlCache.js.map +1 -0
- package/dist/cache/types/ttlCacheRecord.d.ts +13 -0
- package/dist/cache/types/ttlCacheRecord.js +3 -0
- package/dist/cache/types/ttlCacheRecord.js.map +1 -0
- package/dist/controllers/interfaces/controller.interface.d.ts +15 -0
- package/dist/controllers/interfaces/controller.interface.js +3 -0
- package/dist/controllers/interfaces/controller.interface.js.map +1 -0
- package/dist/database/mikro/models/entities/base.entity.d.ts +25 -0
- package/dist/database/mikro/models/entities/base.entity.js +53 -0
- package/dist/database/mikro/models/entities/base.entity.js.map +1 -0
- package/dist/entityMapper/interfaces/entityMapper.interface.d.ts +16 -0
- package/dist/entityMapper/interfaces/entityMapper.interface.js +3 -0
- package/dist/entityMapper/interfaces/entityMapper.interface.js.map +1 -0
- package/dist/entityMapper/models/baseEntityMapper.model.d.ts +71 -0
- package/dist/entityMapper/models/baseEntityMapper.model.js +81 -0
- package/dist/entityMapper/models/baseEntityMapper.model.js.map +1 -0
- package/dist/entityMapper/models/requestEntityMapper.model.d.ts +69 -0
- package/dist/entityMapper/models/requestEntityMapper.model.js +77 -0
- package/dist/entityMapper/models/requestEntityMapper.model.js.map +1 -0
- package/dist/entityMapper/models/responseEntityMapper.model.d.ts +60 -0
- package/dist/entityMapper/models/responseEntityMapper.model.js +64 -0
- package/dist/entityMapper/models/responseEntityMapper.model.js.map +1 -0
- package/dist/entityMapper/types/entityMapper.types.d.ts +28 -0
- package/dist/entityMapper/types/entityMapper.types.js +3 -0
- package/dist/entityMapper/types/entityMapper.types.js.map +1 -0
- package/dist/http/middlewares/request.middleware.d.ts +11 -0
- package/dist/http/middlewares/request.middleware.js +187 -0
- package/dist/http/middlewares/request.middleware.js.map +1 -0
- package/dist/http/middlewares/response.middleware.d.ts +2 -0
- package/dist/http/middlewares/response.middleware.js +30 -0
- package/dist/http/middlewares/response.middleware.js.map +1 -0
- package/dist/http/types/api.types.d.ts +50 -0
- package/dist/http/types/api.types.js +3 -0
- package/dist/http/types/api.types.js.map +1 -0
- package/dist/http/types/primitive.types.d.ts +35 -0
- package/dist/http/types/primitive.types.js +3 -0
- package/dist/http/types/primitive.types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/jest.config.d.ts +3 -0
- package/dist/jest.config.js.map +1 -0
- package/dist/services/interfaces/baseService.d.ts +14 -0
- package/dist/services/interfaces/baseService.js +3 -0
- package/dist/services/interfaces/baseService.js.map +1 -0
- package/dist/tests/dto.test.d.ts +1 -0
- package/dist/tests/dto.test.js +179 -0
- package/dist/tests/dto.test.js.map +1 -0
- package/dist/tests/redisTtlCache.test.d.ts +1 -0
- package/dist/tests/redisTtlCache.test.js +35 -0
- package/dist/tests/redisTtlCache.test.js.map +1 -0
- package/entityMapper/interfaces/entityMapper.interface.ts +17 -0
- package/entityMapper/models/baseEntityMapper.model.ts +93 -0
- package/entityMapper/models/requestEntityMapper.model.ts +91 -0
- package/entityMapper/models/responseEntityMapper.model.ts +77 -0
- package/entityMapper/types/entityMapper.types.ts +29 -0
- package/eslint.config.mjs +12 -0
- package/http/middlewares/request.middleware.ts +201 -0
- package/http/middlewares/response.middleware.ts +37 -0
- package/http/types/api.types.ts +65 -0
- package/http/types/primitive.types.ts +55 -0
- package/index.ts +5 -0
- package/jest.config.ts +10 -0
- package/package.json +47 -0
- package/tests/dto.test.ts +235 -0
- package/tests/redisTtlCache.test.ts +42 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
import { SchemaValidator } from "@forklaunch/validator/interfaces";
|
2
|
+
import * as jose from "jose";
|
3
|
+
import { v4 } from "uuid";
|
4
|
+
import { ForklaunchNextFunction, ForklaunchRequest, ForklaunchResponse } from "../types/api.types";
|
5
|
+
import { AuthMethod, HttpContractDetails, PathParamHttpContractDetails, StringOnlyObject } from "../types/primitive.types";
|
6
|
+
|
7
|
+
export function createRequestContext<
|
8
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
9
|
+
Response extends ForklaunchResponse<any, any>
|
10
|
+
>(schemaValidator: SchemaValidator) {
|
11
|
+
return (req: Request, res: Response, next?: Function) => {
|
12
|
+
req.schemaValidator = schemaValidator;
|
13
|
+
|
14
|
+
let correlationId = v4();
|
15
|
+
|
16
|
+
if (req.headers['x-correlation-id']) {
|
17
|
+
correlationId = req.headers['x-correlation-id'] as string;
|
18
|
+
}
|
19
|
+
|
20
|
+
res.setHeader('x-correlation-id', correlationId);
|
21
|
+
|
22
|
+
req.context = {
|
23
|
+
correlationId: correlationId
|
24
|
+
}
|
25
|
+
|
26
|
+
if (next) {
|
27
|
+
next();
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
export function enrichRequestDetails<
|
33
|
+
SV extends SchemaValidator,
|
34
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
35
|
+
Response extends ForklaunchResponse<any, any>
|
36
|
+
>(contractDetails: PathParamHttpContractDetails<SV> | HttpContractDetails<SV>) {
|
37
|
+
return (req: Request, _res: Response, next?: Function) => {
|
38
|
+
req.contractDetails = contractDetails;
|
39
|
+
|
40
|
+
if (next) {
|
41
|
+
next();
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
export function preHandlerParse<SV extends SchemaValidator>(schemaValidator: SV, object: unknown, schemaInput?: StringOnlyObject<SV>) {
|
47
|
+
if (!schemaInput) {
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
|
51
|
+
const schema = schemaValidator.schemify(schemaInput);
|
52
|
+
if (!schemaValidator.validate(schema, object)) {
|
53
|
+
return 400;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
export function parseRequestParams<
|
58
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
59
|
+
Response extends ForklaunchResponse<any, any>,
|
60
|
+
NextFunction extends ForklaunchNextFunction
|
61
|
+
>(req: Request, res: Response, next?: NextFunction) {
|
62
|
+
const params = req.contractDetails.params;
|
63
|
+
if (preHandlerParse(req.schemaValidator, req.params, params) === 400) {
|
64
|
+
res.status(400).send("Invalid request parameters.");
|
65
|
+
if (next) {
|
66
|
+
next(new Error("Invalid request parameters."));
|
67
|
+
}
|
68
|
+
};
|
69
|
+
if (next) {
|
70
|
+
next();
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
export function parseRequestBody<
|
75
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
76
|
+
Response extends ForklaunchResponse<any, any>,
|
77
|
+
NextFunction extends ForklaunchNextFunction
|
78
|
+
>(req: Request, res: Response, next?: NextFunction) {
|
79
|
+
if (req.headers['content-type'] === 'application/json') {
|
80
|
+
const body = (req.schemaValidator, req.contractDetails as HttpContractDetails<typeof req.schemaValidator>).body;
|
81
|
+
if (preHandlerParse(req.body, body as StringOnlyObject<typeof req.schemaValidator>) === 400) {
|
82
|
+
res.status(400).send("Invalid request body.");
|
83
|
+
if (next) {
|
84
|
+
next(new Error("Invalid request body."));
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
if (next) {
|
89
|
+
next();
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
export function parseRequestHeaders<
|
94
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
95
|
+
Response extends ForklaunchResponse<any, any>,
|
96
|
+
NextFunction extends ForklaunchNextFunction
|
97
|
+
> (req: Request, res: Response, next?: NextFunction) {
|
98
|
+
const headers = req.contractDetails.requestHeaders;
|
99
|
+
if (preHandlerParse(req.schemaValidator, req.headers, headers) === 400) {
|
100
|
+
res.status(400).send("Invalid request headers.");
|
101
|
+
if (next) {
|
102
|
+
next(new Error("Invalid request headers."));
|
103
|
+
}
|
104
|
+
}
|
105
|
+
if (next) {
|
106
|
+
next();
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
export function parseRequestQuery<
|
111
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
112
|
+
Response extends ForklaunchResponse<any, any>,
|
113
|
+
NextFunction extends ForklaunchNextFunction
|
114
|
+
>(req: Request, res: Response, next?: NextFunction) {
|
115
|
+
const query = req.contractDetails.query;
|
116
|
+
if (preHandlerParse(req.query, query) === 400) {
|
117
|
+
res.status(400).send("Invalid request query.");
|
118
|
+
if (next) {
|
119
|
+
next(new Error("Invalid request query."));
|
120
|
+
}
|
121
|
+
}
|
122
|
+
if (next) {
|
123
|
+
next();
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
async function checkAuthorizationToken(authorizationMethod?: AuthMethod, authorizationString?: string): Promise<[number, string] | string | undefined> {
|
128
|
+
if (!authorizationString) {
|
129
|
+
return [401, "No Authorization token provided."];
|
130
|
+
}
|
131
|
+
switch (authorizationMethod) {
|
132
|
+
case 'jwt': {
|
133
|
+
if (!authorizationString.startsWith('Bearer ')) {
|
134
|
+
return [401, "Invalid Authorization token format."];
|
135
|
+
}
|
136
|
+
try {
|
137
|
+
const decodedJwt = await jose.jwtVerify(authorizationString.split(' ')[1], new TextEncoder().encode(process.env.JWT_SECRET || 'your-256-bit-secret'));
|
138
|
+
return decodedJwt.payload.iss;
|
139
|
+
} catch(error) {
|
140
|
+
console.error(error);
|
141
|
+
return [403, "Invalid Authorization token."];
|
142
|
+
}
|
143
|
+
}
|
144
|
+
default:
|
145
|
+
return [401, "Invalid Authorization method."];
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
function mapRoles(authorizationType?: AuthMethod, authorizationToken?: string): string[] {
|
150
|
+
return [];
|
151
|
+
}
|
152
|
+
function mapPermissions(authorizationType?: AuthMethod, authorizationToken?: string): string[] {
|
153
|
+
return [];
|
154
|
+
}
|
155
|
+
|
156
|
+
export async function parseRequestAuth<
|
157
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
158
|
+
Response extends ForklaunchResponse<any, any>,
|
159
|
+
NextFunction extends ForklaunchNextFunction
|
160
|
+
>(req: Request, res: Response, next?: NextFunction) {
|
161
|
+
const auth = req.contractDetails.auth;
|
162
|
+
if (auth) {
|
163
|
+
const errorAndMessage = await checkAuthorizationToken(auth.method, req.headers.authorization);
|
164
|
+
if (Array.isArray(errorAndMessage)) {
|
165
|
+
res.status(errorAndMessage[0]).send(errorAndMessage[1]);
|
166
|
+
if (next) {
|
167
|
+
next(new Error(errorAndMessage[1]));
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
// TODO: Implement role and permission checking
|
172
|
+
const permissionSlugs = mapPermissions(auth.method, req.headers.authorization);
|
173
|
+
const roles = mapRoles(auth.method, req.headers.authorization);
|
174
|
+
|
175
|
+
const permissionErrorMessage = "User does not have sufficient permissions to perform action.";
|
176
|
+
const roleErrorMessage = "User does not have correct role to perform action.";
|
177
|
+
|
178
|
+
// this is wrong, we need to check if any of the user's permissions are in the allowed permissions, while checking that any of the permissions is not in the forbidden slugs
|
179
|
+
// currently this is checking if any of the user's permissions are NOT in the allowed permissions
|
180
|
+
permissionSlugs.forEach(permissionSlug => {
|
181
|
+
if (!req.contractDetails.auth?.allowedSlugs?.has(permissionSlug) || req.contractDetails.auth?.forbiddenSlugs?.has(permissionSlug)) {
|
182
|
+
res.status(403).send(permissionErrorMessage);
|
183
|
+
if (next) {
|
184
|
+
next(new Error(permissionErrorMessage));
|
185
|
+
}
|
186
|
+
}
|
187
|
+
});
|
188
|
+
roles.forEach(role => {
|
189
|
+
if (!req.contractDetails.auth?.allowedRoles?.has(role) || req.contractDetails.auth?.forbiddenRoles?.has(role)) {
|
190
|
+
res.status(403).send(roleErrorMessage);
|
191
|
+
if (next) {
|
192
|
+
next(new Error(roleErrorMessage));
|
193
|
+
}
|
194
|
+
}
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
198
|
+
// if (next) {
|
199
|
+
// next();
|
200
|
+
// }
|
201
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { SchemaValidator } from "@forklaunch/validator/interfaces";
|
2
|
+
import { ForklaunchNextFunction, ForklaunchRequest, ForklaunchResponse } from "../types/api.types";
|
3
|
+
import { HttpContractDetails } from "../types/primitive.types";
|
4
|
+
|
5
|
+
function checkAnyValidation<SV extends SchemaValidator>(contractDetails: HttpContractDetails<SV>) {
|
6
|
+
return contractDetails.body || contractDetails.params || contractDetails.requestHeaders || contractDetails.query;
|
7
|
+
}
|
8
|
+
|
9
|
+
export function parseResponse<
|
10
|
+
Request extends ForklaunchRequest<any, any, any, any>,
|
11
|
+
Response extends ForklaunchResponse<any, any>,
|
12
|
+
NextFunction extends ForklaunchNextFunction
|
13
|
+
> (req: Request, res: Response, next?: NextFunction) {
|
14
|
+
if (req.contractDetails.responseHeaders) {
|
15
|
+
const schema = req.schemaValidator.schemify(req.contractDetails.responseHeaders);
|
16
|
+
req.schemaValidator.validate(schema, res.getHeaders());
|
17
|
+
}
|
18
|
+
|
19
|
+
if (res.statusCode === 500 ||
|
20
|
+
(checkAnyValidation(req.contractDetails) && res.statusCode === 400) ||
|
21
|
+
(req.contractDetails.auth && (res.statusCode === 401 || res.statusCode === 403))
|
22
|
+
) {
|
23
|
+
return;
|
24
|
+
}
|
25
|
+
if (!req.contractDetails.responses.hasOwnProperty(res.statusCode)) {
|
26
|
+
if (next) {
|
27
|
+
next(new Error(`Response code ${res.statusCode} not defined in contract.`));
|
28
|
+
};
|
29
|
+
}
|
30
|
+
|
31
|
+
const schema = req.schemaValidator.schemify(req.contractDetails.responses[res.statusCode]);
|
32
|
+
req.schemaValidator.validate(schema, res.bodyData);
|
33
|
+
|
34
|
+
if (next) {
|
35
|
+
next();
|
36
|
+
}
|
37
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import { Prettify } from "@forklaunch/common";
|
2
|
+
import { Schema, SchemaCatchall, ValidSchemaObject } from "@forklaunch/validator";
|
3
|
+
import { SchemaValidator } from "@forklaunch/validator/interfaces";
|
4
|
+
import { IdiomaticSchema } from "@forklaunch/validator/types";
|
5
|
+
import { IncomingHttpHeaders, OutgoingHttpHeader } from "http";
|
6
|
+
import { ParsedQs } from "qs";
|
7
|
+
import { HttpContractDetails, ParamsDictionary, PathParamHttpContractDetails } from "./primitive.types";
|
8
|
+
|
9
|
+
export interface RequestContext {
|
10
|
+
correlationId: string;
|
11
|
+
idempotencyKey?: string;
|
12
|
+
}
|
13
|
+
|
14
|
+
export interface ForklaunchRequest<
|
15
|
+
SV extends SchemaValidator,
|
16
|
+
P = ParamsDictionary,
|
17
|
+
ReqBody = any,
|
18
|
+
ReqQuery = ParsedQs,
|
19
|
+
Headers = IncomingHttpHeaders,
|
20
|
+
> {
|
21
|
+
context: Prettify<RequestContext>;
|
22
|
+
contractDetails: HttpContractDetails<SV> | PathParamHttpContractDetails<SV>;
|
23
|
+
schemaValidator: SV;
|
24
|
+
|
25
|
+
params: P;
|
26
|
+
headers: Headers;
|
27
|
+
body: ReqBody;
|
28
|
+
query: ReqQuery;
|
29
|
+
}
|
30
|
+
|
31
|
+
export interface ForklaunchResponse<
|
32
|
+
ResBody = any,
|
33
|
+
StatusCode = number,
|
34
|
+
> {
|
35
|
+
bodyData: unknown;
|
36
|
+
statusCode: StatusCode;
|
37
|
+
corked: boolean;
|
38
|
+
|
39
|
+
getHeaders: () => OutgoingHttpHeader;
|
40
|
+
setHeader: (key: string, value: string) => void;
|
41
|
+
status: {
|
42
|
+
<U extends keyof ResBody>(code: U): ForklaunchResponse<ResBody[U], U>;
|
43
|
+
<U extends keyof ResBody>(code: U, message?: string): ForklaunchResponse<ResBody[U], U>;
|
44
|
+
<U extends 500>(code: U): ForklaunchResponse<string, U>;
|
45
|
+
<U extends 500>(code: U, message?: string): ForklaunchResponse<string, U>;
|
46
|
+
}
|
47
|
+
send: {
|
48
|
+
<T>(body?: ResBody, close_connection?: boolean): T;
|
49
|
+
<T>(body?: ResBody): T;
|
50
|
+
}
|
51
|
+
json: {
|
52
|
+
(body?: ResBody): boolean;
|
53
|
+
<T>(body?: ResBody): T;
|
54
|
+
}
|
55
|
+
jsonp: {
|
56
|
+
(body?: ResBody): boolean;
|
57
|
+
<T>(body?: ResBody): T;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
export type MapSchema<SV extends SchemaValidator, T extends IdiomaticSchema<SchemaCatchall<SV>> | ValidSchemaObject<SV>> = Schema<T, SV> extends infer U ?
|
61
|
+
{ [key: string]: unknown } extends U ?
|
62
|
+
never :
|
63
|
+
U :
|
64
|
+
never;
|
65
|
+
export type ForklaunchNextFunction = (err?: any) => void;
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import { SchemaCatchall, ValidSchemaObject } from "@forklaunch/validator";
|
2
|
+
import { SchemaValidator } from "@forklaunch/validator/interfaces";
|
3
|
+
import { UnboxedObjectSchema } from "@forklaunch/validator/types";
|
4
|
+
|
5
|
+
export type ParamsDictionary = { [key: string]: string; };
|
6
|
+
|
7
|
+
export type StringOnlyObject<SV extends SchemaValidator> = Omit<UnboxedObjectSchema<SchemaCatchall<SV>>, number | symbol>;
|
8
|
+
export type NumberOnlyObject<SV extends SchemaValidator> = Omit<UnboxedObjectSchema<SchemaCatchall<SV>>, string | symbol>;
|
9
|
+
|
10
|
+
export type BodyObject<SV extends SchemaValidator> = StringOnlyObject<SV> & unknown;
|
11
|
+
export type ParamsObject<SV extends SchemaValidator> = StringOnlyObject<SV> & unknown;
|
12
|
+
export type QueryObject<SV extends SchemaValidator> = StringOnlyObject<SV> & unknown;
|
13
|
+
export type HeadersObject<SV extends SchemaValidator> = StringOnlyObject<SV> & unknown;
|
14
|
+
export type ResponsesObject<SV extends SchemaValidator> = NumberOnlyObject<SV> & unknown;
|
15
|
+
|
16
|
+
export type Body<SV extends SchemaValidator> = BodyObject<SV>
|
17
|
+
| ValidSchemaObject<SV>
|
18
|
+
| SchemaCatchall<SV>;
|
19
|
+
|
20
|
+
export type AuthMethod = 'jwt' | 'session';
|
21
|
+
export interface PathParamHttpContractDetails<
|
22
|
+
SV extends SchemaValidator,
|
23
|
+
ParamSchemas extends ParamsObject<SV> = ParamsObject<SV>,
|
24
|
+
ResponseSchemas extends ResponsesObject<SV> = ResponsesObject<SV>,
|
25
|
+
QuerySchemas extends QueryObject<SV> = QueryObject<SV>
|
26
|
+
> {
|
27
|
+
name: string,
|
28
|
+
summary: string,
|
29
|
+
responses: ResponseSchemas,
|
30
|
+
requestHeaders?: HeadersObject<SV>,
|
31
|
+
responseHeaders?: HeadersObject<SV>,
|
32
|
+
params?: ParamSchemas,
|
33
|
+
query?: QuerySchemas,
|
34
|
+
auth?: {
|
35
|
+
method: AuthMethod,
|
36
|
+
allowedSlugs?: Set<string>,
|
37
|
+
forbiddenSlugs?: Set<string>,
|
38
|
+
allowedRoles?: Set<string>,
|
39
|
+
forbiddenRoles?: Set<string>
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
export interface HttpContractDetails<
|
44
|
+
SV extends SchemaValidator,
|
45
|
+
ParamSchemas extends ParamsObject<SV> = ParamsObject<SV>,
|
46
|
+
ResponseSchemas extends ResponsesObject<SV> = ResponsesObject<SV>,
|
47
|
+
BodySchema extends Body<SV> = Body<SV>,
|
48
|
+
QuerySchemas extends QueryObject<SV> = QueryObject<SV>
|
49
|
+
> extends PathParamHttpContractDetails<SV, ParamSchemas, ResponseSchemas, QuerySchemas> {
|
50
|
+
body?: BodySchema,
|
51
|
+
contentType?:
|
52
|
+
| 'application/json'
|
53
|
+
| 'multipart/form-data'
|
54
|
+
| 'application/x-www-form-urlencoded';
|
55
|
+
}
|
package/index.ts
ADDED
package/jest.config.ts
ADDED
package/package.json
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
{
|
2
|
+
"name": "@forklaunch/core",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"description": "forklaunch-js core package. Contains useful building blocks.",
|
5
|
+
"main": "index.js",
|
6
|
+
"scripts": {
|
7
|
+
"test": "jest",
|
8
|
+
"build": "tsc",
|
9
|
+
"docs": "typedoc --out docs *",
|
10
|
+
"lint": "eslint . -c eslint.config.mjs",
|
11
|
+
"lint:fix": "eslint . -c eslint.config.mjs --fix"
|
12
|
+
},
|
13
|
+
"author": "Rohin Bhargava",
|
14
|
+
"license": "MIT",
|
15
|
+
"repository": {
|
16
|
+
"type": "git",
|
17
|
+
"url": "git+https://github.com/forklaunch/forklaunch-js.git"
|
18
|
+
},
|
19
|
+
"bugs": {
|
20
|
+
"url": "https://github.com/forklaunch/forklaunch-js/issues"
|
21
|
+
},
|
22
|
+
"homepage": "https://github.com/forklaunch/forklaunch-js#readme",
|
23
|
+
"dependencies": {
|
24
|
+
"@forklaunch/validator": "^0.1.15",
|
25
|
+
"@mikro-orm/core": "^6.2.9",
|
26
|
+
"jose": "^5.6.2",
|
27
|
+
"redis": "^4.6.14",
|
28
|
+
"uuid": "^10.0.0"
|
29
|
+
},
|
30
|
+
"devDependencies": {
|
31
|
+
"@eslint/js": "^9.6.0",
|
32
|
+
"@forklaunch/common": "^0.1.2",
|
33
|
+
"@types/jest": "^29.5.12",
|
34
|
+
"@types/qs": "^6.9.15",
|
35
|
+
"@types/redis": "^4.0.11",
|
36
|
+
"@types/uuid": "^10.0.0",
|
37
|
+
"globals": "^15.8.0",
|
38
|
+
"ts-jest": "^29.1.5",
|
39
|
+
"ts-node": "^10.9.2",
|
40
|
+
"typedoc": "^0.26.3",
|
41
|
+
"typescript": "^5.5.3",
|
42
|
+
"typescript-eslint": "^7.15.0"
|
43
|
+
},
|
44
|
+
"directories": {
|
45
|
+
"test": "tests"
|
46
|
+
}
|
47
|
+
}
|
@@ -0,0 +1,235 @@
|
|
1
|
+
import { SchemaValidator } from "@forklaunch/validator/interfaces";
|
2
|
+
import { TypeboxSchemaValidator, number, string } from "@forklaunch/validator/typebox";
|
3
|
+
import { TCatchall, TIdiomaticSchema, TUnionContainer } from "@forklaunch/validator/typebox/types";
|
4
|
+
import { BaseEntity } from "../database/mikro/models/entities/base.entity";
|
5
|
+
import { RequestEntityMapper } from "../entityMapper/models/requestEntityMapper.model";
|
6
|
+
import { ResponseEntityMapper } from "../entityMapper/models/responseEntityMapper.model";
|
7
|
+
|
8
|
+
class TestEntity extends BaseEntity {
|
9
|
+
name: string;
|
10
|
+
age: number;
|
11
|
+
}
|
12
|
+
|
13
|
+
class T extends RequestEntityMapper<TestEntity, TypeboxSchemaValidator> {
|
14
|
+
toEntity(...additionalArgs: unknown[]): TestEntity {
|
15
|
+
const entity = new TestEntity();
|
16
|
+
entity.id = this.dto.id;
|
17
|
+
entity.name = this.dto.name;
|
18
|
+
entity.age = this.dto.age;
|
19
|
+
|
20
|
+
return entity;
|
21
|
+
}
|
22
|
+
schema = {
|
23
|
+
id: string,
|
24
|
+
name: string,
|
25
|
+
age: number
|
26
|
+
};
|
27
|
+
|
28
|
+
}
|
29
|
+
|
30
|
+
const y = new T(new TypeboxSchemaValidator());
|
31
|
+
y.fromJson({ id: '123', name: 'test', age: 1 });
|
32
|
+
|
33
|
+
var x = { id: '123', name: 'test', age: 1 };
|
34
|
+
|
35
|
+
type r = typeof x extends T['_dto'] ? true : false;
|
36
|
+
|
37
|
+
T.fromJson(new TypeboxSchemaValidator(), x);
|
38
|
+
T.fromJson(new TypeboxSchemaValidator(), {
|
39
|
+
id: '123',
|
40
|
+
name: 'test',
|
41
|
+
age: 1
|
42
|
+
});
|
43
|
+
|
44
|
+
class TestRequestEntityMapper extends RequestEntityMapper<TestEntity, TypeboxSchemaValidator> {
|
45
|
+
schema = {
|
46
|
+
id: string,
|
47
|
+
name: string,
|
48
|
+
age: number,
|
49
|
+
};
|
50
|
+
|
51
|
+
toEntity(...additionalArgs: unknown[]): TestEntity {
|
52
|
+
const entity = new TestEntity();
|
53
|
+
entity.id = this.dto.id;
|
54
|
+
entity.name = this.dto.name;
|
55
|
+
entity.age = this.dto.age;
|
56
|
+
|
57
|
+
return entity;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
type h = Expand<SchemaValidator>;
|
62
|
+
type TypeboxSV = SchemaValidator<TUnionContainer, TIdiomaticSchema, TCatchall>;
|
63
|
+
type y = TypeboxSchemaValidator extends SchemaValidator ? true : false;
|
64
|
+
type j = TypeboxSV extends SchemaValidator<unknown, unknown, unknown> ? true : false;
|
65
|
+
type m = TypeboxSV extends TypeboxSchemaValidator ? true : false;
|
66
|
+
type n = TypeboxSchemaValidator extends TypeboxSV ? true : false;
|
67
|
+
type k = string extends unknown ? true : false;
|
68
|
+
type u = Expand<TypeboxSchemaValidator>;
|
69
|
+
type ExpandRecursively<T> = T extends object
|
70
|
+
? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
|
71
|
+
: T;
|
72
|
+
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
73
|
+
|
74
|
+
|
75
|
+
class TestResponseEntityMapper extends ResponseEntityMapper<TestEntity, TypeboxSchemaValidator> {
|
76
|
+
schema = {
|
77
|
+
id: string,
|
78
|
+
name: string,
|
79
|
+
age: number
|
80
|
+
};
|
81
|
+
|
82
|
+
fromEntity(entity: TestEntity): this {
|
83
|
+
this.dto = {
|
84
|
+
id: entity.id,
|
85
|
+
name: entity.name,
|
86
|
+
age: entity.age
|
87
|
+
};
|
88
|
+
|
89
|
+
return this;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
type iii = Expand<TestResponseEntityMapper['schema']>;
|
94
|
+
|
95
|
+
function extractNonTimeBasedEntityFields<T extends BaseEntity>(entity: T): T {
|
96
|
+
entity.createdAt = new Date(0);
|
97
|
+
entity.updatedAt = new Date(0);
|
98
|
+
return entity;
|
99
|
+
}
|
100
|
+
|
101
|
+
describe('Request Entity Mapper Test', () => {
|
102
|
+
let TestRequestEM: TestRequestEntityMapper;
|
103
|
+
|
104
|
+
|
105
|
+
beforeAll(() => {
|
106
|
+
TestRequestEM = new TestRequestEntityMapper(new TypeboxSchemaValidator());
|
107
|
+
});
|
108
|
+
|
109
|
+
test('Schema Equality', async () => {
|
110
|
+
expect(TestRequestEM.schema).toEqual(TestRequestEntityMapper.schema());
|
111
|
+
});
|
112
|
+
|
113
|
+
test('From JSON', async () => {
|
114
|
+
const json = {
|
115
|
+
id: '123',
|
116
|
+
name: 'test',
|
117
|
+
age: 1,
|
118
|
+
};
|
119
|
+
|
120
|
+
const responseEM = TestRequestEM.fromJson(json);
|
121
|
+
const staticEM = TestRequestEntityMapper.fromJson(new TypeboxSchemaValidator(), json);
|
122
|
+
const expectedDto = {
|
123
|
+
id: '123',
|
124
|
+
name: 'test',
|
125
|
+
age: 1,
|
126
|
+
};
|
127
|
+
|
128
|
+
expect(staticEM.dto).toEqual(expectedDto);
|
129
|
+
expect(responseEM.dto).toEqual(expectedDto);
|
130
|
+
expect(responseEM.dto).toEqual(staticEM.dto);
|
131
|
+
});
|
132
|
+
|
133
|
+
test('Deserialization Equality', async () => {
|
134
|
+
const json = {
|
135
|
+
id: '123',
|
136
|
+
name: 'test',
|
137
|
+
age: 1,
|
138
|
+
};
|
139
|
+
|
140
|
+
const entity = extractNonTimeBasedEntityFields(TestRequestEM.deserializeJsonToEntity(json));
|
141
|
+
const objectEntity = extractNonTimeBasedEntityFields(TestRequestEM.fromJson(json).toEntity());
|
142
|
+
const staticEntity = extractNonTimeBasedEntityFields(TestRequestEntityMapper.deserializeJsonToEntity(new TypeboxSchemaValidator(), json));
|
143
|
+
let expectedEntity = new TestEntity();
|
144
|
+
expectedEntity.id = '123';
|
145
|
+
expectedEntity.name = 'test';
|
146
|
+
expectedEntity.age = 1;
|
147
|
+
|
148
|
+
expectedEntity = extractNonTimeBasedEntityFields(expectedEntity);
|
149
|
+
|
150
|
+
expect(entity).toEqual(expectedEntity);
|
151
|
+
expect(objectEntity).toEqual(expectedEntity);
|
152
|
+
expect(staticEntity).toEqual(expectedEntity);
|
153
|
+
expect(entity).toEqual(objectEntity);
|
154
|
+
expect(entity).toEqual(staticEntity);
|
155
|
+
expect(staticEntity).toEqual(expectedEntity);
|
156
|
+
expect(staticEntity).toEqual(objectEntity);
|
157
|
+
});
|
158
|
+
|
159
|
+
test('Serialization Failure', async () => {
|
160
|
+
const json = {
|
161
|
+
id: '123',
|
162
|
+
name: 'test',
|
163
|
+
};
|
164
|
+
|
165
|
+
// @ts-expect-error
|
166
|
+
expect(() => TestRequestEM.fromJson(json)).toThrow();
|
167
|
+
// @ts-expect-error
|
168
|
+
expect(() => TestRequestEntityMapper.fromJson(new TypeboxSchemaValidator(), json)).toThrow();
|
169
|
+
});
|
170
|
+
});
|
171
|
+
|
172
|
+
describe('Response Entity Mapper Test', () => {
|
173
|
+
let TestResponseEM: TestResponseEntityMapper;
|
174
|
+
|
175
|
+
beforeAll(() => {
|
176
|
+
TestResponseEM = new TestResponseEntityMapper(new TypeboxSchemaValidator());
|
177
|
+
});
|
178
|
+
|
179
|
+
test('Schema Equality', async () => {
|
180
|
+
expect(TestResponseEM.schema).toEqual(TestResponseEntityMapper.schema());
|
181
|
+
});
|
182
|
+
|
183
|
+
test('From Entity', async () => {
|
184
|
+
const entity = new TestEntity();
|
185
|
+
entity.id = '123';
|
186
|
+
entity.name = 'test';
|
187
|
+
entity.age = 1;
|
188
|
+
|
189
|
+
const responseEM = TestResponseEM.fromEntity(entity);
|
190
|
+
const staticEM = TestResponseEntityMapper.fromEntity(new TypeboxSchemaValidator(), entity);
|
191
|
+
const expectedDto = {
|
192
|
+
id: '123',
|
193
|
+
name: 'test',
|
194
|
+
age: 1,
|
195
|
+
};
|
196
|
+
|
197
|
+
expect(staticEM.dto).toEqual(expectedDto);
|
198
|
+
expect(responseEM.dto).toEqual(expectedDto);
|
199
|
+
expect(responseEM.dto).toEqual(staticEM.dto);
|
200
|
+
});
|
201
|
+
|
202
|
+
test('Serialization Equality', async () => {
|
203
|
+
const entity = new TestEntity();
|
204
|
+
entity.id = '123';
|
205
|
+
entity.name = 'test';
|
206
|
+
entity.age = 1;
|
207
|
+
|
208
|
+
const json = TestResponseEM.serializeEntityToJson(entity);
|
209
|
+
const objectJson = TestResponseEM.fromEntity(entity).toJson();
|
210
|
+
const staticJson = TestResponseEntityMapper.serializeEntityToJson(new TypeboxSchemaValidator(), entity);
|
211
|
+
const expectedJson = {
|
212
|
+
id: '123',
|
213
|
+
name: 'test',
|
214
|
+
age: 1,
|
215
|
+
};
|
216
|
+
|
217
|
+
expect(json).toEqual(expectedJson);
|
218
|
+
expect(objectJson).toEqual(expectedJson);
|
219
|
+
expect(staticJson).toEqual(expectedJson);
|
220
|
+
expect(json).toEqual(objectJson);
|
221
|
+
expect(json).toEqual(staticJson);
|
222
|
+
expect(staticJson).toEqual(expectedJson);
|
223
|
+
expect(staticJson).toEqual(objectJson);
|
224
|
+
});
|
225
|
+
|
226
|
+
test('Serialization Failure', async () => {
|
227
|
+
const entity = new TestEntity();
|
228
|
+
entity.id = '123';
|
229
|
+
entity.name = 'test';
|
230
|
+
|
231
|
+
expect(() => TestResponseEM.fromEntity(entity).toJson()).toThrow();
|
232
|
+
expect(() => TestResponseEntityMapper.fromEntity(new TypeboxSchemaValidator(), entity).toJson()).toThrow();
|
233
|
+
});
|
234
|
+
});
|
235
|
+
|