mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-20 12:04:29 +00:00
Implemented a search feature in application, compose, environment, mariadb, mongo, mysql, postgres, project, and redis routers. Each search allows filtering by various parameters and respects user permissions based on their role. The search queries utilize optimized conditions for efficient data retrieval.
815 lines
20 KiB
TypeScript
815 lines
20 KiB
TypeScript
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 { 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,
|
|
),
|
|
with: { domains: true },
|
|
},
|
|
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),
|
|
},
|
|
compose: {
|
|
where: buildServiceFilter(compose.composeId, accessedServices),
|
|
with: { domains: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: desc(projects.createdAt),
|
|
});
|
|
}
|
|
|
|
return await db.query.projects.findMany({
|
|
with: {
|
|
environments: {
|
|
with: {
|
|
applications: {
|
|
with: {
|
|
domains: true,
|
|
},
|
|
},
|
|
mariadb: true,
|
|
mongo: true,
|
|
mysql: true,
|
|
postgres: true,
|
|
redis: true,
|
|
compose: {
|
|
with: {
|
|
domains: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
|
orderBy: desc(projects.createdAt),
|
|
});
|
|
}),
|
|
|
|
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<number>`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`, `,
|
|
)})`;
|
|
}
|