wip: more domain stuf
This commit is contained in:
parent
fbbf1772a1
commit
ddad271a55
|
@ -73,6 +73,7 @@
|
|||
"trpc-openapi": "^1.2.0",
|
||||
"ts-permissions": "^1.0.0",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuidv7": "^0.6.3",
|
||||
"winston": "^3.11.0",
|
||||
"ws": "^8.16.0",
|
||||
"zod": "^3.22.4"
|
||||
|
|
|
@ -173,6 +173,9 @@ dependencies:
|
|||
ua-parser-js:
|
||||
specifier: ^1.0.37
|
||||
version: 1.0.37
|
||||
uuidv7:
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3
|
||||
winston:
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0
|
||||
|
@ -8257,6 +8260,11 @@ packages:
|
|||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
/uuidv7@0.6.3:
|
||||
resolution: {integrity: sha512-zV3eW2NlXTsun/aJ7AixxZjH/byQcH/r3J99MI0dDEkU2cJIBJxhEWUHDTpOaLPRNhebPZoeHuykYREkI9HafA==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/validate-npm-package-license@3.0.4:
|
||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||
dependencies:
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useFieldArray } from "react-hook-form";
|
||||
import { uuidv7 } from "uuidv7";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Form } from "~/components/ui/form";
|
||||
|
@ -10,11 +12,14 @@ import {
|
|||
FormUnsavedChangesIndicator,
|
||||
useForm,
|
||||
} from "~/hooks/forms";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useService } from "../_hooks/service";
|
||||
import DomainEntry from "./_components/DomainEntry";
|
||||
|
||||
const formValidator = z.object({
|
||||
domains: z.array(
|
||||
z.object({
|
||||
domainId: z.string(),
|
||||
domain: z
|
||||
.string()
|
||||
.regex(
|
||||
|
@ -23,39 +28,64 @@ const formValidator = z.object({
|
|||
),
|
||||
|
||||
internalPort: z.coerce.number().int().min(1).max(65535).default(8080),
|
||||
https: z.boolean().default(true),
|
||||
forceSSL: z.boolean().default(true),
|
||||
https: z.coerce.boolean().default(true),
|
||||
forceSSL: z.coerce.boolean().default(true),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export default function DomainsList(
|
||||
{
|
||||
// service,
|
||||
}: {
|
||||
// service: RouterOutputs["projects"]["services"]["get"];
|
||||
},
|
||||
) {
|
||||
export default function DomainsList() {
|
||||
const service = useService();
|
||||
const updateDomain = api.projects.services.updateDomain.useMutation();
|
||||
|
||||
const form = useForm(formValidator, {
|
||||
defaultValues: {
|
||||
domains: [],
|
||||
},
|
||||
});
|
||||
|
||||
const domainsForm = useFieldArray({
|
||||
control: form.control,
|
||||
name: "domains",
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log("setting domains", service.data?.domains ?? []);
|
||||
// form.setValue("domains", service.data?.domains ?? []);
|
||||
// }, [service.data?.domains]);
|
||||
|
||||
console.log(
|
||||
"Rendering fields with ids: ",
|
||||
domainsForm.fields.map((field) => field.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
console.log(data);
|
||||
// await Promise.all(
|
||||
// data.domains.map((domain) => {
|
||||
// if (service.data === undefined) return;
|
||||
|
||||
// return updateDomain.mutateAsync({
|
||||
// projectId: service.data.projectId,
|
||||
// serviceId: service.data.id,
|
||||
// domain: domain.domain,
|
||||
// forceSSL: domain.forceSSL,
|
||||
// https: domain.https,
|
||||
// internalPort: domain.internalPort,
|
||||
// });
|
||||
// }),
|
||||
// );
|
||||
|
||||
// refetch service
|
||||
await service.refetch();
|
||||
})}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<h1 className="col-span-2">Domains</h1>
|
||||
|
||||
<AnimatePresence mode="sync">
|
||||
{domainsForm.fields.map((field, index) => (
|
||||
<DomainEntry
|
||||
form={form}
|
||||
|
@ -66,7 +96,11 @@ export default function DomainsList(
|
|||
/>
|
||||
))}
|
||||
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<motion.div
|
||||
className="flex flex-row flex-wrap items-center gap-4"
|
||||
layout
|
||||
key={service.data?.id}
|
||||
>
|
||||
<FormSubmit
|
||||
form={form}
|
||||
className="col-span-2"
|
||||
|
@ -76,20 +110,26 @@ export default function DomainsList(
|
|||
<Button
|
||||
variant="secondary"
|
||||
icon={PlusIcon}
|
||||
onClick={() =>
|
||||
domainsForm.append({
|
||||
domain: "",
|
||||
onClick={() => {
|
||||
const domain = {
|
||||
domainId: new Date().toISOString(),
|
||||
domain: uuidv7().split("-").at(-1) + ".example.com",
|
||||
forceSSL: false,
|
||||
https: false,
|
||||
internalPort: 8080,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
console.log("add domain: ", domain.domainId);
|
||||
|
||||
domainsForm.append(domain);
|
||||
}}
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
|
||||
<FormUnsavedChangesIndicator form={form} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowRight, CogIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { forwardRef, useState } from "react";
|
||||
import { type UseFieldArrayReturn, type UseFormReturn } from "react-hook-form";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card } from "~/components/ui/card";
|
||||
|
@ -9,18 +10,16 @@ import { Switch } from "~/components/ui/switch";
|
|||
import { SimpleFormField } from "~/hooks/forms";
|
||||
|
||||
type FieldData = {
|
||||
domainId: string;
|
||||
domain: string;
|
||||
internalPort: number;
|
||||
https: boolean;
|
||||
forceSSL: boolean;
|
||||
};
|
||||
|
||||
export default function DomainEntry({
|
||||
form,
|
||||
domains,
|
||||
field,
|
||||
index,
|
||||
}: {
|
||||
const DomainEntry = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
form: UseFormReturn<
|
||||
{
|
||||
domains: FieldData[];
|
||||
|
@ -40,11 +39,29 @@ export default function DomainEntry({
|
|||
|
||||
field: FieldData;
|
||||
index: number;
|
||||
}) {
|
||||
const isOpen = useState(false);
|
||||
}
|
||||
>(({ form, domains, field, index }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="flex flex-row gap-4 p-4">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
type: "spring",
|
||||
}}
|
||||
key={field.domainId}
|
||||
>
|
||||
<Card className="p-4">
|
||||
<h1>
|
||||
Rendering {field.domainId ?? "undefined???"} at index {index}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<SimpleFormField
|
||||
control={form.control}
|
||||
name={`domains.${index}.forceSSL`}
|
||||
|
@ -73,14 +90,59 @@ export default function DomainEntry({
|
|||
/>
|
||||
|
||||
<div className="flex flex-row gap-2 pt-8">
|
||||
<Button variant="secondary" icon={CogIcon} className="mr-2" />
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={CogIcon}
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
icon={TrashIcon}
|
||||
onClick={() => domains.remove(index)}
|
||||
onClick={() => {
|
||||
domains.remove(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div className="flex flex-col gap-4">
|
||||
<SimpleFormField
|
||||
control={form.control}
|
||||
name={`domains.${index}.https`}
|
||||
friendlyName="HTTPS"
|
||||
render={({ field }) => (
|
||||
<div className="pt-2">
|
||||
<Switch {...field} className="mx-auto block" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SimpleFormField
|
||||
control={form.control}
|
||||
name={`domains.${index}.domain`}
|
||||
friendlyName="Domain"
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
<SimpleFormField
|
||||
control={form.control}
|
||||
name={`domains.${index}.internalPort`}
|
||||
friendlyName="Internal Port"
|
||||
className="w-60"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
DomainEntry.displayName = "DomainEntry";
|
||||
export default DomainEntry;
|
||||
|
|
|
@ -4,17 +4,23 @@ import { cn } from "~/utils/utils";
|
|||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
as?: React.ElementType;
|
||||
}
|
||||
>(({ className, as, ...props }, ref) => {
|
||||
const Component = as ?? "div";
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-card text-card-foreground rounded-xl border shadow",
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
);
|
||||
});
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
|
@ -47,7 +53,7 @@ const CardDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
@ -75,9 +81,9 @@ CardFooter.displayName = "CardFooter";
|
|||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import assert from "assert";
|
||||
import { randomBytes } from "crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { env } from "~/env";
|
||||
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||
import { authenticatedProcedure, createTRPCRouter } from "~/server/api/trpc";
|
||||
import { service } from "~/server/db/schema";
|
||||
import {
|
||||
DOCKER_DEPLOY_MODE_MAP,
|
||||
DockerDeployMode,
|
||||
ServiceSource,
|
||||
} from "~/server/db/types";
|
||||
import { zDockerDuration, zDockerImage, zDockerName } from "~/server/utils/zod";
|
||||
import { DOCKER_DEPLOY_MODE_MAP, ServiceSource } from "~/server/db/types";
|
||||
import { zDockerName } from "~/server/utils/zod";
|
||||
import { getServiceContainers } from "./containers";
|
||||
import {
|
||||
deleteServiceDomainsProcedure,
|
||||
updateServiceDomainsProcedure,
|
||||
updateServiceProcedure,
|
||||
} from "./update";
|
||||
|
||||
export const serviceRouter = createTRPCRouter({
|
||||
containers: getServiceContainers,
|
||||
update: updateServiceProcedure,
|
||||
updateDomain: updateServiceDomainsProcedure,
|
||||
deleteDomain: deleteServiceDomainsProcedure,
|
||||
|
||||
get: authenticatedProcedure
|
||||
.meta({
|
||||
|
@ -46,94 +49,10 @@ export const serviceRouter = createTRPCRouter({
|
|||
|
||||
return {
|
||||
...fullServiceData,
|
||||
zeroDowntime: fullServiceData.zeroDowntime === 1,
|
||||
deployMode: DOCKER_DEPLOY_MODE_MAP[fullServiceData.deployMode],
|
||||
};
|
||||
}),
|
||||
|
||||
update: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: "PATCH",
|
||||
path: "/api/projects/:projectId/services/:serviceId",
|
||||
summary: "Update service",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
// sometimes i forget how powerful zod is
|
||||
z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
serviceId: z.string(),
|
||||
})
|
||||
.merge(
|
||||
createInsertSchema(service, {
|
||||
dockerImage: zDockerImage,
|
||||
gitUrl: (schema) =>
|
||||
schema.gitUrl.regex(
|
||||
// https://www.debuggex.com/r/fFggA8Uc4YYKjl34 from https://stackoverflow.com/a/22312124
|
||||
// /(?P<host>(git@|https:\/\/)([\w\.@]+)(\/|:))(?P<owner>[\w,\-,\_]+)\/(?P<repo>[\w,\-,\_]+)(.git){0,1}((\/){0,1})/,
|
||||
/(git@|https:\/\/)([\w\.@]+)(\/|:)([\w,\-,\_]+)\/([\w,\-,\_]+)(.git){0,1}(\/{0,1})/,
|
||||
{
|
||||
message: "Must be a valid git url. (Regex failed)",
|
||||
},
|
||||
),
|
||||
deployMode: z
|
||||
.nativeEnum(DockerDeployMode)
|
||||
// or "replicated" | "global" and transform into 1 or 2
|
||||
.or(
|
||||
z
|
||||
.enum(["replicated", "global"])
|
||||
.transform((val) =>
|
||||
val === "replicated"
|
||||
? DockerDeployMode.Replicated
|
||||
: DockerDeployMode.Global,
|
||||
),
|
||||
),
|
||||
zeroDowntime: z.boolean().transform((val) => (val ? 1 : 0)),
|
||||
restartDelay: zDockerDuration,
|
||||
healthcheckEnabled: z.boolean().transform((val) => (val ? 1 : 0)),
|
||||
healthcheckInterval: zDockerDuration,
|
||||
healthcheckTimeout: zDockerDuration,
|
||||
healthcheckStartPeriod: zDockerDuration,
|
||||
loggingMaxSize: (schema) =>
|
||||
schema.loggingMaxSize.regex(/^\d+[kmg]$/, {
|
||||
message: "Must be an integer plus a modifier (k, m, or g).",
|
||||
}),
|
||||
loggingMaxFiles: (schema) => schema.loggingMaxFiles.positive(),
|
||||
})
|
||||
.omit({
|
||||
id: true,
|
||||
projectId: true,
|
||||
name: true,
|
||||
})
|
||||
.partial(),
|
||||
)
|
||||
.strict(),
|
||||
)
|
||||
.use(projectMiddleware)
|
||||
.use(serviceMiddleware)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// gotta keep TS happy, can't delete properties from input directly
|
||||
const queryUpdate: Omit<typeof input, "projectId" | "serviceId"> & {
|
||||
projectId?: string;
|
||||
serviceId?: string;
|
||||
id?: string;
|
||||
} = structuredClone(input);
|
||||
|
||||
delete queryUpdate.projectId;
|
||||
delete queryUpdate.serviceId;
|
||||
delete queryUpdate.id;
|
||||
|
||||
await ctx.db
|
||||
.update(service)
|
||||
.set(queryUpdate)
|
||||
.where(eq(service.id, ctx.service.id))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
create: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
|
|
202
src/server/api/routers/projects/service/update.ts
Normal file
202
src/server/api/routers/projects/service/update.ts
Normal file
|
@ -0,0 +1,202 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||
import { authenticatedProcedure } from "~/server/api/trpc";
|
||||
import { service, serviceDomain } from "~/server/db/schema";
|
||||
import { DockerDeployMode } from "~/server/db/types";
|
||||
import { zDockerDuration, zDockerImage, zDomain } from "~/server/utils/zod";
|
||||
|
||||
/**
|
||||
* Handles updating basic details of a service
|
||||
*/
|
||||
export const updateServiceProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: "PATCH",
|
||||
path: "/api/projects/:projectId/services/:serviceId",
|
||||
summary: "Update service",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
// sometimes i forget how powerful zod is
|
||||
z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
serviceId: z.string(),
|
||||
})
|
||||
.merge(
|
||||
createInsertSchema(service, {
|
||||
dockerImage: zDockerImage,
|
||||
gitUrl: (schema) =>
|
||||
schema.gitUrl.regex(
|
||||
// https://www.debuggex.com/r/fFggA8Uc4YYKjl34 from https://stackoverflow.com/a/22312124
|
||||
// /(?P<host>(git@|https:\/\/)([\w\.@]+)(\/|:))(?P<owner>[\w,\-,\_]+)\/(?P<repo>[\w,\-,\_]+)(.git){0,1}((\/){0,1})/,
|
||||
/(git@|https:\/\/)([\w\.@]+)(\/|:)([\w,\-,\_]+)\/([\w,\-,\_]+)(.git){0,1}(\/{0,1})/,
|
||||
{
|
||||
message: "Must be a valid git url. (Regex failed)",
|
||||
},
|
||||
),
|
||||
deployMode: z
|
||||
.nativeEnum(DockerDeployMode)
|
||||
// or "replicated" | "global" and transform into 1 or 2
|
||||
.or(
|
||||
z
|
||||
.enum(["replicated", "global"])
|
||||
.transform((val) =>
|
||||
val === "replicated"
|
||||
? DockerDeployMode.Replicated
|
||||
: DockerDeployMode.Global,
|
||||
),
|
||||
),
|
||||
zeroDowntime: z.boolean(),
|
||||
restartDelay: zDockerDuration,
|
||||
healthcheckEnabled: z.boolean(),
|
||||
healthcheckInterval: zDockerDuration,
|
||||
healthcheckTimeout: zDockerDuration,
|
||||
healthcheckStartPeriod: zDockerDuration,
|
||||
loggingMaxSize: (schema) =>
|
||||
schema.loggingMaxSize.regex(/^\d+[kmg]$/, {
|
||||
message: "Must be an integer plus a modifier (k, m, or g).",
|
||||
}),
|
||||
loggingMaxFiles: (schema) => schema.loggingMaxFiles.positive(),
|
||||
})
|
||||
.omit({
|
||||
id: true,
|
||||
projectId: true,
|
||||
name: true,
|
||||
})
|
||||
.partial(),
|
||||
)
|
||||
.strict(),
|
||||
)
|
||||
.use(projectMiddleware)
|
||||
.use(serviceMiddleware)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// gotta keep TS happy, can't delete properties from input directly
|
||||
const queryUpdate: Omit<typeof input, "projectId" | "serviceId"> & {
|
||||
projectId?: string;
|
||||
serviceId?: string;
|
||||
id?: string;
|
||||
} = structuredClone(input);
|
||||
|
||||
delete queryUpdate.projectId;
|
||||
delete queryUpdate.serviceId;
|
||||
delete queryUpdate.id;
|
||||
|
||||
await ctx.db
|
||||
.update(service)
|
||||
.set(queryUpdate)
|
||||
.where(eq(service.id, ctx.service.id))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
export const updateServiceDomainsProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: "PUT",
|
||||
path: "/api/projects/:projectId/services/:serviceId/domains/:domainId",
|
||||
summary: "Update or create a service domains",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
// meta
|
||||
projectId: z.string(),
|
||||
serviceId: z.string(),
|
||||
domainId: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
||||
)
|
||||
.optional(),
|
||||
|
||||
// details
|
||||
domain: zDomain,
|
||||
internalPort: z.coerce.number().int().min(1).max(65535),
|
||||
https: z.boolean(),
|
||||
forceSSL: z.boolean().default(true),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const baseQuery = ctx.db
|
||||
.insert(serviceDomain)
|
||||
.values({
|
||||
id: input.domainId,
|
||||
serviceId: input.serviceId,
|
||||
domain: input.domain,
|
||||
internalPort: input.internalPort,
|
||||
https: input.https,
|
||||
forceSSL: input.forceSSL,
|
||||
})
|
||||
.returning({
|
||||
id: serviceDomain.id,
|
||||
});
|
||||
|
||||
// track if we inserted or updated
|
||||
let updated = false;
|
||||
let domainId = input.domainId;
|
||||
|
||||
if (input.domainId) {
|
||||
const result = await baseQuery
|
||||
.onConflictDoUpdate({
|
||||
set: {
|
||||
internalPort: input.internalPort,
|
||||
https: input.https,
|
||||
forceSSL: input.forceSSL,
|
||||
domain: input.domain,
|
||||
},
|
||||
target: serviceDomain.id,
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (result[0]) {
|
||||
updated = true;
|
||||
domainId = result[0].id;
|
||||
}
|
||||
} else {
|
||||
const result = await baseQuery.execute();
|
||||
|
||||
if (result[0]) {
|
||||
updated = false;
|
||||
domainId = result[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updated,
|
||||
domainId,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a service domain
|
||||
*/
|
||||
export const deleteServiceDomainsProcedure = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: "DELETE",
|
||||
path: "/api/projects/:projectId/services/:serviceId/domains/:domainId",
|
||||
summary: "Delete a service domain",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
serviceId: z.string(),
|
||||
domainId: z.string(),
|
||||
}),
|
||||
)
|
||||
.use(projectMiddleware)
|
||||
.use(serviceMiddleware)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db
|
||||
.delete(serviceDomain)
|
||||
.where(eq(serviceDomain.id, input.domainId))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
});
|
|
@ -169,12 +169,14 @@ export const service = sqliteTable(
|
|||
.$type<DockerDeployMode>()
|
||||
.default(DockerDeployMode.Replicated)
|
||||
.notNull(),
|
||||
zeroDowntime: integer("zero_downtime").default(0).notNull(),
|
||||
zeroDowntime: integer("zero_downtime", { mode: "boolean" })
|
||||
.default(false)
|
||||
.notNull(),
|
||||
|
||||
// deployment usage limits
|
||||
max_cpu: real("max_cpu").default(0).notNull(),
|
||||
max_memory: text("max_memory").default("0").notNull(),
|
||||
max_pids: integer("max_pids").default(0).notNull(),
|
||||
max_pids: integer("max_pids", { mode: "boolean" }).default(false).notNull(),
|
||||
|
||||
// restart policy
|
||||
restart: integer("restart")
|
||||
|
@ -187,7 +189,9 @@ export const service = sqliteTable(
|
|||
restartMaxAttempts: integer("restart_max_attempts"),
|
||||
|
||||
// healthcheck
|
||||
healthcheckEnabled: integer("healthcheck_enabled").default(0).notNull(),
|
||||
healthcheckEnabled: integer("healthcheck_enabled", { mode: "boolean" })
|
||||
.default(false)
|
||||
.notNull(),
|
||||
healthcheckCommand: text("healthcheck_command"),
|
||||
healthcheckInterval: text("healthcheck_interval").default("30s").notNull(),
|
||||
healthcheckTimeout: text("healthcheck_timeout").default("30s").notNull(),
|
||||
|
@ -244,8 +248,8 @@ export const serviceDomain = sqliteTable("service_domain", {
|
|||
|
||||
domain: text("domain").notNull(),
|
||||
internalPort: integer("internal_port").notNull(),
|
||||
https: integer("https").default(0).notNull(),
|
||||
forceSSL: integer("force_ssl").default(0).notNull(),
|
||||
https: integer("https", { mode: "boolean" }).default(false).notNull(),
|
||||
forceSSL: integer("force_ssl", { mode: "boolean" }).default(false).notNull(),
|
||||
});
|
||||
|
||||
export const serviceDomainRelations = relations(serviceDomain, ({ one }) => ({
|
||||
|
|
|
@ -18,3 +18,10 @@ export const zDockerDuration = z
|
|||
.regex(/(?:[\d.]+h)?(?:[\d.]+m)?(?:[\d.]+s)?/, {
|
||||
message: "Must be a valid duration. (Regex failed)",
|
||||
});
|
||||
|
||||
export const zDomain = z
|
||||
.string()
|
||||
.regex(
|
||||
/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/,
|
||||
{ message: "Invalid domain name. (Regex)" },
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue