Refactor the GraphQL API.

This commit is contained in:
Jayden Seric 2019-10-10 10:54:16 +11:00
parent 4d4d58699f
commit f421deaabd
9 changed files with 170 additions and 116 deletions

View File

@ -34,7 +34,7 @@
},
"scripts": {
"dev": "nodemon -i db.json",
"start": "node --experimental-modules -r dotenv/config server",
"test": "eslint . --ext mjs,js && prettier '**/*.{json,yml,md}' -l"
"start": "node -r dotenv/config server",
"test": "eslint . && prettier '**/*.{json,yml,md}' -l"
}
}

View File

@ -1,69 +0,0 @@
import fs from 'fs'
import apolloServerKoa from 'apollo-server-koa'
import lowdb from 'lowdb'
import FileSync from 'lowdb/adapters/FileSync'
import mkdirp from 'mkdirp'
import promisesAll from 'promises-all'
import shortid from 'shortid'
const UPLOAD_DIR = './uploads'
const db = lowdb(new FileSync('db.json'))
// Seed an empty DB.
db.defaults({ uploads: [] }).write()
// Ensure upload directory exists.
mkdirp.sync(UPLOAD_DIR)
const storeFS = ({ stream, filename }) => {
const id = shortid.generate()
const path = `${UPLOAD_DIR}/${id}-${filename}`
return new Promise((resolve, reject) =>
stream
.on('error', error => {
if (stream.truncated)
// Delete the truncated file.
fs.unlinkSync(path)
reject(error)
})
.pipe(fs.createWriteStream(path))
.on('error', error => reject(error))
.on('finish', () => resolve({ id, path }))
)
}
const storeDB = file =>
db
.get('uploads')
.push(file)
.last()
.write()
const processUpload = async upload => {
const { createReadStream, filename, mimetype } = await upload
const stream = createReadStream()
const { id, path } = await storeFS({ stream, filename })
return storeDB({ id, filename, mimetype, path })
}
export default {
Upload: apolloServerKoa.GraphQLUpload,
Query: {
uploads: () => db.get('uploads').value()
},
Mutation: {
singleUpload: (obj, { file }) => processUpload(file),
async multipleUpload(obj, { files }) {
const { resolve, reject } = await promisesAll.all(
files.map(processUpload)
)
if (reject.length)
reject.forEach(({ name, message }) =>
console.error(`${name}: ${message}`)
)
return resolve
}
}
}

8
api/schema.js Normal file
View File

@ -0,0 +1,8 @@
const { GraphQLSchema } = require('graphql')
const { MutationType } = require('./types/Mutation')
const { QueryType } = require('./types/Query')
exports.schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType
})

75
api/server.js Normal file
View File

@ -0,0 +1,75 @@
const { createWriteStream, unlink } = require('fs')
const { ApolloServer } = require('apollo-server-koa')
const Koa = require('koa')
const lowdb = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
const mkdirp = require('mkdirp')
const shortid = require('shortid')
const { schema } = require('./schema')
const UPLOAD_DIR = './uploads'
const db = lowdb(new FileSync('db.json'))
// Seed an empty DB.
db.defaults({ uploads: [] }).write()
// Ensure upload directory exists.
mkdirp.sync(UPLOAD_DIR)
/**
* Stores a GraphQL file upload. The file is stored in the filesystem and its
* metadata is recorded in the DB.
* @param {GraphQLUpload} upload GraphQL file upload.
* @returns {object} File metadata.
*/
const storeUpload = async upload => {
const { createReadStream, filename, mimetype } = await upload
const stream = createReadStream()
const id = shortid.generate()
const path = `${UPLOAD_DIR}/${id}-${filename}`
const file = { id, filename, mimetype, path }
// Store the file in the filesystem.
await new Promise((resolve, reject) => {
stream
.on('error', error => {
unlink(path, () => {
reject(error)
})
})
.pipe(createWriteStream(path))
.on('error', reject)
.on('finish', resolve)
})
// Record the file metadata in the DB.
db.get('uploads')
.push(file)
.write()
return file
}
const app = new Koa()
const server = new ApolloServer({
uploads: {
// Limits here should be stricter than config for surrounding
// infrastructure such as Nginx so errors can be handled elegantly by
// graphql-upload:
// https://github.com/jaydenseric/graphql-upload#type-processrequestoptions
maxFileSize: 10000000, // 10 MB
maxFiles: 20
},
schema,
context: { db, storeUpload }
})
server.applyMiddleware({ app })
app.listen(process.env.PORT, error => {
if (error) throw error
console.info(
`Serving http://localhost:${process.env.PORT} for ${process.env.NODE_ENV}.`
)
})

View File

@ -1,28 +0,0 @@
import apolloServerKoa from 'apollo-server-koa'
import Koa from 'koa'
import resolvers from './resolvers'
import typeDefs from './types'
const app = new Koa()
const server = new apolloServerKoa.ApolloServer({
typeDefs,
resolvers,
uploads: {
// Limits here should be stricter than config for surrounding
// infrastructure such as Nginx so errors can be handled elegantly by
// graphql-upload:
// https://github.com/jaydenseric/graphql-upload#type-uploadoptions
maxFileSize: 10000000, // 10 MB
maxFiles: 20
}
})
server.applyMiddleware({ app })
app.listen(process.env.PORT, error => {
if (error) throw error
console.info(
`Serving http://localhost:${process.env.PORT} for ${process.env.NODE_ENV}.`
)
})

View File

@ -1,17 +0,0 @@
export default /* GraphQL */ `
type File {
id: ID!
path: String!
filename: String!
mimetype: String!
}
type Query {
uploads: [File]
}
type Mutation {
singleUpload(file: Upload!): File!
multipleUpload(files: [Upload!]!): [File!]!
}
`

29
api/types/File.js Normal file
View File

@ -0,0 +1,29 @@
const {
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
GraphQLID
} = require('graphql')
exports.FileType = new GraphQLObjectType({
name: 'File',
description: 'A stored file.',
fields: () => ({
id: {
description: 'Unique ID.',
type: GraphQLNonNull(GraphQLID)
},
path: {
description: 'Where its stored in the filesystem.',
type: GraphQLNonNull(GraphQLString)
},
filename: {
description: 'Filename, including extension.',
type: GraphQLNonNull(GraphQLString)
},
mimetype: {
description: 'MIME type.',
type: GraphQLNonNull(GraphQLString)
}
})
})

43
api/types/Mutation.js Normal file
View File

@ -0,0 +1,43 @@
const { GraphQLUpload } = require('apollo-server-koa')
const { GraphQLList, GraphQLObjectType, GraphQLNonNull } = require('graphql')
const promisesAll = require('promises-all')
const { FileType } = require('./File')
exports.MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: () => ({
singleUpload: {
description: 'Stores a single file.',
type: GraphQLNonNull(FileType),
args: {
file: {
description: 'File to store.',
type: GraphQLNonNull(GraphQLUpload)
}
},
resolve: (parent, { file }, { storeUpload }) => storeUpload(file)
},
multipleUpload: {
description: 'Stores multiple files.',
type: GraphQLNonNull(GraphQLList(GraphQLNonNull(FileType))),
args: {
files: {
description: 'Files to store.',
type: GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLUpload)))
}
},
async resolve(parent, { files }, { storeUpload }) {
const { resolve, reject } = await promisesAll.all(
files.map(storeUpload)
)
if (reject.length)
reject.forEach(({ name, message }) =>
console.error(`${name}: ${message}`)
)
return resolve
}
}
})
})

13
api/types/Query.js Normal file
View File

@ -0,0 +1,13 @@
const { GraphQLList, GraphQLObjectType, GraphQLNonNull } = require('graphql')
const { FileType } = require('./File')
exports.QueryType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
uploads: {
description: 'All stored files.',
type: GraphQLNonNull(GraphQLList(GraphQLNonNull(FileType))),
resolve: (source, args, { db }) => db.get('uploads').value()
}
})
})