haardik | LearnWeb3
haardik | LearnWeb3
Explore posts from servers
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
TTCTheo's Typesafe Cult
Created by haardik | LearnWeb3 on 5/9/2023 in #questions
Infer nested type of trpc router return type
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?
4 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
TTCTheo's Typesafe Cult
Created by haardik | LearnWeb3 on 1/24/2023 in #questions
Refetching tRPC query after mutation from a child
Hey, this might be a pretty noob tRPC question as I'm quite new to the whole tRPC + TanStack Query setup. I have a component NotificationPanel which uses tRPC to query unread notifications for a given user. Once it fetches those notifications, it renders multiple NotificationItem elements and passes the notification object to them. The NotificationItem has a x button which uses tRPC mutate to mark itself READ. At this point, i would like the parent NotificationPanel component to re-fetch unread notifications for the user, but am not sure how to achieve this. I tried digging through TanStack Query docs but didn't find much. On Google, most articles are quite outdated and that approach doesn't seem to work well with tRPC's wrapper around TanStack Query. Any ideas how to achieve this?
7 replies