@creator.co/wapi 1.1.8 → 1.2.0-alpha2
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/.eslintrc.cjs +22 -22
- package/README.md +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/package.json +50 -0
- package/dist/src/API/Request.d.ts +4 -5
- package/dist/src/API/Request.js +3 -4
- package/dist/src/API/Request.js.map +1 -1
- package/dist/src/API/Response.d.ts +1 -1
- package/dist/src/API/Response.js.map +1 -1
- package/dist/src/BaseEvent/EventProcessor.js +0 -1
- package/dist/src/BaseEvent/EventProcessor.js.map +1 -1
- package/dist/src/BaseEvent/Process.d.ts +4 -7
- package/dist/src/BaseEvent/Process.js +1 -2
- package/dist/src/BaseEvent/Process.js.map +1 -1
- package/dist/src/BaseEvent/Transaction.d.ts +6 -6
- package/dist/src/BaseEvent/Transaction.js +2 -4
- package/dist/src/BaseEvent/Transaction.js.map +1 -1
- package/dist/src/Config/Configuration.d.ts +28 -0
- package/dist/src/Config/Configuration.js +93 -0
- package/dist/src/Config/Configuration.js.map +1 -0
- package/dist/src/Config/EnvironmentVar.d.ts +17 -0
- package/dist/src/Config/EnvironmentVar.js +155 -0
- package/dist/src/Config/EnvironmentVar.js.map +1 -0
- package/dist/src/Globals.d.ts +11 -0
- package/dist/src/Globals.js +12 -0
- package/dist/src/Globals.js.map +1 -1
- package/dist/src/Logger/Logger.js +42 -11
- package/dist/src/Logger/Logger.js.map +1 -1
- package/dist/src/Mailer/Mailer.js +50 -24
- package/dist/src/Mailer/Mailer.js.map +1 -1
- package/dist/src/Server/Router.d.ts +30 -0
- package/dist/src/Server/Router.js +21 -0
- package/dist/src/Server/Router.js.map +1 -0
- package/dist/src/Server/lib/ContainerServer.d.ts +11 -0
- package/dist/src/Server/lib/ContainerServer.js +100 -0
- package/dist/src/Server/lib/ContainerServer.js.map +1 -0
- package/dist/src/Server/lib/Server.d.ts +9 -0
- package/dist/src/Server/lib/Server.js +137 -0
- package/dist/src/Server/lib/Server.js.map +1 -0
- package/dist/src/Server/lib/container/GenericHandler.d.ts +4 -0
- package/dist/src/Server/lib/container/GenericHandler.js +138 -0
- package/dist/src/Server/lib/container/GenericHandler.js.map +1 -0
- package/dist/src/Server/lib/container/GenericHandlerEvent.d.ts +14 -0
- package/dist/src/Server/lib/container/GenericHandlerEvent.js +164 -0
- package/dist/src/Server/lib/container/GenericHandlerEvent.js.map +1 -0
- package/dist/src/Server/lib/container/HealthHandler.d.ts +3 -0
- package/dist/src/Server/lib/container/HealthHandler.js +44 -0
- package/dist/src/Server/lib/container/HealthHandler.js.map +1 -0
- package/dist/src/Server/lib/container/Proxy.d.ts +15 -0
- package/dist/src/Server/lib/container/Proxy.js +153 -0
- package/dist/src/Server/lib/container/Proxy.js.map +1 -0
- package/dist/src/Server/lib/container/Utils.d.ts +6 -0
- package/dist/src/Server/lib/container/Utils.js +109 -0
- package/dist/src/Server/lib/container/Utils.js.map +1 -0
- package/dist/src/Validation/Validator.d.ts +5 -0
- package/dist/src/Validation/Validator.js +39 -0
- package/dist/src/Validation/Validator.js.map +1 -0
- package/index.ts +8 -6
- package/package.json +14 -3
- package/src/API/Request.ts +6 -12
- package/src/API/Response.ts +1 -1
- package/src/BaseEvent/EventProcessor.ts +3 -2
- package/src/BaseEvent/Process.ts +11 -10
- package/src/BaseEvent/Transaction.ts +24 -16
- package/src/Config/Configuration.ts +83 -0
- package/src/Config/EnvironmentVar.ts +94 -0
- package/src/Globals.ts +15 -0
- package/src/Server/Router.ts +51 -0
- package/src/Server/lib/ContainerServer.ts +27 -0
- package/src/Server/lib/Server.ts +66 -0
- package/src/Server/lib/container/GenericHandler.ts +63 -0
- package/src/Server/lib/container/GenericHandlerEvent.ts +133 -0
- package/src/Server/lib/container/HealthHandler.ts +5 -0
- package/src/Server/lib/container/Proxy.ts +107 -0
- package/src/Server/lib/container/Utils.ts +45 -0
- package/src/Validation/Validator.ts +35 -0
- package/tsconfig.json +2 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as MemCache from "node-cache"
|
|
2
|
+
import * as DurationParser from "parse-duration"
|
|
3
|
+
|
|
4
|
+
import EnvironmentVar, { EnvironmentType } from "./EnvironmentVar"
|
|
5
|
+
|
|
6
|
+
// Hold cached values at nodeVM level
|
|
7
|
+
const cacheStore = new MemCache()
|
|
8
|
+
|
|
9
|
+
export type ConfigurationSchema = {
|
|
10
|
+
[name: string]: {
|
|
11
|
+
type: EnvironmentType
|
|
12
|
+
required?: boolean
|
|
13
|
+
cachingPolicy?: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// runtime type infer to ConfigurationSchema keys, which type is remote
|
|
18
|
+
type ExtractRemote<Type> = keyof {
|
|
19
|
+
[Property in keyof Type]: Type[Property] extends {
|
|
20
|
+
type: EnvironmentType.PlainRemote | EnvironmentType.SecureRemote
|
|
21
|
+
}
|
|
22
|
+
? Property
|
|
23
|
+
: never
|
|
24
|
+
}
|
|
25
|
+
// runtime type infer to ConfigurationSchema keys, which type is local
|
|
26
|
+
type ExtractLocal<Type> = keyof {
|
|
27
|
+
[Property in keyof Type]: Type[Property] extends {
|
|
28
|
+
type: EnvironmentType.Local
|
|
29
|
+
}
|
|
30
|
+
? Property
|
|
31
|
+
: never
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default class Configuration<T extends ConfigurationSchema> {
|
|
35
|
+
private readonly schema: T
|
|
36
|
+
private readonly remotePrefix: string
|
|
37
|
+
constructor(schema: T, remotePrefix: string) {
|
|
38
|
+
this.schema = schema
|
|
39
|
+
this.remotePrefix = remotePrefix
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public get(propName: ExtractLocal<T>): any {
|
|
43
|
+
propName = propName as string // TODO: find a better way, this seems to come from keyof
|
|
44
|
+
const propSchema = this.schema[propName]
|
|
45
|
+
let v = this.getCachedValue(propName)
|
|
46
|
+
v =
|
|
47
|
+
v ||
|
|
48
|
+
new EnvironmentVar<string>(
|
|
49
|
+
propName,
|
|
50
|
+
propSchema.type,
|
|
51
|
+
propSchema.required,
|
|
52
|
+
this.remotePrefix,
|
|
53
|
+
).syncResolve()
|
|
54
|
+
this.cacheValue(propName, v, propSchema.cachingPolicy)
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
public async asyncGet(propName: ExtractRemote<T>): Promise<any> {
|
|
58
|
+
propName = propName as string // TODO: find a better way, this seems to come from keyof
|
|
59
|
+
const propSchema = this.schema[propName]
|
|
60
|
+
let v = this.getCachedValue(propName)
|
|
61
|
+
v =
|
|
62
|
+
v ||
|
|
63
|
+
(await new EnvironmentVar<string>(
|
|
64
|
+
propName,
|
|
65
|
+
propSchema.type,
|
|
66
|
+
propSchema.required,
|
|
67
|
+
this.remotePrefix,
|
|
68
|
+
).resolve())
|
|
69
|
+
this.cacheValue(propName, v, propSchema.cachingPolicy)
|
|
70
|
+
return v
|
|
71
|
+
}
|
|
72
|
+
// caching layer
|
|
73
|
+
private getCachedValue(valueKey: string): any {
|
|
74
|
+
return cacheStore.get(valueKey)
|
|
75
|
+
}
|
|
76
|
+
private cacheValue(
|
|
77
|
+
valueKey: string,
|
|
78
|
+
value: any,
|
|
79
|
+
policy: string = "1d",
|
|
80
|
+
): void {
|
|
81
|
+
cacheStore.set(valueKey, value, DurationParser(policy, "s"))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SecretsManagerClient,
|
|
3
|
+
GetSecretValueCommand,
|
|
4
|
+
} from "@aws-sdk/client-secrets-manager"
|
|
5
|
+
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"
|
|
6
|
+
// important for dev env to load .env file
|
|
7
|
+
import * as dotenv from "dotenv"
|
|
8
|
+
dotenv.config()
|
|
9
|
+
|
|
10
|
+
// Explicity setting VM level variables in order to persist
|
|
11
|
+
// client across different important and instances.
|
|
12
|
+
// eslint-disable-next-line no-var
|
|
13
|
+
var ssmClient = null,
|
|
14
|
+
secretsClient = null
|
|
15
|
+
|
|
16
|
+
export enum EnvironmentType {
|
|
17
|
+
PlainRemote,
|
|
18
|
+
SecureRemote,
|
|
19
|
+
Local,
|
|
20
|
+
}
|
|
21
|
+
export default class EnvironmentVar<T> {
|
|
22
|
+
private readonly paramName: string
|
|
23
|
+
private readonly type: EnvironmentType
|
|
24
|
+
private readonly optional: boolean
|
|
25
|
+
private readonly paramPrefix: string
|
|
26
|
+
constructor(
|
|
27
|
+
paramName: string,
|
|
28
|
+
type: EnvironmentType,
|
|
29
|
+
optional?: boolean,
|
|
30
|
+
paramPrefix = `/creatorco-api-${process.env.STAGE}/env/` /* only allowed process.env */,
|
|
31
|
+
) {
|
|
32
|
+
this.paramName = paramName
|
|
33
|
+
this.type = type
|
|
34
|
+
this.optional = !!optional
|
|
35
|
+
this.paramPrefix = paramPrefix
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public syncResolve(): T {
|
|
39
|
+
if (this.type == EnvironmentType.Local) return this.getLocalValue()
|
|
40
|
+
else {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"EnvironmentVar syncResolve method can only be called for environments of type 'Local'",
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
public async resolve() {
|
|
47
|
+
if (this.type == EnvironmentType.Local) return this.getLocalValue()
|
|
48
|
+
else if (this.type == EnvironmentType.SecureRemote)
|
|
49
|
+
return this.getSecureValue()
|
|
50
|
+
else return this.getPlainValue()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getLocalValue(): T {
|
|
54
|
+
const value = process.env[this.paramName]
|
|
55
|
+
if (!value && !this.optional) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Local environment ${this.paramName} is not defined, please reconfigure the application and redeploy!`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return value as T
|
|
61
|
+
}
|
|
62
|
+
private async getPlainValue(): Promise<T> {
|
|
63
|
+
if (!ssmClient) ssmClient = new SSMClient()
|
|
64
|
+
const pName = `${this.paramPrefix}${this.paramName}`
|
|
65
|
+
try {
|
|
66
|
+
const data = await ssmClient.send(
|
|
67
|
+
new GetParameterCommand({ Name: pName }),
|
|
68
|
+
)
|
|
69
|
+
if (data.Parameter && data.Parameter.Value)
|
|
70
|
+
return data.Parameter.Value as T
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(error)
|
|
73
|
+
}
|
|
74
|
+
if (!this.optional) {
|
|
75
|
+
throw new Error(`Unable to retrieve plain remote env value: ${pName}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
private async getSecureValue(): Promise<T> {
|
|
79
|
+
if (!secretsClient) secretsClient = new SecretsManagerClient()
|
|
80
|
+
const pName = `${this.paramPrefix}${this.paramName}`
|
|
81
|
+
try {
|
|
82
|
+
const data = await secretsClient.send(
|
|
83
|
+
new GetSecretValueCommand({ SecretId: pName }),
|
|
84
|
+
)
|
|
85
|
+
if (data.Parameter && data.Parameter.Value)
|
|
86
|
+
return data.Parameter.Value as T
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(error)
|
|
89
|
+
}
|
|
90
|
+
if (!this.optional) {
|
|
91
|
+
throw new Error(`Unable to retrieve secure remote env: ${pName}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/Globals.ts
CHANGED
|
@@ -11,4 +11,19 @@ export default class Globals {
|
|
|
11
11
|
public static ErrorCode_InvalidInput = "INVALID_INPUT"
|
|
12
12
|
public static ErrorCode_APIError = "API_ERROR"
|
|
13
13
|
public static ErrorCode_NoRecords = "EMPTY_EVENT"
|
|
14
|
+
|
|
15
|
+
public static Listener_HTTP_DefaultPort = 80
|
|
16
|
+
public static Listener_HTTP_DefaultHost = "localhost"
|
|
17
|
+
public static Listener_HTTP_ProxyRoute = "*"
|
|
18
|
+
public static Listener_HTTP_DefaultTimeout = 30000
|
|
19
|
+
public static Listener_HTTP_DefaultHealthCheckRoute = "/health"
|
|
20
|
+
//Resps
|
|
21
|
+
public static Resp_MSG_EXCEPTION =
|
|
22
|
+
"[Proxy]: Exception during request execution!"
|
|
23
|
+
public static Resp_CODE_EXCEPTION = "EXEC_EXCEPTION"
|
|
24
|
+
public static Resp_STATUSCODE_EXCEPTION = 502
|
|
25
|
+
|
|
26
|
+
public static Resp_MSG_INVALIDRESP = "[Proxy]: Invalid response from server!"
|
|
27
|
+
public static Resp_CODE_INVALIDRESP = "EMPTY_RESPONSE"
|
|
28
|
+
public static Resp_STATUSCODE_INVALIDRESP = 400
|
|
14
29
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
import ContainerServer from "./lib/ContainerServer"
|
|
4
|
+
import Server from "./lib/Server"
|
|
5
|
+
import Transaction, {
|
|
6
|
+
TransactionConfig,
|
|
7
|
+
TransactionExecution,
|
|
8
|
+
} from "../BaseEvent/Transaction"
|
|
9
|
+
|
|
10
|
+
// Route
|
|
11
|
+
export type Route<InputType = object> = {
|
|
12
|
+
path: string
|
|
13
|
+
method: string
|
|
14
|
+
handler: TransactionExecution<Transaction<InputType>>
|
|
15
|
+
inputValidation?: z.ZodObject<any>
|
|
16
|
+
outputValidation?: {
|
|
17
|
+
type: any
|
|
18
|
+
validationSchema: z.ZodObject<any>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Config
|
|
23
|
+
export type RouterConfig = TransactionConfig & {
|
|
24
|
+
routes: Route[]
|
|
25
|
+
// container based configs - otherwise enforced by serverless engine
|
|
26
|
+
port?: number
|
|
27
|
+
timeout?: number
|
|
28
|
+
cors?: {
|
|
29
|
+
origin?: string | string[]
|
|
30
|
+
headers?: string[]
|
|
31
|
+
allowCredentials?: boolean
|
|
32
|
+
}
|
|
33
|
+
healthCheckRoute?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default class Router {
|
|
37
|
+
private readonly config: RouterConfig
|
|
38
|
+
private readonly server: Server
|
|
39
|
+
constructor(config: RouterConfig) {
|
|
40
|
+
this.config = config
|
|
41
|
+
this.server = this.isContainer()
|
|
42
|
+
? new ContainerServer(config)
|
|
43
|
+
: new Server(config)
|
|
44
|
+
}
|
|
45
|
+
public getExport(): CallableFunction {
|
|
46
|
+
return this.server.getExport()
|
|
47
|
+
}
|
|
48
|
+
private isContainer(): boolean {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Proxy from "./container/Proxy"
|
|
2
|
+
import Server from "./Server"
|
|
3
|
+
import { RouterConfig } from "../Router"
|
|
4
|
+
|
|
5
|
+
export default class ContainerServer extends Server {
|
|
6
|
+
protected readonly proxy: Proxy
|
|
7
|
+
constructor(config: RouterConfig) {
|
|
8
|
+
super(config)
|
|
9
|
+
this.proxy = new Proxy(config, this.handleServerlessEvent.bind(this))
|
|
10
|
+
this.listenProcessEvents()
|
|
11
|
+
}
|
|
12
|
+
public getExport(): CallableFunction {
|
|
13
|
+
return this.start
|
|
14
|
+
}
|
|
15
|
+
/* private */
|
|
16
|
+
public async start() {
|
|
17
|
+
await this.proxy.load()
|
|
18
|
+
}
|
|
19
|
+
public async stop(err) {
|
|
20
|
+
await this.proxy.unload(err)
|
|
21
|
+
}
|
|
22
|
+
private listenProcessEvents() {
|
|
23
|
+
// start process listeners
|
|
24
|
+
process.on("unhandledRejection", this.stop.bind(this)) // listen to exceptions
|
|
25
|
+
process.on("SIGINT", this.stop.bind(this)) // listen on SIGINT signal and gracefully stop the server
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { APIGatewayProxyEvent, Context } from "aws-lambda"
|
|
2
|
+
import { pathToRegexp } from "path-to-regexp"
|
|
3
|
+
|
|
4
|
+
import Response from "../../API/Response"
|
|
5
|
+
import Transaction from "../../BaseEvent/Transaction"
|
|
6
|
+
import Validator from "../../Validation/Validator"
|
|
7
|
+
import { RouterConfig } from "../Router"
|
|
8
|
+
|
|
9
|
+
export default class Server {
|
|
10
|
+
protected readonly config: RouterConfig
|
|
11
|
+
constructor(config: RouterConfig) {
|
|
12
|
+
this.config = config
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public getExport(): CallableFunction {
|
|
16
|
+
return this.handleServerlessEvent
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async handleServerlessEvent(
|
|
20
|
+
event: APIGatewayProxyEvent,
|
|
21
|
+
context: Context,
|
|
22
|
+
) {
|
|
23
|
+
// init transaction
|
|
24
|
+
await new Transaction(event, context, this.config).execute(
|
|
25
|
+
async (transaction) => {
|
|
26
|
+
for (const route of this.config.routes) {
|
|
27
|
+
// Check for possible paths
|
|
28
|
+
const handler = this.routeMatches(route.path, route.method, event)
|
|
29
|
+
if (handler) {
|
|
30
|
+
transaction.logger.log("Router accepted route:", route)
|
|
31
|
+
// Validate input?
|
|
32
|
+
if (route.inputValidation) {
|
|
33
|
+
const validationResp = Validator.validateSchema(
|
|
34
|
+
transaction.request.getBody(),
|
|
35
|
+
route.inputValidation,
|
|
36
|
+
)
|
|
37
|
+
if (validationResp && validationResp instanceof Response)
|
|
38
|
+
return validationResp
|
|
39
|
+
}
|
|
40
|
+
// Continue to route handler
|
|
41
|
+
return await route.handler(transaction)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//No route found :/
|
|
45
|
+
return new Response(404, { err: "Route not found!" })
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
// Helper function to match route
|
|
50
|
+
private routeMatches(
|
|
51
|
+
routePath: string,
|
|
52
|
+
routeMethod: string,
|
|
53
|
+
event: APIGatewayProxyEvent,
|
|
54
|
+
): boolean {
|
|
55
|
+
const path = event.path
|
|
56
|
+
const keys = []
|
|
57
|
+
const result = pathToRegexp(routePath, keys).exec(path)
|
|
58
|
+
if (result) {
|
|
59
|
+
const reqMethod = event.httpMethod
|
|
60
|
+
if (reqMethod.toLowerCase() == routeMethod.toLowerCase()) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Request, Response } from "express"
|
|
2
|
+
|
|
3
|
+
import GenericHandlerEvent, {
|
|
4
|
+
GenericHandlerEventResponse,
|
|
5
|
+
} from "./GenericHandlerEvent"
|
|
6
|
+
import Globals from "../../../Globals"
|
|
7
|
+
import Server from "../Server"
|
|
8
|
+
|
|
9
|
+
export default (serverlessHandler: Server["handleServerlessEvent"]) => {
|
|
10
|
+
return async (request: Request, res: Response) => {
|
|
11
|
+
const startTime = Date.now()
|
|
12
|
+
let resp = null
|
|
13
|
+
try {
|
|
14
|
+
// Generate event with request stuff (http to serverless translation)
|
|
15
|
+
const event = new GenericHandlerEvent(request, serverlessHandler)
|
|
16
|
+
// Invoke
|
|
17
|
+
const invokationResp = await event.invoke()
|
|
18
|
+
// Respond
|
|
19
|
+
resp = processServerlessResponse(invokationResp, res)
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error("[Proxy] - Exception during execution!", e)
|
|
22
|
+
resp = res
|
|
23
|
+
.json({
|
|
24
|
+
...e,
|
|
25
|
+
err: Globals.Resp_MSG_EXCEPTION,
|
|
26
|
+
errCode: Globals.Resp_CODE_EXCEPTION,
|
|
27
|
+
})
|
|
28
|
+
.status(Globals.Resp_STATUSCODE_EXCEPTION) //bad gateway
|
|
29
|
+
}
|
|
30
|
+
console.debug(`[Proxy] - Request took ${Date.now() - startTime}ms`)
|
|
31
|
+
return resp
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const processServerlessResponse = (
|
|
35
|
+
invokation: GenericHandlerEventResponse,
|
|
36
|
+
res: Response,
|
|
37
|
+
) => {
|
|
38
|
+
// translate answer to http layer
|
|
39
|
+
if (invokation && invokation.err) {
|
|
40
|
+
// err came
|
|
41
|
+
return res.json({ err: invokation.err }).status(400)
|
|
42
|
+
} else if (!invokation || !invokation.data) {
|
|
43
|
+
// invalid response came
|
|
44
|
+
return res
|
|
45
|
+
.json({
|
|
46
|
+
err: Globals.Resp_MSG_INVALIDRESP,
|
|
47
|
+
errCode: Globals.Resp_CODE_INVALIDRESP,
|
|
48
|
+
})
|
|
49
|
+
.status(Globals.Resp_STATUSCODE_INVALIDRESP)
|
|
50
|
+
} else {
|
|
51
|
+
// valid
|
|
52
|
+
const response = res
|
|
53
|
+
.json(invokation.data.body || {})
|
|
54
|
+
.status(invokation.data.statusCode)
|
|
55
|
+
// Check for headers
|
|
56
|
+
if (invokation?.data?.headers) {
|
|
57
|
+
for (const hKey of Object.keys(invokation.data.headers)) {
|
|
58
|
+
response.header(hKey, invokation.data.headers[hKey])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return response
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, Context } from "aws-lambda"
|
|
2
|
+
import * as cuid from "cuid"
|
|
3
|
+
import { Request } from "express"
|
|
4
|
+
import unflatten from "unflatten"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
parseMultiValueHeaders,
|
|
8
|
+
parseMultiValueQueryStringParameters,
|
|
9
|
+
parseQueryStringParameters,
|
|
10
|
+
nullIfEmpty,
|
|
11
|
+
} from "./Utils"
|
|
12
|
+
import Globals from "../../../Globals"
|
|
13
|
+
import Server from "../Server"
|
|
14
|
+
//
|
|
15
|
+
export type GenericHandlerEventResponse = { err?: Error | string; data?: any }
|
|
16
|
+
//
|
|
17
|
+
export default class GenericHandlerEvent {
|
|
18
|
+
public request: Request
|
|
19
|
+
public serverlessHandler: Server["handleServerlessEvent"]
|
|
20
|
+
constructor(
|
|
21
|
+
request: Request,
|
|
22
|
+
serverlessHandler: Server["handleServerlessEvent"],
|
|
23
|
+
) {
|
|
24
|
+
this.request = request
|
|
25
|
+
this.serverlessHandler = serverlessHandler
|
|
26
|
+
}
|
|
27
|
+
public async invoke(): Promise<GenericHandlerEventResponse> {
|
|
28
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
29
|
+
return new Promise(async (resolve, reject) => {
|
|
30
|
+
try {
|
|
31
|
+
// Build event and context
|
|
32
|
+
const event = this.buildEvent()
|
|
33
|
+
const context = this.buildContext(
|
|
34
|
+
event,
|
|
35
|
+
(err?: Error | string, data?: any) => {
|
|
36
|
+
resolve({ err, data })
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
// Invoke
|
|
40
|
+
await this.serverlessHandler(event, context)
|
|
41
|
+
} catch (e) {
|
|
42
|
+
reject(e) // forward rejection
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
/* private */
|
|
47
|
+
private buildEvent(): APIGatewayProxyEvent {
|
|
48
|
+
return {
|
|
49
|
+
body: this.request.body || null, //enforce key
|
|
50
|
+
headers: this.request.headers ? unflatten(this.request.headers, 2) : [],
|
|
51
|
+
httpMethod: this.request.method
|
|
52
|
+
? this.request.method.toUpperCase()
|
|
53
|
+
: null,
|
|
54
|
+
isBase64Encoded: false,
|
|
55
|
+
multiValueHeaders: parseMultiValueHeaders(
|
|
56
|
+
this.request.headers ? this.request.headers || [] : [],
|
|
57
|
+
),
|
|
58
|
+
multiValueQueryStringParameters: parseMultiValueQueryStringParameters(
|
|
59
|
+
this.request.url,
|
|
60
|
+
),
|
|
61
|
+
path: this.request.path,
|
|
62
|
+
pathParameters: this.request.params
|
|
63
|
+
? nullIfEmpty(this.request.params)
|
|
64
|
+
: null,
|
|
65
|
+
queryStringParameters: this.request.query
|
|
66
|
+
? parseQueryStringParameters(this.request.query)
|
|
67
|
+
: null,
|
|
68
|
+
requestContext: {
|
|
69
|
+
accountId: process.env.AWS_ACCOUNT_ID || null,
|
|
70
|
+
apiId: null,
|
|
71
|
+
authorizer: null,
|
|
72
|
+
domainName: null,
|
|
73
|
+
domainPrefix: null,
|
|
74
|
+
extendedRequestId: cuid(),
|
|
75
|
+
httpMethod: this.request.method
|
|
76
|
+
? this.request.method.toUpperCase()
|
|
77
|
+
: null,
|
|
78
|
+
identity: {
|
|
79
|
+
accessKey: null,
|
|
80
|
+
accountId: process.env.AWS_ACCOUNT_ID || null,
|
|
81
|
+
caller: null,
|
|
82
|
+
apiKey: null,
|
|
83
|
+
apiKeyId: null,
|
|
84
|
+
clientCert: null,
|
|
85
|
+
cognitoAuthenticationProvider: null,
|
|
86
|
+
cognitoAuthenticationType: null,
|
|
87
|
+
cognitoIdentityId: null,
|
|
88
|
+
cognitoIdentityPoolId: null,
|
|
89
|
+
principalOrgId: null,
|
|
90
|
+
sourceIp:
|
|
91
|
+
<string>this.request.headers?.["x-forwarded-for"] ||
|
|
92
|
+
this.request.socket.remoteAddress,
|
|
93
|
+
user: null,
|
|
94
|
+
userAgent: this.request.headers
|
|
95
|
+
? this.request.headers["user-agent"]
|
|
96
|
+
: null,
|
|
97
|
+
userArn: null,
|
|
98
|
+
},
|
|
99
|
+
path: this.request.path,
|
|
100
|
+
protocol: "HTTP/1.1",
|
|
101
|
+
requestId: `${cuid()}-${cuid()}`,
|
|
102
|
+
requestTime: new Date().toISOString(),
|
|
103
|
+
requestTimeEpoch: Date.now(),
|
|
104
|
+
resourceId: null,
|
|
105
|
+
resourcePath: Globals.Listener_HTTP_ProxyRoute,
|
|
106
|
+
stage: process.env.STAGE,
|
|
107
|
+
},
|
|
108
|
+
resource: Globals.Listener_HTTP_ProxyRoute,
|
|
109
|
+
stageVariables: null,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
private buildContext(
|
|
113
|
+
event: APIGatewayProxyEvent,
|
|
114
|
+
callback: (err?: Error | string, data?: any) => void,
|
|
115
|
+
): Context {
|
|
116
|
+
return {
|
|
117
|
+
awsRequestId: event.requestContext.requestId,
|
|
118
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
119
|
+
getRemainingTimeInMillis: () => {
|
|
120
|
+
return 0
|
|
121
|
+
},
|
|
122
|
+
done: (err, data) => callback(err, data),
|
|
123
|
+
fail: (err) => callback(err),
|
|
124
|
+
succeed: (res) => callback(null, res),
|
|
125
|
+
functionName: null,
|
|
126
|
+
functionVersion: "LATEST",
|
|
127
|
+
invokedFunctionArn: null,
|
|
128
|
+
memoryLimitInMB: "-1",
|
|
129
|
+
logGroupName: null,
|
|
130
|
+
logStreamName: null,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Server as HTTPServer, createServer } from "http"
|
|
2
|
+
|
|
3
|
+
import cors from "cors"
|
|
4
|
+
import * as express from "express"
|
|
5
|
+
|
|
6
|
+
import Server from "./../Server"
|
|
7
|
+
import GenericHandler from "./GenericHandler"
|
|
8
|
+
import HealthHandler from "./HealthHandler"
|
|
9
|
+
import { version as appVersion } from "../../../../package.json"
|
|
10
|
+
import Globals from "../../../Globals"
|
|
11
|
+
import { RouterConfig } from "../../Router"
|
|
12
|
+
|
|
13
|
+
export default class Proxy {
|
|
14
|
+
private stopping: boolean
|
|
15
|
+
private readonly config: RouterConfig
|
|
16
|
+
private readonly app: express.Express
|
|
17
|
+
private readonly serverlessHandler: Server["handleServerlessEvent"]
|
|
18
|
+
private listener: HTTPServer
|
|
19
|
+
constructor(
|
|
20
|
+
config: RouterConfig,
|
|
21
|
+
serverlessHandler: Server["handleServerlessEvent"],
|
|
22
|
+
) {
|
|
23
|
+
this.stopping = false
|
|
24
|
+
this.config = config
|
|
25
|
+
this.serverlessHandler = serverlessHandler
|
|
26
|
+
this.app = express()
|
|
27
|
+
this.app.use(
|
|
28
|
+
cors(
|
|
29
|
+
this.config.cors
|
|
30
|
+
? {
|
|
31
|
+
options: {
|
|
32
|
+
origin: this.config?.cors?.origin,
|
|
33
|
+
allowedHeaders: this.config?.cors?.headers,
|
|
34
|
+
credentials: !!this.config?.cors?.allowCredentials,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
: {},
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// //This supposedly fix some 502 codes where nodejs socket would hang during
|
|
42
|
+
// //a request and if behind ALB, it would cause 502 codes. Had experiencied this
|
|
43
|
+
// //and 502 codes reduced dramastically, but still some appearances. Maybe this
|
|
44
|
+
// //is just a palliative work-around for the real issue; TODO: need to investigate
|
|
45
|
+
// //in the future.
|
|
46
|
+
// this.listener.listener.keepAliveTimeout = 120e3
|
|
47
|
+
// this.listener.listener.headersTimeout = 120e3
|
|
48
|
+
}
|
|
49
|
+
async load() {
|
|
50
|
+
await this.startListeners()
|
|
51
|
+
this.installRoutes()
|
|
52
|
+
}
|
|
53
|
+
async unload(err) {
|
|
54
|
+
await this.stopListeners(err)
|
|
55
|
+
}
|
|
56
|
+
/* lifecycle */
|
|
57
|
+
private async startListeners(): Promise<void> {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const port = this.config.port || Globals.Listener_HTTP_DefaultPort
|
|
60
|
+
console.log(`[Proxy] - [STARTING] - v.${appVersion} - :${port}`)
|
|
61
|
+
// Create Server
|
|
62
|
+
this.listener = createServer(this.app)
|
|
63
|
+
// Set defaults
|
|
64
|
+
this.listener.setTimeout(
|
|
65
|
+
this.config.timeout || Globals.Listener_HTTP_DefaultTimeout,
|
|
66
|
+
)
|
|
67
|
+
// Start Server
|
|
68
|
+
this.listener.listen(port, () => {
|
|
69
|
+
console.log(`[Proxy] - [STARTED]`)
|
|
70
|
+
resolve()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
private async stopListeners(err) {
|
|
75
|
+
if (this.stopping) return
|
|
76
|
+
this.stopping = true
|
|
77
|
+
console.debug("[Proxy] - [STOPPING]")
|
|
78
|
+
this.listener.close((_err) => {
|
|
79
|
+
if (err || _err) console.log("[Proxy] - exit output:", err || _err)
|
|
80
|
+
console.log("[Proxy] - [STOPPED]")
|
|
81
|
+
process.exit(err || _err ? 1 : 0)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
/* routing */
|
|
85
|
+
private installRoutes() {
|
|
86
|
+
//Health check route -- This is a bypass route to only check if
|
|
87
|
+
//runtime proxy is working and responding to calls.
|
|
88
|
+
console.log(
|
|
89
|
+
`[Proxy] - [HEALTH-ROUTE] - ${
|
|
90
|
+
this.config.healthCheckRoute ||
|
|
91
|
+
Globals.Listener_HTTP_DefaultHealthCheckRoute
|
|
92
|
+
}`,
|
|
93
|
+
)
|
|
94
|
+
this.app
|
|
95
|
+
.route(
|
|
96
|
+
this.config.healthCheckRoute ||
|
|
97
|
+
Globals.Listener_HTTP_DefaultHealthCheckRoute,
|
|
98
|
+
)
|
|
99
|
+
.get(HealthHandler)
|
|
100
|
+
//Main route -- We use a wildcard route because is not the job of the runtime and neither
|
|
101
|
+
//the task to deny/constrain routes that invoked this task; all the job is done by the
|
|
102
|
+
//load balancer and we just foward everything we have to the function.
|
|
103
|
+
this.app
|
|
104
|
+
.route(Globals.Listener_HTTP_ProxyRoute)
|
|
105
|
+
.all(GenericHandler(this.serverlessHandler))
|
|
106
|
+
}
|
|
107
|
+
}
|