Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update db entries on demand #36

Merged
merged 6 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
DATABASE_URL=
GITHUB_API_TOKEN=
GITHUB_BEARER_TOKEN=
GITHUB_WEBHOOK_SECRET=
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ bun studio
```

> [!important]
> When using any proxy tool, make sure to append the URL with `/api/ghwh`
> When using any proxy tool, make sure to append the URL with
> `/api/github/webhook`

## Dashboard

Expand All @@ -69,4 +70,3 @@ into issues, have questions or suggestions, feel free to open an issue.

- [ ] [Authenticated dashboard](https://github.com/oscarvz/hono-github-tracker/issues/14):
we shouldn't expose the dashboard to the public.
s
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"build": "vite build --mode client && vite build",
"preview": "bun run build && wrangler pages dev",
"deploy": "$npm_execpath run build && wrangler pages deploy",
"studio": "bunx @fiberplane/studio@beta",
"studio": "bunx @fiberplane/studio@canary",
"db:generate": "bun drizzle-kit generate",
"db:migrate": "bun drizzle-kit migrate",
"lint": "biome lint",
Expand Down
98 changes: 92 additions & 6 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Hono } from "hono";
import { and, eq } from "drizzle-orm";
import { type Context, Hono } from "hono";
import { bearerAuth } from "hono/bearer-auth";

import { events, repositories, users } from "../db";
import { events, type EventInsert, repositories, users } from "../db";
import { githubApiMiddleware, githubWebhooksMiddleware } from "../middleware";
import type { HonoEnv } from "../types";

const api = new Hono<HonoEnv>();

api.use("/ghwh", githubApiMiddleware);
api.use("/ghwh", githubWebhooksMiddleware);
api.use("/github/*", githubApiMiddleware);
api.use("/github/webhook", githubWebhooksMiddleware);

api.post("/ghwh", async (c) => {
api.post("/github/webhook", async (c) => {
const db = c.var.db;
const webhooks = c.var.webhooks;
const fetchUserById = c.var.fetchUserById;
Expand All @@ -18,7 +20,6 @@ api.post("/ghwh", async (c) => {
["issues.opened", "star.created", "watch.started"],
async ({ payload, name }) => {
const userId = payload.sender.id;
const user = await fetchUserById(userId);

try {
await db
Expand All @@ -43,6 +44,8 @@ api.post("/ghwh", async (c) => {
}

try {
const user = await fetchUserById(userId);

await db
.insert(users)
.values({
Expand Down Expand Up @@ -81,4 +84,87 @@ api.post("/ghwh", async (c) => {
);
});

api.get(
// Params will be replaced by repo id once dashboard is implemented.
"/github/:owner/:repo",
bearerAuth({
// Basic bearer token auth for now until the dashboard is implemented.
verifyToken: async (token, c: Context<HonoEnv>) =>
token === c.env.GITHUB_BEARER_TOKEN,
}),
async (c) => {
const db = c.var.db;
const fetchUsersWithInteractions = c.var.fetchUsersWithInteractions;
const owner = c.req.param("owner");
const repo = c.req.param("repo");

const countQuery = c.req.query("count");
const count = countQuery ? Number.parseInt(countQuery, 10) : 50;

try {
const { stargazers, watchers, repoId } = await fetchUsersWithInteractions(
{
count,
owner,
repo,
},
);

const usersWithInteractions = stargazers.users.concat(watchers.users);
await db.insert(users).values(usersWithInteractions).onConflictDoNothing({
target: users.id,
});

const stargazerEvents: Array<EventInsert> = stargazers.users.map(
(user) => ({
eventName: "star",
eventAction: "created",
repoId,
userId: user.id,
}),
);
if (stargazerEvents.length > 0) {
await db
.insert(events)
.values(stargazerEvents)
.onConflictDoNothing({
target: users.id,
where: and(
eq(events.eventName, "star"),
eq(events.eventAction, "created"),
eq(events.repoId, repoId),
),
});
}

const watcherEvents: Array<EventInsert> = watchers.users.map((user) => ({
eventName: "watch",
eventAction: "started",
repoId,
userId: user.id,
}));
if (watcherEvents.length > 0) {
await db
.insert(events)
.values(watcherEvents)
.onConflictDoNothing({
target: users.id,
where: and(
eq(events.eventName, "watch"),
eq(events.eventAction, "started"),
eq(events.repoId, repoId),
),
});
}

return c.text("Updated stargazers and watchers!");
} catch (error) {
return c.text(
`Error fetching and storing users and events: ${error}`,
500,
);
}
},
);

export default api;
3 changes: 2 additions & 1 deletion src/client/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ export function Dashboard({ repositories }: DashboardProps) {
<Grid>
{repositories.map(
({
id,
description,
fullName,
latestStar,
stargazersCount,
watchersCount,
}) => (
<GridCol key={fullName} span="content">
<GridCol key={id} span="content">
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Text fw={500}>{fullName}</Text>

Expand Down
2 changes: 2 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ export const eventsUser = relations(events, ({ one }) => ({
}));

export type Repository = typeof repositories.$inferSelect;
export type UserInsert = typeof users.$inferInsert;
export type EventInsert = typeof events.$inferInsert;
59 changes: 58 additions & 1 deletion src/middleware/githubApiMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Octokit } from "@octokit/core";
import { createMiddleware } from "hono/factory";

import type { FetchUserById, HonoEnv } from "../types";
import type {
FetchUserById,
FetchUsersWithInteractions,
HonoEnv,
} from "../types";

let octokitInstance: Octokit | undefined;

Expand All @@ -22,6 +26,58 @@ export const githubApiMiddleware = createMiddleware<HonoEnv, "ghws">(
const githubToken = c.env.GITHUB_API_TOKEN;
const octokit = getOctokitInstance(githubToken);

const fetchUsersWithInteractions: FetchUsersWithInteractions = async ({
owner,
repo,
count,
}) => {
try {
const { repository } = await octokit.graphql<{
repository: ReturnType<FetchUsersWithInteractions>;
}>(
`
query lastUsersWithInteractions($owner: String!, $repo: String!, $count: Int!) {
repository(owner: $owner, name: $repo) {
repoId: databaseId,
stargazers(last: $count) {
users: nodes {
id: databaseId,
avatar: avatarUrl,
handle: login,
name,
location,
company,
email,
twitterUsername
}
}
watchers(last: $count) {
users: nodes {
id: databaseId,
avatar: avatarUrl,
handle: login,
name,
location,
company,
email,
twitterUsername
}
}
}
}
`,
{
count,
owner,
repo,
},
);
return repository;
} catch (error) {
throw new Error(`Github API: error fetching stargazers: ${error}`);
}
};

const fetchUserById: FetchUserById = async (id) => {
try {
const { data } = await octokit.request("GET /user/{id}", { id });
Expand All @@ -31,6 +87,7 @@ export const githubApiMiddleware = createMiddleware<HonoEnv, "ghws">(
}
};

c.set("fetchUsersWithInteractions", fetchUsersWithInteractions);
c.set("fetchUserById", fetchUserById);

await next();
Expand Down
9 changes: 4 additions & 5 deletions src/middleware/githubWebhooksMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Webhooks } from "@octokit/webhooks";
import { createMiddleware } from "hono/factory";

import { type HonoEnv, isWebhookEventName } from "../types";
import type { HonoEnv, WebhookEventName } from "../types";

let webhooks: Webhooks | undefined;

Expand All @@ -28,11 +28,10 @@ export const githubWebhooksMiddleware = createMiddleware<HonoEnv, "/ghws">(

const id = c.req.header("x-github-delivery");
const signature = c.req.header("x-hub-signature-256");
const name = c.req.header("x-github-event");
const name = c.req.header("x-github-event") as WebhookEventName;

const isEventName = isWebhookEventName(name);
if (!(id && isEventName && signature)) {
return c.text("Invalid request", 403);
if (!(id && name && signature)) {
return c.text("Invalid webhook request", 403);
}

const payload = await c.req.text();
Expand Down
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ type Variables = {
webhooks: Webhooks;
db: Db;
fetchUserById: FetchUserById;
fetchUsersWithInteractions: FetchUsersWithInteractions;
};

type EnvVars = {
DATABASE_URL: string;
GITHUB_API_TOKEN: string;
GITHUB_WEBHOOK_SECRET: string;
GITHUB_BEARER_TOKEN: string;
};

export type HonoEnv = {
Expand All @@ -27,6 +29,20 @@ type GithubUser = Endpoints["GET /users/{username}"]["response"]["data"];

export type FetchUserById = (id: GithubUser["id"]) => Promise<GithubUser>;

export type FetchUsersWithInteractions = ({
owner,
repo,
count,
}: {
owner: string;
repo: string;
count: number;
}) => Promise<{
repoId: number;
stargazers: { users: Array<schema.UserInsert> };
watchers: { users: Array<schema.UserInsert> };
}>;

// Octokit isn't exporting this particular type, so we extract it from the
// `verifyAndReceive` method.
export type WebhookEventName = Parameters<
Expand Down