Skip to content

Commit

Permalink
chore: move complexity from resolveResponse (#5691)
Browse files Browse the repository at this point in the history
* shift complexity

* cool

* cool

* cool

* prob

* cool

* cool

* tweak

* mkay

* tweak

* tweak

* cool

* cool

* cool

* cool

* cool

* cool

* cool

* shift more

* cool

* cool

* cool

* cool

* request info

* wip

* cool

* cool

* Update packages/server/src/unstable-core-do-not-import/http/types.ts

* fix

* tweak
  • Loading branch information
KATT committed Apr 30, 2024
1 parent 699d7d0 commit 3d33679
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 187 deletions.
213 changes: 164 additions & 49 deletions packages/server/src/unstable-core-do-not-import/http/contentType.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,197 @@
import { TRPCError } from '../error/TRPCError';
import type { RootConfig } from '../rootConfig';
import { isObject, unsetMarker } from '../utils';
import type { TRPCRequestInfo } from './types';

type ContentTypeHandler = {
isMatch: (opts: Request) => boolean;
getInputs: (req: Request, searchParams: URLSearchParams) => Promise<unknown>;
batching: boolean;
transform: boolean;
parse: (opts: {
path: string;
req: Request;
searchParams: URLSearchParams;
config: RootConfig<any>;
}) => TRPCRequestInfo;
};

const jsonContentTypeHandler: ContentTypeHandler = {
async getInputs(req, searchParams) {
if (req.method === 'GET') {
const input = searchParams.get('input');
if (input === null) {
return undefined;
/**
* Memoize a function that takes no arguments
* @internal
*/
function memo<TReturn>(fn: () => Promise<TReturn>) {
let promise: Promise<TReturn> | null = null;
let value: TReturn | typeof unsetMarker = unsetMarker;
return {
/**
* Lazily read the value
*/
read: async (): Promise<TReturn> => {
if (value !== unsetMarker) {
return value;
}
return JSON.parse(input);
}
return await req.json();
},
if (promise === null) {
// dedupes promises and catches errors
promise = fn().catch((cause) => {
throw new TRPCError({
code: 'BAD_REQUEST',
message: cause instanceof Error ? cause.message : 'Invalid input',
cause,
});
});
}

value = await promise;
promise = null;

return value;
},
/**
* Get an already stored result
*/
result: (): TReturn | undefined => {
return value !== unsetMarker ? value : undefined;
},
};
}

const jsonContentTypeHandler: ContentTypeHandler = {
isMatch(req) {
return !!req.headers.get('content-type')?.startsWith('application/json');
},
batching: true,
transform: true,
parse(opts) {
const { req } = opts;
const isBatchCall = opts.searchParams.get('batch') === '1';
const paths = isBatchCall ? opts.path.split(',') : [opts.path];

type InputRecord = Record<number, unknown>;
const getInputs = memo(async (): Promise<InputRecord> => {
let inputs: unknown = undefined;
if (req.method === 'GET') {
const queryInput = opts.searchParams.get('input');
if (queryInput) {
inputs = JSON.parse(queryInput);
}
} else {
inputs = await req.json();
}
if (inputs === undefined) {
return {};
}

if (!isBatchCall) {
return {
0: opts.config.transformer.input.deserialize(inputs),
};
}

if (!isObject(inputs)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '"input" needs to be an object when doing a batch call',
});
}
const acc: InputRecord = {};
for (const index of paths.keys()) {
const input = inputs[index];
if (input !== undefined) {
acc[index] = opts.config.transformer.input.deserialize(input);
}
}

return acc;
});

return {
isBatchCall,
calls: paths.map((path, index) => ({
path,
getRawInput: async () => {
const inputs = await getInputs.read();
return inputs[index];
},
result: () => {
return getInputs.result()?.[index];
},
})),
};
},
};

const formDataContentTypeHandler: ContentTypeHandler = {
async getInputs(req) {
isMatch(req) {
return !!req.headers.get('content-type')?.startsWith('multipart/form-data');
},
parse(opts) {
const { req } = opts;
if (req.method !== 'POST') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message:
'Only POST requests are supported for multipart/form-data requests',
});
}
const fd = await req.formData();

return fd;
},
isMatch(req) {
return !!req.headers.get('content-type')?.startsWith('multipart/form-data');
const getInputs = memo(async () => {
const fd = await req.formData();
return fd;
});
return {
calls: [
{
path: opts.path,
getRawInput: getInputs.read,
result: getInputs.result,
},
],
isBatchCall: false,
};
},
batching: false,
transform: false,
};

const octetStreamContentTypeHandler: ContentTypeHandler = {
async getInputs(req) {
isMatch(req) {
return !!req.headers
.get('content-type')
?.startsWith('application/octet-stream');
},
parse(opts) {
const { req } = opts;
if (req.method !== 'POST') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message:
'Only POST requests are supported for application/octet-stream requests',
});
}
return req.body;
},
isMatch(req) {
return !!req.headers
.get('content-type')
?.startsWith('application/octet-stream');
const getInputs = memo(async () => {
return req.body;
});
return {
calls: [
{
path: opts.path,
getRawInput: getInputs.read,
result: getInputs.result,
},
],
isBatchCall: false,
};
},
batching: false,
transform: false,
};

const contentTypeHandlers = {
list: [
formDataContentTypeHandler,
jsonContentTypeHandler,
octetStreamContentTypeHandler,
],
/**
* Fallback handler if there is no match
*/
fallback: jsonContentTypeHandler,
};
const handlers = [
jsonContentTypeHandler,
formDataContentTypeHandler,
octetStreamContentTypeHandler,
];

export function getContentTypeHandlerOrThrow(req: Request): ContentTypeHandler {
const handler = contentTypeHandlers.list.find((handler) =>
handler.isMatch(req),
);
function getContentTypeHandler(req: Request): ContentTypeHandler {
const handler = handlers.find((handler) => handler.isMatch(req));
if (handler) {
return handler;
}

if (!handler && req.method === 'GET') {
return contentTypeHandlers.fallback;
// fallback to JSON for get requests so GET-requests can be opened in browser easily
return jsonContentTypeHandler;
}

throw new TRPCError({
Expand All @@ -96,3 +201,13 @@ export function getContentTypeHandlerOrThrow(req: Request): ContentTypeHandler {
: 'Missing content-type header',
});
}

export function getRequestInfo(opts: {
path: string;
req: Request;
searchParams: URLSearchParams;
config: RootConfig<any>;
}): TRPCRequestInfo {
const handler = getContentTypeHandler(opts.req);
return handler.parse(opts);
}

0 comments on commit 3d33679

Please sign in to comment.