mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2149 from gradido/2131-crud-for-messages
feat: 🚀 CRUD For Contribution Messages
This commit is contained in:
commit
f10a014af4
@ -33,6 +33,8 @@ export enum RIGHTS {
|
|||||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||||
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
|
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
|
||||||
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
||||||
|
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
|
||||||
|
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||||
@ -50,4 +52,5 @@ export enum RIGHTS {
|
|||||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||||
|
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export const ROLE_USER = new Role('user', [
|
|||||||
RIGHTS.SEARCH_ADMIN_USERS,
|
RIGHTS.SEARCH_ADMIN_USERS,
|
||||||
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
||||||
RIGHTS.COMMUNITY_STATISTICS,
|
RIGHTS.COMMUNITY_STATISTICS,
|
||||||
|
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
|
||||||
|
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||||
])
|
])
|
||||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||||
|
|
||||||
|
|||||||
11
backend/src/graphql/arg/ContributionMessageArgs.ts
Normal file
11
backend/src/graphql/arg/ContributionMessageArgs.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||||
import { User } from './User'
|
import { User } from '@entity/User'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Contribution {
|
export class Contribution {
|
||||||
@ -16,6 +16,8 @@ export class Contribution {
|
|||||||
this.confirmedAt = contribution.confirmedAt
|
this.confirmedAt = contribution.confirmedAt
|
||||||
this.confirmedBy = contribution.confirmedBy
|
this.confirmedBy = contribution.confirmedBy
|
||||||
this.contributionDate = contribution.contributionDate
|
this.contributionDate = contribution.contributionDate
|
||||||
|
this.state = contribution.contributionStatus
|
||||||
|
this.messagesCount = contribution.messages ? contribution.messages.length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
@ -47,6 +49,12 @@ export class Contribution {
|
|||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
contributionDate: Date
|
contributionDate: Date
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
messagesCount: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
state: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
49
backend/src/graphql/model/ContributionMessage.ts
Normal file
49
backend/src/graphql/model/ContributionMessage.ts
Normal file
@ -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[]
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { User } from '@entity/User'
|
|||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UnconfirmedContribution {
|
export class UnconfirmedContribution {
|
||||||
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
|
constructor(contribution: Contribution, user: User | undefined, creations: Decimal[]) {
|
||||||
this.id = contribution.id
|
this.id = contribution.id
|
||||||
this.userId = contribution.userId
|
this.userId = contribution.userId
|
||||||
this.amount = contribution.amount
|
this.amount = contribution.amount
|
||||||
@ -14,7 +14,10 @@ export class UnconfirmedContribution {
|
|||||||
this.firstName = user ? user.firstName : ''
|
this.firstName = user ? user.firstName : ''
|
||||||
this.lastName = user ? user.lastName : ''
|
this.lastName = user ? user.lastName : ''
|
||||||
this.email = user ? user.email : ''
|
this.email = user ? user.email : ''
|
||||||
|
this.moderator = contribution.moderatorId
|
||||||
this.creation = creations
|
this.creation = creations
|
||||||
|
this.state = contribution.contributionStatus
|
||||||
|
this.messageCount = contribution.messages ? contribution.messages.length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@ -46,4 +49,10 @@ export class UnconfirmedContribution {
|
|||||||
|
|
||||||
@Field(() => [Decimal])
|
@Field(() => [Decimal])
|
||||||
creation: Decimal[]
|
creation: Decimal[]
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
state: string
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
messageCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,10 @@ import {
|
|||||||
MEMO_MAX_CHARS,
|
MEMO_MAX_CHARS,
|
||||||
MEMO_MIN_CHARS,
|
MEMO_MIN_CHARS,
|
||||||
} from './const/const'
|
} 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_IN_REGISTER = 1
|
||||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||||
@ -357,7 +361,14 @@ export class AdminResolver {
|
|||||||
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||||
@Query(() => [UnconfirmedContribution])
|
@Query(() => [UnconfirmedContribution])
|
||||||
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
||||||
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) {
|
if (contributions.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -370,18 +381,11 @@ export class AdminResolver {
|
|||||||
const user = users.find((u) => u.id === contribution.userId)
|
const user = users.find((u) => u.id === contribution.userId)
|
||||||
const creation = userCreations.find((c) => c.id === contribution.userId)
|
const creation = userCreations.find((c) => c.id === contribution.userId)
|
||||||
|
|
||||||
return {
|
return new UnconfirmedContribution(
|
||||||
id: contribution.id,
|
contribution,
|
||||||
userId: contribution.userId,
|
user,
|
||||||
date: contribution.contributionDate,
|
creation ? creation.creations : FULL_CREATION_AVAILABLE,
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,4 +700,46 @@ export class AdminResolver {
|
|||||||
logger.debug(`updateContributionLink successful!`)
|
logger.debug(`updateContributionLink successful!`)
|
||||||
return new ContributionLink(dbContributionLink)
|
return new ContributionLink(dbContributionLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
|
||||||
|
@Mutation(() => ContributionMessage)
|
||||||
|
async adminCreateContributionMessage(
|
||||||
|
@Args() { contributionId, message }: ContributionMessageArgs,
|
||||||
|
@Ctx() context: Context,
|
||||||
|
): Promise<ContributionMessage> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
297
backend/src/graphql/resolver/ContributionMessageResolver.test.ts
Normal file
297
backend/src/graphql/resolver/ContributionMessageResolver.test.ts
Normal file
@ -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',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
84
backend/src/graphql/resolver/ContributionMessageResolver.ts
Normal file
84
backend/src/graphql/resolver/ContributionMessageResolver.ts
Normal file
@ -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<ContributionMessage> {
|
||||||
|
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<ContributionMessageListResult> {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,7 +11,6 @@ import { ContributionType } from '@enum/ContributionType'
|
|||||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||||
import { Contribution, ContributionListResult } from '@model/Contribution'
|
import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||||
import { User } from '@model/User'
|
|
||||||
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
|
|
||||||
@ -90,19 +89,23 @@ export class ContributionResolver {
|
|||||||
userId: number
|
userId: number
|
||||||
confirmedBy?: FindOperator<number> | null
|
confirmedBy?: FindOperator<number> | null
|
||||||
} = { userId: user.id }
|
} = { userId: user.id }
|
||||||
|
|
||||||
if (filterConfirmed) where.confirmedBy = IsNull()
|
if (filterConfirmed) where.confirmedBy = IsNull()
|
||||||
const [contributions, count] = await dbContribution.findAndCount({
|
|
||||||
where,
|
const [contributions, count] = await getConnection()
|
||||||
order: {
|
.createQueryBuilder()
|
||||||
createdAt: order,
|
.select('c')
|
||||||
},
|
.from(dbContribution, 'c')
|
||||||
withDeleted: true,
|
.leftJoinAndSelect('c.messages', 'm')
|
||||||
skip: (currentPage - 1) * pageSize,
|
.where(where)
|
||||||
take: pageSize,
|
.orderBy('c.createdAt', order)
|
||||||
})
|
.limit(pageSize)
|
||||||
|
.offset((currentPage - 1) * pageSize)
|
||||||
|
.getManyAndCount()
|
||||||
|
|
||||||
return new ContributionListResult(
|
return new ContributionListResult(
|
||||||
count,
|
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()
|
.getManyAndCount()
|
||||||
return new ContributionListResult(
|
return new ContributionListResult(
|
||||||
count,
|
count,
|
||||||
dbContributions.map(
|
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||||
(contribution) => new Contribution(contribution, new User(contribution.user)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -261,3 +261,31 @@ export const deleteContribution = gql`
|
|||||||
deleteContribution(id: $id)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm'
|
} from 'typeorm'
|
||||||
import { Contribution } from '../Contribution'
|
import { Contribution } from '../Contribution'
|
||||||
|
import { User } from '../User'
|
||||||
|
|
||||||
@Entity('contribution_messages', {
|
@Entity('contribution_messages', {
|
||||||
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
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 })
|
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||||
userId: number
|
userId: number
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.messages)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User
|
||||||
|
|
||||||
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
message: string
|
message: string
|
||||||
|
|
||||||
|
|||||||
116
database/entity/0047-messages_tables/User.ts
Normal file
116
database/entity/0047-messages_tables/User.ts
Normal file
@ -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[]
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { User } from './0046-adapt_users_table_for_gradidoid/User'
|
export { User } from './0047-messages_tables/User'
|
||||||
|
|||||||
@ -206,6 +206,8 @@ export const listContributions = gql`
|
|||||||
confirmedAt
|
confirmedAt
|
||||||
confirmedBy
|
confirmedBy
|
||||||
deletedAt
|
deletedAt
|
||||||
|
state
|
||||||
|
messagesCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user