aws-classify-server 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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -0
  3. package/lib/cjs/ClassDef.d.ts +6 -0
  4. package/lib/cjs/ClassDef.js +2 -0
  5. package/lib/cjs/ClassifyResponse.d.ts +6 -0
  6. package/lib/cjs/ClassifyResponse.js +11 -0
  7. package/lib/cjs/ClassifyServerless.d.ts +27 -0
  8. package/lib/cjs/ClassifyServerless.js +288 -0
  9. package/lib/cjs/SessionData.d.ts +6 -0
  10. package/lib/cjs/SessionData.js +2 -0
  11. package/lib/cjs/aws-classify-common/EndPointsLogging.d.ts +8 -0
  12. package/lib/cjs/aws-classify-common/EndPointsLogging.js +11 -0
  13. package/lib/cjs/aws-classify-common/LambdaRequest.d.ts +6 -0
  14. package/lib/cjs/aws-classify-common/LambdaRequest.js +2 -0
  15. package/lib/cjs/aws-classify-common/LambdaResponse.d.ts +6 -0
  16. package/lib/cjs/aws-classify-common/LambdaResponse.js +2 -0
  17. package/lib/cjs/aws-classify-common/index.d.ts +4 -0
  18. package/lib/cjs/aws-classify-common/index.js +7 -0
  19. package/lib/cjs/dispatch.d.ts +7 -0
  20. package/lib/cjs/dispatch.js +79 -0
  21. package/lib/cjs/index.d.ts +3 -0
  22. package/lib/cjs/index.js +12 -0
  23. package/lib/esm/ClassDef.d.ts +6 -0
  24. package/lib/esm/ClassDef.js +1 -0
  25. package/lib/esm/ClassifyResponse.d.ts +6 -0
  26. package/lib/esm/ClassifyResponse.js +7 -0
  27. package/lib/esm/ClassifyServerless.d.ts +27 -0
  28. package/lib/esm/ClassifyServerless.js +282 -0
  29. package/lib/esm/SessionData.d.ts +6 -0
  30. package/lib/esm/SessionData.js +1 -0
  31. package/lib/esm/aws-classify-common/EndPointsLogging.d.ts +8 -0
  32. package/lib/esm/aws-classify-common/EndPointsLogging.js +8 -0
  33. package/lib/esm/aws-classify-common/LambdaRequest.d.ts +6 -0
  34. package/lib/esm/aws-classify-common/LambdaRequest.js +1 -0
  35. package/lib/esm/aws-classify-common/LambdaResponse.d.ts +6 -0
  36. package/lib/esm/aws-classify-common/LambdaResponse.js +1 -0
  37. package/lib/esm/aws-classify-common/index.d.ts +4 -0
  38. package/lib/esm/aws-classify-common/index.js +2 -0
  39. package/lib/esm/dispatch.d.ts +7 -0
  40. package/lib/esm/dispatch.js +73 -0
  41. package/lib/esm/index.d.ts +3 -0
  42. package/lib/esm/index.js +3 -0
  43. package/package.json +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sam Elsamman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,306 @@
1
+ # aws-classify
2
+
3
+ A library for calling AWS lambda functions from a browser or react-native app where the lambda functions are implemented as Typescript class members. You create a request and corresponding response class. When you call the request class member function aws-classify takes care of the magic of invoking the corresponding response class member as a Lambda function.
4
+
5
+ The reverse is also true in that class members implemented in the browser can be called from within a Lambda server. The latter uses Web Sockets in the AWS gateway. aws-classify also provides for a static website from which everything can be executed to comply with same-origin policy.
6
+
7
+ * Complex data with classes and cyclic structures can be passed and returned
8
+ * Execptions are passed back to the caller of the request method
9
+ * back-end session data is simply a matter of defining fields in the response class
10
+ * Configuration and deployment via a simple serverless.yml file
11
+
12
+ ### AWS Resources Configured and Deployed
13
+ These AWS resources are automatically configured and deployment is fully automated:
14
+ * AWS Lambda
15
+ * Web Sockets in the AWS Gateway
16
+ * Dynamo DB for managing sessions
17
+ * S3 for a static website
18
+ * Cloudfront with a custom domain name as a CDN
19
+
20
+ All of these resources are configured by the Serverless Framework and deployed by running a script. You only need to login to AWS in order toc reate credentials for the Serverless Framework and to register your domain name and create an SSL certificate.
21
+
22
+ ### Use Cases
23
+ aws-classify harnesses AWS to make it easy to leverage a scalable cloud-based infrastructure with virtually no learning curve. From there you can leverage all of the other AWS resources your application may need. The primary use-case is startups and projects that want a complete "app in a box" solution.
24
+
25
+ ## Installation
26
+
27
+ On the project that implements the lambda functions
28
+
29
+ ```npm install aws-classify-common, aws-classify-server```
30
+
31
+
32
+ On the client project
33
+
34
+
35
+ ```npm install aws-classify-common, aws-classify-client```
36
+
37
+ The framework has been tested with popular client software such as Expo for mobile and React Native for web.
38
+
39
+ ## Calling Lambda Functions from the Client
40
+
41
+ ### Create a Request/Response Pair of Classes
42
+
43
+ Create a request class that you will instantiate on the client:
44
+
45
+ ```typescript
46
+ export class ServerRequest {
47
+ static interfaceName = 'ServerRequest';
48
+ async setCount (count : number) { reqBody() }
49
+ async getCount () : Promise<number> { return reqBody() }
50
+ }
51
+ ```
52
+ Create a corresponding response class that extends the request class:
53
+ ```typescript
54
+ export class ServerResponse extends ServerRequest {
55
+ count = 0;
56
+ async setCount(count: number) {this.count = count}
57
+ async getCount(): Promise<number> {return this.count}
58
+ }
59
+ ```
60
+ Once these classes are registered and instantiated using aws-classify you simply call the request method and the response method is executed as a Lambda function. Any fields (e.g. count) in the response class are saved and restored between calls and serve as session data. Request classes must have a unique static interface name field so they are uniquely identified.
61
+
62
+ ### Using the Request Class
63
+
64
+ You instantiate the request class and invoke its methods like this on the client:
65
+ ```typescript
66
+ const classifyClient = new ClassifyClient(getSession, saveSession);
67
+ serverRequest = classifyClient.createRequest(ServerRequest);
68
+ await serverRequest.setCount(2023);
69
+ ```
70
+ Since aws-classify is client platform-agnostic you need to implement getSession and saveSession for persisting client session data. In the browser it would look like this:
71
+ ```typescript
72
+ async function getSession () {
73
+ return localStorage.getItem('ClassifySession') || "None"
74
+ }
75
+ async function setSession(sessionId : string) {
76
+ localStorage.setItem('ClassifySession', sessionId);
77
+ }
78
+ ```
79
+ If the implementation on the response class throws an Error, the Error will be thrown when you call the request class so it is best to enclose the call in a try/catch:
80
+ ```typescript
81
+ try {
82
+ await serverRequest.setCount(2023);
83
+ } catch (e) { console.log(e) }
84
+ ```
85
+ ### Using the Response class
86
+ You must register the class so it can be instantiated when the client calls. Usually this is done in the file where the class is defined:
87
+ ```typescript
88
+ classifyServerless.registerRespon(ServerResponse)
89
+ ```
90
+ Because class fields are persisted between calls from the same session, you need to declare the response class as serializable so that js-freezedry can serialize it. Usually this is done in the file where the class is defined:
91
+ ```
92
+ serializable(ServerResponse)
93
+ ```
94
+ ## Calling Client Functions from Lambda Functions
95
+ Calling member functions implemented on the client use WebSockets. There are three distinct use cases:
96
+ * ***Same Session*** - Call a client-request for the same session as the caller of your Lambda function.
97
+ * ***Alternate Session*** - Instantiate a response class for a different session and then call a client request for that session. This allows means the response class for the alternate session can change it's session session data
98
+ * ***Direct to Alternate Session*** A shortcut whereby the Lambda fuction directly invokes a request method for an alternate session.
99
+
100
+ ### Setup Requests and Response Classes
101
+ Your Lambda functions can also call requests that will be implemented in the browser. The class setup is analogous in that you define a request class
102
+ ```typescript
103
+ export class ClientRequest {
104
+ static interfaceName = 'ClientRequest';
105
+ setCount(count : number) {reqBody()}
106
+ }
107
+ ```
108
+ and a response class
109
+ ```typescript
110
+ export class ClientResponse extends ClientRequest{
111
+ count = 0;
112
+ setCount(count: number) {
113
+ this.count = 0;
114
+ console.log(count);
115
+ }
116
+ }
117
+ ```
118
+ On the client you need to instantiate the response class like this:
119
+ ```typescript
120
+ const classifyClient = new ClassifyClient(getSession, saveSession);
121
+ const clientResponse = classifyClient.createResponse(ClientResponse);
122
+ ```
123
+
124
+ ### Implement Lambda Response methods
125
+ In all cases you must register the request class.
126
+ ```typescript
127
+ classifyServerless.registerRequest(ClientRequest);
128
+ ```
129
+ For the ***Same Session*** case you instantiate the request object and call the method. The first parameter of **createRequest** is a response object from which the session is to be extracted:
130
+ ```typescript
131
+ export class ServerResponse extends ServerRequest {
132
+ count = 0;
133
+ // ....
134
+ async sendCount() {
135
+ const clientRequest = classifyServerless.createRequest(this, ClientRequest);
136
+ clientRequest.setCount(this.count);
137
+ }
138
+ }
139
+ ```
140
+ For the ***Alternate Session*** case you first instantiate a response object for the target session and call a method that will invoke a request for the target session:
141
+ ```typescript
142
+ export class ServerResponse extends ServerRequest {
143
+ count = 0;
144
+ // ...
145
+ async sendCountTo(sessionId: string) {
146
+ await classifyServerless.createResponse(ServerResponse, sessionId, async serverResponse => {
147
+ await serverResponse.sendCount()
148
+ });
149
+ }
150
+ async sendCount() {
151
+ const clientRequest = classifyServerless.createRequest(this, ClientRequest);
152
+ clientRequest.setCount(this.count);
153
+ }
154
+
155
+ }
156
+ ```
157
+
158
+ And for the ***Direct to Alternate Session*** case you may directly instantiate a request object for the target session and invoke its method
159
+ ```typescript
160
+ export class ServerResponse extends ServerRequest {
161
+ count = 0;
162
+ // ...
163
+ async sendOurCountTo(sessionId: string) {
164
+ const clientRequest = await classifyServerless.createRequestForSession(sessionId, ClientRequest);
165
+ await clientRequest.setCount(this.count);
166
+ }
167
+ }
168
+ ```
169
+ The combination of these use cases allows for a rich set of interactions between code in the cloud and code on the server. For example:
170
+ * You have two sessions "chat" with other by sharing the session Id
171
+ * You can instantiate and invoke your class methods from any normal Lambda function provided that you have a session. This includes any of the AWS event mechanisms capable of invoking Lamda functions
172
+ ## Project Structure
173
+ The mono repo structure is ideal for projects that use aws-classify as you typically will have a server sub-project and one or more client projects, perhaps mobile and web. These projects will share code as the request classes must be present in both the client and the server. The easiest way to accomplish this is using bisync which synchronizes your shared directories automatically.
174
+
175
+ ```
176
+ Root
177
+ - bisync.json (configured to synchronized shared directories)
178
+ - package.json (just for bisync)
179
+ - node_modules (just bisync)
180
+ - cloud
181
+ - serverless.yml
182
+ - server-requests (contains request classes implemented on the server)
183
+ - server-responses (contains implementations on server)
184
+ - client-requests (contains request classes implented on the client)
185
+ - web (could be a project created with create-react-app)
186
+ - server-requests (contains request classes implemented on the server)
187
+ - client-requests (contains request classes implented on the client)
188
+ - client-responses (contains implementation on client)
189
+ - other files pertaining to the client and static website
190
+ ```
191
+ You would then configure bisync.json to keep the shared code in sync
192
+ ```
193
+ [
194
+ ["./cloud/client-requests", "./web/client-requests"],
195
+ ["./clout/server-requests", "./web/server-requests"]
196
+ ]
197
+ ```
198
+ Implementing shared code this way avoid restrictions that packaging schemes have that effectively require code to be present in project tree
199
+ ## Deployment
200
+ This serverless.yml file is all you need to configure AWS:
201
+ ```
202
+ service: 'aws-classify-tests'
203
+
204
+ custom:
205
+
206
+ #
207
+ # You need to fill these out
208
+ #
209
+
210
+ # Specify this even if you don't have a custom domain at this time
211
+ domain: awsclassify.com
212
+ # Get your AWS SSL Certificate and put it here if you have a custom domain
213
+ certificateARN: "arn:aws:acm:us-east-1:############:certificate/########-####-####-####-############"
214
+
215
+ # Map stages to domain prefixes
216
+ prefixes:
217
+ dev: test.
218
+ prod: ''
219
+
220
+ directories:
221
+ responseHandlers: "src/server-responses/index"
222
+ staticWebsite: "./static"
223
+ includeFiles: "node_modules/aws-classify/yml"
224
+
225
+ #Include any scripts needed to build your static site
226
+ #scripts:
227
+ #hooks:
228
+ #'before:deploy:deploy': npm run build:static
229
+
230
+ #
231
+ # Boiler plate
232
+ #
233
+ yml: "../../yml/"
234
+ #yml ${self:custom.directories.includeFiles}
235
+ stage: ${opt:stage,'dev'}
236
+ hostedZoneName: ${self:custom.domain}.
237
+ domainName: ${self:custom.prefixes.${self:custom.stage}}{$self:custome.domain}
238
+ handler: "src/server-responses/index"
239
+ bucketName: ${self:custom.domainName}
240
+ s3Sync:
241
+ noSync: true
242
+ buckets:
243
+ - bucketName: ${self:custom.bucketName}
244
+ localDir: ${self:custom.directories.includeFiles}
245
+ cloudfrontInvalidate: ${file(${self:custom.yml}/custom-cloudfront.yml)}
246
+ serverless-offline:
247
+ httpPort: 4000
248
+ dynamodb: ${file(${self:custom.yml}/custom-dynamodb.yml)}
249
+
250
+
251
+ plugins:
252
+ - serverless-plugin-scripts
253
+ - serverless-s3-sync
254
+ - serverless-cloudfront-invalidate
255
+ - serverless-plugin-typescript
256
+ - serverless-dynamodb-local
257
+ - serverless-offline
258
+
259
+ provider:
260
+ name: aws
261
+ runtime: nodejs16.x
262
+ region: us-east-1
263
+ websocketsApiRouteSelectionExpression: $request.body.action
264
+ logs:
265
+ websocket: true
266
+ environment:
267
+ APIG_ENDPOINT: ${file(${self:custom.yml}/apig.yml)}
268
+ iam:
269
+ role:
270
+ - statements:
271
+ - ${file(${self:custom.yml}/provider-iam.yml)}
272
+
273
+ functions:
274
+ - ${file(${self:custom.yml}/functions.yml)}
275
+
276
+ resources:
277
+ - ${file(${self:custom.yml}/resources-website.yml)}
278
+ # {file(${self:custom.yml}/resources-custom-website.yml)} if you have a custom domain
279
+
280
+ ```
281
+
282
+ To deploy you need to:
283
+ - Create AWS credentials using the AWS console as defined in the Serverless [documents](https://www.serverless.com/framework/docs/providers/aws/guide/credentials/documentation)
284
+ - Install and setup credentials You need to setup your credentials with serverless
285
+ ```
286
+ serverless config credentials \
287
+ --provider aws \
288
+ --key AKIAIOSFODNN7EXAMPLE \
289
+ --secret wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
290
+ ```
291
+ - Obtain a no-cost SSL certificate on the AWS console and put it in the serverless.yml file
292
+ - You can deploy a test version with
293
+ ```
294
+ serverless deploy stage dev
295
+ ```
296
+ and a production version with
297
+ ```
298
+ serverless deploy stage prod
299
+ ```
300
+
301
+ ## Roadmap
302
+ * Create some sample applications
303
+ * Continued shakeout with real-world applications
304
+ * Community feedback
305
+
306
+ At present the library should rightly be considered "beta" until it is more widely adopted. It is ideal for side projects and proof of concepts. It is also a useful reference on how to configure AWS services.
@@ -0,0 +1,6 @@
1
+ import { ClassifyResponse } from "./ClassifyResponse";
2
+ export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
3
+ serverClass: new () => ServerClass;
4
+ clientClass: new () => ClientClass;
5
+ authorizer: ((endPoint: ServerClass, method: string, args: IArguments) => Promise<boolean>) | undefined;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ export declare class ClassifyResponse {
2
+ __sessionId__: string | undefined;
3
+ __connectionId__: string | undefined;
4
+ __interfaceName__: string | undefined;
5
+ constructor(interfaceName: string, sessionId: string, connectionId: string);
6
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClassifyResponse = void 0;
4
+ class ClassifyResponse {
5
+ constructor(interfaceName, sessionId, connectionId) {
6
+ this.__connectionId__ = connectionId;
7
+ this.__sessionId__ = sessionId;
8
+ this.__interfaceName__ = interfaceName;
9
+ }
10
+ }
11
+ exports.ClassifyResponse = ClassifyResponse;
@@ -0,0 +1,27 @@
1
+ import { ClassDef } from "./ClassDef";
2
+ import { APIGatewayProxyEvent, Context } from "aws-lambda";
3
+ export declare class ClassifyServerless {
4
+ logLevel: Partial<typeof EndPointsLogging>;
5
+ classDefs: Map<any, ClassDef<any, any>>;
6
+ requests: Map<any, any>;
7
+ log: (_message: string) => void;
8
+ setLogger(log: (_message: string) => void): void;
9
+ setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
10
+ registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments) => Promise<boolean>): void;
11
+ dispatch(ev: APIGatewayProxyEvent, context: Context, classes?: {}): Promise<string>;
12
+ registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
13
+ getSessionId<T>(responseObj: T): any;
14
+ createRequest<T, U>(responseObj: U, requestClass: new () => T): T;
15
+ createRequestForSession<T>(sessionId: string, requestClass: new () => T): Promise<T>;
16
+ createResponse<T, R>(responseClass: new () => T, sessionId: string, callback: (_obj: T) => Promise<R>, classes?: any): Promise<R>;
17
+ }
18
+ export declare function getSessionData(sessionId: string, interfaceName?: string): Promise<Record<string, any> | undefined>;
19
+ export declare function saveSessionData(sessionId: string, interfaceName?: string, sessionData?: string, connectionId?: string): Promise<string>;
20
+ export declare const EndPointsLogging: {
21
+ create: boolean;
22
+ connect: boolean;
23
+ exceptions: boolean;
24
+ calls: boolean;
25
+ requests: boolean;
26
+ data: boolean;
27
+ };
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EndPointsLogging = exports.saveSessionData = exports.getSessionData = exports.ClassifyServerless = void 0;
13
+ const js_freeze_dry_1 = require("js-freeze-dry");
14
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
15
+ const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
16
+ const client_apigatewaymanagementapi_1 = require("@aws-sdk/client-apigatewaymanagementapi");
17
+ const ddbClient = new client_dynamodb_1.DynamoDBClient({ region: process.env.DD_REGION, endpoint: process.env.DD_ENDPOINT });
18
+ const ddbDocClient = lib_dynamodb_1.DynamoDBDocument.from(ddbClient);
19
+ class ClassifyServerless {
20
+ constructor() {
21
+ this.logLevel = {};
22
+ this.classDefs = new Map();
23
+ this.requests = new Map();
24
+ this.log = msg => console.log(msg);
25
+ }
26
+ setLogger(log) {
27
+ this.log = log;
28
+ }
29
+ setLogLevel(logLevel) {
30
+ this.logLevel = logLevel;
31
+ }
32
+ registerResponse(responseClass, authorizer) {
33
+ // Find ultimate base class
34
+ let clientClass = responseClass;
35
+ while (Object.getPrototypeOf(clientClass).prototype)
36
+ clientClass = Object.getPrototypeOf(clientClass);
37
+ if (clientClass === responseClass)
38
+ throw ('ClassifyServerless.registerResponse: Response class must extend a request class');
39
+ // Get interface name as static property of request class
40
+ const interfaceName = clientClass['interfaceName'];
41
+ if (!interfaceName)
42
+ throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
43
+ this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer });
44
+ responseClass.__interfaceName__ = interfaceName;
45
+ }
46
+ dispatch(ev, context, classes = {}) {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ const request = (0, js_freeze_dry_1.deserialize)(ev.body, classes);
49
+ const logName = `${request.interfaceName}.${request.methodName}`;
50
+ let sessionId = request.sessionId; // May be empty
51
+ // Pseudo interface/method to request socket access and return a session id to use in connect
52
+ // session id will be passed in sec-websocket-protocol on connect request
53
+ if (logName === '$WebSocket.$authorize') {
54
+ const socketResult = yield getSessionData(sessionId, "");
55
+ if (!socketResult) {
56
+ sessionId = context.awsRequestId;
57
+ yield saveSessionData(sessionId, undefined, undefined, undefined);
58
+ }
59
+ const lambdaResponse = {
60
+ data: process.env.IS_OFFLINE ? 'ws://localhost:3001' : `wss://${process.env.APIG_ENDPOINT}`,
61
+ exception: undefined,
62
+ cargo: undefined,
63
+ sessionId: sessionId,
64
+ };
65
+ return (0, js_freeze_dry_1.serialize)(lambdaResponse, classes);
66
+ }
67
+ if (this.logLevel.data && this.logLevel.calls)
68
+ this.log(`Lambda ${logName} invoked with ${ev.body}`);
69
+ else if (this.logLevel.calls)
70
+ this.log(`Lambda ${logName} invoked`);
71
+ const classDef = this.classDefs.get(request.interfaceName);
72
+ if (!classDef)
73
+ throw new Error(`No Response defined for ${request.interfaceName}`);
74
+ if (!classDef.clientClass.prototype[request.methodName])
75
+ throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
76
+ // If authorizer passed in make sure request is authorized
77
+ if (!classDef.authorizer || (yield classDef.authorizer(request.interfaceName, request.methodName, request.args))) {
78
+ // Instantiate the class
79
+ const obj = new classDef.serverClass();
80
+ // Retrieve session data from DynamoDB base on sessionId in request
81
+ const result = yield getSessionData(sessionId, request.interfaceName);
82
+ if (!result)
83
+ sessionId = context.awsRequestId;
84
+ else {
85
+ const sessionData = result[`interface_${request.interfaceName}`];
86
+ if (sessionData) // Enrich it with session data
87
+ Object.assign(obj, (0, js_freeze_dry_1.deserialize)(sessionData, classes));
88
+ }
89
+ obj.__sessionId__ = sessionId;
90
+ obj.__connectionId__ = result === null || result === void 0 ? void 0 : result.connectionId;
91
+ // Call the class method and catch any exceptions it might throw to pass back
92
+ let fnResponse;
93
+ try {
94
+ fnResponse = yield obj[request.methodName].call(obj, ...request.args);
95
+ }
96
+ catch (e) {
97
+ if (this.logLevel.calls)
98
+ this.log(`Lambda ${logName} had exception ${e}`);
99
+ const lambdaResponse = {
100
+ data: undefined,
101
+ exception: e.message,
102
+ cargo: undefined,
103
+ sessionId: sessionId,
104
+ };
105
+ return (0, js_freeze_dry_1.serialize)(lambdaResponse, classes);
106
+ }
107
+ const updatedSessionData = (0, js_freeze_dry_1.serialize)(obj, classes);
108
+ if (!result || updatedSessionData !== result.sessionData)
109
+ sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined);
110
+ // Formulate response
111
+ const lambdaResponse = {
112
+ data: fnResponse,
113
+ exception: undefined,
114
+ cargo: undefined,
115
+ sessionId,
116
+ };
117
+ const response = (0, js_freeze_dry_1.serialize)(lambdaResponse, classes);
118
+ if (this.logLevel.data && this.logLevel.calls)
119
+ this.log(`Lambda ${logName} responded with ${response}`);
120
+ else if (this.logLevel.calls)
121
+ this.log(`Lambda ${logName} responded`);
122
+ return response;
123
+ }
124
+ else {
125
+ this.log(`Lambda ${logName} not authorized`);
126
+ throw new Error("Not Authorized");
127
+ }
128
+ });
129
+ }
130
+ registerRequest(requestClass, classes = {}) {
131
+ const interfaceName = requestClass['interfaceName'];
132
+ if (!interfaceName)
133
+ throw ('ClassifyServerless.registerRequest: Request must have interfaceName as a static property');
134
+ for (const methodName of Object.getOwnPropertyNames(requestClass.prototype)) {
135
+ if (methodName === 'constructor' || typeof (requestClass.prototype)[methodName] !== 'function')
136
+ continue;
137
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
138
+ const expressServer = this;
139
+ requestClass.prototype[methodName] = function (...args) {
140
+ return __awaiter(this, void 0, void 0, function* () {
141
+ try {
142
+ // Build request
143
+ const sessionId = this['__sessionId__'];
144
+ const connectionId = this['__connectionId__'];
145
+ if (!sessionId)
146
+ throw new Error(`You must instantiate ${interfaceName} using the ClassifyServerless.createRequest`);
147
+ if (!connectionId)
148
+ throw new Error(`WebSocket handshake not established for ${interfaceName}`);
149
+ const request = {
150
+ interfaceName: interfaceName,
151
+ args, methodName,
152
+ sessionId
153
+ };
154
+ const payload = (0, js_freeze_dry_1.serialize)(request, classes);
155
+ // Log request
156
+ if (expressServer.logLevel.data && expressServer.logLevel.requests)
157
+ expressServer.log(`Request ${interfaceName} emitting with ${payload}`);
158
+ else if (expressServer.logLevel && expressServer.logLevel.requests)
159
+ expressServer.log(`Request ${interfaceName} emitting`);
160
+ // Post request to the gateway to be sent to src
161
+ const endpoint = process.env.IS_OFFLINE ? 'http://localhost:3001' : `https://${process.env.APIG_ENDPOINT}`;
162
+ if (expressServer.logLevel.calls)
163
+ expressServer.log(`creating APIGatewayManagementApiClient for endpoint ${endpoint}`);
164
+ const client = new client_apigatewaymanagementapi_1.ApiGatewayManagementApiClient({ endpoint });
165
+ if (expressServer.logLevel.data)
166
+ expressServer.log(`posting to ${connectionId} ${payload}`);
167
+ const post = new client_apigatewaymanagementapi_1.PostToConnectionCommand({
168
+ ConnectionId: connectionId,
169
+ Data: Buffer.from(payload)
170
+ });
171
+ yield client.send(post);
172
+ // Catch any exception, so it can be logged and then rethrown
173
+ }
174
+ catch (e) {
175
+ if (expressServer.logLevel.exceptions)
176
+ expressServer.log(e.message);
177
+ throw e;
178
+ }
179
+ });
180
+ };
181
+ if (this.logLevel.create)
182
+ this.log(`${interfaceName} bound to ${requestClass.name}.${methodName}`);
183
+ }
184
+ return requestClass;
185
+ }
186
+ getSessionId(responseObj) {
187
+ const sessionId = responseObj['__sessionId__'];
188
+ if (!sessionId)
189
+ throw Error('ClassifyServerless.getSessionId: no session id');
190
+ return sessionId;
191
+ }
192
+ createRequest(responseObj, requestClass) {
193
+ const obj = new requestClass();
194
+ obj['__sessionId__'] = responseObj['__sessionId__'];
195
+ obj['__connectionId__'] = responseObj['__connectionId__'];
196
+ return obj;
197
+ }
198
+ createRequestForSession(sessionId, requestClass) {
199
+ return __awaiter(this, void 0, void 0, function* () {
200
+ // Get connection Id
201
+ const result = yield getSessionData(sessionId);
202
+ if (!result)
203
+ throw new Error(`ClassifyResponse.createRequestForSession: invalid session id`);
204
+ if (!result.connectionId)
205
+ throw new Error(`WebSocket handshake not established for session`);
206
+ // Create request
207
+ const obj = new requestClass();
208
+ obj['__sessionId__'] = sessionId;
209
+ obj['__connectionId__'] = result.connectionId;
210
+ return obj;
211
+ });
212
+ }
213
+ createResponse(responseClass, sessionId, callback, classes = {}) {
214
+ return __awaiter(this, void 0, void 0, function* () {
215
+ const interfaceName = responseClass.__interfaceName__;
216
+ if (!interfaceName)
217
+ throw new Error(`ClassifyResponse.createResponse: Failed to call registerResponse for ${responseClass.name}`);
218
+ const result = yield getSessionData(sessionId, interfaceName);
219
+ if (!result)
220
+ throw new Error(`ClassifyResponse.createResponse: invalid session id for interface ${interfaceName}`);
221
+ const sessionData = result[`interface_${interfaceName}`];
222
+ // Instantiate the class
223
+ const obj = (new responseClass());
224
+ // Enrich it with session data
225
+ if (sessionData)
226
+ Object.assign(obj, (0, js_freeze_dry_1.deserialize)(sessionData, classes));
227
+ obj.__sessionId__ = sessionId;
228
+ obj.__connectionId__ = result.connectionId;
229
+ if (this.logLevel.calls)
230
+ this.log(`creating response for ${interfaceName} with sessionId=${sessionId} connectionId= ${obj.__connectionId__} sessionData=${sessionData}`);
231
+ const ret = yield callback(obj);
232
+ const updatedSessionData = (0, js_freeze_dry_1.serialize)(obj, classes);
233
+ if (updatedSessionData !== result.sessionData) {
234
+ if (this.logLevel.calls)
235
+ this.log(`saving session data for ${interfaceName} with sessionId=${sessionId} connectionId= ${obj.__connectionId__} sessionData=${updatedSessionData}`);
236
+ yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined);
237
+ }
238
+ return ret;
239
+ });
240
+ }
241
+ }
242
+ exports.ClassifyServerless = ClassifyServerless;
243
+ function getSessionData(sessionId, interfaceName = "") {
244
+ return __awaiter(this, void 0, void 0, function* () {
245
+ if (sessionId) {
246
+ const data = yield ddbDocClient.get({
247
+ TableName: 'classifySessionStore',
248
+ Key: { sessionId },
249
+ ConsistentRead: true,
250
+ ProjectionExpression: `interface_${interfaceName}, connectionId, updated`
251
+ });
252
+ return data.Item;
253
+ }
254
+ return undefined;
255
+ });
256
+ }
257
+ exports.getSessionData = getSessionData;
258
+ function saveSessionData(sessionId, interfaceName, sessionData, connectionId) {
259
+ return __awaiter(this, void 0, void 0, function* () {
260
+ const updateExpressionComponents = ['updated = :time'];
261
+ const expressionAttributeValues = { ':time': new Date().getTime() };
262
+ if (sessionData && interfaceName) {
263
+ updateExpressionComponents.push(`interface_${interfaceName} = :data`);
264
+ expressionAttributeValues[`:data`] = sessionData;
265
+ }
266
+ if (connectionId) {
267
+ updateExpressionComponents.push('connectionId = :connectionId');
268
+ expressionAttributeValues[':connectionId'] = connectionId;
269
+ }
270
+ // First try and out data only if the session id exists
271
+ yield ddbDocClient.update({
272
+ TableName: 'classifySessionStore',
273
+ Key: { sessionId },
274
+ UpdateExpression: `set ${updateExpressionComponents.join(", ")}`,
275
+ ExpressionAttributeValues: expressionAttributeValues
276
+ });
277
+ return sessionId;
278
+ });
279
+ }
280
+ exports.saveSessionData = saveSessionData;
281
+ exports.EndPointsLogging = {
282
+ create: true,
283
+ connect: true,
284
+ exceptions: true,
285
+ calls: true,
286
+ requests: false,
287
+ data: false,
288
+ };