Skip to content

Commit ecd050b

Browse files
Airtable integration with webhook and integration changes (#399)
* CLI create-integration command now accepts an Open AI api key * Create integration docs separated into multiple pages * Initial Airtable integration commit, with OpenAI generated code * OAuth page coming soon * Export DisplayProperty from the SDK * TSConfig made to match GitHub’s with paths * getRecords * Removed duplicate Stripe job from the catalog * Renamed Airtable apiKey option to token * First Airtable job * Export Collaborator and Attachment field types * A typesafe example that uses runTask * WIP on new integration tasks… not working yet * Attempt with class * Revert "Attempt with class" This reverts commit 93a4833. * WIP changing how tasks work * Mock of async local storage * Moved client creation from constructor * New approach with a clone method on TriggerIntegration * Added runTask to Airtable which is used by integration tasks * Added the Airtable icon and connection when using runTask * base().table() is working * runTask options moved to the 3rd param, made optional with optional name * Added some generic arguments * Added generic type to table * Removed old comment * We don’t need to repeat the icon * getRecords and getRecord now returning the right data and types * Creating records * Update records * Delete records * The internal properties of integrations are now hidden by the TypeScript types * Sprinkled a Prettify in there * Improved the types * Added Airtable to the integration catalog * Early work on Airtable webhook registration * More progress with webhooks * connectionKey needs to be cloned for webhooks to work * connectionKey needs to be cloned for webhooks to work * It was unclear that the ActivateSourceService was using a graphileJob id * ActivateSourceService optionally takes a jobId, if missing it generate a unique id * When retrying trigger registration, don’t pass an id so it is generated * Removed Airtable webhooks tasks from the job-catalog example * Added TriggerSourceOption, removed TriggerSourceEvent * WIP with new ExternalSource options * ExternalSourceTrigger setup * DynamicTrigger changed to options, will need some more work * filter gets options passed to it * SourceMetadata v2 renamed to SourceMetadataV2, kept original * Started versioning the backend * Moved param order on io.getEvent and io.cancelEvent * The runTask stuff that allows unknown to work is back * Indexing for v1 and v2, with version on “activateSource” schema * Added todos, to deal with Airtable SDK calls inside the webhook handler * “deliverHttpSourceRequest” queueName changed to the source id so they process in order * ActivateSource changes to deal with old and new data formats * Update existing TriggerSources to v2 * Fix for dynamic.ts typescript errors, need to revisit this later * UpdateSourceService v1 and v2, with new v2 API endpoint * Removed unused imports * More progress on v1 and v2 * Airtable webhooks are now triggering a job * Moved webhooks to a new file * You can do API calls in the webhook handler now, Airtable webhook data is being processed * Airtable events coming through * Defined the Airtable table payload type * TriggerSource metadata is being stored and used * Removed some logs * Added filtering and don’t allow any webhooks that use automated sources * Resend switched to new integration * Moved Resend test jobs to the catalog, and tested it worked * WIP on Slack, there are compile errors * Created a generic type that strips out indexes * Slack updated to use new integration * SendGrid migrated over * Integration runTask is now allowing regular types * Changed io.runTask types so it only allows Json-able types * OpenAI models tasks working * Added Airtable changes to runTask * Removed the index signature crap from the Slack integration * Don’t need to cast the callback result * Updated Resend * Re-ordered runTask params * WIP on openai * onAccountUpdated is Connect only * Removed RunTaskResult * Handle Resend errors, the official SDK doesn’t expose them properly at the moment * Removed OmitIndexSignature * OpenAI converted to new integration, with backwards compatible functions * Put the openai catalog back to what it was originally * Export a standard retry with backoff, to be used * Use the standard exponential backoff in the integrations * Retry options moved earlier so they can be overriden by a task * GitHub tasks migrated * Added sources, fixed one bundling issue * Added GitHub jobs to catalog * Remove duplicate options * Deduplicate events * Removed duplicate Job * Switched Plain over * Set the Plain icon * Converted Stripe over * Supabase adapted * Typeform working * Added dynamic-schedule to catalog * Added background-fetch job catalog * Created dynamic-triggers catalog file * Fixed old general file with runTask param order * Dynamic triggers working * SendGrid updated to use the same tsconfig as other integrations * Removed Airtable webhook, until we have batch support * Added OAuth airtable auth example * Created beta changeset tag * Beta changesets for most packages --------- Co-authored-by: Eric Allam <[email protected]>
1 parent b24aeea commit ecd050b

File tree

127 files changed

+8740
-5070
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+8740
-5070
lines changed

.changeset/healthy-yaks-chew.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@trigger.dev/airtable": minor
3+
"@trigger.dev/sendgrid": minor
4+
"@trigger.dev/supabase": minor
5+
"@trigger.dev/typeform": minor
6+
"@trigger.dev/sdk": minor
7+
"@trigger.dev/github": minor
8+
"@trigger.dev/openai": minor
9+
"@trigger.dev/resend": minor
10+
"@trigger.dev/stripe": minor
11+
"@trigger.dev/plain": minor
12+
"@trigger.dev/slack": minor
13+
"@trigger.dev/core": minor
14+
"@trigger.dev/cli": minor
15+
---
16+
17+
Integrations are now simpler and support authentication during webhook registration

.changeset/pre.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"mode": "pre",
3+
"tag": "beta",
4+
"initialVersions": {
5+
"webapp": "1.0.0",
6+
"@trigger.dev/airtable": "2.0.10",
7+
"@trigger.dev/github": "2.0.14",
8+
"@trigger.dev/openai": "2.0.14",
9+
"@trigger.dev/plain": "2.0.14",
10+
"@trigger.dev/resend": "2.0.14",
11+
"@trigger.dev/sendgrid": "2.0.14",
12+
"@trigger.dev/slack": "2.0.14",
13+
"@trigger.dev/stripe": "2.0.14",
14+
"@trigger.dev/supabase": "2.0.14",
15+
"@trigger.dev/typeform": "2.0.14",
16+
"@trigger.dev/astro": "2.0.14",
17+
"@trigger.dev/cli": "2.0.14",
18+
"@trigger.dev/core": "2.0.14",
19+
"@trigger.dev/database": "0.0.0",
20+
"emails": "1.0.0",
21+
"@trigger.dev/eslint-plugin": "2.0.14",
22+
"@trigger.dev/express": "2.0.14",
23+
"@trigger.dev/integration-kit": "2.0.14",
24+
"@trigger.dev/nextjs": "2.0.14",
25+
"@trigger.dev/react": "2.0.14",
26+
"@trigger.dev/sdk": "2.0.14"
27+
},
28+
"changesets": []
29+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ExternalAccount, Integration, TriggerSource } from "@trigger.dev/database";
2+
import { ConnectionAuth } from "@trigger.dev/core";
3+
import { PrismaClientOrTransaction } from "~/db.server";
4+
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
5+
import { logger } from "~/services/logger.server";
6+
7+
type ResolvableTriggerSource = TriggerSource & {
8+
integration: Integration;
9+
externalAccount: ExternalAccount | null;
10+
};
11+
12+
export async function resolveSourceConnection(
13+
tx: PrismaClientOrTransaction,
14+
source: ResolvableTriggerSource
15+
): Promise<ConnectionAuth | undefined> {
16+
if (source.integration.authSource !== "HOSTED") return;
17+
18+
const connection = await getConnection(tx, source);
19+
20+
if (!connection) {
21+
logger.error(
22+
`Integration connection not found for source ${source.id}, integration ${source.integration.id}`
23+
);
24+
return;
25+
}
26+
27+
const response = await integrationAuthRepository.getCredentials(connection);
28+
29+
if (!response) {
30+
return;
31+
}
32+
33+
return {
34+
type: "oauth2",
35+
scopes: response.scopes,
36+
accessToken: response.accessToken,
37+
};
38+
}
39+
40+
function getConnection(tx: PrismaClientOrTransaction, source: ResolvableTriggerSource) {
41+
if (source.externalAccount) {
42+
return tx.integrationConnection.findFirst({
43+
where: {
44+
integrationId: source.integration.id,
45+
externalAccountId: source.externalAccount.id,
46+
},
47+
include: {
48+
dataReference: true,
49+
},
50+
});
51+
}
52+
53+
return tx.integrationConnection.findFirst({
54+
where: {
55+
integrationId: source.integration.id,
56+
},
57+
include: {
58+
dataReference: true,
59+
},
60+
});
61+
}

apps/webapp/app/presenters/TriggerSourcePresenter.server.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ export class TriggerSourcePresenter {
6262
},
6363
},
6464
},
65+
dynamicTrigger: {
66+
select: {
67+
id: true,
68+
slug: true,
69+
sourceRegistrationJob: {
70+
select: {
71+
job: {
72+
select: {
73+
id: true,
74+
slug: true,
75+
},
76+
},
77+
},
78+
},
79+
},
80+
},
6581
},
6682
where: {
6783
id: triggerSourceId,
@@ -73,10 +89,15 @@ export class TriggerSourcePresenter {
7389
}
7490

7591
const runListPresenter = new RunListPresenter(this.#prismaClient);
76-
const runList = trigger.sourceRegistrationJob
92+
const jobSlug = getJobSlug(
93+
trigger.sourceRegistrationJob?.job.slug,
94+
trigger.dynamicTrigger?.sourceRegistrationJob?.job.slug
95+
);
96+
97+
const runList = jobSlug
7798
? await runListPresenter.call({
7899
userId,
79-
jobSlug: trigger.sourceRegistrationJob.job.slug,
100+
jobSlug,
80101
organizationSlug,
81102
projectSlug,
82103
direction,
@@ -95,7 +116,21 @@ export class TriggerSourcePresenter {
95116
params: trigger.params,
96117
registrationJob: trigger.sourceRegistrationJob?.job,
97118
runList,
119+
dynamic: trigger.dynamicTrigger
120+
? { id: trigger.dynamicTrigger.id, slug: trigger.dynamicTrigger.slug }
121+
: undefined,
98122
},
99123
};
100124
}
101125
}
126+
127+
function getJobSlug(
128+
sourceRegistrationJobSlug: string | undefined,
129+
dynamicSourceRegistrationJobSlug: string | undefined
130+
) {
131+
if (sourceRegistrationJobSlug) {
132+
return sourceRegistrationJobSlug;
133+
}
134+
135+
return dynamicSourceRegistrationJobSlug;
136+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam/route.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { parse } from "@conform-to/zod";
4343
import { z } from "zod";
4444
import { ActivateSourceService } from "~/services/sources/activateSource.server";
4545
import { redirectWithSuccessMessage } from "~/models/message.server";
46+
import { nanoid } from "nanoid";
4647

4748
export const loader = async ({ request, params }: LoaderArgs) => {
4849
const user = await requireUser(request);
@@ -90,7 +91,7 @@ export const action: ActionFunction = async ({ request, params }) => {
9091
try {
9192
const service = new ActivateSourceService();
9293

93-
const result = await service.call(triggerParam, submission.value.jobId);
94+
const result = await service.call(triggerParam);
9495

9596
return redirectWithSuccessMessage(
9697
externalTriggerPath({ slug: organizationSlug }, { slug: projectParam }, { id: triggerParam }),
@@ -167,6 +168,17 @@ export default function Page() {
167168
<NamedIcon name={trigger.active ? "active" : "inactive"} className="h-4 w-4" />
168169
}
169170
/>
171+
{trigger.dynamic && (
172+
<PageInfoProperty
173+
label="Dynamic"
174+
value={
175+
<span className="flex items-center gap-0.5">
176+
<NamedIcon name="dynamic" className="h-4 w-4" />
177+
{trigger.dynamic.slug}
178+
</span>
179+
}
180+
/>
181+
)}
170182
<PageInfoProperty
171183
label="Environment"
172184
value={<EnvironmentLabel environment={trigger.environment} />}
@@ -206,7 +218,7 @@ export default function Page() {
206218
</Button>
207219
</Callout>
208220
</Form>
209-
) : (
221+
) : trigger.dynamic ? null : (
210222
<Callout variant="error" className="justiy-between mb-4 items-center">
211223
This External Trigger hasn't registered successfully. Contact support for help:{" "}
212224
{trigger.id}

apps/webapp/app/routes/api.v1.$endpointSlug.sources.$id.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { ActionArgs } from "@remix-run/server-runtime";
22
import { json } from "@remix-run/server-runtime";
3-
import { UpdateTriggerSourceBodySchema } from "@trigger.dev/core";
3+
import { UpdateTriggerSourceBodyV1Schema } from "@trigger.dev/core";
44
import { z } from "zod";
55
import { authenticateApiRequest } from "~/services/apiAuth.server";
66
import { logger } from "~/services/logger.server";
7-
import { UpdateSourceService } from "~/services/sources/updateSource.server";
7+
import { UpdateSourceServiceV1 } from "~/services/sources/updateSourceV1.server";
88

99
const ParamsSchema = z.object({
1010
endpointSlug: z.string(),
@@ -40,13 +40,13 @@ export async function action({ request, params }: ActionArgs) {
4040
// Now parse the request body
4141
const anyBody = await request.json();
4242

43-
const body = UpdateTriggerSourceBodySchema.safeParse(anyBody);
43+
const body = UpdateTriggerSourceBodyV1Schema.safeParse(anyBody);
4444

4545
if (!body.success) {
4646
return json({ error: "Invalid request body" }, { status: 400 });
4747
}
4848

49-
const service = new UpdateSourceService();
49+
const service = new UpdateSourceServiceV1();
5050

5151
try {
5252
const source = await service.call({

apps/webapp/app/routes/api.v1.$endpointSlug.triggers.$id.registrations.$key.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { ActionArgs } from "@remix-run/server-runtime";
22
import { json } from "@remix-run/server-runtime";
3-
import { RegisterTriggerBodySchema } from "@trigger.dev/core";
3+
import { RegisterTriggerBodySchemaV1 } from "@trigger.dev/core";
44
import { z } from "zod";
55
import { authenticateApiRequest } from "~/services/apiAuth.server";
66
import { logger } from "~/services/logger.server";
7-
import { RegisterTriggerSourceService } from "~/services/triggers/registerTriggerSource.server";
7+
import { RegisterTriggerSourceServiceV1 } from "~/services/triggers/registerTriggerSourceV1.server";
88

99
const ParamsSchema = z.object({
1010
endpointSlug: z.string(),
@@ -41,13 +41,13 @@ export async function action({ request, params }: ActionArgs) {
4141
// Now parse the request body
4242
const anyBody = await request.json();
4343

44-
const body = RegisterTriggerBodySchema.safeParse(anyBody);
44+
const body = RegisterTriggerBodySchemaV1.safeParse(anyBody);
4545

4646
if (!body.success) {
4747
return json({ error: "Invalid request body" }, { status: 400 });
4848
}
4949

50-
const service = new RegisterTriggerSourceService();
50+
const service = new RegisterTriggerSourceServiceV1();
5151

5252
try {
5353
const registration = await service.call({

apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class RunTaskService {
183183
},
184184
},
185185
parent: taskBody.parentId ? { connect: { id: taskBody.parentId } } : undefined,
186-
name: taskBody.name,
186+
name: taskBody.name ?? "Task",
187187
description: taskBody.description,
188188
status,
189189
startedAt: new Date(),
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { ActionArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import { UpdateTriggerSourceBodyV2Schema } from "@trigger.dev/core";
4+
import { z } from "zod";
5+
import { authenticateApiRequest } from "~/services/apiAuth.server";
6+
import { logger } from "~/services/logger.server";
7+
import { UpdateSourceServiceV2 } from "~/services/sources/updateSourceV2.server";
8+
9+
const ParamsSchema = z.object({
10+
endpointSlug: z.string(),
11+
id: z.string(),
12+
});
13+
14+
export async function action({ request, params }: ActionArgs) {
15+
logger.info("Updating source", { url: request.url });
16+
17+
// Ensure this is a POST request
18+
if (request.method.toUpperCase() !== "PUT") {
19+
return { status: 405, body: "Method Not Allowed" };
20+
}
21+
22+
const parsedParams = ParamsSchema.safeParse(params);
23+
24+
if (!parsedParams.success) {
25+
logger.info("Invalid params", { params });
26+
27+
return json({ error: "Invalid params" }, { status: 400 });
28+
}
29+
30+
// Next authenticate the request
31+
const authenticationResult = await authenticateApiRequest(request);
32+
33+
if (!authenticationResult) {
34+
logger.info("Invalid or missing api key", { url: request.url });
35+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
36+
}
37+
38+
const authenticatedEnv = authenticationResult.environment;
39+
40+
// Now parse the request body
41+
const anyBody = await request.json();
42+
43+
const body = UpdateTriggerSourceBodyV2Schema.safeParse(anyBody);
44+
45+
if (!body.success) {
46+
return json({ error: "Invalid request body" }, { status: 400 });
47+
}
48+
49+
const service = new UpdateSourceServiceV2();
50+
51+
try {
52+
const source = await service.call({
53+
environment: authenticatedEnv,
54+
payload: body.data,
55+
endpointSlug: parsedParams.data.endpointSlug,
56+
id: parsedParams.data.id,
57+
});
58+
59+
return json(source);
60+
} catch (error) {
61+
if (error instanceof Error) {
62+
logger.error("Error activating http source", {
63+
url: request.url,
64+
error: error.message,
65+
});
66+
67+
return json({ error: error.message }, { status: 400 });
68+
}
69+
70+
return json({ error: "Something went wrong" }, { status: 500 });
71+
}
72+
}

0 commit comments

Comments
 (0)