Skip to content

Commit 99a0a0c

Browse files
committed
redesign backend
1 parent 93a53ed commit 99a0a0c

File tree

9 files changed

+880
-1846
lines changed

9 files changed

+880
-1846
lines changed

src/api/functions/linkry.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import {
2+
DynamoDBClient,
3+
QueryCommand,
4+
ScanCommand,
5+
} from "@aws-sdk/client-dynamodb";
6+
import { unmarshall } from "@aws-sdk/util-dynamodb";
7+
import { LinkryGroupUUIDToGroupNameMap } from "common/config.js";
8+
import { DelegatedLinkRecord, LinkRecord } from "common/types/linkry.js";
9+
import { FastifyRequest } from "fastify";
10+
11+
export async function fetchLinkEntry(
12+
slug: string,
13+
tableName: string,
14+
dynamoClient: DynamoDBClient,
15+
): Promise<LinkRecord | null> {
16+
const fetchLinkEntry = new QueryCommand({
17+
TableName: tableName,
18+
KeyConditionExpression: "slug = :slug",
19+
ExpressionAttributeValues: {
20+
":slug": { S: slug },
21+
},
22+
ScanIndexForward: false,
23+
});
24+
const result = await dynamoClient.send(fetchLinkEntry);
25+
if (!result.Items) {
26+
return null;
27+
}
28+
const unmarshalled = result.Items.map((x) => unmarshall(x));
29+
const ownerRecord = unmarshalled.filter((x) =>
30+
(x["access"] as string).startsWith("OWNER#"),
31+
)[0];
32+
return {
33+
...ownerRecord,
34+
access: unmarshalled
35+
.filter((x) => (x["access"] as string).startsWith("GROUP#"))
36+
.map((x) => (x["access"] as string).replace("GROUP#", "")),
37+
} as LinkRecord;
38+
}
39+
40+
export async function fetchOwnerRecords(
41+
username: string,
42+
tableName: string,
43+
dynamoClient: DynamoDBClient,
44+
) {
45+
const fetchAllOwnerRecords = new QueryCommand({
46+
TableName: tableName,
47+
IndexName: "AccessIndex",
48+
KeyConditionExpression: "#access = :accessVal",
49+
ExpressionAttributeNames: {
50+
"#access": "access",
51+
},
52+
ExpressionAttributeValues: {
53+
":accessVal": { S: `OWNER#${username}` },
54+
},
55+
ScanIndexForward: false,
56+
});
57+
58+
const result = await dynamoClient.send(fetchAllOwnerRecords);
59+
60+
// Process the results
61+
return (result.Items || []).map((item) => {
62+
const unmarshalledItem = unmarshall(item);
63+
64+
// Strip '#' from access field
65+
if (unmarshalledItem.access) {
66+
unmarshalledItem.access =
67+
unmarshalledItem.access.split("#")[1] || unmarshalledItem.access;
68+
}
69+
70+
return unmarshalledItem as LinkRecord;
71+
});
72+
}
73+
74+
export function extractUniqueSlugs(records: LinkRecord[]) {
75+
return Array.from(
76+
new Set(records.filter((item) => item.slug).map((item) => item.slug)),
77+
);
78+
}
79+
80+
export async function getGroupsForSlugs(
81+
slugs: string[],
82+
ownerRecords: LinkRecord[],
83+
tableName: string,
84+
dynamoClient: DynamoDBClient,
85+
) {
86+
const groupsPromises = slugs.map(async (slug) => {
87+
const groupQueryCommand = new QueryCommand({
88+
TableName: tableName,
89+
KeyConditionExpression:
90+
"#slug = :slugVal AND begins_with(#access, :accessVal)",
91+
ExpressionAttributeNames: {
92+
"#slug": "slug",
93+
"#access": "access",
94+
},
95+
ExpressionAttributeValues: {
96+
":slugVal": { S: slug },
97+
":accessVal": { S: "GROUP#" },
98+
},
99+
ScanIndexForward: false,
100+
});
101+
102+
try {
103+
const response = await dynamoClient.send(groupQueryCommand);
104+
const groupItems = (response.Items || []).map((item) => unmarshall(item));
105+
const groupIds = groupItems.map((item) =>
106+
item.access.replace("GROUP#", ""),
107+
);
108+
const originalRecord =
109+
ownerRecords.find((item) => item.slug === slug) || {};
110+
111+
return {
112+
...originalRecord,
113+
access: groupIds,
114+
};
115+
} catch (error) {
116+
console.error(`Error fetching groups for slug ${slug}:`, error);
117+
const originalRecord =
118+
ownerRecords.find((item) => item.slug === slug) || {};
119+
return {
120+
...originalRecord,
121+
access: [],
122+
};
123+
}
124+
});
125+
126+
const results = await Promise.allSettled(groupsPromises);
127+
128+
return results
129+
.filter((result) => result.status === "fulfilled")
130+
.map((result) => result.value);
131+
}
132+
133+
export function getFilteredUserGroups(request: FastifyRequest) {
134+
const userGroupMembershipIds = request.tokenPayload?.groups || [];
135+
return userGroupMembershipIds.filter((groupId) =>
136+
[...LinkryGroupUUIDToGroupNameMap.keys()].includes(groupId),
137+
);
138+
}
139+
140+
export async function getAllLinks(
141+
tableName: string,
142+
dynamoClient: DynamoDBClient,
143+
): Promise<LinkRecord[]> {
144+
const scan = new ScanCommand({
145+
TableName: tableName,
146+
});
147+
const response = await dynamoClient.send(scan);
148+
const unmarshalled = (response.Items || []).map((item) => unmarshall(item));
149+
const ownerRecords = unmarshalled.filter((x) =>
150+
(x["access"] as string).startsWith("OWNER#"),
151+
);
152+
const delegations = unmarshalled.filter(
153+
(x) => !(x["access"] as string).startsWith("OWNER#"),
154+
);
155+
const accessGroupMap: Record<string, string[]> = {}; // maps slug to access groups
156+
for (const deleg of delegations) {
157+
if (deleg.slug in accessGroupMap) {
158+
accessGroupMap[deleg.slug].push(deleg.access.replace("GROUP#", ""));
159+
} else {
160+
accessGroupMap[deleg.slug] = [deleg.access.replace("GROUP#", "")];
161+
}
162+
}
163+
return ownerRecords.map((x) => ({
164+
...x,
165+
access: accessGroupMap[x.slug],
166+
owner: x["access"].replace("OWNER#", ""),
167+
})) as LinkRecord[];
168+
}
169+
170+
export async function getDelegatedLinks(
171+
userGroups: string[],
172+
ownedSlugs: string[],
173+
tableName: string,
174+
dynamoClient: DynamoDBClient,
175+
): Promise<LinkRecord[]> {
176+
const groupQueries = userGroups.map(async (groupId) => {
177+
try {
178+
const groupQueryCommand = new QueryCommand({
179+
TableName: tableName,
180+
IndexName: "AccessIndex",
181+
KeyConditionExpression: "#access = :accessVal",
182+
ExpressionAttributeNames: {
183+
"#access": "access",
184+
},
185+
ExpressionAttributeValues: {
186+
":accessVal": { S: `GROUP#${groupId}` },
187+
},
188+
});
189+
190+
const response = await dynamoClient.send(groupQueryCommand);
191+
const items = (response.Items || []).map((item) => unmarshall(item));
192+
193+
// Get unique only
194+
const delegatedSlugs = [
195+
...new Set(
196+
items
197+
.filter((item) => item.slug && !ownedSlugs.includes(item.slug))
198+
.map((item) => item.slug),
199+
),
200+
];
201+
202+
if (!delegatedSlugs.length) return [];
203+
204+
// Fetch entry records
205+
const results = await Promise.all(
206+
delegatedSlugs.map(async (slug) => {
207+
try {
208+
const ownerQuery = new QueryCommand({
209+
TableName: tableName,
210+
KeyConditionExpression:
211+
"#slug = :slugVal AND begins_with(#access, :ownerVal)",
212+
ExpressionAttributeNames: {
213+
"#slug": "slug",
214+
"#access": "access",
215+
},
216+
ExpressionAttributeValues: {
217+
":slugVal": { S: slug },
218+
":ownerVal": { S: "OWNER#" },
219+
},
220+
});
221+
222+
const ownerResponse = await dynamoClient.send(ownerQuery);
223+
const ownerRecord = ownerResponse.Items
224+
? unmarshall(ownerResponse.Items[0])
225+
: null;
226+
227+
if (!ownerRecord) return null;
228+
const groupQuery = new QueryCommand({
229+
TableName: tableName,
230+
KeyConditionExpression:
231+
"#slug = :slugVal AND begins_with(#access, :groupVal)",
232+
ExpressionAttributeNames: {
233+
"#slug": "slug",
234+
"#access": "access",
235+
},
236+
ExpressionAttributeValues: {
237+
":slugVal": { S: slug },
238+
":groupVal": { S: "GROUP#" },
239+
},
240+
});
241+
242+
const groupResponse = await dynamoClient.send(groupQuery);
243+
const groupItems = (groupResponse.Items || []).map((item) =>
244+
unmarshall(item),
245+
);
246+
const groupIds = groupItems.map((item) =>
247+
item.access.replace("GROUP#", ""),
248+
);
249+
return {
250+
...ownerRecord,
251+
access: groupIds,
252+
owner: ownerRecord.access.replace("OWNER#", ""),
253+
} as DelegatedLinkRecord;
254+
} catch (error) {
255+
console.error(`Error processing delegated slug ${slug}:`, error);
256+
return null;
257+
}
258+
}),
259+
);
260+
261+
return results.filter(Boolean);
262+
} catch (error) {
263+
console.error(`Error processing group ${groupId}:`, error);
264+
return [];
265+
}
266+
});
267+
const results = await Promise.allSettled(groupQueries);
268+
const allDelegatedLinks = results
269+
.filter((result) => result.status === "fulfilled")
270+
.flatMap((result) => result.value);
271+
const slugMap = new Map();
272+
allDelegatedLinks.forEach((link) => {
273+
if (link && link.slug && !slugMap.has(link.slug)) {
274+
slugMap.set(link.slug, link);
275+
}
276+
});
277+
278+
return Array.from(slugMap.values());
279+
}

0 commit comments

Comments
 (0)