Skip to content

Commit 8b56f58

Browse files
committed
feat: calculate new step position for reject branch
1 parent 793bdda commit 8b56f58

File tree

3 files changed

+143
-44
lines changed

3 files changed

+143
-44
lines changed

packages/backend/src/apps/formsg/common/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ export interface ParsedMrfWorkflow {
5050
trigger: Omit<ParsedMrfWorkflowStep, 'approvalField'>
5151
actions: ParsedMrfWorkflowStep[]
5252
}
53+
54+
export const stepApprovalConfigSchema = z.object({
55+
branch: z.enum(['approve', 'reject']),
56+
stepId: z.string().uuid(),
57+
})

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,26 @@ const createStep: MutationResolvers['createStep'] = async (
3131
throw new BadUserInputError('Action can only be created by system')
3232
}
3333
}
34-
return await Step.transaction(async (trx) => {
35-
await trx.raw('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;')
36-
37-
if (input.connection?.id) {
38-
// if connectionId is specified, verify that the connection exists and belongs to the user
39-
const connection = await context.currentUser
40-
.$relatedQuery('connections')
41-
.findOne({ id: input.connection.id })
42-
if (!connection) {
43-
throw new BadUserInputError('Connection not found')
44-
}
34+
if (input.connection?.id) {
35+
// if connectionId is specified, verify that the connection exists and belongs to the user
36+
const connection = await context.currentUser
37+
.$relatedQuery('connections')
38+
.findOne({ id: input.connection.id })
39+
if (!connection) {
40+
throw new BadUserInputError('Connection not found')
4541
}
42+
}
4643

47-
// Put SELECTs in transaction just in case there's concurrent modification.
48-
const flow = await context.currentUser
49-
.$relatedQuery('flows', trx)
50-
.findOne({
51-
id: input.flow.id,
52-
})
53-
.throwIfNotFound()
44+
// Put SELECTs in transaction just in case there's concurrent modification.
45+
const flow = await context.currentUser
46+
.$relatedQuery('flows')
47+
.findOne({
48+
id: input.flow.id,
49+
})
50+
.throwIfNotFound()
51+
52+
return await Step.transaction(async (trx) => {
53+
await trx.raw('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;')
5454

5555
const previousStep = await flow
5656
.$relatedQuery('steps', trx)
@@ -59,11 +59,11 @@ const createStep: MutationResolvers['createStep'] = async (
5959
})
6060
.throwIfNotFound()
6161

62-
const isApprovalConfigValid = validateApprovalConfig(
62+
const validationResult = await validateApprovalConfig(
6363
input.config,
6464
previousStep,
6565
)
66-
if (!isApprovalConfigValid) {
66+
if (!validationResult.isApprovalConfigValid) {
6767
throw new BadUserInputError('Invalid approval config')
6868
}
6969

@@ -72,13 +72,13 @@ const createStep: MutationResolvers['createStep'] = async (
7272
.patch({
7373
position: raw(`position + 1`),
7474
})
75-
.where('position', '>=', previousStep.position + 1)
75+
.where('position', '>=', validationResult.newStepPosition)
7676

7777
const step = await flow.$relatedQuery('steps', trx).insertAndFetch({
7878
key: input.key,
7979
appKey: input.appKey,
8080
type: 'action',
81-
position: previousStep.position + 1,
81+
position: validationResult.newStepPosition,
8282
parameters: input.parameters,
8383
connectionId: input.connection?.id,
8484
config: input.config,

packages/backend/src/helpers/validate-approval-config.ts

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,136 @@ import { IStep, IStepConfig } from '@plumber/types'
22

33
import get from 'lodash.get'
44

5-
export function validateApprovalConfig(config: IStepConfig, prevStep: IStep) {
5+
import { parsedMrfWorkflowStepSchema } from '@/apps/formsg/common/types'
6+
import Step from '@/models/step'
7+
8+
/**
9+
* TODO: write unit test for this function
10+
*/
11+
export async function validateApprovalConfig(
12+
config: IStepConfig,
13+
prevStep: IStep,
14+
): Promise<
15+
| {
16+
isApprovalConfigValid: true
17+
newStepPosition: number
18+
}
19+
| {
20+
isApprovalConfigValid: false
21+
}
22+
> {
623
/**
7-
* if approval config is not present, or is a trigger, return true
24+
* Case 1: new step is a trigger step, return early
825
*/
9-
if (!config?.approval || !prevStep) {
10-
return true
26+
if (!prevStep) {
27+
return {
28+
isApprovalConfigValid: true,
29+
newStepPosition: 1,
30+
}
1131
}
1232

33+
const isPreviousStepMrfApprovalStep =
34+
prevStep.appKey === 'formsg' &&
35+
prevStep.key === 'mrfSubmission' &&
36+
// if no approval field, it's just a normal mrf step
37+
!!get(prevStep.parameters, 'mrf.approvalField')
38+
1339
/**
14-
* Both approval branch and approval step id must come together
40+
* Case 2: Prev step has no approval config and is not an mrf approval step,
41+
* current step should have no approval config either
1542
*/
16-
if (!(config?.approval?.branch && config?.approval?.stepId)) {
17-
return false
43+
if (!prevStep.config.approval && !isPreviousStepMrfApprovalStep) {
44+
if (!config?.approval) {
45+
return {
46+
isApprovalConfigValid: true,
47+
newStepPosition: prevStep.position + 1,
48+
}
49+
}
50+
return {
51+
isApprovalConfigValid: false,
52+
}
1853
}
1954

2055
/**
21-
* If previous step an approval step return true
56+
* Case 3: Prev step is part of an approval branch, check that
57+
* the new step has the same approval config
2258
*/
23-
if (
24-
prevStep.id === config.approval?.stepId &&
25-
prevStep.appKey === 'formsg' &&
26-
prevStep.key === 'mrfSubmission' &&
27-
!!get(prevStep.parameters, 'mrf.approvalField')
28-
) {
29-
return true
59+
if (prevStep.config.approval) {
60+
const isSameApprovalConfig =
61+
prevStep.config.approval?.branch === config?.approval?.branch &&
62+
prevStep.config.approval?.stepId === config?.approval?.stepId
63+
if (isSameApprovalConfig) {
64+
return {
65+
isApprovalConfigValid: true,
66+
newStepPosition: prevStep.position + 1,
67+
}
68+
}
69+
return {
70+
isApprovalConfigValid: false,
71+
}
3072
}
3173

3274
/**
33-
* If previous step has the same approval branch and step id, return true
75+
* Case 4: Previous step is an approval step
3476
*/
35-
if (
36-
prevStep.config.approval?.branch === config.approval?.branch &&
37-
prevStep.config.approval?.stepId === config.approval?.stepId
38-
) {
39-
return true
77+
if (isPreviousStepMrfApprovalStep) {
78+
// validate approval config
79+
const { success } = parsedMrfWorkflowStepSchema.safeParse(config?.approval)
80+
// validate approval config step id is the same as prev step id
81+
if (!success || config.approval.stepId !== prevStep.id) {
82+
return {
83+
isApprovalConfigValid: false,
84+
}
85+
}
86+
/**
87+
* If approval branch, simply add 1 to the prev step position
88+
*/
89+
if (config.approval.branch === 'approve') {
90+
return {
91+
isApprovalConfigValid: true,
92+
newStepPosition: prevStep.position + 1,
93+
}
94+
}
95+
// If reject branch, find the end of the approval branch and add one to that step
96+
// first find the next mrf step (if any)
97+
const nextMrfStep = await Step.query()
98+
.where('flow_id', prevStep.flowId)
99+
.andWhere('type', 'action')
100+
.andWhere('key', 'mrfSubmission')
101+
.andWhere('position', '>', prevStep.position)
102+
.orderBy('position', 'asc')
103+
.first()
104+
// then find the last approval step in the curr branch (if any)
105+
const lastApprovalStepQuery = Step.query()
106+
.where('flow_id', prevStep.flowId)
107+
.andWhere('position', '>', prevStep.position)
108+
.andWhere('config.approval.branch', 'approve')
109+
.orderBy('position', 'desc')
110+
.first()
111+
112+
if (nextMrfStep) {
113+
lastApprovalStepQuery.andWhere('position', '<', nextMrfStep.position)
114+
}
115+
const lastApprovalStep = await lastApprovalStepQuery.first()
116+
// If last approval step exists, return the position after that
117+
if (lastApprovalStep) {
118+
return {
119+
isApprovalConfigValid: true,
120+
newStepPosition: lastApprovalStep.position + 1,
121+
}
122+
} else {
123+
// If no last approval step, return the position after the previous step
124+
return {
125+
isApprovalConfigValid: true,
126+
newStepPosition: prevStep.position + 1,
127+
}
128+
}
40129
}
41130

42-
return false
131+
/**
132+
* Catch all branch, something seems fishy
133+
*/
134+
return {
135+
isApprovalConfigValid: false,
136+
}
43137
}

0 commit comments

Comments
 (0)