diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 97dba570e..df3e81c82 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -7,6 +7,7 @@ import { findApplicationById, findEnvironmentById, findGitProviderById, + findMemberById, findProjectById, getApplicationStats, IS_CLOUD, @@ -32,7 +33,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; @@ -53,6 +54,8 @@ import { apiSaveGitProvider, apiUpdateApplication, applications, + environments, + projects, } from "@/server/db/schema"; import { deploymentWorker } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/queue-types"; @@ -1002,4 +1005,138 @@ export const applicationRouter = createTRPCRouter({ message: "Deployment cancellation only available in cloud version", }); }), + + search: protectedProcedure + .input( + z.object({ + q: z.string().optional(), + name: z.string().optional(), + appName: z.string().optional(), + description: z.string().optional(), + repository: z.string().optional(), + owner: z.string().optional(), + dockerImage: 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(applications.environmentId, input.environmentId), + ); + } + + if (input.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(applications.name, term), + ilike(applications.appName, term), + ilike(applications.description ?? "", term), + ilike(applications.repository ?? "", term), + ilike(applications.owner ?? "", term), + ilike(applications.dockerImage ?? "", term), + )!, + ); + } + + if (input.name?.trim()) { + baseConditions.push(ilike(applications.name, `%${input.name.trim()}%`)); + } + if (input.appName?.trim()) { + baseConditions.push( + ilike(applications.appName, `%${input.appName.trim()}%`), + ); + } + if (input.description?.trim()) { + baseConditions.push( + ilike( + applications.description ?? "", + `%${input.description.trim()}%`, + ), + ); + } + if (input.repository?.trim()) { + baseConditions.push( + ilike(applications.repository ?? "", `%${input.repository.trim()}%`), + ); + } + if (input.owner?.trim()) { + baseConditions.push( + ilike(applications.owner ?? "", `%${input.owner.trim()}%`), + ); + } + if (input.dockerImage?.trim()) { + baseConditions.push( + ilike( + applications.dockerImage ?? "", + `%${input.dockerImage.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`${applications.applicationId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + + const where = and(...baseConditions); + + const [items, countResult] = await Promise.all([ + db + .select({ + applicationId: applications.applicationId, + name: applications.name, + appName: applications.appName, + description: applications.description, + environmentId: applications.environmentId, + applicationStatus: applications.applicationStatus, + sourceType: applications.sourceType, + createdAt: applications.createdAt, + }) + .from(applications) + .innerJoin( + environments, + eq(applications.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(applications.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(applications) + .innerJoin( + environments, + eq(applications.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + + return { + items, + total: countResult[0]?.count ?? 0, + }; + }), }); diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index f868e2ae1..e3c803cd4 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -16,6 +16,7 @@ import { findDomainsByComposeId, findEnvironmentById, findGitProviderById, + findMemberById, findProjectById, findServerById, getComposeContainer, @@ -41,7 +42,7 @@ import { } from "@dokploy/server/templates/github"; import { processTemplate } from "@dokploy/server/templates/processors"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import _ from "lodash"; import { nanoid } from "nanoid"; import { parse } from "toml"; @@ -58,6 +59,8 @@ import { apiRedeployCompose, apiUpdateCompose, compose as composeTable, + environments, + projects, } from "@/server/db/schema"; import { deploymentWorker } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/queue-types"; @@ -1054,4 +1057,114 @@ export const composeRouter = createTRPCRouter({ message: "Deployment cancellation only available in cloud version", }); }), + + 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(composeTable.environmentId, input.environmentId), + ); + } + + if (input.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(composeTable.name, term), + ilike(composeTable.appName, term), + ilike(composeTable.description ?? "", term), + )!, + ); + } + + if (input.name?.trim()) { + baseConditions.push(ilike(composeTable.name, `%${input.name.trim()}%`)); + } + if (input.appName?.trim()) { + baseConditions.push( + ilike(composeTable.appName, `%${input.appName.trim()}%`), + ); + } + if (input.description?.trim()) { + baseConditions.push( + ilike( + composeTable.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`${composeTable.composeId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + + const where = and(...baseConditions); + + const [items, countResult] = await Promise.all([ + db + .select({ + composeId: composeTable.composeId, + name: composeTable.name, + appName: composeTable.appName, + description: composeTable.description, + environmentId: composeTable.environmentId, + composeStatus: composeTable.composeStatus, + sourceType: composeTable.sourceType, + createdAt: composeTable.createdAt, + }) + .from(composeTable) + .innerJoin( + environments, + eq(composeTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(composeTable.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(composeTable) + .innerJoin( + environments, + eq(composeTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + + return { + items, + total: countResult[0]?.count ?? 0, + }; + }), }); diff --git a/apps/dokploy/server/api/routers/environment.ts b/apps/dokploy/server/api/routers/environment.ts index 9f5eb45c2..16376e9e0 100644 --- a/apps/dokploy/server/api/routers/environment.ts +++ b/apps/dokploy/server/api/routers/environment.ts @@ -11,7 +11,9 @@ import { findMemberById, updateEnvironmentById, } 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 { @@ -21,6 +23,7 @@ import { apiRemoveEnvironment, apiUpdateEnvironment, } from "@/server/db/schema"; +import { environments, projects } from "@/server/db/schema"; // Helper function to filter services within an environment based on user permissions const filterEnvironmentServices = ( @@ -358,4 +361,92 @@ export const environmentRouter = createTRPCRouter({ }); } }), + + search: protectedProcedure + .input( + z.object({ + q: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + projectId: 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.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(environments.name, term), + ilike(environments.description ?? "", term), + )!, + ); + } + + if (input.name?.trim()) { + baseConditions.push(ilike(environments.name, `%${input.name.trim()}%`)); + } + if (input.description?.trim()) { + baseConditions.push( + ilike( + environments.description ?? "", + `%${input.description.trim()}%`, + ), + ); + } + + if (ctx.user.role === "member") { + const { accessedEnvironments } = await findMemberById( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedEnvironments.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${environments.environmentId} IN (${sql.join( + accessedEnvironments.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + + const where = and(...baseConditions); + + const [items, countResult] = await Promise.all([ + db + .select({ + environmentId: environments.environmentId, + name: environments.name, + description: environments.description, + createdAt: environments.createdAt, + env: environments.env, + projectId: environments.projectId, + isDefault: environments.isDefault, + }) + .from(environments) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(environments.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(environments) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + + return { + items, + total: countResult[0]?.count ?? 0, + }; + }), }); diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index bddc71b09..567cd4ad8 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -8,6 +8,7 @@ import { findBackupsByDbId, findEnvironmentById, findMariadbById, + findMemberById, findProjectById, IS_CLOUD, rebuildDatabase, @@ -22,7 +23,7 @@ import { import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { @@ -37,6 +38,7 @@ import { apiUpdateMariaDB, mariadb as mariadbTable, } from "@/server/db/schema"; +import { environments, projects } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; export const mariadbRouter = createTRPCRouter({ create: protectedProcedure @@ -446,4 +448,102 @@ export const mariadbRouter = createTRPCRouter({ await rebuildDatabase(mariadb.mariadbId, "mariadb"); 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(mariadbTable.environmentId, input.environmentId), + ); + } + if (input.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(mariadbTable.name, term), + ilike(mariadbTable.appName, term), + ilike(mariadbTable.description ?? "", term), + )!, + ); + } + if (input.name?.trim()) { + baseConditions.push(ilike(mariadbTable.name, `%${input.name.trim()}%`)); + } + if (input.appName?.trim()) { + baseConditions.push( + ilike(mariadbTable.appName, `%${input.appName.trim()}%`), + ); + } + if (input.description?.trim()) { + baseConditions.push( + ilike( + mariadbTable.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`${mariadbTable.mariadbId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + const where = and(...baseConditions); + const [items, countResult] = await Promise.all([ + db + .select({ + mariadbId: mariadbTable.mariadbId, + name: mariadbTable.name, + appName: mariadbTable.appName, + description: mariadbTable.description, + environmentId: mariadbTable.environmentId, + applicationStatus: mariadbTable.applicationStatus, + createdAt: mariadbTable.createdAt, + }) + .from(mariadbTable) + .innerJoin( + environments, + eq(mariadbTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(mariadbTable.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(mariadbTable) + .innerJoin( + environments, + eq(mariadbTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + return { items, total: countResult[0]?.count ?? 0 }; + }), }); diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index e8454c8a4..ec0a4041c 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -7,6 +7,7 @@ import { deployMongo, findBackupsByDbId, findEnvironmentById, + findMemberById, findMongoById, findProjectById, IS_CLOUD, @@ -21,7 +22,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { @@ -36,6 +37,7 @@ import { apiUpdateMongo, mongo as mongoTable, } from "@/server/db/schema"; +import { environments, projects } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; export const mongoRouter = createTRPCRouter({ create: protectedProcedure @@ -476,4 +478,97 @@ export const mongoRouter = createTRPCRouter({ 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(mongoTable.environmentId, input.environmentId)); + } + if (input.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(mongoTable.name, term), + ilike(mongoTable.appName, term), + ilike(mongoTable.description ?? "", term), + )!, + ); + } + if (input.name?.trim()) { + baseConditions.push(ilike(mongoTable.name, `%${input.name.trim()}%`)); + } + if (input.appName?.trim()) { + baseConditions.push( + ilike(mongoTable.appName, `%${input.appName.trim()}%`), + ); + } + if (input.description?.trim()) { + baseConditions.push( + ilike(mongoTable.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`${mongoTable.mongoId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + const where = and(...baseConditions); + const [items, countResult] = await Promise.all([ + db + .select({ + mongoId: mongoTable.mongoId, + name: mongoTable.name, + appName: mongoTable.appName, + description: mongoTable.description, + environmentId: mongoTable.environmentId, + applicationStatus: mongoTable.applicationStatus, + createdAt: mongoTable.createdAt, + }) + .from(mongoTable) + .innerJoin( + environments, + eq(mongoTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(mongoTable.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(mongoTable) + .innerJoin( + environments, + eq(mongoTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + return { items, total: countResult[0]?.count ?? 0 }; + }), }); diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index b1bc10f32..5a00ef0d0 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -7,6 +7,7 @@ import { deployMySql, findBackupsByDbId, findEnvironmentById, + findMemberById, findMySqlById, findProjectById, IS_CLOUD, @@ -21,7 +22,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { @@ -34,7 +35,9 @@ import { apiSaveEnvironmentVariablesMySql, apiSaveExternalPortMySql, apiUpdateMySql, + environments, mysql as mysqlTable, + projects, } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; @@ -471,4 +474,97 @@ export const mysqlRouter = createTRPCRouter({ 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(mysqlTable.environmentId, input.environmentId)); + } + if (input.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(mysqlTable.name, term), + ilike(mysqlTable.appName, term), + ilike(mysqlTable.description ?? "", term), + )!, + ); + } + if (input.name?.trim()) { + baseConditions.push(ilike(mysqlTable.name, `%${input.name.trim()}%`)); + } + if (input.appName?.trim()) { + baseConditions.push( + ilike(mysqlTable.appName, `%${input.appName.trim()}%`), + ); + } + if (input.description?.trim()) { + baseConditions.push( + ilike(mysqlTable.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`${mysqlTable.mysqlId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + const where = and(...baseConditions); + const [items, countResult] = await Promise.all([ + db + .select({ + mysqlId: mysqlTable.mysqlId, + name: mysqlTable.name, + appName: mysqlTable.appName, + description: mysqlTable.description, + environmentId: mysqlTable.environmentId, + applicationStatus: mysqlTable.applicationStatus, + createdAt: mysqlTable.createdAt, + }) + .from(mysqlTable) + .innerJoin( + environments, + eq(mysqlTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(mysqlTable.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(mysqlTable) + .innerJoin( + environments, + eq(mysqlTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + return { items, total: countResult[0]?.count ?? 0 }; + }), }); diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index d9f69330c..48de9d5a2 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -7,6 +7,7 @@ import { deployPostgres, findBackupsByDbId, findEnvironmentById, + findMemberById, findPostgresById, findProjectById, getMountPath, @@ -22,7 +23,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { @@ -37,6 +38,7 @@ import { apiUpdatePostgres, postgres as postgresTable, } from "@/server/db/schema"; +import { environments, projects } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; export const postgresRouter = createTRPCRouter({ @@ -483,4 +485,104 @@ export const postgresRouter = createTRPCRouter({ 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(postgresTable.environmentId, input.environmentId), + ); + } + if (input.q?.trim()) { + const term = `%${input.q.trim()}%`; + baseConditions.push( + or( + ilike(postgresTable.name, term), + ilike(postgresTable.appName, term), + ilike(postgresTable.description ?? "", term), + )!, + ); + } + if (input.name?.trim()) { + baseConditions.push( + ilike(postgresTable.name, `%${input.name.trim()}%`), + ); + } + if (input.appName?.trim()) { + baseConditions.push( + ilike(postgresTable.appName, `%${input.appName.trim()}%`), + ); + } + if (input.description?.trim()) { + baseConditions.push( + ilike( + postgresTable.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`${postgresTable.postgresId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + } + const where = and(...baseConditions); + const [items, countResult] = await Promise.all([ + db + .select({ + postgresId: postgresTable.postgresId, + name: postgresTable.name, + appName: postgresTable.appName, + description: postgresTable.description, + environmentId: postgresTable.environmentId, + applicationStatus: postgresTable.applicationStatus, + createdAt: postgresTable.createdAt, + }) + .from(postgresTable) + .innerJoin( + environments, + eq(postgresTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where) + .orderBy(desc(postgresTable.createdAt)) + .limit(input.limit) + .offset(input.offset), + db + .select({ count: sql`count(*)::int` }) + .from(postgresTable) + .innerJoin( + environments, + eq(postgresTable.environmentId, environments.environmentId), + ) + .innerJoin(projects, eq(environments.projectId, projects.projectId)) + .where(where), + ]); + return { items, total: countResult[0]?.count ?? 0 }; + }), }); diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index b1df68951..ee353594c 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -34,7 +34,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { and, desc, eq, sql } from "drizzle-orm"; +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"; @@ -277,6 +277,83 @@ export const projectRouter = createTRPCRouter({ }); }), + 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 }) => { diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index dfeff82bb..94939bd20 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -6,6 +6,7 @@ import { createRedis, deployRedis, findEnvironmentById, + findMemberById, findProjectById, findRedisById, IS_CLOUD, @@ -20,7 +21,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { @@ -35,6 +36,7 @@ import { apiUpdateRedis, redis as redisTable, } from "@/server/db/schema"; +import { environments, projects } from "@/server/db/schema"; export const redisRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateRedis) @@ -450,4 +452,97 @@ export const redisRouter = createTRPCRouter({ 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 }; + }), });