Put message creation in a transaction with file uploads to avoid empty messages (#8694)

Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
Max 2025-06-20 20:32:00 +02:00 committed by GitHub
parent 4a42d22692
commit cc96698300
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 83 additions and 42 deletions

View File

@ -283,6 +283,55 @@ describe('Message', () => {
})
})
describe('user sends file, but upload goes wrong', () => {
const file1 = Readable.from('file1')
const upload1 = new Upload()
upload1.resolve({
createReadStream: () => file1,
stream: file1,
filename: 'file1',
encoding: '7bit',
mimetype: 'application/json',
})
const upload2 = new Upload()
upload2.resolve(new Error('Upload failed'))
it('no message is created', async () => {
await expect(
mutate({
mutation: CreateMessage,
variables: {
roomId,
content: 'A message which should not be created',
files: [
{ upload: upload1, name: 'test1', type: 'application/json' },
{ upload: upload2, name: 'test2', type: 'image/png' },
],
},
}),
).resolves.toMatchObject({
errors: {},
data: {
CreateMessage: null,
},
})
await expect(
query({
query: Message,
variables: {
roomId,
},
}),
).resolves.toMatchObject({
errors: undefined,
data: {
Message: [],
},
})
})
})
describe('user does not chat in room', () => {
beforeEach(async () => {
authenticatedUser = await notChattingUser.toJson()

View File

@ -76,9 +76,12 @@ export default {
const {
user: { id: currentUserId },
} = context
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const createMessageCypher = `
try {
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const createMessageCypher = `
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image)
OPTIONAL MATCH (m:Message)-[:INSIDE]->(room)
@ -105,53 +108,42 @@ export default {
date: message.createdAt
}
`
const createMessageTxResponse = await transaction.run(createMessageCypher, {
currentUserId,
roomId,
content,
})
const createMessageTxResponse = await transaction.run(createMessageCypher, {
currentUserId,
roomId,
content,
})
const [message] = await createMessageTxResponse.records.map((record) =>
record.get('message'),
)
const [message] = await createMessageTxResponse.records.map((record) =>
record.get('message'),
)
return message
})
try {
// We cannot combine the query above with the attachments, since you need the resource for matching
const message = await writeTxResultPromise
// this is the case if the room doesn't exist - requires refactoring for implicit rooms
if (!message) {
return null
}
const session = context.driver.session()
const writeFilesPromise = session.writeTransaction(async (transaction) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const atns: any[] = []
if (!isS3configured(CONFIG)) {
return atns
// this is the case if the room doesn't exist - requires refactoring for implicit rooms
if (!message) {
return null
}
for await (const file of files) {
const atn = await attachments(CONFIG).add(
message,
'ATTACHMENT',
file,
{},
{
transaction,
},
)
atns.push(atn)
const atns: File[] = []
if (isS3configured(CONFIG)) {
for await (const file of files) {
const atn = await attachments(CONFIG).add(
message,
'ATTACHMENT',
file,
{},
{
transaction,
},
)
atns.push(atn)
}
}
return atns
return { ...message, files: atns }
})
const atns = await writeFilesPromise
return { ...message, files: atns }
return await writeTxResultPromise
} catch (error) {
throw new Error(error)
} finally {