import { addNewEnvironment, addNewProject, checkProjectAccess, createApplication, createBackup, createCompose, createDomain, createMariadb, createMongo, createMount, createMysql, createPort, createPostgres, createPreviewDeployment, createProject, createRedirect, createRedis, createSecurity, deleteProject, findApplicationById, findComposeById, findEnvironmentById, findMariadbById, findMemberById, findMongoById, findMySqlById, findPostgresById, findProjectById, findRedisById, findUserById, IS_CLOUD, updateProjectById, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import type { AnyPgColumn } from "drizzle-orm/pg-core"; import { z } from "zod"; import { adminProcedure, createTRPCRouter, protectedProcedure, } from "@/server/api/trpc"; import { apiCreateProject, apiFindOneProject, apiRemoveProject, apiUpdateProject, applications, compose, environments, mariadb, mongo, mysql, postgres, projects, redis, } from "@/server/db/schema"; export const projectRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateProject) .mutation(async ({ ctx, input }) => { try { if (ctx.user.role === "member") { await checkProjectAccess( ctx.user.id, "create", ctx.session.activeOrganizationId, ); } const admin = await findUserById(ctx.user.ownerId); if (admin.serversQuantity === 0 && IS_CLOUD) { throw new TRPCError({ code: "NOT_FOUND", message: "No servers available, Please subscribe to a plan", }); } const project = await createProject( input, ctx.session.activeOrganizationId, ); if (ctx.user.role === "member") { await addNewProject( ctx.user.id, project.project.projectId, ctx.session.activeOrganizationId, ); await addNewEnvironment( ctx.user.id, project?.environment?.environmentId || "", ctx.session.activeOrganizationId, ); } return project; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", message: `Error creating the project: ${error instanceof Error ? error.message : error}`, cause: error, }); } }), one: protectedProcedure .input(apiFindOneProject) .query(async ({ input, ctx }) => { if (ctx.user.role === "member") { const { accessedServices } = await findMemberById( ctx.user.id, ctx.session.activeOrganizationId, ); await checkProjectAccess( ctx.user.id, "access", ctx.session.activeOrganizationId, input.projectId, ); const project = await db.query.projects.findFirst({ where: and( eq(projects.projectId, input.projectId), eq(projects.organizationId, ctx.session.activeOrganizationId), ), with: { environments: { with: { applications: { where: buildServiceFilter( applications.applicationId, accessedServices, ), }, compose: { where: buildServiceFilter( compose.composeId, accessedServices, ), }, mariadb: { where: buildServiceFilter( mariadb.mariadbId, accessedServices, ), }, mongo: { where: buildServiceFilter(mongo.mongoId, accessedServices), }, mysql: { where: buildServiceFilter(mysql.mysqlId, accessedServices), }, postgres: { where: buildServiceFilter( postgres.postgresId, accessedServices, ), }, redis: { where: buildServiceFilter(redis.redisId, accessedServices), }, }, }, }, }); if (!project) { throw new TRPCError({ code: "NOT_FOUND", message: "Project not found", }); } return project; } const project = await findProjectById(input.projectId); if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } return project; }), all: protectedProcedure.query(async ({ ctx }) => { if (ctx.user.role === "member") { const { accessedProjects, accessedEnvironments, accessedServices } = await findMemberById(ctx.user.id, ctx.session.activeOrganizationId); if (accessedProjects.length === 0) { return []; } // Build environment filter const environmentFilter = accessedEnvironments.length === 0 ? sql`false` : sql`${environments.environmentId} IN (${sql.join( accessedEnvironments.map((envId) => sql`${envId}`), sql`, `, )})`; return await db.query.projects.findMany({ where: and( sql`${projects.projectId} IN (${sql.join( accessedProjects.map((projectId) => sql`${projectId}`), sql`, `, )})`, eq(projects.organizationId, ctx.session.activeOrganizationId), ), with: { environments: { where: environmentFilter, with: { applications: { where: buildServiceFilter( applications.applicationId, accessedServices, ), columns: { applicationId: true, name: true, applicationStatus: true, }, }, mariadb: { where: buildServiceFilter(mariadb.mariadbId, accessedServices), columns: { mariadbId: true, name: true, applicationStatus: true, }, }, mongo: { where: buildServiceFilter(mongo.mongoId, accessedServices), columns: { mongoId: true, name: true, applicationStatus: true, }, }, mysql: { where: buildServiceFilter(mysql.mysqlId, accessedServices), columns: { mysqlId: true, name: true, applicationStatus: true, }, }, postgres: { where: buildServiceFilter( postgres.postgresId, accessedServices, ), columns: { postgresId: true, name: true, applicationStatus: true, }, }, redis: { where: buildServiceFilter(redis.redisId, accessedServices), columns: { redisId: true, name: true, applicationStatus: true, }, }, compose: { where: buildServiceFilter(compose.composeId, accessedServices), columns: { composeId: true, name: true, composeStatus: true, }, }, }, columns: { environmentId: true, isDefault: true, name: true, }, }, }, orderBy: desc(projects.createdAt), }); } return await db.query.projects.findMany({ with: { environments: { with: { applications: { columns: { applicationId: true, name: true, applicationStatus: true, }, }, mariadb: { columns: { mariadbId: true, }, }, mongo: { columns: { mongoId: true, }, }, mysql: { columns: { mysqlId: true, }, }, postgres: { columns: { postgresId: true, }, }, redis: { columns: { redisId: true, }, }, compose: { columns: { composeId: true, name: true, composeStatus: true, }, }, }, columns: { name: true, environmentId: true, isDefault: true, }, }, }, where: eq(projects.organizationId, ctx.session.activeOrganizationId), orderBy: desc(projects.createdAt), }); }), /** All projects with full environments and services for the admin permissions UI. Admin only. */ allForPermissions: adminProcedure.query(async ({ ctx }) => { return await db.query.projects.findMany({ where: eq(projects.organizationId, ctx.session.activeOrganizationId), orderBy: desc(projects.createdAt), columns: { projectId: true, name: true, }, with: { environments: { columns: { environmentId: true, name: true, isDefault: true, }, with: { applications: { columns: { applicationId: true, appName: true, name: true, createdAt: true, applicationStatus: true, description: true, serverId: true, }, }, mariadb: { columns: { mariadbId: true, appName: true, name: true, createdAt: true, applicationStatus: true, description: true, serverId: true, }, }, postgres: { columns: { postgresId: true, appName: true, name: true, createdAt: true, applicationStatus: true, description: true, serverId: true, }, }, mysql: { columns: { mysqlId: true, appName: true, name: true, createdAt: true, applicationStatus: true, description: true, serverId: true, }, }, mongo: { columns: { mongoId: true, appName: true, name: true, createdAt: true, applicationStatus: true, description: true, serverId: true, }, }, redis: { columns: { redisId: true, appName: true, name: true, createdAt: true, applicationStatus: true, description: true, serverId: true, }, }, compose: { columns: { composeId: true, appName: true, name: true, createdAt: true, composeStatus: true, description: true, serverId: true, }, }, }, }, }, }); }), search: protectedProcedure .input( z.object({ q: z.string().optional(), name: z.string().optional(), description: z.string().optional(), limit: z.number().min(1).max(100).default(20), offset: z.number().min(0).default(0), }), ) .query(async ({ ctx, input }) => { const baseConditions = [ eq(projects.organizationId, ctx.session.activeOrganizationId), ]; if (input.q?.trim()) { const term = `%${input.q.trim()}%`; baseConditions.push( or( ilike(projects.name, term), ilike(projects.description ?? "", term), )!, ); } if (input.name?.trim()) { baseConditions.push(ilike(projects.name, `%${input.name.trim()}%`)); } if (input.description?.trim()) { baseConditions.push( ilike(projects.description ?? "", `%${input.description.trim()}%`), ); } if (ctx.user.role === "member") { const { accessedProjects } = await findMemberById( ctx.user.id, ctx.session.activeOrganizationId, ); if (accessedProjects.length === 0) return { items: [], total: 0 }; baseConditions.push( sql`${projects.projectId} IN (${sql.join( accessedProjects.map((id) => sql`${id}`), sql`, `, )})`, ); } const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db.query.projects.findMany({ where, limit: input.limit, offset: input.offset, orderBy: desc(projects.createdAt), columns: { projectId: true, name: true, description: true, createdAt: true, organizationId: true, env: true, }, }), db .select({ count: sql`count(*)::int` }) .from(projects) .where(where), ]); return { items, total: countResult[0]?.count ?? 0, }; }), remove: protectedProcedure .input(apiRemoveProject) .mutation(async ({ input, ctx }) => { try { if (ctx.user.role === "member") { await checkProjectAccess( ctx.user.id, "delete", ctx.session.activeOrganizationId, ); } const currentProject = await findProjectById(input.projectId); if ( currentProject.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this project", }); } const deletedProject = await deleteProject(input.projectId); return deletedProject; } catch (error) { throw error; } }), update: protectedProcedure .input(apiUpdateProject) .mutation(async ({ input, ctx }) => { try { const currentProject = await findProjectById(input.projectId); if ( currentProject.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this project", }); } const project = await updateProjectById(input.projectId, { ...input, }); return project; } catch (error) { throw error; } }), duplicate: protectedProcedure .input( z.object({ sourceEnvironmentId: z.string(), name: z.string(), description: z.string().optional(), includeServices: z.boolean().default(true), selectedServices: z .array( z.object({ id: z.string(), type: z.enum([ "application", "postgres", "mariadb", "mongo", "mysql", "redis", "compose", ]), }), ) .optional(), duplicateInSameProject: z.boolean().default(false), }), ) .mutation(async ({ ctx, input }) => { try { if (ctx.user.role === "member") { await checkProjectAccess( ctx.user.id, "create", ctx.session.activeOrganizationId, ); } // Get source project const sourceEnvironment = input.duplicateInSameProject ? await findEnvironmentById(input.sourceEnvironmentId) : null; if ( input.duplicateInSameProject && sourceEnvironment?.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } // Create new project or use existing one const targetProject = input.duplicateInSameProject ? sourceEnvironment : await createProject( { name: input.name, description: input.description, env: sourceEnvironment?.project.env, }, ctx.session.activeOrganizationId, ).then((value) => value.environment); console.log("targetProject", targetProject); if (input.includeServices) { const servicesToDuplicate = input.selectedServices || []; // Helper function to duplicate a service const duplicateService = async (id: string, type: string) => { switch (type) { case "application": { const { applicationId, domains, security, ports, registry, redirects, previewDeployments, mounts, appName, refreshToken, ...application } = await findApplicationById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newApplication = await createApplication({ ...application, appName: newAppName, name: input.duplicateInSameProject ? `${application.name} (copy)` : application.name, environmentId: targetProject?.environmentId || "", }); for (const domain of domains) { const { domainId, ...rest } = domain; await createDomain({ ...rest, applicationId: newApplication.applicationId, domainType: "application", }); } for (const port of ports) { const { portId, ...rest } = port; await createPort({ ...rest, applicationId: newApplication.applicationId, }); } for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newApplication.applicationId, serviceType: "application", }); } for (const redirect of redirects) { const { redirectId, ...rest } = redirect; await createRedirect({ ...rest, applicationId: newApplication.applicationId, }); } for (const secure of security) { const { securityId, ...rest } = secure; await createSecurity({ ...rest, applicationId: newApplication.applicationId, }); } for (const previewDeployment of previewDeployments) { const { previewDeploymentId, ...rest } = previewDeployment; await createPreviewDeployment({ ...rest, applicationId: newApplication.applicationId, domainId: undefined, }); } break; } case "postgres": { const { postgresId, mounts, backups, appName, ...postgres } = await findPostgresById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newPostgres = await createPostgres({ ...postgres, appName: newAppName, name: input.duplicateInSameProject ? `${postgres.name} (copy)` : postgres.name, environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newPostgres.postgresId, serviceType: "postgres", }); } for (const backup of backups) { const { backupId, appName: _appName, ...rest } = backup; await createBackup({ ...rest, postgresId: newPostgres.postgresId, }); } break; } case "mariadb": { const { mariadbId, mounts, backups, appName, ...mariadb } = await findMariadbById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newMariadb = await createMariadb({ ...mariadb, appName: newAppName, name: input.duplicateInSameProject ? `${mariadb.name} (copy)` : mariadb.name, environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newMariadb.mariadbId, serviceType: "mariadb", }); } for (const backup of backups) { const { backupId, appName: _appName, ...rest } = backup; await createBackup({ ...rest, mariadbId: newMariadb.mariadbId, }); } break; } case "mongo": { const { mongoId, mounts, backups, appName, ...mongo } = await findMongoById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newMongo = await createMongo({ ...mongo, appName: newAppName, name: input.duplicateInSameProject ? `${mongo.name} (copy)` : mongo.name, environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newMongo.mongoId, serviceType: "mongo", }); } for (const backup of backups) { const { backupId, appName: _appName, ...rest } = backup; await createBackup({ ...rest, mongoId: newMongo.mongoId, }); } break; } case "mysql": { const { mysqlId, mounts, backups, appName, ...mysql } = await findMySqlById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newMysql = await createMysql({ ...mysql, appName: newAppName, name: input.duplicateInSameProject ? `${mysql.name} (copy)` : mysql.name, environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newMysql.mysqlId, serviceType: "mysql", }); } for (const backup of backups) { const { backupId, appName: _appName, ...rest } = backup; await createBackup({ ...rest, mysqlId: newMysql.mysqlId, }); } break; } case "redis": { const { redisId, mounts, appName, ...redis } = await findRedisById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newRedis = await createRedis({ ...redis, appName: newAppName, name: input.duplicateInSameProject ? `${redis.name} (copy)` : redis.name, environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newRedis.redisId, serviceType: "redis", }); } break; } case "compose": { const { composeId, mounts, domains, appName, refreshToken, ...compose } = await findComposeById(id); const newAppName = appName.substring( 0, appName.lastIndexOf("-"), ); const newCompose = await createCompose({ ...compose, appName: newAppName, name: input.duplicateInSameProject ? `${compose.name} (copy)` : compose.name, environmentId: targetProject?.environmentId || "", }); for (const mount of mounts) { const { mountId, ...rest } = mount; await createMount({ ...rest, serviceId: newCompose.composeId, serviceType: "compose", }); } for (const domain of domains) { const { domainId, ...rest } = domain; await createDomain({ ...rest, composeId: newCompose.composeId, domainType: "compose", }); } break; } } }; // Duplicate selected services for (const service of servicesToDuplicate) { await duplicateService(service.id, service.type); } } if (!input.duplicateInSameProject && ctx.user.role === "member") { await addNewProject( ctx.user.id, targetProject?.projectId || "", ctx.session.activeOrganizationId, ); } return targetProject; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", message: `Error duplicating the project: ${error instanceof Error ? error.message : error}`, cause: error, }); } }), }); function buildServiceFilter( fieldName: AnyPgColumn, accessedServices: string[], ) { return accessedServices.length === 0 ? sql`false` : sql`${fieldName} IN (${sql.join( accessedServices.map((serviceId) => sql`${serviceId}`), sql`, `, )})`; }