Generate fake test data from custom and webhook event schemas

This commit is contained in:
Eric Allam
2023-02-03 16:19:49 +00:00
parent 0d2d9a043c
commit 2a51c5a1a3
13 changed files with 306 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Generate and send JSON Schema for custom and webhook events
@@ -0,0 +1,87 @@
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { JSONSchemaFaker } from "json-schema-faker";
import type { Workflow, WorkflowRun } from ".prisma/client";
export class WorkflowTestPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
async data({
organizationSlug,
workflowSlug,
environmentSlug,
}: {
organizationSlug: string;
workflowSlug: string;
environmentSlug: string;
}) {
const workflow = await this.#prismaClient.workflow.findFirst({
where: {
slug: workflowSlug,
organization: {
slug: organizationSlug,
},
},
include: {
runs: {
where: {
environment: {
slug: environmentSlug,
},
},
include: {
event: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
externalSource: true,
},
});
if (!workflow) {
throw new Error("Workflow not found");
}
const payload = await this.#getPayload(workflow, workflow.runs[0]);
const status =
workflow.status === "CREATED"
? workflow.type === "WEBHOOK" &&
workflow.externalSource?.manualRegistration
? "TESTABLE"
: "CREATED"
: workflow.status;
return { payload, status };
}
async #getPayload(
workflow: Workflow,
lastRun?: WorkflowRun & { event: { payload: any } }
) {
if (workflow.type === "SCHEDULE") {
return {
scheduledTime: new Date(),
lastRunAt: lastRun?.startedAt,
};
}
if (lastRun) {
return lastRun.event.payload;
}
if (workflow.jsonSchema) {
// @ts-ignore
return JSONSchemaFaker.generate(workflow.jsonSchema);
}
return {};
}
}
@@ -13,7 +13,8 @@ import { Body } from "~/components/primitives/text/Body";
import { Title } from "~/components/primitives/text/Title";
import { useCurrentOrganization } from "~/hooks/useOrganizations";
import { useCurrentWorkflow } from "~/hooks/useWorkflows";
import { getMostRecentWorkflowRun } from "~/models/workflowRun.server";
import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server";
import { WorkflowTestPresenter } from "~/presenters/testPresenter.server";
import { requireUserId } from "~/services/session.server";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -22,12 +23,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
invariant(workflowSlug, "workflowSlug is required");
invariant(organizationSlug, "organizationSlug is required");
const environmentSlug = await getRuntimeEnvironmentFromRequest(request);
try {
const latestRun = await getMostRecentWorkflowRun({
workflowSlug,
organizationSlug,
});
return typedjson({ latestRun });
const presenter = new WorkflowTestPresenter();
return typedjson(
await presenter.data({ workflowSlug, organizationSlug, environmentSlug })
);
} catch (error: any) {
console.error(error);
throw new Response("Error ", { status: 400 });
@@ -35,24 +38,26 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export default function Page() {
const { latestRun } = useTypedLoaderData<typeof loader>();
const { payload, status } = useTypedLoaderData<typeof loader>();
const organization = useCurrentOrganization();
invariant(organization, "Organization not found");
const workflow = useCurrentWorkflow();
invariant(workflow, "Workflow not found");
console.log("Workflow status is", { status, payload });
return (
<>
<Title>Test</Title>
{workflow.status === "CREATED" && (
{status === "CREATED" && (
<>
<PanelWarning className="mb-6">
This workflow requires its APIs to be connected before it can run.
</PanelWarning>
</>
)}
{workflow.status === "DISABLED" && (
{status === "DISABLED" ? (
<PanelInfo className="mb-6">
<Body className="flex grow items-center justify-between">
This workflow is disabled. Runs cannot be triggered or tested while
@@ -62,21 +67,13 @@ export default function Page() {
Settings
</TertiaryLink>
</PanelInfo>
)}
{workflow.status === "READY" && (
) : (
<Panel className="mt-4">
<Tester
organizationSlug={organization.slug}
workflowSlug={workflow.slug}
eventNames={workflow.eventNames}
initialValue={
workflow.type === "SCHEDULE"
? JSON.stringify({ scheduledTime: new Date() }, null, 2)
: latestRun == null
? "{\n\n}"
: JSON.stringify(latestRun.event.payload, null, 2)
}
initialValue={JSON.stringify(payload, null, 2)}
/>
</Panel>
)}
@@ -201,6 +201,7 @@ export class DispatchWorkflowRun {
});
if (
!workflowRun.isTest &&
workflowRun.workflow.externalSource &&
workflowRun.workflow.externalSource.status === "CREATED"
) {
@@ -136,6 +136,12 @@ export class RegisterWorkflow {
service: payload.trigger.service,
eventNames: payload.trigger.name,
triggerTtlInSeconds: payload.triggerTTL,
jsonSchema:
"schema" in payload.trigger
? payload.trigger.schema
? payload.trigger.schema
: undefined
: undefined,
},
create: {
organizationId: organization.id,
@@ -147,6 +153,12 @@ export class RegisterWorkflow {
service: payload.trigger.service,
eventNames: payload.trigger.name,
triggerTtlInSeconds: payload.triggerTTL,
jsonSchema:
"schema" in payload.trigger
? payload.trigger.schema
? payload.trigger.schema
: undefined
: undefined,
},
include: {
externalSource: true,
+2 -1
View File
@@ -90,6 +90,7 @@
"isbot": "^3.6.5",
"javascript-time-ago": "^2.5.7",
"json-query": "^2.2.2",
"json-schema-faker": "0.5.0-rcv.46",
"jsonata": "^1.8.6",
"jsonschema": "^1.4.1",
"keyv": "^4.3.2",
@@ -126,7 +127,7 @@
"zod-error": "^1.1.0"
},
"devDependencies": {
"@faker-js/faker": "^7.5.0",
"@faker-js/faker": "^7.6.0",
"@remix-run/dev": "^1.7.2",
"@remix-run/eslint-config": "^1.7.2",
"@swc/core": "^1.3.4",
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Workflow" ADD COLUMN "jsonSchema" JSONB;
+2
View File
@@ -120,6 +120,8 @@ model Workflow {
packageJson Json?
jsonSchema Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+131 -1
View File
@@ -1,5 +1,11 @@
import { z } from "zod";
import { Trigger, customEvent, sendEvent } from "@trigger.dev/sdk";
import {
Trigger,
customEvent,
sendEvent,
scheduleEvent,
webhookEvent,
} from "@trigger.dev/sdk";
import { ulid } from "ulid";
const userCreatedEvent = z.object({
@@ -70,3 +76,127 @@ new Trigger({
await ctx.logger.error("This is an error");
},
}).listen();
export const bookingPayloadSchema = z.object({
triggerEvent: z.string(),
createdAt: z.coerce.date(),
payload: z.object({
type: z.string(),
title: z.string(),
description: z.string(),
additionalNotes: z.string(),
customInputs: z.object({}),
startTime: z.coerce.date(),
endTime: z.coerce.date(),
organizer: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
timeZone: z.string(),
language: z.object({ locale: z.string() }),
}),
attendees: z.array(
z.object({
email: z.string(),
name: z.string(),
timeZone: z.string(),
language: z.object({ locale: z.string() }),
})
),
location: z.string(),
destinationCalendar: z.object({
id: z.number(),
integration: z.string(),
externalId: z.string(),
userId: z.number(),
eventTypeId: z.null(),
credentialId: z.number(),
}),
hideCalendarNotes: z.boolean(),
requiresConfirmation: z.null(),
eventTypeId: z.number(),
seatsShowAttendees: z.boolean(),
uid: z.string(),
conferenceData: z.object({
createRequest: z.object({ requestId: z.string() }),
}),
videoCallData: z.object({
type: z.string(),
id: z.string(),
password: z.string(),
url: z.string(),
}),
appsStatus: z.array(
z.object({
appName: z.string(),
type: z.string(),
success: z.number(),
failures: z.number(),
errors: z.array(z.any()).optional(),
warnings: z.array(z.any()).optional(),
})
),
eventTitle: z.string(),
eventDescription: z.null(),
price: z.number(),
currency: z.string(),
length: z.number(),
bookingId: z.number(),
metadata: z.object({ videoCallUrl: z.string() }),
status: z.string(),
}),
});
new Trigger({
id: "calcom-booking-custom-event",
name: "Cal.com booking custom event",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
triggerTTL: 60 * 60 * 24,
on: customEvent({ name: "calcom.booking", schema: bookingPayloadSchema }),
run: async (event, ctx) => {
return event;
},
}).listen();
new Trigger({
id: "testing-schedule-test-events",
name: "Testing scheduled test payloads",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
triggerTTL: 60 * 60 * 24,
on: scheduleEvent({
rateOf: { hours: 1 },
}),
run: async (event, ctx) => {
return event;
},
}).listen();
new Trigger({
id: "smoke-test-webhook-schema-test",
name: "Smoke Test Webhook Schema Test",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
logLevel: "debug",
on: webhookEvent({
service: "cal.com",
eventName: "BOOKING_CREATED",
filter: {
triggerEvent: ["BOOKING_CREATED"],
},
schema: bookingPayloadSchema,
verifyPayload: {
enabled: true,
header: "X-Cal-Signature-256",
},
}),
run: async (event, ctx) => {
await ctx.logger.info("Received a cal.com booking", {
event,
wallTime: new Date(),
});
},
}).listen();
+2
View File
@@ -11,6 +11,7 @@ export const CustomEventTriggerSchema = z.object({
service: z.literal("trigger"),
name: z.string(),
filter: EventFilterSchema,
schema: JsonSchema.optional(),
});
export type CustomEventTrigger = z.infer<typeof CustomEventTriggerSchema>;
@@ -21,6 +22,7 @@ export const WebhookEventTriggerSchema = z.object({
filter: EventFilterSchema,
source: JsonSchema.optional(),
manualRegistration: z.boolean().default(false),
schema: JsonSchema.optional(),
});
export type WebhookEventTrigger = z.infer<typeof WebhookEventTriggerSchema>;
+2 -1
View File
@@ -45,6 +45,7 @@
"uuid": "^9.0.0",
"ws": "^8.11.0",
"zod": "^3.20.2",
"zod-error": "^1.1.0"
"zod-error": "^1.1.0",
"zod-to-json-schema": "^3.20.2"
}
}
+7
View File
@@ -3,10 +3,15 @@ import {
TriggerMetadataSchema,
ScheduleSourceSchema,
ScheduledEventPayloadSchema,
} from "@trigger.dev/common-schemas";
import type {
CustomEventTrigger,
EventFilter,
WebhookEventTrigger,
} from "@trigger.dev/common-schemas";
import { z } from "zod";
import slugify from "slug";
import zodToJsonSchema from "zod-to-json-schema";
export type EventRule = z.infer<typeof EventFilterSchema>;
@@ -30,6 +35,7 @@ export function customEvent<TSchema extends z.ZodTypeAny>(
service: "trigger",
name: options.name,
filter: { event: [options.name], payload: options.filter ?? {} },
schema: zodToJsonSchema(options.schema) as CustomEventTrigger["schema"],
},
schema: options.schema,
};
@@ -80,6 +86,7 @@ export function webhookEvent<TSchema extends z.ZodTypeAny>(
event: options.eventName,
},
manualRegistration: true,
schema: zodToJsonSchema(options.schema) as WebhookEventTrigger["schema"],
},
schema: options.schema,
};
+37 -1
View File
@@ -48,7 +48,7 @@ importers:
'@codemirror/search': ^6.2.3
'@codemirror/state': ^6.1.3
'@codemirror/view': ^6.5.0
'@faker-js/faker': ^7.5.0
'@faker-js/faker': ^7.6.0
'@headlessui/react': ^1.6.4
'@heroicons/react': ^2.0.12
'@jsonhero/fetch-hero': ^0.2.2
@@ -136,6 +136,7 @@ importers:
isbot: ^3.6.5
javascript-time-ago: ^2.5.7
json-query: ^2.2.2
json-schema-faker: 0.5.0-rcv.46
jsonata: ^1.8.6
jsonschema: ^1.4.1
keyv: ^4.3.2
@@ -242,6 +243,7 @@ importers:
isbot: 3.6.5
javascript-time-ago: 2.5.9
json-query: 2.2.2
json-schema-faker: 0.5.0-rcv.46
jsonata: 1.8.6
jsonschema: 1.4.1
keyv: 4.5.2
@@ -966,6 +968,7 @@ importers:
ws: ^8.11.0
zod: ^3.20.2
zod-error: ^1.1.0
zod-to-json-schema: ^3.20.2
dependencies:
debug: 4.3.4
evt: 2.4.13
@@ -976,6 +979,7 @@ importers:
ws: 8.12.0
zod: 3.20.2
zod-error: 1.1.0
zod-to-json-schema: 3.20.2_zod@3.20.2
devDependencies:
'@trigger.dev/common-schemas': link:../common-schemas
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
@@ -9749,6 +9753,10 @@ packages:
combined-stream: 1.0.8
mime-types: 2.1.35
/format-util/1.0.5:
resolution: {integrity: sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==}
dev: false
/format/0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
@@ -11298,6 +11306,23 @@ packages:
resolution: {integrity: sha512-y+IcVZSdqNmS4fO8t1uZF6RMMs0xh3SrTjJr9bp1X3+v0Q13+7Cyv12dSmKwDswp/H427BVtpkLWhGxYu3ZWRA==}
dev: false
/json-schema-faker/0.5.0-rcv.46:
resolution: {integrity: sha512-Q+sGrxptZfezwm7M9W9VmHT9E8s5fWPCaRC4J2zUjb3CmDsxokiCBdHdS/psu91Tafc/ITv+GtIztGzUVT2zIg==}
hasBin: true
dependencies:
json-schema-ref-parser: 6.1.0
jsonpath-plus: 5.1.0
dev: false
/json-schema-ref-parser/6.1.0:
resolution: {integrity: sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==}
deprecated: Please switch to @apidevtools/json-schema-ref-parser
dependencies:
call-me-maybe: 1.0.2
js-yaml: 3.14.1
ono: 4.0.11
dev: false
/json-schema-traverse/0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
@@ -11349,6 +11374,11 @@ packages:
graceful-fs: 4.2.10
dev: true
/jsonpath-plus/5.1.0:
resolution: {integrity: sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==}
engines: {node: '>=10.0.0'}
dev: false
/jsonschema/1.4.1:
resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==}
dev: false
@@ -13101,6 +13131,12 @@ packages:
dependencies:
mimic-fn: 2.1.0
/ono/4.0.11:
resolution: {integrity: sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==}
dependencies:
format-util: 1.0.5
dev: false
/open/8.4.0:
resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
engines: {node: '>=12'}