perf: Server-Side Data Fetching in App Router: /workflows (#16563)

* wip

* complete server side fetching for app router

* fix type error

* fix

* address comments

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
Benny JooandAnik Dhabal Babu authored and GitHub committed 2024-09-10 19:25:59 -07:00
1 parent d9777a3011
commit 976013512c
10 files changed
+359 -195

No files matched your search

@@ -1,18 +1,25 @@
import { withAppDirSsg } from "app/WithAppDirSsg";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { z } from "zod";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import LegacyPage from "@calcom/features/ee/workflows/pages/workflow";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getStaticProps } from "@lib/workflows/[workflow]/getStaticProps";
const querySchema = z.object({
workflow: z.string(),
});
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const { workflow: id } = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const workflow = await WorkflowRepository.getById({ id: +id });
const parsed = querySchema.safeParse({ ...params, ...searchParams });
if (!parsed.success) {
notFound();
}
const workflow = await WorkflowRepository.getById({ id: +parsed.data.workflow });
return await _generateMetadata(
() => (workflow && workflow.name ? workflow.name : "Untitled"),
@@ -20,11 +27,38 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
);
};
const getData = withAppDirSsg(getStaticProps);
export const generateStaticParams = () => [];
export default WithLayout({ getLayout: null, getData, Page: LegacyPage })<"P">;
const Page = async ({ params, searchParams }: PageProps) => {
const session = await getServerSession(AUTH_OPTIONS);
const user = session?.user;
const parsed = querySchema.safeParse({ ...params, ...searchParams });
if (!parsed.success) {
notFound();
}
const workflow = await WorkflowRepository.getById({ id: +parsed.data.workflow });
let verifiedEmails, verifiedNumbers;
try {
verifiedEmails = await WorkflowRepository.getVerifiedEmails({
userEmail: user?.email ?? null,
userId: user?.id ?? null,
teamId: workflow?.team?.id,
});
} catch (err) {}
try {
verifiedNumbers = await WorkflowRepository.getVerifiedNumbers({
userId: user?.id ?? null,
teamId: workflow?.team?.id,
});
} catch (err) {}
return (
<LegacyPage workflowData={workflow} verifiedEmails={verifiedEmails} verifiedNumbers={verifiedNumbers} />
);
};
export default WithLayout({ getLayout: null, ServerPage: Page });
export const dynamic = "force-static";
// generate segments on demand
export const dynamicParams = true;
+25 -1
View File
@@ -1,7 +1,12 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import LegacyPage from "@calcom/features/ee/workflows/pages/index";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -9,4 +14,23 @@ export const generateMetadata = async () =>
(t) => t("workflows_to_automate_notifications")
);
export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
const Page = async ({ params, searchParams }: PageProps) => {
const session = await getServerSession(AUTH_OPTIONS);
const user = session?.user;
const filters = getTeamsFiltersFromQuery({ ...searchParams, ...params });
let filteredList;
try {
filteredList = await WorkflowRepository.getFilteredList({
userId: user?.id,
input: {
filters,
},
});
} catch (err) {}
return <LegacyPage filteredList={filteredList} />;
};
export default WithLayout({ getLayout: null, ServerPage: Page })<"P">;
@@ -8,7 +8,7 @@ const querySchema = z.object({
export const getStaticProps: GetStaticProps = (ctx) => {
const params = querySchema.safeParse(ctx.params);
console.log("Built workflow page:", params);
if (!params.success) return { notFound: true };
if (!params.success) return { notFound: true } as const;
return {
props: {
-2
View File
@@ -1,5 +1,3 @@
"use client";
import type { GetStaticPaths } from "next";
import Workflow from "@calcom/features/ee/workflows/pages/workflow";
+19 -7
View File
@@ -11,6 +11,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { HttpError } from "@calcom/lib/http-error";
import type { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { trpc } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar, CreateButtonWithTeamsList, showToast } from "@calcom/ui";
@@ -22,16 +23,27 @@ import EmptyScreen from "../components/EmptyScreen";
import SkeletonLoader from "../components/SkeletonLoaderList";
import WorkflowList from "../components/WorkflowListPage";
function WorkflowsPage() {
type PageProps = {
filteredList?: Awaited<ReturnType<typeof WorkflowRepository.getFilteredList>>;
};
function WorkflowsPage({ filteredList }: PageProps) {
const { t } = useLocale();
const session = useSession();
const router = useRouter();
const routerQuery = useRouterQuery();
const filters = getTeamsFiltersFromQuery(routerQuery);
const queryRes = trpc.viewer.workflows.filteredList.useQuery({
filters,
});
const { data, isPending: _isPending } = trpc.viewer.workflows.filteredList.useQuery(
{
filters,
},
{
enabled: !filteredList,
}
);
const filteredWorkflows = filteredList ?? data;
const isPending = filteredList ? false : _isPending;
const createMutation = trpc.viewer.workflows.create.useMutation({
onSuccess: async ({ workflow }) => {
@@ -74,7 +86,7 @@ function WorkflowsPage() {
) : null
}>
<>
{queryRes.data?.totalCount ? (
{filteredWorkflows?.totalCount ? (
<div className="flex">
<TeamsFilter />
<div className="mb-4 ml-auto">
@@ -90,11 +102,11 @@ function WorkflowsPage() {
</div>
) : null}
<FilterResults
queryRes={queryRes}
queryRes={{ isPending, data: filteredWorkflows }}
emptyScreen={<EmptyScreen isFilteredView={false} />}
noResultsScreen={<EmptyScreen isFilteredView={true} />}
SkeletonLoader={SkeletonLoader}>
<WorkflowList workflows={queryRes.data?.filtered} />
<WorkflowList workflows={filteredWorkflows?.filtered} />
</FilterResults>
</>
</ShellMain>
@@ -12,6 +12,7 @@ import { SENDER_ID } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { HttpError } from "@calcom/lib/http-error";
import type { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import type { TimeUnit, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { MembershipRole, WorkflowActions } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
@@ -37,7 +38,17 @@ export type FormValues = {
selectAll: boolean;
};
function WorkflowPage() {
type PageProps = {
workflowData?: Awaited<ReturnType<typeof WorkflowRepository.getById>>;
verifiedNumbers?: Awaited<ReturnType<typeof WorkflowRepository.getVerifiedNumbers>>;
verifiedEmails?: Awaited<ReturnType<typeof WorkflowRepository.getVerifiedEmails>>;
};
function WorkflowPage({
workflowData: workflowDataProp,
verifiedNumbers: verifiedNumbersProp,
verifiedEmails: verifiedEmailsProp,
}: PageProps) {
const { t, i18n } = useLocale();
const session = useSession();
const params = useParamsWithFallback();
@@ -58,27 +69,36 @@ function WorkflowPage() {
const user = userQuery.data;
const {
data: workflow,
isError,
data: workflowData,
isError: _isError,
error,
isPending: isPendingWorkflow,
isPending: _isPendingWorkflow,
} = trpc.viewer.workflows.get.useQuery(
{ id: +workflowId },
{
enabled: !!workflowId,
enabled: workflowDataProp ? false : !!workflowId,
}
);
const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
const workflow = workflowDataProp || workflowData;
const isPendingWorkflow = workflowDataProp ? false : _isPendingWorkflow;
const isError = workflowDataProp ? false : _isError;
const { data: verifiedNumbersData } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
{ teamId: workflow?.team?.id },
{
enabled: !!workflow?.id,
enabled: verifiedNumbersProp ? false : !!workflow?.id,
}
);
const verifiedNumbers = verifiedNumbersProp || verifiedNumbersData;
const { data: verifiedEmails } = trpc.viewer.workflows.getVerifiedEmails.useQuery({
teamId: workflow?.team?.id,
});
const { data: verifiedEmailsData } = trpc.viewer.workflows.getVerifiedEmails.useQuery(
{
teamId: workflow?.team?.id,
},
{ enabled: !verifiedEmailsProp }
);
const verifiedEmails = verifiedEmailsProp || verifiedEmailsData;
const isOrg = workflow?.team?.isOrganization ?? false;
@@ -135,7 +155,7 @@ function WorkflowPage() {
setSelectedOptions(activeOn || []);
} else {
setSelectedOptions(
workflowData.activeOn.flatMap((active) => {
workflowData.activeOn?.flatMap((active) => {
if (workflowData.teamId && active.eventType.parentId) return [];
return {
value: String(active.eventType.id),
@@ -152,7 +172,7 @@ function WorkflowPage() {
}
}
//translate dynamic variables into local language
const steps = workflowData.steps.map((step) => {
const steps = workflowData.steps?.map((step) => {
const updatedStep = {
...step,
senderName: step.sender,
@@ -342,7 +362,7 @@ function WorkflowPage() {
)}
</>
) : (
<Alert severity="error" title="Something went wrong" message={error.message} />
<Alert severity="error" title="Something went wrong" message={error?.message ?? ""} />
)}
</ShellMain>
</Form>
+226
View File
@@ -1,6 +1,13 @@
import { z } from "zod";
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import type { TFilteredListInputSchema } from "@calcom/trpc/server/routers/viewer/workflows/filteredList.schema";
import type { TGetVerifiedEmailsInputSchema } from "@calcom/trpc/server/routers/viewer/workflows/getVerifiedEmails.schema";
import type { TGetVerifiedNumbersInputSchema } from "@calcom/trpc/server/routers/viewer/workflows/getVerifiedNumbers.schema";
export const ZGetInputSchema = z.object({
id: z.number(),
@@ -8,6 +15,48 @@ export const ZGetInputSchema = z.object({
export type TGetInputSchema = z.infer<typeof ZGetInputSchema>;
const { include: includedFields } = Prisma.validator<Prisma.WorkflowDefaultArgs>()({
include: {
activeOn: {
select: {
eventType: {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
},
activeOnTeams: {
select: {
team: {
select: {
id: true,
name: true,
},
},
},
},
steps: true,
team: {
select: {
id: true,
slug: true,
name: true,
members: true,
logoUrl: true,
isOrganization: true,
},
},
},
});
export class WorkflowRepository {
static async getById({ id }: TGetInputSchema) {
return await prisma.workflow.findFirst({
@@ -50,4 +99,181 @@ export class WorkflowRepository {
},
});
}
static async getVerifiedNumbers({
userId,
teamId,
}: TGetVerifiedNumbersInputSchema & { userId: number | null }) {
if (!userId) {
throw new Error("User Id not found");
}
const verifiedNumbers = await prisma.verifiedNumber.findMany({
where: {
OR: [{ userId }, { teamId }],
},
});
return verifiedNumbers;
}
static async getVerifiedEmails({
userEmail,
userId,
teamId,
}: TGetVerifiedEmailsInputSchema & { userEmail: string | null; userId: number | null }) {
if (!userId) {
throw new Error("User Id not found");
}
if (!userEmail) {
throw new Error("User email not found");
}
let verifiedEmails: string[] = [userEmail];
if (teamId) {
const team = await prisma.team.findFirst({
where: {
id: teamId,
},
select: {
members: {
include: {
user: {
select: {
id: true,
email: true,
},
},
},
},
},
});
if (!team) {
throw new Error("Team not found");
}
const isTeamMember = team.members.some((member) => member.userId === userId);
if (!isTeamMember) {
throw new Error("You are not a member of this team");
}
verifiedEmails = verifiedEmails.concat(team.members.map((member) => member.user.email));
}
const emails = (
await prisma.verifiedEmail.findMany({
where: {
OR: [{ userId }, { teamId }],
},
})
).map((verifiedEmail) => verifiedEmail.email);
verifiedEmails = verifiedEmails.concat(emails);
return verifiedEmails;
}
static async getFilteredList({ userId, input }: { userId?: number; input: TFilteredListInputSchema }) {
const filters = input?.filters;
const filtered = filters && hasFilter(filters);
const allWorkflows = await prisma.workflow.findMany({
where: {
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
accepted: true,
},
},
},
},
],
},
include: includedFields,
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
});
if (!filtered) {
const workflowsWithReadOnly: WorkflowType[] = allWorkflows.map((workflow) => {
const readOnly = !!workflow.team?.members?.find(
(member) => member.userId === userId && member.role === MembershipRole.MEMBER
);
return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow };
});
return {
filtered: workflowsWithReadOnly,
totalCount: allWorkflows.length,
};
}
const where = {
OR: [] as Prisma.WorkflowWhereInput[],
};
if (filtered) {
if (!!filters.teamIds) {
where.OR.push({
team: {
id: {
in: filters.teamIds ?? [],
},
members: {
some: {
userId,
accepted: true,
},
},
},
});
}
if (!!filters.userIds) {
where.OR.push({
userId: {
in: filters.userIds,
},
teamId: null,
});
}
const filteredWorkflows = await prisma.workflow.findMany({
where,
include: includedFields,
orderBy: {
id: "asc",
},
});
const workflowsWithReadOnly: WorkflowType[] = filteredWorkflows.map((workflow) => {
const readOnly = !!workflow.team?.members?.find(
(member) => member.userId === userId && member.role === MembershipRole.MEMBER
);
return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow };
});
return {
filtered: workflowsWithReadOnly,
totalCount: allWorkflows.length,
};
}
}
}
@@ -1,8 +1,6 @@
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import type { PrismaClient } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/client";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TFilteredListInputSchema } from "./filteredList.schema";
@@ -58,105 +56,5 @@ const { include: includedFields } = Prisma.validator<Prisma.WorkflowDefaultArgs>
});
export const filteredListHandler = async ({ ctx, input }: FilteredListOptions) => {
const { prisma, user } = ctx;
const filters = input?.filters;
const filtered = filters && hasFilter(filters);
const allWorkflows = await prisma.workflow.findMany({
where: {
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
},
],
},
include: includedFields,
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
});
if (!filtered) {
const workflowsWithReadOnly: WorkflowType[] = allWorkflows.map((workflow) => {
const readOnly = !!workflow.team?.members?.find(
(member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER
);
return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow };
});
return {
filtered: workflowsWithReadOnly,
totalCount: allWorkflows.length,
};
}
const where = {
OR: [] as Prisma.WorkflowWhereInput[],
};
if (filtered) {
if (!!filters.teamIds) {
where.OR.push({
team: {
id: {
in: filters.teamIds ?? [],
},
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
});
}
if (!!filters.userIds) {
where.OR.push({
userId: {
in: filters.userIds,
},
teamId: null,
});
}
const filteredWorkflows = await prisma.workflow.findMany({
where,
include: includedFields,
orderBy: {
id: "asc",
},
});
const workflowsWithReadOnly: WorkflowType[] = filteredWorkflows.map((workflow) => {
const readOnly = !!workflow.team?.members?.find(
(member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER
);
return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow };
});
return {
filtered: workflowsWithReadOnly,
totalCount: allWorkflows.length,
};
}
return await WorkflowRepository.getFilteredList({ userId: ctx.user.id, input });
};
@@ -1,8 +1,6 @@
import { prisma } from "@calcom/prisma";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import type { TGetVerifiedEmailsInputSchema } from "./getVerifiedEmails.schema";
type GetVerifiedEmailsOptions = {
@@ -13,51 +11,9 @@ type GetVerifiedEmailsOptions = {
};
export const getVerifiedEmailsHandler = async ({ ctx, input }: GetVerifiedEmailsOptions) => {
const { user } = ctx;
const { teamId } = input;
let verifiedEmails: string[] = [user.email];
if (teamId) {
const team = await prisma.team.findFirst({
where: {
id: teamId,
},
select: {
members: {
include: {
user: {
select: {
id: true,
email: true,
},
},
},
},
},
});
if (!team) {
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
}
const isTeamMember = team.members.some((member) => member.userId === user.id);
if (!isTeamMember) {
throw new TRPCError({ code: "FORBIDDEN", message: "You are not a member of this team" });
}
verifiedEmails = verifiedEmails.concat(team.members.map((member) => member.user.email));
}
const emails = (
await prisma.verifiedEmail.findMany({
where: {
OR: [{ userId: user.id }, { teamId: input.teamId }],
},
})
).map((verifiedEmail) => verifiedEmail.email);
verifiedEmails = verifiedEmails.concat(emails);
return verifiedEmails;
return await WorkflowRepository.getVerifiedEmails({
userId: ctx.user.id,
userEmail: ctx.user.email,
teamId: input.teamId,
});
};
@@ -1,4 +1,4 @@
import { prisma } from "@calcom/prisma";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TGetVerifiedNumbersInputSchema } from "./getVerifiedNumbers.schema";
@@ -11,12 +11,8 @@ type GetVerifiedNumbersOptions = {
};
export const getVerifiedNumbersHandler = async ({ ctx, input }: GetVerifiedNumbersOptions) => {
const { user } = ctx;
const verifiedNumbers = await prisma.verifiedNumber.findMany({
where: {
OR: [{ userId: user.id }, { teamId: input.teamId }],
},
return await WorkflowRepository.getVerifiedNumbers({
userId: ctx.user.id,
teamId: input.teamId,
});
return verifiedNumbers;
};