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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/build/lib/cache.d.ts +3 -0
- package/build/lib/cache.js +72 -0
- package/build/lib/constants.d.ts +1 -0
- package/build/lib/constants.js +1 -0
- package/build/lib/index.d.ts +2 -0
- package/build/lib/index.js +2 -0
- package/build/lib/message.d.ts +2 -0
- package/build/lib/message.js +27 -0
- package/build/lib/threadCache.d.ts +4 -0
- package/build/lib/threadCache.js +31 -0
- package/build/lib/types.d.ts +18 -0
- package/build/lib/types.js +1 -0
- package/build/parameters.json +1 -0
- package/build/serverless/functions/message.d.ts +2 -0
- package/build/serverless/functions/message.http.json +1 -0
- package/build/serverless/functions/message.js +93 -0
- package/build/serverless/functions/messageStreamHandler.d.ts +3 -0
- package/build/serverless/functions/messageStreamHandler.js +79 -0
- package/build/serverless/functions/middlewares/userProvider.d.ts +7 -0
- package/build/serverless/functions/middlewares/userProvider.js +24 -0
- package/build/serverless/functions/thread.d.ts +2 -0
- package/build/serverless/functions/thread.http.json +1 -0
- package/build/serverless/functions/thread.js +57 -0
- package/build/serverless/functions/wsNotifyMessage.d.ts +3 -0
- package/build/serverless/functions/wsNotifyMessage.js +73 -0
- package/build/serverless/functions/wsOnConnect.d.ts +3 -0
- package/build/serverless/functions/wsOnConnect.js +23 -0
- package/build/serverless/functions/wsOnDisconnect.d.ts +3 -0
- package/build/serverless/functions/wsOnDisconnect.js +21 -0
- package/build/serverless/functions/wsOnMessage.d.ts +2 -0
- package/build/serverless/functions/wsOnMessage.js +49 -0
- package/build/serverless/functions/wsOnMessage.websocket.json +1 -0
- package/build/serverless/template.json +1 -0
- 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,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,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,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 @@
|
|
|
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,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 @@
|
|
|
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,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,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,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,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
|
+
}
|