Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
SoraKumo001 committed Jan 2, 2023
0 parents commit f52716f
Show file tree
Hide file tree
Showing 8 changed files with 3,151 additions and 0 deletions.
67 changes: 67 additions & 0 deletions .eslintrc.json
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"
}
]
}
]
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
node_modules
test
8 changes: 8 additions & 0 deletions .npmignore
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

105 changes: 105 additions & 0 deletions README.md
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,
},
};
```
32 changes: 32 additions & 0 deletions package.json
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"
}
146 changes: 146 additions & 0 deletions src/index.ts
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();
}
};
Loading

0 comments on commit f52716f

Please sign in to comment.