Files
dokploy/packages/server/src/services/server.ts
T
Mauricio Siu 02760212f7 feat: list associated services in delete server modal
Show the services attached to a remote server directly in its delete
confirmation modal, with a link to each service and a per-service
delete action, instead of only showing a generic 'has active
services' blocker.
2026-09-01 03:11:12 -06:00

282 lines
7.1 KiB
TypeScript

import { db } from "@dokploy/server/db";
import {
type apiCreateServer,
member,
organization,
server,
} from "@dokploy/server/db/schema";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import type { z } from "zod";
export type Server = typeof server.$inferSelect;
export const createServer = async (
input: z.infer<typeof apiCreateServer>,
organizationId: string,
) => {
const newServer = await db
.insert(server)
.values({
...input,
organizationId: organizationId,
createdAt: new Date().toISOString(),
} as typeof server.$inferInsert)
.returning()
.then((value) => value[0]);
if (!newServer) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the server",
});
}
return newServer;
};
export const findServerById = async (serverId: string) => {
const currentServer = await db.query.server.findFirst({
where: eq(server.serverId, serverId),
with: {
deployments: true,
sshKey: true,
},
});
if (!currentServer) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
return currentServer;
};
/**
* Removes the SSH private key material from a server record before it is sent
* to a client. `findServerById` eagerly loads the `sshKey` relation (needed for
* server-side SSH operations), but the private key must never leave the server:
* no client feature consumes it, and returning it exposed it to any member with
* only `server:read`. Server-side callers keep using `findServerById` directly.
*/
export const redactServerSshKey = <
T extends { sshKey?: { privateKey: string } | null },
>(
serverRecord: T,
): T => {
if (!serverRecord.sshKey) {
return serverRecord;
}
return {
...serverRecord,
sshKey: { ...serverRecord.sshKey, privateKey: "" },
};
};
export const findServersByUserId = async (userId: string) => {
const orgs = await db.query.organization.findMany({
where: eq(organization.ownerId, userId),
with: {
servers: true,
},
});
const servers = orgs.flatMap((org) => org.servers);
return servers;
};
export const deleteServer = async (serverId: string) => {
const currentServer = await db
.delete(server)
.where(eq(server.serverId, serverId))
.returning()
.then((value) => value[0]);
return currentServer;
};
export const haveActiveServices = async (serverId: string) => {
const currentServer = await db.query.server.findFirst({
where: eq(server.serverId, serverId),
columns: { serverId: true },
with: {
applications: { columns: { applicationId: true } },
compose: { columns: { composeId: true } },
libsql: { columns: { libsqlId: true } },
mariadb: { columns: { mariadbId: true } },
mongo: { columns: { mongoId: true } },
mysql: { columns: { mysqlId: true } },
postgres: { columns: { postgresId: true } },
redis: { columns: { redisId: true } },
},
});
if (!currentServer) {
return false;
}
const total =
currentServer?.applications?.length +
currentServer?.compose?.length +
currentServer?.libsql?.length +
currentServer?.mariadb?.length +
currentServer?.mongo?.length +
currentServer?.mysql?.length +
currentServer?.postgres?.length +
currentServer?.redis?.length;
if (total === 0) {
return false;
}
return true;
};
export const SERVICE_TYPES_BY_SERVER = [
{ type: "application", relation: "applications", idColumn: "applicationId" },
{ type: "compose", relation: "compose", idColumn: "composeId" },
{ type: "postgres", relation: "postgres", idColumn: "postgresId" },
{ type: "mysql", relation: "mysql", idColumn: "mysqlId" },
{ type: "mariadb", relation: "mariadb", idColumn: "mariadbId" },
{ type: "mongo", relation: "mongo", idColumn: "mongoId" },
{ type: "redis", relation: "redis", idColumn: "redisId" },
{ type: "libsql", relation: "libsql", idColumn: "libsqlId" },
] as const;
export interface ServerService {
id: string;
type: (typeof SERVICE_TYPES_BY_SERVER)[number]["type"];
name: string;
projectId: string;
environmentId: string;
url: string;
}
export const getServicesByServerId = async (
serverId: string,
): Promise<ServerService[]> => {
const currentServer = await db.query.server.findFirst({
where: eq(server.serverId, serverId),
columns: { serverId: true },
with: {
applications: {
columns: { applicationId: true, name: true },
with: { environment: { with: { project: true } } },
},
compose: {
columns: { composeId: true, name: true },
with: { environment: { with: { project: true } } },
},
postgres: {
columns: { postgresId: true, name: true },
with: { environment: { with: { project: true } } },
},
mysql: {
columns: { mysqlId: true, name: true },
with: { environment: { with: { project: true } } },
},
mariadb: {
columns: { mariadbId: true, name: true },
with: { environment: { with: { project: true } } },
},
mongo: {
columns: { mongoId: true, name: true },
with: { environment: { with: { project: true } } },
},
redis: {
columns: { redisId: true, name: true },
with: { environment: { with: { project: true } } },
},
libsql: {
columns: { libsqlId: true, name: true },
with: { environment: { with: { project: true } } },
},
},
});
if (!currentServer) {
return [];
}
const services: ServerService[] = [];
for (const { type, relation, idColumn } of SERVICE_TYPES_BY_SERVER) {
const rows = currentServer[relation as keyof typeof currentServer] as Array<
Record<string, any>
>;
for (const row of rows) {
const projectId = row.environment.project.projectId as string;
const environmentId = row.environment.environmentId as string;
services.push({
id: row[idColumn],
type,
name: row.name,
projectId,
environmentId,
url: `/dashboard/project/${projectId}/environment/${environmentId}/services/${type}/${row[idColumn]}`,
});
}
}
return services;
};
export const updateServerById = async (
serverId: string,
serverData: Partial<Server>,
) => {
const result = await db
.update(server)
.set({
...serverData,
})
.where(eq(server.serverId, serverId))
.returning()
.then((res) => res[0]);
return result;
};
export const getAllServers = async () => {
const servers = await db.query.server.findMany();
return servers;
};
export const getAccessibleServerIds = async (session: {
userId: string;
activeOrganizationId: string;
}): Promise<Set<string>> => {
const { userId, activeOrganizationId } = session;
const allOrgServers = await db.query.server.findMany({
where: eq(server.organizationId, activeOrganizationId),
columns: {
serverId: true,
},
});
const memberRecord = await db.query.member.findFirst({
where: and(
eq(member.userId, userId),
eq(member.organizationId, activeOrganizationId),
),
columns: { accessedServers: true, role: true },
});
if (memberRecord?.role === "owner" || memberRecord?.role === "admin") {
return new Set(allOrgServers.map((s) => s.serverId));
}
const licensed = await hasValidLicense(activeOrganizationId);
if (!licensed) {
return new Set(allOrgServers.map((s) => s.serverId));
}
return new Set(memberRecord?.accessedServers ?? []);
};