Make the frontend resilient to old workflow run output formats (#10522)
- Create zod schemas for everything related to a workflow run - Update the types to be inferred from the zod schemas - Improper workflow run outputs will render a blank screen; we could show an error in the future https://github.com/user-attachments/assets/8e666c3e-82b0-4ab5-8804-2f70130ea257
This commit is contained in:
committed by
GitHub
parent
5a39903d42
commit
8bd9bc9d31
@ -1,4 +1,4 @@
|
|||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRunUnsafe } from '@/workflow/hooks/useWorkflowRunUnsafe';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { CodeEditor } from 'twenty-ui';
|
import { CodeEditor } from 'twenty-ui';
|
||||||
@ -12,7 +12,8 @@ export const WorkflowRunOutputVisualizer = ({
|
|||||||
}: {
|
}: {
|
||||||
workflowRunId: string;
|
workflowRunId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
const workflowRun = useWorkflowRunUnsafe({ workflowRunId });
|
||||||
|
|
||||||
if (!isDefined(workflowRun)) {
|
if (!isDefined(workflowRun)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,23 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { WorkflowRun } from '@/workflow/types/Workflow';
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
|
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
|
||||||
|
|
||||||
export const useWorkflowRun = ({
|
export const useWorkflowRun = ({
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
}: {
|
}: {
|
||||||
workflowRunId: string;
|
workflowRunId: string;
|
||||||
}) => {
|
}): WorkflowRun | undefined => {
|
||||||
const { record } = useFindOneRecord<WorkflowRun>({
|
const { record: rawRecord } = useFindOneRecord({
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||||
objectRecordId: workflowRunId,
|
objectRecordId: workflowRunId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { success, data: record } = workflowRunSchema.safeParse(rawRecord);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
|
|
||||||
|
export const useWorkflowRunUnsafe = ({
|
||||||
|
workflowRunId,
|
||||||
|
}: {
|
||||||
|
workflowRunId: string;
|
||||||
|
}) => {
|
||||||
|
const { record } = useFindOneRecord<WorkflowRun>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||||
|
objectRecordId: workflowRunId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return record;
|
||||||
|
};
|
||||||
@ -1,179 +1,89 @@
|
|||||||
type BaseWorkflowActionSettings = {
|
import {
|
||||||
input: object;
|
workflowActionSchema,
|
||||||
outputSchema: object;
|
workflowCodeActionSchema,
|
||||||
errorHandlingOptions: {
|
workflowCodeActionSettingsSchema,
|
||||||
retryOnFailure: {
|
workflowCreateRecordActionSchema,
|
||||||
value: boolean;
|
workflowCreateRecordActionSettingsSchema,
|
||||||
};
|
workflowCronTriggerSchema,
|
||||||
continueOnFailure: {
|
workflowDatabaseEventTriggerSchema,
|
||||||
value: boolean;
|
workflowDeleteRecordActionSchema,
|
||||||
};
|
workflowDeleteRecordActionSettingsSchema,
|
||||||
};
|
workflowFindRecordsActionSchema,
|
||||||
};
|
workflowFindRecordsActionSettingsSchema,
|
||||||
|
workflowManualTriggerSchema,
|
||||||
|
workflowRunOutputSchema,
|
||||||
|
workflowRunSchema,
|
||||||
|
workflowSendEmailActionSchema,
|
||||||
|
workflowSendEmailActionSettingsSchema,
|
||||||
|
workflowTriggerSchema,
|
||||||
|
workflowUpdateRecordActionSchema,
|
||||||
|
workflowUpdateRecordActionSettingsSchema,
|
||||||
|
} from '@/workflow/validation-schemas/workflowSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export type WorkflowCodeActionSettings = BaseWorkflowActionSettings & {
|
export type WorkflowCodeActionSettings = z.infer<
|
||||||
input: {
|
typeof workflowCodeActionSettingsSchema
|
||||||
serverlessFunctionId: string;
|
>;
|
||||||
serverlessFunctionVersion: string;
|
export type WorkflowSendEmailActionSettings = z.infer<
|
||||||
serverlessFunctionInput: {
|
typeof workflowSendEmailActionSettingsSchema
|
||||||
[key: string]: any;
|
>;
|
||||||
};
|
export type WorkflowCreateRecordActionSettings = z.infer<
|
||||||
};
|
typeof workflowCreateRecordActionSettingsSchema
|
||||||
};
|
>;
|
||||||
|
export type WorkflowUpdateRecordActionSettings = z.infer<
|
||||||
|
typeof workflowUpdateRecordActionSettingsSchema
|
||||||
|
>;
|
||||||
|
export type WorkflowDeleteRecordActionSettings = z.infer<
|
||||||
|
typeof workflowDeleteRecordActionSettingsSchema
|
||||||
|
>;
|
||||||
|
export type WorkflowFindRecordsActionSettings = z.infer<
|
||||||
|
typeof workflowFindRecordsActionSettingsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export type WorkflowSendEmailActionSettings = BaseWorkflowActionSettings & {
|
export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;
|
||||||
input: {
|
export type WorkflowSendEmailAction = z.infer<
|
||||||
connectedAccountId: string;
|
typeof workflowSendEmailActionSchema
|
||||||
email: string;
|
>;
|
||||||
subject?: string;
|
export type WorkflowCreateRecordAction = z.infer<
|
||||||
body?: string;
|
typeof workflowCreateRecordActionSchema
|
||||||
};
|
>;
|
||||||
};
|
export type WorkflowUpdateRecordAction = z.infer<
|
||||||
|
typeof workflowUpdateRecordActionSchema
|
||||||
type ObjectRecord = Record<string, any>;
|
>;
|
||||||
|
export type WorkflowDeleteRecordAction = z.infer<
|
||||||
export type WorkflowCreateRecordActionSettings = BaseWorkflowActionSettings & {
|
typeof workflowDeleteRecordActionSchema
|
||||||
input: {
|
>;
|
||||||
objectName: string;
|
export type WorkflowFindRecordsAction = z.infer<
|
||||||
objectRecord: ObjectRecord;
|
typeof workflowFindRecordsActionSchema
|
||||||
};
|
>;
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowUpdateRecordActionSettings = BaseWorkflowActionSettings & {
|
|
||||||
input: {
|
|
||||||
objectName: string;
|
|
||||||
objectRecord: ObjectRecord;
|
|
||||||
objectRecordId: string;
|
|
||||||
fieldsToUpdate: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowDeleteRecordActionSettings = BaseWorkflowActionSettings & {
|
|
||||||
input: {
|
|
||||||
objectName: string;
|
|
||||||
objectRecordId: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowFindRecordsActionSettings = BaseWorkflowActionSettings & {
|
|
||||||
input: {
|
|
||||||
objectName: string;
|
|
||||||
limit?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type BaseWorkflowAction = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
valid: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowCodeAction = BaseWorkflowAction & {
|
|
||||||
type: 'CODE';
|
|
||||||
settings: WorkflowCodeActionSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowSendEmailAction = BaseWorkflowAction & {
|
|
||||||
type: 'SEND_EMAIL';
|
|
||||||
settings: WorkflowSendEmailActionSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowCreateRecordAction = BaseWorkflowAction & {
|
|
||||||
type: 'CREATE_RECORD';
|
|
||||||
settings: WorkflowCreateRecordActionSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowUpdateRecordAction = BaseWorkflowAction & {
|
|
||||||
type: 'UPDATE_RECORD';
|
|
||||||
settings: WorkflowUpdateRecordActionSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowDeleteRecordAction = BaseWorkflowAction & {
|
|
||||||
type: 'DELETE_RECORD';
|
|
||||||
settings: WorkflowDeleteRecordActionSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowFindRecordsAction = BaseWorkflowAction & {
|
|
||||||
type: 'FIND_RECORDS';
|
|
||||||
settings: WorkflowFindRecordsActionSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowAction =
|
|
||||||
| WorkflowCodeAction
|
|
||||||
| WorkflowSendEmailAction
|
|
||||||
| WorkflowCreateRecordAction
|
|
||||||
| WorkflowUpdateRecordAction
|
|
||||||
| WorkflowDeleteRecordAction
|
|
||||||
| WorkflowFindRecordsAction;
|
|
||||||
|
|
||||||
|
export type WorkflowAction = z.infer<typeof workflowActionSchema>;
|
||||||
export type WorkflowActionType = WorkflowAction['type'];
|
export type WorkflowActionType = WorkflowAction['type'];
|
||||||
|
|
||||||
export type WorkflowStep = WorkflowAction;
|
export type WorkflowStep = WorkflowAction;
|
||||||
|
|
||||||
export type WorkflowStepType = WorkflowStep['type'];
|
export type WorkflowStepType = WorkflowStep['type'];
|
||||||
|
|
||||||
type BaseTrigger = {
|
export type WorkflowDatabaseEventTrigger = z.infer<
|
||||||
name?: string;
|
typeof workflowDatabaseEventTriggerSchema
|
||||||
type: string;
|
>;
|
||||||
};
|
export type WorkflowManualTrigger = z.infer<typeof workflowManualTriggerSchema>;
|
||||||
|
export type WorkflowCronTrigger = z.infer<typeof workflowCronTriggerSchema>;
|
||||||
export type WorkflowDatabaseEventTrigger = BaseTrigger & {
|
|
||||||
type: 'DATABASE_EVENT';
|
|
||||||
settings: {
|
|
||||||
eventName: string;
|
|
||||||
input?: object;
|
|
||||||
outputSchema: object;
|
|
||||||
objectType?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowManualTrigger = BaseTrigger & {
|
|
||||||
type: 'MANUAL';
|
|
||||||
settings: {
|
|
||||||
objectType?: string;
|
|
||||||
outputSchema: object;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowCronTrigger = BaseTrigger & {
|
|
||||||
type: 'CRON';
|
|
||||||
settings: (
|
|
||||||
| {
|
|
||||||
type: 'HOURS';
|
|
||||||
schedule: { hour: number; minute: number };
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'MINUTES';
|
|
||||||
schedule: { minute: number };
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'CUSTOM';
|
|
||||||
pattern: string;
|
|
||||||
}
|
|
||||||
) & { outputSchema: object };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
|
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
|
||||||
|
|
||||||
export type WorkflowManualTriggerAvailability =
|
export type WorkflowManualTriggerAvailability =
|
||||||
| 'EVERYWHERE'
|
| 'EVERYWHERE'
|
||||||
| 'WHEN_RECORD_SELECTED';
|
| 'WHEN_RECORD_SELECTED';
|
||||||
|
|
||||||
export type WorkflowTrigger =
|
export type WorkflowTrigger = z.infer<typeof workflowTriggerSchema>;
|
||||||
| WorkflowDatabaseEventTrigger
|
|
||||||
| WorkflowManualTrigger
|
|
||||||
| WorkflowCronTrigger;
|
|
||||||
|
|
||||||
export type WorkflowTriggerType = WorkflowTrigger['type'];
|
export type WorkflowTriggerType = WorkflowTrigger['type'];
|
||||||
|
|
||||||
export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED';
|
export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED';
|
||||||
|
|
||||||
export type WorkflowVersionStatus =
|
export type WorkflowVersionStatus =
|
||||||
| 'DRAFT'
|
| 'DRAFT'
|
||||||
| 'ACTIVE'
|
| 'ACTIVE'
|
||||||
| 'DEACTIVATED'
|
| 'DEACTIVATED'
|
||||||
| 'ARCHIVED';
|
| 'ARCHIVED';
|
||||||
|
|
||||||
|
// Keep existing types that are not covered by schemas
|
||||||
export type WorkflowVersion = {
|
export type WorkflowVersion = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -186,32 +96,10 @@ export type WorkflowVersion = {
|
|||||||
__typename: 'WorkflowVersion';
|
__typename: 'WorkflowVersion';
|
||||||
};
|
};
|
||||||
|
|
||||||
type StepRunOutput = {
|
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
|
||||||
id: string;
|
export type WorkflowRunOutputStepsOutput = WorkflowRunOutput['stepsOutput'];
|
||||||
outputs: {
|
|
||||||
attemptCount: number;
|
|
||||||
result: object | undefined;
|
|
||||||
error: string | undefined;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowRunOutputStepsOutput = Record<string, StepRunOutput>;
|
export type WorkflowRun = z.infer<typeof workflowRunSchema>;
|
||||||
|
|
||||||
export type WorkflowRunOutput = {
|
|
||||||
flow: {
|
|
||||||
trigger: WorkflowTrigger;
|
|
||||||
steps: WorkflowAction[];
|
|
||||||
};
|
|
||||||
stepsOutput?: WorkflowRunOutputStepsOutput;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowRun = {
|
|
||||||
__typename: 'WorkflowRun';
|
|
||||||
id: string;
|
|
||||||
workflowVersionId: string;
|
|
||||||
output: WorkflowRunOutput | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Workflow = {
|
export type Workflow = {
|
||||||
__typename: 'Workflow';
|
__typename: 'Workflow';
|
||||||
|
|||||||
@ -0,0 +1,205 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Base schemas
|
||||||
|
export const objectRecordSchema = z.record(z.any());
|
||||||
|
|
||||||
|
export const baseWorkflowActionSettingsSchema = z.object({
|
||||||
|
input: z.object({}).passthrough(),
|
||||||
|
outputSchema: z.object({}).passthrough(),
|
||||||
|
errorHandlingOptions: z.object({
|
||||||
|
retryOnFailure: z.object({
|
||||||
|
value: z.boolean(),
|
||||||
|
}),
|
||||||
|
continueOnFailure: z.object({
|
||||||
|
value: z.boolean(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const baseWorkflowActionSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
valid: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const baseTriggerSchema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Action settings schemas
|
||||||
|
export const workflowCodeActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
serverlessFunctionId: z.string(),
|
||||||
|
serverlessFunctionVersion: z.string(),
|
||||||
|
serverlessFunctionInput: z.record(z.any()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowSendEmailActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
connectedAccountId: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
subject: z.string().optional(),
|
||||||
|
body: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowCreateRecordActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
objectName: z.string(),
|
||||||
|
objectRecord: objectRecordSchema,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowUpdateRecordActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
objectName: z.string(),
|
||||||
|
objectRecord: objectRecordSchema,
|
||||||
|
objectRecordId: z.string(),
|
||||||
|
fieldsToUpdate: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowDeleteRecordActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
objectName: z.string(),
|
||||||
|
objectRecordId: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowFindRecordsActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
objectName: z.string(),
|
||||||
|
limit: z.number().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Action schemas
|
||||||
|
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||||
|
type: z.literal('CODE'),
|
||||||
|
settings: workflowCodeActionSettingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
|
||||||
|
type: z.literal('SEND_EMAIL'),
|
||||||
|
settings: workflowSendEmailActionSettingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||||
|
{
|
||||||
|
type: z.literal('CREATE_RECORD'),
|
||||||
|
settings: workflowCreateRecordActionSettingsSchema,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||||
|
{
|
||||||
|
type: z.literal('UPDATE_RECORD'),
|
||||||
|
settings: workflowUpdateRecordActionSettingsSchema,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||||
|
{
|
||||||
|
type: z.literal('DELETE_RECORD'),
|
||||||
|
settings: workflowDeleteRecordActionSettingsSchema,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
|
||||||
|
type: z.literal('FIND_RECORDS'),
|
||||||
|
settings: workflowFindRecordsActionSettingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combined action schema
|
||||||
|
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||||
|
workflowCodeActionSchema,
|
||||||
|
workflowSendEmailActionSchema,
|
||||||
|
workflowCreateRecordActionSchema,
|
||||||
|
workflowUpdateRecordActionSchema,
|
||||||
|
workflowDeleteRecordActionSchema,
|
||||||
|
workflowFindRecordsActionSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Trigger schemas
|
||||||
|
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
|
||||||
|
type: z.literal('DATABASE_EVENT'),
|
||||||
|
settings: z.object({
|
||||||
|
eventName: z.string(),
|
||||||
|
input: z.object({}).passthrough().optional(),
|
||||||
|
outputSchema: z.object({}).passthrough(),
|
||||||
|
objectType: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowManualTriggerSchema = baseTriggerSchema.extend({
|
||||||
|
type: z.literal('MANUAL'),
|
||||||
|
settings: z.object({
|
||||||
|
objectType: z.string().optional(),
|
||||||
|
outputSchema: z.object({}).passthrough(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
|
||||||
|
type: z.literal('CRON'),
|
||||||
|
settings: z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal('HOURS'),
|
||||||
|
schedule: z.object({ hour: z.number(), minute: z.number() }),
|
||||||
|
outputSchema: z.object({}).passthrough(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('MINUTES'),
|
||||||
|
schedule: z.object({ minute: z.number() }),
|
||||||
|
outputSchema: z.object({}).passthrough(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('CUSTOM'),
|
||||||
|
pattern: z.string(),
|
||||||
|
outputSchema: z.object({}).passthrough(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combined trigger schema
|
||||||
|
export const workflowTriggerSchema = z.discriminatedUnion('type', [
|
||||||
|
workflowDatabaseEventTriggerSchema,
|
||||||
|
workflowManualTriggerSchema,
|
||||||
|
workflowCronTriggerSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Step output schemas
|
||||||
|
const workflowExecutorOutputSchema = z.object({
|
||||||
|
result: z.any().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowRunOutputStepsOutputSchema = z.record(
|
||||||
|
workflowExecutorOutputSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Final workflow run output schema
|
||||||
|
export const workflowRunOutputSchema = z.object({
|
||||||
|
flow: z.object({
|
||||||
|
trigger: workflowTriggerSchema,
|
||||||
|
steps: z.array(workflowActionSchema),
|
||||||
|
}),
|
||||||
|
stepsOutput: workflowRunOutputStepsOutputSchema.optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workflowRunSchema = z.object({
|
||||||
|
__typename: z.literal('WorkflowRun'),
|
||||||
|
id: z.string(),
|
||||||
|
workflowVersionId: z.string(),
|
||||||
|
output: workflowRunOutputSchema.nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
|
||||||
@ -11,175 +11,6 @@ jest.mock('uuid', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('generateWorkflowRunDiagram', () => {
|
describe('generateWorkflowRunDiagram', () => {
|
||||||
it('marks node as failed when not at least one attempt is in output', () => {
|
|
||||||
const trigger: WorkflowTrigger = {
|
|
||||||
name: 'Company created',
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
settings: {
|
|
||||||
eventName: 'company.created',
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const steps: WorkflowStep[] = [
|
|
||||||
{
|
|
||||||
id: 'step1',
|
|
||||||
name: 'Step 1',
|
|
||||||
type: 'CODE',
|
|
||||||
valid: true,
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
retryOnFailure: { value: true },
|
|
||||||
continueOnFailure: { value: false },
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
|
||||||
serverlessFunctionVersion: '1',
|
|
||||||
serverlessFunctionInput: {},
|
|
||||||
},
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'step2',
|
|
||||||
name: 'Step 2',
|
|
||||||
type: 'CODE',
|
|
||||||
valid: true,
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
retryOnFailure: { value: true },
|
|
||||||
continueOnFailure: { value: false },
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
|
||||||
serverlessFunctionVersion: '1',
|
|
||||||
serverlessFunctionInput: {},
|
|
||||||
},
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'step3',
|
|
||||||
name: 'Step 3',
|
|
||||||
type: 'CODE',
|
|
||||||
valid: true,
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
retryOnFailure: { value: true },
|
|
||||||
continueOnFailure: { value: false },
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
|
||||||
serverlessFunctionVersion: '1',
|
|
||||||
serverlessFunctionInput: {},
|
|
||||||
},
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
|
||||||
step1: {
|
|
||||||
id: 'step1',
|
|
||||||
outputs: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"edges": [
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-green-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "trigger",
|
|
||||||
"target": "step1",
|
|
||||||
"type": "success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step1",
|
|
||||||
"target": "step2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step2",
|
|
||||||
"target": "step3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"icon": "IconPlaylistAdd",
|
|
||||||
"isLeafNode": false,
|
|
||||||
"name": "Company created",
|
|
||||||
"nodeType": "trigger",
|
|
||||||
"runStatus": "success",
|
|
||||||
"triggerType": "DATABASE_EVENT",
|
|
||||||
},
|
|
||||||
"id": "trigger",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"isLeafNode": false,
|
|
||||||
"name": "Step 1",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "failure",
|
|
||||||
},
|
|
||||||
"id": "step1",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"isLeafNode": false,
|
|
||||||
"name": "Step 2",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "not-executed",
|
|
||||||
},
|
|
||||||
"id": "step2",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 150,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"isLeafNode": false,
|
|
||||||
"name": "Step 3",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "not-executed",
|
|
||||||
},
|
|
||||||
"id": "step3",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 300,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('marks node as failed when the last attempt failed', () => {
|
it('marks node as failed when the last attempt failed', () => {
|
||||||
const trigger: WorkflowTrigger = {
|
const trigger: WorkflowTrigger = {
|
||||||
name: 'Company created',
|
name: 'Company created',
|
||||||
@ -247,14 +78,8 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
];
|
];
|
||||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||||
step1: {
|
step1: {
|
||||||
id: 'step1',
|
result: undefined,
|
||||||
outputs: [
|
error: '',
|
||||||
{
|
|
||||||
attemptCount: 1,
|
|
||||||
result: undefined,
|
|
||||||
error: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -265,7 +90,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -275,7 +100,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -284,7 +109,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -422,34 +247,16 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
];
|
];
|
||||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||||
step1: {
|
step1: {
|
||||||
id: 'step1',
|
result: {},
|
||||||
outputs: [
|
error: undefined,
|
||||||
{
|
|
||||||
attemptCount: 1,
|
|
||||||
result: {},
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
step2: {
|
step2: {
|
||||||
id: 'step2',
|
result: {},
|
||||||
outputs: [
|
error: undefined,
|
||||||
{
|
|
||||||
attemptCount: 1,
|
|
||||||
result: {},
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
step3: {
|
step3: {
|
||||||
id: 'step3',
|
result: {},
|
||||||
outputs: [
|
error: undefined,
|
||||||
{
|
|
||||||
attemptCount: 1,
|
|
||||||
result: {},
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -460,7 +267,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -470,7 +277,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -480,7 +287,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -626,7 +433,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -636,7 +443,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -645,7 +452,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -801,14 +608,8 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
];
|
];
|
||||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||||
step1: {
|
step1: {
|
||||||
id: 'step1',
|
result: {},
|
||||||
outputs: [
|
error: undefined,
|
||||||
{
|
|
||||||
attemptCount: 1,
|
|
||||||
result: {},
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -819,7 +620,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -829,7 +630,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-13",
|
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -839,7 +640,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-14",
|
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
@ -848,7 +649,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-15",
|
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
|
|||||||
@ -86,12 +86,7 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
} else if (!isDefined(runResult)) {
|
} else if (!isDefined(runResult)) {
|
||||||
runStatus = 'running';
|
runStatus = 'running';
|
||||||
} else {
|
} else {
|
||||||
const lastAttempt = runResult.outputs.at(-1);
|
if (isDefined(runResult.error)) {
|
||||||
|
|
||||||
if (!isDefined(lastAttempt)) {
|
|
||||||
// Should never happen. Should we throw instead?
|
|
||||||
runStatus = 'failure';
|
|
||||||
} else if (isDefined(lastAttempt.error)) {
|
|
||||||
runStatus = 'failure';
|
runStatus = 'failure';
|
||||||
} else {
|
} else {
|
||||||
runStatus = 'success';
|
runStatus = 'success';
|
||||||
|
|||||||
Reference in New Issue
Block a user