diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 0d8252402..4f144f1e9 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -33,6 +33,8 @@ export enum RIGHTS { LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', + CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE', + LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', @@ -50,4 +52,5 @@ export enum RIGHTS { CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', + ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', } diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index f14e77b17..eabaf8e99 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -31,6 +31,8 @@ export const ROLE_USER = new Role('user', [ RIGHTS.SEARCH_ADMIN_USERS, RIGHTS.LIST_CONTRIBUTION_LINKS, RIGHTS.COMMUNITY_STATISTICS, + RIGHTS.CREATE_CONTRIBUTION_MESSAGE, + RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/arg/ContributionMessageArgs.ts b/backend/src/graphql/arg/ContributionMessageArgs.ts new file mode 100644 index 000000000..dd75baed0 --- /dev/null +++ b/backend/src/graphql/arg/ContributionMessageArgs.ts @@ -0,0 +1,11 @@ +import { ArgsType, Field, InputType } from 'type-graphql' + +@InputType() +@ArgsType() +export default class ContributionMessageArgs { + @Field(() => Number) + contributionId: number + + @Field(() => String) + message: string +} diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts index aa878990c..1f690a3d8 100644 --- a/backend/src/graphql/model/Contribution.ts +++ b/backend/src/graphql/model/Contribution.ts @@ -1,7 +1,7 @@ import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' import { Contribution as dbContribution } from '@entity/Contribution' -import { User } from './User' +import { User } from '@entity/User' @ObjectType() export class Contribution { @@ -16,6 +16,8 @@ export class Contribution { this.confirmedAt = contribution.confirmedAt this.confirmedBy = contribution.confirmedBy this.contributionDate = contribution.contributionDate + this.state = contribution.contributionStatus + this.messagesCount = contribution.messages ? contribution.messages.length : 0 } @Field(() => Number) @@ -47,6 +49,12 @@ export class Contribution { @Field(() => Date) contributionDate: Date + + @Field(() => Number) + messagesCount: number + + @Field(() => String) + state: string } @ObjectType() diff --git a/backend/src/graphql/model/ContributionMessage.ts b/backend/src/graphql/model/ContributionMessage.ts new file mode 100644 index 000000000..1357c02cb --- /dev/null +++ b/backend/src/graphql/model/ContributionMessage.ts @@ -0,0 +1,49 @@ +import { Field, ObjectType } from 'type-graphql' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { User } from '@entity/User' + +@ObjectType() +export class ContributionMessage { + constructor(contributionMessage: DbContributionMessage, user: User) { + this.id = contributionMessage.id + this.message = contributionMessage.message + this.createdAt = contributionMessage.createdAt + this.updatedAt = contributionMessage.updatedAt + this.type = contributionMessage.type + this.userFirstName = user.firstName + this.userLastName = user.lastName + this.userId = user.id + } + + @Field(() => Number) + id: number + + @Field(() => String) + message: string + + @Field(() => Date) + createdAt: Date + + @Field(() => Date, { nullable: true }) + updatedAt?: Date | null + + @Field(() => String) + type: string + + @Field(() => String, { nullable: true }) + userFirstName: string | null + + @Field(() => String, { nullable: true }) + userLastName: string | null + + @Field(() => Number, { nullable: true }) + userId: number | null +} +@ObjectType() +export class ContributionMessageListResult { + @Field(() => Number) + count: number + + @Field(() => [ContributionMessage]) + messages: ContributionMessage[] +} diff --git a/backend/src/graphql/model/UnconfirmedContribution.ts b/backend/src/graphql/model/UnconfirmedContribution.ts index 1d697a971..5847b08d0 100644 --- a/backend/src/graphql/model/UnconfirmedContribution.ts +++ b/backend/src/graphql/model/UnconfirmedContribution.ts @@ -5,7 +5,7 @@ import { User } from '@entity/User' @ObjectType() export class UnconfirmedContribution { - constructor(contribution: Contribution, user: User, creations: Decimal[]) { + constructor(contribution: Contribution, user: User | undefined, creations: Decimal[]) { this.id = contribution.id this.userId = contribution.userId this.amount = contribution.amount @@ -14,7 +14,10 @@ export class UnconfirmedContribution { this.firstName = user ? user.firstName : '' this.lastName = user ? user.lastName : '' this.email = user ? user.email : '' + this.moderator = contribution.moderatorId this.creation = creations + this.state = contribution.contributionStatus + this.messageCount = contribution.messages ? contribution.messages.length : 0 } @Field(() => String) @@ -46,4 +49,10 @@ export class UnconfirmedContribution { @Field(() => [Decimal]) creation: Decimal[] + + @Field(() => String) + state: string + + @Field(() => Number) + messageCount: number } diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index e70fe71ee..f4656aec8 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -62,6 +62,10 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, } from './const/const' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import ContributionMessageArgs from '@arg/ContributionMessageArgs' +import { ContributionMessageType } from '@enum/MessageType' +import { ContributionMessage } from '@model/ContributionMessage' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -357,7 +361,14 @@ export class AdminResolver { @Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS]) @Query(() => [UnconfirmedContribution]) async listUnconfirmedContributions(): Promise { - const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } }) + const contributions = await getConnection() + .createQueryBuilder() + .select('c') + .from(Contribution, 'c') + .leftJoinAndSelect('c.messages', 'm') + .where({ confirmedAt: IsNull() }) + .getMany() + if (contributions.length === 0) { return [] } @@ -370,18 +381,11 @@ export class AdminResolver { const user = users.find((u) => u.id === contribution.userId) const creation = userCreations.find((c) => c.id === contribution.userId) - return { - id: contribution.id, - userId: contribution.userId, - date: contribution.contributionDate, - memo: contribution.memo, - amount: contribution.amount, - moderator: contribution.moderatorId, - firstName: user ? user.firstName : '', - lastName: user ? user.lastName : '', - email: user ? user.email : '', - creation: creation ? creation.creations : FULL_CREATION_AVAILABLE, - } + return new UnconfirmedContribution( + contribution, + user, + creation ? creation.creations : FULL_CREATION_AVAILABLE, + ) }) } @@ -696,4 +700,46 @@ export class AdminResolver { logger.debug(`updateContributionLink successful!`) return new ContributionLink(dbContributionLink) } + + @Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE]) + @Mutation(() => ContributionMessage) + async adminCreateContributionMessage( + @Args() { contributionId, message }: ContributionMessageArgs, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction('READ UNCOMMITTED') + const contributionMessage = DbContributionMessage.create() + try { + const contribution = await Contribution.findOne({ id: contributionId }) + if (!contribution) { + throw new Error('Contribution not found') + } + contributionMessage.contributionId = contributionId + contributionMessage.createdAt = new Date() + contributionMessage.message = message + contributionMessage.userId = user.id + contributionMessage.type = ContributionMessageType.DIALOG + await queryRunner.manager.insert(DbContributionMessage, contributionMessage) + + if ( + contribution.contributionStatus === ContributionStatus.DELETED || + contribution.contributionStatus === ContributionStatus.DENIED || + contribution.contributionStatus === ContributionStatus.PENDING + ) { + contribution.contributionStatus = ContributionStatus.IN_PROGRESS + await queryRunner.manager.update(Contribution, { id: contributionId }, contribution) + } + await queryRunner.commitTransaction() + } catch (e) { + await queryRunner.rollbackTransaction() + logger.error(`ContributionMessage was not successful: ${e}`) + throw new Error(`ContributionMessage was not successful: ${e}`) + } finally { + await queryRunner.release() + } + return new ContributionMessage(contributionMessage, user) + } } diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts new file mode 100644 index 000000000..6c617acb4 --- /dev/null +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -0,0 +1,297 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { GraphQLError } from 'graphql' +import { + adminCreateContributionMessage, + createContribution, + createContributionMessage, +} from '@/seeds/graphql/mutations' +import { listContributionMessages, login } from '@/seeds/graphql/queries' +import { userFactory } from '@/seeds/factory/user' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { peterLustig } from '@/seeds/users/peter-lustig' + +let mutate: any, query: any, con: any +let testEnv: any +let result: any + +beforeAll(async () => { + testEnv = await testEnvironment() + mutate = testEnv.mutate + query = testEnv.query + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('ContributionMessageResolver', () => { + describe('adminCreateContributionMessage', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: adminCreateContributionMessage, + variables: { contributionId: 1, message: 'This is a test message' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + result = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + describe('input not valid', () => { + it('throws error when contribution does not exist', async () => { + await expect( + mutate({ + mutation: adminCreateContributionMessage, + variables: { + contributionId: -1, + message: 'Test', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'ContributionMessage was not successful: Error: Contribution not found', + ), + ], + }), + ) + }) + }) + + describe('valid input', () => { + it('creates ContributionMessage', async () => { + await expect( + mutate({ + mutation: adminCreateContributionMessage, + variables: { + contributionId: result.data.createContribution.id, + message: 'Admin Test', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + adminCreateContributionMessage: expect.objectContaining({ + id: expect.any(Number), + message: 'Admin Test', + type: 'DIALOG', + userFirstName: 'Peter', + userLastName: 'Lustig', + }), + }, + }), + ) + }) + }) + }) + }) + + describe('createContributionMessage', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: createContributionMessage, + variables: { contributionId: 1, message: 'This is a test message' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + describe('input not valid', () => { + it('throws error when contribution does not exist', async () => { + await expect( + mutate({ + mutation: createContributionMessage, + variables: { + contributionId: -1, + message: 'Test', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'ContributionMessage was not successful: Error: Contribution not found', + ), + ], + }), + ) + }) + + it('throws error when other user tries to send createContributionMessage', async () => { + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await expect( + mutate({ + mutation: createContributionMessage, + variables: { + contributionId: result.data.createContribution.id, + message: 'Test', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'ContributionMessage was not successful: Error: Can not send message to contribution of another user', + ), + ], + }), + ) + }) + }) + + describe('valid input', () => { + beforeAll(async () => { + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + it('creates ContributionMessage', async () => { + await expect( + mutate({ + mutation: createContributionMessage, + variables: { + contributionId: result.data.createContribution.id, + message: 'User Test', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + createContributionMessage: expect.objectContaining({ + id: expect.any(Number), + message: 'User Test', + type: 'DIALOG', + userFirstName: 'Bibi', + userLastName: 'Bloxberg', + }), + }, + }), + ) + }) + }) + }) + }) + + describe('listContributionMessages', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: listContributionMessages, + variables: { contributionId: 1 }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + it('returns a list of contributionmessages', async () => { + await expect( + mutate({ + mutation: listContributionMessages, + variables: { contributionId: result.data.createContribution.id }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listContributionMessages: { + count: 2, + messages: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + message: 'Admin Test', + type: 'DIALOG', + userFirstName: 'Peter', + userLastName: 'Lustig', + }), + expect.objectContaining({ + id: expect.any(Number), + message: 'User Test', + type: 'DIALOG', + userFirstName: 'Bibi', + userLastName: 'Bloxberg', + }), + ]), + }, + }, + }), + ) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts new file mode 100644 index 000000000..408481513 --- /dev/null +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -0,0 +1,84 @@ +import { backendLogger as logger } from '@/server/logger' +import { RIGHTS } from '@/auth/RIGHTS' +import { Context, getUser } from '@/server/context' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { Arg, Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql' +import ContributionMessageArgs from '@arg/ContributionMessageArgs' +import { Contribution } from '@entity/Contribution' +import { ContributionMessageType } from '@enum/MessageType' +import { ContributionStatus } from '@enum/ContributionStatus' +import { getConnection } from '@dbTools/typeorm' +import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage' +import Paginated from '@arg/Paginated' +import { Order } from '@enum/Order' + +@Resolver() +export class ContributionMessageResolver { + @Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE]) + @Mutation(() => ContributionMessage) + async createContributionMessage( + @Args() { contributionId, message }: ContributionMessageArgs, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction('READ UNCOMMITTED') + const contributionMessage = DbContributionMessage.create() + try { + const contribution = await Contribution.findOne({ id: contributionId }) + if (!contribution) { + throw new Error('Contribution not found') + } + if (contribution.userId !== user.id) { + throw new Error('Can not send message to contribution of another user') + } + + contributionMessage.contributionId = contributionId + contributionMessage.createdAt = new Date() + contributionMessage.message = message + contributionMessage.userId = user.id + contributionMessage.type = ContributionMessageType.DIALOG + await queryRunner.manager.insert(DbContributionMessage, contributionMessage) + + if (contribution.contributionStatus === ContributionStatus.IN_PROGRESS) { + contribution.contributionStatus = ContributionStatus.PENDING + await queryRunner.manager.update(Contribution, { id: contributionId }, contribution) + } + await queryRunner.commitTransaction() + } catch (e) { + await queryRunner.rollbackTransaction() + logger.error(`ContributionMessage was not successful: ${e}`) + throw new Error(`ContributionMessage was not successful: ${e}`) + } finally { + await queryRunner.release() + } + return new ContributionMessage(contributionMessage, user) + } + + @Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES]) + @Query(() => ContributionMessageListResult) + async listContributionMessages( + @Arg('contributionId') contributionId: number, + @Args() + { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, + ): Promise { + const [contributionMessages, count] = await getConnection() + .createQueryBuilder() + .select('cm') + .from(DbContributionMessage, 'cm') + .leftJoinAndSelect('cm.user', 'u') + .where({ contributionId: contributionId }) + .orderBy('cm.createdAt', order) + .limit(pageSize) + .offset((currentPage - 1) * pageSize) + .getManyAndCount() + + return { + count, + messages: contributionMessages.map( + (message) => new ContributionMessage(message, message.user), + ), + } + } +} diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 8056ffde3..fc93880f1 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -11,7 +11,6 @@ import { ContributionType } from '@enum/ContributionType' import { ContributionStatus } from '@enum/ContributionStatus' import { Contribution, ContributionListResult } from '@model/Contribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' -import { User } from '@model/User' import { validateContribution, getUserCreation, updateCreations } from './util/creations' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' @@ -90,19 +89,23 @@ export class ContributionResolver { userId: number confirmedBy?: FindOperator | null } = { userId: user.id } + if (filterConfirmed) where.confirmedBy = IsNull() - const [contributions, count] = await dbContribution.findAndCount({ - where, - order: { - createdAt: order, - }, - withDeleted: true, - skip: (currentPage - 1) * pageSize, - take: pageSize, - }) + + const [contributions, count] = await getConnection() + .createQueryBuilder() + .select('c') + .from(dbContribution, 'c') + .leftJoinAndSelect('c.messages', 'm') + .where(where) + .orderBy('c.createdAt', order) + .limit(pageSize) + .offset((currentPage - 1) * pageSize) + .getManyAndCount() + return new ContributionListResult( count, - contributions.map((contribution) => new Contribution(contribution, new User(user))), + contributions.map((contribution) => new Contribution(contribution, user)), ) } @@ -123,9 +126,7 @@ export class ContributionResolver { .getManyAndCount() return new ContributionListResult( count, - dbContributions.map( - (contribution) => new Contribution(contribution, new User(contribution.user)), - ), + dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), ) } diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index bf898bd7d..e5f290645 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -261,3 +261,31 @@ export const deleteContribution = gql` deleteContribution(id: $id) } ` + +export const createContributionMessage = gql` + mutation ($contributionId: Float!, $message: String!) { + createContributionMessage(contributionId: $contributionId, message: $message) { + id + message + createdAt + updatedAt + type + userFirstName + userLastName + } + } +` + +export const adminCreateContributionMessage = gql` + mutation ($contributionId: Float!, $message: String!) { + adminCreateContributionMessage(contributionId: $contributionId, message: $message) { + id + message + createdAt + updatedAt + type + userFirstName + userLastName + } + } +` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 3bd042ac2..60dffa21b 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -292,3 +292,26 @@ export const searchAdminUsers = gql` } } ` + +export const listContributionMessages = gql` + query ($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) { + listContributionMessages( + contributionId: $contributionId + pageSize: $pageSize + currentPage: $currentPage + order: $order + ) { + count + messages { + id + message + createdAt + updatedAt + type + userFirstName + userLastName + userId + } + } + } +` diff --git a/database/entity/0047-messages_tables/ContributionMessage.ts b/database/entity/0047-messages_tables/ContributionMessage.ts index d9ac124dd..e5226043d 100644 --- a/database/entity/0047-messages_tables/ContributionMessage.ts +++ b/database/entity/0047-messages_tables/ContributionMessage.ts @@ -8,6 +8,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm' import { Contribution } from '../Contribution' +import { User } from '../User' @Entity('contribution_messages', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci', @@ -26,6 +27,10 @@ export class ContributionMessage extends BaseEntity { @Column({ name: 'user_id', unsigned: true, nullable: false }) userId: number + @ManyToOne(() => User, (user) => user.messages) + @JoinColumn({ name: 'user_id' }) + user: User + @Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' }) message: string diff --git a/database/entity/0047-messages_tables/User.ts b/database/entity/0047-messages_tables/User.ts new file mode 100644 index 000000000..a772a3c99 --- /dev/null +++ b/database/entity/0047-messages_tables/User.ts @@ -0,0 +1,116 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + unique: true, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + unique: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @DeleteDateColumn() + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + emailHash: Buffer + + @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] +} diff --git a/database/entity/User.ts b/database/entity/User.ts index 02a99fcd1..7d15bf559 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0046-adapt_users_table_for_gradidoid/User' +export { User } from './0047-messages_tables/User' diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 03299dd49..88c312a3f 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -206,6 +206,8 @@ export const listContributions = gql` confirmedAt confirmedBy deletedAt + state + messagesCount } } }