Skip to content

Commit d34de8b

Browse files
authored
feat: global state inside proxy and dynamic request and response rules (#46)
* feat: support for custom state inside proxy and request/response rules * refactor: rename state interface attributes for external use * fix: fix typos that should have been avoided when copying code from response_processor to request_processor * refactor: reduce state drilling by creating singleton - GlobalStateProvider * refactor: - separate function generation and execution logic (to avoid random dataflow of sharedState as args across the code) - rename the exposed sharedState variable * refactor: make redundant sharedstate refs logic more readable
1 parent d59835c commit d34de8b

File tree

9 files changed

+152
-48
lines changed

9 files changed

+152
-48
lines changed

src/components/interfaces/state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default interface IInitialState extends Record<string, any> {
2+
sharedState?: Record<string, any>;
3+
variables?: Record<string, any>;
4+
}

src/components/proxy-middleware/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const MIDDLEWARE_TYPE = {
2929
RULES: "RULES",
3030
LOGGER: "LOGGER",
3131
SSL_CERT: "SSL_CERT",
32+
GLOBAL_STATE: "GLOBAL_STATE", // SEEMS UNUSUED, BUT ADDING FOR COMPLETENESS
3233
};
3334

3435
class ProxyMiddlewareManager {
@@ -37,7 +38,7 @@ class ProxyMiddlewareManager {
3738
proxyConfig,
3839
rulesHelper,
3940
loggerService,
40-
sslConfigFetcher
41+
sslConfigFetcher,
4142
) {
4243
/*
4344
{
@@ -59,6 +60,7 @@ class ProxyMiddlewareManager {
5960
// this.sslProxyingManager = new SSLProxyingManager(sslConfigFetcher);
6061
}
6162

63+
/* NOT USEFUL */
6264
init_config = (config = {}) => {
6365
Object.keys(MIDDLEWARE_TYPE).map((middleware_key) => {
6466
this.config[middleware_key] =
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import IInitialGlobalState from '../../interfaces/state';
2+
3+
declare class GlobalState {
4+
private state: IInitialGlobalState;
5+
constructor(initialState?: IInitialGlobalState);
6+
7+
setSharedState(newSharedState: Record<string, any>): void;
8+
getSharedStateRef(): Record<string, any>;
9+
getSharedStateCopy(): Record<string, any>;
10+
setVariables(newVariables: Record<string, any>): void;
11+
getVariablesRef(): Record<string, any>;
12+
getVariablesCopy(): Record<string, any>;
13+
}
14+
15+
declare const ProxyGlobal: GlobalState;
16+
17+
export { GlobalState as default, ProxyGlobal };
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {cloneDeep} from 'lodash';
2+
3+
interface IState extends Record<string, any> {
4+
sharedState?: Record<string, any>;
5+
variables?: Record<string, any>;
6+
}
7+
8+
export class State {
9+
private state: IState;
10+
constructor(sharedState: Record<string, any>, envVars: Record<string, any>) {
11+
this.state = {
12+
variables: envVars,
13+
sharedState,
14+
};
15+
}
16+
17+
setSharedState(newSharedState: Record<string, any>) {
18+
this.state.sharedState = newSharedState;
19+
}
20+
21+
getSharedStateRef() {
22+
return this.state.sharedState;
23+
}
24+
25+
getSharedStateCopy() {
26+
return cloneDeep(this.state.sharedState);
27+
}
28+
29+
setVariables(newVariables: Record<string, any>) {
30+
this.state.variables = newVariables;
31+
}
32+
33+
getVariablesRef() {
34+
return this.state.variables;
35+
}
36+
37+
getVariablesCopy() {
38+
return cloneDeep(this.state.variables);
39+
}
40+
}
41+
42+
export default class GlobalStateProvider {
43+
private static instance: State
44+
static initInstance(sharedState: Record<string, any> = {}, envVars: Record<string, any> = {}) {
45+
if (!GlobalStateProvider.instance) {
46+
GlobalStateProvider.instance = new State(sharedState ?? {}, envVars ?? {});
47+
}
48+
49+
return GlobalStateProvider.instance;
50+
}
51+
52+
static getInstance() {
53+
if (!GlobalStateProvider.instance) {
54+
console.error("[GlobalStateProvider]", "Init first");
55+
}
56+
57+
return GlobalStateProvider.instance;
58+
}
59+
}

src/components/proxy-middleware/rule_action_processor/processors/modify_request_processor.js

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
} from "@requestly/requestly-core";
66
import { get_request_url } from "../../helpers/proxy_ctx_helper";
77
import { build_action_processor_response } from "../utils";
8-
import ConsoleCapture from "capture-console-logs";
9-
import { getFunctionFromString } from "../../../../utils";
10-
11-
const { types } = require("util");
8+
import { executeUserFunction, getFunctionFromString } from "../../../../utils";
129

1310
const process_modify_request_action = (action, ctx) => {
1411
const allowed_handlers = [PROXY_HANDLER_TYPE.ON_REQUEST_END];
@@ -76,24 +73,7 @@ const modify_request_using_code = async (action, ctx) => {
7673
/*Do nothing -- could not parse body as JSON */
7774
}
7875

79-
const consoleCapture = new ConsoleCapture()
80-
consoleCapture.start(true)
81-
82-
finalRequest = userFunction(args);
83-
84-
if (types.isPromise(finalRequest)) {
85-
finalRequest = await finalRequest;
86-
}
87-
88-
consoleCapture.stop()
89-
const consoleLogs = consoleCapture.getCaptures()
90-
91-
ctx.rq.consoleLogs.push(...consoleLogs)
92-
93-
const isRequestJSON = !!args.bodyAsJson;
94-
if (typeof finalRequest === "object" && isRequestJSON) {
95-
finalRequest = JSON.stringify(finalRequest);
96-
}
76+
finalRequest = await executeUserFunction(ctx, userFunction, args)
9777

9878
if (finalRequest && typeof finalRequest === "string") {
9979
return modify_request(ctx, finalRequest);

src/components/proxy-middleware/rule_action_processor/processors/modify_response_processor.js

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@ import { getResponseContentTypeHeader, getResponseHeaders, get_request_url } fro
77
import { build_action_processor_response, build_post_process_data } from "../utils";
88
import fs from "fs";
99
import { getContentType, parseJsonBody } from "../../helpers/http_helpers";
10-
import ConsoleCapture from "capture-console-logs";
11-
import { getFunctionFromString } from "../../../../utils";
10+
import { executeUserFunction, getFunctionFromString } from "../../../../utils";
1211
import { RQ_INTERCEPTED_CONTENT_TYPES } from "../../constants";
1312

14-
const { types } = require("util");
15-
1613
const process_modify_response_action = async (action, ctx) => {
1714
const allowed_handlers = [PROXY_HANDLER_TYPE.ON_REQUEST,PROXY_HANDLER_TYPE.ON_RESPONSE_END, PROXY_HANDLER_TYPE.ON_ERROR];
1815

@@ -144,23 +141,7 @@ const modify_response_using_code = async (action, ctx) => {
144141
/*Do nothing -- could not parse body as JSON */
145142
}
146143

147-
const consoleCapture = new ConsoleCapture()
148-
consoleCapture.start(true)
149-
150-
finalResponse = userFunction(args);
151-
152-
if (types.isPromise(finalResponse)) {
153-
finalResponse = await finalResponse;
154-
}
155-
156-
consoleCapture.stop()
157-
const consoleLogs = consoleCapture.getCaptures()
158-
159-
ctx.rq.consoleLogs.push(...consoleLogs)
160-
161-
if (typeof finalResponse === "object") {
162-
finalResponse = JSON.stringify(finalResponse);
163-
}
144+
finalResponse = await executeUserFunction(ctx, action.response, args)
164145

165146
if (finalResponse && typeof finalResponse === "string") {
166147
return modify_response(ctx, finalResponse, action.statusCode);

src/rq-proxy-provider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import RQProxy from "./rq-proxy";
22
import ILoggerService from "./components/interfaces/logger-service";
33
import IRulesDataSource from "./components/interfaces/rules-data-source";
4+
import IInitialState from "./components/interfaces/state";
45
import { ProxyConfig } from "./types";
56

67
class RQProxyProvider {
78
static rqProxyInstance:any = null;
89

910
// TODO: rulesDataSource can be static here
10-
static createInstance = (proxyConfig: ProxyConfig, rulesDataSource: IRulesDataSource, loggerService: ILoggerService) => {
11-
RQProxyProvider.rqProxyInstance = new RQProxy(proxyConfig, rulesDataSource, loggerService);
11+
static createInstance = (proxyConfig: ProxyConfig, rulesDataSource: IRulesDataSource, loggerService: ILoggerService, initialGlobalState?: IInitialState) => {
12+
RQProxyProvider.rqProxyInstance = new RQProxy(proxyConfig, rulesDataSource, loggerService, initialGlobalState);
1213
}
1314

1415
static getInstance = (): RQProxy => {

src/rq-proxy.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ProxyConfig } from "./types";
44
import RulesHelper from "./utils/helpers/rules-helper";
55
import ProxyMiddlewareManager from "./components/proxy-middleware";
66
import ILoggerService from "./components/interfaces/logger-service";
7+
import IInitialState from "./components/interfaces/state";
8+
import GlobalStateProvider, {State} from "./components/proxy-middleware/middlewares/state";
79

810

911
class RQProxy {
@@ -12,12 +14,19 @@ class RQProxy {
1214

1315
rulesHelper: RulesHelper;
1416
loggerService: ILoggerService;
17+
globalState: State;
1518

16-
constructor(proxyConfig: ProxyConfig, rulesDataSource: IRulesDataSource, loggerService: ILoggerService) {
19+
constructor(
20+
proxyConfig: ProxyConfig,
21+
rulesDataSource: IRulesDataSource,
22+
loggerService: ILoggerService,
23+
initialGlobalState?: IInitialState
24+
) {
1725
this.initProxy(proxyConfig);
1826

1927
this.rulesHelper = new RulesHelper(rulesDataSource);
2028
this.loggerService = loggerService;
29+
this.globalState = GlobalStateProvider.initInstance(initialGlobalState?.sharedState ?? {}, initialGlobalState?.variables ?? {});
2130
}
2231

2332
initProxy = (proxyConfig: ProxyConfig) => {

src/utils/index.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,54 @@
1+
import { types } from "util";
2+
import ConsoleCapture from "capture-console-logs";
3+
import GlobalStateProvider from "../components/proxy-middleware/middlewares/state";
4+
5+
// Only used for verification now. For execution, we regenerate the function in executeUserFunction with the sharedState
16
export const getFunctionFromString = function (functionStringEscaped) {
2-
return new Function("return " + functionStringEscaped)();
7+
return new Function(`return ${functionStringEscaped}`)();
38
};
9+
10+
11+
/* Expects that the functionString has already been validated to be representing a proper function */
12+
export async function executeUserFunction(ctx, functionString: string, args) {
13+
14+
const generateFunctionWithSharedState = function (functionStringEscaped) {
15+
16+
const SHARED_STATE_VAR_NAME = "$sharedState";
17+
18+
const sharedState = GlobalStateProvider.getInstance().getSharedStateCopy();
19+
20+
return new Function(`${SHARED_STATE_VAR_NAME}`, `return { func: ${functionStringEscaped}, updatedSharedState: ${SHARED_STATE_VAR_NAME}}`)(sharedState);
21+
};
22+
23+
const {func: generatedFunction, updatedSharedState} = generateFunctionWithSharedState(functionString);
24+
25+
const consoleCapture = new ConsoleCapture()
26+
consoleCapture.start(true)
27+
28+
let finalResponse = generatedFunction(args);
29+
30+
if (types.isPromise(finalResponse)) {
31+
finalResponse = await finalResponse;
32+
}
33+
34+
consoleCapture.stop()
35+
const consoleLogs = consoleCapture.getCaptures()
36+
37+
ctx.rq.consoleLogs.push(...consoleLogs)
38+
39+
/**
40+
* If we use GlobalState.getSharedStateRef instead of GlobalState.getSharedStateCopy
41+
* then this update is completely unnecessary.
42+
* Because then the function gets a reference to the global states,
43+
* and any changes made inside the userFunction will directly be reflected there.
44+
*
45+
* But we are using it here to make the data flow obvious as we read this code.
46+
*/
47+
GlobalStateProvider.getInstance().setSharedState(updatedSharedState);
48+
49+
if (typeof finalResponse === "object") {
50+
finalResponse = JSON.stringify(finalResponse);
51+
}
52+
53+
return finalResponse;
54+
}

0 commit comments

Comments
 (0)