solid-start-trpc
Version:
```ts import { createSolidAPIHandler } from "solid-start-trpc";
459 lines (448 loc) • 13.5 kB
JavaScript
function getMessageFromUnknownError(err, fallback) {
if (typeof err === 'string') {
return err;
}
if (err instanceof Error && typeof err.message === 'string') {
return err.message;
}
return fallback;
}
function getErrorFromUnknown(cause) {
if (cause instanceof Error) {
return cause;
}
const message = getMessageFromUnknownError(cause, 'Unknown error');
return new Error(message);
}
function getTRPCErrorFromUnknown(cause) {
const error = getErrorFromUnknown(cause);
// this should ideally be an `instanceof TRPCError` but for some reason that isn't working
// ref https://github.com/trpc/trpc/issues/331
if (error.name === 'TRPCError') {
return cause;
}
const trpcError = new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
cause: error,
message: error.message
});
// Inherit stack from error
trpcError.stack = error.stack;
return trpcError;
}
function getCauseFromUnknown(cause) {
if (cause instanceof Error) {
return cause;
}
return undefined;
}
class TRPCError extends Error {
constructor(opts){
const code = opts.code;
const message = opts.message ?? getMessageFromUnknownError(opts.cause, code);
const cause = opts !== undefined ? getErrorFromUnknown(opts.cause) : undefined;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore https://github.com/tc39/proposal-error-cause
super(message, {
cause
});
this.code = code;
this.cause = cause;
this.name = 'TRPCError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
function invert(obj) {
const newObj = Object.create(null);
for(const key in obj){
const v = obj[key];
newObj[v] = key;
}
return newObj;
}
// reference: https://www.jsonrpc.org/specification
/**
* JSON-RPC 2.0 Error codes
*
* `-32000` to `-32099` are reserved for implementation-defined server-errors.
* For tRPC we're copying the last digits of HTTP 4XX errors.
*/ const TRPC_ERROR_CODES_BY_KEY = {
/**
* Invalid JSON was received by the server.
* An error occurred on the server while parsing the JSON text.
*/ PARSE_ERROR: -32700,
/**
* The JSON sent is not a valid Request object.
*/ BAD_REQUEST: -32600,
/**
* Internal JSON-RPC error.
*/ INTERNAL_SERVER_ERROR: -32603,
// Implementation specific errors
UNAUTHORIZED: -32001,
FORBIDDEN: -32003,
NOT_FOUND: -32004,
METHOD_NOT_SUPPORTED: -32005,
TIMEOUT: -32008,
CONFLICT: -32009,
PRECONDITION_FAILED: -32012,
PAYLOAD_TOO_LARGE: -32013,
TOO_MANY_REQUESTS: -32029,
CLIENT_CLOSED_REQUEST: -32099
};
invert(TRPC_ERROR_CODES_BY_KEY);
const TRPC_ERROR_CODES_BY_NUMBER = invert(TRPC_ERROR_CODES_BY_KEY);
const JSONRPC2_TO_HTTP_CODE = {
PARSE_ERROR: 400,
BAD_REQUEST: 400,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
TIMEOUT: 408,
CONFLICT: 409,
CLIENT_CLOSED_REQUEST: 499,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
METHOD_NOT_SUPPORTED: 405,
TOO_MANY_REQUESTS: 429
};
function getStatusCodeFromKey(code) {
return JSONRPC2_TO_HTTP_CODE[code] ?? 500;
}
function getHTTPStatusCode(json) {
const arr = Array.isArray(json) ? json : [
json
];
const httpStatuses = new Set(arr.map((res)=>{
if ('error' in res) {
const data = res.error.data;
if (typeof data.httpStatus === 'number') {
return data.httpStatus;
}
const code = TRPC_ERROR_CODES_BY_NUMBER[res.error.code];
return getStatusCodeFromKey(code);
}
return 200;
}));
if (httpStatuses.size !== 1) {
return 207;
}
const httpStatus = httpStatuses.values().next().value;
return httpStatus;
}
/**
* @internal
*/ function callProcedure(opts) {
const { type , path } = opts;
if (!(path in opts.procedures) || !opts.procedures[path]?._def[type]) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `No "${type}"-procedure on path "${path}"`
});
}
const procedure = opts.procedures[path];
return procedure(opts);
}
/**
* The default check to see if we're in a server
*/ typeof window === 'undefined' || 'Deno' in window || globalThis.process?.env?.NODE_ENV === 'test' || !!globalThis.process?.env?.JEST_WORKER_ID;
function getPath(args) {
const p = args.params.trpc;
if (typeof p === "string") {
return p;
}
if (Array.isArray(p)) {
return p.join("/");
}
return null;
}
function notFoundError(opts) {
const error = opts.router.getErrorShape({
error: new TRPCError({
message: 'Query "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: "INTERNAL_SERVER_ERROR"
}),
type: "unknown",
ctx: undefined,
path: undefined,
input: undefined
});
const json = {
id: -1,
error
};
return new Response(JSON.stringify(json), {
status: 500
});
}
function transformTRPCResponseItem(router, item) {
if ('error' in item) {
return {
...item,
error: router._def._config.transformer.output.serialize(item.error)
};
}
if ('data' in item.result) {
return {
...item,
result: {
...item.result,
data: router._def._config.transformer.output.serialize(item.result.data)
}
};
}
return item;
}
/**
* Takes a unserialized `TRPCResponse` and serializes it with the router's transformers
**/ function transformTRPCResponse(router, itemOrItems) {
return Array.isArray(itemOrItems) ? itemOrItems.map((item)=>transformTRPCResponseItem(router, item)) : transformTRPCResponseItem(router, itemOrItems);
}
const HTTP_METHOD_PROCEDURE_TYPE_MAP = {
GET: 'query',
POST: 'mutation'
};
function getRawProcedureInputOrThrow(req) {
try {
if (req.method === 'GET') {
if (!req.query.has('input')) {
return undefined;
}
const raw = req.query.get('input');
return JSON.parse(raw);
}
if (typeof req.body === 'string') {
// A mutation with no inputs will have req.body === ''
return req.body.length === 0 ? undefined : JSON.parse(req.body);
}
return req.body;
} catch (err) {
throw new TRPCError({
code: 'PARSE_ERROR',
cause: getCauseFromUnknown(err)
});
}
}
async function resolveHTTPResponse(opts) {
const { createContext , onError , router , req } = opts;
const batchingEnabled = opts.batching?.enabled ?? true;
if (req.method === 'HEAD') {
// can be used for lambda warmup
return {
status: 204
};
}
const type = HTTP_METHOD_PROCEDURE_TYPE_MAP[req.method] ?? 'unknown';
let ctx = undefined;
let paths = undefined;
const isBatchCall = !!req.query.get('batch');
function endResponse(untransformedJSON, errors) {
let status = getHTTPStatusCode(untransformedJSON);
const headers = {
'Content-Type': 'application/json'
};
const meta = opts.responseMeta?.({
ctx,
paths,
type,
data: Array.isArray(untransformedJSON) ? untransformedJSON : [
untransformedJSON
],
errors
}) ?? {};
for (const [key, value] of Object.entries(meta.headers ?? {})){
headers[key] = value;
}
if (meta.status) {
status = meta.status;
}
const transformedJSON = transformTRPCResponse(router, untransformedJSON);
const body = JSON.stringify(transformedJSON);
return {
body,
status,
headers
};
}
try {
if (opts.error) {
throw opts.error;
}
if (isBatchCall && !batchingEnabled) {
throw new Error(`Batching is not enabled on the server`);
}
/* istanbul ignore if */ if (type === 'subscription') {
throw new TRPCError({
message: 'Subscriptions should use wsLink',
code: 'METHOD_NOT_SUPPORTED'
});
}
if (type === 'unknown') {
throw new TRPCError({
message: `Unexpected request method ${req.method}`,
code: 'METHOD_NOT_SUPPORTED'
});
}
const rawInput = getRawProcedureInputOrThrow(req);
paths = isBatchCall ? opts.path.split(',') : [
opts.path
];
ctx = await createContext();
const deserializeInputValue = (rawValue)=>{
return typeof rawValue !== 'undefined' ? router._def._config.transformer.input.deserialize(rawValue) : rawValue;
};
const getInputs = ()=>{
if (!isBatchCall) {
return {
0: deserializeInputValue(rawInput)
};
}
/* istanbul ignore if */ if (rawInput == null || typeof rawInput !== 'object' || Array.isArray(rawInput)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '"input" needs to be an object when doing a batch call'
});
}
const input = {};
for(const key in rawInput){
const k = key;
const rawValue = rawInput[k];
const value = deserializeInputValue(rawValue);
input[k] = value;
}
return input;
};
const inputs = getInputs();
const rawResults = await Promise.all(paths.map(async (path, index)=>{
const input = inputs[index];
try {
const output = await callProcedure({
procedures: router._def.procedures,
path,
rawInput: input,
ctx,
type
});
return {
input,
path,
data: output
};
} catch (cause) {
const error = getTRPCErrorFromUnknown(cause);
onError?.({
error,
path,
input,
ctx,
type: type,
req
});
return {
input,
path,
error
};
}
}));
const errors = rawResults.flatMap((obj)=>obj.error ? [
obj.error
] : []);
const resultEnvelopes = rawResults.map((obj)=>{
const { path , input } = obj;
if (obj.error) {
return {
error: router.getErrorShape({
error: obj.error,
type,
path,
input,
ctx
})
};
} else {
return {
result: {
data: obj.data
}
};
}
});
const result = isBatchCall ? resultEnvelopes : resultEnvelopes[0];
return endResponse(result, errors);
} catch (cause) {
// we get here if
// - batching is called when it's not enabled
// - `createContext()` throws
// - post body is too large
// - input deserialization fails
const error = getTRPCErrorFromUnknown(cause);
onError?.({
error,
path: undefined,
input: undefined,
ctx,
type: type,
req
});
return endResponse({
error: router.getErrorShape({
error,
type,
path: undefined,
input: undefined,
ctx
})
}, [
error
]);
}
}
function createSolidAPIHandler(opts) {
return async args => {
const path = getPath(args);
if (path === null) {
return notFoundError(opts);
}
const res = {
headers: {}
};
const url = new URL(args.request.url);
const req = {
query: url.searchParams,
method: args.request.method,
headers: Object.fromEntries(args.request.headers),
body: await args.request.text()
};
const result = await resolveHTTPResponse({
router: opts.router,
responseMeta: opts.responseMeta,
req,
path,
createContext: async () => await opts.createContext?.({
req: args.request,
res
})
});
const mRes = new Response(result.body, {
status: result.status
});
for (const [key, value] of Object.entries(result.headers ? {
...res.headers,
...result.headers
} : res.headers)) {
if (typeof value === "undefined") {
continue;
}
if (typeof value === "string") {
mRes.headers.set(key, value);
continue;
}
for (const v of value) {
mRes.headers.append(key, v);
}
}
return mRes;
};
}
export { createSolidAPIHandler };