Skip to content

Commit 9ac97bd

Browse files
committed
feat: testStep for mrf actions should test multiple steps
1 parent 6410a36 commit 9ac97bd

File tree

12 files changed

+149
-53
lines changed

12 files changed

+149
-53
lines changed

packages/backend/src/apps/formsg/actions/mrf-submission/index.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { IGlobalVariable, IRawAction } from '@plumber/types'
1+
import type { IGlobalVariable, IRawAction } from '@plumber/types'
22

3-
import getDataOutMetadata from '../../triggers/new-submission/get-data-out-metadata'
3+
import StepError from '@/errors/step'
4+
import ExecutionStep from '@/models/execution-step'
5+
import Step from '@/models/step'
6+
7+
import getDataOutMetadata from '../../common/get-data-out-metadata'
8+
import {
9+
type ParsedMrfWorkflowStep,
10+
parsedMrfWorkflowStepSchema,
11+
} from '../../common/types'
412

513
const action: IRawAction = {
614
name: 'New form response',
@@ -10,8 +18,45 @@ const action: IRawAction = {
1018
'This is a hidden action that signifies a subsequent MRF submission',
1119
getDataOutMetadata,
1220

13-
async testRun(_$: IGlobalVariable) {
14-
// TODO: this should run testRun for the trigger execution step
21+
async testRun($: IGlobalVariable) {
22+
// Gets the execution step of the trigger
23+
const { mrf } = $.step.parameters as unknown as {
24+
mrf: ParsedMrfWorkflowStep
25+
}
26+
27+
if (!mrf || parsedMrfWorkflowStepSchema.safeParse(mrf).success === false) {
28+
throw new StepError(
29+
'Misconfigured MRF step',
30+
'Reconnect your MRF form and try again.',
31+
$.step.position,
32+
$.app.name,
33+
)
34+
}
35+
36+
const triggerStep = await Step.query()
37+
.findOne({
38+
flow_id: $.flow.id,
39+
type: 'trigger',
40+
key: 'newSubmission',
41+
app_key: 'formsg',
42+
})
43+
.throwIfNotFound()
44+
const triggerExecutionStep = await ExecutionStep.query()
45+
.where('step_id', triggerStep.id)
46+
.andWhere('execution_id', $.execution.id)
47+
.first()
48+
49+
if (!triggerExecutionStep) {
50+
$.setActionItem({
51+
raw: null,
52+
})
53+
return
54+
}
55+
56+
$.setActionItem({
57+
raw: triggerExecutionStep.dataOut,
58+
meta: triggerExecutionStep.metadata,
59+
})
1560
},
1661
}
1762

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import logger from '@/helpers/logger'
1111
import { parseS3Id } from '@/helpers/s3'
1212

13-
import { ADDRESS_LABELS } from '../../common/constants'
13+
import { ADDRESS_LABELS } from './constants'
1414

1515
function buildQuestionMetadatum(fieldData: IJSONObject): IDataOutMetadatum {
1616
const question: IDataOutMetadatum = {
@@ -322,6 +322,7 @@ async function getDataOutMetadata(
322322
executionStep: IExecutionStep,
323323
): Promise<IDataOutMetadata> {
324324
const data = executionStep.dataOut
325+
325326
if (!data || !data.fields) {
326327
return null
327328
}

packages/backend/src/apps/formsg/common/process-table-field.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FOR_EACH_INPUT_SOURCE } from '@/apps/toolbox/common/constants'
22

3-
import { extractLastTopLevelBracketContent } from '../triggers/new-submission/get-data-out-metadata'
3+
import { extractLastTopLevelBracketContent } from './get-data-out-metadata'
44

55
type TableColumn = {
66
id: string

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ export interface FormSchema {
3636
* This will be stored in the step's parameters and
3737
* not modifiable by the user
3838
*/
39-
export interface ParsedMrfWorkflowStep {
40-
defaultStepName: string
41-
formWorkflowStepId: string
42-
type: 'static' | 'dynamic' | 'conditional'
43-
fields?: string[]
44-
approvalField?: string
45-
}
39+
export const parsedMrfWorkflowStepSchema = z.object({
40+
defaultStepName: z.string(),
41+
formWorkflowStepId: z.string(),
42+
type: z.enum(['static', 'dynamic', 'conditional']),
43+
fields: z.array(z.string()),
44+
approvalField: z.string().optional(),
45+
})
46+
47+
export type ParsedMrfWorkflowStep = z.infer<typeof parsedMrfWorkflowStepSchema>
4648

4749
export interface ParsedMrfWorkflow {
4850
trigger: Omit<ParsedMrfWorkflowStep, 'approvalField'>

packages/backend/src/apps/formsg/triggers/new-submission/create-mrf-steps.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ export async function createMrfSteps(
1919
await Step.transaction(async (trx) => {
2020
const triggerStepName = `MRF: ${trigger.defaultStepName}`
2121
// Update the trigger step parameters
22-
await Step.query(trx).patchAndFetchById($.step.id, {
23-
parameters: {
24-
mrf: trigger,
22+
const updatedTriggerStep = await Step.query(trx).patchAndFetchById(
23+
$.step.id,
24+
{
25+
parameters: {
26+
mrf: trigger,
27+
},
28+
config: raw(
29+
`jsonb_set(config, '{stepName}', to_jsonb(?::text), true)`,
30+
[triggerStepName],
31+
),
2532
},
26-
config: raw(`jsonb_set(config, '{stepName}', to_jsonb(?::text), true)`, [
27-
triggerStepName,
28-
]),
29-
})
33+
)
3034

3135
const existingMrfSteps = await Step.query(trx)
3236
.where('flow_id', $.flow.id)
@@ -84,6 +88,7 @@ export async function createMrfSteps(
8488
const updatedStep = await Step.query(trx).patchAndFetchById(
8589
existingStep.id,
8690
{
91+
connectionId: updatedTriggerStep.connectionId,
8792
parameters,
8893
config: raw(
8994
`jsonb_set(config, '{stepName}', to_jsonb(?::text), true)`,
@@ -106,6 +111,7 @@ export async function createMrfSteps(
106111
appKey: MRF_APP_KEY,
107112
key: MRF_KEY,
108113
position: newStepPosition,
114+
connectionId: updatedTriggerStep.connectionId,
109115
parameters,
110116
config: {
111117
stepName,

packages/backend/src/apps/formsg/triggers/new-submission/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { z } from 'zod'
66
import StepError from '@/errors/step'
77
import ExecutionStep from '@/models/execution-step'
88

9+
import getDataOutMetadata from '../../common/get-data-out-metadata'
910
import { getFormDetailsFromGlobalVariable } from '../../common/webhook-settings'
1011

1112
import { createMrfSteps } from './create-mrf-steps'
1213
import { fetchFormSchema } from './fetch-form-schema'
13-
import getDataOutMetadata from './get-data-out-metadata'
1414
import getMockData from './get-mock-data'
1515
import { parseWorkflowData } from './get-workflow-data'
1616

@@ -36,7 +36,6 @@ const trigger: IRawTrigger = {
3636
hideWebhookUrl: true,
3737
errorMsg:
3838
'Make a new submission to the form you connected and test the step again.',
39-
mockDataMsg: 'The mock responses below are based on your form fields.',
4039
},
4140
arguments: [
4241
{
@@ -112,6 +111,7 @@ const trigger: IRawTrigger = {
112111
const formSchema = await fetchFormSchema($, formId)
113112

114113
if (formSchema.form.responseMode === 'multirespondent') {
114+
// Create MRF steps for multirespondent forms
115115
const mrfWorkflowData = await parseWorkflowData($, formSchema)
116116
await createMrfSteps($, mrfWorkflowData)
117117
}

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

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { raw } from 'objection'
22

3+
import Flow from '@/models/flow'
4+
import Step from '@/models/step'
35
import testStep from '@/services/test-step'
46

57
import type { MutationResolvers } from '../__generated__/types.generated'
@@ -12,29 +14,84 @@ const executeStep: MutationResolvers['executeStep'] = async (
1214
const { stepId, testRunMetadata } = params.input
1315

1416
// Just checking for permissions here
15-
const stepToTest = await context.currentUser
17+
let stepToTest = await context.currentUser
1618
.withAccessibleSteps({ requiredRole: 'editor' })
1719
.withGraphFetched('flow')
1820
.findById(stepId)
1921
.throwIfNotFound()
2022

23+
const isMrf = stepToTest.appKey === 'formsg' && stepToTest.parameters.mrf
24+
25+
/**
26+
* If it is an MRF step, we need to test all steps starting from the trigger step
27+
* regardless of whether the trigger or action is being checked
28+
*/
29+
if (isMrf) {
30+
const mrfSteps = await Step.query()
31+
.where('app_key', 'formsg')
32+
.andWhere('flow_id', stepToTest.flowId)
33+
.orderBy('position', 'asc')
34+
.limit(1)
35+
36+
if (mrfSteps.length === 1) {
37+
stepToTest = mrfSteps[0]
38+
}
39+
}
40+
2141
const { executionStep, executionId } = await testStep({
2242
stepId: stepToTest.id,
2343
testRunMetadata,
2444
})
2545

26-
// Update flow to use the new test execution
27-
await stepToTest.flow.$query().patch({
46+
await Flow.query().patchAndFetchById(stepToTest.flowId, {
2847
testExecutionId: executionId,
2948
})
3049

31-
// Update step status
50+
let shouldContinueTestingMrfSteps = false
51+
3252
if (!executionStep.isFailed) {
33-
await stepToTest.$query().patch({
53+
const updatedStep = await stepToTest.$query().patchAndFetch({
54+
// Update step status
3455
status: 'completed',
3556
// clear templateConfig in config when step is tested successfully
3657
config: raw(`config - 'templateConfig'`),
3758
})
59+
60+
// we check if the step is an MRF step again after testing
61+
shouldContinueTestingMrfSteps = !!updatedStep.parameters.mrf
62+
} else {
63+
return executionStep
64+
}
65+
66+
if (!shouldContinueTestingMrfSteps) {
67+
return executionStep
68+
}
69+
70+
/**
71+
* Test remaining steps in the mrf flow
72+
*/
73+
const remainingSteps = await Step.query()
74+
.where('app_key', 'formsg')
75+
.andWhere('flow_id', stepToTest.flowId)
76+
.andWhere('type', 'action')
77+
.orderBy('position', 'asc')
78+
79+
for (const remainingStep of remainingSteps) {
80+
const { executionStep } = await testStep({
81+
stepId: remainingStep.id,
82+
testRunMetadata,
83+
})
84+
85+
if (!executionStep.isFailed) {
86+
await remainingStep.$query().patch({
87+
// Update step status
88+
status: 'completed',
89+
// clear templateConfig in config when step is tested successfully
90+
config: raw(`config - 'templateConfig'`),
91+
})
92+
} else {
93+
break
94+
}
3895
}
3996

4097
return executionStep

packages/backend/src/graphql/schema.graphql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,6 @@ type TriggerInstructions {
759759
afterUrlMsg: String
760760
hideWebhookUrl: Boolean
761761
errorMsg: String
762-
mockDataMsg: String
763762
}
764763

765764
type StepTransferDetails {

packages/backend/src/services/action.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export const processAction = async (options: ProcessActionOptions) => {
170170
: await actionCommand.run($, metadata)
171171
if (result) {
172172
runResult = result
173+
metadata
173174
}
174175
} catch (error) {
175176
executionError = error
@@ -224,7 +225,7 @@ export const processAction = async (options: ProcessActionOptions) => {
224225
appKey: $.app.key,
225226
jobId,
226227
key: step.key,
227-
metadata,
228+
metadata: { ...metadata, ...$.actionOutput.data?.meta },
228229
})
229230

230231
// TODO on Oct: remove this once the logging is not needed anymore

packages/frontend/src/components/FlowStepTestController/TestResult.tsx

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,6 @@ function getNoOutputMessage(
2525
return selectedActionOrTrigger?.webhookTriggerInstructions?.errorMsg ?? null
2626
}
2727

28-
function getMockDataMessage(
29-
selectedActionOrTrigger: TestResultsProps['selectedActionOrTrigger'],
30-
): string | null {
31-
// Type guard for ITrigger
32-
if (
33-
!selectedActionOrTrigger ||
34-
!('webhookTriggerInstructions' in selectedActionOrTrigger)
35-
) {
36-
return null
37-
}
38-
39-
return (
40-
selectedActionOrTrigger?.webhookTriggerInstructions?.mockDataMsg ?? null
41-
)
42-
}
43-
4428
interface TestResultsProps {
4529
step: IStep
4630
selectedActionOrTrigger: ITrigger | IAction | undefined
@@ -65,6 +49,12 @@ export default function TestResult(props: TestResultsProps): JSX.Element {
6549
const { testExecutionSteps } = useContext(EditorContext)
6650
const isForEachStep = step.key === TOOLBOX_ACTIONS.ForEach
6751

52+
const testResultMessage = isForEachStep
53+
? getForEachDataMessage(testExecutionSteps, step)
54+
: isMock
55+
? 'The mock responses below are based on your form fields.'
56+
: null
57+
6858
const Content = () => {
6959
// No data only happens if user hasn't executed yet, or step returned null.
7060
if (!variables?.length) {
@@ -84,13 +74,9 @@ export default function TestResult(props: TestResultsProps): JSX.Element {
8474

8575
return (
8676
<Box w="100%">
87-
{(isMock || isForEachStep) && (
77+
{testResultMessage && (
8878
<Infobox variant="info">
89-
<Text>
90-
{isForEachStep
91-
? getForEachDataMessage(testExecutionSteps, step)
92-
: getMockDataMessage(selectedActionOrTrigger)}
93-
</Text>
79+
<Text>{testResultMessage}</Text>
9480
</Infobox>
9581
)}
9682
<VariablesList variables={variables} customStyles={{ py: 0, px: 2 }} />

0 commit comments

Comments
 (0)