feat(libsql): implement libsql service schema and update related components

- Created a new SQL type for 'libsql' and established a corresponding table with necessary fields and constraints.
- Updated existing tables (backup, mount, volume_backup) to include foreign key references to 'libsql'.
- Enhanced the libsql schema in the application to support additional fields such as stopGracePeriodSwarm and endpointSpecSwarm.
- Adjusted form handling and validation to accommodate the new libsql service type, improving overall integration and functionality.
This commit is contained in:
Mauricio Siu
2026-03-23 21:51:02 -06:00
parent cf25c17c20
commit c752cf3f9e
11 changed files with 58 additions and 115 deletions

View File

@@ -17,7 +17,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
EndpointSpecForm,
HealthCheckForm,
@@ -111,13 +110,6 @@ const menuItems: MenuItem[] = [
},
];
const hasStopGracePeriodSwarm = (
value: unknown,
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
typeof value === "object" &&
value !== null &&
"stopGracePeriodSwarm" in value;
interface Props {
id: string;
type:
@@ -131,107 +123,6 @@ interface Props {
}
export const AddSwarmSettings = ({ id, type }: Props) => {
const queryMap = {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
application: () => api.application.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
};
const { mutateAsync, isError, error, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddSwarmSettings>({
defaultValues: {
healthCheckSwarm: null,
restartPolicySwarm: null,
placementSwarm: null,
updateConfigSwarm: null,
rollbackConfigSwarm: null,
modeSwarm: null,
labelsSwarm: null,
networkSwarm: null,
},
resolver: zodResolver(addSwarmSettings),
});
useEffect(() => {
if (data) {
form.reset({
healthCheckSwarm: data.healthCheckSwarm
? JSON.stringify(data.healthCheckSwarm, null, 2)
: null,
restartPolicySwarm: data.restartPolicySwarm
? JSON.stringify(data.restartPolicySwarm, null, 2)
: null,
placementSwarm: data.placementSwarm
? JSON.stringify(data.placementSwarm, null, 2)
: null,
updateConfigSwarm: data.updateConfigSwarm
? JSON.stringify(data.updateConfigSwarm, null, 2)
: null,
rollbackConfigSwarm: data.rollbackConfigSwarm
? JSON.stringify(data.rollbackConfigSwarm, null, 2)
: null,
modeSwarm: data.modeSwarm
? JSON.stringify(data.modeSwarm, null, 2)
: null,
labelsSwarm: data.labelsSwarm
? JSON.stringify(data.labelsSwarm, null, 2)
: null,
networkSwarm: data.networkSwarm
? JSON.stringify(data.networkSwarm, null, 2)
: null,
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: AddSwarmSettings) => {
await mutateAsync({
applicationId: id || "",
libsqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
mysqlId: id || "",
postgresId: id || "",
redisId: id || "",
healthCheckSwarm: data.healthCheckSwarm,
restartPolicySwarm: data.restartPolicySwarm,
placementSwarm: data.placementSwarm,
updateConfigSwarm: data.updateConfigSwarm,
rollbackConfigSwarm: data.rollbackConfigSwarm,
modeSwarm: data.modeSwarm,
labelsSwarm: data.labelsSwarm,
networkSwarm: data.networkSwarm,
})
.then(async () => {
toast.success("Swarm settings updated");
refetch();
})
.catch(() => {
toast.error("Error updating the swarm settings");
});
};
const [activeMenu, setActiveMenu] = useState<string>("health-check");
const [open, setOpen] = useState(false);
return (

View File

@@ -30,6 +30,8 @@ CREATE TABLE "libsql" (
"modeSwarm" json,
"labelsSwarm" json,
"networkSwarm" json,
"stopGracePeriodSwarm" bigint,
"endpointSpecSwarm" json,
"replicas" integer DEFAULT 1 NOT NULL,
"createdAt" text NOT NULL,
"environmentId" text NOT NULL,

View File

@@ -1,5 +1,5 @@
{
"id": "72bb48c9-d724-458b-bd91-3ae50e9afe0f",
"id": "2819200c-c7c0-474e-8d78-b96ee4785b2a",
"prevId": "68c71185-6e91-4b8d-8f91-795457e75ab2",
"version": "7",
"dialect": "postgresql",
@@ -3759,6 +3759,18 @@
"primaryKey": false,
"notNull": false
},
"stopGracePeriodSwarm": {
"name": "stopGracePeriodSwarm",
"type": "bigint",
"primaryKey": false,
"notNull": false
},
"endpointSpecSwarm": {
"name": "endpointSpecSwarm",
"type": "json",
"primaryKey": false,
"notNull": false
},
"replicas": {
"name": "replicas",
"type": "integer",

View File

@@ -1076,8 +1076,8 @@
{
"idx": 153,
"version": "7",
"when": 1774303955927,
"tag": "0153_green_boomerang",
"when": 1774322599182,
"tag": "0153_motionless_mastermind",
"breakpoints": true
}
]

View File

@@ -238,6 +238,7 @@ export const backupRouter = createTRPCRouter({
backup.mysqlId ||
backup.mariadbId ||
backup.mongoId ||
backup.libsqlId ||
backup.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {

View File

@@ -100,6 +100,7 @@ export const mountRouter = createTRPCRouter({
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.libsqlId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -125,6 +126,7 @@ export const mountRouter = createTRPCRouter({
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.libsqlId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -144,6 +146,7 @@ export const mountRouter = createTRPCRouter({
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.libsqlId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {

View File

@@ -15,6 +15,7 @@ import {
updateVolumeBackupSchema,
volumeBackups,
} from "@dokploy/server/db/schema";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import {
execAsyncRemote,
execAsyncStream,
@@ -25,7 +26,6 @@ import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({
@@ -41,6 +41,7 @@ export const volumeBackupsRouter = createTRPCRouter({
"mongo",
"redis",
"compose",
"libsql",
]),
}),
)
@@ -58,6 +59,7 @@ export const volumeBackupsRouter = createTRPCRouter({
mongo: true,
redis: true,
compose: true,
libsql: true,
},
orderBy: [desc(volumeBackups.createdAt)],
});
@@ -72,6 +74,7 @@ export const volumeBackupsRouter = createTRPCRouter({
input.mariadbId ||
input.mongoId ||
input.redisId ||
input.libsqlId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -113,6 +116,7 @@ export const volumeBackupsRouter = createTRPCRouter({
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -136,6 +140,7 @@ export const volumeBackupsRouter = createTRPCRouter({
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -161,6 +166,7 @@ export const volumeBackupsRouter = createTRPCRouter({
existingVb.mariadbId ||
existingVb.mongoId ||
existingVb.redisId ||
existingVb.libsqlId ||
existingVb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -220,6 +226,7 @@ export const volumeBackupsRouter = createTRPCRouter({
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {

View File

@@ -8,6 +8,7 @@ import {
keepLatestNBackups,
runCommand,
runComposeBackup,
runLibsqlBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
@@ -38,6 +39,7 @@ export const runJobs = async (job: QueueJob) => {
mysql,
mongo,
mariadb,
libsql,
compose,
backupType,
} = backup;
@@ -75,6 +77,14 @@ export const runJobs = async (job: QueueJob) => {
}
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, server.serverId);
} else if (databaseType === "libsql" && libsql) {
const server = await findServerById(libsql.serverId as string);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await runLibsqlBackup(libsql, backup);
await keepLatestNBackups(backup, server.serverId);
}
} else if (backupType === "compose" && compose) {
const server = await findServerById(compose.serverId as string);
@@ -141,6 +151,7 @@ export const initializeJobs = async () => {
mysql: true,
postgres: true,
mongo: true,
libsql: true,
compose: true,
},
});

View File

@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm";
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -9,6 +16,8 @@ import { mounts } from "./mount";
import { server } from "./server";
import {
applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
@@ -66,6 +75,8 @@ export const libsql = pgTable("libsql", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -131,6 +142,8 @@ const createSchema = createInsertSchema(libsql, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
});
export const apiCreateLibsql = createSchema

View File

@@ -18,7 +18,8 @@ export const getVolumeServiceAppName = (
volumeBackup.mysql?.appName ||
volumeBackup.mariadb?.appName ||
volumeBackup.mongo?.appName ||
volumeBackup.redis?.appName;
volumeBackup.redis?.appName ||
volumeBackup.libsql?.appName;
return serviceAppName || volumeBackup.appName;
};

View File

@@ -26,6 +26,7 @@ const getProjectName = (
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {
@@ -48,6 +49,7 @@ const getOrganizationId = (
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {