Merge pull request #3846 from Dokploy/feat/add-more-endpoints-for-search

feat: add search functionality across multiple routers with member ac…
This commit is contained in:
Mauricio Siu
2026-03-01 01:27:38 -06:00
committed by GitHub
9 changed files with 914 additions and 8 deletions

View File

@@ -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<number>`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,
};
}),
});

View File

@@ -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<number>`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,
};
}),
});

View File

@@ -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<number>`count(*)::int` })
.from(environments)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
});

View File

@@ -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<number>`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 };
}),
});

View File

@@ -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<number>`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 };
}),
});

View File

@@ -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<number>`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 };
}),
});

View File

@@ -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<number>`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 };
}),
});

View File

@@ -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<number>`count(*)::int` })
.from(projects)
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
remove: protectedProcedure
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {

View File

@@ -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<number>`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 };
}),
});