@orpc/nest 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,61 @@ You can find the full documentation [here](https://orpc.unnoq.com).
63
63
 
64
64
  ## `@orpc/nest`
65
65
 
66
- Deeply integrate oRPC with [NestJS](https://nestjs.com/).
66
+ Deeply integrate oRPC with [NestJS](https://nestjs.com/). Read the [documentation](https://orpc.unnoq.com/docs/openapi/nest/implement-contract) for more information.
67
+
68
+ ### Implement Contract
69
+
70
+ An overview of how to implement an [oRPC contract](https://orpc.unnoq.com/docs/contract-first/define-contract) in NestJS.
71
+
72
+ ```ts
73
+ import { Implement, implement, ORPCError } from '@orpc/nest'
74
+
75
+ @Controller()
76
+ export class PlanetController {
77
+ /**
78
+ * Implement a standalone procedure
79
+ */
80
+ @Implement(contract.planet.list)
81
+ list() {
82
+ return implement(contract.planet.list).handler(({ input }) => {
83
+ // Implement logic here
84
+
85
+ return []
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Implement entire a contract
91
+ */
92
+ @Implement(contract.planet)
93
+ planet() {
94
+ return {
95
+ list: implement(contract.planet.list).handler(({ input }) => {
96
+ // Implement logic here
97
+ return []
98
+ }),
99
+ find: implement(contract.planet.find).handler(({ input }) => {
100
+ // Implement logic here
101
+ return {
102
+ id: 1,
103
+ name: 'Earth',
104
+ description: 'The planet Earth',
105
+ }
106
+ }),
107
+ create: implement(contract.planet.create).handler(({ input }) => {
108
+ // Implement logic here
109
+ return {
110
+ id: 1,
111
+ name: 'Earth',
112
+ description: 'The planet Earth',
113
+ }
114
+ }),
115
+ }
116
+ }
117
+
118
+ // other handlers...
119
+ }
120
+ ```
67
121
 
68
122
  ## Sponsors
69
123
 
package/dist/index.d.mts CHANGED
@@ -1,17 +1,36 @@
1
- import { AnyContractProcedure, HTTPPath, AnyContractRouter } from '@orpc/contract';
2
- import { BuilderConfig, ImplementerInternal } from '@orpc/server';
3
- export { ORPCError } from '@orpc/server';
4
1
  import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
+ import { ContractRouter, HTTPPath, AnyContractRouter, ContractProcedure } from '@orpc/contract';
3
+ import { Router } from '@orpc/server';
4
+ export { ImplementedProcedure, Implementer, ImplementerInternal, ImplementerInternalWithMiddlewares, ORPCError, ProcedureImplementer, RouterImplementer, RouterImplementerWithMiddlewares, implement } from '@orpc/server';
5
+ import { Promisable } from '@orpc/shared';
5
6
  import { Observable } from 'rxjs';
6
7
 
7
- declare function Implement(contract: AnyContractProcedure): MethodDecorator;
8
-
8
+ /**
9
+ * Decorator in controller handler to implement a oRPC contract.
10
+ *
11
+ * @see {@link https://orpc.unnoq.com/docs/openapi/nest/implement-contract#implement-your-contract NestJS Implement Contract Docs}
12
+ */
13
+ declare function Implement<T extends ContractRouter<any>>(contract: T): <U extends Promisable<Router<T, Record<never, never>>>>(target: Record<PropertyKey, any>, propertyKey: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => U>) => void;
9
14
  declare class ImplementInterceptor implements NestInterceptor {
10
15
  intercept(ctx: ExecutionContext, next: CallHandler<any>): Observable<any>;
11
16
  }
12
17
 
13
- declare function toFastifyPattern(path: HTTPPath): string;
14
-
15
- declare function implement<T extends AnyContractRouter>(contract: T, config?: BuilderConfig): ImplementerInternal<T, Record<never, never>, Record<never, never>>;
18
+ declare function toNestPattern(path: HTTPPath): string;
19
+ type PopulatedContractRouterPaths<T extends AnyContractRouter> = T extends ContractProcedure<infer UInputSchema, infer UOutputSchema, infer UErrors, infer UMeta> ? ContractProcedure<UInputSchema, UOutputSchema, UErrors, UMeta> : {
20
+ [K in keyof T]: T[K] extends AnyContractRouter ? PopulatedContractRouterPaths<T[K]> : never;
21
+ };
22
+ interface PopulateContractRouterPathsOptions {
23
+ path?: readonly string[];
24
+ }
25
+ /**
26
+ * populateContractRouterPaths is completely optional,
27
+ * because the procedure's path is required for NestJS implementation.
28
+ * This utility automatically populates any missing paths
29
+ * Using the router's keys + `/`.
30
+ *
31
+ * @see {@link https://orpc.unnoq.com/docs/openapi/nest/implement-contract#define-your-contract NestJS Implement Contract Docs}
32
+ */
33
+ declare function populateContractRouterPaths<T extends AnyContractRouter>(router: T, options?: PopulateContractRouterPathsOptions): PopulatedContractRouterPaths<T>;
16
34
 
17
- export { Implement, ImplementInterceptor, implement, toFastifyPattern };
35
+ export { Implement as Impl, Implement, ImplementInterceptor, populateContractRouterPaths, toNestPattern };
36
+ export type { PopulateContractRouterPathsOptions, PopulatedContractRouterPaths };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,36 @@
1
- import { AnyContractProcedure, HTTPPath, AnyContractRouter } from '@orpc/contract';
2
- import { BuilderConfig, ImplementerInternal } from '@orpc/server';
3
- export { ORPCError } from '@orpc/server';
4
1
  import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
+ import { ContractRouter, HTTPPath, AnyContractRouter, ContractProcedure } from '@orpc/contract';
3
+ import { Router } from '@orpc/server';
4
+ export { ImplementedProcedure, Implementer, ImplementerInternal, ImplementerInternalWithMiddlewares, ORPCError, ProcedureImplementer, RouterImplementer, RouterImplementerWithMiddlewares, implement } from '@orpc/server';
5
+ import { Promisable } from '@orpc/shared';
5
6
  import { Observable } from 'rxjs';
6
7
 
7
- declare function Implement(contract: AnyContractProcedure): MethodDecorator;
8
-
8
+ /**
9
+ * Decorator in controller handler to implement a oRPC contract.
10
+ *
11
+ * @see {@link https://orpc.unnoq.com/docs/openapi/nest/implement-contract#implement-your-contract NestJS Implement Contract Docs}
12
+ */
13
+ declare function Implement<T extends ContractRouter<any>>(contract: T): <U extends Promisable<Router<T, Record<never, never>>>>(target: Record<PropertyKey, any>, propertyKey: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => U>) => void;
9
14
  declare class ImplementInterceptor implements NestInterceptor {
10
15
  intercept(ctx: ExecutionContext, next: CallHandler<any>): Observable<any>;
11
16
  }
12
17
 
13
- declare function toFastifyPattern(path: HTTPPath): string;
14
-
15
- declare function implement<T extends AnyContractRouter>(contract: T, config?: BuilderConfig): ImplementerInternal<T, Record<never, never>, Record<never, never>>;
18
+ declare function toNestPattern(path: HTTPPath): string;
19
+ type PopulatedContractRouterPaths<T extends AnyContractRouter> = T extends ContractProcedure<infer UInputSchema, infer UOutputSchema, infer UErrors, infer UMeta> ? ContractProcedure<UInputSchema, UOutputSchema, UErrors, UMeta> : {
20
+ [K in keyof T]: T[K] extends AnyContractRouter ? PopulatedContractRouterPaths<T[K]> : never;
21
+ };
22
+ interface PopulateContractRouterPathsOptions {
23
+ path?: readonly string[];
24
+ }
25
+ /**
26
+ * populateContractRouterPaths is completely optional,
27
+ * because the procedure's path is required for NestJS implementation.
28
+ * This utility automatically populates any missing paths
29
+ * Using the router's keys + `/`.
30
+ *
31
+ * @see {@link https://orpc.unnoq.com/docs/openapi/nest/implement-contract#define-your-contract NestJS Implement Contract Docs}
32
+ */
33
+ declare function populateContractRouterPaths<T extends AnyContractRouter>(router: T, options?: PopulateContractRouterPathsOptions): PopulatedContractRouterPaths<T>;
16
34
 
17
- export { Implement, ImplementInterceptor, implement, toFastifyPattern };
35
+ export { Implement as Impl, Implement, ImplementInterceptor, populateContractRouterPaths, toNestPattern };
36
+ export type { PopulateContractRouterPathsOptions, PopulatedContractRouterPaths };
package/dist/index.mjs CHANGED
@@ -1,133 +1,87 @@
1
- import { call, ORPCError as ORPCError$1, implementerInternal } from '@orpc/server';
2
- export { ORPCError } from '@orpc/server';
3
- import { applyDecorators, Get, Head, Put, Patch, Delete, Post, UseInterceptors } from '@nestjs/common';
4
- import { fallbackContractConfig } from '@orpc/contract';
5
- import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializer, StandardBracketNotationSerializer, standardizeHTTPPath } from '@orpc/openapi-client/standard';
1
+ import { applyDecorators, Delete, Patch, Put, Post, Get, Head, UseInterceptors } from '@nestjs/common';
2
+ import { toORPCError } from '@orpc/client';
3
+ import { isContractProcedure, ContractProcedure, fallbackContractConfig } from '@orpc/contract';
4
+ import { standardizeHTTPPath, StandardOpenAPISerializer, StandardOpenAPIJsonSerializer, StandardBracketNotationSerializer } from '@orpc/openapi-client/standard';
6
5
  import { StandardOpenAPICodec } from '@orpc/openapi/standard';
6
+ import { getRouter, unlazy, isProcedure, createProcedureClient, ORPCError } from '@orpc/server';
7
+ export { ORPCError, implement } from '@orpc/server';
8
+ import { toArray, get } from '@orpc/shared';
9
+ import { flattenHeader } from '@orpc/standard-server';
7
10
  import { toStandardLazyRequest, sendStandardResponse } from '@orpc/standard-server-node';
8
11
  import { mergeMap } from 'rxjs';
9
- import '@orpc/shared';
12
+ import { toHttpPath } from '@orpc/client/standard';
10
13
 
11
- const COMMON_ORPC_ERROR_DEFS = {
12
- BAD_REQUEST: {
13
- status: 400,
14
- message: "Bad Request"
15
- },
16
- UNAUTHORIZED: {
17
- status: 401,
18
- message: "Unauthorized"
19
- },
20
- FORBIDDEN: {
21
- status: 403,
22
- message: "Forbidden"
23
- },
24
- NOT_FOUND: {
25
- status: 404,
26
- message: "Not Found"
27
- },
28
- METHOD_NOT_SUPPORTED: {
29
- status: 405,
30
- message: "Method Not Supported"
31
- },
32
- NOT_ACCEPTABLE: {
33
- status: 406,
34
- message: "Not Acceptable"
35
- },
36
- TIMEOUT: {
37
- status: 408,
38
- message: "Request Timeout"
39
- },
40
- CONFLICT: {
41
- status: 409,
42
- message: "Conflict"
43
- },
44
- PRECONDITION_FAILED: {
45
- status: 412,
46
- message: "Precondition Failed"
47
- },
48
- PAYLOAD_TOO_LARGE: {
49
- status: 413,
50
- message: "Payload Too Large"
51
- },
52
- UNSUPPORTED_MEDIA_TYPE: {
53
- status: 415,
54
- message: "Unsupported Media Type"
55
- },
56
- UNPROCESSABLE_CONTENT: {
57
- status: 422,
58
- message: "Unprocessable Content"
59
- },
60
- TOO_MANY_REQUESTS: {
61
- status: 429,
62
- message: "Too Many Requests"
63
- },
64
- CLIENT_CLOSED_REQUEST: {
65
- status: 499,
66
- message: "Client Closed Request"
67
- },
68
- INTERNAL_SERVER_ERROR: {
69
- status: 500,
70
- message: "Internal Server Error"
71
- },
72
- NOT_IMPLEMENTED: {
73
- status: 501,
74
- message: "Not Implemented"
75
- },
76
- BAD_GATEWAY: {
77
- status: 502,
78
- message: "Bad Gateway"
79
- },
80
- SERVICE_UNAVAILABLE: {
81
- status: 503,
82
- message: "Service Unavailable"
83
- },
84
- GATEWAY_TIMEOUT: {
85
- status: 504,
86
- message: "Gateway Timeout"
87
- }
88
- };
89
- function fallbackORPCErrorStatus(code, status) {
90
- return status ?? COMMON_ORPC_ERROR_DEFS[code]?.status ?? 500;
91
- }
92
- function fallbackORPCErrorMessage(code, message) {
93
- return message || COMMON_ORPC_ERROR_DEFS[code]?.message || code;
14
+ function toNestPattern(path) {
15
+ return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/*$1").replace(/\/\{([^}]+)\}/g, "/:$1");
94
16
  }
95
- class ORPCError extends Error {
96
- defined;
97
- code;
98
- status;
99
- data;
100
- constructor(code, ...[options]) {
101
- if (options?.status && !isORPCErrorStatus(options.status)) {
102
- throw new Error("[ORPCError] Invalid error status code.");
17
+ function populateContractRouterPaths(router, options = {}) {
18
+ const path = toArray(options.path);
19
+ if (isContractProcedure(router)) {
20
+ if (router["~orpc"].route.path === void 0) {
21
+ return new ContractProcedure({
22
+ ...router["~orpc"],
23
+ route: {
24
+ ...router["~orpc"].route,
25
+ path: toHttpPath(path)
26
+ }
27
+ });
103
28
  }
104
- const message = fallbackORPCErrorMessage(code, options?.message);
105
- super(message, options);
106
- this.code = code;
107
- this.status = fallbackORPCErrorStatus(code, options?.status);
108
- this.defined = options?.defined ?? false;
109
- this.data = options?.data;
29
+ return router;
110
30
  }
111
- toJSON() {
112
- return {
113
- defined: this.defined,
114
- code: this.code,
115
- status: this.status,
116
- message: this.message,
117
- data: this.data
118
- };
31
+ const populated = {};
32
+ for (const key in router) {
33
+ populated[key] = populateContractRouterPaths(router[key], { ...options, path: [...path, key] });
119
34
  }
120
- }
121
- function toORPCError(error) {
122
- return error instanceof ORPCError ? error : new ORPCError("INTERNAL_SERVER_ERROR", {
123
- message: "Internal server error",
124
- cause: error
125
- });
126
- }
127
- function isORPCErrorStatus(status) {
128
- return status < 200 || status >= 400;
35
+ return populated;
129
36
  }
130
37
 
38
+ const MethodDecoratorMap = {
39
+ HEAD: Head,
40
+ GET: Get,
41
+ POST: Post,
42
+ PUT: Put,
43
+ PATCH: Patch,
44
+ DELETE: Delete
45
+ };
46
+ function Implement(contract) {
47
+ if (isContractProcedure(contract)) {
48
+ const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
49
+ const path = contract["~orpc"].route.path;
50
+ if (path === void 0) {
51
+ throw new Error(`
52
+ @Implement decorator requires contract to have a 'path'.
53
+ Please define one using 'path' property on the '.route' method.
54
+ Or use "populateContractRouterPaths" utility to automatically fill in any missing paths.
55
+ `);
56
+ }
57
+ return (target, propertyKey, descriptor) => {
58
+ applyDecorators(
59
+ MethodDecoratorMap[method](toNestPattern(path)),
60
+ UseInterceptors(ImplementInterceptor)
61
+ )(target, propertyKey, descriptor);
62
+ };
63
+ }
64
+ return (target, propertyKey, descriptor) => {
65
+ for (const key in contract) {
66
+ let methodName = `${propertyKey}_${key}`;
67
+ let i = 0;
68
+ while (methodName in target) {
69
+ methodName = `${propertyKey}_${key}_${i++}`;
70
+ }
71
+ target[methodName] = async function(...args) {
72
+ const router = await descriptor.value.apply(this, args);
73
+ return getRouter(router, [key]);
74
+ };
75
+ for (const p of Reflect.getOwnMetadataKeys(target, propertyKey)) {
76
+ Reflect.defineMetadata(p, Reflect.getOwnMetadata(p, target, propertyKey), target, methodName);
77
+ }
78
+ for (const p of Reflect.getOwnMetadataKeys(target.constructor, propertyKey)) {
79
+ Reflect.defineMetadata(p, Reflect.getOwnMetadata(p, target.constructor, propertyKey), target.constructor, methodName);
80
+ }
81
+ Implement(get(contract, [key]))(target, methodName, Object.getOwnPropertyDescriptor(target, methodName));
82
+ }
83
+ };
84
+ }
131
85
  const codec = new StandardOpenAPICodec(
132
86
  new StandardOpenAPISerializer(
133
87
  new StandardOpenAPIJsonSerializer(),
@@ -137,7 +91,13 @@ const codec = new StandardOpenAPICodec(
137
91
  class ImplementInterceptor {
138
92
  intercept(ctx, next) {
139
93
  return next.handle().pipe(
140
- mergeMap(async (procedure) => {
94
+ mergeMap(async (impl) => {
95
+ const { default: procedure } = await unlazy(impl);
96
+ if (!isProcedure(procedure)) {
97
+ throw new Error(`
98
+ The return value of the @Implement controller handler must be a corresponding implemented router or procedure.
99
+ `);
100
+ }
141
101
  const req = ctx.switchToHttp().getRequest();
142
102
  const res = ctx.switchToHttp().getResponse();
143
103
  const nodeReq = "raw" in req ? req.raw : req;
@@ -148,13 +108,17 @@ class ImplementInterceptor {
148
108
  const standardResponse = await (async () => {
149
109
  let isDecoding = false;
150
110
  try {
111
+ const client = createProcedureClient(procedure);
151
112
  isDecoding = true;
152
113
  const input = await codec.decode(standardRequest, flattenParams(req.params), procedure);
153
114
  isDecoding = false;
154
- const output = await call(procedure, input);
115
+ const output = await client(input, {
116
+ signal: standardRequest.signal,
117
+ lastEventId: flattenHeader(standardRequest.headers["last-event-id"])
118
+ });
155
119
  return codec.encode(output, procedure);
156
120
  } catch (e) {
157
- const error = isDecoding && !(e instanceof ORPCError$1) ? new ORPCError$1("BAD_REQUEST", {
121
+ const error = isDecoding && !(e instanceof ORPCError) ? new ORPCError("BAD_REQUEST", {
158
122
  message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`,
159
123
  cause: e
160
124
  }) : toORPCError(e);
@@ -178,30 +142,4 @@ function flattenParams(params) {
178
142
  return flatten;
179
143
  }
180
144
 
181
- function toFastifyPattern(path) {
182
- return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/*$1").replace(/\/\{([^}]+)\}/g, "/:$1");
183
- }
184
-
185
- function Implement(contract) {
186
- const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
187
- const path = contract["~orpc"].route.path;
188
- if (path === void 0) {
189
- throw new Error(`
190
- oRPC Fastify integration requires procedure to have a 'path'.
191
- Please define one using 'path' property on the '.route' method.
192
- `);
193
- }
194
- return (target, propertyKey, descriptor) => {
195
- const MethodDecorator = method === "GET" ? Get : method === "HEAD" ? Head : method === "PUT" ? Put : method === "PATCH" ? Patch : method === "DELETE" ? Delete : Post;
196
- applyDecorators(
197
- MethodDecorator(toFastifyPattern(path)),
198
- UseInterceptors(ImplementInterceptor)
199
- )(target, propertyKey, descriptor);
200
- };
201
- }
202
-
203
- function implement(contract, config = {}) {
204
- return implementerInternal(contract, config, []);
205
- }
206
-
207
- export { Implement, ImplementInterceptor, implement, toFastifyPattern };
145
+ export { Implement as Impl, Implement, ImplementInterceptor, populateContractRouterPaths, toNestPattern };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/nest",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.2",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orpc.unnoq.com",
7
7
  "repository": {
@@ -28,8 +28,7 @@
28
28
  "@nestjs/core": ">=11.0.0",
29
29
  "express": ">=5.0.0",
30
30
  "fastify": ">=5.0.0",
31
- "rxjs": ">=7.0.0",
32
- "@orpc/contract": "1.2.0"
31
+ "rxjs": ">=7.0.0"
33
32
  },
34
33
  "peerDependenciesMeta": {
35
34
  "express": {
@@ -40,27 +39,33 @@
40
39
  }
41
40
  },
42
41
  "dependencies": {
43
- "@orpc/openapi-client": "1.2.0",
44
- "@orpc/server": "1.2.0",
45
42
  "@orpc/client": "1.2.0",
43
+ "@orpc/openapi": "1.2.0",
46
44
  "@orpc/shared": "1.2.0",
45
+ "@orpc/contract": "1.2.0",
46
+ "@orpc/openapi-client": "1.2.0",
47
+ "@orpc/server": "1.2.0",
47
48
  "@orpc/standard-server": "1.2.0",
48
- "@orpc/standard-server-node": "1.2.0",
49
- "@orpc/openapi": "1.2.0"
49
+ "@orpc/standard-server-node": "1.2.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@nestjs/common": "^11.0.0",
52
+ "@nestjs/common": "^11.1.0",
53
53
  "@nestjs/core": "^11.0.0",
54
- "@nestjs/platform-express": "^11.0.0",
55
- "@nestjs/platform-fastify": "^11.0.0",
54
+ "@nestjs/platform-express": "^11.1.0",
55
+ "@nestjs/platform-fastify": "^11.1.0",
56
+ "@nestjs/testing": "^11.1.0",
57
+ "@ts-rest/core": "^3.52.1",
56
58
  "@types/express": "^5.0.1",
57
59
  "express": "^5.0.0",
58
60
  "fastify": "^5.0.0",
59
- "rxjs": "^7.0.0"
61
+ "rxjs": "^7.0.0",
62
+ "supertest": "^7.1.0",
63
+ "zod": "^3.24.4"
60
64
  },
61
65
  "scripts": {
62
66
  "build": "unbuild",
63
67
  "build:watch": "pnpm run build --watch",
64
- "type:check": "tsc -b"
68
+ "type:check": "tsc -b",
69
+ "type:check:test": "tsc -p tsconfig.test.json --noEmit"
65
70
  }
66
71
  }