somod-chat-service 0.0.1

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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/build/lib/cache.d.ts +3 -0
  4. package/build/lib/cache.js +72 -0
  5. package/build/lib/constants.d.ts +1 -0
  6. package/build/lib/constants.js +1 -0
  7. package/build/lib/index.d.ts +2 -0
  8. package/build/lib/index.js +2 -0
  9. package/build/lib/message.d.ts +2 -0
  10. package/build/lib/message.js +27 -0
  11. package/build/lib/threadCache.d.ts +4 -0
  12. package/build/lib/threadCache.js +31 -0
  13. package/build/lib/types.d.ts +18 -0
  14. package/build/lib/types.js +1 -0
  15. package/build/parameters.json +1 -0
  16. package/build/serverless/functions/message.d.ts +2 -0
  17. package/build/serverless/functions/message.http.json +1 -0
  18. package/build/serverless/functions/message.js +93 -0
  19. package/build/serverless/functions/messageStreamHandler.d.ts +3 -0
  20. package/build/serverless/functions/messageStreamHandler.js +79 -0
  21. package/build/serverless/functions/middlewares/userProvider.d.ts +7 -0
  22. package/build/serverless/functions/middlewares/userProvider.js +24 -0
  23. package/build/serverless/functions/thread.d.ts +2 -0
  24. package/build/serverless/functions/thread.http.json +1 -0
  25. package/build/serverless/functions/thread.js +57 -0
  26. package/build/serverless/functions/wsNotifyMessage.d.ts +3 -0
  27. package/build/serverless/functions/wsNotifyMessage.js +73 -0
  28. package/build/serverless/functions/wsOnConnect.d.ts +3 -0
  29. package/build/serverless/functions/wsOnConnect.js +23 -0
  30. package/build/serverless/functions/wsOnDisconnect.d.ts +3 -0
  31. package/build/serverless/functions/wsOnDisconnect.js +21 -0
  32. package/build/serverless/functions/wsOnMessage.d.ts +2 -0
  33. package/build/serverless/functions/wsOnMessage.js +49 -0
  34. package/build/serverless/functions/wsOnMessage.websocket.json +1 -0
  35. package/build/serverless/template.json +1 -0
  36. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 SOMOD
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,35 @@
1
+ # SOMOD Chat Service
2
+
3
+ ---
4
+
5
+ Text Chat Service developed using [SOMOD](https://somod.dev).
6
+
7
+ ## Install
8
+
9
+ Install as an npm package in somod modules
10
+
11
+ ```bash
12
+ npm install somod-chat-service
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Apis
18
+
19
+ > : TODO for doc:
20
+
21
+ refer template for available APIs
22
+
23
+ ### Extending
24
+
25
+ > : TODO for doc
26
+
27
+ ## Issues
28
+
29
+ The project issues, features, and milestones are maintained in this GitHub repo.
30
+
31
+ Create issues or feature requests at https://github.com/somod-dev/somod-chat-service/issues
32
+
33
+ ## Contributions
34
+
35
+ Please read our [CONTRIBUTING](https://github.com/somod-dev/somod/blob/main/CONTRIBUTING.md) guide before contributing to this project.
@@ -0,0 +1,3 @@
1
+ export declare const cache: (max: number, ttl?: number) => {
2
+ get: <T>(key: string, fetcher: () => Promise<T>, ttl?: number) => Promise<T>;
3
+ };
@@ -0,0 +1,72 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ export var cache = function (max, ttl) {
3
+ var cacheTtl = ttl;
4
+ var nodes = {};
5
+ var first = undefined;
6
+ var last = undefined;
7
+ var size = 0;
8
+ var remove = function (key) {
9
+ var _a, _b;
10
+ var node = nodes[key];
11
+ if (node.next) {
12
+ node.next.previous = node.previous;
13
+ }
14
+ else {
15
+ last = (_a = node.previous) === null || _a === void 0 ? void 0 : _a.content.key;
16
+ }
17
+ if (node.previous) {
18
+ node.previous.next = node.next;
19
+ }
20
+ else {
21
+ first = (_b = node.next) === null || _b === void 0 ? void 0 : _b.content.key;
22
+ }
23
+ size--;
24
+ delete nodes[key];
25
+ };
26
+ var add = function (key, value) {
27
+ var node = {
28
+ content: {
29
+ key: key,
30
+ cachedAt: Date.now(),
31
+ value: value
32
+ }
33
+ };
34
+ nodes[key] = node;
35
+ size++;
36
+ if (first) {
37
+ node.next = nodes[first];
38
+ nodes[first].previous = node;
39
+ }
40
+ first = key;
41
+ };
42
+ var get = function (key, fetcher, ttl) { return __awaiter(void 0, void 0, void 0, function () {
43
+ var node, value, now;
44
+ return __generator(this, function (_a) {
45
+ switch (_a.label) {
46
+ case 0:
47
+ node = nodes[key];
48
+ value = node === null || node === void 0 ? void 0 : node.content.value;
49
+ now = Date.now();
50
+ if (!(node === undefined ||
51
+ (ttl && node.content.cachedAt < now - ttl) ||
52
+ (cacheTtl && node.content.cachedAt < now - cacheTtl))) return [3 /*break*/, 2];
53
+ return [4 /*yield*/, fetcher()];
54
+ case 1:
55
+ value = _a.sent();
56
+ _a.label = 2;
57
+ case 2:
58
+ if (node) {
59
+ remove(node.content.key);
60
+ }
61
+ add(key, value);
62
+ if (size > max && last) {
63
+ remove(last);
64
+ }
65
+ return [2 /*return*/, value];
66
+ }
67
+ });
68
+ }); };
69
+ return {
70
+ get: get
71
+ };
72
+ };
@@ -0,0 +1 @@
1
+ export declare const UserProviderMiddlewareKey = "chat-backend-user-id";
@@ -0,0 +1 @@
1
+ export var UserProviderMiddlewareKey = "chat-backend-user-id";
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export { UserProviderMiddlewareKey } from "./constants";
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export { UserProviderMiddlewareKey } from "./constants";
@@ -0,0 +1,2 @@
1
+ import { Message } from "./types";
2
+ export declare const putMessage: (tableName: string, userId: string, message: Omit<Message, "seqNo">) => Promise<Message>;
@@ -0,0 +1,27 @@
1
+ import { __assign, __awaiter, __generator } from "tslib";
2
+ import { marshall } from "@aws-sdk/util-dynamodb";
3
+ import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
4
+ var dynamodb = new DynamoDBClient();
5
+ export var putMessage = function (tableName, userId, message) { return __awaiter(void 0, void 0, void 0, function () {
6
+ var msg, messageWithKeys, putItemCommand;
7
+ return __generator(this, function (_a) {
8
+ switch (_a.label) {
9
+ case 0:
10
+ msg = __assign(__assign({}, message), { seqNo: Date.now() });
11
+ messageWithKeys = __assign(__assign({}, msg), { userId: userId });
12
+ putItemCommand = new PutItemCommand({
13
+ TableName: tableName,
14
+ Item: marshall(messageWithKeys),
15
+ ConditionExpression: "attribute_not_exists(#userId) AND attribute_not_exists(#seqNo)",
16
+ ExpressionAttributeNames: {
17
+ "#userId": "userId",
18
+ "#seqNo": "seqNo"
19
+ }
20
+ });
21
+ return [4 /*yield*/, dynamodb.send(putItemCommand)];
22
+ case 1:
23
+ _a.sent();
24
+ return [2 /*return*/, msg];
25
+ }
26
+ });
27
+ }); };
@@ -0,0 +1,4 @@
1
+ import { Thread } from "./types";
2
+ export declare const threadCache: {
3
+ get: (threadId: string) => Promise<Thread>;
4
+ };
@@ -0,0 +1,31 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
3
+ import { cache } from "./cache";
4
+ import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
5
+ var dynamodb = new DynamoDBClient();
6
+ var _threadCache = cache(100, 60 * 60000);
7
+ export var threadCache = {
8
+ get: function (threadId) { return __awaiter(void 0, void 0, void 0, function () {
9
+ return __generator(this, function (_a) {
10
+ switch (_a.label) {
11
+ case 0: return [4 /*yield*/, _threadCache.get(threadId, function () { return __awaiter(void 0, void 0, void 0, function () {
12
+ var threadResult;
13
+ return __generator(this, function (_a) {
14
+ switch (_a.label) {
15
+ case 0: return [4 /*yield*/, dynamodb.send(new GetItemCommand({
16
+ TableName: process.env.THREAD_TABLE_NAME,
17
+ Key: marshall({ id: threadId })
18
+ }))];
19
+ case 1:
20
+ threadResult = _a.sent();
21
+ return [2 /*return*/, threadResult.Item
22
+ ? unmarshall(threadResult.Item)
23
+ : undefined];
24
+ }
25
+ });
26
+ }); })];
27
+ case 1: return [2 /*return*/, _a.sent()];
28
+ }
29
+ });
30
+ }); }
31
+ };
@@ -0,0 +1,18 @@
1
+ export type ThreadInput = {
2
+ participants: string[];
3
+ };
4
+ export type Thread = {
5
+ id: string;
6
+ } & ThreadInput;
7
+ export type MessageInput = {
8
+ threadId: string;
9
+ type: "text";
10
+ action: "new" | "edit" | "delete";
11
+ from: string;
12
+ message: string;
13
+ };
14
+ export type Message = {
15
+ id: string;
16
+ seqNo: number;
17
+ sentAt: number;
18
+ } & MessageInput;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ {"parameters":{"chat.http-api.id":{"type":"string"},"chat.websocket-api.id":{"type":"string"},"chat.enable.websocket":{"type":"boolean","default":true}}}
@@ -0,0 +1,2 @@
1
+ declare const _default: import("aws-lambda").APIGatewayProxyHandlerV2<never>;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ {"/chat/post-message":{"POST":{"body":{"parser":"json","schema":{"type":"object","additionalProperties":false,"required":["threadId","type","action","from","message"],"properties":{"threadId":{"type":"string","pattern":"^[a-f0-9]{32}$"},"type":{"enum":["text"]},"action":{"enum":["new","edit","delete"]},"from":{"type":"string","pattern":"^[a-f0-9]{32}$"},"message":{"type":"string","maxLength":256}}}}}},"/chat/sync-messages":{"GET":{"parameters":[{"name":"from","in":"query","schema":{"type":"number","multipleOf":1,"minimum":0},"required":false}]}}}
@@ -0,0 +1,93 @@
1
+ import { __assign, __awaiter, __generator } from "tslib";
2
+ import { RouteBuilder } from "somod-http-extension";
3
+ import { UserProviderMiddlewareKey } from "../../lib";
4
+ import { putMessage } from "../../lib/message";
5
+ import { v1 as v1uuid } from "uuid";
6
+ import { QueryCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
7
+ import { convertToAttr, unmarshall } from "@aws-sdk/util-dynamodb";
8
+ import { threadCache } from "../../lib/threadCache";
9
+ var dynamodb = new DynamoDBClient();
10
+ var builder = new RouteBuilder();
11
+ var postMessageHandler = function (request) { return __awaiter(void 0, void 0, void 0, function () {
12
+ var userId, thread, message;
13
+ return __generator(this, function (_a) {
14
+ switch (_a.label) {
15
+ case 0:
16
+ userId = request.body.from;
17
+ return [4 /*yield*/, threadCache.get(request.body.threadId)];
18
+ case 1:
19
+ thread = _a.sent();
20
+ if (thread === undefined) {
21
+ return [2 /*return*/, {
22
+ statusCode: 400,
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ message: "Invalid threadId : does not exist" })
25
+ }];
26
+ }
27
+ if (!thread.participants.includes(userId)) {
28
+ return [2 /*return*/, {
29
+ statusCode: 400,
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({
32
+ message: "Invalid threadId : from '".concat(userId, "' is not a participant in thread '").concat(thread.id, "'")
33
+ })
34
+ }];
35
+ }
36
+ return [4 /*yield*/, putMessage(process.env.MESSAGE_BOX_TABLE_NAME + "", userId, __assign(__assign({}, request.body), { id: v1uuid().split("-").join(""), sentAt: Date.now() }))];
37
+ case 2:
38
+ message = _a.sent();
39
+ return [2 /*return*/, {
40
+ statusCode: 200,
41
+ headers: { "Content-Type": "application-json" },
42
+ body: JSON.stringify({
43
+ id: message.id,
44
+ seqNo: message.seqNo,
45
+ sentAt: message.sentAt
46
+ })
47
+ }];
48
+ }
49
+ });
50
+ }); };
51
+ var syncMessagesHandler = function (request, event) { return __awaiter(void 0, void 0, void 0, function () {
52
+ var userId, queryCommandInput, queryCommand, result, messages;
53
+ return __generator(this, function (_a) {
54
+ switch (_a.label) {
55
+ case 0:
56
+ userId = event.somodMiddlewareContext.get(UserProviderMiddlewareKey);
57
+ queryCommandInput = {
58
+ TableName: process.env.MESSAGE_BOX_TABLE_NAME + "",
59
+ KeyConditionExpression: "#userId = :userId",
60
+ ExpressionAttributeNames: {
61
+ "#userId": "userId"
62
+ },
63
+ ExpressionAttributeValues: {
64
+ ":userId": convertToAttr(userId)
65
+ }
66
+ };
67
+ if (request.parameters.query.from) {
68
+ queryCommandInput.KeyConditionExpression += " AND #seqNo >= :seqNo";
69
+ queryCommandInput.ExpressionAttributeNames["#seqNo"] = "seqNo";
70
+ queryCommandInput.ExpressionAttributeValues[":seqNo"] = convertToAttr(request.parameters.query.from);
71
+ }
72
+ queryCommand = new QueryCommand(queryCommandInput);
73
+ return [4 /*yield*/, dynamodb.send(queryCommand)];
74
+ case 1:
75
+ result = _a.sent();
76
+ messages = (result.Items || []).map(function (item) {
77
+ var message = unmarshall(item);
78
+ delete message.userId;
79
+ return message;
80
+ });
81
+ return [2 /*return*/, {
82
+ statusCode: 200,
83
+ headers: {
84
+ "Content-Type": "application/json"
85
+ },
86
+ body: JSON.stringify(messages)
87
+ }];
88
+ }
89
+ });
90
+ }); };
91
+ builder.add("/chat/post-message", "POST", postMessageHandler);
92
+ builder.add("/chat/sync-messages", "GET", syncMessagesHandler);
93
+ export default builder.getHandler();
@@ -0,0 +1,3 @@
1
+ import { DynamoDBStreamHandler } from "aws-lambda";
2
+ declare const streamHandler: DynamoDBStreamHandler;
3
+ export default streamHandler;
@@ -0,0 +1,79 @@
1
+ import { __awaiter, __generator, __rest } from "tslib";
2
+ import { threadCache } from "../../lib/threadCache";
3
+ import { putMessage } from "../../lib/message";
4
+ import { unmarshall } from "@aws-sdk/util-dynamodb";
5
+ import { PublishCommand, SNSClient } from "@aws-sdk/client-sns";
6
+ var sns = new SNSClient();
7
+ var transfer = function (message) { return __awaiter(void 0, void 0, void 0, function () {
8
+ var thread, targets;
9
+ return __generator(this, function (_a) {
10
+ switch (_a.label) {
11
+ case 0: return [4 /*yield*/, threadCache.get(message.threadId)];
12
+ case 1:
13
+ thread = _a.sent();
14
+ if (!thread) return [3 /*break*/, 3];
15
+ targets = thread.participants.filter(function (p) { return p != message.from; });
16
+ return [4 /*yield*/, Promise.all(targets.map(function (target) { return __awaiter(void 0, void 0, void 0, function () {
17
+ return __generator(this, function (_a) {
18
+ switch (_a.label) {
19
+ case 0: return [4 /*yield*/, putMessage(process.env.MESSAGE_BOX_TABLE_NAME + "", target, message)];
20
+ case 1:
21
+ _a.sent();
22
+ return [2 /*return*/];
23
+ }
24
+ });
25
+ }); }))];
26
+ case 2:
27
+ _a.sent();
28
+ return [3 /*break*/, 4];
29
+ case 3: throw new Error("Thread '".concat(message.threadId, "' not found"));
30
+ case 4: return [2 /*return*/];
31
+ }
32
+ });
33
+ }); };
34
+ var notify = function (userId, message) { return __awaiter(void 0, void 0, void 0, function () {
35
+ var publishCommand;
36
+ return __generator(this, function (_a) {
37
+ switch (_a.label) {
38
+ case 0:
39
+ publishCommand = new PublishCommand({
40
+ TopicArn: process.env.MSG_NOTIFICATION_TOPIC + "",
41
+ Message: JSON.stringify(message),
42
+ MessageAttributes: { userId: { DataType: "String", StringValue: userId } }
43
+ });
44
+ return [4 /*yield*/, sns.send(publishCommand)];
45
+ case 1:
46
+ _a.sent();
47
+ return [2 /*return*/];
48
+ }
49
+ });
50
+ }); };
51
+ var streamHandler = function (event) { return __awaiter(void 0, void 0, void 0, function () {
52
+ var messageRaw, message, userId, msg;
53
+ var _a, _b;
54
+ return __generator(this, function (_c) {
55
+ switch (_c.label) {
56
+ case 0:
57
+ messageRaw = (_b = (_a = event.Records[0]) === null || _a === void 0 ? void 0 : _a.dynamodb) === null || _b === void 0 ? void 0 : _b.NewImage;
58
+ if (!messageRaw) return [3 /*break*/, 5];
59
+ message = unmarshall(messageRaw);
60
+ userId = message.userId, msg = __rest(message, ["userId"]);
61
+ if (!(message.from === userId)) return [3 /*break*/, 2];
62
+ return [4 /*yield*/, transfer(msg)];
63
+ case 1:
64
+ _c.sent();
65
+ return [3 /*break*/, 4];
66
+ case 2: return [4 /*yield*/, notify(userId, msg)];
67
+ case 3:
68
+ _c.sent();
69
+ _c.label = 4;
70
+ case 4: return [3 /*break*/, 6];
71
+ case 5:
72
+ // eslint-disable-next-line no-console
73
+ console.error(event);
74
+ throw new Error("messageRaw is not found in the event");
75
+ case 6: return [2 /*return*/];
76
+ }
77
+ });
78
+ }); };
79
+ export default streamHandler;
@@ -0,0 +1,7 @@
1
+ import { Middleware } from "somod";
2
+ import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
3
+ type InterfaceToRecordConvertor<T> = {
4
+ [K in keyof T]: T[K];
5
+ };
6
+ declare const userProviderMiddleware: Middleware<InterfaceToRecordConvertor<APIGatewayProxyEventV2>, APIGatewayProxyResultV2>;
7
+ export default userProviderMiddleware;
@@ -0,0 +1,24 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ import { UserProviderMiddlewareKey } from "../../../lib";
3
+ var userProviderMiddleware = function (next, event) { return __awaiter(void 0, void 0, void 0, function () {
4
+ var key;
5
+ return __generator(this, function (_a) {
6
+ switch (_a.label) {
7
+ case 0:
8
+ key = event.headers[UserProviderMiddlewareKey];
9
+ if (key === undefined) {
10
+ return [2 /*return*/, {
11
+ statusCode: 400,
12
+ headers: { "Content-Type": "application/json" },
13
+ body: JSON.stringify({
14
+ message: "Expected ".concat(UserProviderMiddlewareKey, " in the request headers")
15
+ })
16
+ }];
17
+ }
18
+ event.somodMiddlewareContext.set(UserProviderMiddlewareKey, key);
19
+ return [4 /*yield*/, next()];
20
+ case 1: return [2 /*return*/, _a.sent()];
21
+ }
22
+ });
23
+ }); };
24
+ export default userProviderMiddleware;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("aws-lambda").APIGatewayProxyHandlerV2<never>;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ {"/chat/thread":{"POST":{"body":{"parser":"json","schema":{"type":"object","additionalProperties":false,"required":["participants"],"properties":{"participants":{"type":"array","items":{"type":"string","pattern":"^[a-f0-9]{32}$"},"minItems":2,"maxItems":2}}}}}},"/chat/thread/{id}":{"GET":{"parameters":[{"name":"id","in":"path","schema":{"type":"string","pattern":"^[a-f0-9]{32}$"},"required":true}]}}}
@@ -0,0 +1,57 @@
1
+ import { __assign, __awaiter, __generator } from "tslib";
2
+ import { RouteBuilder } from "somod-http-extension";
3
+ import { DynamoDBClient, GetItemCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";
4
+ import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
5
+ import { v1 as v1uuid } from "uuid";
6
+ var builder = new RouteBuilder();
7
+ var dynamodb = new DynamoDBClient();
8
+ var createThread = function (request) { return __awaiter(void 0, void 0, void 0, function () {
9
+ var thread, putItemCommand;
10
+ return __generator(this, function (_a) {
11
+ switch (_a.label) {
12
+ case 0:
13
+ thread = __assign(__assign({}, request.body), { id: v1uuid().split("-").join("") });
14
+ putItemCommand = new PutItemCommand({
15
+ TableName: process.env.THREAD_TABLE_NAME + "",
16
+ Item: marshall(thread),
17
+ ConditionExpression: "attribute_not_exists(#id)",
18
+ ExpressionAttributeNames: {
19
+ "#id": "id"
20
+ }
21
+ });
22
+ return [4 /*yield*/, dynamodb.send(putItemCommand)];
23
+ case 1:
24
+ _a.sent();
25
+ return [2 /*return*/, {
26
+ statusCode: 200,
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify(thread)
29
+ }];
30
+ }
31
+ });
32
+ }); };
33
+ var getThread = function (request) { return __awaiter(void 0, void 0, void 0, function () {
34
+ var getItemCommand, result;
35
+ return __generator(this, function (_a) {
36
+ switch (_a.label) {
37
+ case 0:
38
+ getItemCommand = new GetItemCommand({
39
+ TableName: process.env.THREAD_TABLE_NAME + "",
40
+ Key: marshall({ id: request.parameters.path.id })
41
+ });
42
+ return [4 /*yield*/, dynamodb.send(getItemCommand)];
43
+ case 1:
44
+ result = _a.sent();
45
+ return [2 /*return*/, result.Item
46
+ ? {
47
+ statusCode: 200,
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify(unmarshall(result.Item))
50
+ }
51
+ : { statusCode: 404 }];
52
+ }
53
+ });
54
+ }); };
55
+ builder.add("/chat/thread", "POST", createThread);
56
+ builder.add("/chat/thread/{id}", "GET", getThread);
57
+ export default builder.getHandler();
@@ -0,0 +1,3 @@
1
+ import { SNSHandler } from "aws-lambda";
2
+ declare const WebsocketNotifyMessage: SNSHandler;
3
+ export default WebsocketNotifyMessage;
@@ -0,0 +1,73 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";
3
+ import { convertToAttr, unmarshall } from "@aws-sdk/util-dynamodb";
4
+ import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";
5
+ var dynamodb = new DynamoDBClient();
6
+ var apiGwManagementApi = new ApiGatewayManagementApiClient({
7
+ endpoint: process.env.CONNECTIONS_ENDPOINT
8
+ });
9
+ var notifyMessage = function (connectionId, message) { return __awaiter(void 0, void 0, void 0, function () {
10
+ var data, e_1;
11
+ return __generator(this, function (_a) {
12
+ switch (_a.label) {
13
+ case 0:
14
+ data = JSON.stringify(message);
15
+ _a.label = 1;
16
+ case 1:
17
+ _a.trys.push([1, 3, , 4]);
18
+ return [4 /*yield*/, apiGwManagementApi.send(new PostToConnectionCommand({ ConnectionId: connectionId, Data: data }))];
19
+ case 2:
20
+ _a.sent();
21
+ return [3 /*break*/, 4];
22
+ case 3:
23
+ e_1 = _a.sent();
24
+ // eslint-disable-next-line no-console
25
+ console.error(e_1);
26
+ return [3 /*break*/, 4];
27
+ case 4: return [2 /*return*/];
28
+ }
29
+ });
30
+ }); };
31
+ var WebsocketNotifyMessage = function (event) { return __awaiter(void 0, void 0, void 0, function () {
32
+ var userId, message, connections;
33
+ var _a, _b, _c;
34
+ return __generator(this, function (_d) {
35
+ switch (_d.label) {
36
+ case 0:
37
+ userId = (_b = (_a = event.Records[0]) === null || _a === void 0 ? void 0 : _a.Sns.MessageAttributes["userId"]) === null || _b === void 0 ? void 0 : _b.Value;
38
+ message = JSON.parse((_c = event.Records[0]) === null || _c === void 0 ? void 0 : _c.Sns.Message);
39
+ if (!(userId && message)) return [3 /*break*/, 3];
40
+ return [4 /*yield*/, dynamodb.send(new QueryCommand({
41
+ TableName: process.env.CONNECTIONS_TABLE_NAME + "",
42
+ IndexName: "ByUserId",
43
+ KeyConditionExpression: "#userId = :userId",
44
+ ExpressionAttributeNames: {
45
+ "#userId": "userId"
46
+ },
47
+ ExpressionAttributeValues: {
48
+ ":userId": convertToAttr(userId)
49
+ }
50
+ }))];
51
+ case 1:
52
+ connections = _d.sent();
53
+ return [4 /*yield*/, Promise.all((connections.Items || []).map(function (item) { return __awaiter(void 0, void 0, void 0, function () {
54
+ var conn;
55
+ return __generator(this, function (_a) {
56
+ switch (_a.label) {
57
+ case 0:
58
+ conn = unmarshall(item);
59
+ return [4 /*yield*/, notifyMessage(conn.id, message)];
60
+ case 1:
61
+ _a.sent();
62
+ return [2 /*return*/];
63
+ }
64
+ });
65
+ }); }))];
66
+ case 2:
67
+ _d.sent();
68
+ _d.label = 3;
69
+ case 3: return [2 /*return*/];
70
+ }
71
+ });
72
+ }); };
73
+ export default WebsocketNotifyMessage;
@@ -0,0 +1,3 @@
1
+ import { APIGatewayProxyWebsocketHandlerV2 } from "aws-lambda";
2
+ declare const WebsocketOnConnect: APIGatewayProxyWebsocketHandlerV2;
3
+ export default WebsocketOnConnect;
@@ -0,0 +1,23 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ import { UserProviderMiddlewareKey } from "../../lib";
3
+ import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
4
+ import { marshall } from "@aws-sdk/util-dynamodb";
5
+ var dynamodb = new DynamoDBClient();
6
+ var WebsocketOnConnect = function (event) { return __awaiter(void 0, void 0, void 0, function () {
7
+ var userId, connectionId;
8
+ return __generator(this, function (_a) {
9
+ switch (_a.label) {
10
+ case 0:
11
+ userId = event.somodMiddlewareContext.get(UserProviderMiddlewareKey);
12
+ connectionId = event.requestContext.connectionId;
13
+ return [4 /*yield*/, dynamodb.send(new PutItemCommand({
14
+ TableName: process.env.CONNECTIONS_TABLE_NAME + "",
15
+ Item: marshall({ id: connectionId, userId: userId })
16
+ }))];
17
+ case 1:
18
+ _a.sent();
19
+ return [2 /*return*/, { statusCode: 200, body: "Connected." }];
20
+ }
21
+ });
22
+ }); };
23
+ export default WebsocketOnConnect;
@@ -0,0 +1,3 @@
1
+ import { APIGatewayProxyWebsocketHandlerV2 } from "aws-lambda";
2
+ declare const WebsocketOnDisconnect: APIGatewayProxyWebsocketHandlerV2;
3
+ export default WebsocketOnDisconnect;
@@ -0,0 +1,21 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ import { DynamoDBClient, DeleteItemCommand } from "@aws-sdk/client-dynamodb";
3
+ import { marshall } from "@aws-sdk/util-dynamodb";
4
+ var dynamodb = new DynamoDBClient();
5
+ var WebsocketOnDisconnect = function (event) { return __awaiter(void 0, void 0, void 0, function () {
6
+ var connectionId;
7
+ return __generator(this, function (_a) {
8
+ switch (_a.label) {
9
+ case 0:
10
+ connectionId = event.requestContext.connectionId;
11
+ return [4 /*yield*/, dynamodb.send(new DeleteItemCommand({
12
+ TableName: process.env.CONNECTIONS_TABLE_NAME + "",
13
+ Key: marshall({ id: connectionId })
14
+ }))];
15
+ case 1:
16
+ _a.sent();
17
+ return [2 /*return*/, { statusCode: 200, body: "Disconnected." }];
18
+ }
19
+ });
20
+ }); };
21
+ export default WebsocketOnDisconnect;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("aws-lambda").APIGatewayProxyWebsocketHandlerV2<never>;
2
+ export default _default;
@@ -0,0 +1,49 @@
1
+ import { __assign, __awaiter, __generator, __rest } from "tslib";
2
+ import { RouteBuilder } from "somod-websocket-extension";
3
+ import { threadCache } from "../../lib/threadCache";
4
+ import { putMessage } from "../../lib/message";
5
+ import { v1 as v1uuid } from "uuid";
6
+ var builder = new RouteBuilder();
7
+ builder.add("$default", function (message) { return __awaiter(void 0, void 0, void 0, function () {
8
+ var userId, thread, _a, wsMsgId, msg, messageResult;
9
+ return __generator(this, function (_b) {
10
+ switch (_b.label) {
11
+ case 0:
12
+ userId = message.body.from;
13
+ return [4 /*yield*/, threadCache.get(message.body.threadId)];
14
+ case 1:
15
+ thread = _b.sent();
16
+ if (thread === undefined) {
17
+ return [2 /*return*/, {
18
+ statusCode: 400,
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ message: "Invalid threadId : does not exist" })
21
+ }];
22
+ }
23
+ if (!thread.participants.includes(userId)) {
24
+ return [2 /*return*/, {
25
+ statusCode: 400,
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({
28
+ message: "Invalid threadId : from '".concat(userId, "' is not a participant in thread '").concat(thread.id, "'")
29
+ })
30
+ }];
31
+ }
32
+ _a = message.body, wsMsgId = _a.wsMsgId, msg = __rest(_a, ["wsMsgId"]);
33
+ return [4 /*yield*/, putMessage(process.env.MESSAGE_BOX_TABLE_NAME + "", userId, __assign(__assign({}, msg), { id: v1uuid().split("-").join(""), sentAt: Date.now() }))];
34
+ case 2:
35
+ messageResult = _b.sent();
36
+ return [2 /*return*/, {
37
+ statusCode: 200,
38
+ headers: { "Content-Type": "application-json" },
39
+ body: JSON.stringify({
40
+ wsMsgId: wsMsgId,
41
+ id: messageResult.id,
42
+ seqNo: messageResult.seqNo,
43
+ sentAt: messageResult.sentAt
44
+ })
45
+ }];
46
+ }
47
+ });
48
+ }); });
49
+ export default builder.getHandler();
@@ -0,0 +1 @@
1
+ {"$default":{"body":{"parser":"json","schema":{"type":"object","additionalProperties":false,"required":["wsMsgId","threadId","type","action","from","message"],"properties":{"wsMsgId":{"type":"string","pattern":"^[a-f0-9]{32}$"},"threadId":{"type":"string","pattern":"^[a-f0-9]{32}$"},"type":{"enum":["text"]},"action":{"enum":["new","edit","delete"]},"from":{"type":"string","pattern":"^[a-f0-9]{32}$"},"message":{"type":"string","maxLength":256}}}}}}
@@ -0,0 +1 @@
1
+ {"Resources":{"ChatApi":{"Type":"AWS::Serverless::HttpApi","SOMOD::Access":"public","SOMOD::Output":{"default":true},"Properties":{}},"ThreadTable":{"Type":"AWS::DynamoDB::Table","SOMOD::Output":{"default":true,"attributes":["Arn"]},"Properties":{"TableName":{"SOMOD::ResourceName":"Thread"},"BillingMode":"PAY_PER_REQUEST","KeySchema":[{"AttributeName":"id","KeyType":"HASH"}],"AttributeDefinitions":[{"AttributeName":"id","AttributeType":"S"}]}},"MessageBox":{"Type":"AWS::DynamoDB::Table","SOMOD::Output":{"default":true,"attributes":["Arn","StreamArn"]},"Properties":{"TableName":{"SOMOD::ResourceName":"MessageBox"},"BillingMode":"PAY_PER_REQUEST","KeySchema":[{"AttributeName":"userId","KeyType":"HASH"},{"AttributeName":"seqNo","KeyType":"RANGE"}],"AttributeDefinitions":[{"AttributeName":"userId","AttributeType":"S"},{"AttributeName":"seqNo","AttributeType":"N"}],"StreamSpecification":{"StreamViewType":"NEW_IMAGE"}}},"MessageStreamHandlerDLQ":{"Type":"AWS::SQS::Queue","SOMOD::Output":{"default":true,"attributes":["Arn","QueueName","QueueUrl"]},"Properties":{"QueueName":{"SOMOD::ResourceName":"MessageStreamHandlerDLQ"}}},"MessageStreamHandler":{"Type":"AWS::Serverless::Function","Properties":{"FunctionName":{"SOMOD::ResourceName":"MsgStrmHandler"},"CodeUri":{"SOMOD::Function":{"name":"messageStreamHandler","type":"DynamoDB"}},"Environment":{"Variables":{"MESSAGE_BOX_TABLE_NAME":{"SOMOD::Ref":{"resource":"MessageBox"}},"THREAD_TABLE_NAME":{"SOMOD::Ref":{"resource":"ThreadTable"}},"MSG_NOTIFICATION_TOPIC":{"SOMOD::Ref":{"resource":"MessageNotificationTopic"}}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"MessageBox","attribute":"Arn"}}],"Action":["dynamodb:PutItem"]},{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"ThreadTable","attribute":"Arn"}}],"Action":["dynamodb:GetItem"]},{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"MessageNotificationTopic"}}],"Action":["sns:Publish"]},{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"MessageStreamHandlerDLQ","attribute":"Arn"}}],"Action":["sqs:SendMessage"]}]}],"Events":{"Stream":{"Type":"DynamoDB","Properties":{"Stream":{"SOMOD::Ref":{"resource":"MessageBox","attribute":"StreamArn"}},"BatchSize":1,"MaximumRetryAttempts":3,"StartingPosition":"LATEST","DestinationConfig":{"OnFailure":{"Destination":{"SOMOD::Ref":{"resource":"MessageStreamHandlerDLQ","attribute":"Arn"}}}}}}}}},"UserProviderMiddleware":{"Type":"SOMOD::Serverless::FunctionMiddleware","SOMOD::Access":"public","SOMOD::Output":{"default":true},"Properties":{"CodeUri":{"SOMOD::FunctionMiddleware":{"name":"userProvider"}}}},"CommonLibsLayer":{"Type":"AWS::Serverless::LayerVersion","SOMOD::Output":{"default":true},"Properties":{"RetentionPolicy":"Delete","ContentUri":{"SOMOD::FunctionLayer":{"name":"commonlibs","libraries":["decorated-ajv"]}}}},"Message":{"Type":"AWS::Serverless::Function","SOMOD::Access":"public","Properties":{"FunctionName":{"SOMOD::ResourceName":"MessageApi"},"CodeUri":{"SOMOD::Function":{"name":"message","type":"HttpApi","middlewares":[{"module":"somod-http-extension","resource":"SomodHttpMiddleware"},{"resource":"UserProviderMiddleware"}]}},"Layers":[{"SOMOD::Ref":{"resource":"CommonLibsLayer"}}],"Environment":{"Variables":{"MESSAGE_BOX_TABLE_NAME":{"SOMOD::Ref":{"resource":"MessageBox"}},"THREAD_TABLE_NAME":{"SOMOD::Ref":{"resource":"ThreadTable"}}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"MessageBox","attribute":"Arn"}}],"Action":["dynamodb:PutItem","dynamodb:Query"]},{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"ThreadTable","attribute":"Arn"}}],"Action":["dynamodb:GetItem"]}]}],"Events":{"PostMessage":{"Type":"HttpApi","Properties":{"ApiId":{"SOMOD::Ref":{"resource":"ChatApi"}},"Method":"POST","Path":"/chat/post-message"}},"SyncMessages":{"Type":"HttpApi","Properties":{"ApiId":{"SOMOD::Ref":{"resource":"ChatApi"}},"Method":"GET","Path":"/chat/sync-messages"}}}}},"Thread":{"Type":"AWS::Serverless::Function","SOMOD::Access":"public","Properties":{"FunctionName":{"SOMOD::ResourceName":"ThreadApi"},"CodeUri":{"SOMOD::Function":{"name":"thread","type":"HttpApi","middlewares":[{"module":"somod-http-extension","resource":"SomodHttpMiddleware"}]}},"Layers":[{"SOMOD::Ref":{"resource":"CommonLibsLayer"}}],"Environment":{"Variables":{"THREAD_TABLE_NAME":{"SOMOD::Ref":{"resource":"ThreadTable"}}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"ThreadTable","attribute":"Arn"}}],"Action":["dynamodb:PutItem","dynamodb:GetItem"]}]}],"Events":{"CreateThread":{"Type":"HttpApi","Properties":{"ApiId":{"SOMOD::Ref":{"resource":"ChatApi"}},"Method":"POST","Path":"/chat/thread"}},"GetThread":{"Type":"HttpApi","Properties":{"ApiId":{"SOMOD::Ref":{"resource":"ChatApi"}},"Method":"GET","Path":"/chat/thread/{id}"}}}}},"MessageNotificationTopic":{"Type":"AWS::SNS::Topic","SOMOD::Access":"public","SOMOD::Output":{"default":true,"attributes":["TopicName"]},"Properties":{"TopicName":{"SOMOD::ResourceName":"MsgNotificationTopic"}}},"WebSocketApi":{"Type":"AWS::ApiGatewayV2::Api","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"Name":{"SOMOD::ResourceName":"ChatWebsocketApi"},"ProtocolType":"WEBSOCKET","RouteSelectionExpression":"$request.body.action"}},"ConnectRoute":{"Type":"AWS::ApiGatewayV2::Route","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"RouteKey":"$connect","OperationName":"ConnectRoute","Target":{"Fn::Join":["/",["integrations",{"SOMOD::Ref":{"resource":"ConnectInteg"}}]]}}},"ConnectInteg":{"Type":"AWS::ApiGatewayV2::Integration","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"Description":"Connect Integration","IntegrationType":"AWS_PROXY","IntegrationUri":{"Fn::Sub":["arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnConnectFunctionArn}/invocations",{"OnConnectFunctionArn":{"SOMOD::Ref":{"resource":"OnConnectFunction","attribute":"Arn"}}}]}}},"DisconnectRoute":{"Type":"AWS::ApiGatewayV2::Route","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"RouteKey":"$disconnect","OperationName":"DisconnectRoute","Target":{"Fn::Join":["/",["integrations",{"SOMOD::Ref":{"resource":"DisconnectInteg"}}]]}}},"DisconnectInteg":{"Type":"AWS::ApiGatewayV2::Integration","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"Description":"Disconnect Integration","IntegrationType":"AWS_PROXY","IntegrationUri":{"Fn::Sub":["arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnDisconnectFunctionArn}/invocations",{"OnDisconnectFunctionArn":{"SOMOD::Ref":{"resource":"OnDisconnectFunction","attribute":"Arn"}}}]}}},"DefaultRoute":{"Type":"AWS::ApiGatewayV2::Route","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"default":true},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"RouteKey":"$default","OperationName":"DefaultRoute","Target":{"Fn::Join":["/",["integrations",{"SOMOD::Ref":{"resource":"DefaultInteg"}}]]}}},"DefaultRouteResponse":{"Type":"AWS::ApiGatewayV2::RouteResponse","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"default":true},"Properties":{"RouteId":{"SOMOD::Ref":{"resource":"DefaultRoute"}},"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"RouteResponseKey":"$default"}},"DefaultInteg":{"Type":"AWS::ApiGatewayV2::Integration","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"default":true},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"Description":"Default Integration","IntegrationType":"AWS_PROXY","IntegrationUri":{"Fn::Sub":["arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnMessageFunctionArn}/invocations",{"OnMessageFunctionArn":{"SOMOD::Ref":{"resource":"OnMessageFunction","attribute":"Arn"}}}]}}},"DefaultIntegResponse":{"Type":"AWS::ApiGatewayV2::IntegrationResponse","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"IntegrationId":{"SOMOD::Ref":{"resource":"DefaultInteg"}},"IntegrationResponseKey":"$default"}},"Stage":{"Type":"AWS::ApiGatewayV2::Stage","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"StageName":"$default","ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"AutoDeploy":true}},"ConnectionsTable":{"Type":"AWS::DynamoDB::Table","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"default":true,"attributes":["Arn"]},"Properties":{"AttributeDefinitions":[{"AttributeName":"id","AttributeType":"S"},{"AttributeName":"userId","AttributeType":"S"}],"KeySchema":[{"AttributeName":"id","KeyType":"HASH"}],"GlobalSecondaryIndexes":[{"IndexName":"ByUserId","KeySchema":[{"AttributeName":"userId","KeyType":"HASH"}],"Projection":{"ProjectionType":"KEYS_ONLY"}}],"BillingMode":"PAY_PER_REQUEST"}},"OnConnectFunction":{"Type":"AWS::Serverless::Function","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"attributes":["Arn"]},"Properties":{"CodeUri":{"SOMOD::Function":{"type":"WebSocket","name":"wsOnConnect","middlewares":[{"resource":"UserProviderMiddleware"}]}},"Environment":{"Variables":{"CONNECTIONS_TABLE_NAME":{"SOMOD::Ref":{"resource":"ConnectionsTable"}}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"ConnectionsTable","attribute":"Arn"}}],"Action":["dynamodb:PutItem"]}]}]}},"OnConnectPermission":{"Type":"AWS::Lambda::Permission","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"Action":"lambda:InvokeFunction","FunctionName":{"SOMOD::Ref":{"resource":"OnConnectFunction"}},"Principal":"apigateway.amazonaws.com","SourceArn":{"Fn::Sub":["arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiId}/$default/$connect",{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}}}]}}},"OnDisconnectFunction":{"Type":"AWS::Serverless::Function","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"attributes":["Arn"]},"Properties":{"CodeUri":{"SOMOD::Function":{"type":"WebSocket","name":"wsOnDisconnect"}},"Environment":{"Variables":{"CONNECTIONS_TABLE_NAME":{"SOMOD::Ref":{"resource":"ConnectionsTable"}}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"ConnectionsTable","attribute":"Arn"}}],"Action":["dynamodb:DeleteItem"]}]}]}},"OnDisconnectPermission":{"Type":"AWS::Lambda::Permission","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"Action":"lambda:InvokeFunction","FunctionName":{"SOMOD::Ref":{"resource":"OnDisconnectFunction"}},"Principal":"apigateway.amazonaws.com","SourceArn":{"Fn::Sub":["arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiId}/$default/$disconnect",{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}}}]}}},"OnMessageFunction":{"Type":"AWS::Serverless::Function","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"SOMOD::Output":{"attributes":["Arn"]},"Properties":{"CodeUri":{"SOMOD::Function":{"type":"WebSocket","name":"wsOnMessage","middlewares":[{"module":"somod-websocket-extension","resource":"SomodWebSocketMiddleware"}]}},"Layers":[{"SOMOD::Ref":{"resource":"CommonLibsLayer"}}],"Environment":{"Variables":{"MESSAGE_BOX_TABLE_NAME":{"SOMOD::Ref":{"resource":"MessageBox"}},"THREAD_TABLE_NAME":{"SOMOD::Ref":{"resource":"ThreadTable"}}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"MessageBox","attribute":"Arn"}}],"Action":["dynamodb:PutItem"]},{"Effect":"Allow","Resource":[{"SOMOD::Ref":{"resource":"ThreadTable","attribute":"Arn"}}],"Action":["dynamodb:GetItem"]}]}]}},"OnMessagePermission":{"Type":"AWS::Lambda::Permission","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"Action":"lambda:InvokeFunction","FunctionName":{"SOMOD::Ref":{"resource":"OnMessageFunction"}},"Principal":"apigateway.amazonaws.com","SourceArn":{"Fn::Sub":["arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiId}/$default/$default",{"ApiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}}}]}}},"NotifyMessageFunction":{"Type":"AWS::Serverless::Function","SOMOD::CreateIf":{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},"Properties":{"CodeUri":{"SOMOD::Function":{"type":"SNS","name":"wsNotifyMessage"}},"Environment":{"Variables":{"CONNECTIONS_TABLE_NAME":{"SOMOD::Ref":{"resource":"ConnectionsTable"}},"CONNECTIONS_ENDPOINT":{"Fn::Sub":["https://${WebSocketApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}",{"WebSocketApi":{"SOMOD::Ref":{"resource":"WebSocketApi"}},"Stage":{"SOMOD::Ref":{"resource":"Stage"}}}]}}},"Policies":[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":[{"Fn::Sub":["${ConnectionsTableArn}/index/ByUserId",{"ConnectionsTableArn":{"SOMOD::Ref":{"resource":"ConnectionsTable","attribute":"Arn"}}}]}],"Action":["dynamodb:Query"]}]},{"Statement":[{"Effect":"Allow","Action":["execute-api:ManageConnections"],"Resource":[{"Fn::Sub":["arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*",{"WebSocketApi":{"SOMOD::Ref":{"resource":"WebSocketApi"}}}]}]}]}],"Events":{"NotifyFromSNS":{"Type":"SNS","Properties":{"Topic":{"SOMOD::Ref":{"resource":"MessageNotificationTopic"}}}}},"Timeout":300}}},"Outputs":{"chat.http-api.id":{"Fn::Sub":["https://${apiId}.execute-api.${AWS::Region}.amazonaws.com/",{"apiId":{"SOMOD::Ref":{"resource":"ChatApi"}}}]},"chat.websocket-api.id":{"SOMOD::If":[{"SOMOD::Equals":[{"SOMOD::Parameter":"chat.enable.websocket"},true]},{"Fn::Sub":["wss://${apiId}.execute-api.${AWS::Region}.amazonaws.com/$default",{"apiId":{"SOMOD::Ref":{"resource":"WebSocketApi"}}}]},null]}}}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "somod-chat-service",
3
+ "version": "0.0.1",
4
+ "description": "Serverless Chat Service from SOMOD",
5
+ "scripts": {
6
+ "prettier": "npx prettier --check --ignore-unknown --no-error-on-unmatched-pattern ./**/*",
7
+ "eslint": "npx eslint ./ --no-error-on-unmatched-pattern",
8
+ "prebuild": "npm run prettier && npm run eslint",
9
+ "build": "npx somod build --serverless",
10
+ "pretest": "npm run build",
11
+ "test": "echo 'No Tests'",
12
+ "prepack": "npm run test",
13
+ "deploy": "npx somod deploy"
14
+ },
15
+ "keywords": [
16
+ "somod",
17
+ "chat",
18
+ "serverless",
19
+ "sam",
20
+ "websocket",
21
+ "aws"
22
+ ],
23
+ "author": "Raghavendra K R <raghavendra@sodaru.com>",
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "@types/aws-lambda": "^8.10.130",
27
+ "@types/uuid": "^9.0.7",
28
+ "aws-sdk": "^2.1510.0",
29
+ "decorated-ajv": "^1.1.0",
30
+ "eslint-config-sodaru": "^1.0.1",
31
+ "prettier-config-sodaru": "^1.0.0",
32
+ "somod": "^1.17.3",
33
+ "somod-middleware": "^1.17.3"
34
+ },
35
+ "module": "build/lib/index.js",
36
+ "typings": "build/lib/index.d.ts",
37
+ "files": [
38
+ "build"
39
+ ],
40
+ "sideEffects": false,
41
+ "somod": "1.17.3",
42
+ "eslintConfig": {
43
+ "extends": [
44
+ "sodaru"
45
+ ]
46
+ },
47
+ "prettier": "prettier-config-sodaru",
48
+ "dependencies": {
49
+ "@aws-sdk/client-apigatewaymanagementapi": "^3.465.0",
50
+ "@aws-sdk/client-dynamodb": "^3.465.0",
51
+ "@aws-sdk/client-sns": "^3.465.0",
52
+ "@aws-sdk/util-dynamodb": "^3.465.0",
53
+ "somod-http-extension": "^1.2.3",
54
+ "somod-websocket-extension": "^1.0.0",
55
+ "uuid": "^9.0.1"
56
+ }
57
+ }