Skip to content

Commit 7cb5853

Browse files
committed
feat: implement middleware system & basic middlewares
1 parent dfc1a62 commit 7cb5853

20 files changed

Lines changed: 512 additions & 195 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface MiddlewareConfig {
2+
GlobalMiddlewares: readonly [];
3+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
export type Expand<T> = T extends object
2+
? T extends infer O
3+
? O extends Function
4+
? O
5+
: {
6+
[K in keyof O]: O[K];
7+
}
8+
: never
9+
: T;
10+
11+
export type DataReturnType<Data, TContext> = {
12+
data: Data;
13+
__context: TContext;
14+
__headers: HeaderEntry[];
15+
};
16+
17+
export type HeaderEntry = [string, string];
18+
19+
export type NextFunction = <TOutContext = undefined>(ctx?: {
20+
context: TOutContext;
21+
headers?: HeaderEntry[];
22+
}) => Promise<DataReturnType<any, TOutContext>>;
23+
24+
export type DataFunctionArgs<TInContext> = {
25+
request: Request;
26+
params: Record<string, string>;
27+
context: TInContext;
28+
};
29+
30+
export type MiddlewareFunction<in out TInContext = any, TOutContext = any> = (
31+
args: DataFunctionArgs<TInContext>,
32+
next: NextFunction,
33+
) => Promise<DataReturnType<any, TOutContext>>;
34+
35+
export type MiddlewareObject<
36+
TDependencies extends readonly MiddlewareObject[] = any,
37+
TOutContext = any,
38+
> = {
39+
deps: readonly [...TDependencies];
40+
fn: MiddlewareFunction<Expand<MergeMiddlewareContext<TDependencies>>, TOutContext>;
41+
};
42+
43+
export type MergeMiddlewareContext<T extends readonly MiddlewareObject[]> = T extends readonly [
44+
infer M,
45+
]
46+
? M extends MiddlewareObject<any, infer TOutContext>
47+
? TOutContext
48+
: never
49+
: T extends readonly [infer M, ...infer R extends readonly MiddlewareObject[]]
50+
? M extends MiddlewareObject<any, infer TOutContext>
51+
? TOutContext & MergeMiddlewareContext<R>
52+
: never
53+
: {};
54+
55+
export type ExecutionEnvironmentQueueItem = {
56+
data: { context: any } | undefined;
57+
};
58+
59+
export type ExecutionEnvironment = {
60+
queue: Map<Function, ExecutionEnvironmentQueueItem>;
61+
ctx: {
62+
request: Request;
63+
params: Record<string, string>;
64+
context: Record<string, unknown>;
65+
};
66+
};
67+
68+
export type DataWithOptions<Data> = {
69+
__dataObject: true;
70+
data: Data;
71+
headers?: HeaderEntry[];
72+
};
73+
74+
export type ServerFunction<TContext, Data> = (
75+
args: DataFunctionArgs<TContext>,
76+
) => Promise<Data | DataWithOptions<Data>>;
77+
78+
export type RemixDataFunctionArgs = {
79+
request: Request;
80+
params: Record<string, string>;
81+
};
82+
83+
export type TypedResponse<T = unknown> = T extends Response
84+
? T
85+
: Omit<Response, 'json'> & { json(): Promise<T> };
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
export type GlobalMiddlewares =
2+
import('@app-builder/core/middleware-config').MiddlewareConfig['GlobalMiddlewares'];
3+
4+
import { TypedResponse } from '@remix-run/server-runtime';
5+
import type {
6+
DataReturnType,
7+
DataWithOptions,
8+
ExecutionEnvironment,
9+
ExecutionEnvironmentQueueItem,
10+
Expand,
11+
HeaderEntry,
12+
MergeMiddlewareContext,
13+
MiddlewareFunction,
14+
MiddlewareObject,
15+
NextFunction,
16+
RemixDataFunctionArgs,
17+
ServerFunction,
18+
} from './middleware-types';
19+
20+
let globalMiddlewares: readonly MiddlewareObject[] = [];
21+
22+
const isDataWithOptions = (data: any): data is DataWithOptions<any> => {
23+
return data instanceof Object && '__dataObject' in data && data.__dataObject;
24+
};
25+
26+
function createMiddlewareCallbable(
27+
object: MiddlewareObject,
28+
env: ExecutionEnvironment,
29+
globalEnv: ExecutionEnvironment | undefined,
30+
next: NextFunction,
31+
) {
32+
// Create a env specific to middleware queue to not pollute the global request one
33+
const middlewareEnv = createExecutionEnvironment(env);
34+
middlewareEnv.ctx.context = { ...globalEnv?.ctx.context };
35+
36+
const finalMiddleware: NextFunction = async (args): Promise<DataReturnType<any, any>> => {
37+
middlewareEnv.ctx.context = { ...middlewareEnv.ctx.context, ...args?.context };
38+
return object.fn(middlewareEnv.ctx, next);
39+
};
40+
41+
return createMiddlewareChain(object.deps, middlewareEnv, globalEnv, finalMiddleware);
42+
}
43+
44+
function createServerFunctionCallable(
45+
middlewares: readonly MiddlewareObject[],
46+
env: ExecutionEnvironment,
47+
fn: ServerFunction<any, any>,
48+
) {
49+
const final: NextFunction = async (args): Promise<DataReturnType<any, any>> => {
50+
env.ctx.context = { ...globalEnv.ctx.context, ...env.ctx.context, ...args?.context };
51+
const res = await fn(env.ctx);
52+
const isResDataObject = isDataWithOptions(res);
53+
const data = isResDataObject ? res.data : res;
54+
const resHeaders = isResDataObject ? res.headers : [];
55+
56+
return {
57+
data,
58+
__context: env.ctx.context,
59+
__headers: [...(args?.headers ?? []), ...(resHeaders ?? [])],
60+
};
61+
};
62+
63+
const globalEnv = createExecutionEnvironment(env);
64+
65+
const routeMiddlewareChain = createMiddlewareChain(middlewares, env, globalEnv, final);
66+
const globalMiddlewareChain = createMiddlewareChain(
67+
globalMiddlewares,
68+
globalEnv,
69+
undefined,
70+
(nextArgs) => {
71+
globalEnv.ctx.context = { ...globalEnv.ctx.context, ...nextArgs?.context };
72+
return routeMiddlewareChain();
73+
},
74+
);
75+
76+
return globalMiddlewareChain;
77+
}
78+
79+
function createMiddlewareChain(
80+
middlewares: readonly MiddlewareObject[],
81+
env: ExecutionEnvironment,
82+
globalEnv: ExecutionEnvironment | undefined,
83+
final: NextFunction,
84+
): NextFunction {
85+
return middlewares.reduceRight<NextFunction>((nextFn, middleware) => {
86+
return async (args) => {
87+
env.ctx.context = { ...env.ctx.context, ...args?.context };
88+
89+
const queueItem = env.queue.get(middleware.fn);
90+
if (queueItem) {
91+
return nextFn(queueItem.data);
92+
}
93+
94+
const callable = createMiddlewareCallbable(middleware, env, globalEnv, (nextArgs) => {
95+
env.queue.set(middleware.fn, { data: nextArgs });
96+
return nextFn(nextArgs);
97+
});
98+
99+
return callable();
100+
};
101+
}, final);
102+
}
103+
104+
function createExecutionEnvironment(
105+
...args: [ExecutionEnvironment] | [Request, Record<string, string>]
106+
): ExecutionEnvironment {
107+
if (args[0] instanceof Request) {
108+
return {
109+
queue: new Map<Function, ExecutionEnvironmentQueueItem>(),
110+
ctx: { context: {}, request: args[0], params: args[1] ?? {} },
111+
};
112+
}
113+
114+
return {
115+
queue: args[0].queue,
116+
ctx: { ...args[0].ctx, context: {} },
117+
};
118+
}
119+
120+
function appendToHeaders(headers: Headers, entries: HeaderEntry[]): Headers {
121+
for (const [key, value] of entries) {
122+
headers.append(key, value);
123+
}
124+
return headers;
125+
}
126+
127+
function buildResponse(ret: DataReturnType<any, any>) {
128+
if (ret.data instanceof Response) {
129+
return ret.data;
130+
}
131+
132+
const headers = appendToHeaders(new Headers(), ret.__headers);
133+
return Response.json(ret.data, { headers });
134+
}
135+
136+
export function createServerFn<T extends readonly MiddlewareObject[], Ret>(
137+
middlewares: readonly [...T],
138+
fn: ServerFunction<Expand<MergeMiddlewareContext<[...GlobalMiddlewares, ...T]>>, Ret>,
139+
) {
140+
return async (args: RemixDataFunctionArgs): Promise<TypedResponse<Ret>> => {
141+
const env = createExecutionEnvironment(args.request, args.params);
142+
const middlewareChain = createServerFunctionCallable(middlewares, env, fn);
143+
144+
const ret = await middlewareChain();
145+
return buildResponse(ret);
146+
};
147+
}
148+
149+
export function createMiddleware<T extends readonly MiddlewareObject[], TOutContext>(
150+
middlewares: readonly [...T],
151+
fn: MiddlewareFunction<Expand<MergeMiddlewareContext<T>>, TOutContext>,
152+
): MiddlewareObject<any, TOutContext> {
153+
return { deps: middlewares, fn: fn as any };
154+
}
155+
156+
export function createMiddlewareWithGlobalContext<
157+
T extends readonly MiddlewareObject[],
158+
TOutContext,
159+
>(
160+
middlewares: readonly [...T],
161+
fn: MiddlewareFunction<Expand<MergeMiddlewareContext<[...GlobalMiddlewares, ...T]>>, TOutContext>,
162+
): MiddlewareObject<any, TOutContext> {
163+
return createMiddleware(middlewares, fn as any);
164+
}
165+
166+
export function setGlobalMiddlewares<T extends readonly MiddlewareObject[]>(...middlewares: T) {
167+
globalMiddlewares = middlewares;
168+
return middlewares;
169+
}

packages/app-builder/src/entry.server.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { initServerServices } from './services/init.server';
1616
import { captureUnexpectedRemixError } from './services/monitoring';
1717
import { checkEnv, getClientEnvVars, getServerEnv } from './utils/environment';
1818
import { NonceProvider } from './utils/nonce';
19+
import './global-middlewares';
1920

2021
const ABORT_DELAY = 70000;
2122

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { setGlobalMiddlewares } from '@app-builder/core/requests';
2+
import { servicesMiddleware } from './middlewares/services-middleware';
3+
4+
export const globalMiddlewares = setGlobalMiddlewares(servicesMiddleware);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createMiddlewareWithGlobalContext } from '@app-builder/core/requests';
2+
import { getRoute } from '@app-builder/utils/routes';
3+
4+
export const authMiddleware = createMiddlewareWithGlobalContext(
5+
[],
6+
async function authMiddleware({ request, context }, next) {
7+
const { authSessionService } = context.services;
8+
const authSession = await authSessionService.getSession(request);
9+
10+
const authInfo = await context.services.authService.isAuthenticated(request, {
11+
failureRedirect: getRoute('/sign-in'),
12+
});
13+
14+
const res = await next({ context: { authInfo } });
15+
16+
// Updating the token in the cookies if it has been updated
17+
const tokenUpdate = authInfo.tokenService.getUpdate();
18+
if (tokenUpdate.status) {
19+
const { marbleToken, refreshToken } = tokenUpdate;
20+
authSession.set('authToken', marbleToken);
21+
if (refreshToken) {
22+
authSession.set('refreshToken', refreshToken);
23+
}
24+
res.__headers.push(['Set-Cookie', await authSessionService.commitSession(authSession)]);
25+
}
26+
27+
return res;
28+
},
29+
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createMiddlewareWithGlobalContext } from '@app-builder/core/requests';
2+
3+
export const oidcMiddleware = createMiddlewareWithGlobalContext(
4+
[],
5+
async function oidcMiddleware({ request, context }, next) {
6+
const oidc = await context.services.authService.makeOidcService(context.appConfig);
7+
const tokens = await oidc.authenticate(request);
8+
9+
return next({ context: { oidc, oidcTokens: tokens } });
10+
},
11+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createMiddleware } from '@app-builder/core/requests';
2+
import { initServerServices } from '@app-builder/services/init.server';
3+
4+
export const servicesMiddleware = createMiddleware(
5+
[],
6+
async function servicesMiddleware({ request }, next) {
7+
const services = initServerServices(request);
8+
const appConfig = await services.appConfigRepository.getAppConfig();
9+
10+
return next({ context: { services, appConfig } });
11+
},
12+
);

0 commit comments

Comments
 (0)