New integration: @trigger.dev/supabase (#203)

* WIP supabase integration

* supabase oauth working

* Supabase database triggers

* Specify postgres:14

* Limit refreshOAuthToken jobs to 10 attempts

* Better displaying types and removing onChange for now

* WIP on the supabase db client

* Finishing the supabase-js integration

* Adding changeset

* Added supabase to the integration catalogs, and added an optional icon to Integrations

* Reworking how we handle types for the triggers (wip)

* Update fully over to the new way to define supabase triggers

* Go back to using the type for the event name

* Add back in the icon to the JobListPresenter since it was moved from the ProjectPresenter

* Remove unused import
This commit is contained in:
Eric Allam
2023-07-26 18:01:27 +01:00
committed by GitHub
parent 8f050b40c1
commit 767e09ee18
45 changed files with 2005 additions and 191 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/supabase": patch
"@trigger.dev/sdk": patch
---
Added io.integration.runTask and initial @trigger.dev/supabase integration
@@ -28,12 +28,14 @@ export function ConnectToIntegrationSheet({
button,
className,
callbackUrl,
icon,
}: {
integration: Integration;
organizationId: string;
button: React.ReactNode;
callbackUrl: string;
className?: string;
icon?: string;
}) {
const [integrationMethod, setIntegrationMethod] = useState<
IntegrationMethod | undefined
@@ -48,7 +50,10 @@ export function ConnectToIntegrationSheet({
<SheetTrigger className={className}>{button}</SheetTrigger>
<SheetContent size="lg" className="relative">
<SheetHeader>
<NamedIconInBox name={integration.identifier} className="h-9 w-9" />
<NamedIconInBox
name={icon ?? integration.identifier}
className="h-9 w-9"
/>
<div className="grow">
<Header1>{integration.name}</Header1>
{integration.description && (
@@ -60,6 +60,7 @@ export function ConnectToOAuthForm({
},
] = useForm({
lastSubmission: fetcher.data,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, {
// Create the schema without any constraint defined
@@ -187,15 +188,16 @@ export function ConnectToOAuthForm({
</div>
)}
</div>
<div>
<Header2>Scopes</Header2>
<Paragraph variant="small" className="mb-4">
Select the scopes you want to grant to {integration.name} in order
for it to access your data. Note: If you try and perform an action
in a Job that requires a scope you havent granted, that task will
fail.
</Paragraph>
{/* <Header3 className="mb-2">
{authMethod.scopes.length > 0 && (
<div>
<Header2>Scopes</Header2>
<Paragraph variant="small" className="mb-4">
Select the scopes you want to grant to {integration.name} in order
for it to access your data. Note: If you try and perform an action
in a Job that requires a scope you havent granted, that task will
fail.
</Paragraph>
{/* <Header3 className="mb-2">
Select from popular scope collections
</Header3>
<fieldset>
@@ -205,60 +207,63 @@ export function ConnectToOAuthForm({
variant="button/small"
/>
</fieldset> */}
<div className="mb-2 mt-4 flex items-center justify-between">
<Header3>Select {integration.name} scopes</Header3>
<Paragraph variant="small" className="text-slate-500">
{simplur`${selectedScopes.size} scope[|s] selected`}
</Paragraph>
</div>
<Input
placeholder="Search scopes"
className="mb-2"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
{filteredItems.length === 0 && (
<Paragraph variant="small" className="p-4">
No scopes match {filterText}. Try a different search query.
<div className="mb-2 mt-4 flex items-center justify-between">
<Header3>Select {integration.name} scopes</Header3>
<Paragraph variant="small" className="text-slate-500">
{simplur`${selectedScopes.size} scope[|s] selected`}
</Paragraph>
)}
{authMethod.scopes.map((s) => {
return (
<Checkbox
key={s.name}
id={s.name}
value={s.name}
name="scopes"
label={s.name}
defaultChecked={s.defaultChecked ?? false}
badges={s.annotations?.map((a) => a.label)}
description={s.description}
variant="description"
className={cn(
filteredItems.find((f) => f.name === s.name) ? "" : "hidden"
)}
onChange={(isChecked) => {
if (isChecked) {
setSelectedScopes((selected) => {
selected.add(s.name);
return new Set(selected);
});
} else {
setSelectedScopes((selected) => {
selected.delete(s.name);
return new Set(selected);
});
}
}}
/>
);
})}
</div>
<Input
placeholder="Search scopes"
className="mb-2"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
{filteredItems.length === 0 && (
<Paragraph variant="small" className="p-4">
No scopes match {filterText}. Try a different search query.
</Paragraph>
)}
{authMethod.scopes.map((s) => {
return (
<Checkbox
key={s.name}
id={s.name}
value={s.name}
name="scopes"
label={s.name}
defaultChecked={s.defaultChecked ?? false}
badges={s.annotations?.map((a) => a.label)}
description={s.description}
variant="description"
className={cn(
filteredItems.find((f) => f.name === s.name)
? ""
: "hidden"
)}
onChange={(isChecked) => {
if (isChecked) {
setSelectedScopes((selected) => {
selected.add(s.name);
return new Set(selected);
});
} else {
setSelectedScopes((selected) => {
selected.delete(s.name);
return new Set(selected);
});
}
}}
/>
);
})}
</div>
</div>
</div>
)}
</Fieldset>
<div className="absolute bottom-0 left-0 flex w-full items-center justify-end gap-x-4 rounded-b-md border-t border-slate-800 bg-midnight-900 p-4">
@@ -268,7 +273,7 @@ export function ConnectToOAuthForm({
className="flex gap-2"
disabled={transition.state !== "idle"}
variant="primary/medium"
LeadingIcon={integration.identifier}
LeadingIcon={integration.icon ?? integration.identifier}
>
Connect to {integration.name}
</Button>
@@ -172,15 +172,16 @@ export function UpdateOAuthForm({
</div>
)}
</div>
<div>
<Header2>Scopes</Header2>
<Paragraph variant="small" className="mb-4">
Select the scopes you want to grant to {integration.name} in order
for it to access your data. Note: If you try and perform an action
in a Job that requires a scope you havent granted, that task will
fail.
</Paragraph>
{/* <Header3 className="mb-2">
{authMethod.scopes.length > 0 && (
<div>
<Header2>Scopes</Header2>
<Paragraph variant="small" className="mb-4">
Select the scopes you want to grant to {integration.name} in order
for it to access your data. Note: If you try and perform an action
in a Job that requires a scope you havent granted, that task will
fail.
</Paragraph>
{/* <Header3 className="mb-2">
Select from popular scope collections
</Header3>
<fieldset>
@@ -190,60 +191,63 @@ export function UpdateOAuthForm({
variant="button/small"
/>
</fieldset> */}
<div className="mb-2 mt-4 flex items-center justify-between">
<Header3>Select {integration.name} scopes</Header3>
<Paragraph variant="small" className="text-slate-500">
{simplur`${selectedScopes.size} scope[|s] selected`}
</Paragraph>
</div>
<Input
placeholder="Search scopes"
className="mb-2"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
{filteredItems.length === 0 && (
<Paragraph variant="small" className="p-4">
No scopes match {filterText}. Try a different search query.
<div className="mb-2 mt-4 flex items-center justify-between">
<Header3>Select {integration.name} scopes</Header3>
<Paragraph variant="small" className="text-slate-500">
{simplur`${selectedScopes.size} scope[|s] selected`}
</Paragraph>
)}
{authMethod.scopes.map((s) => {
return (
<Checkbox
key={s.name}
id={s.name}
value={s.name}
name="scopes"
label={s.name}
defaultChecked={s.defaultChecked ?? false}
badges={s.annotations?.map((a) => a.label)}
description={s.description}
variant="description"
className={cn(
filteredItems.find((f) => f.name === s.name) ? "" : "hidden"
)}
onChange={(isChecked) => {
if (isChecked) {
setSelectedScopes((selected) => {
selected.add(s.name);
return new Set(selected);
});
} else {
setSelectedScopes((selected) => {
selected.delete(s.name);
return new Set(selected);
});
}
}}
/>
);
})}
</div>
<Input
placeholder="Search scopes"
className="mb-2"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
{filteredItems.length === 0 && (
<Paragraph variant="small" className="p-4">
No scopes match {filterText}. Try a different search query.
</Paragraph>
)}
{authMethod.scopes.map((s) => {
return (
<Checkbox
key={s.name}
id={s.name}
value={s.name}
name="scopes"
label={s.name}
defaultChecked={s.defaultChecked ?? false}
badges={s.annotations?.map((a) => a.label)}
description={s.description}
variant="description"
className={cn(
filteredItems.find((f) => f.name === s.name)
? ""
: "hidden"
)}
onChange={(isChecked) => {
if (isChecked) {
setSelectedScopes((selected) => {
selected.add(s.name);
return new Set(selected);
});
} else {
setSelectedScopes((selected) => {
selected.delete(s.name);
return new Set(selected);
});
}
}}
/>
);
})}
</div>
</div>
</div>
)}
</Fieldset>
<div className="absolute bottom-0 left-0 flex w-full items-center justify-end gap-x-4 rounded-b-md border-t border-slate-800 bg-midnight-900 p-4">
+4 -1
View File
@@ -124,7 +124,10 @@ export function TaskCard({
)}
{connection && (
<RunPanelIconProperty
icon={connection.integration.definitionId}
icon={
connection.integration.definition.icon ??
connection.integration.definitionId
}
label="Connection"
value={connection.integration.slug}
/>
@@ -48,6 +48,7 @@ export class IntegrationClientPresenter {
id: true,
name: true,
packageName: true,
icon: true,
},
},
connectionType: true,
@@ -119,6 +120,7 @@ export class IntegrationClientPresenter {
identifier: integration.definition.id,
name: integration.definition.name,
packageName: integration.definition.packageName,
icon: integration.definition.icon,
},
authMethod: {
type: integration.authMethod?.type ?? "local",
@@ -51,6 +51,7 @@ export class IntegrationsPresenter {
select: {
id: true,
name: true,
icon: true,
},
},
authSource: true,
@@ -111,6 +112,7 @@ export class IntegrationsPresenter {
return {
id: c.id,
title: c.title ?? c.slug,
icon: c.definition.icon ?? c.definition.id,
slug: c.slug,
integrationIdentifier: c.definition.id,
description: c.description,
@@ -158,7 +158,9 @@ export class JobListPresenter {
const integrations = alias.version.integrations.map((integration) => ({
key: integration.key,
title: integration.integration.slug,
icon: integration.integration.definition.id,
icon:
integration.integration.definition.icon ??
integration.integration.definition.id,
setupStatus: integration.integration.setupStatus,
}));
@@ -46,6 +46,11 @@ const taskSelect = {
definitionId: true,
title: true,
slug: true,
definition: {
select: {
icon: true,
},
},
},
},
},
@@ -41,6 +41,11 @@ export class TriggerSourcePresenter {
slug: true,
definitionId: true,
setupStatus: true,
definition: {
select: {
icon: true,
},
},
},
},
environment: {
@@ -36,6 +36,11 @@ export class TriggersPresenter {
slug: true,
definitionId: true,
setupStatus: true,
definition: {
select: {
icon: true,
},
},
},
},
environment: {
@@ -185,10 +185,12 @@ function PossibleIntegrationsList({
integration={option}
organizationId={organizationId}
callbackUrl={callbackUrl}
icon={option.icon}
button={
<AddIntegrationConnection
identifier={option.identifier}
name={option.name}
icon={option.icon}
isIntegration
/>
}
@@ -330,10 +332,7 @@ function ConnectedIntegrationsList({
<TableCell to={path}>{client.title}</TableCell>
<TableCell to={path}>
<span className="flex items-center gap-1">
<NamedIcon
name={client.integrationIdentifier}
className="h-5 w-5"
/>
<NamedIcon name={client.icon} className="h-5 w-5" />
{client.integration.name}
</span>
</TableCell>
@@ -503,15 +502,17 @@ function AddIntegrationConnection({
identifier,
name,
isIntegration,
icon,
}: {
identifier: string;
name: string;
isIntegration: boolean;
icon?: string;
}) {
return (
<div className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
<NamedIconInBox
name={identifier}
name={icon ?? identifier}
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
/>
<Paragraph
@@ -105,7 +105,7 @@ export default function Integrations() {
}
/>
<PageInfoProperty
icon={client.integration.identifier}
icon={client.integration.icon ?? client.integration.identifier}
label="API"
value={client.integration.name}
/>
@@ -87,7 +87,10 @@ export default function Integrations() {
<TableCell to={path}>
<div className="flex items-center gap-1">
<NamedIcon
name={t.integration.definitionId}
name={
t.integration.definition.icon ??
t.integration.definitionId
}
className="h-8 w-8"
/>
<LabelValueStack
@@ -170,7 +170,10 @@ export default function Page() {
<PageInfoRow>
<PageInfoGroup>
<PageInfoProperty
icon={trigger.integration.definitionId}
icon={
trigger.integration.definition.icon ??
trigger.integration.definitionId
}
label={trigger.integration.title ?? ""}
value={trigger.integration.slug}
/>
@@ -5,6 +5,7 @@ import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
import { OAuthClient, OAuthClientSchema } from "~/services/externalApis/types";
import { logger } from "~/services/logger.server";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { requestUrl } from "~/utils/requestUrl.server";
@@ -22,6 +23,7 @@ export async function loader({ request }: LoaderArgs) {
}
const url = requestUrl(request);
const parsedParams = ParamsSchema.safeParse(
Object.fromEntries(url.searchParams)
);
@@ -75,11 +75,7 @@ export function createSchema(
redirectTo: z.string(),
scopes: z.preprocess(
(data) => (typeof data === "string" ? [data] : data),
z
.array(z.string(), {
required_error: "You must select at least one scope",
})
.nonempty("You must select at least one scope")
z.array(z.string()).default([])
),
})
.refine(
@@ -1,17 +1,16 @@
import type {
SecretReference,
ExternalAccount,
IntegrationConnection,
ConnectionType,
Integration,
ConnectionAttempt,
ConnectionType,
ExternalAccount,
Integration,
IntegrationAuthMethod,
IntegrationConnection,
IntegrationDefinition,
SecretReference,
} from "@trigger.dev/database";
import jsonpointer from "jsonpointer";
import { customAlphabet } from "nanoid";
import * as crypto from "node:crypto";
import createSlug from "slug";
import type {
PrismaClient,
PrismaClientOrTransaction,
@@ -31,7 +30,7 @@ import {
} from "./oauth2.server";
import {
AccessToken,
ApiAuthenticationMethod,
AccessTokenSchema,
ApiAuthenticationMethodOAuth2,
ConnectionMetadata,
GrantTokenParams,
@@ -39,7 +38,7 @@ import {
OAuthClientSchema,
RefreshTokenParams,
} from "./types";
import { AccessTokenSchema } from "./types";
import { logger } from "../logger.server";
export type ConnectionWithSecretReference = IntegrationConnection & {
dataReference: SecretReference;
@@ -127,6 +126,16 @@ export class IntegrationAuthRepository {
});
}
logger.debug("Creating Integration", {
id,
clientType,
scopes,
title,
slug,
integrationIdentifier,
integrationAuthMethod,
});
const client = await tx.integration.create({
data: {
id,
@@ -391,6 +400,8 @@ export class IntegrationAuthRepository {
expiresInPointer:
authMethod.config.token.expiresInPointer ?? "/expires_in",
scopePointer: authMethod.config.token.scopePointer ?? "/scope",
authorizationMethod: authMethod.config.token.authorizationMethod,
bodyFormat: authMethod.config.token.bodyFormat,
};
const token = await grantOAuth2Token(
@@ -662,6 +673,9 @@ export class IntegrationAuthRepository {
expiresInPointer:
authMethod.config.token.expiresInPointer ?? "/expires_in",
scopePointer: authMethod.config.token.scopePointer ?? "/scope",
authorizationMethod: authMethod.config.token.authorizationMethod,
bodyFormat: authMethod.config.token.bodyFormat,
skipScopes: authMethod.config.refresh.skipScopes,
};
//todo do we need pkce here?
@@ -3,6 +3,7 @@ import { openai } from "./integrations/openai";
import { plain } from "./integrations/plain";
import { resend } from "./integrations/resend";
import { slack } from "./integrations/slack";
import { supabaseManagement, supabase } from "./integrations/supabase";
import { typeform } from "./integrations/typeform";
import type { Integration } from "./types";
@@ -35,4 +36,6 @@ export const integrationCatalog = new IntegrationCatalog({
resend,
slack,
typeform,
supabaseManagement,
supabase,
});
@@ -0,0 +1,183 @@
import type { HelpSample, Integration } from "../types";
const managementUsageSample: HelpSample = {
title: "Using the client",
code: `
import { SupabaseManagement } from "@trigger.dev/supabase";
const supabase = new SupabaseManagement({
id: "__SLUG__",
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
}),
run: async (payload, io, ctx) => {
},
});
`,
};
const managementApiKeyUsageSample: HelpSample = {
title: "Using the client",
code: `
import { SupabaseManagement } from "@trigger.dev/supabase";
const supabase = new SupabaseManagement({
id: "__SLUG__",
apiKey: process.env.SUPABASE_API_KEY!,
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
}),
run: async (payload, io, ctx) => {
},
});
`,
};
export const supabaseManagement: Integration = {
identifier: "supabase-management",
icon: "supabase",
name: "Supabase Management",
packageName: "@trigger.dev/supabase",
authenticationMethods: {
apikey: {
type: "apikey",
help: {
samples: [
{
title: "Creating the client",
code: `
import { SupabaseManagement } from "@trigger.dev/supabase";
const supabase = new SupabaseManagement({
id: "__SLUG__"
apiKey: process.env.SUPABASE_API_KEY!,
});
`,
},
managementApiKeyUsageSample,
],
},
},
oauth2: {
name: "OAuth",
type: "oauth2",
client: {
id: {
envName: "CLOUD_SUPABASE_CLIENT_ID",
},
secret: {
envName: "CLOUD_SUPABASE_CLIENT_SECRET",
},
},
config: {
authorization: {
url: "https://api.supabase.com/v1/oauth/authorize",
scopeSeparator: " ",
},
token: {
url: "https://api.supabase.com/v1/oauth/token",
metadata: { accountPointer: "/team/name" },
authorizationMethod: "body",
},
refresh: {
url: "https://api.supabase.com/v1/oauth/token",
skipScopes: true,
},
},
scopes: [
{
name: "all",
description:
"Grants full access to all resources available in the Supabase Management API.",
defaultChecked: true,
},
],
help: {
samples: [
{
title: "Creating the client",
code: `
import { SupabaseManagement } from "@trigger.dev/supabase";
const supabase = new SupabaseManagement({
id: "__SLUG__"
});
`,
},
managementUsageSample,
],
},
},
},
};
const supabaseUsageSample: HelpSample = {
title: "Using the client",
code: `
import { Supabase } from "@trigger.dev/supabase";
import { Database } from "@/supabase.types";
const supabase = new Supabase<Database>({
id: "__SLUG__",
projectId: process.env.SUPABASE_ID!,
supabaseKey: process.env.SUPABASE_API_KEY!,
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
version: "0.1.1",
trigger: eventTrigger({
name: "foo.bar
}),
integrations: {
supabase
},
run: async (payload, io, ctx) => {
await io.supabase.runTask("get-users", async (db) => {
return await db.from("users").select("*");
});
},
});
`,
};
export const supabase: Integration = {
identifier: "supabase",
icon: "supabase",
name: "Supabase",
packageName: "@trigger.dev/supabase",
authenticationMethods: {
apikey: {
type: "apikey",
help: {
samples: [
{
title: "Creating the client",
code: `
import { Supabase } from "@trigger.dev/supabase";
const supabase = new Supabase({
id: "__SLUG__"
projectId: process.env.SUPABASE_ID!,
supabaseKey: process.env.SUPABASE_KEY!,
});
`,
},
],
},
},
},
};
@@ -1,5 +1,6 @@
import simpleOauth2 from "simple-oauth2";
import jsonpointer from "jsonpointer";
import * as crypto from "node:crypto";
import simpleOauth2 from "simple-oauth2";
import type {
AccessToken,
CreateUrlParams,
@@ -7,7 +8,6 @@ import type {
OAuthClient,
RefreshTokenParams,
} from "./types";
import jsonpointer from "jsonpointer";
export function getClientConfig({
env,
@@ -126,6 +126,8 @@ export async function grantOAuth2Token(
expiresInPointer,
scopePointer,
pkceCode,
authorizationMethod,
bodyFormat,
}: GrantTokenParams,
strategy?: string
): Promise<AccessToken> {
@@ -145,7 +147,11 @@ export async function grantOAuth2Token(
tokenHost: `${tokenUrlObj.protocol}//${tokenUrlObj.host}`,
tokenPath: tokenUrlObj.pathname,
},
};
options: {
authorizationMethod,
bodyFormat,
},
} as const;
const simpleOAuthClient = new simpleOauth2.AuthorizationCode(clientConfig);
@@ -161,7 +167,6 @@ export async function grantOAuth2Token(
const token = await simpleOAuthClient.getToken({
code,
redirect_uri: callbackUrl,
scope: requestedScopes.join(scopeSeparator),
...pkceParams,
});
@@ -188,6 +193,9 @@ export async function refreshOAuth2Token(
refreshTokenPointer,
expiresInPointer,
scopePointer,
authorizationMethod,
bodyFormat,
skipScopes,
}: RefreshTokenParams,
strategy?: string
) {
@@ -208,6 +216,10 @@ export async function refreshOAuth2Token(
tokenPath: tokenUrlObj.pathname,
refreshPath: tokenUrlObj.pathname,
},
options: {
authorizationMethod,
bodyFormat,
},
};
const simpleOAuthClient = new simpleOauth2.AuthorizationCode(clientConfig);
@@ -220,7 +232,7 @@ export async function refreshOAuth2Token(
});
const newToken = await oldToken.refresh({
scope: requestedScopes.join(scopeSeparator),
...(skipScopes ? {} : { scope: requestedScopes.join(scopeSeparator) }),
});
return convertToken({
@@ -3,6 +3,8 @@ import { z } from "zod";
export type Integration = {
/** Used to uniquely identify an integration */
identifier: string;
/** identifier is used by default as the icon name, but you can specify a different one using icon */
icon?: string;
/** The name of the integration */
name: string;
/** The description of the integration */
@@ -93,10 +95,20 @@ export type ApiAuthenticationMethodOAuth2 = {
scopePointer?: string;
/** Some APIs have strange granting logic, this allows total control to deal with that */
grantTokenStrategy?: string;
/** Format of data sent in the request body. Defaults to form. */
bodyFormat?: "form" | "json";
/**
* Indicates the method used to send the client.id/client.secret authorization params at the token request.
* If set to body, the bodyFormat option will be used to format the credentials.
* Defaults to header
*/
authorizationMethod?: "header" | "body";
};
/** Refresh is how a token is refreshed */
refresh: {
url: string;
/** Skip including scopes with the refresh_token request */
skipScopes?: boolean;
/** Some APIs have strange refreshing logic, this allows total control to deal with that */
refreshTokenStrategy?: string;
};
@@ -141,6 +153,8 @@ export type GrantTokenParams = {
expiresInPointer: string;
scopePointer: string;
pkceCode?: string;
authorizationMethod?: "header" | "body";
bodyFormat?: "form" | "json";
};
export type RefreshTokenParams = {
@@ -154,6 +168,9 @@ export type RefreshTokenParams = {
refreshTokenPointer: string;
expiresInPointer: string;
scopePointer: string;
authorizationMethod?: "header" | "body";
bodyFormat?: "form" | "json";
skipScopes?: boolean;
};
type AdditionalField = {
@@ -216,6 +216,7 @@ function getWorkerQueue() {
},
refreshOAuthToken: {
queueName: "internal-queue",
maxAttempts: 10,
handler: async (payload, job) => {
await integrationAuthRepository.refreshConnection({
connectionId: payload.connectionId,
+8 -6
View File
@@ -5,16 +5,17 @@ import { seedCloud } from "./seedCloud";
import { prisma } from "../app/db.server";
async function seedIntegrationAuthMethods() {
for (const [identifier, integration] of Object.entries(
for (const [_, integration] of Object.entries(
integrationCatalog.getIntegrations()
)) {
await prisma.integrationDefinition.upsert({
where: {
id: identifier,
id: integration.identifier,
},
create: {
id: identifier,
id: integration.identifier,
name: integration.name,
icon: integration.icon ?? integration.identifier,
instructions: "Instructions go here",
description: integration.description,
packageName: integration.packageName,
@@ -23,6 +24,7 @@ async function seedIntegrationAuthMethods() {
name: integration.name,
description: integration.description,
packageName: integration.packageName,
icon: integration.icon ?? integration.identifier,
},
});
@@ -30,12 +32,12 @@ async function seedIntegrationAuthMethods() {
integration.authenticationMethods
)) {
if (authMethod.type === "oauth2") {
console.log(`Upserting auth method ${identifier}.${key}`);
console.log(`Upserting auth method ${integration.identifier}.${key}`);
await prisma.integrationAuthMethod.upsert({
where: {
definitionId_key: {
definitionId: identifier,
definitionId: integration.identifier,
key,
},
},
@@ -49,7 +51,7 @@ async function seedIntegrationAuthMethods() {
scopes: authMethod.scopes,
definition: {
connect: {
id: identifier,
id: integration.identifier,
},
},
help: authMethod.help,
+1 -1
View File
@@ -10,7 +10,7 @@ networks:
services:
database:
container_name: database
image: postgres:latest
image: postgres:14
restart: always
volumes:
- database-data:/var/lib/postgresql/data/
+4 -2
View File
@@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"generate:types": "npx supabase gen types typescript --project-id axtbanoixaztvdntngew --schema public --schema public_2 > src/supabase.types.ts"
},
"dependencies": {
"@trigger.dev/github": "workspace:*",
@@ -25,7 +26,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "5.0.4",
"zod": "3.21.4"
"zod": "3.21.4",
"@trigger.dev/supabase": "workspace:*"
},
"devDependencies": {
"eslint": "8.42.0",
@@ -0,0 +1,284 @@
import { Database } from "@/supabase.types";
import { client } from "@/trigger";
import { Job, eventTrigger } from "@trigger.dev/sdk";
import { SupabaseManagement, Supabase } from "@trigger.dev/supabase";
import { z } from "zod";
const supabase = new SupabaseManagement({
id: "supabase",
});
const db = supabase.db<Database>(process.env.SUPABASE_ID!);
const dbNoTypes = supabase.db(process.env.SUPABASE_ID!);
const supabaseManagementKey = new SupabaseManagement({
id: "supabase-management-key",
apiKey: process.env.SUPABASE_API_KEY!,
});
const dbKey = supabase.db<Database>(process.env.SUPABASE_ID!);
const supabaseDB = new Supabase<Database>({
id: "supabase-db",
supabaseUrl: `https://${process.env.SUPABASE_ID}.supabase.co`,
supabaseKey: process.env.SUPABASE_KEY!,
});
new Job(client, {
id: "supabase-playground",
name: "Supabase Playground",
version: "0.1.1",
trigger: eventTrigger({
name: "supabase.playground",
}),
integrations: {
supabase,
supabaseDB,
supabaseManagementKey,
},
run: async (payload, io, ctx) => {
await io.supabaseManagementKey.getPGConfig("get-pg-config", {
ref: payload.ref,
});
await io.supabase.getOrganizations("get-orgs");
await io.supabase.getProjects("get-projects");
await io.supabase.listFunctions("list-functions", {
ref: payload.ref,
});
await io.supabase.runQuery("run-query", {
ref: payload.ref,
query: "SELECT * FROM users",
});
await io.supabase.getTypescriptTypes("get-typescript-types", {
ref: payload.ref,
});
const users = await io.supabaseDB.runTask(
"fetch-users",
async (db) => {
const { data, error } = await db.from("users").select("*");
if (error) throw error;
return data;
},
{ name: "Fetch Users" }
);
const newUser = await io.supabaseDB.runTask(
"create-user",
async (db) => {
return await db
.from("users")
.insert({
first_name: "John",
last_name: "Doe",
email_address: "john@trigger.dev",
})
.select();
},
{ name: "New Users" }
);
},
});
new Job(client, {
id: "supabase-create-todo",
name: "Supabase Create Todo",
version: "0.1.1",
trigger: eventTrigger({
name: "supabase.create-todo",
schema: z.object({
contents: z.string(),
user_id: z.number(),
}),
}),
integrations: {
supabaseDB,
},
run: async (payload, io, ctx) => {
const newTodo = await io.supabaseDB.runTask(
"create-todo",
async (db) => {
const { data, error } = await db
.from("todos")
.insert({
contents: payload.contents,
user_id: payload.user_id,
is_complete: false,
})
.select();
if (error) throw error;
return data;
},
{
name: "Create Todo",
properties: [{ label: "Contents", text: payload.contents }],
}
);
},
});
new Job(client, {
id: "supabase-create-project",
name: "Supabase Create Project",
version: "0.1.1",
trigger: eventTrigger({
name: "supabase.create",
schema: z.object({
name: z.string(),
organization_id: z.string(),
plan: z.enum(["free", "pro"]),
region: z.enum(["us-east-1", "us-west-1"]),
password: z.string(),
}),
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {
await io.supabase.createProject("create-project", {
name: payload.name,
organization_id: payload.organization_id,
plan: payload.plan,
region: payload.region,
kps_enabled: true,
db_pass: payload.password,
});
},
});
new Job(client, {
id: "supabase-on-user-insert",
name: "Supabase On User Insert",
version: "0.1.1",
trigger: db.onInserted({
table: "users",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-user-insert-2",
name: "Supabase On User Insert 2",
version: "0.1.1",
trigger: db.onInserted({
table: "users",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-user-email-changed",
name: "Supabase On User Email Changed",
version: "0.1.1",
trigger: db.onUpdated({
table: "users",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-user-deleted",
name: "Supabase On User Deleted",
version: "0.1.1",
trigger: db.onDeleted({
table: "users",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-todo-created",
name: "Supabase On TODO created",
version: "0.1.1",
trigger: dbKey.onInserted({
table: "todos",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-todo-created",
name: "Supabase On TODO created",
version: "0.1.1",
trigger: dbKey.onInserted({
table: "todos",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-todo-completed",
name: "Supabase On TODO completed",
version: "0.1.1",
trigger: dbKey.onUpdated({
table: "todos",
filter: {
old_record: {
is_complete: [false],
},
record: {
is_complete: [true],
},
},
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {
await io.logger.log("Todo Completed", { payload });
},
});
new Job(client, {
id: "supabase-on-tweet-created-or-deleted",
name: "Supabase On Tweet Created or Deleted",
version: "0.1.1",
trigger: dbKey.onDeleted({
schema: "public_2",
table: "tweets",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
new Job(client, {
id: "supabase-on-todo-created-no-types",
name: "Supabase On TODO created",
version: "0.1.1",
trigger: dbNoTypes.onInserted({
table: "todos",
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {},
});
@@ -11,6 +11,7 @@ import "@/jobs/schedules";
import "@/jobs/slack";
import "@/jobs/typeform";
import "@/jobs/edgeCases";
import "@/jobs/supabase";
import { createPagesRoute } from "@trigger.dev/nextjs";
@@ -0,0 +1,118 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
public: {
Tables: {
todos: {
Row: {
contents: string | null
created_at: string | null
id: number
is_complete: boolean | null
user_id: number | null
}
Insert: {
contents?: string | null
created_at?: string | null
id?: number
is_complete?: boolean | null
user_id?: number | null
}
Update: {
contents?: string | null
created_at?: string | null
id?: number
is_complete?: boolean | null
user_id?: number | null
}
Relationships: [
{
foreignKeyName: "todos_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
users: {
Row: {
created_at: string | null
email_address: string | null
first_name: string | null
id: number
last_name: string | null
}
Insert: {
created_at?: string | null
email_address?: string | null
first_name?: string | null
id?: number
last_name?: string | null
}
Update: {
created_at?: string | null
email_address?: string | null
first_name?: string | null
id?: number
last_name?: string | null
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public_2: {
Tables: {
tweets: {
Row: {
content: string
created_at: string | null
id: number
tweet_id: string
}
Insert: {
content: string
created_at?: string | null
id?: number
tweet_id: string
}
Update: {
content?: string
created_at?: string | null
id?: number
tweet_id?: string
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
+78 -23
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -15,31 +19,75 @@
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./src/*"],
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
"@trigger.dev/nextjs": ["../../packages/nextjs/src/index"],
"@trigger.dev/nextjs/*": ["../../packages/nextjs/src/*"],
"@trigger.dev/internal": ["../../packages/internal/src/index"],
"@trigger.dev/internal/*": ["../../packages/internal/src/*"],
"@/*": [
"./src/*"
],
"@trigger.dev/sdk": [
"../../packages/trigger-sdk/src/index"
],
"@trigger.dev/sdk/*": [
"../../packages/trigger-sdk/src/*"
],
"@trigger.dev/nextjs": [
"../../packages/nextjs/src/index"
],
"@trigger.dev/nextjs/*": [
"../../packages/nextjs/src/*"
],
"@trigger.dev/internal": [
"../../packages/internal/src/index"
],
"@trigger.dev/internal/*": [
"../../packages/internal/src/*"
],
"@trigger.dev/integration-kit": [
"../../packages/integration-kit/src/index"
],
"@trigger.dev/integration-kit/*": [
"../../packages/integration-kit/src/*"
],
"@trigger.dev/github": ["../../integrations/github/src/index"],
"@trigger.dev/github/*": ["../../integrations/github/src/*"],
"@trigger.dev/slack": ["../../integrations/slack/src/index"],
"@trigger.dev/slack/*": ["../../integrations/slack/src/*"],
"@trigger.dev/openai": ["../../integrations/openai/src/index"],
"@trigger.dev/openai/*": ["../../integrations/openai/src/*"],
"@trigger.dev/resend": ["../../integrations/resend/src/index"],
"@trigger.dev/resend/*": ["../../integrations/resend/src/*"],
"@trigger.dev/typeform": ["../../integrations/typeform/src/index"],
"@trigger.dev/typeform/*": ["../../integrations/typeform/src/*"],
"@trigger.dev/plain": ["../../integrations/plain/src/index"],
"@trigger.dev/plain/*": ["../../integrations/plain/src/*"]
"@trigger.dev/github": [
"../../integrations/github/src/index"
],
"@trigger.dev/github/*": [
"../../integrations/github/src/*"
],
"@trigger.dev/slack": [
"../../integrations/slack/src/index"
],
"@trigger.dev/slack/*": [
"../../integrations/slack/src/*"
],
"@trigger.dev/openai": [
"../../integrations/openai/src/index"
],
"@trigger.dev/openai/*": [
"../../integrations/openai/src/*"
],
"@trigger.dev/resend": [
"../../integrations/resend/src/index"
],
"@trigger.dev/resend/*": [
"../../integrations/resend/src/*"
],
"@trigger.dev/typeform": [
"../../integrations/typeform/src/index"
],
"@trigger.dev/typeform/*": [
"../../integrations/typeform/src/*"
],
"@trigger.dev/plain": [
"../../integrations/plain/src/index"
],
"@trigger.dev/plain/*": [
"../../integrations/plain/src/*"
],
"@trigger.dev/supabase": [
"../../integrations/supabase/src/index"
],
"@trigger.dev/supabase/*": [
"../../integrations/supabase/src/*"
]
},
"plugins": [
{
@@ -47,6 +95,13 @@
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
+3
View File
@@ -0,0 +1,3 @@
# @trigger.dev/supabase
The official Supabase integration for [Trigger.dev](https://trigger.dev). See our [docs](https://trigger.dev/docs/integrations/apis/supabase) for more info.
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@trigger.dev/supabase",
"version": "0.1.0",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"dist/index.js",
"dist/index.d.ts",
"dist/index.js.map"
],
"devDependencies": {
"@types/node": "18.x",
"rimraf": "^3.0.2",
"tsup": "7.1.x",
"typescript": "4.9.4"
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run build:tsup",
"build:tsup": "tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^2.0.0-next.5",
"@trigger.dev/sdk": "workspace:^2.0.0-next.16",
"supabase-management-js": "^0.1.2",
"zod": "3.21.4"
},
"engines": {
"node": ">=18.0.0"
}
}
@@ -0,0 +1,99 @@
import {
SupabaseClient,
SupabaseClientOptions,
createClient,
} from "@supabase/supabase-js";
import { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
import { GenericSchema } from "./types";
const tasks = {};
export type SupabaseIntegrationOptions<TSchema extends string> =
| {
/** The unique ID for this integration */
id: string;
/** The Supabase project url (e.g. "https://<project-id>.supabase.co") */
supabaseUrl: string;
/** The Supabase service account API Key (found in your Supabase Project Settings -> API -> service_role) */
supabaseKey: string;
/** Options that are passed through to the call the createClient */
options?: SupabaseClientOptions<TSchema>;
}
| {
id: string;
projectId: string;
supabaseKey: string;
options?: SupabaseClientOptions<TSchema>;
};
/**
* A Trigger Integration for Supabase
*
* @example
* ```ts
* import { Supabase } from "@trigger.dev/supabase";
* import { Database } from "@/supabase.types";
*
* const supabase = new Supabase<Database>({
* id: "my-supabase",
* projectId: process.env.SUPABASE_ID!,
* supabaseKey: process.env.SUPABASE_API_KEY!,
* });
* ```
*/
export class Supabase<
Database = any,
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any
> implements
TriggerIntegration<
IntegrationClient<
SupabaseClient<Database, SchemaName, Schema>,
typeof tasks
>
>
{
client: IntegrationClient<
SupabaseClient<Database, SchemaName, Schema>,
typeof tasks
>;
constructor(private options: SupabaseIntegrationOptions<SchemaName>) {
const supabaseOptions = options.options || {};
const supabaseUrl =
"projectId" in options
? `https://${options.projectId}.supabase.co`
: options.supabaseUrl;
const supabaseClient = createClient(supabaseUrl, options.supabaseKey, {
...supabaseOptions,
auth: {
...supabaseOptions.auth,
persistSession: false,
},
});
this.client = {
tasks,
usesLocalAuth: true,
client: supabaseClient,
auth: {
supabaseUrl,
supabaseKey: options.supabaseKey,
},
};
}
get id() {
return this.options.id;
}
get metadata() {
return { id: "supabase", name: "Supabase" };
}
}
@@ -0,0 +1,28 @@
export type GenericTable = {
Row: Record<string, unknown>;
Insert: Record<string, unknown>;
Update: Record<string, unknown>;
};
export type GenericUpdatableView = {
Row: Record<string, unknown>;
Insert: Record<string, unknown>;
Update: Record<string, unknown>;
};
export type GenericNonUpdatableView = {
Row: Record<string, unknown>;
};
export type GenericView = GenericUpdatableView | GenericNonUpdatableView;
export type GenericFunction = {
Args: Record<string, unknown>;
Returns: unknown;
};
export type GenericSchema = {
Tables: Record<string, GenericTable>;
Views: Record<string, GenericView>;
Functions: Record<string, GenericFunction>;
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./management";
export * from "./database";
@@ -0,0 +1,431 @@
import {
EventFilter,
EventSpecification,
ExternalSource,
ExternalSourceTrigger,
HandlerEvent,
IntegrationClient,
Logger,
TriggerIntegration,
} from "@trigger.dev/sdk";
import { SupabaseManagementAPI } from "supabase-management-js";
import { z } from "zod";
import { Prettify, safeParseBody } from "@trigger.dev/integration-kit";
import * as tasks from "./tasks";
import { randomUUID } from "crypto";
import { GenericSchema } from "../database/types";
export type SupabaseManagementIntegrationOptions =
| {
id: string;
}
| {
id: string;
apiKey: string;
};
type SupabaseManagementIntegrationClient = IntegrationClient<
SupabaseManagementAPI,
typeof tasks
>;
type SupabaseManagementIntegration =
TriggerIntegration<SupabaseManagementIntegrationClient>;
class SupabaseDatabase<Database = any> {
constructor(
private integration: SupabaseManagement,
private projectRef: string
) {}
onInserted<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any,
TTableName extends string & keyof Schema["Tables"] = string &
keyof Schema["Tables"],
TTable extends Schema["Tables"][TTableName] = Schema["Tables"][TTableName]
>(params: { table: TTableName; schema?: SchemaName; filter?: EventFilter }) {
return createTrigger<{
table: TTableName;
record: Prettify<TTable["Row"]>;
type: "INSERT";
schema: SchemaName;
old_record: null;
}>(this.integration.source, {
event: "INSERT",
projectRef: this.projectRef,
...params,
});
}
onUpdated<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any,
TTableName extends string & keyof Schema["Tables"] = string &
keyof Schema["Tables"],
TTable extends Schema["Tables"][TTableName] = Schema["Tables"][TTableName]
>(params: { table: TTableName; schema?: SchemaName; filter?: EventFilter }) {
return createTrigger<{
table: TTableName;
record: Prettify<TTable["Row"]>;
type: "UPDATE";
schema: SchemaName;
old_record: Prettify<TTable["Row"]>;
}>(this.integration.source, {
event: "UPDATE",
projectRef: this.projectRef,
...params,
});
}
onDeleted<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any,
TTableName extends string & keyof Schema["Tables"] = string &
keyof Schema["Tables"],
TTable extends Schema["Tables"][TTableName] = Schema["Tables"][TTableName]
>(params: { table: TTableName; schema?: SchemaName; filter?: EventFilter }) {
return createTrigger<{
table: TTableName;
record: null;
type: "DELETE";
schema: SchemaName;
old_record: Prettify<TTable["Row"]>;
}>(this.integration.source, {
event: "DELETE",
projectRef: this.projectRef,
...params,
});
}
}
export class SupabaseManagement implements SupabaseManagementIntegration {
client: SupabaseManagementIntegrationClient;
constructor(private options: SupabaseManagementIntegrationOptions) {
if ("apiKey" in options) {
if (!options.apiKey || options.apiKey === "") {
throw `Can't create SupabaseManagement integration (${options.id}) as apiKey is undefined`;
}
this.client = {
tasks,
usesLocalAuth: true,
client: new SupabaseManagementAPI({ accessToken: options.apiKey }),
auth: {
apiKey: options.apiKey,
},
};
} else {
this.client = {
tasks,
usesLocalAuth: false,
clientFactory: (auth) => {
return new SupabaseManagementAPI({ accessToken: auth.accessToken });
},
};
}
}
get id() {
return this.options.id;
}
get metadata() {
return { id: "supabase-management", name: "Supabase Management API" };
}
get source(): WebhookEventSource {
return createWebhookEventSource(this);
}
/**
* Creates a new database instance that can be used to listen to changes in the database.
*
* @param projectIdOrUrl The project ID or URL of the Supabase project (e.g. `https://<project-id>.supabase.co`)
* @param options Options for the database instance
*/
db<Database = any>(projectIdOrUrl: string) {
const projectRef = getProjectRef(projectIdOrUrl);
return new SupabaseDatabase<Database>(this, projectRef);
}
}
/**
*
* @param projectIdOrUrl The project ID or URL of the Supabase project (e.g. `https://<project-id>.supabase.co`)
* @returns The project reference of the Supabase project (e.g. `<project-id>`)
*/
function getProjectRef(projectIdOrUrl: string) {
if (projectIdOrUrl.startsWith("http")) {
const url = new URL(projectIdOrUrl);
return url.hostname.split(".")[0];
}
return projectIdOrUrl;
}
type WebhookEventSource = ReturnType<typeof createWebhookEventSource>;
type WebhookEvents = "INSERT" | "UPDATE" | "DELETE";
function createTrigger<TEvent extends any>(
source: WebhookEventSource,
params: { event: WebhookEvents; filter?: EventFilter } & {
projectRef: string;
table: string;
schema?: string;
}
): ExternalSourceTrigger<EventSpecification<TEvent>, WebhookEventSource> {
const eventSpecification = {
name: params.event,
title: "Supabase DB Webhook",
source: "supabase",
icon: "supabase",
filter: {
...params.filter,
type: [params.event],
schema: [params.schema ?? "public"],
},
properties: [],
parsePayload: (payload: any) => payload as TEvent,
};
return new ExternalSourceTrigger({
event: eventSpecification,
params: {
projectRef: params.projectRef,
table: params.table,
schema: params.schema ?? "public",
},
source,
});
}
const WebhookSchema = z.object({
projectRef: z.string(),
table: z.string(),
schema: z.string(),
});
const WebhookData = z.object({
triggerName: z.string(),
table: z.string(),
schema: z.string(),
});
export function createWebhookEventSource(
integration: SupabaseManagementIntegration
): ExternalSource<
SupabaseManagementIntegration,
{
projectRef: string;
table: string;
schema: string;
},
"HTTP"
> {
return new ExternalSource("HTTP", {
id: "supabase.webhook",
schema: WebhookSchema,
version: "0.1.1",
integration,
filter: (params) => {
return {
table: [params.table],
};
},
key: (params) => `${params.projectRef}-${params.schema}-${params.table}`,
properties: (params) => [
{
label: "Project Ref",
text: params.projectRef,
},
{
label: "Table",
text: `${params.schema}.${params.table}`,
},
],
handler: webhookHandler,
register: async (event, io, ctx) => {
const { params, source: httpSource, events, missingEvents } = event;
const webhookData = WebhookData.safeParse(httpSource.data);
if (httpSource.active && webhookData.success) {
const { triggerName, table, schema } = webhookData.data;
const allEvents = new Set<string>([
...events,
...missingEvents,
]) as Set<WebhookEvents>;
const condition = createTriggerCondition(Array.from(allEvents));
const query = createTriggerQuery({
triggerName,
condition,
schema: params.schema,
table: params.table,
url: httpSource.url,
secret: httpSource.secret,
});
const queryResults = await io.integration.runQuery("update-trigger", {
ref: params.projectRef,
query,
});
await io.logger.debug("Query results", { queryResults });
return {
data: {
triggerName: triggerName,
table: table,
schema: schema,
},
registeredEvents: Array.from(allEvents),
};
}
const url = new URL(httpSource.url);
const id = url.pathname.split("/").pop() ?? randomUUID();
// Create the trigger name using the last 12 characters of the id
const triggerName = `tr_${id.slice(-12)}`;
const condition = createTriggerCondition(events as WebhookEvents[]);
const query = createTriggerQuery({
triggerName,
condition,
schema: params.schema,
table: params.table,
url: httpSource.url,
secret: httpSource.secret,
});
const queryResults = await io.integration.runQuery("create-trigger", {
ref: params.projectRef,
query,
});
await io.logger.debug("Query results", { queryResults });
return {
data: {
triggerName: triggerName,
table: params.table,
schema: params.schema,
},
registeredEvents: events,
};
},
});
}
function createTriggerQuery({
triggerName,
condition,
schema,
table,
url,
secret,
}: {
triggerName: string;
condition: string;
schema: string;
table: string;
url: string;
secret: string;
}): string {
return `
CREATE OR REPLACE TRIGGER ${triggerName}
AFTER ${condition} on "${schema}"."${table}"
FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('${url}', 'POST', '{"Content-type":"application/json", "Authorization": "Bearer ${secret}" }', '{}', '1000')
`;
}
function createTriggerCondition(events: WebhookEvents[]): string {
return events.join(" OR ");
}
async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger) {
logger.debug(
"[inside supabase management integration] Handling webhook handler"
);
const { rawEvent: request, source } = event;
if (!request.body) {
logger.debug("[inside supabase management integration] No body found");
return;
}
// Check the Bearer token matches the secret
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
logger.debug(
"[inside supabase management integration] No Authorization header found"
);
return { events: [] };
}
const authHeaderParts = authHeader.split(" ");
if (authHeaderParts.length !== 2) {
logger.debug(
"[inside supabase management integration] Authorization header is not in the correct format"
);
return { events: [] };
}
const token = authHeaderParts[1];
if (token !== source.secret) {
logger.debug(
"[inside supabase management integration] Authorization header does not match the secret"
);
return { events: [] };
}
const rawBody = await request.text();
const payload = safeParseBody(rawBody);
if (!payload) {
return { events: [] };
}
// Generate a unique ID for the event
const id = randomUUID();
return {
events: [
{
id,
name: payload.type,
source: "supabase",
payload,
context: {},
},
],
};
}
@@ -0,0 +1,185 @@
import { AuthenticatedTask } from "@trigger.dev/sdk";
import type {
CreateProjectRequestBody,
CreateProjectResponseData,
GetOrganizationsResponseData,
GetPostgRESTConfigResponseData,
GetProjectPGConfigResponseData,
GetProjectsResponseData,
GetTypescriptTypesResponseData,
ListFunctionsResponseData,
RunQueryResponseData,
SupabaseManagementAPI,
} from "supabase-management-js";
export const getOrganizations: AuthenticatedTask<
SupabaseManagementAPI,
void,
GetOrganizationsResponseData
> = {
run: async (params, client) => {
return client.getOrganizations();
},
init: (params) => {
return {
name: "Get Organizations",
params,
icon: "supabase",
};
},
};
export const getProjects: AuthenticatedTask<
SupabaseManagementAPI,
void,
GetProjectsResponseData
> = {
run: async (params, client) => {
return client.getProjects();
},
init: (params) => {
return {
name: "Get Projects",
params,
icon: "supabase",
};
},
};
export const createProject: AuthenticatedTask<
SupabaseManagementAPI,
CreateProjectRequestBody,
CreateProjectResponseData
> = {
run: async (params, client) => {
return client.createProject(params);
},
init: (params) => {
return {
name: "Create Project",
params,
icon: "supabase",
properties: [
{ label: "Name", text: params.name },
{ label: "Org", text: params.organization_id },
{ label: "Region", text: params.region },
{ label: "Plan", text: params.plan },
],
};
},
};
export const listFunctions: AuthenticatedTask<
SupabaseManagementAPI,
{ ref: string },
ListFunctionsResponseData
> = {
run: async (params, client) => {
return client.listFunctions(params.ref);
},
init: (params) => {
return {
name: "List Functions",
params,
icon: "supabase",
properties: [
{
label: "Project",
text: params.ref,
},
],
};
},
};
export const runQuery: AuthenticatedTask<
SupabaseManagementAPI,
{ ref: string; query: string },
RunQueryResponseData
> = {
run: async (params, client) => {
return client.runQuery(params.ref, params.query);
},
init: (params) => {
return {
name: "Run Query",
params,
icon: "supabase",
properties: [
{
label: "Project",
text: params.ref,
},
],
};
},
};
export const getTypescriptTypes: AuthenticatedTask<
SupabaseManagementAPI,
{ ref: string },
GetTypescriptTypesResponseData
> = {
run: async (params, client) => {
return client.getTypescriptTypes(params.ref);
},
init: (params) => {
return {
name: "Get Typescript Types",
params,
icon: "supabase",
properties: [
{
label: "Project",
text: params.ref,
},
],
};
},
};
export const getPostgRESTConfig: AuthenticatedTask<
SupabaseManagementAPI,
{ ref: string },
GetPostgRESTConfigResponseData
> = {
run: async (params, client) => {
return client.getPostgRESTConfig(params.ref);
},
init: (params) => {
return {
name: "Get PostgREST Config",
params,
icon: "supabase",
properties: [
{
label: "Project",
text: params.ref,
},
],
};
},
};
export const getPGConfig: AuthenticatedTask<
SupabaseManagementAPI,
{ ref: string },
GetProjectPGConfigResponseData
> = {
run: async (params, client) => {
return client.getPGConfig(params.ref);
},
init: (params) => {
return {
name: "Get PG Config",
params,
icon: "supabase",
properties: [
{
label: "Project",
text: params.ref,
},
],
};
},
};
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"composite": false,
"declaration": false,
"declarationMap": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"resolveJsonModule": true,
"lib": ["es2019", "dom"],
"module": "CommonJS",
"target": "es2021"
},
"include": ["./src/**/*.ts", "tsup.config.ts"],
"exclude": ["node_modules"]
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from "tsup";
export default defineConfig([
{
name: "main",
entry: ["./src/index.ts"],
outDir: "./dist",
platform: "node",
format: ["cjs"],
legacyOutput: true,
sourcemap: true,
clean: true,
bundle: true,
splitting: false,
dts: true,
treeshake: {
preset: "smallest",
},
esbuildPlugins: [],
noExternal: [],
external: ["http", "https", "util", "events", "tty", "os", "timers"],
},
]);
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "IntegrationDefinition" ADD COLUMN "icon" TEXT;
+1
View File
@@ -135,6 +135,7 @@ model IntegrationDefinition {
name String
instructions String?
description String?
icon String?
packageName String @default("")
authMethods IntegrationAuthMethod[]
+15 -3
View File
@@ -4,7 +4,13 @@ import {
RunTaskOptions,
ServerTask,
} from "@trigger.dev/internal";
import { IO } from "./io";
import { IO, IOTask } from "./io";
type IntegrationRunTaskFunction<TClient> = <TResult>(
key: string | any[],
callback: (client: TClient, task: IOTask, io: IO) => Promise<TResult>,
options?: RunTaskOptions
) => Promise<TResult>;
export type ClientFactory<TClient> = (auth: ConnectionAuth) => TClient;
@@ -88,12 +94,18 @@ type ExtractIntegrationClientClient<
usesLocalAuth: true;
client: infer TClient;
}
? { client: TClient }
? {
client: TClient;
runTask: IntegrationRunTaskFunction<TClient>;
}
: TIntegrationClient extends {
usesLocalAuth: false;
clientFactory: ClientFactory<infer TClient>;
}
? { client: TClient }
? {
client: TClient;
runTask: IntegrationRunTaskFunction<TClient>;
}
: never;
type ExtractIntegrationClient<
+2 -1
View File
@@ -236,13 +236,14 @@ export class IO {
key,
{
name: "Update Source",
description: `Update Source ${options.key}`,
description: "Update Source",
properties: [
{
label: "key",
text: options.key,
},
],
params: options,
redact: {
paths: ["secret"],
},
+15 -1
View File
@@ -1,4 +1,4 @@
import { ConnectionAuth } from "@trigger.dev/internal";
import { ConnectionAuth, RunTaskOptions } from "@trigger.dev/internal";
import {
AuthenticatedTask,
IOWithIntegrations,
@@ -41,6 +41,20 @@ export function createIOWithIntegrations<
client,
} as any;
ioConnection.runTask = async (
key: string | any[],
callback: (client: any, task: any, io: IO) => Promise<any>,
options?: RunTaskOptions
) => {
return await io.runTask(
key,
{ name: "Task", icon: integration.metadata.id, ...options },
async (ioTask) => {
return await callback(client, ioTask, io);
}
);
};
if (integration.client.tasks) {
const tasks: Record<
string,
+211 -2
View File
@@ -414,6 +414,7 @@ importers:
'@trigger.dev/resend': workspace:*
'@trigger.dev/sdk': workspace:*
'@trigger.dev/slack': workspace:*
'@trigger.dev/supabase': workspace:*
'@trigger.dev/typeform': workspace:*
'@types/node': 18.15.13
'@types/node-fetch': 2.6.x
@@ -437,6 +438,7 @@ importers:
'@trigger.dev/resend': link:../../integrations/resend
'@trigger.dev/sdk': link:../../packages/trigger-sdk
'@trigger.dev/slack': link:../../integrations/slack
'@trigger.dev/supabase': link:../../integrations/supabase
'@trigger.dev/typeform': link:../../integrations/typeform
'@types/node': 18.15.13
'@types/react': 18.0.26
@@ -564,6 +566,29 @@ importers:
rimraf: 3.0.2
tsup: 6.6.3
integrations/supabase:
specifiers:
'@supabase/supabase-js': ^2.26.0
'@trigger.dev/integration-kit': workspace:^2.0.0-next.5
'@trigger.dev/sdk': workspace:^2.0.0-next.16
'@types/node': 18.x
rimraf: ^3.0.2
supabase-management-js: ^0.1.2
tsup: 7.1.x
typescript: 4.9.4
zod: 3.21.4
dependencies:
'@supabase/supabase-js': 2.31.0
'@trigger.dev/integration-kit': link:../../packages/integration-kit
'@trigger.dev/sdk': link:../../packages/trigger-sdk
supabase-management-js: 0.1.2
zod: 3.21.4
devDependencies:
'@types/node': 18.15.13
rimraf: 3.0.2
tsup: 7.1.0_typescript@4.9.4
typescript: 4.9.4
integrations/typeform:
specifiers:
'@trigger.dev/integration-kit': workspace:^2.0.0-next.5
@@ -9128,7 +9153,7 @@ packages:
globby: 11.1.0
ip: 2.0.0
lodash: 4.17.21
node-fetch: 2.6.11
node-fetch: 2.6.12
open: 8.4.0
pretty-hrtime: 1.0.3
prompts: 2.4.2
@@ -9656,6 +9681,62 @@ packages:
file-system-cache: 2.1.1
dev: true
/@supabase/functions-js/2.1.2:
resolution: {integrity: sha512-QCR6pwJs9exCl37bmpMisUd6mf+0SUBJ6mUpiAjEkSJ/+xW8TCuO14bvkWHADd5hElJK9MxNlMQXxSA4DRz9nQ==}
dependencies:
cross-fetch: 3.1.8
transitivePeerDependencies:
- encoding
dev: false
/@supabase/gotrue-js/2.46.1:
resolution: {integrity: sha512-tebFX3XvPqEJKHOVgkXTN20g9iUhLx6tebIYQvTggYTrqOT2af8oTpSBdgYzbwJ291G6P6CSpR6KY0cT9ade5A==}
dependencies:
cross-fetch: 3.1.8
transitivePeerDependencies:
- encoding
dev: false
/@supabase/postgrest-js/1.7.2:
resolution: {integrity: sha512-GK80JpRq8l6Qll85erICypAfQCied8tdlXfsDN14W844HqXCSOisk8AaE01DAwGJanieaoN5fuqhzA2yKxDvEQ==}
dependencies:
cross-fetch: 3.1.8
transitivePeerDependencies:
- encoding
dev: false
/@supabase/realtime-js/2.7.3:
resolution: {integrity: sha512-c7TzL81sx2kqyxsxcDduJcHL9KJdCOoKimGP6lQSqiZKX42ATlBZpWbyy9KFGFBjAP4nyopMf5JhPi2ZH9jyNw==}
dependencies:
'@types/phoenix': 1.6.0
'@types/websocket': 1.0.5
websocket: 1.0.34
transitivePeerDependencies:
- supports-color
dev: false
/@supabase/storage-js/2.5.1:
resolution: {integrity: sha512-nkR0fQA9ScAtIKA3vNoPEqbZv1k5B5HVRYEvRWdlP6mUpFphM9TwPL2jZ/ztNGMTG5xT6SrHr+H7Ykz8qzbhjw==}
dependencies:
cross-fetch: 3.1.8
transitivePeerDependencies:
- encoding
dev: false
/@supabase/supabase-js/2.31.0:
resolution: {integrity: sha512-W9/4s+KnSUX67wJKBn/3yLq+ieycnMzVjK3nNTLX5Wko3ypNT/081l2iFYrf+nsLQ1CiT4mA92I3dxCy6CmxTg==}
dependencies:
'@supabase/functions-js': 2.1.2
'@supabase/gotrue-js': 2.46.1
'@supabase/postgrest-js': 1.7.2
'@supabase/realtime-js': 2.7.3
'@supabase/storage-js': 2.5.1
cross-fetch: 3.1.8
transitivePeerDependencies:
- encoding
- supports-color
dev: false
/@swc/core-darwin-arm64/1.3.26:
resolution: {integrity: sha512-FWWflBfKRYrUJtko2xiedC5XCa31O75IZZqnTWuLpe9g3C5tnUuF3M8LSXZS/dn6wprome1MhtG9GMPkSYkhkg==}
engines: {node: '>=10'}
@@ -10445,6 +10526,10 @@ packages:
pg-types: 2.2.0
dev: false
/@types/phoenix/1.6.0:
resolution: {integrity: sha512-qwfpsHmFuhAS/dVd4uBIraMxRd56vwBUYQGZ6GpXnFuM2XMRFJbIyruFKKlW2daQliuYZwe0qfn/UjFCDKic5g==}
dev: false
/@types/pretty-hrtime/1.0.1:
resolution: {integrity: sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==}
dev: true
@@ -10551,6 +10636,12 @@ packages:
resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==}
dev: true
/@types/websocket/1.0.5:
resolution: {integrity: sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==}
dependencies:
'@types/node': 20.4.2
dev: false
/@types/ws/8.5.4:
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
dependencies:
@@ -12051,6 +12142,14 @@ packages:
engines: {node: '>=0.2.0'}
dev: false
/bufferutil/4.0.7:
resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==}
engines: {node: '>=6.14.2'}
requiresBuild: true
dependencies:
node-gyp-build: 4.6.0
dev: false
/bufrw/1.3.0:
resolution: {integrity: sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ==}
engines: {node: '>= 0.10.x'}
@@ -12804,6 +12903,14 @@ packages:
cross-spawn: 7.0.3
dev: false
/cross-fetch/3.1.8:
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
dependencies:
node-fetch: 2.6.12
transitivePeerDependencies:
- encoding
dev: false
/cross-spawn/5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
dependencies:
@@ -12939,6 +13046,13 @@ packages:
resolution: {integrity: sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==}
dev: false
/d/1.0.1:
resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
dependencies:
es5-ext: 0.10.62
type: 1.2.0
dev: false
/damerau-levenshtein/1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -13587,10 +13701,35 @@ packages:
is-date-object: 1.0.5
is-symbol: 1.0.4
/es5-ext/0.10.62:
resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==}
engines: {node: '>=0.10'}
requiresBuild: true
dependencies:
es6-iterator: 2.0.3
es6-symbol: 3.1.3
next-tick: 1.1.0
dev: false
/es6-iterator/2.0.3:
resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
dependencies:
d: 1.0.1
es5-ext: 0.10.62
es6-symbol: 3.1.3
dev: false
/es6-object-assign/1.1.0:
resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==}
dev: true
/es6-symbol/3.1.3:
resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==}
dependencies:
d: 1.0.1
ext: 1.7.0
dev: false
/esbuild-android-64/0.15.18:
resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==}
engines: {node: '>=12'}
@@ -15305,6 +15444,12 @@ packages:
transitivePeerDependencies:
- supports-color
/ext/1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
dependencies:
type: 2.7.2
dev: false
/extend-shallow/2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
@@ -17117,6 +17262,10 @@ packages:
gopd: 1.0.1
has-tostringtag: 1.0.0
/is-typedarray/1.0.0:
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
dev: false
/is-unicode-supported/0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
@@ -18796,6 +18945,10 @@ packages:
engines: {node: '>= 0.4.0'}
dev: true
/next-tick/1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
dev: false
/next/12.3.4_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ==}
engines: {node: '>=12.22.0'}
@@ -19043,7 +19196,6 @@ packages:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: true
/node-fetch/2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
@@ -19065,6 +19217,11 @@ packages:
formdata-polyfill: 4.0.10
dev: false
/node-gyp-build/4.6.0:
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
hasBin: true
dev: false
/node-int64/0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@@ -19343,6 +19500,10 @@ packages:
- debug
dev: false
/openapi-fetch/0.6.2:
resolution: {integrity: sha512-Faj29Kzh7oCbt1bz6vAGNKtRJlV/GolOQTx87eYUnfCK7eVXdN9jQVojroc7tcJ5OQgyhbeOqD7LS/8UtGBnMQ==}
dev: false
/opentracing/0.14.7:
resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==}
engines: {node: '>=0.10'}
@@ -22396,6 +22557,13 @@ packages:
pirates: 4.0.5
ts-interface-checker: 0.1.13
/supabase-management-js/0.1.2:
resolution: {integrity: sha512-2sdCM1Gc7XI6zMVbOxdG0hsn1RQOqdD7pGfQwrgnLtampLe9mq7FTeK6a0LS/UhsDYl7jEVYK9BINP18zDxj6Q==}
engines: {node: '>=18.0.0'}
dependencies:
openapi-fetch: 0.6.2
dev: false
/supports-color/5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -23303,6 +23471,14 @@ packages:
media-typer: 0.3.0
mime-types: 2.1.35
/type/1.2.0:
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
dev: false
/type/2.7.2:
resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
dev: false
/typed-array-length/1.0.4:
resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
dependencies:
@@ -23310,6 +23486,12 @@ packages:
for-each: 0.3.3
is-typed-array: 1.1.10
/typedarray-to-buffer/3.1.5:
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
dependencies:
is-typedarray: 1.0.0
dev: false
/typedarray/0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
dev: true
@@ -23675,6 +23857,14 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/utf-8-validate/5.0.10:
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
engines: {node: '>=6.14.2'}
requiresBuild: true
dependencies:
node-gyp-build: 4.6.0
dev: false
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -24131,6 +24321,20 @@ packages:
- uglify-js
dev: true
/websocket/1.0.34:
resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==}
engines: {node: '>=4.0.0'}
dependencies:
bufferutil: 4.0.7
debug: 2.6.9
es5-ext: 0.10.62
typedarray-to-buffer: 3.1.5
utf-8-validate: 5.0.10
yaeti: 0.0.6
transitivePeerDependencies:
- supports-color
dev: false
/whatwg-url/5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
@@ -24362,6 +24566,11 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
/yaeti/0.0.6:
resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==}
engines: {node: '>=0.10.32'}
dev: false
/yallist/2.1.2:
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}