|
| 1 | +import { promises as fs } from "fs"; |
| 2 | +import { parse } from "url"; |
| 3 | +import formidable from "formidable"; |
| 4 | +import type { |
| 5 | + ApolloServer, |
| 6 | + BaseContext, |
| 7 | + ContextThunk, |
| 8 | + GraphQLRequest, |
| 9 | + HTTPGraphQLRequest, |
| 10 | +} from "@apollo/server"; |
| 11 | +import type { NextApiRequest, NextApiResponse } from "next"; |
| 12 | + |
| 13 | +/** |
| 14 | + * Request parameter conversion options |
| 15 | + */ |
| 16 | +export type FormidableOptions = formidable.Options; |
| 17 | + |
| 18 | +/** |
| 19 | + * File type used by resolver |
| 20 | + */ |
| 21 | +export type FormidableFile = formidable.File; |
| 22 | + |
| 23 | +/** |
| 24 | + * Converting NextApiRequest to Apollo's Header |
| 25 | + * Identical header names are overwritten by later values |
| 26 | + * @returns Header in Map format |
| 27 | + */ |
| 28 | +export const createHeaders = (req: NextApiRequest) => |
| 29 | + new Map( |
| 30 | + Object.entries(req.headers).flatMap<[string, string]>(([key, value]) => |
| 31 | + Array.isArray(value) |
| 32 | + ? value.flatMap<[string, string]>((v) => (v ? [[key, v]] : [])) |
| 33 | + : value |
| 34 | + ? [[key, value]] |
| 35 | + : [] |
| 36 | + ) |
| 37 | + ); |
| 38 | + |
| 39 | +/** |
| 40 | + * Retrieve search from NextApiRequest |
| 41 | + * @returns search |
| 42 | + */ |
| 43 | +export const createSearch = (req: NextApiRequest) => |
| 44 | + parse(req.url ?? "").search ?? ""; |
| 45 | + |
| 46 | +/** |
| 47 | + * Make GraphQL requests multipart/form-data compliant |
| 48 | + * @returns [body to be set in executeHTTPGraphQLRequest, function for temporary file deletion] |
| 49 | + */ |
| 50 | +export const createBody = ( |
| 51 | + req: NextApiRequest, |
| 52 | + options?: formidable.Options |
| 53 | +) => { |
| 54 | + const form = formidable(options); |
| 55 | + return new Promise<[GraphQLRequest, () => void]>((resolve, reject) => { |
| 56 | + form.parse(req, async (error, fields, files) => { |
| 57 | + if (error) { |
| 58 | + reject(error); |
| 59 | + } else if (!req.headers["content-type"]?.match(/^multipart\/form-data/)) { |
| 60 | + resolve([fields, () => {}]); |
| 61 | + } else { |
| 62 | + if ( |
| 63 | + "operations" in fields && |
| 64 | + "map" in fields && |
| 65 | + typeof fields.operations === "string" && |
| 66 | + typeof fields.map === "string" |
| 67 | + ) { |
| 68 | + const request = JSON.parse(fields.operations); |
| 69 | + const map: { [key: string]: [string] } = JSON.parse(fields.map); |
| 70 | + Object.entries(map).forEach(([key, [value]]) => { |
| 71 | + value.split(".").reduce((a, b, index, array) => { |
| 72 | + if (array.length - 1 === index) a[b] = files[key]; |
| 73 | + else return a[b]; |
| 74 | + }, request); |
| 75 | + }); |
| 76 | + const removeFiles = () => { |
| 77 | + Object.values(files).forEach((file) => { |
| 78 | + if (Array.isArray(file)) { |
| 79 | + file.forEach(({ filepath }) => { |
| 80 | + fs.rm(filepath); |
| 81 | + }); |
| 82 | + } else { |
| 83 | + fs.rm(file.filepath); |
| 84 | + } |
| 85 | + }); |
| 86 | + }; |
| 87 | + resolve([request, removeFiles]); |
| 88 | + } else { |
| 89 | + reject(Error("multipart type error")); |
| 90 | + } |
| 91 | + } |
| 92 | + }); |
| 93 | + }); |
| 94 | +}; |
| 95 | + |
| 96 | +/** |
| 97 | + * Creating methods |
| 98 | + * @returns method string |
| 99 | + */ |
| 100 | +export const createMethod = (req: NextApiRequest) => req.method ?? ""; |
| 101 | + |
| 102 | +/** |
| 103 | + * Execute a GraphQL request |
| 104 | + */ |
| 105 | +export const executeHTTPGraphQLRequest = async <Context extends BaseContext>({ |
| 106 | + req, |
| 107 | + res, |
| 108 | + apolloServer, |
| 109 | + options, |
| 110 | + context, |
| 111 | +}: { |
| 112 | + req: NextApiRequest; |
| 113 | + res: NextApiResponse; |
| 114 | + apolloServer: ApolloServer<Context>; |
| 115 | + context: ContextThunk<Context>; |
| 116 | + options?: FormidableOptions; |
| 117 | +}) => { |
| 118 | + const [body, removeFiles] = await createBody(req, options); |
| 119 | + try { |
| 120 | + const httpGraphQLRequest: HTTPGraphQLRequest = { |
| 121 | + method: createMethod(req), |
| 122 | + headers: createHeaders(req), |
| 123 | + search: createSearch(req), |
| 124 | + body, |
| 125 | + }; |
| 126 | + const result = await apolloServer.executeHTTPGraphQLRequest({ |
| 127 | + httpGraphQLRequest, |
| 128 | + context, |
| 129 | + }); |
| 130 | + result.status && res.status(result.status); |
| 131 | + result.headers.forEach((value, key) => { |
| 132 | + res.setHeader(key, value); |
| 133 | + }); |
| 134 | + if (result.body.kind === "complete") { |
| 135 | + res.end(result.body.string); |
| 136 | + } else { |
| 137 | + for await (const chunk of result.body.asyncIterator) { |
| 138 | + res.write(chunk); |
| 139 | + } |
| 140 | + res.end(); |
| 141 | + } |
| 142 | + return result; |
| 143 | + } finally { |
| 144 | + removeFiles(); |
| 145 | + } |
| 146 | +}; |
0 commit comments