feat: add importedAt-based repository sorting (#226)

* repositories: add importedAt sorting

* repositories: use tanstack table for repo list
This commit is contained in:
ARUNAVO RAY
2026-03-15 08:52:45 +05:30
committed by GitHub
parent 1bca7df5ab
commit 6f53a3ed41
20 changed files with 2370 additions and 40 deletions

View File

@@ -181,6 +181,7 @@ export const repositorySchema = z.object({
errorMessage: z.string().optional().nullable(),
destinationOrg: z.string().optional().nullable(),
metadata: z.string().optional().nullable(), // JSON string for metadata sync state
importedAt: z.coerce.date(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
@@ -395,6 +396,9 @@ export const repositories = sqliteTable("repositories", {
destinationOrg: text("destination_org"),
metadata: text("metadata"), // JSON string storing metadata sync state (issues, PRs, releases, etc.)
importedAt: integer("imported_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
@@ -410,6 +414,7 @@ export const repositories = sqliteTable("repositories", {
index("idx_repositories_organization").on(table.organization),
index("idx_repositories_is_fork").on(table.isForked),
index("idx_repositories_is_starred").on(table.isStarred),
index("idx_repositories_user_imported_at").on(table.userId, table.importedAt),
uniqueIndex("uniq_repositories_user_full_name").on(table.userId, table.fullName),
uniqueIndex("uniq_repositories_user_normalized_full_name").on(table.userId, table.normalizedFullName),
]);

View File

@@ -287,6 +287,7 @@ export async function getGithubRepositories({
lastMirrored: undefined,
errorMessage: undefined,
importedAt: new Date(),
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
}));
@@ -348,6 +349,7 @@ export async function getGithubStarredRepositories({
lastMirrored: undefined,
errorMessage: undefined,
importedAt: new Date(),
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
}));
@@ -492,6 +494,7 @@ export async function getGithubOrganizationRepositories({
lastMirrored: undefined,
errorMessage: undefined,
importedAt: new Date(),
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
}));

View File

@@ -28,6 +28,7 @@ function sampleRepo(overrides: Partial<GitRepo> = {}): GitRepo {
status: 'imported',
lastMirrored: undefined,
errorMessage: undefined,
importedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -56,6 +56,7 @@ export function normalizeGitRepoToInsert(
status: 'imported',
lastMirrored: repo.lastMirrored ?? null,
errorMessage: repo.errorMessage ?? null,
importedAt: repo.importedAt || new Date(),
createdAt: repo.createdAt || new Date(),
updatedAt: repo.updatedAt || new Date(),
};

View File

@@ -0,0 +1,68 @@
import { describe, expect, test } from "bun:test";
import type { Repository } from "@/lib/db/schema";
import { sortRepositories } from "@/lib/repository-sorting";
function makeRepo(overrides: Partial<Repository>): Repository {
return {
id: "id",
userId: "user-1",
configId: "config-1",
name: "repo",
fullName: "owner/repo",
normalizedFullName: "owner/repo",
url: "https://github.com/owner/repo",
cloneUrl: "https://github.com/owner/repo.git",
owner: "owner",
organization: null,
mirroredLocation: "",
isPrivate: false,
isForked: false,
forkedFrom: null,
hasIssues: true,
isStarred: false,
isArchived: false,
size: 1,
hasLFS: false,
hasSubmodules: false,
language: null,
description: null,
defaultBranch: "main",
visibility: "public",
status: "imported",
lastMirrored: null,
errorMessage: null,
destinationOrg: null,
metadata: null,
importedAt: new Date("2026-01-01T00:00:00.000Z"),
createdAt: new Date("2020-01-01T00:00:00.000Z"),
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
...overrides,
};
}
describe("sortRepositories", () => {
test("defaults to recently imported first", () => {
const repos = [
makeRepo({ id: "a", fullName: "owner/a", importedAt: new Date("2026-01-01T00:00:00.000Z") }),
makeRepo({ id: "b", fullName: "owner/b", importedAt: new Date("2026-03-01T00:00:00.000Z") }),
makeRepo({ id: "c", fullName: "owner/c", importedAt: new Date("2025-12-01T00:00:00.000Z") }),
];
const sorted = sortRepositories(repos, undefined);
expect(sorted.map((repo) => repo.id)).toEqual(["b", "a", "c"]);
});
test("supports name and updated sorting", () => {
const repos = [
makeRepo({ id: "a", fullName: "owner/zeta", updatedAt: new Date("2026-01-01T00:00:00.000Z") }),
makeRepo({ id: "b", fullName: "owner/alpha", updatedAt: new Date("2026-03-01T00:00:00.000Z") }),
makeRepo({ id: "c", fullName: "owner/middle", updatedAt: new Date("2025-12-01T00:00:00.000Z") }),
];
const nameSorted = sortRepositories(repos, "name-asc");
expect(nameSorted.map((repo) => repo.id)).toEqual(["b", "c", "a"]);
const updatedSorted = sortRepositories(repos, "updated-desc");
expect(updatedSorted.map((repo) => repo.id)).toEqual(["b", "a", "c"]);
});
});

View File

@@ -0,0 +1,40 @@
import type { Repository } from "@/lib/db/schema";
export type RepositorySortOrder =
| "imported-desc"
| "imported-asc"
| "updated-desc"
| "updated-asc"
| "name-asc"
| "name-desc";
function getTimestamp(value: Date | string | null | undefined): number {
if (!value) return 0;
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
export function sortRepositories(
repositories: Repository[],
sortOrder: string | undefined,
): Repository[] {
const order = (sortOrder ?? "imported-desc") as RepositorySortOrder;
return [...repositories].sort((a, b) => {
switch (order) {
case "imported-asc":
return getTimestamp(a.importedAt) - getTimestamp(b.importedAt);
case "updated-desc":
return getTimestamp(b.updatedAt) - getTimestamp(a.updatedAt);
case "updated-asc":
return getTimestamp(a.updatedAt) - getTimestamp(b.updatedAt);
case "name-asc":
return a.fullName.localeCompare(b.fullName, undefined, { sensitivity: "base" });
case "name-desc":
return b.fullName.localeCompare(a.fullName, undefined, { sensitivity: "base" });
case "imported-desc":
default:
return getTimestamp(b.importedAt) - getTimestamp(a.importedAt);
}
});
}