mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #636 from Human-Connection/uploads-on-server
Image upload backend implementation
This commit is contained in:
commit
bfb4e06a35
@ -109,4 +109,4 @@
|
||||
"prettier": "~1.14.3",
|
||||
"supertest": "~4.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
backend/public/uploads/.gitkeep
Normal file
0
backend/public/uploads/.gitkeep
Normal file
@ -12,6 +12,7 @@ import rewards from './resolvers/rewards.js'
|
||||
import socialMedia from './resolvers/socialMedia.js'
|
||||
import notifications from './resolvers/notifications'
|
||||
import comments from './resolvers/comments'
|
||||
import users from './resolvers/users'
|
||||
|
||||
export const typeDefs = fs
|
||||
.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql'))
|
||||
@ -35,5 +36,6 @@ export const resolvers = {
|
||||
...socialMedia.Mutation,
|
||||
...notifications.Mutation,
|
||||
...comments.Mutation,
|
||||
...users.Mutation,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import cheerio from 'cheerio'
|
||||
const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g
|
||||
|
||||
export default function(content) {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
const urls = $('.mention')
|
||||
.map((_, el) => {
|
||||
@ -1,6 +1,12 @@
|
||||
import extractIds from './extractMentions'
|
||||
import extractIds from '.'
|
||||
|
||||
describe('extractIds', () => {
|
||||
describe('content undefined', () => {
|
||||
it('returns empty array', () => {
|
||||
expect(extractIds()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('extract', () => {
|
||||
describe('searches through links', () => {
|
||||
it('ignores links without .mention class', () => {
|
||||
const content =
|
||||
@ -1,4 +1,4 @@
|
||||
import extractIds from './extractMentions'
|
||||
import extractIds from './extractIds'
|
||||
|
||||
const notify = async (resolve, root, args, context, resolveInfo) => {
|
||||
// extract user ids before xss-middleware removes link classes
|
||||
|
||||
@ -3,7 +3,7 @@ import { UserInputError } from 'apollo-server'
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
|
||||
const validateUsername = async (resolve, root, args, context, info) => {
|
||||
if (args.name && args.name.length >= USERNAME_MIN_LENGTH) {
|
||||
if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) {
|
||||
/* eslint-disable-next-line no-return-await */
|
||||
return await resolve(root, args, context, info)
|
||||
} else {
|
||||
|
||||
27
backend/src/resolvers/fileUpload/index.js
Normal file
27
backend/src/resolvers/fileUpload/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { createWriteStream } from 'fs'
|
||||
import path from 'path'
|
||||
import slug from 'slug'
|
||||
|
||||
const storeUpload = ({ createReadStream, fileLocation }) =>
|
||||
new Promise((resolve, reject) =>
|
||||
createReadStream()
|
||||
.pipe(createWriteStream(`public${fileLocation}`))
|
||||
.on('finish', resolve)
|
||||
.on('error', reject),
|
||||
)
|
||||
|
||||
export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) {
|
||||
const upload = params[file]
|
||||
|
||||
if (upload) {
|
||||
const { createReadStream, filename } = await upload
|
||||
const { name } = path.parse(filename)
|
||||
const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
|
||||
await uploadCallback({ createReadStream, fileLocation })
|
||||
delete params[file]
|
||||
|
||||
params[url] = fileLocation
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
65
backend/src/resolvers/fileUpload/spec.js
Normal file
65
backend/src/resolvers/fileUpload/spec.js
Normal file
@ -0,0 +1,65 @@
|
||||
import fileUpload from '.'
|
||||
|
||||
describe('fileUpload', () => {
|
||||
let params
|
||||
let uploadCallback
|
||||
|
||||
beforeEach(() => {
|
||||
params = {
|
||||
uploadAttribute: {
|
||||
filename: 'avatar.jpg',
|
||||
mimetype: 'image/jpeg',
|
||||
encoding: '7bit',
|
||||
createReadStream: jest.fn(),
|
||||
},
|
||||
}
|
||||
uploadCallback = jest.fn()
|
||||
})
|
||||
|
||||
it('calls uploadCallback', async () => {
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(uploadCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('file name', () => {
|
||||
it('saves the upload url in params[url]', async () => {
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/)
|
||||
})
|
||||
|
||||
it('uses the name without file ending', async () => {
|
||||
params.uploadAttribute.filename = 'somePng.png'
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/)
|
||||
})
|
||||
|
||||
it('creates a url safe name', async () => {
|
||||
params.uploadAttribute.filename =
|
||||
'/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/)
|
||||
})
|
||||
|
||||
describe('in case of duplicates', () => {
|
||||
it('creates unique names to avoid overwriting existing files', async () => {
|
||||
const { attribute: first } = await fileUpload(
|
||||
{
|
||||
...params,
|
||||
},
|
||||
{ file: 'uploadAttribute', url: 'attribute' },
|
||||
uploadCallback,
|
||||
)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const { attribute: second } = await fileUpload(
|
||||
{
|
||||
...params,
|
||||
},
|
||||
{ file: 'uploadAttribute', url: 'attribute' },
|
||||
uploadCallback,
|
||||
)
|
||||
expect(first).not.toEqual(second)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,8 +1,15 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import fileUpload from './fileUpload'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
UpdatePost: async (object, params, context, resolveInfo) => {
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
},
|
||||
|
||||
CreatePost: async (object, params, context, resolveInfo) => {
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
const result = await neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
|
||||
const session = context.driver.session()
|
||||
|
||||
15
backend/src/resolvers/users.js
Normal file
15
backend/src/resolvers/users.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import fileUpload from './fileUpload'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
UpdateUser: async (object, params, context, resolveInfo) => {
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
},
|
||||
CreateUser: async (object, params, context, resolveInfo) => {
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -67,6 +67,7 @@ describe('users', () => {
|
||||
it('with no name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: null,
|
||||
}
|
||||
const expected = 'Username must be at least 3 characters long!'
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
scalar Upload
|
||||
|
||||
type Query {
|
||||
isLoggedIn: Boolean!
|
||||
# Get the currently logged in User based on the given JWT Token
|
||||
@ -18,6 +20,7 @@ type Query {
|
||||
)
|
||||
CommentByPost(postId: ID!): [Comment]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# Get a JWT Token for the given Email and password
|
||||
login(email: String!, password: String!): String!
|
||||
@ -99,6 +102,7 @@ type User {
|
||||
slug: String
|
||||
password: String!
|
||||
avatar: String
|
||||
avatarUpload: Upload
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
@ -175,6 +179,7 @@ type Post {
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: VisibilityEnum
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user