Skip to content

Commit

Permalink
fix(trpc): allow to pass custom headers to trpc client (#441)
Browse files Browse the repository at this point in the history
  • Loading branch information
goetzrobin committed May 26, 2023
1 parent cd39120 commit a2b7eae
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 19 deletions.
15 changes: 13 additions & 2 deletions apps/trpc-app-e2e-playwright/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,25 @@ describe('tRPC Demo App', () => {
).toContain(/Analog + tRPC/i);
});

test<TRPCTestContext>(`If user enters the first note the note should be stored
successfully and listed in the notes array`, async (ctx) => {
test<TRPCTestContext>(`
If user enters the first note the note should be storedsuccessfully and listed in the notes array.
Still unauthorized the user should not be able to delete the note and the error should be displayed.
After the users clicks the Login button and gets authorized, deleting the note again should work successfully,
and the error should disappear.
`, async (ctx) => {
await ctx.notesPage.typeNote(notes.first.note);

await ctx.notesPage.addNote();
expect(await ctx.notesPage.notes().elementHandles()).toHaveLength(1);

await ctx.notesPage.removeNote(0);
expect(await ctx.notesPage.notes().elementHandles()).toHaveLength(1);
expect(await ctx.notesPage.getDeleteErrorCount()).toBe(1);

await ctx.notesPage.toggleLogin();
await ctx.notesPage.removeNote(0);
await page.waitForSelector('.no-notes');
expect(await ctx.notesPage.notes().elementHandles()).toHaveLength(0);
expect(await ctx.notesPage.getDeleteErrorCount()).toBe(0);
});
});
9 changes: 8 additions & 1 deletion apps/trpc-app-e2e-playwright/tests/fixtures/notes.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Page } from 'playwright';
export class NotesPage {
constructor(readonly page: Page) {}

async toggleLogin() {
await this.page.getByTestId('loginBtn').click();
}

async typeNote(note: string) {
await this.page.getByTestId('newNoteInput').fill(note);
}
Expand All @@ -16,7 +20,10 @@ export class NotesPage {
await this.waitForTrpcResponse(
this.page.getByTestId('removeNoteAtIndexBtn' + index).click()
);
await this.page.waitForSelector('.no-notes');
}

async getDeleteErrorCount() {
return this.page.locator('[data-testid="deleteError"]').count();
}

notes() {
Expand Down
46 changes: 42 additions & 4 deletions apps/trpc-app/src/app/pages/index.page.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { injectTRPCClient } from '../../trpc-client';
import {
ChangeDetectionStrategy,
Component,
effect,
signal,
} from '@angular/core';
import { injectTRPCClient, tRPCHeaders } from '../../trpc-client';
import { AsyncPipe, DatePipe, JsonPipe, NgFor, NgIf } from '@angular/common';
import { FormsModule, NgForm } from '@angular/forms';
import { Note } from '../../note';
import { shareReplay, Subject, switchMap, take } from 'rxjs';
import { catchError, of, shareReplay, Subject, switchMap, take } from 'rxjs';
import { waitFor } from '@analogjs/trpc';
import { TRPCClientError } from '@trpc/client';
import { AppRouter } from '../../server/trpc/routers';

const inputTw =
'focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:outline-0 block w-full appearance-none rounded-lg px-3 py-2 transition-colors text-base leading-tight md:text-sm bg-black/[.05] dark:bg-zinc-50/10 focus:bg-white dark:focus:bg-dark placeholder:text-zinc-500 dark:placeholder:text-zinc-400 contrast-more:border contrast-more:border-current';
Expand All @@ -28,6 +35,9 @@ const btnTw =
src="/assets/spartan.svg"
/>
</div>
<button data-testid="loginBtn" (click)="toggleLogin()" class="${btnTw}">
{{ loggedIn() ? 'Log out' : 'Log in' }}
</button>
<form class="py-2 flex items-center" #f="ngForm" (ngSubmit)="addPost(f)">
<label class="sr-only" for="newNote"> Note </label>
<input
Expand Down Expand Up @@ -89,6 +99,9 @@ const btnTw =
</div>
</div>
</ng-template>
<p data-testid="deleteError" *ngIf="error()?.message">
{{ error()?.message }}
</p>
`,
})
export default class HomeComponent {
Expand All @@ -99,10 +112,23 @@ export default class HomeComponent {
shareReplay(1)
);
public newNote = '';
public loggedIn = signal(false);
public error = signal<TRPCClientError<AppRouter> | undefined>(undefined);

constructor() {
void waitFor(this.notes$);
this.triggerRefresh$.next();

effect(
() =>
tRPCHeaders.mutate(
(h) =>
(h['authorization'] = this.loggedIn()
? 'Bearer authToken'
: undefined)
),
{ allowSignalWrites: true }
);
}

public noteTrackBy = (index: number, note: Note) => {
Expand All @@ -114,6 +140,7 @@ export default class HomeComponent {
form.form.markAllAsTouched();
return;
}
console.log(tRPCHeaders());
this._trpc.note.create
.mutate({ title: this.newNote })
.pipe(take(1))
Expand All @@ -123,9 +150,20 @@ export default class HomeComponent {
}

public removePost(id: number) {
this.error.set(undefined);
this._trpc.note.remove
.mutate({ id })
.pipe(take(1))
.pipe(
take(1),
catchError((e) => {
this.error.set(e);
return of(null);
})
)
.subscribe(() => this.triggerRefresh$.next());
}

public toggleLogin() {
this.loggedIn.update((loggedIn) => !loggedIn);
}
}
10 changes: 9 additions & 1 deletion apps/trpc-app/src/server/trpc/context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { inferAsyncReturnType } from '@trpc/server';
import { getRequestHeader, H3Event } from 'h3';

/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export const createContext = () => ({});
export const createContext = async (event: H3Event) => {
// Create your context based on the request object
// Will be available as `ctx` in all your resolvers
const authorization = getRequestHeader(event, 'authorization');
return {
hasAuth: authorization && authorization.split(' ')[1]?.length > 0,
};
};
export type Context = inferAsyncReturnType<typeof createContext>;
4 changes: 2 additions & 2 deletions apps/trpc-app/src/server/trpc/routers/notes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
import { protectedProcedure, publicProcedure, router } from '../trpc';
import { Note } from '../../../note';

let noteId = 0;
Expand All @@ -19,7 +19,7 @@ export const noteRouter = router({
})
),
list: publicProcedure.query(() => notes),
remove: publicProcedure
remove: protectedProcedure
.input(
z.object({
id: z.number(),
Expand Down
13 changes: 12 additions & 1 deletion apps/trpc-app/src/server/trpc/trpc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { initTRPC } from '@trpc/server';
import { initTRPC, TRPCError } from '@trpc/server';
import { Context } from './context';
import superjson from 'superjson';

Expand All @@ -9,5 +9,16 @@ const t = initTRPC.context<Context>().create({
* Unprotected procedure
**/
export const publicProcedure = t.procedure;

const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.hasAuth) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx,
});
});

export const protectedProcedure = t.procedure.use(isAuthed);
export const router = t.router;
export const middleware = t.middleware;
13 changes: 7 additions & 6 deletions apps/trpc-app/src/trpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { createTrpcClient } from '@analogjs/trpc';
import { inject } from '@angular/core';
import superjson from 'superjson';

export const { provideTRPCClient, tRPCClient } = createTrpcClient<AppRouter>({
url: 'http://localhost:4205/api/trpc',
options: {
transformer: superjson,
},
});
export const { provideTRPCClient, tRPCClient, tRPCHeaders } =
createTrpcClient<AppRouter>({
url: 'http://localhost:4205/api/trpc',
options: {
transformer: superjson,
},
});

export function injectTRPCClient() {
return inject(tRPCClient);
Expand Down
13 changes: 11 additions & 2 deletions packages/trpc/src/lib/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InjectionToken, Provider, TransferState } from '@angular/core';
import { InjectionToken, Provider, signal, TransferState } from '@angular/core';
import 'isomorphic-fetch';
import { httpBatchLink } from '@trpc/client';
import { httpBatchLink, HttpBatchLinkOptions } from '@trpc/client';
import { AnyRouter } from '@trpc/server';
import { transferStateLink } from './links/transfer-state-link';
import {
Expand All @@ -10,10 +10,12 @@ import {
} from './cache-state';
import { createTRPCRxJSProxyClient } from './trpc-rxjs-proxy';
import { CreateTRPCClientOptions } from '@trpc/client/src/createTRPCUntypedClient';
import { HTTPHeaders } from '@trpc/client/src/links/types';

export type TrpcOptions<T extends AnyRouter> = {
url: string;
options?: Partial<CreateTRPCClientOptions<T>>;
batchLinkOptions?: Omit<HttpBatchLinkOptions, 'url' | 'headers'>;
};

export type TrpcClient<AppRouter extends AnyRouter> = ReturnType<
Expand All @@ -25,7 +27,9 @@ const tRPC_INJECTION_TOKEN = new InjectionToken<unknown>(
export const createTrpcClient = <AppRouter extends AnyRouter>({
url,
options,
batchLinkOptions,
}: TrpcOptions<AppRouter>) => {
const tRPCHeaders = signal<HTTPHeaders>({});
const provideTRPCClient = (): Provider[] => [
provideTrpcCacheState(),
provideTrpcCacheStateStatusManager(),
Expand All @@ -40,6 +44,10 @@ export const createTrpcClient = <AppRouter extends AnyRouter>({
...(options?.links ?? []),
transferStateLink(),
httpBatchLink({
...(batchLinkOptions ?? {}),
headers() {
return tRPCHeaders();
},
url: url ?? '',
}),
],
Expand All @@ -51,5 +59,6 @@ export const createTrpcClient = <AppRouter extends AnyRouter>({
return {
tRPCClient: tRPC_INJECTION_TOKEN as InjectionToken<TrpcClient<AppRouter>>,
provideTRPCClient,
tRPCHeaders,
};
};

0 comments on commit a2b7eae

Please sign in to comment.