Skip to content

Commit

Permalink
chore: migrate trpc, ssgInit, ssrInit
Browse files Browse the repository at this point in the history
  • Loading branch information
DmytroHryshyn committed Nov 29, 2023
1 parent 6c4b115 commit c4565da
Show file tree
Hide file tree
Showing 48 changed files with 402 additions and 128 deletions.
212 changes: 212 additions & 0 deletions apps/web/app/_trpc/createTRPCNextLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team
// file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts
// repo link: https://github.com/trpc/next-13
// code is / will continue to be adapted for our usage
import { dehydrate, QueryClient } from "@tanstack/query-core";
import type { DehydratedState, QueryKey } from "@tanstack/react-query";

import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc";
import {
callProcedure,
type AnyProcedure,
type AnyQueryProcedure,
type AnyRouter,
type DataTransformer,
type inferProcedureInput,
type inferProcedureOutput,
type inferRouterContext,
type MaybePromise,
type ProcedureRouterRecord,
} from "@calcom/trpc/server";

import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared";

export function getArrayQueryKey(
queryKey: string | [string] | [string, ...unknown[]] | unknown[],
type: string
): QueryKey {
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey];
const [arrayPath, input] = queryKeyArrayed;

if (!input && (!type || type === "any")) {
return arrayPath.length ? [arrayPath] : ([] as unknown as QueryKey);
}

return [
arrayPath,
{
...(typeof input !== "undefined" && { input: input }),
...(type && type !== "any" && { type: type }),
},
];
}

// copy starts
// copied from trpc/trpc repo
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58
function transformQueryOrMutationCacheErrors<
TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0]
>(result: TState): TState {
const error = result.state.error as Maybe<TRPCClientError<any>>;
if (error instanceof Error && error.name === "TRPCClientError") {
const newError: TRPCClientErrorLike<any> = {
message: error.message,
data: error.data,
shape: error.shape,
};
return {
...result,
state: {
...result.state,
error: newError,
},
};
}
return result;
}
// copy ends

interface CreateTRPCNextLayoutOptions<TRouter extends AnyRouter> {
router: TRouter;
createContext: () => MaybePromise<inferRouterContext<TRouter>>;
transformer?: DataTransformer;
}

/**
* @internal
*/
export type DecorateProcedure<TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure
? {
fetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
fetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
prefetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
prefetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
}
: never;

type OmitNever<TType> = Pick<
TType,
{
[K in keyof TType]: TType[K] extends never ? never : K;
}[keyof TType]
>;
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TPath extends string = ""
> = OmitNever<{
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"], `${TPath}${TKey & string}.`>
: TProcedures[TKey] extends AnyQueryProcedure
? DecorateProcedure<TProcedures[TKey]>
: never;
}>;

type CreateTRPCNextLayout<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter["_def"]["record"]> & {
dehydrate(): Promise<DehydratedState>;
queryClient: QueryClient;
};

const getStateContainer = <TRouter extends AnyRouter>(opts: CreateTRPCNextLayoutOptions<TRouter>) => {
let _trpc: {
queryClient: QueryClient;
context: inferRouterContext<TRouter>;
} | null = null;

return () => {
if (_trpc === null) {
_trpc = {
context: opts.createContext(),
queryClient: new QueryClient(),
};
}

return _trpc;
};
};

export function createTRPCNextLayout<TRouter extends AnyRouter>(
opts: CreateTRPCNextLayoutOptions<TRouter>
): CreateTRPCNextLayout<TRouter> {
const getState = getStateContainer(opts);

const transformer = opts.transformer ?? {
serialize: (v) => v,
deserialize: (v) => v,
};

return createFlatProxy((key) => {
const state = getState();
const { queryClient } = state;
if (key === "queryClient") {
return queryClient;
}

if (key === "dehydrate") {
// copy starts
// copied from trpc/trpc repo
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229
const dehydratedCache = dehydrate(queryClient, {
shouldDehydrateQuery() {
// makes sure errors are also dehydrated
return true;
},
});

// since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects
const dehydratedCacheWithErrors = {
...dehydratedCache,
queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors),
mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors),
};

return () => transformer.serialize(dehydratedCacheWithErrors);
}
// copy ends

return createRecursiveProxy(async (callOpts) => {
const path = [key, ...callOpts.path];
const utilName = path.pop();
const ctx = await state.context;

const caller = opts.router.createCaller(ctx);

const pathStr = path.join(".");
const input = callOpts.args[0];

if (utilName === "fetchInfinite") {
return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
caller.query(pathStr, input)
);
}

if (utilName === "prefetch") {
return queryClient.prefetchQuery({
queryKey: getArrayQueryKey([path, input], "query"),
queryFn: async () => {
const res = await callProcedure({
procedures: opts.router._def.procedures,
path: pathStr,
rawInput: input,
ctx,
type: "query",
});
return res;
},
});
}

if (utilName === "prefetchInfinite") {
return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
caller.query(pathStr, input)
);
}

return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () =>
caller.query(pathStr, input)
);
}) as CreateTRPCNextLayout<TRouter>;
});
}
3 changes: 3 additions & 0 deletions apps/web/app/_trpc/serverClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { appRouter } from "@calcom/trpc/server/routers/_app";

export const serverClient = appRouter.createCaller({});
34 changes: 34 additions & 0 deletions apps/web/app/_trpc/ssgInit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers } from "next/headers";
import superjson from "superjson";

import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";

import { createTRPCNextLayout } from "./createTRPCNextLayout";

export async function ssgInit() {
const locale = headers().get("x-locale") ?? "en";

const i18n = (await serverSideTranslations(locale, ["common"])) || "en";

const ssg = createTRPCNextLayout({
router: appRouter,
transformer: superjson,
createContext() {
return { prisma, session: null, locale, i18n };
},
});

// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];

ssg.queryClient.setQueryData(queryKey, { i18n });

return ssg;
}
50 changes: 50 additions & 0 deletions apps/web/app/_trpc/ssrInit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { type GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers, cookies } from "next/headers";
import superjson from "superjson";

import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";

import { createTRPCNextLayout } from "./createTRPCNextLayout";

export async function ssrInit(options?: { noI18nPreload: boolean }) {
const req = {
headers: headers(),
cookies: cookies(),
};

const locale = await getLocale(req);

const i18n = (await serverSideTranslations(locale, ["common", "vital"])) || "en";

const ssr = createTRPCNextLayout({
router: appRouter,
transformer: superjson,
createContext() {
return { prisma, session: null, locale, i18n, req: req as unknown as GetServerSidePropsContext["req"] };
},
});

// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
if (!options?.noI18nPreload) {
ssr.queryClient.setQueryData(queryKey, { i18n });
}

await Promise.allSettled([
// So feature flags are available on first render
ssr.viewer.features.map.prefetch(),
// Provides a better UX to the users who have already upgraded.
ssr.viewer.teams.hasTeamPlan.prefetch(),
ssr.viewer.public.session.prefetch(),
]);

return ssr;
}
5 changes: 5 additions & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ const matcherConfigUserTypeEmbedRoute = {

/** @type {import("next").NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ["next-i18next"],
},
i18n: {
...i18n,
localeDetection: false,
Expand Down Expand Up @@ -231,6 +234,8 @@ const nextConfig = {
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
// by next.js will be dropped. Doesn't make much sense, but how it is
fs: false,
"pg-native": false,
"superagent-proxy": false,
};

/**
Expand Down
2 changes: 1 addition & 1 deletion apps/web/pages/api/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("MonthlyDigestEmail", {
await renderEmail("MonthlyDigestEmail", {
language: t,
Created: 12,
Completed: 13,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ export default class ResponseEmail extends BaseEmail {
this.toAddresses = toAddresses;
}

protected getNodeMailerPayload(): Record<string, unknown> {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const toAddresses = this.toAddresses;
const subject = `${this.form.name} has a new response`;
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject,
html: renderEmail("ResponseEmail", {
html: await renderEmail("ResponseEmail", {
form: this.form,
orderedResponses: this.orderedResponses,
subject,
Expand Down
2 changes: 1 addition & 1 deletion packages/emails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
```ts
import { renderEmail } from "@calcom/emails";

renderEmail("TeamInviteEmail", */{
await renderEmail("TeamInviteEmail", */{
language: t,
from: "[email protected]",
to: "[email protected]",
Expand Down
Loading

0 comments on commit c4565da

Please sign in to comment.