chore: move logics to FilterSegmentRepository (#21648)

* chore: move logics to FilterSegmentRepository

* add explicit select

* replace TRPCError with Error

* convert to non static methods and implement the interface

* apply feedback

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
Eunjae Lee
2025-06-13 13:49:26 +02:00
committed by GitHub
co-authored by sean-brydon
parent b39404860f
commit 8078c5657b
18 changed files with 578 additions and 466 deletions
@@ -1,4 +1,4 @@
import prismock from "../../../../../../../tests/libs/__mocks__/prisma";
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
@@ -6,12 +6,15 @@ import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { createHandler } from "../create.handler";
import { type TCreateFilterSegmentInputSchema } from "../create.schema";
import { FilterSegmentRepository } from "../../filterSegment";
import type { TCreateFilterSegmentInputSchema } from "../../filterSegment.type";
describe("createHandler", () => {
const repository = new FilterSegmentRepository();
describe("FilterSegmentRepository.create()", () => {
const userId = 1;
const mockUser = {
id: 1,
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
@@ -40,8 +43,8 @@ describe("createHandler", () => {
name: "My Bookings",
};
const result = await createHandler({
ctx: { user: mockUser },
const result = await repository.create({
userId,
input,
});
@@ -74,8 +77,8 @@ describe("createHandler", () => {
name: "Team Bookings",
};
const result = await createHandler({
ctx: { user: mockUser },
const result = await repository.create({
userId,
input,
});
@@ -109,8 +112,8 @@ describe("createHandler", () => {
};
await expect(
createHandler({
ctx: { user: mockUser },
repository.create({
userId,
input,
})
).rejects.toThrow("You must be a team admin or owner to create team segments");
@@ -124,8 +127,8 @@ describe("createHandler", () => {
};
await expect(
createHandler({
ctx: { user: mockUser },
repository.create({
userId,
// @ts-expect-error - Testing invalid input where teamId is missing for TEAM scope
input: invalidInput,
})
@@ -141,8 +144,8 @@ describe("createHandler", () => {
};
await expect(
createHandler({
ctx: { user: mockUser },
repository.create({
userId,
// @ts-expect-error - Testing invalid input where teamId is present for USER scope
input: invalidInput,
})
@@ -1,16 +1,19 @@
import prismock from "../../../../../../../tests/libs/__mocks__/prisma";
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { deleteHandler } from "../delete.handler";
import { type TDeleteFilterSegmentInputSchema } from "../delete.schema";
import { FilterSegmentRepository } from "../../filterSegment";
import { type TDeleteFilterSegmentInputSchema } from "../../filterSegment.type";
describe("deleteHandler", () => {
const repository = new FilterSegmentRepository();
describe("FilterSegmentRepository.delete()", () => {
const userId = 1;
const mockUser = {
id: 1,
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
@@ -34,15 +37,12 @@ describe("deleteHandler", () => {
id: segment.id,
};
const result = await deleteHandler({
ctx: { user: mockUser },
input,
});
expect(result).toEqual({
id: segment.id,
message: "Filter segment deleted successfully",
});
await expect(
repository.delete({
userId,
id: segment.id,
})
).resolves.not.toThrow();
// Verify segment is deleted
const deletedSegment = await prismock.filterSegment.findUnique({
@@ -90,15 +90,12 @@ describe("deleteHandler", () => {
id: segment.id,
};
const result = await deleteHandler({
ctx: { user: mockUser },
input,
});
expect(result).toEqual({
id: segment.id,
message: "Filter segment deleted successfully",
});
await expect(
repository.delete({
userId,
id: segment.id,
})
).resolves.not.toThrow();
// Verify segment is deleted
const deletedSegment = await prismock.filterSegment.findUnique({
@@ -113,9 +110,9 @@ describe("deleteHandler", () => {
};
await expect(
deleteHandler({
ctx: { user: mockUser },
input,
repository.delete({
userId,
id: 999,
})
).rejects.toThrow("Filter segment not found or you don't have permission to delete it");
});
@@ -160,9 +157,9 @@ describe("deleteHandler", () => {
};
await expect(
deleteHandler({
ctx: { user: mockUser },
input,
repository.delete({
userId,
id: segment.id,
})
).rejects.toThrow("Filter segment not found or you don't have permission to delete it");
});
@@ -1,16 +1,19 @@
import prismock from "../../../../../../../tests/libs/__mocks__/prisma";
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { listHandler } from "../list.handler";
import { type TListFilterSegmentsInputSchema } from "../list.schema";
import { FilterSegmentRepository } from "../../filterSegment";
import { type TListFilterSegmentsInputSchema } from "../../filterSegment.type";
describe("listHandler", () => {
const repository = new FilterSegmentRepository();
describe("FilterSegmentRepository.get()", () => {
const userId = 1;
const mockUser = {
id: 1,
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
@@ -48,9 +51,9 @@ describe("listHandler", () => {
tableIdentifier: "bookings",
};
const result = await listHandler({
ctx: { user: mockUser },
input,
const result = await repository.get({
userId,
tableIdentifier: input.tableIdentifier,
});
expect(result.segments).toHaveLength(2);
@@ -132,9 +135,9 @@ describe("listHandler", () => {
tableIdentifier: "bookings",
};
const result = await listHandler({
ctx: { user: mockUser },
input,
const result = await repository.get({
userId,
tableIdentifier: input.tableIdentifier,
});
expect(result.segments).toHaveLength(2);
@@ -216,9 +219,9 @@ describe("listHandler", () => {
tableIdentifier: "bookings",
};
const result = await listHandler({
ctx: { user: mockUser },
input,
const result = await repository.get({
userId,
tableIdentifier: input.tableIdentifier,
});
expect(result.segments).toHaveLength(2);
@@ -251,9 +254,9 @@ describe("listHandler", () => {
tableIdentifier: "bookings",
};
const result = await listHandler({
ctx: { user: mockUser },
input,
const result = await repository.get({
userId,
tableIdentifier: input.tableIdentifier,
});
expect(result).toEqual({
@@ -261,4 +264,74 @@ describe("listHandler", () => {
preferredSegmentId: null,
});
});
it("should return segments with user preferences", async () => {
// Create a filter segment
const segment = await prismock.filterSegment.create({
data: {
userId: mockUser.id,
scope: "USER",
tableIdentifier: "contacts",
name: "Important Contacts",
activeFilters: [{ f: "priority" }],
sorting: [{ id: "lastContact", desc: true }],
columnVisibility: {},
columnSizing: {},
perPage: 50,
},
});
// Create a user preference for this segment
await prismock.userFilterSegmentPreference.create({
data: {
userId: mockUser.id,
tableIdentifier: "contacts",
segmentId: segment.id,
},
});
const result = await repository.get({
userId: mockUser.id,
tableIdentifier: "contacts",
});
expect(result.segments).toHaveLength(1);
expect(result.preferredSegmentId).toBe(segment.id);
expect(result.segments[0].name).toBe("Important Contacts");
});
it("should handle segments with null JSON fields gracefully", async () => {
// Create segment with minimal data (some fields will be null/undefined)
await prismock.filterSegment.create({
data: {
userId: mockUser.id,
scope: "USER",
tableIdentifier: "reports",
name: "Basic Report",
activeFilters: undefined, // This should be parsed to []
sorting: undefined, // This should be parsed to []
columnVisibility: undefined, // This should be parsed to {}
columnSizing: undefined, // This should be parsed to {}
perPage: 10,
searchTerm: null,
},
});
const result = await repository.get({
userId: mockUser.id,
tableIdentifier: "reports",
});
expect(result.segments).toHaveLength(1);
expect(result.segments[0]).toEqual(
expect.objectContaining({
name: "Basic Report",
activeFilters: [], // Should default to empty array
sorting: [], // Should default to empty array
columnVisibility: {}, // Should default to empty object
columnSizing: {}, // Should default to empty object
perPage: 10,
})
);
});
});
@@ -1,4 +1,4 @@
import prismock from "../../../../../../../tests/libs/__mocks__/prisma";
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
@@ -6,12 +6,15 @@ import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { updateHandler } from "../update.handler";
import { type TUpdateFilterSegmentInputSchema } from "../update.schema";
import { FilterSegmentRepository } from "../../filterSegment";
import { type TUpdateFilterSegmentInputSchema } from "../../filterSegment.type";
describe("updateHandler", () => {
const repository = new FilterSegmentRepository();
describe("FilterSegmentRepository.update()", () => {
const userId = 1;
const mockUser = {
id: 1,
id: userId,
name: "Test User",
} as NonNullable<TrpcSessionUser>;
@@ -61,8 +64,8 @@ describe("updateHandler", () => {
],
};
const result = await updateHandler({
ctx: { user: mockUser },
const result = await repository.update({
userId,
input,
});
@@ -130,8 +133,8 @@ describe("updateHandler", () => {
],
};
const result = await updateHandler({
ctx: { user: mockUser },
const result = await repository.update({
userId,
input,
});
@@ -171,8 +174,8 @@ describe("updateHandler", () => {
};
await expect(
updateHandler({
ctx: { user: mockUser },
repository.update({
userId,
input,
})
).rejects.toThrow("Filter segment not found or you don't have permission to update it");
@@ -236,8 +239,8 @@ describe("updateHandler", () => {
};
await expect(
updateHandler({
ctx: { user: mockUser },
repository.update({
userId,
input,
})
).rejects.toThrow("Filter segment not found or you don't have permission to update it");
+276 -2
View File
@@ -1,7 +1,281 @@
import type { FilterSegmentOutput } from "@calcom/features/data-table/lib/types";
import {
ZActiveFilters,
ZSortingState,
ZColumnSizing,
ZColumnVisibility,
} from "@calcom/features/data-table/lib/types";
import { prisma } from "@calcom/prisma";
import type { FilterSegment, UserFilterSegmentPreference } from "@calcom/prisma/client";
export class FilterSegmentRepository {
static async setPreference({
import type { TCreateFilterSegmentInputSchema, TUpdateFilterSegmentInputSchema } from "./filterSegment.type";
export interface IFilterSegmentRepository {
get({ userId, tableIdentifier }: { userId: number; tableIdentifier: string }): Promise<{
segments: FilterSegmentOutput[];
preferredSegmentId: number | null;
}>;
create({
userId,
input,
}: {
userId: number;
input: TCreateFilterSegmentInputSchema;
}): Promise<FilterSegment>;
update({
userId,
input,
}: {
userId: number;
input: TUpdateFilterSegmentInputSchema;
}): Promise<FilterSegment>;
delete({ userId, id }: { userId: number; id: number }): Promise<void>;
setPreference({
userId,
tableIdentifier,
segmentId,
}: {
userId: number;
tableIdentifier: string;
segmentId: number | null;
}): Promise<UserFilterSegmentPreference | null>;
}
export class FilterSegmentRepository implements IFilterSegmentRepository {
async get({ userId, tableIdentifier }: { userId: number; tableIdentifier: string }) {
// Get all teams that the user is a member of
const userTeamIds = await prisma.membership
.findMany({
where: {
userId,
accepted: true,
},
select: {
teamId: true,
},
})
.then((memberships) => memberships.map((m) => m.teamId));
// Fetch both user-scoped and team-scoped segments
const segments = await prisma.filterSegment.findMany({
where: {
tableIdentifier,
OR: [
// User-scoped segments created by the current user
{
scope: "USER",
userId,
},
// Team-scoped segments for teams the user belongs to
{
scope: "TEAM",
teamId: {
in: userTeamIds,
},
},
],
},
select: {
id: true,
name: true,
tableIdentifier: true,
scope: true,
activeFilters: true,
sorting: true,
columnVisibility: true,
columnSizing: true,
perPage: true,
searchTerm: true,
createdAt: true,
updatedAt: true,
userId: true,
teamId: true,
team: {
select: {
id: true,
name: true,
},
},
},
orderBy: [
{ scope: "desc" }, // USER segments first, then TEAM segments
{ createdAt: "desc" }, // Newest first within each scope
],
});
const parsedSegments: FilterSegmentOutput[] = segments.map((segment) => ({
...segment,
activeFilters: ZActiveFilters.catch([]).parse(segment.activeFilters),
sorting: ZSortingState.catch([]).parse(segment.sorting),
columnVisibility: ZColumnVisibility.catch({}).parse(segment.columnVisibility),
columnSizing: ZColumnSizing.catch({}).parse(segment.columnSizing),
}));
const preference = await prisma.userFilterSegmentPreference.findUnique({
where: {
userId_tableIdentifier: {
userId,
tableIdentifier,
},
},
select: {
segmentId: true,
},
});
return {
segments: parsedSegments,
preferredSegmentId: preference?.segmentId || null,
};
}
async create({ userId, input }: { userId: number; input: TCreateFilterSegmentInputSchema }) {
const { scope, teamId, ...filterData } = input;
// If scope is TEAM, verify user has admin/owner permissions
if (scope === "TEAM") {
if (!teamId) {
throw new Error("Team ID is required for team scope");
}
const membership = await prisma.membership.findFirst({
select: {
id: true,
},
where: {
userId,
teamId,
accepted: true,
role: {
in: ["ADMIN", "OWNER"],
},
},
});
if (!membership) {
throw new Error("You must be a team admin or owner to create team segments");
}
}
// For USER scope, ensure no teamId is provided
if (scope === "USER" && teamId) {
throw new Error("Team ID is not allowed for user scope");
}
// Create the filter segment
const filterSegment = await prisma.filterSegment.create({
data: {
...filterData,
scope,
...(scope === "TEAM" ? { teamId } : {}),
userId,
},
});
return filterSegment;
}
async update({ userId, input }: { userId: number; input: TUpdateFilterSegmentInputSchema }) {
const { id, name, activeFilters, sorting, columnVisibility, columnSizing, perPage, searchTerm } = input;
// First, fetch the existing segment to check permissions
const existingSegment = await prisma.filterSegment.findFirst({
where: {
id,
...(input.scope === "TEAM"
? {
scope: "TEAM",
teamId: input.teamId,
team: {
members: {
some: {
userId,
accepted: true,
role: {
in: ["ADMIN", "OWNER"],
},
},
},
},
}
: {
scope: "USER",
userId,
}),
},
select: {
id: true,
},
});
if (!existingSegment) {
throw new Error("Filter segment not found or you don't have permission to update it");
}
// Update the filter segment with only the allowed fields
const updatedSegment = await prisma.filterSegment.update({
where: { id },
data: {
name,
activeFilters,
sorting,
columnVisibility,
columnSizing,
perPage,
searchTerm,
},
});
return updatedSegment;
}
async delete({ userId, id }: { userId: number; id: number }) {
// First, fetch the existing segment to check permissions
const existingSegment = await prisma.filterSegment.findFirst({
where: {
id,
OR: [
{
scope: "TEAM",
teamId: { not: null },
team: {
members: {
some: {
userId,
accepted: true,
role: {
in: ["ADMIN", "OWNER"],
},
},
},
},
},
{
scope: "USER",
userId,
},
],
},
select: {
id: true,
},
});
if (!existingSegment) {
throw new Error("Filter segment not found or you don't have permission to delete it");
}
// Delete the filter segment
await prisma.filterSegment.delete({
where: { id },
});
}
async setPreference({
userId,
tableIdentifier,
segmentId,
@@ -0,0 +1,100 @@
import { z } from "zod";
import type { FilterSegmentOutput } from "@calcom/features/data-table/lib/types";
import {
ZActiveFilters,
ZSortingState,
ZColumnSizing,
ZColumnVisibility,
} from "@calcom/features/data-table/lib/types";
export type FilterSegmentsListResponse = {
segments: FilterSegmentOutput[];
preferredSegmentId: number | null;
};
export const ZListFilterSegmentsInputSchema = z.object({
tableIdentifier: z.string(),
});
export type TListFilterSegmentsInputSchema = z.infer<typeof ZListFilterSegmentsInputSchema>;
export const ZDeleteFilterSegmentInputSchema = z.object({
id: z.number().int().positive(),
});
export type TDeleteFilterSegmentInputSchema = z.infer<typeof ZDeleteFilterSegmentInputSchema>;
// Base schema for common fields
const baseCreateSchema = {
name: z.string().min(1),
tableIdentifier: z.string().min(1),
activeFilters: ZActiveFilters.optional(),
sorting: ZSortingState.optional(),
columnVisibility: ZColumnVisibility.optional(),
columnSizing: ZColumnSizing.optional(),
searchTerm: z.string().nullable().optional(),
perPage: z.number().int().min(1),
};
// Schema for team scope - requires teamId
const teamCreateSchema = z.object({
scope: z.literal("TEAM"),
teamId: z.number().int().positive(),
...baseCreateSchema,
});
// Schema for user scope - no teamId allowed
const userCreateSchema = z.object({
scope: z.literal("USER"),
teamId: z.undefined().optional(),
...baseCreateSchema,
});
export const ZCreateFilterSegmentInputSchema = z.discriminatedUnion("scope", [
teamCreateSchema,
userCreateSchema,
]);
export type TCreateFilterSegmentInputSchema = z.infer<typeof ZCreateFilterSegmentInputSchema>;
export const ZSetFilterSegmentPreferenceInputSchema = z.object({
tableIdentifier: z.string(),
segmentId: z.number().nullable(),
});
export type TSetFilterSegmentPreferenceInputSchema = z.infer<typeof ZSetFilterSegmentPreferenceInputSchema>;
// Base schema for common fields that can be updated
const baseUpdateSchema = {
name: z.string().min(1).optional(),
tableIdentifier: z.string().min(1).optional(),
activeFilters: ZActiveFilters.optional(),
sorting: ZSortingState.optional(),
columnVisibility: ZColumnVisibility.optional(),
columnSizing: ZColumnSizing.optional(),
searchTerm: z.string().nullable().optional(),
perPage: z.number().int().min(1).optional(),
};
// Schema for team scope updates
const teamUpdateSchema = z.object({
id: z.number().int().positive(),
scope: z.literal("TEAM"),
teamId: z.number().int().positive(),
...baseUpdateSchema,
});
// Schema for user scope updates
const userUpdateSchema = z.object({
id: z.number().int().positive(),
scope: z.literal("USER"),
...baseUpdateSchema,
});
export const ZUpdateFilterSegmentInputSchema = z.discriminatedUnion("scope", [
teamUpdateSchema,
userUpdateSchema,
]);
export type TUpdateFilterSegmentInputSchema = z.infer<typeof ZUpdateFilterSegmentInputSchema>;
@@ -1,17 +1,13 @@
import {
ZCreateFilterSegmentInputSchema,
ZDeleteFilterSegmentInputSchema,
ZListFilterSegmentsInputSchema,
ZSetFilterSegmentPreferenceInputSchema,
ZUpdateFilterSegmentInputSchema,
} from "@calcom/lib/server/repository/filterSegment.type";
import authedProcedure from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { ZCreateFilterSegmentInputSchema } from "./create.schema";
import { ZDeleteFilterSegmentInputSchema } from "./delete.schema";
import { ZListFilterSegmentsInputSchema } from "./list.schema";
import { ZSetFilterSegmentPreferenceInputSchema } from "./preference.schema";
import { ZUpdateFilterSegmentInputSchema } from "./update.schema";
type FilterSegmentsRouterHandlerCache = {
list?: typeof import("./list.handler").listHandler;
create?: typeof import("./create.handler").createHandler;
update?: typeof import("./update.handler").updateHandler;
delete?: typeof import("./delete.handler").deleteHandler;
};
export const filterSegmentsRouter = router({
list: authedProcedure.input(ZListFilterSegmentsInputSchema).query(async ({ input, ctx }) => {
@@ -24,27 +20,27 @@ export const filterSegmentsRouter = router({
}),
create: authedProcedure.input(ZCreateFilterSegmentInputSchema).mutation(async ({ input, ctx }) => {
const { createHandler } = await import("./create.handler");
const { createFilterSegmentHandler } = await import("./create.handler");
return createHandler({
return createFilterSegmentHandler({
ctx,
input,
});
}),
update: authedProcedure.input(ZUpdateFilterSegmentInputSchema).mutation(async ({ input, ctx }) => {
const { updateHandler } = await import("./update.handler");
const { updateFilterSegmentHandler } = await import("./update.handler");
return updateHandler({
return updateFilterSegmentHandler({
ctx,
input,
});
}),
delete: authedProcedure.input(ZDeleteFilterSegmentInputSchema).mutation(async ({ input, ctx }) => {
const { deleteHandler } = await import("./delete.handler");
const { deleteFilterSegmentHandler } = await import("./delete.handler");
return deleteHandler({
return deleteFilterSegmentHandler({
ctx,
input,
});
@@ -53,9 +49,9 @@ export const filterSegmentsRouter = router({
setPreference: authedProcedure
.input(ZSetFilterSegmentPreferenceInputSchema)
.mutation(async ({ input, ctx }) => {
const { setPreferenceHandler } = await import("./preference.handler");
const { setFilterSegmentPreferenceHandler } = await import("./preference.handler");
return setPreferenceHandler({
return setFilterSegmentPreferenceHandler({
ctx,
input,
});
@@ -1,11 +1,8 @@
import { prisma } from "@calcom/prisma";
import { FilterSegmentRepository } from "@calcom/lib/server/repository/filterSegment";
import type { TCreateFilterSegmentInputSchema } from "@calcom/lib/server/repository/filterSegment.type";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TCreateFilterSegmentInputSchema } from "./create.schema";
export const createHandler = async ({
export const createFilterSegmentHandler = async ({
ctx,
input,
}: {
@@ -14,54 +11,9 @@ export const createHandler = async ({
};
input: TCreateFilterSegmentInputSchema;
}) => {
const { scope, teamId, ...filterData } = input;
const userId = ctx.user.id;
// If scope is TEAM, verify user has admin/owner permissions
if (scope === "TEAM") {
if (!teamId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Team ID is required for team scope",
});
}
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId,
accepted: true,
role: {
in: ["ADMIN", "OWNER"],
},
},
});
if (!membership) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You must be a team admin or owner to create team segments",
});
}
}
// For USER scope, ensure no teamId is provided
if (scope === "USER" && teamId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Team ID is not allowed for user scope",
});
}
// Create the filter segment
const filterSegment = await prisma.filterSegment.create({
data: {
...filterData,
scope,
...(scope === "TEAM" ? { teamId } : {}),
userId,
},
const repository = new FilterSegmentRepository();
return await repository.create({
userId: ctx.user.id,
input,
});
return filterSegment;
};
@@ -1,36 +0,0 @@
import { z } from "zod";
import { ZActiveFilters, ZColumnSizing, ZColumnVisibility, ZSortingState } from "./types";
// Base schema for common fields
const baseCreateSchema = {
name: z.string().min(1),
tableIdentifier: z.string().min(1),
activeFilters: ZActiveFilters.optional(),
sorting: ZSortingState.optional(),
columnVisibility: ZColumnVisibility.optional(),
columnSizing: ZColumnSizing.optional(),
searchTerm: z.string().nullable().optional(),
perPage: z.number().int().min(1),
};
// Schema for team scope - requires teamId
const teamCreateSchema = z.object({
scope: z.literal("TEAM"),
teamId: z.number().int().positive(),
...baseCreateSchema,
});
// Schema for user scope - no teamId allowed
const userCreateSchema = z.object({
scope: z.literal("USER"),
teamId: z.undefined(),
...baseCreateSchema,
});
export const ZCreateFilterSegmentInputSchema = z.discriminatedUnion("scope", [
teamCreateSchema,
userCreateSchema,
]);
export type TCreateFilterSegmentInputSchema = z.infer<typeof ZCreateFilterSegmentInputSchema>;
@@ -1,11 +1,8 @@
import { prisma } from "@calcom/prisma";
import { FilterSegmentRepository } from "@calcom/lib/server/repository/filterSegment";
import type { TDeleteFilterSegmentInputSchema } from "@calcom/lib/server/repository/filterSegment.type";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TDeleteFilterSegmentInputSchema } from "./delete.schema";
export const deleteHandler = async ({
export const deleteFilterSegmentHandler = async ({
ctx,
input,
}: {
@@ -14,54 +11,14 @@ export const deleteHandler = async ({
};
input: TDeleteFilterSegmentInputSchema;
}) => {
const { id } = input;
const userId = ctx.user.id;
// First, fetch the existing segment to check permissions
const existingSegment = await prisma.filterSegment.findFirst({
where: {
id,
OR: [
{
scope: "TEAM",
teamId: { not: null },
team: {
members: {
some: {
userId,
accepted: true,
role: {
in: ["ADMIN", "OWNER"],
},
},
},
},
},
{
scope: "USER",
userId,
},
],
},
select: {
id: true,
},
});
if (!existingSegment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Filter segment not found or you don't have permission to delete it",
});
}
// Delete the filter segment
await prisma.filterSegment.delete({
where: { id },
const repository = new FilterSegmentRepository();
await repository.delete({
userId: ctx.user.id,
id: input.id,
});
return {
id,
id: input.id,
message: "Filter segment deleted successfully",
};
};
@@ -1,7 +0,0 @@
import { z } from "zod";
export const ZDeleteFilterSegmentInputSchema = z.object({
id: z.number().int().positive(),
});
export type TDeleteFilterSegmentInputSchema = z.infer<typeof ZDeleteFilterSegmentInputSchema>;
@@ -1,15 +1,7 @@
import { prisma } from "@calcom/prisma";
import { FilterSegmentRepository } from "@calcom/lib/server/repository/filterSegment";
import type { TListFilterSegmentsInputSchema } from "@calcom/lib/server/repository/filterSegment.type";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import type { TListFilterSegmentsInputSchema } from "./list.schema";
import {
ZActiveFilters,
ZSortingState,
ZColumnSizing,
ZColumnVisibility,
type FilterSegmentOutput,
} from "./types";
export const listHandler = async ({
ctx,
input,
@@ -19,77 +11,9 @@ export const listHandler = async ({
};
input: TListFilterSegmentsInputSchema;
}) => {
const { tableIdentifier } = input;
const userId = ctx.user.id;
// Get all teams that the user is a member of
const userTeamIds = await prisma.membership
.findMany({
where: {
userId,
accepted: true,
},
select: {
teamId: true,
},
})
.then((memberships) => memberships.map((m) => m.teamId));
// Fetch both user-scoped and team-scoped segments
const segments = await prisma.filterSegment.findMany({
where: {
tableIdentifier,
OR: [
// User-scoped segments created by the current user
{
scope: "USER",
userId,
},
// Team-scoped segments for teams the user belongs to
{
scope: "TEAM",
teamId: {
in: userTeamIds,
},
},
],
},
include: {
team: {
select: {
id: true,
name: true,
},
},
},
orderBy: [
{ scope: "desc" }, // USER segments first, then TEAM segments
{ createdAt: "desc" }, // Newest first within each scope
],
const repository = new FilterSegmentRepository();
return await repository.get({
userId: ctx.user.id,
tableIdentifier: input.tableIdentifier,
});
const parsedSegments: FilterSegmentOutput[] = segments.map((segment) => ({
...segment,
activeFilters: ZActiveFilters.catch([]).parse(segment.activeFilters),
sorting: ZSortingState.catch([]).parse(segment.sorting),
columnVisibility: ZColumnVisibility.catch({}).parse(segment.columnVisibility),
columnSizing: ZColumnSizing.catch({}).parse(segment.columnSizing),
}));
const preference = await prisma.userFilterSegmentPreference.findUnique({
where: {
userId_tableIdentifier: {
userId,
tableIdentifier,
},
},
select: {
segmentId: true,
},
});
return {
segments: parsedSegments,
preferredSegmentId: preference?.segmentId || null,
};
};
@@ -1,7 +0,0 @@
import { z } from "zod";
export const ZListFilterSegmentsInputSchema = z.object({
tableIdentifier: z.string(),
});
export type TListFilterSegmentsInputSchema = z.infer<typeof ZListFilterSegmentsInputSchema>;
@@ -1,9 +1,8 @@
import { FilterSegmentRepository } from "@calcom/lib/server/repository/filterSegment";
import type { TSetFilterSegmentPreferenceInputSchema } from "@calcom/lib/server/repository/filterSegment.type";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import type { TSetFilterSegmentPreferenceInputSchema } from "./preference.schema";
export const setPreferenceHandler = async ({
export const setFilterSegmentPreferenceHandler = async ({
ctx,
input,
}: {
@@ -12,12 +11,10 @@ export const setPreferenceHandler = async ({
};
input: TSetFilterSegmentPreferenceInputSchema;
}) => {
const { tableIdentifier, segmentId } = input;
const userId = ctx.user.id;
return await FilterSegmentRepository.setPreference({
userId,
tableIdentifier,
segmentId,
const repository = new FilterSegmentRepository();
return await repository.setPreference({
userId: ctx.user.id,
tableIdentifier: input.tableIdentifier,
segmentId: input.segmentId,
});
};
@@ -1,8 +0,0 @@
import { z } from "zod";
export const ZSetFilterSegmentPreferenceInputSchema = z.object({
tableIdentifier: z.string(),
segmentId: z.number().nullable(),
});
export type TSetFilterSegmentPreferenceInputSchema = z.infer<typeof ZSetFilterSegmentPreferenceInputSchema>;
@@ -1,16 +0,0 @@
import type { FilterSegmentOutput } from "@calcom/features/data-table/lib/types";
export type { SortingState, ColumnSizingState, VisibilityState } from "@tanstack/react-table";
export {
type ActiveFilter,
type FilterSegmentOutput,
ZActiveFilters,
ZSortingState,
ZColumnSizing,
ZColumnVisibility,
} from "@calcom/features/data-table/lib/types";
export type FilterSegmentsListResponse = {
segments: FilterSegmentOutput[];
preferredSegmentId: number | null;
};
@@ -1,11 +1,8 @@
import { prisma } from "@calcom/prisma";
import { FilterSegmentRepository } from "@calcom/lib/server/repository/filterSegment";
import type { TUpdateFilterSegmentInputSchema } from "@calcom/lib/server/repository/filterSegment.type";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TUpdateFilterSegmentInputSchema } from "./update.schema";
export const updateHandler = async ({
export const updateFilterSegmentHandler = async ({
ctx,
input,
}: {
@@ -14,59 +11,9 @@ export const updateHandler = async ({
};
input: TUpdateFilterSegmentInputSchema;
}) => {
const { id, name, activeFilters, sorting, columnVisibility, columnSizing, perPage, searchTerm } = input;
const userId = ctx.user.id;
// First, fetch the existing segment to check permissions
const existingSegment = await prisma.filterSegment.findFirst({
where: {
id,
...(input.scope === "TEAM"
? {
scope: "TEAM",
teamId: input.teamId,
team: {
members: {
some: {
userId,
accepted: true,
role: {
in: ["ADMIN", "OWNER"],
},
},
},
},
}
: {
scope: "USER",
userId,
}),
},
select: {
id: true,
},
const repository = new FilterSegmentRepository();
return await repository.update({
userId: ctx.user.id,
input,
});
if (!existingSegment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Filter segment not found or you don't have permission to update it",
});
}
// Update the filter segment with only the allowed fields
const updatedSegment = await prisma.filterSegment.update({
where: { id },
data: {
name,
activeFilters,
sorting,
columnVisibility,
columnSizing,
perPage,
searchTerm,
},
});
return updatedSegment;
};
@@ -1,37 +0,0 @@
import { z } from "zod";
import { ZActiveFilters, ZColumnSizing, ZColumnVisibility, ZSortingState } from "./types";
// Base schema for common fields that can be updated
const baseUpdateSchema = {
name: z.string().min(1).optional(),
tableIdentifier: z.string().min(1).optional(),
activeFilters: ZActiveFilters.optional(),
sorting: ZSortingState.optional(),
columnVisibility: ZColumnVisibility.optional(),
columnSizing: ZColumnSizing.optional(),
searchTerm: z.string().nullable().optional(),
perPage: z.number().int().min(1).optional(),
};
// Schema for team scope updates
const teamUpdateSchema = z.object({
id: z.number().int().positive(),
scope: z.literal("TEAM"),
teamId: z.number().int().positive(),
...baseUpdateSchema,
});
// Schema for user scope updates
const userUpdateSchema = z.object({
id: z.number().int().positive(),
scope: z.literal("USER"),
...baseUpdateSchema,
});
export const ZUpdateFilterSegmentInputSchema = z.discriminatedUnion("scope", [
teamUpdateSchema,
userUpdateSchema,
]);
export type TUpdateFilterSegmentInputSchema = z.infer<typeof ZUpdateFilterSegmentInputSchema>;