-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeed.ts
173 lines (158 loc) · 5.2 KB
/
feed.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// write lambda function to get posts for a user feed with tags of inside user
import { lambda, sdk } from '@pulumi/aws';
import { getToken } from '../../auth';
import type { IComment } from '#tables/tables/comment';
import type { IPost } from '#tables/tables/post';
import type { CUser, IUser } from '#tables/tables/user';
import type { lambdaEvent } from '#utils/util';
import { CommentsTable, PostsTable, TagsTable, UsersTable } from '#tables/index';
import {
deconstruct,
postEpoch,
currentEndpoint,
CUSTOM_ERROR_CODES,
makeCustomError,
decodeJWT,
populateResponse,
STATUS_CODES,
} from '#utils/util';
/**
* The feed lambda
* @description
* - The lambda is used to get the feed of a user
* - The lambda is triggered by a GET request to /posts/feed/{postID}
*
* The postID is used to get the next 10 posts from the feed and use null to get the first 10 posts
* @see https://www.pulumi.com/docs/guides/crosswalk/aws/api-gateway/#lambda-request-handling
*/
export const feed = new lambda.CallbackFunction<
lambdaEvent,
{
body: string;
statusCode: number;
}
>('feed', {
runtime: lambda.Runtime.NodeJS16dX,
callback: async event => {
const userID = decodeJWT(getToken(event)).data?.id;
let { postID }: any = event.pathParameters!;
if (postID === 'null') postID = null;
const client = new sdk.DynamoDB.DocumentClient(currentEndpoint);
try {
const { Items } = await client
.query({
TableName: UsersTable.get(),
IndexName: 'userID',
KeyConditionExpression: 'userID = :id',
ExpressionAttributeValues: {
':id': userID,
},
})
.promise();
if (!Items?.length) {
return populateResponse(
STATUS_CODES.NOT_FOUND,
makeCustomError('User not found', CUSTOM_ERROR_CODES.USER_NOT_FOUND),
);
}
const { tags } = Items[0] as IUser & Pick<CUser, 'tags'>;
// batch get all the tags
const promises = [];
for (const tag of tags) {
promises.push(
client
.query({
TableName: TagsTable.get(),
IndexName: 'tag-index',
KeyConditionExpression: 'tag = :tag',
ExpressionAttributeValues: {
':tag': tag,
},
// since a user can have upto 5 tags and each tag can have upto 1000s of posts so we need to limit the query as the user cannot read all the posts
Limit: 500,
})
.promise(),
);
}
const results = await Promise.all(promises);
const postsIdsTags = results.flatMap(result => result.Items);
interface tagItem {
postID: string;
tag: string;
}
// unique postIds using set
const uniquePosts = [...new Set(postsIdsTags as tagItem[])];
// use postID to get the next postIds from the array
let nextPostIds: tagItem[] | null = null;
if (postID) {
const idx = uniquePosts.indexOf(postID);
nextPostIds = uniquePosts.slice(idx + 1);
}
// only 20 posts
const postsToFeed = nextPostIds?.slice(0, 20) ?? uniquePosts.slice(0, 20);
const postPromises = [];
for (const post of postsToFeed) {
postPromises.push(
client
.query({
TableName: PostsTable.get(),
IndexName: 'postID',
KeyConditionExpression: 'postID = :postID',
ExpressionAttributeValues: {
':postID': post.postID,
},
})
.promise(),
);
}
const postResults = await Promise.all(postPromises);
const posts = [];
for (const postResult of postResults) {
if (!postResult.Items?.[0]) continue;
// get all the tags for the post
const tags = postsIdsTags
.filter(item => item?.postID === postResult.Items?.[0]?.postID)
.map(item => item?.tag ?? '');
const { Items: comments } = await client
.query({
TableName: CommentsTable.get(),
IndexName: 'postID',
KeyConditionExpression: 'postID = :postID',
ExpressionAttributeValues: {
':postID': postResult.Items[0]?.postID,
},
})
.promise();
posts.push(
...postResult.Items.map(
item =>
({
...item,
tag: tags,
comments:
comments?.map((com: IComment) => ({
commentID: com.commentID,
content: com.content,
userID: com.userID,
})) ?? [],
} as IPost),
),
);
}
if (!posts.length) return populateResponse(STATUS_CODES.OK, []);
return populateResponse(
STATUS_CODES.OK,
posts.map(post => {
const { timestamp } = deconstruct(post.postID!, postEpoch);
return { ...post, createdAt: timestamp };
}),
);
} catch (error) {
console.error(error);
return populateResponse(
STATUS_CODES.INTERNAL_SERVER_ERROR,
makeCustomError('Internal Server Error', CUSTOM_ERROR_CODES.POST_ERROR),
);
}
},
});