feat: server support custom name

This commit is contained in:
hehehai
2024-05-24 15:43:05 +08:00
parent 907dc0784f
commit fae180f157
21 changed files with 334 additions and 178 deletions

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Folder } from "lucide-react"; import { AlertTriangle, Folder } from "lucide-react";
import { useEffect } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -31,6 +31,15 @@ const AddTemplateSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
message: "Name is required", message: "Name is required",
}), }),
appName: z
.string()
.min(1, {
message: "App name is required",
})
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
message:
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(), description: z.string().optional(),
}); });
@@ -38,10 +47,12 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
interface Props { interface Props {
projectId: string; projectId: string;
projectName?: string;
} }
export const AddApplication = ({ projectId }: Props) => { export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation(); api.application.create.useMutation();
@@ -49,34 +60,34 @@ export const AddApplication = ({ projectId }: Props) => {
const form = useForm<AddTemplate>({ const form = useForm<AddTemplate>({
defaultValues: { defaultValues: {
name: "", name: "",
appName: `${projectName}-`,
description: "", description: "",
}, },
resolver: zodResolver(AddTemplateSchema), resolver: zodResolver(AddTemplateSchema),
}); });
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddTemplate) => { const onSubmit = async (data: AddTemplate) => {
await mutateAsync({ await mutateAsync({
name: data.name, name: data.name,
appName: data.appName,
description: data.description, description: data.description,
projectId, projectId,
}) })
.then(async () => { .then(async () => {
toast.success("Service Created"); toast.success("Service Created");
form.reset();
setVisible(false);
await utils.project.one.invalidate({ await utils.project.one.invalidate({
projectId, projectId,
}); });
}) })
.catch(() => { .catch((e) => {
toast.error("Error to create the service"); toast.error("Error to create the service");
}); });
}; };
return ( return (
<Dialog> <Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer space-x-3" className="w-full cursor-pointer space-x-3"
@@ -108,22 +119,40 @@ export const AddApplication = ({ projectId }: Props) => {
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4" className="grid w-full gap-4"
> >
<div className="flex flex-col gap-4"> <FormField
<FormField control={form.control}
control={form.control} name="name"
name="name" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>Name</FormLabel>
<FormLabel>Name</FormLabel> <FormControl>
<FormControl> <Input
<Input placeholder="Frontend" {...field} /> placeholder="Frontend"
</FormControl> {...field}
onChange={(e) => {
<FormMessage /> const val = e.target.value?.trim() || "";
</FormItem> form.setValue("appName", `${projectName}-${val}`);
)} field.onChange(val);
/> }}
</div> />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>AppName</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="description" name="description"

View File

@@ -29,8 +29,8 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Database } from "lucide-react"; import { Database, AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useMemo, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -58,6 +58,15 @@ const databasesUserDefaultPlaceholder: Record<
const baseDatabaseSchema = z.object({ const baseDatabaseSchema = z.object({
name: z.string().min(1, "Name required"), name: z.string().min(1, "Name required"),
appName: z
.string()
.min(1, {
message: "App name is required",
})
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
message:
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
databasePassword: z.string(), databasePassword: z.string(),
dockerImage: z.string(), dockerImage: z.string(),
description: z.string().nullable(), description: z.string().nullable(),
@@ -101,30 +110,52 @@ const mySchema = z.discriminatedUnion("type", [
.merge(baseDatabaseSchema), .merge(baseDatabaseSchema),
]); ]);
const databasesMap = {
postgres: {
icon: <PostgresqlIcon />,
label: "PostgreSQL",
},
mongo: {
icon: <MongodbIcon />,
label: "MongoDB",
},
mariadb: {
icon: <MariadbIcon />,
label: "MariaDB",
},
mysql: {
icon: <MysqlIcon />,
label: "MySQL",
},
redis: {
icon: <RedisIcon />,
label: "Redis",
},
};
type AddDatabase = z.infer<typeof mySchema>; type AddDatabase = z.infer<typeof mySchema>;
interface Props { interface Props {
projectId: string; projectId: string;
projectName?: string;
} }
export const AddDatabase = ({ projectId }: Props) => { export const AddDatabase = ({ projectId, projectName }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation(); const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const { mutateAsync: createMongo } = api.mongo.create.useMutation(); const redisMutation = api.redis.create.useMutation();
const mariadbMutation = api.mariadb.create.useMutation();
const { mutateAsync: createRedis } = api.redis.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation();
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
const form = useForm<AddDatabase>({ const form = useForm<AddDatabase>({
defaultValues: { defaultValues: {
type: "postgres", type: "postgres",
dockerImage: "", dockerImage: "",
name: "", name: "",
appName: `${projectName}-`,
databasePassword: "", databasePassword: "",
description: "", description: "",
databaseName: "", databaseName: "",
@@ -133,76 +164,65 @@ export const AddDatabase = ({ projectId }: Props) => {
resolver: zodResolver(mySchema), resolver: zodResolver(mySchema),
}); });
const type = form.watch("type"); const type = form.watch("type");
const activeMutation = {
useEffect(() => { postgres: postgresMutation,
form.reset({ mongo: mongoMutation,
type: "postgres", redis: redisMutation,
dockerImage: "", mariadb: mariadbMutation,
name: "", mysql: mysqlMutation,
databasePassword: "", };
description: "",
databaseName: "",
databaseUser: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddDatabase) => { const onSubmit = async (data: AddDatabase) => {
const defaultDockerImage = const defaultDockerImage =
data.dockerImage || dockerImageDefaultPlaceholder[data.type]; data.dockerImage || dockerImageDefaultPlaceholder[data.type];
let promise: Promise<unknown> | null = null; let promise: Promise<unknown> | null = null;
const commonParams = {
name: data.name,
appName: data.appName,
dockerImage: defaultDockerImage,
projectId,
description: data.description,
};
if (data.type === "postgres") { if (data.type === "postgres") {
promise = createPostgresql({ promise = postgresMutation.mutateAsync({
name: data.name, ...commonParams,
dockerImage: defaultDockerImage,
databasePassword: data.databasePassword, databasePassword: data.databasePassword,
databaseName: data.databaseName, databaseName: data.databaseName,
databaseUser: databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type], data.databaseUser || databasesUserDefaultPlaceholder[data.type],
projectId,
description: data.description,
}); });
} else if (data.type === "mongo") { } else if (data.type === "mongo") {
promise = createMongo({ promise = mongoMutation.mutateAsync({
name: data.name, ...commonParams,
dockerImage: defaultDockerImage,
databasePassword: data.databasePassword, databasePassword: data.databasePassword,
databaseUser: databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type], data.databaseUser || databasesUserDefaultPlaceholder[data.type],
projectId,
description: data.description,
}); });
} else if (data.type === "redis") { } else if (data.type === "redis") {
promise = createRedis({ promise = redisMutation.mutateAsync({
name: data.name, ...commonParams,
dockerImage: defaultDockerImage,
databasePassword: data.databasePassword, databasePassword: data.databasePassword,
projectId, projectId,
description: data.description,
}); });
} else if (data.type === "mariadb") { } else if (data.type === "mariadb") {
promise = createMariadb({ promise = mariadbMutation.mutateAsync({
name: data.name, ...commonParams,
dockerImage: defaultDockerImage,
databasePassword: data.databasePassword, databasePassword: data.databasePassword,
projectId,
databaseRootPassword: data.databaseRootPassword, databaseRootPassword: data.databaseRootPassword,
databaseName: data.databaseName, databaseName: data.databaseName,
databaseUser: databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type], data.databaseUser || databasesUserDefaultPlaceholder[data.type],
description: data.description,
}); });
} else if (data.type === "mysql") { } else if (data.type === "mysql") {
promise = createMysql({ promise = mysqlMutation.mutateAsync({
name: data.name, ...commonParams,
dockerImage: defaultDockerImage,
databasePassword: data.databasePassword, databasePassword: data.databasePassword,
databaseName: data.databaseName, databaseName: data.databaseName,
databaseUser: databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type], data.databaseUser || databasesUserDefaultPlaceholder[data.type],
projectId,
databaseRootPassword: data.databaseRootPassword, databaseRootPassword: data.databaseRootPassword,
description: data.description,
}); });
} }
@@ -210,6 +230,17 @@ export const AddDatabase = ({ projectId }: Props) => {
await promise await promise
.then(async () => { .then(async () => {
toast.success("Database Created"); toast.success("Database Created");
form.reset({
type: "postgres",
dockerImage: "",
name: "",
appName: `${projectName}-`,
databasePassword: "",
description: "",
databaseName: "",
databaseUser: "",
});
setVisible(false);
await utils.project.one.invalidate({ await utils.project.one.invalidate({
projectId, projectId,
}); });
@@ -220,7 +251,7 @@ export const AddDatabase = ({ projectId }: Props) => {
} }
}; };
return ( return (
<Dialog> <Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer space-x-3" className="w-full cursor-pointer space-x-3"
@@ -230,18 +261,10 @@ export const AddDatabase = ({ projectId }: Props) => {
<span>Database</span> <span>Database</span>
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl"> <DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Databases</DialogTitle> <DialogTitle>Databases</DialogTitle>
</DialogHeader> </DialogHeader>
{/* {isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)} */}
<Form {...form}> <Form {...form}>
<form <form
@@ -264,99 +287,40 @@ export const AddDatabase = ({ projectId }: Props) => {
defaultValue={field.value} defaultValue={field.value}
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
> >
<FormItem className="flex w-full items-center space-x-3 space-y-0"> {Object.entries(databasesMap).map(([key, value]) => (
<FormControl className="w-full"> <FormItem
<div> key={key}
<RadioGroupItem className="flex w-full items-center space-x-3 space-y-0"
value="postgres" >
id="postgres" <FormControl className="w-full">
className="peer sr-only" <div>
/> <RadioGroupItem
<Label value={key}
htmlFor="postgres" id={key}
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary" className="peer sr-only"
> />
<PostgresqlIcon /> <Label
Postgresql htmlFor={key}
</Label> className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
</div> >
</FormControl> {value.icon}
</FormItem> {value.label}
<FormItem className="flex items-center space-x-3 space-y-0"> </Label>
<FormControl className="w-full"> </div>
<div> </FormControl>
<RadioGroupItem </FormItem>
value="mysql" ))}
id="mysql"
className="peer sr-only"
/>
<Label
htmlFor="mysql"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<MysqlIcon />
Mysql
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="mariadb"
id="mariadb"
className="peer sr-only"
/>
<Label
htmlFor="mariadb"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<MariadbIcon />
Mariadb
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="mongo"
id="mongo"
className="peer sr-only"
/>
<Label
htmlFor="mongo"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<MongodbIcon />
Mongo
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="redis"
id="redis"
className="peer sr-only"
/>
<Label
htmlFor="redis"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<RedisIcon />
Redis
</Label>
</div>
</FormControl>
</FormItem>
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
{activeMutation[field.value].isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{activeMutation[field.value].error?.message}
</span>
</div>
)}
</FormItem> </FormItem>
)} )}
/> />
@@ -372,13 +336,34 @@ export const AddDatabase = ({ projectId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Name" {...field} /> <Input
placeholder="Name"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${projectName}-${val}`);
field.onChange(val);
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>AppName</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -183,8 +183,11 @@ const Project = (
Actions Actions
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<AddApplication projectId={projectId} /> <AddApplication
<AddDatabase projectId={projectId} /> projectId={projectId}
projectName={data?.name}
/>
<AddDatabase projectId={projectId} projectName={data?.name} />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}

View File

@@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId); await addNewService(ctx.user.authId, newApplication.applicationId);
} }
} catch (error) { } catch (error: unknown) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error to create the application", message: "Error to create the application",

View File

@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
return true; return true;
} catch (error) { } catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error input: Inserting mariadb database", message: "Error input: Inserting mariadb database",

View File

@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
return true; return true;
} catch (error) { } catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error input: Inserting mongo database", message: "Error input: Inserting mongo database",

View File

@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
return true; return true;
} catch (error) { } catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error input: Inserting mysql database", message: "Error input: Inserting mysql database",

View File

@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
return true; return true;
} catch (error) { } catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error input: Inserting postgresql database", message: "Error input: Inserting postgresql database",

View File

@@ -15,11 +15,23 @@ import { findAdmin } from "./admin";
import { createTraefikConfig } from "@/server/utils/traefik/application"; import { createTraefikConfig } from "@/server/utils/traefik/application";
import { docker } from "@/server/constants"; import { docker } from "@/server/constants";
import { getAdvancedStats } from "@/server/monitoring/utilts"; import { getAdvancedStats } from "@/server/monitoring/utilts";
import { validUniqueServerAppName } from "./project";
export type Application = typeof applications.$inferSelect; export type Application = typeof applications.$inferSelect;
export const createApplication = async ( export const createApplication = async (
input: typeof apiCreateApplication._type, input: typeof apiCreateApplication._type,
) => { ) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Application with this 'AppName' already exists",
});
}
}
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const newApplication = await tx const newApplication = await tx
.insert(applications) .insert(applications)

View File

@@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
import { pullImage } from "@/server/utils/docker/utils"; import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Mariadb = typeof mariadb.$inferSelect; export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMariadb = await db const newMariadb = await db
.insert(mariadb) .insert(mariadb)
.values({ .values({

View File

@@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo";
import { pullImage } from "@/server/utils/docker/utils"; import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Mongo = typeof mongo.$inferSelect; export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => { export const createMongo = async (input: typeof apiCreateMongo._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMongo = await db const newMongo = await db
.insert(mongo) .insert(mongo)
.values({ .values({

View File

@@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql";
import { pullImage } from "@/server/utils/docker/utils"; import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { nanoid } from "nanoid"; import { validUniqueServerAppName } from "./project";
export type MySql = typeof mysql.$inferSelect; export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => { export const createMysql = async (input: typeof apiCreateMySql._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMysql = await db const newMysql = await db
.insert(mysql) .insert(mysql)
.values({ .values({

View File

@@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
import { pullImage } from "@/server/utils/docker/utils"; import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm"; import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Postgres = typeof postgres.$inferSelect; export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => { export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newPostgres = await db const newPostgres = await db
.insert(postgres) .insert(postgres)
.values({ .values({

View File

@@ -1,5 +1,14 @@
import { db } from "@/server/db"; import { db } from "@/server/db";
import { type apiCreateProject, projects } from "@/server/db/schema"; import {
type apiCreateProject,
applications,
mariadb,
mongo,
mysql,
postgres,
projects,
redis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { findAdmin } from "./admin"; import { findAdmin } from "./admin";
@@ -73,3 +82,41 @@ export const updateProjectById = async (
return result; return result;
}; };
export const validUniqueServerAppName = async (appName: string) => {
const query = await db.query.projects.findMany({
with: {
applications: {
where: eq(applications.appName, appName),
},
mariadb: {
where: eq(mariadb.appName, appName),
},
mongo: {
where: eq(mongo.appName, appName),
},
mysql: {
where: eq(mysql.appName, appName),
},
postgres: {
where: eq(postgres.appName, appName),
},
redis: {
where: eq(redis.appName, appName),
},
},
});
// Filter out items with non-empty fields
const nonEmptyProjects = query.filter(
(project) =>
project.applications.length > 0 ||
project.mariadb.length > 0 ||
project.mongo.length > 0 ||
project.mysql.length > 0 ||
project.postgres.length > 0 ||
project.redis.length > 0,
);
return nonEmptyProjects.length === 0;
};

View File

@@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis";
import { pullImage } from "@/server/utils/docker/utils"; import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Redis = typeof redis.$inferSelect; export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 // https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const createRedis = async (input: typeof apiCreateRedis._type) => { export const createRedis = async (input: typeof apiCreateRedis._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newRedis = await db const newRedis = await db
.insert(redis) .insert(redis)
.values({ .values({

View File

@@ -128,6 +128,7 @@ const createSchema = createInsertSchema(applications, {
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({
name: true, name: true,
appName: true,
description: true, description: true,
projectId: true, projectId: true,
}); });

View File

@@ -79,6 +79,7 @@ const createSchema = createInsertSchema(mariadb, {
export const apiCreateMariaDB = createSchema export const apiCreateMariaDB = createSchema
.pick({ .pick({
name: true, name: true,
appName: true,
dockerImage: true, dockerImage: true,
databaseRootPassword: true, databaseRootPassword: true,
projectId: true, projectId: true,

View File

@@ -73,6 +73,7 @@ const createSchema = createInsertSchema(mongo, {
export const apiCreateMongo = createSchema export const apiCreateMongo = createSchema
.pick({ .pick({
name: true, name: true,
appName: true,
dockerImage: true, dockerImage: true,
projectId: true, projectId: true,
description: true, description: true,

View File

@@ -77,6 +77,7 @@ const createSchema = createInsertSchema(mysql, {
export const apiCreateMySql = createSchema export const apiCreateMySql = createSchema
.pick({ .pick({
name: true, name: true,
appName: true,
dockerImage: true, dockerImage: true,
projectId: true, projectId: true,
description: true, description: true,

View File

@@ -74,6 +74,7 @@ const createSchema = createInsertSchema(postgres, {
export const apiCreatePostgres = createSchema export const apiCreatePostgres = createSchema
.pick({ .pick({
name: true, name: true,
appName: true,
databaseName: true, databaseName: true,
databaseUser: true, databaseUser: true,
databasePassword: true, databasePassword: true,

View File

@@ -69,6 +69,7 @@ const createSchema = createInsertSchema(redis, {
export const apiCreateRedis = createSchema export const apiCreateRedis = createSchema
.pick({ .pick({
name: true, name: true,
appName: true,
databasePassword: true, databasePassword: true,
dockerImage: true, dockerImage: true,
projectId: true, projectId: true,