Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions apps/trpc-app/src/app/pages/index.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ const btnTw =
<form class="py-2 flex items-center" #f="ngForm" (ngSubmit)="addPost(f)">
<label class="sr-only" for="newNote"> Note </label>
<input
id="newNote"
required
autocomplete="off"
data-testid="newNoteInput"
class="${inputTw}"
name="newNote"
[(ngModel)]="newNote"
placeholder="Enter a note..."
/>
<button data-testid="addNoteBtn" class="ml-2 ${btnTw}">+</button>
</form>
Expand Down Expand Up @@ -134,16 +136,18 @@ export default class HomeComponent {
};

public addPost(form: NgForm) {
if (!form.valid) {
if (!form.valid || !this.newNote?.trim()) {
form.form.markAllAsTouched();
return;
}
this._trpc.note.create
.mutate({ title: this.newNote })
.mutate({ title: this.newNote.trim() })
.pipe(take(1))
.subscribe(() => this.triggerRefresh$.next());
this.newNote = '';
form.form.reset();
.subscribe(() => {
this.triggerRefresh$.next();
this.newNote = '';
form.form.reset();
});
}

public removePost(id: number) {
Expand Down
4 changes: 4 additions & 0 deletions apps/trpc-app/src/server/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ import { noteRouter } from './notes';
export const appRouter = router({
note: noteRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;

// Also export createCaller for server-side usage
export const createCaller = appRouter.createCaller;
14 changes: 8 additions & 6 deletions apps/trpc-app/src/trpc-client.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { AppRouter } from './server/trpc/routers';
import type { AppRouter } from './server/trpc/routers';
import type { CreateTrpcProxyClient } from '@benpsnyder/analogjs-esm-trpc';
import { createTrpcClient } from '@benpsnyder/analogjs-esm-trpc';
import { inject } from '@angular/core';
import superjson from 'superjson';

export const { provideTrpcClient, TrpcClient, TrpcHeaders } =
createTrpcClient<AppRouter>({
url: '/api/trpc',
options: {
transformer: superjson,
},
transformer: superjson,
});

export function injectTrpcClient() {
return inject(TrpcClient);
// Ensure the injected type is preserved
export function injectTrpcClient(): CreateTrpcProxyClient<AppRouter> {
const client = inject(TrpcClient);
// Add type assertion to help with debugging
return client as CreateTrpcProxyClient<AppRouter>;
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@
"@mdx-js/react": "3.1.0",
"@nx/angular": "21.3.11",
"@nx/devkit": "21.3.11",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@trpc/client": "^11.0.0",
"@trpc/server": "^11.0.0",
"ajv-formats": "^3.0.1",
"defu": "^6.1.4",
"destr": "^2.0.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ export const appConfig: ApplicationConfig = {
expect(configContent).toContain(
'@benpsnyder/analogjs-esm-content/prism-highlighter',
);
});
}, 10000); // Increase timeout to 10 seconds
});
22 changes: 0 additions & 22 deletions packages/nx-plugin/package-lock.json

This file was deleted.

113 changes: 110 additions & 3 deletions packages/trpc/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,112 @@
# Analog
# @analogjs/trpc

The fullstack meta-framework for Angular
Angular/Nitro-based tRPC integration for Analog.

Learn more at [analogjs.org](https://analogjs.org)
## Features

- Full tRPC v11 support
- Angular integration with RxJS observables
- Server-side rendering (SSR) support
- Transfer state for hydration
- Type-safe API calls

## Installation

```bash
npm install @analogjs/trpc @trpc/client@^11.0.0 @trpc/server@^11.0.0
```

## Usage

### Client Setup

```typescript
// trpc-client.ts
import { AppRouter } from './server/trpc/routers';
import { createTrpcClient } from '@analogjs/trpc';
import { inject } from '@angular/core';
import superjson from 'superjson';

export const { provideTrpcClient, TrpcClient, TrpcHeaders } =
createTrpcClient<AppRouter>({
url: '/api/trpc',
options: {
transformer: superjson, // Optional: transformers are now handled via links
},
});

export function injectTrpcClient() {
return inject(TrpcClient);
}
```

### Server Setup

```typescript
// server/trpc/routers/index.ts
import { router } from '../trpc';
import { noteRouter } from './notes';

export const appRouter = router({
note: noteRouter,
});

export type AppRouter = typeof appRouter;
```

### Component Usage

```typescript
// my-component.ts
import { Component } from '@angular/core';
import { injectTrpcClient } from './trpc-client';

@Component({
selector: 'app-my-component',
template: `
@if (hello$ | async; as hello) {
{{ hello.greeting }}
}
`,
})
export class MyComponent {
private trpc = injectTrpcClient();
hello$ = this.trpc.hello.query({ name: 'Analog' });
}
```

## tRPC v11 Migration

This package now supports tRPC v11. The main changes include:

- Transformers are now handled via links instead of the `transformer` option
- Backward compatibility is maintained for existing transformer usage
- Updated peer dependencies to require tRPC v11

### Breaking Changes

- `@trpc/client` and `@trpc/server` must be version 11.0.0 or higher
- Transformers are now processed through links (backward compatible)

## API Reference

### `createTrpcClient<TRouter>(options)`

Creates a tRPC client with Angular integration.

#### Options

- `url`: The base URL for tRPC API calls
- `options.transformer`: (Deprecated) Use transformer link instead
- `options.links`: Additional tRPC links
- `batchLinkOptions`: Options for the HTTP batch link

#### Returns

- `TrpcClient`: Injection token for the tRPC client
- `provideTrpcClient`: Provider function for Angular DI
- `TrpcHeaders`: Signal for managing HTTP headers

## License

MIT
4 changes: 2 additions & 2 deletions packages/trpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"peerDependencies": {
"@angular/common": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/core": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@trpc/client": "^10.25.0",
"@trpc/server": "^10.25.0",
"@trpc/client": "^11.0.0",
"@trpc/server": "^11.0.0",
"isomorphic-fetch": "^3.0.0",
"superjson": "^2.2.1"
},
Expand Down
63 changes: 31 additions & 32 deletions packages/trpc/server/src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
* **/

import type { ResponseMeta } from '@trpc/server/http';
import { resolveHTTPResponse } from '@trpc/server/http';
import { resolveResponse } from '@trpc/server/http';
import type {
AnyRouter,
inferRouterContext,
inferRouterError,
ProcedureType,
} from '@trpc/server';
import { TRPCError } from '@trpc/server';
import { createURL } from 'ufo';
import type { H3Event } from 'h3';
import { createError, defineEventHandler, isMethod, readBody } from 'h3';
import {
createError,
defineEventHandler,
isMethod,
readBody,
getRequestURL,
} from 'h3';
import type { TRPCResponse } from '@trpc/server/rpc';

type MaybePromise<T> = T | Promise<T>;
Expand Down Expand Up @@ -83,61 +88,55 @@ export function createTrpcNitroHandler<TRouter extends AnyRouter>({
return defineEventHandler(async (event) => {
const { req, res } = event.node;

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const $url = createURL(req.url!);
// Get the full URL using h3's getRequestURL
const fullUrl = getRequestURL(event);

const path = getPath(event);

if (path === null) {
const error = router.getErrorShape({
error: new TRPCError({
throw createError({
statusCode: 500,
statusMessage: JSON.stringify({
message:
'Param "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: 'INTERNAL_SERVER_ERROR',
}),
type: 'unknown',
ctx: undefined,
path: undefined,
input: undefined,
});

throw createError({
statusCode: 500,
statusMessage: JSON.stringify(error),
});
}

const httpResponse = await resolveHTTPResponse({
const body = isMethod(event, 'GET') ? null : await readBody(event);
const request = new Request(fullUrl, {
method: req.method!,
headers: req.headers as HeadersInit,
body: body ? JSON.stringify(body) : undefined,
});

const httpResponse = await resolveResponse({
batching,
router,
req: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams,
},
req: request,
path,
error: null,
createContext: async () => await createContext?.(event),
responseMeta,
onError: (o) => {
responseMeta: responseMeta as any,
onError: (o: any) => {
onError?.({
...o,
req,
});
},
});

const { status, headers, body } = httpResponse;
const { status, headers, body: responseBody } = httpResponse;

res.statusCode = status;

headers &&
Object.keys(headers).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
res.setHeader(key, headers[key]!);
if (headers) {
headers.forEach((value, key) => {
res.setHeader(key, value);
});
}

return body;
return responseBody;
});
}
Loading