From 2899eb6762e4e6db0492071cb2e8f6303be4106f Mon Sep 17 00:00:00 2001 From: Trivernis Date: Wed, 22 Jan 2020 19:59:59 +0100 Subject: [PATCH] [WIP] Add media to posts - Add graphql upload handling - Add file handling for posts - Add media url to Post model --- config/default.toml | 1 - package.json | 3 +- src/app.ts | 49 +------ src/index.ts | 2 +- src/lib/UploadManager.ts | 136 ++++++++++++++++++ src/lib/errors/InvalidFileError.ts | 14 ++ src/lib/errors/UploadFailedError.ts | 10 ++ src/lib/globals.ts | 13 ++ src/lib/models/Post.ts | 6 + src/lib/regex.ts | 18 +++ src/routes/GraphqlRoute.ts | 81 +++++++++++ src/routes/UploadRoute.ts | 87 ++--------- src/{ => routes}/graphql/BaseResolver.ts | 2 +- src/{ => routes}/graphql/BlacklistedResult.ts | 0 src/{ => routes}/graphql/MutationResolver.ts | 55 +++++-- src/{ => routes}/graphql/QueryResolver.ts | 14 +- src/{ => routes}/graphql/SearchResult.ts | 2 +- src/{ => routes}/graphql/Token.ts | 0 src/{ => routes}/graphql/resolvers.ts | 0 src/{ => routes}/graphql/schema.graphql | 18 ++- yarn.lock | 24 +++- 21 files changed, 379 insertions(+), 156 deletions(-) create mode 100644 src/lib/UploadManager.ts create mode 100644 src/lib/errors/InvalidFileError.ts create mode 100644 src/lib/errors/UploadFailedError.ts create mode 100644 src/routes/GraphqlRoute.ts rename src/{ => routes}/graphql/BaseResolver.ts (84%) rename src/{ => routes}/graphql/BlacklistedResult.ts (100%) rename src/{ => routes}/graphql/MutationResolver.ts (88%) rename src/{ => routes}/graphql/QueryResolver.ts (92%) rename src/{ => routes}/graphql/SearchResult.ts (81%) rename src/{ => routes}/graphql/Token.ts (100%) rename src/{ => routes}/graphql/resolvers.ts (100%) rename src/{ => routes}/graphql/schema.graphql (93%) diff --git a/config/default.toml b/config/default.toml index 62bec3b..14e8a6a 100644 --- a/config/default.toml +++ b/config/default.toml @@ -51,7 +51,6 @@ angularIndex = "index.html" # The path to the public files publicPath = "./public" - # Configuration for the api [api] diff --git a/package.json b/package.json index 5aa3d63..d4be8d4 100644 --- a/package.json +++ b/package.json @@ -93,11 +93,12 @@ "reflect-metadata": "^0.1.13", "sequelize": "^5.19.6", "sequelize-typescript": "^1.0.0", - "sharp": "^0.23.4", + "sharp": "^0.24.0", "socket.io": "^2.2.0", "socket.io-redis": "^5.2.0", "sqlite3": "^4.1.0", "stream-buffers": "^3.0.2", + "stream-to-array": "^2.3.0", "toml": "^3.0.0", "uuid": "^3.3.3", "winston": "^3.2.1", diff --git a/src/app.ts b/src/app.ts index 2e9c2e0..3e1a6e5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,14 +4,9 @@ import * as cookieParser from "cookie-parser"; import * as cors from "cors"; import * as express from "express"; import {Request, Response} from "express"; -import * as graphqlHTTP from "express-graphql"; import * as session from "express-session"; import sharedsession = require("express-socket.io-session"); import * as fsx from "fs-extra"; -import {buildSchema, GraphQLError} from "graphql"; -import {importSchema} from "graphql-import"; -import queryComplexity, {directiveEstimator, simpleEstimator} from "graphql-query-complexity"; -import {graphqlUploadExpress} from "graphql-upload"; import * as http from "http"; import {IncomingMessage} from "http"; import * as httpStatus from "http-status"; @@ -21,9 +16,9 @@ import {RedisClient} from "redis"; import {Sequelize} from "sequelize-typescript"; import * as socketIo from "socket.io"; import * as socketIoRedis from "socket.io-redis"; -import {resolver} from "./graphql/resolvers"; import dataaccess from "./lib/dataAccess"; import globals from "./lib/globals"; +import {GraphqlRoute} from "./routes/GraphqlRoute"; import HomeRoute from "./routes/HomeRoute"; import {UploadRoute} from "./routes/UploadRoute"; @@ -161,8 +156,10 @@ class App { const uploadRoute = new UploadRoute(this.publicPath); const homeRoute = new HomeRoute(); + const graphqlRoute = new GraphqlRoute(); await uploadRoute.init(); await homeRoute.init(this.io); + await graphqlRoute.init(); this.app.use("/home", homeRoute.router); this.limiter({ @@ -191,46 +188,8 @@ class App { path: "/graphql", total: config.get("api.rateLimit.graphql.total"), }); + this.app.use("/graphql", graphqlRoute.router); - this.app.use("/graphql", graphqlUploadExpress({ - maxFileSize: config.get("api.maxFileSize"), - maxFiles: 10, - })); - // @ts-ignore - this.app.use("/graphql", graphqlHTTP(async (request: any, response: any, {variables}) => { - response.setHeader("X-Max-Query-Complexity", config.get("api.maxQueryComplexity")); - return { - // @ts-ignore all - context: {session: request.session}, - formatError: (err: GraphQLError | any) => { - if (err.statusCode) { - response.status(err.statusCode); - } else { - response.status(400); - } - logger.debug(err.message); - logger.silly(err.stack); - return err.graphqlError ?? err; - }, - graphiql: config.get("api.graphiql"), - rootValue: resolver(request, response), - schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), - validationRules: [ - queryComplexity({ - estimators: [ - directiveEstimator(), - simpleEstimator(), - ], - maximumComplexity: config.get("api.maxQueryComplexity"), - onComplete: (complexity: number) => { - logger.debug(`QueryComplexity: ${complexity}`); - response.setHeader("X-Query-Complexity", complexity); - }, - variables, - }), - ], - }; - })); // allow access to cluster information this.app.use("/cluster-info", (req: Request, res: Response) => { res.json({ diff --git a/src/index.ts b/src/index.ts index 928d26a..ddd928e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ if (cluster.isMaster) { cluster.on("exit", (worker, code) => { console.error(`[CLUSTER-M] Worker ${worker.id} died! (code: ${code})`); console.log("[CLUSTER-M] Starting new worker"); - cluster.fork(); + setTimeout(cluster.fork, 1000); }); cluster.on("online", (worker) => { worker.process.stdout.on("data", (data) => { diff --git a/src/lib/UploadManager.ts b/src/lib/UploadManager.ts new file mode 100644 index 0000000..a12d68c --- /dev/null +++ b/src/lib/UploadManager.ts @@ -0,0 +1,136 @@ +import * as config from "config"; +import * as crypto from "crypto"; +import * as ffmpeg from "fluent-ffmpeg"; +import * as fsx from "fs-extra"; +import * as path from "path"; +import * as sharp from "sharp"; +import {Readable} from "stream"; +import {ReadableStreamBuffer} from "stream-buffers"; +import globals from "./globals"; + +const toArray = require("stream-to-array"); + +const dataDirName = "data"; + +interface IUploadConfirmation { + /** + * Indicates the error that might have occured during the upload + */ + error?: string; + + /** + * The file that has been uploaded + */ + fileName?: string; + + /** + * If the upload was successful + */ + success: boolean; +} + +type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside"; + +/** + * A helper class for uploading and managing files + */ +export class UploadManager { + + /** + * Returns the hash of the current time to be used as a filename. + */ + private static getCrypticFileName() { + const hash = crypto.createHash("md5"); + hash.update(Number(Date.now()).toString()); + return hash.digest("hex"); + } + + private readonly dataDir: string; + + constructor() { + this.dataDir = path.join(globals.getPublicDir(), dataDirName); + } + + /** + * Deletes an image for a provided web path. + * @param webPath + */ + public async deleteWebFile(webPath: string) { + const realPath = path.join(dataDirName, path.basename(webPath)); + if (await fsx.pathExists(realPath)) { + await fsx.unlink(realPath); + } else { + globals.logger.warn(`Could not delete web image ${realPath}: Not found!`); + } + } + + /** + * Converts a file to the webp format and stores it with a uuid filename. + * The web path for the image is returned. + * @param data + * @param width + * @param height + * @param fit + */ + public async processAndStoreImage(data: Buffer, width = 512, height = 512, + fit: ImageFit = "cover"): Promise { + const fileBasename = UploadManager.getCrypticFileName() + "." + config.get("api.imageFormat"); + await fsx.ensureDir(this.dataDir); + const filePath = path.join(this.dataDir, fileBasename); + let image = sharp(data) + .resize(width, height, { + fit, + }) + .normalise(); + if (config.get("api.imageFormat") === "webp") { + image = image.webp({ + reductionEffort: 6, + smartSubsample: true, + }); + } else { + image = image.png({ + adaptiveFiltering: true, + colors: 128, + }); + } + await image.toFile(filePath); + return `/${dataDirName}/${fileBasename}`; + } + + /** + * Converts a video into a smaller format and .mp4 and returns the web path + * @param data + * @param width + */ + public async processAndStoreVideo(data: Buffer, width: number = 720): Promise { + return new Promise(async (resolve) => { + const fileBasename = UploadManager.getCrypticFileName() + ".mp4"; + await fsx.ensureDir(this.dataDir); + const filePath = path.join(this.dataDir, fileBasename); + const videoFileStream = new ReadableStreamBuffer({ + chunkSize: 2048, + frequency: 10, + }); + videoFileStream.put(data); + const video = ffmpeg(videoFileStream); + video + .on("end", () => { + resolve(`/${dataDirName}/${fileBasename}`); + }) + .size(`${width}x?`) + .toFormat("libx264") + .output(filePath); + }); + } + + /** + * Convers a readable to a buffer + * @param stream + */ + public async streamToBuffer(stream: Readable) { + const parts = await toArray(stream); + const buffers = parts + .map((part: any) => Buffer.isBuffer(part) ? part : Buffer.from(part)); + return Buffer.concat(buffers); + } +} diff --git a/src/lib/errors/InvalidFileError.ts b/src/lib/errors/InvalidFileError.ts new file mode 100644 index 0000000..15886fa --- /dev/null +++ b/src/lib/errors/InvalidFileError.ts @@ -0,0 +1,14 @@ +import * as httpStatus from "http-status"; +import {BaseError} from "./BaseError"; + +/** + * An error that is thrown when a invalid file type is uploaded + */ +export class InvalidFileError extends BaseError { + + public readonly statusCode = httpStatus.NOT_ACCEPTABLE; + + constructor(mimetype: string) { + super(`The mimetype '${mimetype}' is not allowed.`); + } +} diff --git a/src/lib/errors/UploadFailedError.ts b/src/lib/errors/UploadFailedError.ts new file mode 100644 index 0000000..8109cea --- /dev/null +++ b/src/lib/errors/UploadFailedError.ts @@ -0,0 +1,10 @@ +import {BaseError} from "./BaseError"; + +/** + * An error that is thrown when a file failed to upload + */ +export class UploadFailedError extends BaseError { + constructor() { + super("Failed to upload the file"); + } +} diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 8847300..ca97fff 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -1,5 +1,6 @@ import * as config from "config"; import {EventEmitter} from "events"; +import * as path from "path"; import * as winston from "winston"; require("winston-daily-rotate-file"); @@ -38,6 +39,18 @@ namespace globals { }), ], }); + + /** + * Returns the absolute public path + */ + export function getPublicDir(): string { + let publicPath = config.get("frontend.publicPath"); + if (!path.isAbsolute(publicPath)) { + publicPath = path.normalize(path.join(__dirname, "../", publicPath)); + } + return publicPath; + } + export const internalEmitter: EventEmitter = new EventEmitter(); } diff --git a/src/lib/models/Post.ts b/src/lib/models/Post.ts index fad5013..8d60709 100644 --- a/src/lib/models/Post.ts +++ b/src/lib/models/Post.ts @@ -33,6 +33,12 @@ export class Post extends Model { @Column({allowNull: true}) public activityId: number; + /** + * An url pointing to any media that belongs to the post + */ + @Column({allowNull: true, type: sqz.STRING(512)}) + public mediaUrl: string; + /** * The author of the post */ diff --git a/src/lib/regex.ts b/src/lib/regex.ts index edfc0dc..859e4f6 100644 --- a/src/lib/regex.ts +++ b/src/lib/regex.ts @@ -1,5 +1,7 @@ export namespace is { const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g; + const videoRegex = /video\/.*/g; + const imageRegex = /image\/.*/g; /** * Tests if a string is a valid email. @@ -8,4 +10,20 @@ export namespace is { export function email(testString: string) { return emailRegex.test(testString); } + + /** + * Returns if the mimetype is a video + * @param mimetype + */ + export function video(mimetype: string) { + return videoRegex.test(mimetype); + } + + /** + * Returns if the mimetype is an image + * @param mimetype + */ + export function image(mimetype: string) { + return imageRegex.test(mimetype); + } } diff --git a/src/routes/GraphqlRoute.ts b/src/routes/GraphqlRoute.ts new file mode 100644 index 0000000..ea68e64 --- /dev/null +++ b/src/routes/GraphqlRoute.ts @@ -0,0 +1,81 @@ +import * as config from "config"; +import {Router} from "express"; +import * as graphqlHTTP from "express-graphql"; +import {buildSchema, GraphQLError} from "graphql"; +import {importSchema} from "graphql-import"; +import queryComplexity, {directiveEstimator, simpleEstimator} from "graphql-query-complexity"; +import {graphqlUploadExpress} from "graphql-upload"; +import * as path from "path"; +import globals from "../lib/globals"; +import Route from "../lib/Route"; +import {resolver} from "./graphql/resolvers"; + +const logger = globals.logger; + +/** + * A class for the /grpahql route + */ +export class GraphqlRoute extends Route { + /** + * Constructor, creates new router. + */ + constructor() { + super(); + this.router = Router(); + } + + + /** + * Initializes the route + * @param params + */ + public async init(...params: any): Promise { + this.router.use(graphqlUploadExpress({ + maxFileSize: config.get("api.maxFileSize"), + maxFiles: 10, + })); + // @ts-ignore + this.router.use(graphqlHTTP(async (request: any, response: any, {variables}) => { + response.setHeader("X-Max-Query-Complexity", config.get("api.maxQueryComplexity")); + return { + // @ts-ignore all + context: {session: request.session}, + formatError: (err: GraphQLError | any) => { + if (err.statusCode) { + response.status(err.statusCode); + } else { + response.status(400); + } + logger.debug(err.message); + logger.silly(err.stack); + return err.graphqlError ?? err; + }, + graphiql: config.get("api.graphiql"), + rootValue: resolver(request, response), + schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), + validationRules: [ + queryComplexity({ + estimators: [ + directiveEstimator(), + simpleEstimator(), + ], + maximumComplexity: config.get("api.maxQueryComplexity"), + onComplete: (complexity: number) => { + logger.debug(`QueryComplexity: ${complexity}`); + response.setHeader("X-Query-Complexity", complexity); + }, + variables, + }), + ], + }; + })); + } + + /** + * Destroys the route + * @param params + */ + public async destroy(...params: any): Promise { + return undefined; + } +} diff --git a/src/routes/UploadRoute.ts b/src/routes/UploadRoute.ts index 66d2c1c..1797dae 100644 --- a/src/routes/UploadRoute.ts +++ b/src/routes/UploadRoute.ts @@ -2,17 +2,16 @@ import * as bodyParser from "body-parser"; import * as config from "config"; import * as crypto from "crypto"; import {Router} from "express"; -import * as fileUpload from "express-fileupload"; import {UploadedFile} from "express-fileupload"; +import * as fileUpload from "express-fileupload"; import * as ffmpeg from "fluent-ffmpeg"; import * as fsx from "fs-extra"; import * as status from "http-status"; import * as path from "path"; -import * as sharp from "sharp"; -import {ReadableStreamBuffer} from "stream-buffers"; import globals from "../lib/globals"; import {Group, User} from "../lib/models"; import Route from "../lib/Route"; +import {UploadManager} from "../lib/UploadManager"; const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path; const dataDirName = "data"; @@ -54,12 +53,14 @@ export class UploadRoute extends Route { * The directory where the uploaded data will be saved in */ public readonly dataDir: string; + private uploadManager: UploadManager; constructor(private publicPath: string) { super(); this.router = Router(); this.dataDir = path.join(this.publicPath, dataDirName); ffmpeg.setFfmpegPath(ffmpegPath); + this.uploadManager = new UploadManager(); } /** @@ -118,9 +119,9 @@ export class UploadRoute extends Route { try { const user = await User.findByPk(request.session.userId); if (user) { - fileName = await this.processAndStoreImage(profilePic.data); + fileName = await this.uploadManager.processAndStoreImage(profilePic.data); if (user.profilePicture) { - await this.deleteWebFile(user.profilePicture); + await this.uploadManager.deleteWebFile(user.profilePicture); } user.profilePicture = fileName; await user.save(); @@ -162,9 +163,9 @@ export class UploadRoute extends Route { } const isAdmin = await group.$has("rAdmins", user); if (isAdmin) { - fileName = await this.processAndStoreImage(groupPicture.data); + fileName = await this.uploadManager.processAndStoreImage(groupPicture.data); if (group.picture) { - await this.deleteWebFile(group.picture); + await this.uploadManager.deleteWebFile(group.picture); } group.picture = fileName; await group.save(); @@ -186,76 +187,4 @@ export class UploadRoute extends Route { success, }; } - - /** - * Converts a file to the webp format and stores it with a uuid filename. - * The web path for the image is returned. - * @param data - * @param width - * @param height - * @param fit - */ - private async processAndStoreImage(data: Buffer, width = 512, height = 512, - fit: ImageFit = "cover"): Promise { - const fileBasename = UploadRoute.getFileName() + "." + config.get("api.imageFormat"); - await fsx.ensureDir(this.dataDir); - const filePath = path.join(this.dataDir, fileBasename); - let image = await sharp(data) - .resize(width, height, { - fit, - }) - .normalise(); - if (config.get("api.imageFormat") === "webp") { - image = await image.webp({ - reductionEffort: 6, - smartSubsample: true, - }); - } else { - image = await image.png({ - adaptiveFiltering: true, - colors: 128, - }); - } - await image.toFile(filePath); - return `/${dataDirName}/${fileBasename}`; - } - - /** - * Converts a video into a smaller format and .mp4 and returns the web path - * @param data - * @param width - */ - private async processAndStoreVideo(data: Buffer, width: number = 720): Promise { - return new Promise(async (resolve) => { - const fileBasename = UploadRoute.getFileName() + ".mp4"; - await fsx.ensureDir(this.dataDir); - const filePath = path.join(this.dataDir, fileBasename); - const videoFileStream = new ReadableStreamBuffer({ - chunkSize: 2048, - frequency: 10, - }); - videoFileStream.put(data); - const video = ffmpeg(videoFileStream); - video - .on("end", () => { - resolve(`/${dataDirName}/${fileBasename}`); - }) - .size(`${width}x?`) - .toFormat("libx264") - .output(filePath); - }); - } - - /** - * Deletes an image for a provided web path. - * @param webPath - */ - private async deleteWebFile(webPath: string) { - const realPath = path.join(this.dataDir, path.basename(webPath)); - if (await fsx.pathExists(realPath)) { - await fsx.unlink(realPath); - } else { - globals.logger.warn(`Could not delete web image ${realPath}: Not found!`); - } - } } diff --git a/src/graphql/BaseResolver.ts b/src/routes/graphql/BaseResolver.ts similarity index 84% rename from src/graphql/BaseResolver.ts rename to src/routes/graphql/BaseResolver.ts index 753af99..2bbe4cf 100644 --- a/src/graphql/BaseResolver.ts +++ b/src/routes/graphql/BaseResolver.ts @@ -1,4 +1,4 @@ -import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors"; +import {NotLoggedInGqlError} from "../../lib/errors/graphqlErrors"; /** * Base resolver class to provide common methods to all resolver classes diff --git a/src/graphql/BlacklistedResult.ts b/src/routes/graphql/BlacklistedResult.ts similarity index 100% rename from src/graphql/BlacklistedResult.ts rename to src/routes/graphql/BlacklistedResult.ts diff --git a/src/graphql/MutationResolver.ts b/src/routes/graphql/MutationResolver.ts similarity index 88% rename from src/graphql/MutationResolver.ts rename to src/routes/graphql/MutationResolver.ts index 236f067..696c2a5 100644 --- a/src/graphql/MutationResolver.ts +++ b/src/routes/graphql/MutationResolver.ts @@ -1,17 +1,21 @@ import {GraphQLError} from "graphql"; +import {FileUpload} from "graphql-upload"; import * as yaml from "js-yaml"; import isEmail from "validator/lib/isEmail"; -import dataaccess from "../lib/dataAccess"; -import {BlacklistedError} from "../lib/errors/BlacklistedError"; -import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError"; -import {InvalidEmailError} from "../lib/errors/InvalidEmailError"; -import {NotAGroupAdminError} from "../lib/errors/NotAGroupAdminError"; -import {NotAnAdminError} from "../lib/errors/NotAnAdminError"; -import {NotTheGroupCreatorError} from "../lib/errors/NotTheGroupCreatorError"; -import {PostNotFoundError} from "../lib/errors/PostNotFoundError"; -import globals from "../lib/globals"; -import {InternalEvents} from "../lib/InternalEvents"; -import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../lib/models"; +import dataaccess from "../../lib/dataAccess"; +import {BlacklistedError} from "../../lib/errors/BlacklistedError"; +import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; +import {InvalidEmailError} from "../../lib/errors/InvalidEmailError"; +import {InvalidFileError} from "../../lib/errors/InvalidFileError"; +import {NotAGroupAdminError} from "../../lib/errors/NotAGroupAdminError"; +import {NotAnAdminError} from "../../lib/errors/NotAnAdminError"; +import {NotTheGroupCreatorError} from "../../lib/errors/NotTheGroupCreatorError"; +import {PostNotFoundError} from "../../lib/errors/PostNotFoundError"; +import globals from "../../lib/globals"; +import {InternalEvents} from "../../lib/InternalEvents"; +import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models"; +import {is} from "../../lib/regex"; +import {UploadManager} from "../../lib/UploadManager"; import {BaseResolver} from "./BaseResolver"; const legit = require("legit"); @@ -21,6 +25,17 @@ const legit = require("legit"); */ export class MutationResolver extends BaseResolver { + /** + * An instance of the upload manager to handle uploads + */ + protected uploadManager: UploadManager; + + constructor() { + super(); + this.uploadManager = new UploadManager(); + } + + /** * Accepts the usage of cookies and stores the session * @param args @@ -130,13 +145,27 @@ export class MutationResolver extends BaseResolver { * @param activityId * @param request */ - public async createPost({content, activityId}: { content: string, activityId?: number }, request: any): - Promise { + public async createPost({content, activityId, file}: { content: string, activityId?: number, file: FileUpload }, + request: any): Promise { this.ensureLoggedIn(request); if (content.length > 2048) { throw new GraphQLError("Content too long."); } const post = await dataaccess.createPost(content, request.session.userId, activityId); + if (file) { + let fileUrl: string; + if (is.video(file.mimetype)) { + const fileBuffer = await this.uploadManager.streamToBuffer(file.createReadStream()); + fileUrl = await this.uploadManager.processAndStoreVideo(fileBuffer); + } else if (is.image(file.mimetype)) { + const fileBuffer = await this.uploadManager.streamToBuffer(file.createReadStream()); + fileUrl = await this.uploadManager.processAndStoreImage(fileBuffer); + } else { + throw new InvalidFileError(file.mimetype); + } + post.mediaUrl = fileUrl; + await post.save(); + } globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); return post; } diff --git a/src/graphql/QueryResolver.ts b/src/routes/graphql/QueryResolver.ts similarity index 92% rename from src/graphql/QueryResolver.ts rename to src/routes/graphql/QueryResolver.ts index a6cf288..263e360 100644 --- a/src/graphql/QueryResolver.ts +++ b/src/routes/graphql/QueryResolver.ts @@ -1,12 +1,12 @@ import {GraphQLError} from "graphql"; import {Op} from "sequelize"; -import dataaccess from "../lib/dataAccess"; -import {ChatNotFoundError} from "../lib/errors/ChatNotFoundError"; -import {PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; -import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError"; -import {RequestNotFoundError} from "../lib/errors/RequestNotFoundError"; -import {UserNotFoundError} from "../lib/errors/UserNotFoundError"; -import {Activity, BlacklistedPhrase, ChatRoom, Event, Group, Post, Request, User} from "../lib/models"; +import dataaccess from "../../lib/dataAccess"; +import {ChatNotFoundError} from "../../lib/errors/ChatNotFoundError"; +import {PostNotFoundGqlError} from "../../lib/errors/graphqlErrors"; +import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; +import {RequestNotFoundError} from "../../lib/errors/RequestNotFoundError"; +import {UserNotFoundError} from "../../lib/errors/UserNotFoundError"; +import {Activity, BlacklistedPhrase, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models"; import {BlacklistedResult} from "./BlacklistedResult"; import {MutationResolver} from "./MutationResolver"; import {SearchResult} from "./SearchResult"; diff --git a/src/graphql/SearchResult.ts b/src/routes/graphql/SearchResult.ts similarity index 81% rename from src/graphql/SearchResult.ts rename to src/routes/graphql/SearchResult.ts index bee76ef..4ca0631 100644 --- a/src/graphql/SearchResult.ts +++ b/src/routes/graphql/SearchResult.ts @@ -1,4 +1,4 @@ -import {Event, Group, Post, User} from "../lib/models"; +import {Event, Group, Post, User} from "../../lib/models"; /** * A class to wrap search results returned by the search resolver diff --git a/src/graphql/Token.ts b/src/routes/graphql/Token.ts similarity index 100% rename from src/graphql/Token.ts rename to src/routes/graphql/Token.ts diff --git a/src/graphql/resolvers.ts b/src/routes/graphql/resolvers.ts similarity index 100% rename from src/graphql/resolvers.ts rename to src/routes/graphql/resolvers.ts diff --git a/src/graphql/schema.graphql b/src/routes/graphql/schema.graphql similarity index 93% rename from src/graphql/schema.graphql rename to src/routes/graphql/schema.graphql index 3015271..f8239af 100644 --- a/src/graphql/schema.graphql +++ b/src/routes/graphql/schema.graphql @@ -85,7 +85,7 @@ type Mutation { sendMessage(chatId: ID!, content: String!): ChatMessage "create a post that can belong to an activity" - createPost(content: String!, activityId: ID): Post! + createPost(content: String!, activityId: ID, file: Upload): Post! "delete the post for a given post id" deletePost(postId: ID!): Boolean! @@ -340,6 +340,9 @@ type Post { "the activity that belongs to the post" activity: Activity + + "the uploaded file or video for the post" + media: Media } "represents a request of any type" @@ -508,8 +511,21 @@ type BlacklistedResult { phrases: [String!]! } +"a type of uploaded media" +type Media { + + "the url pointing to the media in the data folder" + url: String! + "the type of media that is uploaded" + type: MediaType +} +"represents the type of media" +enum MediaType { + VIDEO + IMAGE +} "represents the type of vote performed on a post" enum VoteType { diff --git a/yarn.lock b/yarn.lock index 3f6bb53..23f601c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -534,7 +534,7 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= -any-promise@^1.3.0: +any-promise@^1.1.0, any-promise@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= @@ -4655,6 +4655,11 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" + integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A== + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -4737,17 +4742,17 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -sharp@^0.23.4: - version "0.23.4" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.23.4.tgz#ca36067cb6ff7067fa6c77b01651cb9a890f8eb3" - integrity sha512-fJMagt6cT0UDy9XCsgyLi0eiwWWhQRxbwGmqQT6sY8Av4s0SVsT/deg8fobBQCTDU5iXRgz0rAeXoE2LBZ8g+Q== +sharp@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.24.0.tgz#1200f4bb36ccc2bb36a78f0bcba0302cf1f7a5fd" + integrity sha512-kUtQE6+HJnNqO0H6ueOBtRXahktuqydIBaFMvhDelf/KaK9j/adEdjf4Y3+bbjYOa5i6hi2EAa2Y2G9umP4s2g== dependencies: color "^3.1.2" detect-libc "^1.0.3" nan "^2.14.0" npmlog "^4.1.2" prebuild-install "^5.3.3" - semver "^6.3.0" + semver "^7.1.1" simple-get "^3.1.0" tar "^5.0.5" tunnel-agent "^0.6.0" @@ -5025,6 +5030,13 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +stream-to-array@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" + integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M= + dependencies: + any-promise "^1.1.0" + streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"