Skip to content

Commit f52716f

Browse files
committed
first commit
0 parents  commit f52716f

File tree

8 files changed

+3151
-0
lines changed

8 files changed

+3151
-0
lines changed

.eslintrc.json

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"env": {
3+
"node": true,
4+
"es6": true,
5+
"browser": true
6+
},
7+
"extends": [
8+
"eslint:recommended",
9+
"plugin:prettier/recommended",
10+
"plugin:@typescript-eslint/recommended"
11+
],
12+
"parser": "@typescript-eslint/parser",
13+
"parserOptions": {
14+
"ecmaVersion": 6,
15+
"sourceType": "module",
16+
"ecmaFeatures": {
17+
"modules": true
18+
}
19+
},
20+
"plugins": [
21+
"@typescript-eslint",
22+
"import"
23+
],
24+
"rules": {
25+
"prettier/prettier": "error",
26+
"no-empty": 0,
27+
"@typescript-eslint/explicit-module-boundary-types": 0,
28+
"@typescript-eslint/no-non-null-assertion": 0,
29+
"@typescript-eslint/no-var-requires": 0,
30+
"import/order": [
31+
"error",
32+
{
33+
"groups": [
34+
"builtin",
35+
"external",
36+
"internal",
37+
[
38+
"parent",
39+
"sibling"
40+
],
41+
"object",
42+
"type",
43+
"index"
44+
],
45+
"pathGroupsExcludedImportTypes": [
46+
"builtin"
47+
],
48+
"alphabetize": {
49+
"order": "asc",
50+
"caseInsensitive": true
51+
},
52+
"pathGroups": [
53+
{
54+
"pattern": "@/components/common",
55+
"group": "internal",
56+
"position": "before"
57+
},
58+
{
59+
"pattern": "@/components/hooks",
60+
"group": "internal",
61+
"position": "before"
62+
}
63+
]
64+
}
65+
]
66+
}
67+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
node_modules
3+
test

.npmignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.github
2+
/node_modules
3+
/src
4+
.eslintrc.json
5+
.gitignore
6+
tsconfig.json
7+
yarn.lock
8+

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# @react-libraries/next-apollo-server
2+
3+
Package for calling ApolloServer4 from Next.js.
4+
'scalar Upload' is available for multipart data.
5+
6+
## Sample
7+
8+
- Source
9+
<https://github.com/ReactLibraries/next-apollo-server>
10+
- App
11+
- <https://github.com/SoraKumo001/next-urql>
12+
- <https://github.com/SoraKumo001/next-apollo-server>
13+
14+
### src/pages/api/graphql.ts
15+
16+
```ts
17+
import { promises as fs } from "fs";
18+
import { ApolloServer } from "@apollo/server";
19+
import { IResolvers } from "@graphql-tools/utils";
20+
import {
21+
executeHTTPGraphQLRequest,
22+
FormidableFile,
23+
} from "@react-libraries/next-apollo-server";
24+
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
25+
26+
/**
27+
* Type settings for GraphQL
28+
*/
29+
const typeDefs = `
30+
# Return date
31+
scalar Date
32+
type Query {
33+
date: Date!
34+
}
35+
36+
# Return file information
37+
type File {
38+
name: String!
39+
type: String!
40+
value: String!
41+
}
42+
scalar Upload
43+
type Mutation {
44+
upload(file: Upload!): File!
45+
}
46+
`;
47+
48+
/**
49+
* Set Context type
50+
*/
51+
type Context = { req: NextApiRequest; res: NextApiResponse };
52+
53+
/**
54+
* Resolver for GraphQL
55+
*/
56+
const resolvers: IResolvers<Context> = {
57+
Query: {
58+
date: async (_context, _args) => new Date(),
59+
},
60+
Mutation: {
61+
upload: async (_context, { file }: { file: FormidableFile }) => {
62+
return {
63+
name: file.originalFilename,
64+
type: file.mimetype,
65+
value: await fs.readFile(file.filepath, { encoding: "utf8" }),
66+
};
67+
},
68+
},
69+
};
70+
71+
/**
72+
* apolloServer
73+
*/
74+
const apolloServer = new ApolloServer<Context>({
75+
typeDefs,
76+
resolvers,
77+
plugins: [],
78+
});
79+
apolloServer.start();
80+
81+
/**
82+
* APIRoute handler for Next.js
83+
*/
84+
const handler: NextApiHandler = async (req, res) => {
85+
// Convert NextApiRequest to body format for GraphQL (multipart/form-data support).
86+
return executeHTTPGraphQLRequest({
87+
req,
88+
res,
89+
apolloServer,
90+
context: async () => ({ req, res }),
91+
options: {
92+
// Maximum upload file size set at 10 MB
93+
maxFileSize: 10 * 1024 * 1024,
94+
},
95+
});
96+
};
97+
98+
export default handler;
99+
100+
export const config = {
101+
api: {
102+
bodyParser: false,
103+
},
104+
};
105+
```

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@react-libraries/next-apollo-server",
3+
"version": "0.0.1",
4+
"main": "dist/index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"build": "tsc -b",
8+
"lint:fix": "eslint --fix && prettier -w src"
9+
},
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"dependencies": {
14+
"@types/formidable": "^2.0.5",
15+
"formidable": "^2.1.1"
16+
},
17+
"devDependencies": {
18+
"@apollo/server": "^4.3.0",
19+
"@types/node": "^18.11.18",
20+
"@typescript-eslint/eslint-plugin": "^5.47.1",
21+
"eslint": "^8.31.0",
22+
"eslint-config-next": "^13.1.1",
23+
"eslint-config-prettier": "^8.5.0",
24+
"eslint-plugin-import": "^2.26.0",
25+
"graphql": "^16.6.0",
26+
"next": "^13.1.1",
27+
"prettier": "^2.8.1",
28+
"typescript": "^4.9.4"
29+
},
30+
"author": "SoraKumo <[email protected]>",
31+
"repository": "https://github.com/ReactLibraries/next-apollo-server"
32+
}

src/index.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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

Comments
 (0)