somod-chat-service 0.8.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/cache.js +33 -44
- package/build/lib/constants.js +1 -1
- package/build/lib/getUserIdFromEvent.js +1 -1
- package/build/lib/message.js +40 -55
- package/build/lib/sessionUtil.js +53 -64
- package/build/lib/threadCache.js +14 -27
- package/build/lib/types.js +2 -2
- package/build/serverless/functions/message.js +77 -90
- package/build/serverless/functions/messageStreamHandler.js +39 -75
- package/build/serverless/functions/middlewares/userProvider.js +20 -28
- package/build/serverless/functions/thread.js +54 -75
- package/build/serverless/functions/wsNotifyMessage.js +33 -67
- package/build/serverless/functions/wsOnConnect.js +10 -19
- package/build/serverless/functions/wsOnDisconnect.js +9 -18
- package/build/serverless/functions/wsOnMessage.js +44 -49
- package/build/serverless/template.json +1 -1
- package/package.json +6 -6
package/build/lib/cache.js
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var _a, _b;
|
|
10
|
-
var node = nodes[key];
|
|
1
|
+
export const cache = (max, ttl) => {
|
|
2
|
+
const cacheTtl = ttl;
|
|
3
|
+
const nodes = {};
|
|
4
|
+
let first = undefined;
|
|
5
|
+
let last = undefined;
|
|
6
|
+
let size = 0;
|
|
7
|
+
const remove = (key) => {
|
|
8
|
+
const node = nodes[key];
|
|
11
9
|
if (node.next) {
|
|
12
10
|
node.next.previous = node.previous;
|
|
13
11
|
}
|
|
14
12
|
else {
|
|
15
|
-
last =
|
|
13
|
+
last = node.previous?.content.key;
|
|
16
14
|
}
|
|
17
15
|
if (node.previous) {
|
|
18
16
|
node.previous.next = node.next;
|
|
19
17
|
}
|
|
20
18
|
else {
|
|
21
|
-
first =
|
|
19
|
+
first = node.next?.content.key;
|
|
22
20
|
}
|
|
23
21
|
size--;
|
|
24
22
|
delete nodes[key];
|
|
25
23
|
};
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const add = (key, value) => {
|
|
25
|
+
const node = {
|
|
28
26
|
content: {
|
|
29
|
-
key
|
|
27
|
+
key,
|
|
30
28
|
cachedAt: Date.now(),
|
|
31
|
-
value
|
|
29
|
+
value
|
|
32
30
|
}
|
|
33
31
|
};
|
|
34
32
|
nodes[key] = node;
|
|
@@ -39,34 +37,25 @@ export var cache = function (max, ttl) {
|
|
|
39
37
|
}
|
|
40
38
|
first = key;
|
|
41
39
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
add(key, value);
|
|
62
|
-
if (size > max && last) {
|
|
63
|
-
remove(last);
|
|
64
|
-
}
|
|
65
|
-
return [2 /*return*/, value];
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}); };
|
|
40
|
+
const get = async (key, fetcher, ttl) => {
|
|
41
|
+
const node = nodes[key];
|
|
42
|
+
let value = node?.content.value;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (node === undefined ||
|
|
45
|
+
(ttl && node.content.cachedAt < now - ttl) ||
|
|
46
|
+
(cacheTtl && node.content.cachedAt < now - cacheTtl)) {
|
|
47
|
+
value = await fetcher();
|
|
48
|
+
}
|
|
49
|
+
if (node) {
|
|
50
|
+
remove(node.content.key);
|
|
51
|
+
}
|
|
52
|
+
add(key, value);
|
|
53
|
+
if (size > max && last) {
|
|
54
|
+
remove(last);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
69
58
|
return {
|
|
70
|
-
get
|
|
59
|
+
get
|
|
71
60
|
};
|
|
72
61
|
};
|
package/build/lib/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const UserProviderMiddlewareKey = "chat-backend-user-id";
|
package/build/lib/message.js
CHANGED
|
@@ -1,61 +1,46 @@
|
|
|
1
|
-
import { __assign, __awaiter, __generator } from "tslib";
|
|
2
1
|
import { marshall } from "@aws-sdk/util-dynamodb";
|
|
3
2
|
import { typeToAllowedActionsMap } from "./types";
|
|
4
3
|
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
|
|
5
4
|
import { threadCache } from "./threadCache";
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"#seqNo": "seqNo"
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
return [4 /*yield*/, dynamodb.send(putItemCommand)];
|
|
24
|
-
case 1:
|
|
25
|
-
_a.sent();
|
|
26
|
-
return [2 /*return*/, msg];
|
|
5
|
+
const dynamodb = new DynamoDBClient();
|
|
6
|
+
export const putMessage = async (tableName, userId, message) => {
|
|
7
|
+
const msg = {
|
|
8
|
+
...message,
|
|
9
|
+
seqNo: Date.now()
|
|
10
|
+
};
|
|
11
|
+
const messageWithKeys = { ...msg, userId };
|
|
12
|
+
const 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"
|
|
27
19
|
}
|
|
28
20
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
})
|
|
56
|
-
}];
|
|
57
|
-
}
|
|
58
|
-
return [2 /*return*/];
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}); };
|
|
21
|
+
await dynamodb.send(putItemCommand);
|
|
22
|
+
return msg;
|
|
23
|
+
};
|
|
24
|
+
export const validateIncomingMessage = async (message, userId) => {
|
|
25
|
+
let errorMessage = undefined;
|
|
26
|
+
const thread = await threadCache.get(message.threadId);
|
|
27
|
+
if (thread === undefined) {
|
|
28
|
+
errorMessage = "Invalid threadId : does not exist";
|
|
29
|
+
}
|
|
30
|
+
else if (!thread.participants.includes(userId)) {
|
|
31
|
+
errorMessage = `Invalid threadId : from '${userId}' is not a participant in thread '${thread.id}'`;
|
|
32
|
+
}
|
|
33
|
+
else if (!typeToAllowedActionsMap[message.type].includes(message.action)) {
|
|
34
|
+
errorMessage = `Invalid action : action must be '${typeToAllowedActionsMap[message.type].join(",")}' for '${message.type}' type`;
|
|
35
|
+
}
|
|
36
|
+
if (errorMessage) {
|
|
37
|
+
return {
|
|
38
|
+
statusCode: 400,
|
|
39
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
message: errorMessage,
|
|
42
|
+
threadId: message.threadId
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
package/build/lib/sessionUtil.js
CHANGED
|
@@ -1,79 +1,68 @@
|
|
|
1
|
-
var _a, _b;
|
|
2
|
-
import { __awaiter, __generator } from "tslib";
|
|
3
1
|
import { verify } from "jsonwebtoken";
|
|
4
2
|
import { sessionRequirement } from "./types";
|
|
5
3
|
import { threadCache } from "./threadCache";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
4
|
+
const sessionJwtSecret = process.env.SESSION_SECRET ?? "";
|
|
5
|
+
const sessionForce = process.env.SESSION_FORCE ?? "";
|
|
6
|
+
export const Error = {
|
|
9
7
|
required: "missing required field: sessionToken",
|
|
10
8
|
invalid: "invalid field: sessionToken"
|
|
11
9
|
};
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
if (!!sessionToken) return [3 /*break*/, 5];
|
|
21
|
-
isSessionRequiredForThisTypeAction = (_a = sessionRequirement[type]) === null || _a === void 0 ? void 0 : _a[action];
|
|
22
|
-
if (!(isSessionRequiredForThisTypeAction !== undefined)) return [3 /*break*/, 4];
|
|
23
|
-
oneDay_1 = Date.now() + 1000 * 60 * 60 * 24;
|
|
24
|
-
if (!(isSessionRequiredForThisTypeAction === "always")) return [3 /*break*/, 1];
|
|
25
|
-
result.error = Error.required;
|
|
26
|
-
result.sessionRequiredTill = oneDay_1;
|
|
27
|
-
return [3 /*break*/, 4];
|
|
28
|
-
case 1:
|
|
29
|
-
if (!(isSessionRequiredForThisTypeAction === "thread")) return [3 /*break*/, 4];
|
|
30
|
-
if (!(sessionForce == "true")) return [3 /*break*/, 2];
|
|
31
|
-
result.error = Error.required;
|
|
32
|
-
result.sessionRequiredTill = oneDay_1;
|
|
33
|
-
return [3 /*break*/, 4];
|
|
34
|
-
case 2: return [4 /*yield*/, threadCache.get(threadId, -1)];
|
|
35
|
-
case 3:
|
|
36
|
-
thread_1 = _d.sent();
|
|
37
|
-
sessionRequired = Object.fromEntries((_c = (_b = thread_1 === null || thread_1 === void 0 ? void 0 : thread_1.sessionRequired) === null || _b === void 0 ? void 0 : _b.map(function (userId, index) {
|
|
38
|
-
var _a, _b;
|
|
39
|
-
return [
|
|
40
|
-
userId,
|
|
41
|
-
(_b = (_a = thread_1 === null || thread_1 === void 0 ? void 0 : thread_1.sessionRequiredTill) === null || _a === void 0 ? void 0 : _a[index]) !== null && _b !== void 0 ? _b : oneDay_1
|
|
42
|
-
];
|
|
43
|
-
})) !== null && _c !== void 0 ? _c : []);
|
|
44
|
-
if (sessionRequired[userId] !== undefined &&
|
|
45
|
-
Date.now() < sessionRequired[userId]) {
|
|
10
|
+
export const handleSessionToken = async (userId, threadId, type, action, sessionToken) => {
|
|
11
|
+
const result = { sessionId: "" };
|
|
12
|
+
if (sessionJwtSecret) {
|
|
13
|
+
if (!sessionToken) {
|
|
14
|
+
const isSessionRequiredForThisTypeAction = sessionRequirement[type]?.[action];
|
|
15
|
+
if (isSessionRequiredForThisTypeAction !== undefined) {
|
|
16
|
+
const oneDay = Date.now() + 1000 * 60 * 60 * 24; // 1 day
|
|
17
|
+
if (isSessionRequiredForThisTypeAction === "always") {
|
|
46
18
|
result.error = Error.required;
|
|
47
|
-
result.sessionRequiredTill =
|
|
19
|
+
result.sessionRequiredTill = oneDay;
|
|
48
20
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
session = verify(sessionToken, sessionJwtSecret + "", {
|
|
54
|
-
algorithms: ["HS512"],
|
|
55
|
-
ignoreExpiration: true
|
|
56
|
-
});
|
|
57
|
-
now = Date.now();
|
|
58
|
-
if (session.participants.includes(userId) &&
|
|
59
|
-
session.startTime <= now &&
|
|
60
|
-
now <= session.endTime) {
|
|
61
|
-
result.sessionId = session.id;
|
|
21
|
+
else if (isSessionRequiredForThisTypeAction === "thread") {
|
|
22
|
+
if (sessionForce == "true") {
|
|
23
|
+
result.error = Error.required;
|
|
24
|
+
result.sessionRequiredTill = oneDay;
|
|
62
25
|
}
|
|
63
26
|
else {
|
|
64
|
-
|
|
27
|
+
const thread = await threadCache.get(threadId, -1); // ttl = -1 will force the cache to fetch from db
|
|
28
|
+
const sessionRequired = Object.fromEntries(thread?.sessionRequired?.map((userId, index) => [
|
|
29
|
+
userId,
|
|
30
|
+
thread?.sessionRequiredTill?.[index] ?? oneDay
|
|
31
|
+
]) ?? []);
|
|
32
|
+
if (sessionRequired[userId] !== undefined &&
|
|
33
|
+
Date.now() < sessionRequired[userId]) {
|
|
34
|
+
result.error = Error.required;
|
|
35
|
+
result.sessionRequiredTill = sessionRequired[userId];
|
|
36
|
+
}
|
|
65
37
|
}
|
|
66
38
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
try {
|
|
43
|
+
const session = verify(sessionToken, sessionJwtSecret + "", {
|
|
44
|
+
algorithms: ["HS512"],
|
|
45
|
+
ignoreExpiration: true
|
|
46
|
+
});
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (session.participants.includes(userId) &&
|
|
49
|
+
session.startTime <= now &&
|
|
50
|
+
now <= session.endTime) {
|
|
51
|
+
result.sessionId = session.id;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
73
54
|
result.error = Error.invalid;
|
|
74
55
|
}
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.error("session token verification failed. userId=" +
|
|
60
|
+
userId +
|
|
61
|
+
", error=" +
|
|
62
|
+
err);
|
|
63
|
+
result.error = Error.invalid;
|
|
64
|
+
}
|
|
77
65
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
};
|
package/build/lib/threadCache.js
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
|
-
import { __awaiter, __generator } from "tslib";
|
|
2
1
|
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
|
|
3
2
|
import { cache } from "./cache";
|
|
4
3
|
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
get:
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
case 1:
|
|
20
|
-
threadResult = _a.sent();
|
|
21
|
-
return [2 /*return*/, threadResult.Item
|
|
22
|
-
? unmarshall(threadResult.Item)
|
|
23
|
-
: undefined];
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
}); }, ttl)];
|
|
27
|
-
case 1: return [2 /*return*/, _a.sent()];
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}); }
|
|
4
|
+
const dynamodb = new DynamoDBClient();
|
|
5
|
+
const _threadCache = cache(100, 60 * 60000);
|
|
6
|
+
export const threadCache = {
|
|
7
|
+
get: async (threadId, ttl) => {
|
|
8
|
+
return await _threadCache.get(threadId, async () => {
|
|
9
|
+
const threadResult = await dynamodb.send(new GetItemCommand({
|
|
10
|
+
TableName: process.env.THREAD_TABLE_NAME,
|
|
11
|
+
Key: marshall({ id: threadId })
|
|
12
|
+
}));
|
|
13
|
+
return threadResult.Item
|
|
14
|
+
? unmarshall(threadResult.Item)
|
|
15
|
+
: undefined;
|
|
16
|
+
}, ttl);
|
|
17
|
+
}
|
|
31
18
|
};
|
package/build/lib/types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const typeToAllowedActionsMap = {
|
|
2
2
|
text: ["new", "edit"],
|
|
3
3
|
image: ["new", "edit"],
|
|
4
4
|
control: [
|
|
@@ -15,7 +15,7 @@ export var typeToAllowedActionsMap = {
|
|
|
15
15
|
/**
|
|
16
16
|
* Store the message type and actions for which the session token is required
|
|
17
17
|
*/
|
|
18
|
-
export
|
|
18
|
+
export const sessionRequirement = {
|
|
19
19
|
text: { new: "thread", edit: "thread" },
|
|
20
20
|
image: { new: "thread", edit: "thread" },
|
|
21
21
|
control: {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { __assign, __awaiter, __generator, __rest } from "tslib";
|
|
2
1
|
import { RouteBuilder } from "somod-http-extension";
|
|
3
2
|
import { UserProviderMiddlewareKey } from "../../lib";
|
|
4
3
|
import { putMessage, validateIncomingMessage } from "../../lib/message";
|
|
@@ -7,97 +6,85 @@ import { QueryCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
|
7
6
|
import { convertToAttr, unmarshall } from "@aws-sdk/util-dynamodb";
|
|
8
7
|
import { handleSessionToken } from "../../lib/sessionUtil";
|
|
9
8
|
import { getUserIdFromEvent } from "../../lib/getUserIdFromEvent";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
sessionIdResult
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: {})))];
|
|
43
|
-
case 3:
|
|
44
|
-
message = _b.sent();
|
|
45
|
-
return [2 /*return*/, {
|
|
46
|
-
statusCode: 200,
|
|
47
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
id: message.id,
|
|
50
|
-
seqNo: message.seqNo,
|
|
51
|
-
sentAt: message.sentAt,
|
|
52
|
-
from: message.from
|
|
53
|
-
})
|
|
54
|
-
}];
|
|
55
|
-
}
|
|
9
|
+
const dynamodb = new DynamoDBClient();
|
|
10
|
+
const builder = new RouteBuilder();
|
|
11
|
+
const postMessageHandler = async (request, event) => {
|
|
12
|
+
const userId = event.somodMiddlewareContext.get(UserProviderMiddlewareKey);
|
|
13
|
+
const messageValidationError = await validateIncomingMessage(request.body, userId);
|
|
14
|
+
if (messageValidationError) {
|
|
15
|
+
return messageValidationError;
|
|
16
|
+
}
|
|
17
|
+
const sessionIdResult = await handleSessionToken(userId, request.body.threadId, request.body.type, request.body.action, request.body.sessionToken);
|
|
18
|
+
if (sessionIdResult.error) {
|
|
19
|
+
return {
|
|
20
|
+
statusCode: 400,
|
|
21
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
message: sessionIdResult.error,
|
|
24
|
+
threadId: request.body.threadId,
|
|
25
|
+
sessionRequiredTill: sessionIdResult.sessionRequiredTill
|
|
26
|
+
})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
30
|
+
const { sessionToken, ...msg } = request.body;
|
|
31
|
+
const actions = ["sessionStart", "sessionExtend", "sessionEnd"];
|
|
32
|
+
const message = await putMessage(process.env.MESSAGE_BOX_TABLE_NAME + "", userId, {
|
|
33
|
+
...msg,
|
|
34
|
+
sessionId: sessionIdResult.sessionId,
|
|
35
|
+
id: v1uuid().split("-").join(""),
|
|
36
|
+
from: userId,
|
|
37
|
+
sentAt: Date.now(),
|
|
38
|
+
...(actions.includes(request.body.action)
|
|
39
|
+
? { sessionToken: sessionToken }
|
|
40
|
+
: {})
|
|
56
41
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"Content-Type": "application/json; charset=utf-8"
|
|
95
|
-
},
|
|
96
|
-
body: JSON.stringify(messages)
|
|
97
|
-
}];
|
|
98
|
-
}
|
|
42
|
+
return {
|
|
43
|
+
statusCode: 200,
|
|
44
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
id: message.id,
|
|
47
|
+
seqNo: message.seqNo,
|
|
48
|
+
sentAt: message.sentAt,
|
|
49
|
+
from: message.from
|
|
50
|
+
})
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const syncMessagesHandler = async (request, event) => {
|
|
54
|
+
const userId = getUserIdFromEvent(event);
|
|
55
|
+
const queryCommandInput = {
|
|
56
|
+
TableName: process.env.MESSAGE_BOX_TABLE_NAME + "",
|
|
57
|
+
KeyConditionExpression: "#userId = :userId",
|
|
58
|
+
ExpressionAttributeNames: {
|
|
59
|
+
"#userId": "userId"
|
|
60
|
+
},
|
|
61
|
+
ExpressionAttributeValues: {
|
|
62
|
+
":userId": convertToAttr(userId)
|
|
63
|
+
},
|
|
64
|
+
Limit: 100
|
|
65
|
+
};
|
|
66
|
+
if (request.parameters.query.from) {
|
|
67
|
+
queryCommandInput.KeyConditionExpression += " AND #seqNo >= :seqNo";
|
|
68
|
+
queryCommandInput.ExpressionAttributeNames["#seqNo"] = "seqNo";
|
|
69
|
+
queryCommandInput.ExpressionAttributeValues[":seqNo"] = {
|
|
70
|
+
N: request.parameters.query.from
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const queryCommand = new QueryCommand(queryCommandInput);
|
|
74
|
+
const result = await dynamodb.send(queryCommand);
|
|
75
|
+
const messages = (result.Items || []).map(item => {
|
|
76
|
+
const message = unmarshall(item);
|
|
77
|
+
delete message.userId;
|
|
78
|
+
return message;
|
|
99
79
|
});
|
|
100
|
-
|
|
80
|
+
return {
|
|
81
|
+
statusCode: 200,
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(messages)
|
|
86
|
+
};
|
|
87
|
+
};
|
|
101
88
|
builder.add("/post-message", "POST", postMessageHandler);
|
|
102
89
|
builder.add("/sync-messages", "GET", syncMessagesHandler);
|
|
103
90
|
export default builder.getHandler();
|