refactor(libsql): update form validation and import resolver

- Replaced zodResolver import with standardSchemaResolver for improved schema handling.
- Refactored DockerProviderSchema to streamline validation logic and enhance readability.
- Updated external port validation to check for empty values and ensure proper error handling.
- Adjusted service access checks in the libsql router for better permission management.
This commit is contained in:
Mauricio Siu
2026-03-23 00:51:45 -06:00
parent b61ca31981
commit b9aa275759
4 changed files with 359 additions and 410 deletions

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -26,64 +26,37 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
const createDockerProviderSchema = (sqldNode?: string) =>
z
.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalGRPCPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalAdminPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
})
.superRefine((data, ctx) => {
if (
data.externalPort === null &&
data.externalGRPCPort === null &&
data.externalAdminPort === null
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Either externalPort, externalGRPCPort or externalAdminPort must be provided.",
path: ["externalPort", "externalGRPCPort", "externalAdminPort"],
});
}
if (sqldNode === "replica" && data.externalGRPCPort !== null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "externalGRPCPort cannot be set when sqldNode is 'replica'",
path: ["externalGRPCPort"],
});
}
});
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a === null || a === undefined || a === "") return null;
const parsed = Number.parseInt(String(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalGRPCPort: z.preprocess((a) => {
if (a === null || a === undefined || a === "") return null;
const parsed = Number.parseInt(String(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalAdminPort: z.preprocess((a) => {
if (a === null || a === undefined || a === "") return null;
const parsed = Number.parseInt(String(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
libsqlId: string;
@@ -96,31 +69,18 @@ export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => {
const [connectionGRPCUrl, setGRPCConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const DockerProviderSchema = createDockerProviderSchema(data?.sqldNode);
type DockerProvider = z.infer<typeof DockerProviderSchema>;
const form = useForm<DockerProvider>({
const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
const fieldsToUpdate: Partial<DockerProvider> = {};
if (data?.externalPort !== undefined) {
fieldsToUpdate.externalPort = data.externalPort;
}
if (data?.externalGRPCPort !== undefined) {
fieldsToUpdate.externalGRPCPort = data.externalGRPCPort;
}
if (data?.externalAdminPort !== undefined) {
fieldsToUpdate.externalAdminPort = data.externalAdminPort;
}
if (Object.keys(fieldsToUpdate).length > 0) {
form.reset(fieldsToUpdate);
if (data) {
form.reset({
externalPort: data.externalPort,
externalGRPCPort: data.externalGRPCPort,
externalAdminPort: data.externalAdminPort,
});
}
}, [form.reset, data, form]);
@@ -135,168 +95,157 @@ export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => {
toast.success("External port/ports updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port/ports");
.catch((error: Error) => {
toast.error(error?.message || "Error saving the external port/ports");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
const port = form.watch("externalPort") || data?.externalPort;
setConnectionUrl(
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`,
);
return `http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
};
setConnectionUrl(buildConnectionUrl());
const buildGRPCConnectionUrl = () => {
if (data?.sqldNode === "replica") return "";
const port = form.watch("externalGRPCPort") || data?.externalGRPCPort;
return `http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
};
setGRPCConnectionUrl(buildGRPCConnectionUrl());
}, [
data?.appName,
data?.externalGRPCPort,
data?.databasePassword,
form,
data?.databaseUser,
getIp,
]);
if (data?.sqldNode !== "replica") {
const grpcPort =
form.watch("externalGRPCPort") || data?.externalGRPCPort;
setGRPCConnectionUrl(
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${grpcPort}`,
);
}
}, [data?.externalGRPCPort, data?.databasePassword, form, data?.databaseUser, getIp]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable through the internet, you
must set a port and ensure that the port is not being used by
another application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
<div className="flex w-full flex-col gap-5">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable through the internet, you
must set a port and ensure that the port is not being used by
another application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
<div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="8080"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="8080"
{...field}
value={field.value as string}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
{!!data?.externalPort && (
<div className="md:col-span-2">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
)}
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="externalAdminPort"
render={({ field }) => {
return (
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalAdminPort"
render={({ field }) => (
<FormItem>
<FormLabel>External Admin Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="5000"
{...field}
value={field.value as string}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{data?.sqldNode !== "replica" && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalGRPCPort"
render={({ field }) => (
<FormItem>
<FormLabel>
External Admin Port (Internet)
External GRPC Port (Internet)
</FormLabel>
<FormControl>
<Input
placeholder="5000"
placeholder="5001"
{...field}
value={field.value || ""}
value={field.value as string}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
)}
/>
</div>
</div>
{data?.sqldNode !== "replica" && (
<>
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="externalGRPCPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>
External GRPC Port (Internet)
</FormLabel>
<FormControl>
<Input
placeholder="5001"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
{!!data?.externalGRPCPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External GRPC Host</Label>
<ToggleVisibilityInput
value={connectionGRPCUrl}
disabled
/>
</div>
{!!data?.externalGRPCPort && (
<div className="md:col-span-2">
<Label>External GRPC Host</Label>
<ToggleVisibilityInput
value={connectionGRPCUrl}
disabled
/>
</div>
)}
</>
</div>
)}
</div>
</>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isPending}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
<div className="flex justify-end">
<Button type="submit" isLoading={isPending}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";

View File

@@ -1,6 +1,5 @@
import {
addNewService,
checkServiceAccess,
checkPortInUse,
createLibsql,
createMount,
deployLibsql,
@@ -17,11 +16,16 @@ import {
stopServiceRemote,
updateLibsqlById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { db } from "@/server/db";
import {
apiChangeLibsqlStatus,
@@ -40,18 +44,10 @@ export const libsqlRouter = createTRPCRouter({
.input(apiCreateLibsql)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -69,13 +65,7 @@ export const libsqlRouter = createTRPCRouter({
const newLibsql = await createLibsql({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newLibsql.libsqlId,
project.organizationId,
);
}
await addNewService(ctx, newLibsql.libsqlId);
await createMount({
serviceId: newLibsql.libsqlId,
@@ -85,25 +75,22 @@ export const libsqlRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newLibsql.libsqlId,
resourceName: newLibsql.appName,
});
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw error;
}
}),
one: protectedProcedure
.input(apiFindOneLibsql)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.libsqlId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.libsqlId, "read");
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
@@ -120,30 +107,34 @@ export const libsqlRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
const service = await findLibsqlById(input.libsqlId);
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Libsql",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await startServiceRemote(libsql.serverId, libsql.appName);
} else {
await startService(service.appName);
await startService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "done",
});
return service;
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
stop: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
@@ -155,49 +146,77 @@ export const libsqlRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
saveExternalPorts: protectedProcedure
.input(apiSaveExternalPortsLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
service: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (libsql.sqldNode === "replica" && input.externalGRPCPort !== null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "externalGRPCPort cannot be set when sqldNode is 'replica'",
});
}
const portsToCheck = [
{ port: input.externalPort, name: "externalPort", current: libsql.externalPort },
{ port: input.externalGRPCPort, name: "externalGRPCPort", current: libsql.externalGRPCPort },
{ port: input.externalAdminPort, name: "externalAdminPort", current: libsql.externalAdminPort },
];
for (const { port, name, current } of portsToCheck) {
if (port && port !== current) {
const portCheck = await checkPortInUse(
port,
libsql.serverId || undefined,
);
if (portCheck.isInUse) {
throw new TRPCError({
code: "CONFLICT",
message: `Port ${port} (${name}) is already in use by ${portCheck.conflictingContainer}`,
});
}
}
}
await updateLibsqlById(input.libsqlId, {
externalPort: input.externalPort,
externalGRPCPort: input.externalGRPCPort,
externalAdminPort: input.externalAdminPort,
});
await deployLibsql(input.libsqlId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
deploy: protectedProcedure
.input(apiDeployLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Libsql",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return deployLibsql(input.libsqlId);
}),
deployWithLogs: protectedProcedure
@@ -210,55 +229,54 @@ export const libsqlRouter = createTRPCRouter({
},
})
.input(apiDeployLibsql)
.subscription(async ({ input, ctx }) => {
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Libsql",
});
}
return observable<string>((emit) => {
deployLibsql(input.libsqlId, (log) => {
emit.next(log);
});
.subscription(async function* ({ input, ctx, signal }) {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
deployLibsql(input.libsqlId, (log) => {
queue.push(log);
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
yield queue.shift()!;
} else {
await new Promise((r) => setTimeout(r, 50));
}
if (signal?.aborted) {
return;
}
}
}),
changeStatus: protectedProcedure
.input(apiChangeLibsqlStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Libsql status",
});
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
remove: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.libsqlId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.libsqlId, "delete");
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
@@ -268,7 +286,12 @@ export const libsqlRouter = createTRPCRouter({
message: "You are not authorized to delete this Libsql",
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
const cleanupOperations = [
async () => await removeService(libsql?.appName, libsql.serverId),
async () => await removeLibsqlById(input.libsqlId),
@@ -285,16 +308,9 @@ export const libsqlRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesLibsql)
.mutation(async ({ input, ctx }) => {
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
envVars: ["write"],
});
const service = await updateLibsqlById(input.libsqlId, {
env: input.env,
});
@@ -306,21 +322,20 @@ export const libsqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.libsqlId,
});
return true;
}),
reload: protectedProcedure
.input(apiResetLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Libsql",
});
}
if (libsql.serverId) {
await stopServiceRemote(libsql.serverId, libsql.appName);
} else {
@@ -338,33 +353,38 @@ export const libsqlRouter = createTRPCRouter({
await updateLibsqlById(input.libsqlId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateLibsql)
.mutation(async ({ input, ctx }) => {
const { libsqlId, ...rest } = input;
const libsql = await findLibsqlById(libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this Libsql",
});
}
const service = await updateLibsqlById(libsqlId, {
await checkServicePermissionAndAccess(ctx, libsqlId, {
service: ["create"],
});
const libsql = await updateLibsqlById(libsqlId, {
...rest,
});
if (!service) {
if (!libsql) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error updating Libsql",
message: "Error updating Libsql",
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsqlId,
resourceName: libsql.appName,
});
return true;
}),
move: protectedProcedure
@@ -375,31 +395,10 @@ export const libsqlRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this libsql",
});
}
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the libsql's projectId
const updatedLibsql = await db
.update(libsqlTable)
.set({
@@ -416,23 +415,27 @@ export const libsqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedLibsql.libsqlId,
resourceName: updatedLibsql.appName,
});
return updatedLibsql;
}),
rebuild: protectedProcedure
.input(apiRebuildLibsql)
.mutation(async ({ input, ctx }) => {
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MariaDB database",
});
}
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
await rebuildDatabase(libsql.libsqlId, "libsql");
await rebuildDatabase(input.libsqlId, "libsql");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.libsqlId,
});
return true;
}),
});

View File

@@ -96,15 +96,6 @@ const createSchema = createInsertSchema(mounts, {
mountPath: z.string().min(1),
mountId: z.string().optional(),
filePath: z.string().optional(),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
});
export const apiCreateMount = createSchema
@@ -118,6 +109,16 @@ export const apiCreateMount = createSchema
})
.extend({
serviceId: z.string().min(1),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
"libsql",
]),
});
export const apiFindOneMount = z.object({
@@ -133,23 +134,19 @@ export const apiRemoveMount = createSchema
// })
.required();
export const apiFindMountByApplicationId = createSchema
.extend({
serviceId: z.string().min(1),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
})
.pick({
serviceId: true,
serviceType: true,
});
export const apiFindMountByApplicationId = z.object({
serviceId: z.string().min(1),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
"libsql",
]),
});
export const apiUpdateMount = createSchema.partial().extend({
mountId: z.string().min(1),