repos.ts (5.39 KB)
import { queryOptions } from "@tanstack/react-query";
import { notFound } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server";
import * as z from "zod";
import { Repo } from "@/db/repo";
import { getRepoDOStub } from "@/do/repo";
import { auth } from "@/lib/auth";
import { NotFoundError, UnauthorizedError } from "@/lib/errors";
import { Result } from "@/lib/result";

const getReposByOwnerInputSchema = z.object({
  owner: z.string().min(1),
});

export const getReposByOwnerFn = createServerFn({
  method: "GET",
})
  .inputValidator(getReposByOwnerInputSchema)
  .handler(async ({ data }) => {
    const result = await Repo.getByOwner({ owner: data.owner });
    return result.unwrapOrThrow({
      DatabaseError: "Database error occurred while fetching repositories.",
      ValidationError: `Invalid owner name (${data.owner}) provided.`,
    });
  });

export const getReposByOwnerOpts = (
  data: z.infer<typeof getReposByOwnerInputSchema>
) =>
  queryOptions({
    queryKey: ["reposByOwner", data.owner],
    queryFn: async () => await getReposByOwnerFn({ data }),
  });

const getRepoByOwnerAndNameSchema = z.object({
  owner: z.string().min(1),
  name: z.string().min(1),
});

export const getRepoByOwnerAndNameFn = createServerFn({
  method: "GET",
})
  .inputValidator(getRepoByOwnerAndNameSchema)
  .handler(async ({ data }) => {
    const result = await Repo.getByOwnerAndName(data);
    const repo = result.unwrapOrThrow({
      DatabaseError: "Database error occurred while fetching repository.",
    });
    if (!repo) {
      throw notFound();
    }
    return repo;
  });

export const getRepoByOwnerAndNameOpts = (
  data: z.infer<typeof getRepoByOwnerAndNameSchema>
) =>
  queryOptions({
    queryKey: ["repoByOwnerAndName", data.owner, data.name],
    queryFn: async () => await getRepoByOwnerAndNameFn({ data }),
  });

export const createRepoSchema = z.object({
  name: z
    .string()
    .min(1, { message: "Repository name is required" })
    .max(100, { message: "Repository name must be less than 100 characters" })
    .regex(/^[a-zA-Z0-9_-]+$/, {
      message:
        "Repository name can only contain letters, numbers, hyphens, and underscores",
    })
    .refine((val) => val.endsWith(".git") === false, {
      message: "Repository name cannot end with .git",
    }),
  description: z
    .string()
    .max(500, { message: "Description must be less than 500 characters" }),
  isPrivate: z.boolean(),
});

export const createRepoFn = createServerFn({
  method: "POST",
})
  .inputValidator(createRepoSchema)
  .handler(async ({ data }) => {
    const sessionResult = (
      await Result.tryCatchAsync(
        () => auth.api.getSession({ headers: getRequestHeaders() }),
        (e) => new UnauthorizedError({ cause: e })
      )
    ).andThen((session) =>
      session ? Result.ok(session) : Result.err(new UnauthorizedError())
    );

    const { user } = sessionResult.unwrapOrThrow({
      UnauthorizedError: "You must be logged in to create a repository.",
    });
    if (!user.username) {
      throw new Error("User does not have a username");
    }

    const result = await Repo.create({
      name: data.name,
      description: data.description.trim() || undefined,
      isPrivate: data.isPrivate,
      ownerId: user.id,
      owner: user.username,
    });

    const repo = result.unwrapOrThrow({
      DatabaseError: "Database error occurred while creating repository.",
    });

    const fullName = `${repo.owner}/${repo.name}`;

    const stub = getRepoDOStub(fullName);
    await stub.ensureRepoInitialized();

    return repo;
  });

export const updateRepoSchema = z.object({
  id: z.number(),
  description: z.string().optional(),
  isPrivate: z.boolean(),
});

export const updateRepoFn = createServerFn({
  method: "POST",
})
  .inputValidator(updateRepoSchema)
  .handler(async ({ data }) => {
    const sessionResult = (
      await Result.tryCatchAsync(
        () => auth.api.getSession({ headers: getRequestHeaders() }),
        (e) => new UnauthorizedError({ cause: e })
      )
    ).andThen((session) =>
      session ? Result.ok(session) : Result.err(new UnauthorizedError())
    );

    const { user } = sessionResult.unwrapOrThrow({
      UnauthorizedError: "You must be logged in to update a repository.",
    });

    const repoResult = (await Repo.getById({ id: data.id }))
      .andThen((repo) =>
        repo
          ? Result.ok(repo)
          : Result.err(new NotFoundError({ what: "Repository" }))
      )
      .andThen((repo) =>
        repo.ownerId === user.id
          ? Result.ok(repo)
          : Result.err(new UnauthorizedError())
      );

    const repo = repoResult.unwrapOrThrow({
      DatabaseError: "Database error occurred.",
      NotFoundError: `Repository not found (ID: ${data.id})`,
      UnauthorizedError: `You do not have permission to update this repository (ID: ${data.id})`,
      ValidationError: `Invalid repository ID (ID: ${data.id}) provided.`,
    });

    const updatedRepoResult = await Repo.update({
      id: repo.id,
      description: data.description?.trim() || undefined,
      isPrivate: data.isPrivate,
      ownerId: repo.ownerId,
    });
    const updatedRepo = updatedRepoResult.unwrapOrThrow({
      DatabaseError: "Database error occurred while updating repository.",
      ValidationError: `Invalid repository ID (ID: ${data.id}) provided.`,
    });
    return updatedRepo;
  });