-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f52716f
Showing
8 changed files
with
3,151 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
{ | ||
"env": { | ||
"node": true, | ||
"es6": true, | ||
"browser": true | ||
}, | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:prettier/recommended", | ||
"plugin:@typescript-eslint/recommended" | ||
], | ||
"parser": "@typescript-eslint/parser", | ||
"parserOptions": { | ||
"ecmaVersion": 6, | ||
"sourceType": "module", | ||
"ecmaFeatures": { | ||
"modules": true | ||
} | ||
}, | ||
"plugins": [ | ||
"@typescript-eslint", | ||
"import" | ||
], | ||
"rules": { | ||
"prettier/prettier": "error", | ||
"no-empty": 0, | ||
"@typescript-eslint/explicit-module-boundary-types": 0, | ||
"@typescript-eslint/no-non-null-assertion": 0, | ||
"@typescript-eslint/no-var-requires": 0, | ||
"import/order": [ | ||
"error", | ||
{ | ||
"groups": [ | ||
"builtin", | ||
"external", | ||
"internal", | ||
[ | ||
"parent", | ||
"sibling" | ||
], | ||
"object", | ||
"type", | ||
"index" | ||
], | ||
"pathGroupsExcludedImportTypes": [ | ||
"builtin" | ||
], | ||
"alphabetize": { | ||
"order": "asc", | ||
"caseInsensitive": true | ||
}, | ||
"pathGroups": [ | ||
{ | ||
"pattern": "@/components/common", | ||
"group": "internal", | ||
"position": "before" | ||
}, | ||
{ | ||
"pattern": "@/components/hooks", | ||
"group": "internal", | ||
"position": "before" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
dist | ||
node_modules | ||
test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.github | ||
/node_modules | ||
/src | ||
.eslintrc.json | ||
.gitignore | ||
tsconfig.json | ||
yarn.lock | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# @react-libraries/next-apollo-server | ||
|
||
Package for calling ApolloServer4 from Next.js. | ||
'scalar Upload' is available for multipart data. | ||
|
||
## Sample | ||
|
||
- Source | ||
<https://github.com/ReactLibraries/next-apollo-server> | ||
- App | ||
- <https://github.com/SoraKumo001/next-urql> | ||
- <https://github.com/SoraKumo001/next-apollo-server> | ||
|
||
### src/pages/api/graphql.ts | ||
|
||
```ts | ||
import { promises as fs } from "fs"; | ||
import { ApolloServer } from "@apollo/server"; | ||
import { IResolvers } from "@graphql-tools/utils"; | ||
import { | ||
executeHTTPGraphQLRequest, | ||
FormidableFile, | ||
} from "@react-libraries/next-apollo-server"; | ||
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; | ||
|
||
/** | ||
* Type settings for GraphQL | ||
*/ | ||
const typeDefs = ` | ||
# Return date | ||
scalar Date | ||
type Query { | ||
date: Date! | ||
} | ||
# Return file information | ||
type File { | ||
name: String! | ||
type: String! | ||
value: String! | ||
} | ||
scalar Upload | ||
type Mutation { | ||
upload(file: Upload!): File! | ||
} | ||
`; | ||
|
||
/** | ||
* Set Context type | ||
*/ | ||
type Context = { req: NextApiRequest; res: NextApiResponse }; | ||
|
||
/** | ||
* Resolver for GraphQL | ||
*/ | ||
const resolvers: IResolvers<Context> = { | ||
Query: { | ||
date: async (_context, _args) => new Date(), | ||
}, | ||
Mutation: { | ||
upload: async (_context, { file }: { file: FormidableFile }) => { | ||
return { | ||
name: file.originalFilename, | ||
type: file.mimetype, | ||
value: await fs.readFile(file.filepath, { encoding: "utf8" }), | ||
}; | ||
}, | ||
}, | ||
}; | ||
|
||
/** | ||
* apolloServer | ||
*/ | ||
const apolloServer = new ApolloServer<Context>({ | ||
typeDefs, | ||
resolvers, | ||
plugins: [], | ||
}); | ||
apolloServer.start(); | ||
|
||
/** | ||
* APIRoute handler for Next.js | ||
*/ | ||
const handler: NextApiHandler = async (req, res) => { | ||
// Convert NextApiRequest to body format for GraphQL (multipart/form-data support). | ||
return executeHTTPGraphQLRequest({ | ||
req, | ||
res, | ||
apolloServer, | ||
context: async () => ({ req, res }), | ||
options: { | ||
// Maximum upload file size set at 10 MB | ||
maxFileSize: 10 * 1024 * 1024, | ||
}, | ||
}); | ||
}; | ||
|
||
export default handler; | ||
|
||
export const config = { | ||
api: { | ||
bodyParser: false, | ||
}, | ||
}; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@react-libraries/next-apollo-server", | ||
"version": "0.0.1", | ||
"main": "dist/index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"build": "tsc -b", | ||
"lint:fix": "eslint --fix && prettier -w src" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"dependencies": { | ||
"@types/formidable": "^2.0.5", | ||
"formidable": "^2.1.1" | ||
}, | ||
"devDependencies": { | ||
"@apollo/server": "^4.3.0", | ||
"@types/node": "^18.11.18", | ||
"@typescript-eslint/eslint-plugin": "^5.47.1", | ||
"eslint": "^8.31.0", | ||
"eslint-config-next": "^13.1.1", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-plugin-import": "^2.26.0", | ||
"graphql": "^16.6.0", | ||
"next": "^13.1.1", | ||
"prettier": "^2.8.1", | ||
"typescript": "^4.9.4" | ||
}, | ||
"author": "SoraKumo <[email protected]>", | ||
"repository": "https://github.com/ReactLibraries/next-apollo-server" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { promises as fs } from "fs"; | ||
import { parse } from "url"; | ||
import formidable from "formidable"; | ||
import type { | ||
ApolloServer, | ||
BaseContext, | ||
ContextThunk, | ||
GraphQLRequest, | ||
HTTPGraphQLRequest, | ||
} from "@apollo/server"; | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
|
||
/** | ||
* Request parameter conversion options | ||
*/ | ||
export type FormidableOptions = formidable.Options; | ||
|
||
/** | ||
* File type used by resolver | ||
*/ | ||
export type FormidableFile = formidable.File; | ||
|
||
/** | ||
* Converting NextApiRequest to Apollo's Header | ||
* Identical header names are overwritten by later values | ||
* @returns Header in Map format | ||
*/ | ||
export const createHeaders = (req: NextApiRequest) => | ||
new Map( | ||
Object.entries(req.headers).flatMap<[string, string]>(([key, value]) => | ||
Array.isArray(value) | ||
? value.flatMap<[string, string]>((v) => (v ? [[key, v]] : [])) | ||
: value | ||
? [[key, value]] | ||
: [] | ||
) | ||
); | ||
|
||
/** | ||
* Retrieve search from NextApiRequest | ||
* @returns search | ||
*/ | ||
export const createSearch = (req: NextApiRequest) => | ||
parse(req.url ?? "").search ?? ""; | ||
|
||
/** | ||
* Make GraphQL requests multipart/form-data compliant | ||
* @returns [body to be set in executeHTTPGraphQLRequest, function for temporary file deletion] | ||
*/ | ||
export const createBody = ( | ||
req: NextApiRequest, | ||
options?: formidable.Options | ||
) => { | ||
const form = formidable(options); | ||
return new Promise<[GraphQLRequest, () => void]>((resolve, reject) => { | ||
form.parse(req, async (error, fields, files) => { | ||
if (error) { | ||
reject(error); | ||
} else if (!req.headers["content-type"]?.match(/^multipart\/form-data/)) { | ||
resolve([fields, () => {}]); | ||
} else { | ||
if ( | ||
"operations" in fields && | ||
"map" in fields && | ||
typeof fields.operations === "string" && | ||
typeof fields.map === "string" | ||
) { | ||
const request = JSON.parse(fields.operations); | ||
const map: { [key: string]: [string] } = JSON.parse(fields.map); | ||
Object.entries(map).forEach(([key, [value]]) => { | ||
value.split(".").reduce((a, b, index, array) => { | ||
if (array.length - 1 === index) a[b] = files[key]; | ||
else return a[b]; | ||
}, request); | ||
}); | ||
const removeFiles = () => { | ||
Object.values(files).forEach((file) => { | ||
if (Array.isArray(file)) { | ||
file.forEach(({ filepath }) => { | ||
fs.rm(filepath); | ||
}); | ||
} else { | ||
fs.rm(file.filepath); | ||
} | ||
}); | ||
}; | ||
resolve([request, removeFiles]); | ||
} else { | ||
reject(Error("multipart type error")); | ||
} | ||
} | ||
}); | ||
}); | ||
}; | ||
|
||
/** | ||
* Creating methods | ||
* @returns method string | ||
*/ | ||
export const createMethod = (req: NextApiRequest) => req.method ?? ""; | ||
|
||
/** | ||
* Execute a GraphQL request | ||
*/ | ||
export const executeHTTPGraphQLRequest = async <Context extends BaseContext>({ | ||
req, | ||
res, | ||
apolloServer, | ||
options, | ||
context, | ||
}: { | ||
req: NextApiRequest; | ||
res: NextApiResponse; | ||
apolloServer: ApolloServer<Context>; | ||
context: ContextThunk<Context>; | ||
options?: FormidableOptions; | ||
}) => { | ||
const [body, removeFiles] = await createBody(req, options); | ||
try { | ||
const httpGraphQLRequest: HTTPGraphQLRequest = { | ||
method: createMethod(req), | ||
headers: createHeaders(req), | ||
search: createSearch(req), | ||
body, | ||
}; | ||
const result = await apolloServer.executeHTTPGraphQLRequest({ | ||
httpGraphQLRequest, | ||
context, | ||
}); | ||
result.status && res.status(result.status); | ||
result.headers.forEach((value, key) => { | ||
res.setHeader(key, value); | ||
}); | ||
if (result.body.kind === "complete") { | ||
res.end(result.body.string); | ||
} else { | ||
for await (const chunk of result.body.asyncIterator) { | ||
res.write(chunk); | ||
} | ||
res.end(); | ||
} | ||
return result; | ||
} finally { | ||
removeFiles(); | ||
} | ||
}; |
Oops, something went wrong.