From 781ee274b8c23d8e55686b67bc7cc40b8f61d920 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 25 Jan 2020 00:27:51 +0100 Subject: [PATCH 1/4] Fix allow media id of user to be null --- src/lib/models/User.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index 7fbe1f5..c7ae2ec 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -103,10 +103,9 @@ export class User extends Model { * The id of the media that is the users profile picture */ @ForeignKey(() => Media) - @Column({allowNull: false}) + @Column({allowNull: true}) public mediaId: number; - /** * The media of the user */ From 9328367bb58288e4ccadfb36766d38537afbc886 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 25 Jan 2020 11:15:46 +0100 Subject: [PATCH 2/4] Add configurable levels - Add Level table and gql type - change level field on User - add hook on user to update the level --- CHANGELOG.md | 3 +++ src/lib/dataAccess.ts | 2 +- src/lib/models/Level.ts | 33 +++++++++++++++++++++++++++++++ src/lib/models/Media.ts | 4 +++- src/lib/models/User.ts | 32 ++++++++++++++++++++++++++++-- src/lib/models/index.ts | 1 + src/routes/graphql/schema.graphql | 16 ++++++++++++--- 7 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/lib/models/Level.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index add2222..f2ff883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - delete handler for media to delete the corresponding file - type for create post to know if it is a media or text post (media posts are invisible until a media file is uploaded) - reports and mutations to report posts and create reasons to report +- level entity ### Removed @@ -51,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - config behaviour to use all files that reside in the ./config directory with the .toml format - default response timeout from 2 minutes to 30 seconds - cluster api to start workers with a 2 second delay each to avoid race conditions +- levels to be configured in the backend ### Fixed @@ -60,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - style issues - graphql schema for denyRequest using the wrong parameters - sendRequest allowing duplicates +- upload throwing an error when the old picture doesn't exist ## [0.9] - 2019-10-29 diff --git a/src/lib/dataAccess.ts b/src/lib/dataAccess.ts index 471e38e..cc566eb 100644 --- a/src/lib/dataAccess.ts +++ b/src/lib/dataAccess.ts @@ -67,12 +67,12 @@ namespace dataaccess { models.Media, models.Report, models.ReportReason, + models.Level, ]); } catch (err) { globals.logger.error(err.message); globals.logger.debug(err.stack); } - await databaseCleanup(); setInterval(databaseCleanup, config.get("database.cleanupInterval") * 1000); } diff --git a/src/lib/models/Level.ts b/src/lib/models/Level.ts new file mode 100644 index 0000000..51863e1 --- /dev/null +++ b/src/lib/models/Level.ts @@ -0,0 +1,33 @@ +import * as sqz from "sequelize"; +import {Column, Model, NotNull, Table, Unique} from "sequelize-typescript"; + +/** + * A level of the ranking system + */ +@Table({underscored: true}) +export class Level extends Model { + + /** + * The name of the level + */ + @NotNull + @Unique + @Column({allowNull: false, type: sqz.STRING(64), unique: true}) + public name: string; + + /** + * The number of the level + */ + @NotNull + @Unique + @Column({allowNull: false, unique: true}) + public levelNumber: number; + + /** + * The required points for the level + */ + @NotNull + @Unique + @Column({allowNull: false, unique: true}) + public points: number; +} diff --git a/src/lib/models/Media.ts b/src/lib/models/Media.ts index cc5c62e..606cafb 100644 --- a/src/lib/models/Media.ts +++ b/src/lib/models/Media.ts @@ -18,7 +18,9 @@ export class Media extends Model { */ @BeforeDestroy public static async deleteMediaFile(instance: Media) { - await fsx.unlink(instance.path); + if (await fsx.pathExists(instance.path)) { + await fsx.unlink(instance.path); + } } /** diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index c7ae2ec..7d6c147 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -1,5 +1,6 @@ import * as sqz from "sequelize"; import { + BeforeUpdate, BelongsTo, BelongsToMany, Column, @@ -24,6 +25,7 @@ import {Friendship} from "./Friendship"; import {Group} from "./Group"; import {GroupAdmin} from "./GroupAdmin"; import {GroupMember} from "./GroupMember"; +import {Level} from "./Level"; import {Media} from "./Media"; import {Post} from "./Post"; import {PostVote} from "./PostVote"; @@ -35,6 +37,19 @@ import {Request, RequestType} from "./Request"; @Table({underscored: true}) export class User extends Model { + /** + * A function that is called before the user is updated. + * It assigns the corresponding level to the user + * @param instance + */ + @BeforeUpdate + public static async assignLevel(instance: User) { + const level = await Level.findOne({where: {points: {[sqz.Op.lte]: instance.rankpoints}}, order: [["levelNumber", "desc"]]}) as Level; + if (level) { + instance.$set("rLevel", level); + } + } + /** * The name of the user */ @@ -99,6 +114,13 @@ export class User extends Model { @Column({defaultValue: false, allowNull: false}) public isAdmin: boolean; + /** + * The level of the user + */ + @ForeignKey(() => Level) + @Column({allowNull: true}) + public levelId: number; + /** * The id of the media that is the users profile picture */ @@ -106,6 +128,12 @@ export class User extends Model { @Column({allowNull: true}) public mediaId: number; + /** + * The level of the user + */ + @BelongsTo(() => Level) + public rLevel: Level; + /** * The media of the user */ @@ -220,8 +248,8 @@ export class User extends Model { /** * The level of the user which is the points divided by 100 */ - public get level(): number { - return Math.ceil(this.getDataValue("rankpoints") / 100); + public async level(): Promise { + return await this.$get("rLevel") as Level; } /** diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts index 55fff59..99e86de 100644 --- a/src/lib/models/index.ts +++ b/src/lib/models/index.ts @@ -16,3 +16,4 @@ export {BlacklistedPhrase} from "./BlacklistedPhrase"; export {Media} from "./Media"; export {Report} from "./Report"; export {ReportReason} from "./ReportReason"; +export {Level} from "./Level"; diff --git a/src/routes/graphql/schema.graphql b/src/routes/graphql/schema.graphql index 044c166..e0082e2 100644 --- a/src/routes/graphql/schema.graphql +++ b/src/routes/graphql/schema.graphql @@ -189,7 +189,7 @@ interface UserData { points: Int! "the levels of the user depending on the points" - level: Int! + level: Level } "represents a single user account" @@ -240,7 +240,7 @@ type User implements UserData{ eventCount: Int! "the levels of the user depending on the points" - level: Int! + level: Level } type Profile implements UserData { @@ -311,7 +311,7 @@ type Profile implements UserData { points: Int! "the levels of the user depending on the points" - level: Int! + level: Level "the custom settings for the frontend" settings: String! @@ -571,6 +571,16 @@ type ReportReason { description: String! } +"A level of a user" +type Level { + + "The name of the level" + name: String! + + "The number of the level in the ranking" + levelNumber: String! +} + "represents the type of media" enum MediaType { VIDEO From e8eb85993e4c151bf8ee65ac869ef51741bd7c61 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 25 Jan 2020 11:43:51 +0100 Subject: [PATCH 3/4] Add level configuration and query - Add query getLevels to get all possible levels - Add mutation createLevel as a way for admins to create new levels - Remove column levelNumber and replace it with method that looks up the number of the level by counting --- src/lib/UploadManager.ts | 2 +- src/lib/errors/LevelAlreadyExistsError.ts | 10 ++++++ src/lib/models/Level.ts | 13 ++++---- src/lib/models/User.ts | 2 +- src/routes/graphql/MutationResolver.ts | 39 ++++++++++++++++++++++- src/routes/graphql/QueryResolver.ts | 24 ++++++++++++-- src/routes/graphql/schema.graphql | 14 +++++++- 7 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 src/lib/errors/LevelAlreadyExistsError.ts diff --git a/src/lib/UploadManager.ts b/src/lib/UploadManager.ts index 223ea89..00856c9 100644 --- a/src/lib/UploadManager.ts +++ b/src/lib/UploadManager.ts @@ -98,7 +98,7 @@ export class UploadManager { * @param extension */ public async processAndStoreVideo(data: Buffer, extension: string): Promise { - const fileBasename = UploadManager.getCrypticFileName() + extension; + const fileBasename = UploadManager.getCrypticFileName() + "." + extension; await fsx.ensureDir(this.dataDir); const filePath = path.join(this.dataDir, fileBasename); await fsx.writeFile(filePath, data); diff --git a/src/lib/errors/LevelAlreadyExistsError.ts b/src/lib/errors/LevelAlreadyExistsError.ts new file mode 100644 index 0000000..3ca5e2d --- /dev/null +++ b/src/lib/errors/LevelAlreadyExistsError.ts @@ -0,0 +1,10 @@ +import {BaseError} from "./BaseError"; + +/** + * An error that is thrown when the level already exists + */ +export class LevelAlreadyExistsError extends BaseError { + constructor(property: string) { + super(`A level with the property value '${property}' already exists`); + } +} diff --git a/src/lib/models/Level.ts b/src/lib/models/Level.ts index 51863e1..727b86c 100644 --- a/src/lib/models/Level.ts +++ b/src/lib/models/Level.ts @@ -16,18 +16,17 @@ export class Level extends Model { public name: string; /** - * The number of the level + * The required points for the level */ @NotNull @Unique @Column({allowNull: false, unique: true}) - public levelNumber: number; + public points: number; /** - * The required points for the level + * Returns the number of the level as the number of the database entry */ - @NotNull - @Unique - @Column({allowNull: false, unique: true}) - public points: number; + public async levelNumber(): Promise { + return Level.count({where: {points: {[sqz.Op.lte]: this.points}}}); + } } diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index 7d6c147..87b3f9c 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -44,7 +44,7 @@ export class User extends Model { */ @BeforeUpdate public static async assignLevel(instance: User) { - const level = await Level.findOne({where: {points: {[sqz.Op.lte]: instance.rankpoints}}, order: [["levelNumber", "desc"]]}) as Level; + const level = await Level.findOne({where: {points: {[sqz.Op.lte]: instance.rankpoints}}, order: [["points", "desc"]]}) as Level; if (level) { instance.$set("rLevel", level); } diff --git a/src/routes/graphql/MutationResolver.ts b/src/routes/graphql/MutationResolver.ts index 9933479..6e19237 100644 --- a/src/routes/graphql/MutationResolver.ts +++ b/src/routes/graphql/MutationResolver.ts @@ -1,11 +1,13 @@ import {GraphQLError} from "graphql"; import * as yaml from "js-yaml"; +import sequelize from "sequelize"; import isEmail from "validator/lib/isEmail"; import dataAccess from "../../lib/dataAccess"; import {BlacklistedError} from "../../lib/errors/BlacklistedError"; import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; import {HandleInUseError} from "../../lib/errors/HandleInUseError"; import {InvalidEmailError} from "../../lib/errors/InvalidEmailError"; +import {LevelAlreadyExistsError} from "../../lib/errors/LevelAlreadyExistsError"; import {NotAGroupAdminError} from "../../lib/errors/NotAGroupAdminError"; import {NotAnAdminError} from "../../lib/errors/NotAnAdminError"; import {NotTheGroupCreatorError} from "../../lib/errors/NotTheGroupCreatorError"; @@ -15,7 +17,18 @@ import {ReportReasonNameAlreadyExistsError} from "../../lib/errors/ReportReasonN import {ReportReasonNotFoundError} from "../../lib/errors/ReportReasonNotFoundError"; import globals from "../../lib/globals"; import {InternalEvents} from "../../lib/InternalEvents"; -import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models"; +import { + Activity, + BlacklistedPhrase, + ChatMessage, + ChatRoom, + Event, + Group, + Level, + Post, + Request, + User, +} from "../../lib/models"; import {Report} from "../../lib/models"; import {ReportReason} from "../../lib/models"; import {UploadManager} from "../../lib/UploadManager"; @@ -147,6 +160,8 @@ export class MutationResolver extends BaseResolver { await user.save(); return user.settings; } catch (err) { + globals.logger.warning(err.message); + globals.logger.debug(err.stack); throw new GraphQLError("Invalid settings json."); } } @@ -557,4 +572,26 @@ export class MutationResolver extends BaseResolver { } return ReportReason.create({name, description}); } + + /** + * Creates a new level + * @param name + * @param levelNumber + * @param requiredPoints + * @param request + */ + public async createLevel({name, requiredPoints}: {name: string, requiredPoints: number}, request: any): + Promise { + this.ensureLoggedIn(request); + const user = await User.findByPk(request.session.userId); + if (!user.isAdmin) { + throw new NotAnAdminError(); + } + const existingLevel = await Level.findOne({where: {[sequelize.Op.or]: [{name}, {points: requiredPoints}]}}); + if (existingLevel) { + throw new LevelAlreadyExistsError( + existingLevel.name === name ? "name" : "points"); + } + return Level.create({name, points: requiredPoints}); + } } diff --git a/src/routes/graphql/QueryResolver.ts b/src/routes/graphql/QueryResolver.ts index 7f0c1ed..3a7c374 100644 --- a/src/routes/graphql/QueryResolver.ts +++ b/src/routes/graphql/QueryResolver.ts @@ -7,7 +7,18 @@ import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; import {NotAnAdminError} from "../../lib/errors/NotAnAdminError"; import {RequestNotFoundError} from "../../lib/errors/RequestNotFoundError"; import {UserNotFoundError} from "../../lib/errors/UserNotFoundError"; -import {Activity, BlacklistedPhrase, ChatRoom, Event, Group, Post, Report, Request, User} from "../../lib/models"; +import { + Activity, + BlacklistedPhrase, + ChatRoom, + Event, + Group, + Level, + Post, + Report, + Request, + User +} from "../../lib/models"; import {BlacklistedResult} from "./BlacklistedResult"; import {MutationResolver} from "./MutationResolver"; import {SearchResult} from "./SearchResult"; @@ -198,6 +209,15 @@ export class QueryResolver extends MutationResolver { if (!user?.isAdmin) { throw new NotAnAdminError(); } - return Report.findAll({limit: first, offset}); + return Report.findAll({limit: first, offset, order: [["id", "DESC"]]}); + } + + /** + * Returns the levels that are configured + * @param first + * @param offset + */ + public async getLevels({first, offset}: {first: number, offset: number}): Promise { + return Level.findAll({limit: first, offset, order: [["points", "ASC"]]}); } } diff --git a/src/routes/graphql/schema.graphql b/src/routes/graphql/schema.graphql index e0082e2..4ddf7be 100644 --- a/src/routes/graphql/schema.graphql +++ b/src/routes/graphql/schema.graphql @@ -48,6 +48,9 @@ type Query { "Returns all issued reports with pagination" getReports(first: Int = 20, offset: Int = 0): [Report!]! @complexity(value: 1, multipliers: ["first"]) + + "Returns the levels configured in the backend" + getLevels(first: Int =20, offset: Int = 0): [Level!]! @complexity(value: 1, multipliers: ["first"]) } type Mutation { @@ -143,6 +146,9 @@ type Mutation { "Creates a new report reason" createReportReason(name: String!, description: String!): ReportReason + + "Creates a new level" + createLevel(name: String!, requiredPoints: Int!): Level! } interface UserData { @@ -574,11 +580,17 @@ type ReportReason { "A level of a user" type Level { + "The level id" + id: ID! + "The name of the level" name: String! "The number of the level in the ranking" - levelNumber: String! + levelNumber: Int! + + "The points required for this level" + points: Int! } "represents the type of media" From c01d37e35584fd696532013e9a62e4990a036661 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 25 Jan 2020 11:45:18 +0100 Subject: [PATCH 4/4] Update changelog (previous) Closes #99 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ff883..62fbe01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - graphql schema for denyRequest using the wrong parameters - sendRequest allowing duplicates - upload throwing an error when the old picture doesn't exist - +- extension of uploaded videos doesn't have a dot ## [0.9] - 2019-10-29