import { addNewService, checkPortInUse, checkServiceAccess, createMount, createRedis, deployRedis, findEnvironmentById, findMemberById, findProjectById, findRedisById, IS_CLOUD, rebuildDatabase, removeRedisById, removeService, startService, startServiceRemote, stopService, stopServiceRemote, updateRedisById, } 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 { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { apiChangeRedisStatus, apiCreateRedis, apiDeployRedis, apiFindOneRedis, apiRebuildRedis, apiResetRedis, apiSaveEnvironmentVariablesRedis, apiSaveExternalPortRedis, apiUpdateRedis, redis as redisTable, } from "@/server/db/schema"; import { environments, projects } from "@/server/db/schema"; export const redisRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateRedis) .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", ); } if (IS_CLOUD && !input.serverId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to use a server to create a Redis", }); } if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } const newRedis = await createRedis({ ...input, }); if (ctx.user.role === "member") { await addNewService( ctx.user.id, newRedis.redisId, project.organizationId, ); } await createMount({ serviceId: newRedis.redisId, serviceType: "redis", volumeName: `${newRedis.appName}-data`, mountPath: "/data", type: "volume", }); return newRedis; } catch (error) { throw error; } }), one: protectedProcedure .input(apiFindOneRedis) .query(async ({ input, ctx }) => { if (ctx.user.role === "member") { await checkServiceAccess( ctx.user.id, input.redisId, ctx.session.activeOrganizationId, "access", ); } const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this Redis", }); } return redis; }), start: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this Redis", }); } if (redis.serverId) { await startServiceRemote(redis.serverId, redis.appName); } else { await startService(redis.appName); } await updateRedisById(input.redisId, { applicationStatus: "done", }); return redis; }), reload: protectedProcedure .input(apiResetRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this Redis", }); } if (redis.serverId) { await stopServiceRemote(redis.serverId, redis.appName); } else { await stopService(redis.appName); } await updateRedisById(input.redisId, { applicationStatus: "idle", }); if (redis.serverId) { await startServiceRemote(redis.serverId, redis.appName); } else { await startService(redis.appName); } await updateRedisById(input.redisId, { applicationStatus: "done", }); return true; }), stop: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this Redis", }); } if (redis.serverId) { await stopServiceRemote(redis.serverId, redis.appName); } else { await stopService(redis.appName); } await updateRedisById(input.redisId, { applicationStatus: "idle", }); return redis; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this external port", }); } if (input.externalPort) { const portCheck = await checkPortInUse( input.externalPort, redis.serverId || undefined, ); if (portCheck.isInUse) { throw new TRPCError({ code: "CONFLICT", message: `Port ${input.externalPort} is already in use by ${portCheck.conflictingContainer}`, }); } } await updateRedisById(input.redisId, { externalPort: input.externalPort, }); await deployRedis(input.redisId); return redis; }), deploy: protectedProcedure .input(apiDeployRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Redis", }); } return deployRedis(input.redisId); }), deployWithLogs: protectedProcedure .meta({ openapi: { path: "/deploy/redis-with-logs", method: "POST", override: true, enabled: false, }, }) .input(apiDeployRedis) .subscription(async function* ({ input, ctx, signal }) { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this Redis", }); } const queue: string[] = []; const done = false; deployRedis(input.redisId, (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(apiChangeRedisStatus) .mutation(async ({ input, ctx }) => { const mongo = await findRedisById(input.redisId); if ( mongo.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to change this Redis status", }); } await updateRedisById(input.redisId, { applicationStatus: input.applicationStatus, }); return mongo; }), remove: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { if (ctx.user.role === "member") { await checkServiceAccess( ctx.user.id, input.redisId, ctx.session.activeOrganizationId, "delete", ); } const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this Redis", }); } const cleanupOperations = [ async () => await removeService(redis?.appName, redis.serverId), async () => await removeRedisById(input.redisId), ]; for (const operation of cleanupOperations) { try { await operation(); } catch (_) {} } return redis; }), saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", }); } const updatedRedis = await updateRedisById(input.redisId, { env: input.env, }); if (!updatedRedis) { throw new TRPCError({ code: "BAD_REQUEST", message: "Error adding environment variables", }); } return true; }), update: protectedProcedure .input(apiUpdateRedis) .mutation(async ({ input }) => { const { redisId, ...rest } = input; const redis = await updateRedisById(redisId, { ...rest, }); if (!redis) { throw new TRPCError({ code: "BAD_REQUEST", message: "Error updating Redis", }); } return true; }), move: protectedProcedure .input( z.object({ redisId: z.string(), targetEnvironmentId: z.string(), }), ) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to move this redis", }); } 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 redis's projectId const updatedRedis = await db .update(redisTable) .set({ environmentId: input.targetEnvironmentId, }) .where(eq(redisTable.redisId, input.redisId)) .returning() .then((res) => res[0]); if (!updatedRedis) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to move redis", }); } return updatedRedis; }), rebuild: protectedProcedure .input(apiRebuildRedis) .mutation(async ({ input, ctx }) => { const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to rebuild this Redis database", }); } await rebuildDatabase(redis.redisId, "redis"); return true; }), search: protectedProcedure .input( z.object({ q: z.string().optional(), name: z.string().optional(), appName: z.string().optional(), description: z.string().optional(), projectId: z.string().optional(), environmentId: 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.projectId) { baseConditions.push(eq(environments.projectId, input.projectId)); } if (input.environmentId) { baseConditions.push(eq(redisTable.environmentId, input.environmentId)); } if (input.q?.trim()) { const term = `%${input.q.trim()}%`; baseConditions.push( or( ilike(redisTable.name, term), ilike(redisTable.appName, term), ilike(redisTable.description ?? "", term), )!, ); } if (input.name?.trim()) { baseConditions.push(ilike(redisTable.name, `%${input.name.trim()}%`)); } if (input.appName?.trim()) { baseConditions.push( ilike(redisTable.appName, `%${input.appName.trim()}%`), ); } if (input.description?.trim()) { baseConditions.push( ilike(redisTable.description ?? "", `%${input.description.trim()}%`), ); } if (ctx.user.role === "member") { const { accessedServices } = await findMemberById( ctx.user.id, ctx.session.activeOrganizationId, ); if (accessedServices.length === 0) return { items: [], total: 0 }; baseConditions.push( sql`${redisTable.redisId} IN (${sql.join( accessedServices.map((id) => sql`${id}`), sql`, `, )})`, ); } const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db .select({ redisId: redisTable.redisId, name: redisTable.name, appName: redisTable.appName, description: redisTable.description, environmentId: redisTable.environmentId, applicationStatus: redisTable.applicationStatus, createdAt: redisTable.createdAt, }) .from(redisTable) .innerJoin( environments, eq(redisTable.environmentId, environments.environmentId), ) .innerJoin(projects, eq(environments.projectId, projects.projectId)) .where(where) .orderBy(desc(redisTable.createdAt)) .limit(input.limit) .offset(input.offset), db .select({ count: sql`count(*)::int` }) .from(redisTable) .innerJoin( environments, eq(redisTable.environmentId, environments.environmentId), ) .innerJoin(projects, eq(environments.projectId, projects.projectId)) .where(where), ]); return { items, total: countResult[0]?.count ?? 0 }; }), });