Skip to content

Commit 047a5b7

Browse files
committed
fixup! feat(server): Change the nest integration to support Nest features that were competing with Orpc
1 parent 7f74cf3 commit 047a5b7

File tree

7 files changed

+93
-128
lines changed

7 files changed

+93
-128
lines changed

packages/nest/src/filters/orpc-exception.filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const codec = new StandardOpenAPICodec(
4545
@Injectable()
4646
export class ORPCExceptionFilter implements ExceptionFilter {
4747
constructor(
48-
private config: StandardServerNode.SendStandardResponseOptions | undefined,
48+
private config?: StandardServerNode.SendStandardResponseOptions | undefined,
4949
) {}
5050

5151
async catch(exception: ORPCError<any, any>, host: ArgumentsHost) {

packages/nest/src/implement.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import { oc, ORPCError } from '@orpc/contract'
1010
import { implement, lazy } from '@orpc/server'
1111
import * as StandardServerNode from '@orpc/standard-server-node'
1212
import supertest from 'supertest'
13-
import { expect, it, vi } from 'vitest'
13+
import { beforeEach, describe, expect, it, vi } from 'vitest'
1414
import { z } from 'zod'
1515
import { ORPCExceptionFilter } from './filters/orpc-exception.filter'
1616
import { Implement } from './implement'
1717
import { ORPCModule } from './module'
18+
import * as Utils from './utils'
1819

1920
const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse')
20-
const setStandardResponseSpy = vi.spyOn(StandardServerNode, 'setStandardResponse')
21+
const setStandardResponseSpy = vi.spyOn(Utils, 'setStandardNodeResponse')
2122

2223
beforeEach(() => {
2324
vi.clearAllMocks()

packages/nest/src/implement.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as StandardServerFastify from '@orpc/standard-server-fastify'
1919
import * as StandardServerNode from '@orpc/standard-server-node'
2020
import { mergeMap } from 'rxjs'
2121
import { ORPC_MODULE_CONFIG_SYMBOL } from './module'
22-
import { toNestPattern } from './utils'
22+
import { setStandardFastifyResponse, setStandardNodeResponse, toNestPattern } from './utils'
2323

2424
const MethodDecoratorMap = {
2525
HEAD: Head,
@@ -176,10 +176,10 @@ export class ImplementInterceptor implements NestInterceptor {
176176
})()
177177
// Set status and headers
178178
if ('raw' in res) {
179-
return StandardServerFastify.setStandardResponse(res as FastifyReply, standardResponse, this.config)
179+
return setStandardFastifyResponse(res as FastifyReply, standardResponse, this.config)
180180
}
181181
else {
182-
return StandardServerNode.setStandardResponse(res as Response, standardResponse, this.config)
182+
return setStandardNodeResponse(res as Response, standardResponse, this.config)
183183
}
184184
}),
185185
)

packages/nest/src/utils.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import type { AnyContractRouter, HTTPPath } from '@orpc/contract'
2+
import type { StandardBody, StandardHeaders, StandardResponse } from '@orpc/standard-server'
3+
import type { NodeHttpResponse } from '@orpc/standard-server-node'
4+
import type { FastifyReply } from 'fastify/types/reply'
5+
import type { SendStandardResponseOptions } from '../../standard-server-aws-lambda/src'
6+
import { Readable } from 'node:stream'
27
import { toHttpPath } from '@orpc/client/standard'
38
import { ContractProcedure, isContractProcedure } from '@orpc/contract'
49
import { standardizeHTTPPath } from '@orpc/openapi-client/standard'
510
import { toArray } from '@orpc/shared'
11+
import { toNodeHttpBody } from '@orpc/standard-server-node'
612

713
export function toNestPattern(path: HTTPPath): string {
814
return standardizeHTTPPath(path)
@@ -54,3 +60,70 @@ export function populateContractRouterPaths<T extends AnyContractRouter>(router:
5460

5561
return populated as any
5662
}
63+
64+
export function setStandardFastifyResponse(
65+
reply: FastifyReply,
66+
standardResponse: StandardResponse,
67+
options: SendStandardResponseOptions = { shouldStringifyBody: false },
68+
) {
69+
if (options.shouldStringifyBody === undefined) {
70+
options.shouldStringifyBody = false
71+
}
72+
73+
return new Promise((resolve, reject) => {
74+
reply.raw.once('error', reject)
75+
reply.raw.once('close', resolve)
76+
77+
const resHeaders: StandardHeaders = { ...standardResponse.headers }
78+
79+
const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options)
80+
81+
reply.code(standardResponse.status)
82+
reply.headers(resHeaders)
83+
return resolve(resBody)
84+
})
85+
}
86+
87+
export function setStandardNodeResponse(
88+
res: NodeHttpResponse,
89+
standardResponse: StandardResponse,
90+
options: SendStandardResponseOptions = { shouldStringifyBody: false },
91+
): Promise<void | StandardBody | undefined> {
92+
if (options.shouldStringifyBody === undefined) {
93+
options.shouldStringifyBody = false
94+
}
95+
96+
return new Promise((resolve, reject) => {
97+
res.once('error', reject)
98+
res.once('close', resolve)
99+
100+
const resHeaders: StandardHeaders = { ...standardResponse.headers }
101+
102+
const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options)
103+
104+
res.statusCode = standardResponse.status
105+
for (const [key, value] of Object.entries(resHeaders)) {
106+
if (value !== undefined) {
107+
res.setHeader(key, value)
108+
}
109+
}
110+
111+
if (resBody === undefined) {
112+
return resolve(undefined)
113+
}
114+
else if (resBody instanceof Readable) {
115+
res.once('close', () => {
116+
if (!resBody.closed) {
117+
resBody.destroy(res.errored ?? undefined)
118+
}
119+
})
120+
121+
resBody.once('error', error => res.destroy(error))
122+
123+
resBody.pipe(res)
124+
}
125+
else {
126+
return resolve(resBody)
127+
}
128+
})
129+
}
Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { StandardHeaders, StandardResponse } from '@orpc/standard-server'
22
import type { ToNodeHttpBodyOptions } from '@orpc/standard-server-node'
33
import type { FastifyReply } from 'fastify'
4-
import { toNodeHttpBody, toResponseBody } from '@orpc/standard-server-node'
4+
import { toNodeHttpBody } from '@orpc/standard-server-node'
55

66
export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions { }
77

@@ -23,22 +23,3 @@ export function sendStandardResponse(
2323
reply.send(resBody)
2424
})
2525
}
26-
27-
export function setStandardResponse(
28-
reply: FastifyReply,
29-
standardResponse: StandardResponse,
30-
options: SendStandardResponseOptions = {},
31-
) {
32-
return new Promise((resolve, reject) => {
33-
reply.raw.once('error', reject)
34-
reply.raw.once('close', resolve)
35-
36-
const resHeaders: StandardHeaders = { ...standardResponse.headers }
37-
38-
const resBody = toResponseBody(standardResponse.body, resHeaders, options)
39-
40-
reply.code(standardResponse.status)
41-
reply.headers(resHeaders)
42-
return resolve(resBody)
43-
})
44-
}

packages/standard-server-node/src/body.ts

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export function toStandardBody(req: NodeHttpRequest, options: ToStandardBodyOpti
5050
})
5151
}
5252

53-
export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {}
53+
export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {
54+
shouldStringifyBody?: boolean
55+
}
5456

5557
/**
5658
* @param body
@@ -60,59 +62,11 @@ export interface ToNodeHttpBodyOptions extends ToEventStreamOptions {}
6062
export function toNodeHttpBody(
6163
body: StandardBody,
6264
headers: StandardHeaders,
63-
options: ToNodeHttpBodyOptions = {},
65+
options: ToNodeHttpBodyOptions = { shouldStringifyBody: true },
6466
): Readable | undefined | string {
65-
const currentContentDisposition = flattenHeader(headers['content-disposition'])
66-
67-
delete headers['content-type']
68-
delete headers['content-disposition']
69-
70-
if (body === undefined) {
71-
return
72-
}
73-
74-
if (body instanceof Blob) {
75-
headers['content-type'] = body.type
76-
headers['content-length'] = body.size.toString()
77-
headers['content-disposition'] = currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : 'blob')
78-
79-
return Readable.fromWeb(body.stream())
67+
if (options.shouldStringifyBody === undefined) {
68+
options.shouldStringifyBody = true
8069
}
81-
82-
if (body instanceof FormData) {
83-
const response = new Response(body)
84-
headers['content-type'] = response.headers.get('content-type')!
85-
86-
return Readable.fromWeb(response.body!)
87-
}
88-
89-
if (body instanceof URLSearchParams) {
90-
headers['content-type'] = 'application/x-www-form-urlencoded'
91-
92-
return body.toString()
93-
}
94-
95-
if (isAsyncIteratorObject(body)) {
96-
headers['content-type'] = 'text/event-stream'
97-
98-
return toEventStream(body, options)
99-
}
100-
101-
headers['content-type'] = 'application/json'
102-
103-
return stringifyJSON(body)
104-
}
105-
106-
/**
107-
* @param body
108-
* @param headers - WARNING: The headers can be mutated by the function and may affect the original headers.
109-
* @param options
110-
*/
111-
export function toResponseBody(
112-
body: StandardBody,
113-
headers: StandardHeaders,
114-
options: ToNodeHttpBodyOptions = {},
115-
): Readable | undefined | StandardBody {
11670
const currentContentDisposition = flattenHeader(headers['content-disposition'])
11771

11872
delete headers['content-type']
@@ -150,15 +104,11 @@ export function toResponseBody(
150104
}
151105

152106
headers['content-type'] = 'application/json'
153-
// It seems like Nest/Node, in case of a string body, remove or alter the string if
154-
// content type json is not set.
155-
// We also need to "double" stringify it, else the string will be encoded as an Array
156-
// This match the behavior of #toNodeHttpBody
157-
if (typeof body === 'string') {
158-
return stringifyJSON(body)
159-
}
160107

161-
return body
108+
if (options.shouldStringifyBody === false && typeof body !== 'string') {
109+
return body as unknown as string
110+
}
111+
return stringifyJSON(body)
162112
}
163113

164114
function _streamToFormData(stream: Readable, contentType: string): Promise<FormData> {
Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import type { StandardBody, StandardHeaders, StandardResponse } from '@orpc/standard-server'
1+
import type { StandardHeaders, StandardResponse } from '@orpc/standard-server'
22
import type { ToNodeHttpBodyOptions } from './body'
33
import type { NodeHttpResponse } from './types'
4-
import { Readable } from 'node:stream'
5-
import { toNodeHttpBody, toResponseBody } from './body'
4+
import { toNodeHttpBody } from './body'
65

76
export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions {}
87

@@ -40,42 +39,3 @@ export function sendStandardResponse(
4039
}
4140
})
4241
}
43-
export function setStandardResponse(
44-
res: NodeHttpResponse,
45-
standardResponse: StandardResponse,
46-
options: SendStandardResponseOptions = {},
47-
): Promise<void | StandardBody | undefined> {
48-
return new Promise((resolve, reject) => {
49-
res.once('error', reject)
50-
res.once('close', resolve)
51-
52-
const resHeaders: StandardHeaders = { ...standardResponse.headers }
53-
54-
const resBody = toResponseBody(standardResponse.body, resHeaders, options)
55-
56-
res.statusCode = standardResponse.status
57-
for (const [key, value] of Object.entries(resHeaders)) {
58-
if (value !== undefined) {
59-
res.setHeader(key, value)
60-
}
61-
}
62-
63-
if (resBody === undefined) {
64-
return resolve(undefined)
65-
}
66-
else if (resBody instanceof Readable) {
67-
res.once('close', () => {
68-
if (!resBody.closed) {
69-
resBody.destroy(res.errored ?? undefined)
70-
}
71-
})
72-
73-
resBody.once('error', error => res.destroy(error))
74-
75-
resBody.pipe(res)
76-
}
77-
else {
78-
return resolve(resBody)
79-
}
80-
})
81-
}

0 commit comments

Comments
 (0)