Skip to content

Commit 2374b94

Browse files
committed
fix: skip timestamp check for same user
1 parent df565c7 commit 2374b94

File tree

7 files changed

+78
-37
lines changed

7 files changed

+78
-37
lines changed

packages/backend/src/graphql/__tests__/mutations/create-step.itest.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('createStep mutation integration tests', async () => {
6767
testFlow = await testUser.$relatedQuery('flows').insertAndFetch({
6868
name: 'Test Flow',
6969
// additional flow properties as needed
70+
updatedBy: owner.id,
7071
})
7172
testFlowTimestampString = String(new Date(testFlow.updatedAt).getTime())
7273

@@ -448,7 +449,7 @@ describe('createStep mutation integration tests', async () => {
448449
expect((newStep as any).connectionId).toBeNull()
449450
})
450451

451-
describe('updatedAt validation', () => {
452+
describe('updatedAt and updatedBy validation', () => {
452453
it('should succeed when input.flow.updatedAt matches flow.updatedAt (timestamp string)', async () => {
453454
const params = {
454455
input: {
@@ -469,7 +470,7 @@ describe('createStep mutation integration tests', async () => {
469470
expect(newStep.key).toBe('newStep')
470471
})
471472

472-
it('should throw when input.flow.updatedAt is different from flow.updatedAt (timestamp string)', async () => {
473+
it('should succeed when input.flow.updatedAt is different from flow.updatedAt for same user', async () => {
473474
const futureTimestamp = (
474475
new Date(testFlow.updatedAt).getTime() + 1000
475476
).toString()
@@ -486,23 +487,22 @@ describe('createStep mutation integration tests', async () => {
486487
},
487488
}
488489

489-
await expect(createStep(null, params, context)).rejects.toThrow(
490-
BadUserInputError,
491-
)
492-
await expect(createStep(null, params, context)).rejects.toThrow(
493-
REFRESH_PIPE_MESSAGE,
494-
)
490+
const newStep = await createStep(null, params, context)
491+
492+
expect(newStep).toBeDefined()
493+
expect(newStep.key).toBe('newStep')
495494
})
496495

497-
it('should throw when input.flow.updatedAt is different from flow.updatedAt (unix timestamp string)', async () => {
498-
const futureUnixTimestamp = Math.floor(
499-
(new Date(testFlow.updatedAt).getTime() + 1000) / 1000,
496+
it('should throw when input.flow.updatedAt is different from flow.updatedAt for different user', async () => {
497+
const futureTimestamp = (
498+
new Date(testFlow.updatedAt).getTime() + 1000
500499
).toString()
500+
context.currentUser = editor
501501
const params = {
502502
input: {
503503
flow: {
504504
id: testFlow.id,
505-
updatedAt: futureUnixTimestamp,
505+
updatedAt: futureTimestamp,
506506
},
507507
previousStep: { id: existingSteps[0].id },
508508
key: 'newStep',
@@ -519,10 +519,11 @@ describe('createStep mutation integration tests', async () => {
519519
)
520520
})
521521

522-
it('should throw when input.flow.updatedAt is older than flow.updatedAt (timestamp string)', async () => {
522+
it('should throw when input.flow.updatedAt is older than flow.updatedAt for different user', async () => {
523523
const oldTimestamp = (
524524
new Date(testFlow.updatedAt).getTime() - 5000
525525
).toString()
526+
context.currentUser = editor
526527
const params = {
527528
input: {
528529
flow: {
@@ -544,7 +545,7 @@ describe('createStep mutation integration tests', async () => {
544545
)
545546
})
546547

547-
it('should throw when input.flow.updatedAt is an invalid timestamp string', async () => {
548+
it('should not throw when input.flow.updatedAt is an invalid timestamp string', async () => {
548549
const params = {
549550
input: {
550551
flow: {
@@ -558,12 +559,10 @@ describe('createStep mutation integration tests', async () => {
558559
},
559560
}
560561

561-
await expect(createStep(null, params, context)).rejects.toThrow(
562-
BadUserInputError,
563-
)
564-
await expect(createStep(null, params, context)).rejects.toThrow(
565-
REFRESH_PIPE_MESSAGE,
566-
)
562+
const newStep = await createStep(null, params, context)
563+
564+
expect(newStep).toBeDefined()
565+
expect(newStep.key).toBe('newStep')
567566
})
568567
})
569568
})

packages/backend/src/graphql/mutations/create-step.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const createStep: MutationResolvers['createStep'] = async (
2424
})
2525
.throwIfNotFound()
2626

27-
flow.assertNotUpdatedSince(input.flow.updatedAt)
27+
flow.assertNotUpdatedSince(input.flow.updatedAt, context.currentUser.id)
2828

2929
// if connectionId is specified, verify that the connection exists
3030
// and the user has the appropriate permissions to use it
@@ -84,7 +84,11 @@ const createStep: MutationResolvers['createStep'] = async (
8484
})
8585
}
8686

87-
const updatedFlow = await flow.patchLastUpdated({ flowId: flow.id, trx })
87+
const updatedFlow = await flow.patchLastUpdated({
88+
flowId: flow.id,
89+
updatedBy: context.currentUser.id,
90+
trx,
91+
})
8892

8993
return {
9094
...step,

packages/backend/src/graphql/mutations/delete-step.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const deleteStep: MutationResolvers['deleteStep'] = async (
3838
})
3939

4040
const flow = steps[0].flow
41-
flow.assertNotUpdatedSince(input.flow.updatedAt)
41+
flow.assertNotUpdatedSince(input.flow.updatedAt, context.currentUser.id)
4242

4343
if (!steps.every((step) => step.flowId === steps[0].flowId)) {
4444
throw new Error('All steps to be deleted must be from the same pipe!')
@@ -114,7 +114,11 @@ const deleteStep: MutationResolvers['deleteStep'] = async (
114114
.patch({ position: raw(`position - ${steps.length}`) })
115115
}
116116

117-
await flow.patchLastUpdated({ flowId: flow.id, trx })
117+
await flow.patchLastUpdated({
118+
flowId: flow.id,
119+
updatedBy: context.currentUser.id,
120+
trx,
121+
})
118122

119123
return await flow
120124
.$query(trx)

packages/backend/src/graphql/mutations/update-step.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ const updateStep: MutationResolvers['updateStep'] = async (
2828
throw new BadUserInputError('Step not found')
2929
}
3030

31-
step.flow.assertNotUpdatedSince(input.flow.updatedAt)
31+
step.flow.assertNotUpdatedSince(
32+
input.flow.updatedAt,
33+
context.currentUser.id,
34+
)
3235

3336
if (input.connection.id) {
3437
// if connectionId is specified, verify that the connection exists
@@ -121,11 +124,13 @@ const updateStep: MutationResolvers['updateStep'] = async (
121124
// update the flow's last updated
122125
const updatedFlow = await step.flow.patchLastUpdated({
123126
flowId: step.flowId,
127+
updatedBy: context.currentUser.id,
124128
trx,
125129
})
126130

127131
// do this instead of spreading the updatedStep so that it preserves the Model instance
128132
updatedStep.flow.updatedAt = updatedFlow.updatedAt
133+
updatedStep.flow.updatedBy = context.currentUser.id
129134
return updatedStep
130135
})
131136

packages/backend/src/models/__tests__/flow.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { randomUUID } from 'crypto'
12
import { describe, expect, it } from 'vitest'
23

34
import { BadUserInputError } from '@/errors/graphql-errors/bad-user-input'
45
import Flow from '@/models/flow'
56

67
describe('flow model', () => {
8+
const updatedBy = randomUUID()
9+
710
describe('assertNotUpdatedSince', () => {
811
it('should not throw when timestamps match', () => {
912
const flow = new Flow()
@@ -13,7 +16,9 @@ describe('flow model', () => {
1316

1417
const clientUpdatedAt = String(updatedAt.getTime())
1518

16-
expect(() => flow.assertNotUpdatedSince(clientUpdatedAt)).not.toThrow()
19+
expect(() =>
20+
flow.assertNotUpdatedSince(clientUpdatedAt, updatedBy),
21+
).not.toThrow()
1722
})
1823

1924
it('should throw BadUserInputError when timestamps mismatch', () => {
@@ -24,10 +29,10 @@ describe('flow model', () => {
2429

2530
const mismatched = String(updatedAt.getTime() + 1)
2631

27-
expect(() => flow.assertNotUpdatedSince(mismatched)).toThrowError(
28-
BadUserInputError,
29-
)
30-
expect(() => flow.assertNotUpdatedSince(mismatched)).toThrow(
32+
expect(() =>
33+
flow.assertNotUpdatedSince(mismatched, updatedBy),
34+
).toThrowError(BadUserInputError)
35+
expect(() => flow.assertNotUpdatedSince(mismatched, updatedBy)).toThrow(
3136
'This Pipe has been edited by another user. Please refresh the page to see the latest changes and try again.',
3237
)
3338
})
@@ -38,10 +43,12 @@ describe('flow model', () => {
3843

3944
flow.updatedAt = updatedAt.toISOString()
4045

41-
expect(() => flow.assertNotUpdatedSince('not-a-number')).toThrowError(
42-
BadUserInputError,
43-
)
44-
expect(() => flow.assertNotUpdatedSince('not-a-number')).toThrow(
46+
expect(() =>
47+
flow.assertNotUpdatedSince('not-a-number', updatedBy),
48+
).toThrowError(BadUserInputError)
49+
expect(() =>
50+
flow.assertNotUpdatedSince('not-a-number', updatedBy),
51+
).toThrow(
4552
'This Pipe has been edited by another user. Please refresh the page to see the latest changes and try again.',
4653
)
4754
})

packages/backend/src/models/__tests__/step.itest.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Step from '@/models/step'
55

66
import Execution from '../execution'
77
import Flow from '../flow'
8+
import User from '../user'
89

910
const flowId = randomUUID()
1011
const stepId = randomUUID()
@@ -15,9 +16,14 @@ describe('step model', () => {
1516
let step: Step
1617

1718
beforeEach(async () => {
19+
const user = await User.query().insert({
20+
id: randomUUID(),
21+
22+
})
1823
await Flow.query().insert({
1924
id: flowId,
2025
name: 'test flow',
26+
userId: user.id,
2127
})
2228
step = await Step.query()
2329
.insert({
@@ -171,15 +177,19 @@ describe('step model', () => {
171177
it('should patch the flow last updated', async () => {
172178
const flow = await step.$relatedQuery('flow')
173179
const originalUpdatedAt = flow.updatedAt
174-
175-
await flow.patchLastUpdated({ flowId: flow.id })
180+
const updatedBy = flow.userId
181+
await flow.patchLastUpdated({
182+
flowId: flow.id,
183+
updatedBy,
184+
})
176185

177186
const updatedFlow = await step.$relatedQuery('flow')
178187

179188
expect(updatedFlow.updatedAt).toBeDefined()
180189
expect(new Date(updatedFlow.updatedAt).getTime()).toBeGreaterThan(
181190
new Date(originalUpdatedAt).getTime(),
182191
)
192+
expect(updatedFlow.updatedBy).toBe(updatedBy)
183193
})
184194
})
185195
})

packages/backend/src/models/flow.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Flow extends Base {
5050
userId: { type: 'string', format: 'uuid' },
5151
remoteWebhookId: { type: 'string' },
5252
active: { type: 'boolean' },
53+
updatedBy: { type: 'string', format: 'uuid' },
5354

5455
config: {
5556
type: 'object',
@@ -251,19 +252,30 @@ class Flow extends Base {
251252

252253
async patchLastUpdated({
253254
flowId,
255+
updatedBy,
254256
trx,
255257
}: {
256258
flowId: string
259+
updatedBy: string
257260
trx?: Transaction
258261
}) {
259262
return await this.$query(trx).patchAndFetchById(flowId, {
260263
updatedAt: new Date().toISOString(),
264+
updatedBy,
261265
})
262266
}
263267

264-
assertNotUpdatedSince(clientUpdatedAt: string) {
268+
assertNotUpdatedSince(clientUpdatedAt: string, updatedBy: string) {
265269
const inputTimestamp = Number(clientUpdatedAt)
266270
const flowTimestamp = new Date(this.updatedAt).getTime()
271+
const flowLastUpdatedBy = this.updatedBy
272+
273+
// return early if the flow was last updated by the same user
274+
// avoid potential issues where its a valid update by the same user
275+
// but the client timestamp is not updated due to race condition or network issues
276+
if (flowLastUpdatedBy === updatedBy) {
277+
return true
278+
}
267279

268280
if (isNaN(inputTimestamp) || inputTimestamp !== flowTimestamp) {
269281
throw new BadUserInputError(

0 commit comments

Comments
 (0)