feat(services): add bulk service move functionality across projects

- Implement service move feature for applications, compose, databases, and other services
- Add move dialog with project selection for bulk service transfer
- Create move mutation endpoints for each service type
- Enhance project management with cross-project service relocation
- Improve user experience with error handling and success notifications
This commit is contained in:
Mauricio Siu
2025-03-08 18:39:02 -06:00
parent ff8d922f2b
commit b34987530e
8 changed files with 509 additions and 3 deletions

View File

@@ -668,4 +668,49 @@ export const applicationRouter = createTRPCRouter({
return stats;
}),
move: protectedProcedure
.input(
z.object({
applicationId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this application",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the application's projectId
const updatedApplication = await db
.update(applications)
.set({
projectId: input.targetProjectId,
})
.where(eq(applications.applicationId, input.applicationId))
.returning()
.then((res) => res[0]);
if (!updatedApplication) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move application",
});
}
return updatedApplication;
}),
});

View File

@@ -8,7 +8,7 @@ import {
apiFindCompose,
apiRandomizeCompose,
apiUpdateCompose,
compose,
compose as composeTable,
} from "@/server/db/schema";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
@@ -24,6 +24,7 @@ import { dump } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy";
@@ -157,8 +158,8 @@ export const composeRouter = createTRPCRouter({
4;
const result = await db
.delete(compose)
.where(eq(compose.composeId, input.composeId))
.delete(composeTable)
.where(eq(composeTable.composeId, input.composeId))
.returning();
const cleanupOperations = [
@@ -501,4 +502,48 @@ export const composeRouter = createTRPCRouter({
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
move: protectedProcedure
.input(
z.object({
composeId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the compose's projectId
const updatedCompose = await db
.update(composeTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(composeTable.composeId, input.composeId))
.returning()
.then((res) => res[0]);
if (!updatedCompose) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move compose",
});
}
return updatedCompose;
}),
});

View File

@@ -8,6 +8,7 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
mariadb as mariadbTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -30,6 +31,9 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
@@ -322,4 +326,47 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
mariadbId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mariadb's projectId
const updatedMariadb = await db
.update(mariadbTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mariadbTable.mariadbId, input.mariadbId))
.returning()
.then((res) => res[0]);
if (!updatedMariadb) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mariadb",
});
}
return updatedMariadb;
}),
});

View File

@@ -8,6 +8,7 @@ import {
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
mongo as mongoTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -30,6 +31,9 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
@@ -336,4 +340,47 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
mongoId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mongo's projectId
const updatedMongo = await db
.update(mongoTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mongoTable.mongoId, input.mongoId))
.returning()
.then((res) => res[0]);
if (!updatedMongo) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mongo",
});
}
return updatedMongo;
}),
});

View File

@@ -8,6 +8,7 @@ import {
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
mysql as mysqlTable,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -32,6 +33,9 @@ import {
updateMySqlById,
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const mysqlRouter = createTRPCRouter({
create: protectedProcedure
@@ -332,4 +336,47 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
mysqlId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mysql's projectId
const updatedMysql = await db
.update(mysqlTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mysqlTable.mysqlId, input.mysqlId))
.returning()
.then((res) => res[0]);
if (!updatedMysql) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mysql",
});
}
return updatedMysql;
}),
});

View File

@@ -8,6 +8,7 @@ import {
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
postgres as postgresTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -30,6 +31,9 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
@@ -352,4 +356,49 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
postgresId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the postgres's projectId
const updatedPostgres = await db
.update(postgresTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(postgresTable.postgresId, input.postgresId))
.returning()
.then((res) => res[0]);
if (!updatedPostgres) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move postgres",
});
}
return updatedPostgres;
}),
});

View File

@@ -8,6 +8,7 @@ import {
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
redis as redisTable,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -30,6 +31,9 @@ import {
updateRedisById,
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
@@ -316,4 +320,47 @@ export const redisRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
redisId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the redis's projectId
const updatedRedis = await db
.update(redisTable)
.set({
projectId: input.targetProjectId,
})
.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;
}),
});