Refactor the GraphQL API.
This commit is contained in:
parent
4d4d58699f
commit
f421deaabd
@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon -i db.json",
|
"dev": "nodemon -i db.json",
|
||||||
"start": "node --experimental-modules -r dotenv/config server",
|
"start": "node -r dotenv/config server",
|
||||||
"test": "eslint . --ext mjs,js && prettier '**/*.{json,yml,md}' -l"
|
"test": "eslint . && prettier '**/*.{json,yml,md}' -l"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
8
api/schema.js
Normal 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
75
api/server.js
Normal 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}.`
|
||||||
|
)
|
||||||
|
})
|
||||||
@ -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}.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@ -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
29
api/types/File.js
Normal 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 it’s 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
43
api/types/Mutation.js
Normal 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
13
api/types/Query.js
Normal 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user