Skip to content

Commit 61e187b

Browse files
authored
fix: search route (#378)
1 parent 6eced81 commit 61e187b

File tree

5 files changed

+105
-76
lines changed

5 files changed

+105
-76
lines changed

packages/app/app/components/RepoSearch.vue

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,47 @@
11
<script lang="ts" setup>
2+
import type { RepoNode } from "../../server/utils/types";
23
const search = useSessionStorage("search", "");
4+
const searchResults = ref<RepoNode[]>([]);
5+
const isLoading = ref(false);
36
7+
let activeController: AbortController | null = null;
48
const throttledSearch = useThrottle(search, 500, true, false);
59
6-
const { data, status } = useFetch("/api/repo/search", {
7-
query: computed(() => ({ text: throttledSearch.value })),
8-
immediate: !!throttledSearch.value,
9-
});
10+
watch(
11+
throttledSearch,
12+
async (newValue) => {
13+
activeController?.abort();
14+
searchResults.value = [];
15+
if (!newValue) {
16+
isLoading.value = false;
17+
return;
18+
}
19+
20+
const controller = new AbortController();
21+
activeController = controller;
22+
23+
isLoading.value = true;
24+
try {
25+
const response = await fetch(
26+
`/api/repo/search?text=${encodeURIComponent(newValue)}`,
27+
{ signal: activeController.signal },
28+
);
29+
const data = (await response.json()) as { nodes: RepoNode[] };
30+
if (activeController === controller) {
31+
searchResults.value = data.nodes ?? [];
32+
}
33+
} catch (err: any) {
34+
if (err.name !== "AbortError") {
35+
console.error(err);
36+
}
37+
} finally {
38+
if (activeController === controller) {
39+
isLoading.value = false;
40+
}
41+
}
42+
},
43+
{ immediate: false },
44+
);
1045
1146
const examples = [
1247
{
@@ -37,16 +72,12 @@ const examples = [
3772
];
3873
3974
const router = useRouter();
40-
4175
function openFirstResult() {
42-
if (data.value?.nodes[0]) {
43-
const { owner, name } = data.value.nodes[0];
76+
const [first] = searchResults.value;
77+
if (first) {
4478
router.push({
4579
name: "repo:details",
46-
params: {
47-
owner: owner.login,
48-
repo: name,
49-
},
80+
params: { owner: first.owner.login, repo: first.name },
5081
});
5182
}
5283
}
@@ -64,13 +95,13 @@ function openFirstResult() {
6495
@keydown.enter="openFirstResult()"
6596
/>
6697

67-
<div v-if="status === 'pending'" class="-mb-2 relative">
98+
<div v-if="isLoading" class="-mb-2 relative">
6899
<UProgress size="xs" class="absolute inset-x-0 top-0" />
69100
</div>
70101

71-
<div v-if="data?.nodes.length">
102+
<div v-if="searchResults.length">
72103
<RepoButton
73-
v-for="repo in data.nodes"
104+
v-for="repo in searchResults"
74105
:key="repo.id"
75106
:owner="repo.owner.login"
76107
:name="repo.name"
@@ -79,7 +110,7 @@ function openFirstResult() {
79110
</div>
80111

81112
<div
82-
v-else-if="search && status !== 'pending'"
113+
v-else-if="search && !isLoading"
83114
class="text-gray-500 p-12 text-center"
84115
>
85116
No repositories found

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"nuxt-shiki": "0.3.0",
3131
"octokit": "^4.0.2",
3232
"query-registry": "^3.0.1",
33+
"string-similarity": "^4.0.4",
3334
"unstorage": "^1.16.0",
3435
"vue": "^3.5.13",
3536
"vue-router": "^4.4.3",
Lines changed: 41 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,66 @@
11
import { z } from "zod";
2+
import { useOctokitApp } from "../../utils/octokit";
3+
import stringSimilarity from "string-similarity";
24

35
const querySchema = z.object({
46
text: z.string(),
57
});
68

79
export default defineEventHandler(async (event) => {
8-
const r2Binding = useBinding(event);
910
const request = toWebRequest(event);
1011
const signal = request.signal;
1112

1213
try {
1314
const query = await getValidatedQuery(event, (data) =>
1415
querySchema.parse(data),
1516
);
17+
if (!query.text) return { nodes: [] };
1618

17-
if (!query.text) {
18-
return { nodes: [] };
19-
}
20-
19+
const app = useOctokitApp(event);
2120
const searchText = query.text.toLowerCase();
21+
const matches: RepoNode[] = [];
2222

23-
// Internal pagination: iterate until uniqueNodes is filled or no more objects
24-
let cursor: string | undefined;
25-
const seen = new Set<string>();
26-
const uniqueNodes = [];
27-
const maxNodes = 10;
28-
let keepGoing = true;
23+
await app.eachRepository(async ({ repository }) => {
24+
if (signal.aborted) return;
25+
if (repository.private) return;
2926

30-
while (uniqueNodes.length < maxNodes && keepGoing && !signal.aborted) {
31-
const listResult = await r2Binding.list({
32-
prefix: usePackagesBucket.base,
33-
limit: 1000,
34-
cursor,
35-
});
36-
const { objects, truncated } = listResult;
37-
cursor = truncated ? listResult.cursor : undefined;
27+
const repoName = repository.name.toLowerCase();
28+
const ownerLogin = repository.owner.login.toLowerCase();
29+
30+
const nameScore = stringSimilarity.compareTwoStrings(
31+
repoName,
32+
searchText,
33+
);
34+
const ownerScore = stringSimilarity.compareTwoStrings(
35+
ownerLogin,
36+
searchText,
37+
);
3838

39-
for (const obj of objects) {
40-
const parts = parseKey(obj.key);
41-
const orgRepo = `${parts.org}/${parts.repo}`.toLowerCase();
42-
const applies =
43-
parts.org.toLowerCase().includes(searchText) ||
44-
parts.repo.toLowerCase().includes(searchText) ||
45-
orgRepo.includes(searchText);
46-
if (!applies) continue;
39+
matches.push({
40+
id: repository.id,
41+
name: repository.name,
42+
owner: {
43+
login: repository.owner.login,
44+
avatarUrl: repository.owner.avatar_url,
45+
},
46+
stars: repository.stargazers_count || 0,
47+
score: Math.max(nameScore, ownerScore),
48+
});
49+
});
4750

48-
const key = `${parts.org}/${parts.repo}`;
49-
if (!seen.has(key)) {
50-
seen.add(key);
51-
uniqueNodes.push({
52-
name: parts.repo,
53-
owner: {
54-
login: parts.org,
55-
avatarUrl: `https://github.com/${parts.org}.png`,
56-
},
57-
});
58-
if (uniqueNodes.length >= maxNodes) break;
59-
}
60-
}
51+
matches.sort((a, b) =>
52+
b.score !== a.score ? b.score - a.score : b.stars - a.stars,
53+
);
6154

62-
if (!truncated || uniqueNodes.length >= maxNodes) {
63-
keepGoing = false;
64-
}
65-
}
55+
const top = matches.slice(0, 10).map((node) => ({
56+
id: node.id,
57+
name: node.name,
58+
owner: node.owner,
59+
stars: node.stars,
60+
}));
6661

67-
return {
68-
nodes: uniqueNodes,
69-
};
62+
return { nodes: top };
7063
} catch (error) {
71-
console.error("Error in repository search:", error);
72-
return {
73-
nodes: [],
74-
error: true,
75-
message: (error as Error).message,
76-
};
64+
return { nodes: [], error: true, message: (error as Error).message };
7765
}
7866
});
79-
80-
function parseKey(key: string) {
81-
const parts = key.split(":");
82-
return {
83-
org: parts[2],
84-
repo: parts[3],
85-
};
86-
}

packages/app/server/utils/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,11 @@ export interface Cursor {
1414
timestamp: number;
1515
sha: string;
1616
}
17+
18+
export type RepoNode = {
19+
id: number;
20+
name: string;
21+
owner: { login: string; avatarUrl: string };
22+
stars: number;
23+
score: number;
24+
};

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)