haardik | LearnWeb3
haardik | LearnWeb3
TtRPC
Created by haardik | LearnWeb3 on 1/2/2024 in #❓-help
Including multi-tenant config into tRPC context
Hey all, I've been working on upgrading my app to support multi-tenancy, inspired by Vercel's Platforms starter kit. The core of the relevant logic comes down to this Next.js middleware:
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
* 5. All files inside subdirectories of /public/static
*/
'/((?!api/|_next/|_static/|static/|_vercel|[\\w-]+\\.\\w+).*)',
],
};

export default function middleware(req: NextRequest) {
const url = req.nextUrl;

// Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
const hostname = req.headers
.get('host')!
.replace('.localhost:3000', `.${ROOT_DOMAIN}`);

const searchParams = req.nextUrl.searchParams.toString();
const path = `${url.pathname}${
searchParams.length > 0 ? `?${searchParams}` : ''
}`;

// Rewrite root to `/app`
if (hostname === 'localhost:3000' || hostname === ROOT_DOMAIN) {
return NextResponse.rewrite(
new URL(`/app${path === '/' ? '' : path}`, req.url),
);
}

const tenantSlug = hostname.replace(`.${ROOT_DOMAIN}`, '');

// Rewrite everything else to `/[tenantSlug]`
return NextResponse.rewrite(new URL(`/${tenantSlug}${path}`, req.url));
}
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
* 5. All files inside subdirectories of /public/static
*/
'/((?!api/|_next/|_static/|static/|_vercel|[\\w-]+\\.\\w+).*)',
],
};

export default function middleware(req: NextRequest) {
const url = req.nextUrl;

// Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
const hostname = req.headers
.get('host')!
.replace('.localhost:3000', `.${ROOT_DOMAIN}`);

const searchParams = req.nextUrl.searchParams.toString();
const path = `${url.pathname}${
searchParams.length > 0 ? `?${searchParams}` : ''
}`;

// Rewrite root to `/app`
if (hostname === 'localhost:3000' || hostname === ROOT_DOMAIN) {
return NextResponse.rewrite(
new URL(`/app${path === '/' ? '' : path}`, req.url),
);
}

const tenantSlug = hostname.replace(`.${ROOT_DOMAIN}`, '');

// Rewrite everything else to `/[tenantSlug]`
return NextResponse.rewrite(new URL(`/${tenantSlug}${path}`, req.url));
}
This middleware is set up to explicitly exclude requests to /api Now, within my tRPC router, it is helpful in certain places to be able to access the tenantSlug value. I could go and change all my tRPC queries/mutations to take that as input and change client-side code everywhere but that would be hundreds of functions and even more calls in the client code. I have been thinking about how to get this be part of tRPC's context, but I'm a bit stuck Currently my context looks like this (i have a mix of HTTP and WebSockets)
export async function createContext(
ctx: CreateWSSContextFnOptions | CreateNextContextOptions,
): Promise<{
session: Session | null;
req: IncomingMessage | NextApiRequest;
res: ws | NextApiResponse;
prisma: ExtendedPrismaClient;
}> {
const { req, res } = ctx;

let session: Session | null;
try {
// @ts-expect-error -- This will fail if called client side
session = await getServerSession(req, res, getAuthOptions(req));
} catch (error) {
session = await getSession(ctx);
}

const contextInner = await createContextInner();

return {
...contextInner,
session,
req,
res,
};
}
export async function createContext(
ctx: CreateWSSContextFnOptions | CreateNextContextOptions,
): Promise<{
session: Session | null;
req: IncomingMessage | NextApiRequest;
res: ws | NextApiResponse;
prisma: ExtendedPrismaClient;
}> {
const { req, res } = ctx;

let session: Session | null;
try {
// @ts-expect-error -- This will fail if called client side
session = await getServerSession(req, res, getAuthOptions(req));
} catch (error) {
session = await getSession(ctx);
}

const contextInner = await createContextInner();

return {
...contextInner,
session,
req,
res,
};
}
Since the middleware ignores requests to /api - the req object in context (in case of HTTP calls) doesn't include the subdomain/slug in the rquest headers. I figured someone here had to have run into this in the past and may have figured out a solutin already - so any help is appreciated!
6 replies
TtRPC
Created by haardik | LearnWeb3 on 10/3/2023 in #❓-help
tRPC Client within Next.js but with external standalone server?
Hi, I have an old Next.js project that used tRPC and we're currently in the process of separating out the frontend and backend parts of it for various reasons. To reuse code, I was hoping to set up a standalone tRPC Server In that case, what's the recommended method of setting up a tRPC Client on the Next side? using @trpc/next or @trpc/react ? I tried (briefly) using @trpc/next and it was giving me issues around not having a QueryClient set - but im not sure if the react method is the way to go either.
3 replies
TtRPC
Created by haardik | LearnWeb3 on 9/30/2023 in #❓-help
tRPC Websockets with a standalone Bun Server?
Hey folks! Experiementing with setting up a standalone tRPC Server using Bun. The HTTP part is great - i'm wondering if anyone has succeeded getting it to work with Bun's websocket server? if not, i'll continue figuring it out and hopefully add an adapter to tRPC.
4 replies
TtRPC
Created by haardik | LearnWeb3 on 9/28/2023 in #❓-help
Unable to get mutation to trigger subscription because EventEmitter not being shared
Hey folks, Been struggling with this for a few hours now hopelessly and trying random things - read all related posts in this forum, on github issues, and stackoverflow - and still don't understand what is going on. I have a next app with a custom HTTP server and using tRPC. WSLink etc is all fine - i'm doing everything the proper way. I have a router with these two functions:
sendMessage: protectedProcedure
.input(z.string())
.mutation(async ({ ctx, input }) => {
try {
const chatRepo = new CommunityChatRepository(ctx.prisma);
const result = await chatRepo.sendMessage(ctx.session!.user.id, input);

if (!result.ok) throw result.error;
console.log({ namesSendMsg: wsee.eventNames() });

wsee.emit('onNewMessage', result.value);

return result.value;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
} else {
console.error(error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Internal server error',
});
}
}
}),

onNewMessage: publicProcedure.subscription(() => {
return observable<ChatMessageWithSenderInformation>((emit) => {
const onNewMessage = (data: ChatMessageWithSenderInformation) => {
emit.next(data);
};

console.log({ names: wsee.eventNames() });
wsee.on('onNewMessage', onNewMessage);
console.log({ names: wsee.eventNames() });

console.log(`added listener ${wsee.listenerCount('onNewMessage')}`);

return () => {
console.log(`removed listener ${wsee.listenerCount('onNewMessage')}`);
wsee.off('onNewMessage', onNewMessage);
};
});
}),
sendMessage: protectedProcedure
.input(z.string())
.mutation(async ({ ctx, input }) => {
try {
const chatRepo = new CommunityChatRepository(ctx.prisma);
const result = await chatRepo.sendMessage(ctx.session!.user.id, input);

if (!result.ok) throw result.error;
console.log({ namesSendMsg: wsee.eventNames() });

wsee.emit('onNewMessage', result.value);

return result.value;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
} else {
console.error(error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Internal server error',
});
}
}
}),

onNewMessage: publicProcedure.subscription(() => {
return observable<ChatMessageWithSenderInformation>((emit) => {
const onNewMessage = (data: ChatMessageWithSenderInformation) => {
emit.next(data);
};

console.log({ names: wsee.eventNames() });
wsee.on('onNewMessage', onNewMessage);
console.log({ names: wsee.eventNames() });

console.log(`added listener ${wsee.listenerCount('onNewMessage')}`);

return () => {
console.log(`removed listener ${wsee.listenerCount('onNewMessage')}`);
wsee.off('onNewMessage', onNewMessage);
};
});
}),
I've added a bunch of console logs to help explain. So, in my logs I see that when the app loads up I get a WS Connection to the server. On the client side, I have trpc.chat.onNewMessage.useSubscription which console logs {names: []} first and then {names: ['onNewMessage']} as it should based on the code for the router. But, when I send a message, the mutation fails to trigger the subscription because it logs {namesSendMsg: []} i.e. for some reason it does;n't recognize the attached listener I thought it was because maybe somehow multiple EventEmitter instances are being created due to HMR - so I did a workaround similar to what we do with Prisma for next dev mode: src/eventEmitter.ts:
import { EventEmitter } from 'events';

import type { ChatMessageWithSenderInformation } from './server/repositories/CommunityChatRepository';

interface WSEvents {
// Community Chat
onNewMessage: (message: ChatMessageWithSenderInformation) => void;

// Trivia
join: () => void;
getQuestions: () => void;
onRankingUpdate: () => void;
onChoiceUpdate: () => void;
}

export declare interface WSEventEmitter {
on<WEv extends keyof WSEvents>(event: WEv, listener: WSEvents[WEv]): this;
off<WEv extends keyof WSEvents>(event: WEv, listener: WSEvents[WEv]): this;
once<WEv extends keyof WSEvents>(event: WEv, listener: WSEvents[WEv]): this;
emit<WEv extends keyof WSEvents>(
event: WEv,
...args: Parameters<WSEvents[WEv]>
): boolean;
}

export class WSEventEmitter extends EventEmitter {}

const globalForWSEE = globalThis as unknown as { wsee: WSEventEmitter };
const isDevMode = process.env.NODE_ENV !== 'production';

export const wsee = globalForWSEE.wsee || new WSEventEmitter();

if (isDevMode) globalForWSEE.wsee = wsee;
import { EventEmitter } from 'events';

import type { ChatMessageWithSenderInformation } from './server/repositories/CommunityChatRepository';

interface WSEvents {
// Community Chat
onNewMessage: (message: ChatMessageWithSenderInformation) => void;

// Trivia
join: () => void;
getQuestions: () => void;
onRankingUpdate: () => void;
onChoiceUpdate: () => void;
}

export declare interface WSEventEmitter {
on<WEv extends keyof WSEvents>(event: WEv, listener: WSEvents[WEv]): this;
off<WEv extends keyof WSEvents>(event: WEv, listener: WSEvents[WEv]): this;
once<WEv extends keyof WSEvents>(event: WEv, listener: WSEvents[WEv]): this;
emit<WEv extends keyof WSEvents>(
event: WEv,
...args: Parameters<WSEvents[WEv]>
): boolean;
}

export class WSEventEmitter extends EventEmitter {}

const globalForWSEE = globalThis as unknown as { wsee: WSEventEmitter };
const isDevMode = process.env.NODE_ENV !== 'production';

export const wsee = globalForWSEE.wsee || new WSEventEmitter();

if (isDevMode) globalForWSEE.wsee = wsee;
this did not help either. using this wsee everywhere doesn't change anything. im lost and have no idea WHY there are supposedly two different event emitter instances being used here?
6 replies
TtRPC
Created by haardik | LearnWeb3 on 5/9/2023 in #❓-help
How to infer type of a nested object from app router output?
I have a tRPC router than returns a nested object through a db query. It looks like this:
ILessonCommentProps.comments: ({
_count: {
likes: number;
comments: number;
bookmarks: number;
};
comments: ({
user: GetResult<{
id: string;
displayName: string | null;
email: string | null;
emailVerified: Date | null;
... 17 more ...;
updatedAt: Date;
}, unknown>;
subComments: GetResult<...>[];
} & GetResult<...>)[];
bookmarks: {
...;
}[];
likes: {
...;
}[];
} & GetResult<...>) | null
ILessonCommentProps.comments: ({
_count: {
likes: number;
comments: number;
bookmarks: number;
};
comments: ({
user: GetResult<{
id: string;
displayName: string | null;
email: string | null;
emailVerified: Date | null;
... 17 more ...;
updatedAt: Date;
}, unknown>;
subComments: GetResult<...>[];
} & GetResult<...>)[];
bookmarks: {
...;
}[];
likes: {
...;
}[];
} & GetResult<...>) | null
I'd like to infer the type of the comments property to use as an interface for props on a component. I tried doing this but this doesn't work:
interface ILessonCommentProps {
comments: AppRouterOutputs['lessonBuilder']['getLessonLikesCommentsBookmarks']['comments'];
}
interface ILessonCommentProps {
comments: AppRouterOutputs['lessonBuilder']['getLessonLikesCommentsBookmarks']['comments'];
}
I was wondering if this is even possible?
8 replies