feat: handle template conflict error

This commit is contained in:
2024-12-03 11:32:21 +00:00
parent cb8822e280
commit ee776f4c6e
7 changed files with 116 additions and 62 deletions

View File

@@ -56,36 +56,21 @@ func fetchTemplate(c echo.Context) error {
return c.JSON(http.StatusOK, template)
}
func createOrUpdateTemplate(c echo.Context) error {
mgr := templateManagerFrom(c)
exists, err := mgr.hasTemplate(c.Request().Context(), c.Param("templateName"))
if err != nil {
return err
}
if !exists {
return createTemplate(c)
}
var body postTemplateRequestBody
err = json.NewDecoder(c.Request().Body).Decode(&body)
if err != nil {
return err
}
if body.ImageTag != nil || body.BuildArgs != nil {
return buildTemplate(c, body)
}
return updateTemplate(c, body)
}
func createTemplate(c echo.Context) error {
mgr := templateManagerFrom(c)
name := c.Param("templateName")
mgr := templateManagerFrom(c)
exists, err := mgr.hasTemplate(c.Request().Context(), name)
if err != nil {
return err
}
if exists {
return echo.NewHTTPError(http.StatusConflict)
}
var body createTemplateRequestBody
err := json.NewDecoder(c.Request().Body).Decode(&body)
if err != nil {
if err = json.NewDecoder(c.Request().Body).Decode(&body); err != nil {
return err
}
@@ -101,6 +86,29 @@ func createTemplate(c echo.Context) error {
return c.JSON(http.StatusOK, createdTemplate)
}
func updateOrBuildTemplate(c echo.Context) error {
mgr := templateManagerFrom(c)
exists, err := mgr.hasTemplate(c.Request().Context(), c.Param("templateName"))
if err != nil {
return err
}
if !exists {
return echo.NewHTTPError(http.StatusNotFound)
}
var body postTemplateRequestBody
err = json.NewDecoder(c.Request().Body).Decode(&body)
if err != nil {
return err
}
if body.ImageTag != nil || body.BuildArgs != nil {
return buildTemplate(c, body)
}
return updateTemplate(c, body)
}
func updateTemplate(c echo.Context, body postTemplateRequestBody) error {
name := c.Param("templateName")
mgr := templateManagerFrom(c)

View File

@@ -9,7 +9,8 @@ func DefineRoutes(g *echo.Group, services service.Services) {
g.Use(newTemplateManagerMiddleware(services))
g.GET("/templates", fetchAllTemplates)
g.GET("/templates/:templateName", fetchTemplate, validateTemplateName)
g.POST("/templates/:templateName", createOrUpdateTemplate, validateTemplateName)
g.PUT("/templates/:templateName", createTemplate, validateTemplateName)
g.POST("/templates/:templateName", updateOrBuildTemplate, validateTemplateName)
g.DELETE("/templates/:templateName", deleteTemplate, validateTemplateName)
g.GET("/templates/:templateName/:filePath", fetchTemplateFile, validateTemplateName, validateTemplateFilePath)
g.POST("/templates/:templateName/:filePath", updateTemplateFile, validateTemplateName, validateTemplateFilePath)

View File

@@ -41,6 +41,7 @@ type buildTemplateOptions struct {
}
var errTemplateNotFound = errors.New("template not found")
var errTemplateExists = errors.New("template already exists")
var errBaseTemplateNotFound = errors.New("base template not found")
var errTemplateFileNotFound = errors.New("template file not found")

View File

@@ -1,6 +1,6 @@
import { promiseOrThrow } from "./lib/errors";
interface ApiErrorResponse {
interface ApiErrorDetails {
code: string;
error: string;
}
@@ -8,12 +8,13 @@ interface ApiErrorResponse {
const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
const API_ERROR_WORKSPACE_EXISTS = "WORKSPACE_EXISTS";
enum ApiError {
NotFound = "NOT_FOUND",
BadRequest = "BAD_REQUEST",
Internal = "INTERNAL",
Network = "NETWORK",
}
type ApiError =
| { type: "NOT_FOUND" }
| { type: "NETWORK" }
| { type: "BAD_REQUEST" }
| { type: "CONFLICT" }
| { type: "INTERNAL" }
| { type: "BAD_REQUEST"; details: ApiErrorDetails };
async function fetchApi(
url: URL | RequestInfo,
@@ -21,22 +22,27 @@ async function fetchApi(
): Promise<Response> {
const res = await promiseOrThrow(
fetch(`${import.meta.env.VITE_API_URL}/api${url}`, init),
() => ApiError.Network,
() => ({ type: "NETWORK" }),
);
if (res.status !== 200) {
switch (res.status) {
case 400:
throw await res.json();
throw {
type: "BAD_REQUEST",
details: await res.json(),
};
case 404:
throw ApiError.NotFound;
throw { type: "NOT_FOUND" };
case 409:
throw { type: "CONFLICT" };
default:
throw ApiError.Internal;
throw { type: "INTERNAL" };
}
}
return res;
}
function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
function isApiErrorResponse(error: unknown): error is ApiErrorDetails {
return (
error !== null &&
error !== undefined &&
@@ -49,8 +55,7 @@ function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
export {
API_ERROR_BAD_TEMPLATE,
API_ERROR_WORKSPACE_EXISTS,
ApiError,
fetchApi,
isApiErrorResponse,
};
export type { ApiErrorResponse };
export type { ApiError, ApiErrorDetails };

View File

@@ -6,7 +6,8 @@ import type {
TemplateImage,
BaseTemplate,
} from "./types";
import { fetchApi } from "@/api";
import { ApiError, fetchApi } from "@/api";
import { promiseOrThrow } from "@/lib/errors";
function useTemplates() {
return useSWR(
@@ -17,7 +18,7 @@ function useTemplates() {
}
function useTemplate(name: string) {
return useSWR(
return useSWR<Template, ApiError>(
["/templates", name],
async (): Promise<Template> =>
fetchApi(`/templates/${name}`).then((res) => res.json()),
@@ -45,7 +46,7 @@ function useDeleteTemplate() {
function useCreateTemplate() {
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<unknown | null>(null);
const [error, setError] = useState<ApiError | null>(null);
const { mutate } = useSWRConfig();
const createTemplate = useCallback(
@@ -60,14 +61,17 @@ function useCreateTemplate() {
}): Promise<Template | null> => {
try {
const res = await fetchApi(`/templates/${name}`, {
method: "POST",
method: "PUT",
body: JSON.stringify({ description, baseTemplate }),
headers: {
"Content-Type": "application/json",
},
});
const template: Template = await res.json();
const template = await promiseOrThrow<Template, ApiError>(
res.json(),
() => ({ type: "INTERNAL" }),
);
mutate(["/templates", name], template, {
populateCache: (newTemplate) => newTemplate,
revalidate: false,
@@ -75,7 +79,7 @@ function useCreateTemplate() {
return template;
} catch (err: unknown) {
setError(err);
setError(err as ApiError);
return null;
} finally {
setIsCreating(false);
@@ -88,10 +92,12 @@ function useCreateTemplate() {
}
function useTemplateFile(templateName: string, filePath: string) {
return useSWR(filePath ? ["/templates", templateName, filePath] : null, () =>
fetchApi(`/templates/${templateName}/${filePath}`).then((res) =>
res.text(),
),
return useSWR<string, ApiError>(
filePath ? ["/templates", templateName, filePath] : null,
() =>
fetchApi(`/templates/${templateName}/${filePath}`).then((res) =>
res.text(),
),
);
}

View File

@@ -30,6 +30,8 @@ import {
} from "@/components/ui/select";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import type { BaseTemplate } from "./types";
import { useEffect } from "react";
import { useToast } from "@/hooks/use-toast";
const NewTemplateForm = object({
baseTemplate: nonempty(string()),
@@ -75,7 +77,8 @@ function TemplateFormContainer() {
function TemplateForm({ baseTemplates }: { baseTemplates: BaseTemplate[] }) {
const router = useRouter();
const { createTemplate, isCreatingTemplate } = useCreateTemplate();
const { createTemplate, isCreatingTemplate, error } = useCreateTemplate();
const { toast } = useToast();
const form = useForm({
resolver: superstructResolver(NewTemplateForm),
disabled: isCreatingTemplate,
@@ -86,6 +89,36 @@ function TemplateForm({ baseTemplates }: { baseTemplates: BaseTemplate[] }) {
},
});
useEffect(() => {
if (!error) return;
switch (error.type) {
case "CONFLICT":
toast({
variant: "destructive",
title: "Template already exists",
description: "Please use another name for the template",
});
break;
case "NETWORK":
toast({
variant: "destructive",
title: "Failed to create the template",
description: "Network error",
});
break;
default:
toast({
variant: "destructive",
title: "Failed to create the template",
description: "Unknown error",
});
break;
}
}, [error, toast]);
async function onSubmit(values: Infer<typeof NewTemplateForm>) {
const createdTemplate = await createTemplate({
name: values.templateName,

View File

@@ -1,4 +1,4 @@
import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api";
import { API_ERROR_BAD_TEMPLATE } from "@/api";
import { CodeMirrorEditor } from "@/components/codemirror-editor";
import { Button } from "@/components/ui/button.tsx";
import {
@@ -12,6 +12,8 @@ import {
SidebarMenuItem,
SidebarProvider,
} from "@/components/ui/sidebar.tsx";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/hooks/use-toast";
import { cn } from "@/lib/utils";
import { Link, useRouter } from "@tanstack/react-router";
import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
@@ -25,10 +27,8 @@ import {
createTemplateEditorStore,
useTemplateEditorStore,
} from "./template-editor-store";
import type { Template } from "./types";
import { useToast } from "@/hooks/use-toast";
import { Toaster } from "@/components/ui/toaster";
import { TemplateEditorTopBar } from "./template-editor-top-bar";
import type { Template } from "./types";
function TemplateEditor() {
const { templateName, _splat } = templateEditorRoute.useParams();
@@ -43,13 +43,13 @@ function TemplateEditor() {
}
if (error || !template) {
if (error === ApiError.NotFound) {
if (error?.type === "NOT_FOUND") {
return <TemplateNotFound />;
}
let message = "";
switch (error) {
case ApiError.Network:
switch (error?.type) {
case "NETWORK":
message = "Having trouble contacting the server.";
break;
default:
@@ -178,11 +178,11 @@ function Editor() {
if (error || fileContent === undefined) {
let message = "";
switch (error) {
case ApiError.NotFound:
switch (error?.type) {
case "NOT_FOUND":
message = "This file does not exist in the template.";
break;
case ApiError.Network:
case "NETWORK":
message = "Having trouble contacting the server.";
break;
default: