From ab9e4a989cb96f3da3f978370f0bf13b0495e70d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 3 Oct 2023 16:44:18 +0100 Subject: [PATCH 01/79] Improves the performance of run resuming (#522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improves the perform of run resuming When runs resume, we try and make sure that tasks that have already been completed are cached and reused. Worst case scenario the client needs to hit the API server once for a non-cached task that is indeed completed on the server, but this can get pretty expensive when there are a larger number of tasks. This commit does 2 different things to help: - noop tasks are no longer “cached” using the cachedTasks strategy, instead their idempotency keys are shoved into a bloom filter and the client tests for their inclusion in the bloom filter before running them (since they don’t have any concept of output, this works) - Additional cached tasks are lazy loaded when a task is run. This allows us to progressively fetch additional tasks to be cached on the client, which will cut down on cache misses by a decent amount * Create warm-carrots-float.md * Make io.yield backwards compat with older platform versions * Better support old clients connecting to server versions that support lazy loading cached tasks * Fixed type errors when settings headers with unknown value * Better yield not support error message * Rename _version to _serverVersion to be more clear --- .changeset/flat-olives-grab.md | 10 + .changeset/warm-carrots-float.md | 6 + apps/webapp/app/consts.ts | 1 + apps/webapp/app/models/task.server.ts | 86 +++++- .../app/routes/api.v1.runs.$runId.tasks.ts | 85 +++++- .../resources.projects.$projectId.endpoint.ts | 19 +- .../webapp/app/services/endpointApi.server.ts | 51 ++-- .../endpoints/createEndpoint.server.ts | 2 + .../endpoints/indexEndpoint.server.ts | 13 + .../validateCreateEndpoint.server.ts | 2 + .../runs/performRunExecutionV1.server.ts | 40 +++ .../runs/performRunExecutionV2.server.ts | 283 ++++++++++++------ packages/astro/src/index.ts | 1 + packages/core/src/bloom.ts | 63 ++++ packages/core/src/index.ts | 27 ++ packages/core/src/schemas/api.ts | 37 ++- .../migration.sql | 2 + .../migration.sql | 2 + packages/database/prisma/schema.prisma | 3 + packages/express/src/index.ts | 8 + packages/nextjs/src/index.ts | 10 +- packages/remix/src/index.ts | 2 +- packages/trigger-sdk/src/apiClient.ts | 127 +++++++- packages/trigger-sdk/src/errors.ts | 7 +- packages/trigger-sdk/src/io.ts | 228 ++++++++++---- packages/trigger-sdk/src/triggerClient.ts | 59 +++- references/job-catalog/src/stressTest.ts | 65 +++- 27 files changed, 1029 insertions(+), 210 deletions(-) create mode 100644 .changeset/flat-olives-grab.md create mode 100644 .changeset/warm-carrots-float.md create mode 100644 packages/core/src/bloom.ts create mode 100644 packages/database/prisma/migrations/20230929100348_add_yielded_executions_to_job_run/migration.sql create mode 100644 packages/database/prisma/migrations/20231003092741_add_version_to_endpoint/migration.sql diff --git a/.changeset/flat-olives-grab.md b/.changeset/flat-olives-grab.md new file mode 100644 index 000000000..4e5fe2a30 --- /dev/null +++ b/.changeset/flat-olives-grab.md @@ -0,0 +1,10 @@ +--- +"@trigger.dev/sdk": patch +"@trigger.dev/express": patch +"@trigger.dev/nextjs": patch +"@trigger.dev/astro": patch +"@trigger.dev/remix": patch +"@trigger.dev/core": patch +--- + +Send client version back to the server via headers diff --git a/.changeset/warm-carrots-float.md b/.changeset/warm-carrots-float.md new file mode 100644 index 000000000..cf51b6357 --- /dev/null +++ b/.changeset/warm-carrots-float.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/core": patch +"@trigger.dev/sdk": patch +--- + +Better performance when resuming a run, especially one with a large amount of tasks diff --git a/apps/webapp/app/consts.ts b/apps/webapp/app/consts.ts index c6e9c85cd..51ab2cc44 100644 --- a/apps/webapp/app/consts.ts +++ b/apps/webapp/app/consts.ts @@ -5,3 +5,4 @@ export const DEFAULT_MAX_CONCURRENT_RUNS = 10; export const MAX_CONCURRENT_RUNS_LIMIT = 20; export const PREPROCESS_RETRY_LIMIT = 2; export const EXECUTE_JOB_RETRY_LIMIT = 10; +export const MAX_RUN_YIELDED_EXECUTIONS = 100; diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts index 19951eb6f..ac468779a 100644 --- a/apps/webapp/app/models/task.server.ts +++ b/apps/webapp/app/models/task.server.ts @@ -1,5 +1,5 @@ import type { Task, TaskAttempt } from "@trigger.dev/database"; -import { ServerTask } from "@trigger.dev/core"; +import { CachedTask, ServerTask } from "@trigger.dev/core"; export type TaskWithAttempts = Task & { attempts: TaskAttempt[] }; @@ -25,3 +25,87 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask operation: task.operation, }; } + +export type TaskForCaching = Pick< + Task, + "id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId" +>; + +export function prepareTasksForCaching( + possibleTasks: TaskForCaching[], + maxSize: number +): { + tasks: CachedTask[]; + cursor: string | undefined; +} { + const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop); + + // Select tasks using greedy approach + const tasksToRun: CachedTask[] = []; + let remainingSize = maxSize; + + for (const task of tasks) { + const cachedTask = prepareTaskForCaching(task); + const size = calculateCachedTaskSize(cachedTask); + + if (size <= remainingSize) { + tasksToRun.push(cachedTask); + remainingSize -= size; + } + } + + return { + tasks: tasksToRun, + cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined, + }; +} + +export function prepareTasksForCachingLegacy( + possibleTasks: TaskForCaching[], + maxSize: number +): { + tasks: CachedTask[]; + cursor: string | undefined; +} { + const tasks = possibleTasks.filter((task) => task.status === "COMPLETED"); + + // Prepare tasks and calculate their sizes + const availableTasks = tasks.map((task) => { + const cachedTask = prepareTaskForCaching(task); + return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) }; + }); + + // Sort tasks in ascending order by size + availableTasks.sort((a, b) => a.size - b.size); + + // Select tasks using greedy approach + const tasksToRun: CachedTask[] = []; + let remainingSize = maxSize; + + for (const { task, size } of availableTasks) { + if (size <= remainingSize) { + tasksToRun.push(task); + remainingSize -= size; + } + } + + return { + tasks: tasksToRun, + cursor: undefined, + }; +} + +function prepareTaskForCaching(task: TaskForCaching): CachedTask { + return { + id: task.idempotencyKey, // We should eventually move this back to task.id + status: task.status, + idempotencyKey: task.idempotencyKey, + noop: task.noop, + output: task.output as any, + parentId: task.parentId, + }; +} + +function calculateCachedTaskSize(task: CachedTask): number { + return JSON.stringify(task).length; +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 0e5eaecd9..54c964534 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -1,10 +1,16 @@ import type { ActionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { TaskStatus } from "@trigger.dev/database"; -import { RunTaskBodyOutput, RunTaskBodyOutputSchema, ServerTask } from "@trigger.dev/core"; +import { + API_VERSIONS, + RunTaskBodyOutput, + RunTaskBodyOutputSchema, + RunTaskResponseWithCachedTasksBody, + ServerTask, +} from "@trigger.dev/core"; import { z } from "zod"; import { $transaction, PrismaClient, prisma } from "~/db.server"; -import { taskWithAttemptsToServerTask } from "~/models/task.server"; +import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { ulid } from "~/services/ulid.server"; @@ -16,6 +22,8 @@ const ParamsSchema = z.object({ const HeadersSchema = z.object({ "idempotency-key": z.string(), + "trigger-version": z.string().optional().nullable(), + "x-cached-tasks-cursor": z.string().optional().nullable(), }); export async function action({ request, params }: ActionArgs) { @@ -37,7 +45,11 @@ export async function action({ request, params }: ActionArgs) { return json({ error: "Invalid or Missing idempotency key" }, { status: 400 }); } - const { "idempotency-key": idempotencyKey } = headers.data; + const { + "idempotency-key": idempotencyKey, + "trigger-version": triggerVersion, + "x-cached-tasks-cursor": cachedTasksCursor, + } = headers.data; const { runId } = ParamsSchema.parse(params); @@ -48,6 +60,8 @@ export async function action({ request, params }: ActionArgs) { body: anyBody, runId, idempotencyKey, + triggerVersion, + cachedTasksCursor, }); const body = RunTaskBodyOutputSchema.safeParse(anyBody); @@ -71,6 +85,26 @@ export async function action({ request, params }: ActionArgs) { return json({ error: "Something went wrong" }, { status: 500 }); } + if (triggerVersion === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) { + const requestMigration = new ChangeRequestLazyLoadedCachedTasks(); + + const responseBody = await requestMigration.call(runId, task, cachedTasksCursor); + + logger.debug( + "RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks", + { + responseBody, + cachedTasksCursor, + } + ); + + return json(responseBody, { + headers: { + "trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, + }, + }); + } + return json(task); } catch (error) { if (error instanceof Error) { @@ -81,6 +115,51 @@ export async function action({ request, params }: ActionArgs) { } } +class ChangeRequestLazyLoadedCachedTasks { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call( + runId: string, + task: ServerTask, + cursor?: string | null + ): Promise { + if (!cursor) { + return { + task, + }; + } + + // We need to limit the cached tasks to not be too large >2MB when serialized + const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000; + + const nextTasks = await this.#prismaClient.task.findMany({ + where: { + runId, + status: "COMPLETED", + noop: false, + }, + take: 250, + cursor: { + id: cursor, + }, + orderBy: { + id: "asc", + }, + }); + + const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT); + + return { + task, + cachedTasks: preparedTasks, + }; + } +} + export class RunTaskService { #prismaClient: PrismaClient; diff --git a/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts b/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts index 0412919ec..ece220cb7 100644 --- a/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts +++ b/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts @@ -2,28 +2,15 @@ import { parse } from "@conform-to/zod"; import { ActionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { - CreateEndpointError, - CreateEndpointService, -} from "~/services/endpoints/createEndpoint.server"; -import { requireUserId } from "~/services/session.server"; -import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core"; -import { env } from "process"; +import { CreateEndpointError } from "~/services/endpoints/createEndpoint.server"; import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server"; -const ParamsSchema = z.object({ - projectId: z.string(), -}); - export const bodySchema = z.object({ environmentId: z.string(), url: z.string().url("Must be a valid URL"), }); -export async function action({ request, params }: ActionArgs) { - const userId = await requireUserId(request); - const { projectId } = ParamsSchema.parse(params); - +export async function action({ request }: ActionArgs) { const formData = await request.formData(); const submission = parse(formData, { schema: bodySchema }); @@ -48,7 +35,7 @@ export async function action({ request, params }: ActionArgs) { } const service = new ValidateCreateEndpointService(); - const result = await service.call({ + await service.call({ url: submission.value.url, environment, }); diff --git a/apps/webapp/app/services/endpointApi.server.ts b/apps/webapp/app/services/endpointApi.server.ts index 8cce435fa..494930a07 100644 --- a/apps/webapp/app/services/endpointApi.server.ts +++ b/apps/webapp/app/services/endpointApi.server.ts @@ -1,7 +1,9 @@ import { + API_VERSIONS, ApiEventLog, DeliverEventResponseSchema, DeserializedJson, + EndpointHeadersSchema, ErrorWithStackSchema, HttpSourceRequest, HttpSourceResponseSchema, @@ -89,6 +91,15 @@ export class EndpointApi { }; } + const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries())); + + if (headers.success && headers.data["trigger-version"]) { + return { + ...pongResponse.data, + triggerVersion: headers.data["trigger-version"], + }; + } + return pongResponse.data; } @@ -129,41 +140,15 @@ export class EndpointApi { const anyBody = await response.json(); const data = IndexEndpointResponseSchema.parse(anyBody); + const headers = EndpointHeadersSchema.parse(Object.fromEntries(response.headers.entries())); return { ok: true, data, + headers, } as const; } - async deliverEvent(event: ApiEventLog) { - const response = await safeFetch(this.url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-trigger-api-key": this.apiKey, - "x-trigger-action": "DELIVER_EVENT", - }, - body: JSON.stringify(event), - }); - - if (!response) { - throw new Error(`Could not connect to endpoint ${this.url}`); - } - - if (!response.ok) { - throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`); - } - - const anyBody = await response.json(); - - logger.debug("deliverEvent() response from endpoint", { - body: anyBody, - }); - - return DeliverEventResponseSchema.parse(anyBody); - } - async executeJobRequest(options: RunJobBody) { const startTimeInMs = performance.now(); @@ -338,6 +323,15 @@ export class EndpointApi { }; } + const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries())); + + if (headers.success && headers.data["trigger-version"]) { + return { + ...validateResponse.data, + triggerVersion: headers.data["trigger-version"], + }; + } + return validateResponse.data; } } @@ -359,6 +353,7 @@ function addStandardRequestOptions(options: RequestInit) { headers: { ...options.headers, "user-agent": "triggerdotdev-server/2.0.0", + "x-trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, }, }; } diff --git a/apps/webapp/app/services/endpoints/createEndpoint.server.ts b/apps/webapp/app/services/endpoints/createEndpoint.server.ts index bd1839222..20c62e7ef 100644 --- a/apps/webapp/app/services/endpoints/createEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/createEndpoint.server.ts @@ -74,9 +74,11 @@ export class CreateEndpointService { slug: id, url: endpointUrl, indexingHookIdentifier: indexingHookIdentifier(), + version: pong.triggerVersion, }, update: { url: endpointUrl, + version: pong.triggerVersion, }, }); diff --git a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts index bc529fe0c..2e6da0182 100644 --- a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts @@ -41,6 +41,7 @@ export class IndexEndpointService { } const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data; + const { "trigger-version": triggerVersion } = indexResponse.headers; logger.debug("Indexing endpoint", { endpointId: endpoint.id, @@ -48,6 +49,7 @@ export class IndexEndpointService { endpointSlug: endpoint.slug, source: source, sourceData: sourceData, + triggerVersion, stats: { jobs: jobs.length, sources: sources.length, @@ -56,6 +58,17 @@ export class IndexEndpointService { }, }); + if (triggerVersion && triggerVersion !== endpoint.version) { + await this.#prismaClient.endpoint.update({ + where: { + id: endpoint.id, + }, + data: { + version: triggerVersion, + }, + }); + } + const indexStats = { jobs: 0, sources: 0, diff --git a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts index d578a26d9..4bbe5231f 100644 --- a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts @@ -58,9 +58,11 @@ export class ValidateCreateEndpointService { slug: validationResult.endpointId, url: endpointUrl, indexingHookIdentifier: indexingHookIdentifier(), + version: validationResult.triggerVersion, }, update: { url: endpointUrl, + version: validationResult.triggerVersion, }, }); diff --git a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts index 591f6f00b..7cfdf2d96 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts @@ -263,6 +263,7 @@ export class PerformRunExecutionV1Service { .flat() .filter(Boolean) .map((t) => CachedTaskSchema.parse(t)), + yieldedExecutions: run.yieldedExecutions, }); if (!response) { @@ -354,6 +355,11 @@ export class PerformRunExecutionV1Service { break; } + case "YIELD_EXECUTION": { + await this.#resumeYieldedExecution(execution, safeBody.data.key); + + break; + } default: { const _exhaustiveCheck: never = status; throw new Error(`Non-exhaustive match for value: ${status}`); @@ -393,6 +399,40 @@ export class PerformRunExecutionV1Service { }); } + async #resumeYieldedExecution(execution: FoundRunExecution, key: string) { + const { run } = execution; + + return await $transaction(this.#prismaClient, async (tx) => { + await tx.jobRunExecution.update({ + where: { + id: execution.id, + }, + data: { + status: "SUCCESS", + completedAt: new Date(), + run: { + update: { + yieldedExecutions: { + push: key, + }, + }, + }, + }, + }); + + const newJobExecution = await tx.jobRunExecution.create({ + data: { + runId: run.id, + reason: "EXECUTE_JOB", + status: "PENDING", + retryLimit: EXECUTE_JOB_RETRY_LIMIT, + }, + }); + + await enqueueRunExecutionV1(newJobExecution, run.queue.id, run.queue.maxJobs, tx); + }); + } + async #resumeRunWithTask(execution: FoundRunExecution, data: RunJobResumeWithTask) { const { run } = execution; diff --git a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts index 61feab018..1af87b8c3 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts @@ -1,12 +1,17 @@ import { - CachedTask, + API_VERSIONS, + BloomFilter, + ConnectionAuth, + EndpointHeadersSchema, RunJobError, RunJobInvalidPayloadError, RunJobResumeWithTask, RunJobRetryWithTask, RunJobSuccess, RunJobUnresolvedAuthError, + RunSourceContext, RunSourceContextSchema, + supportsFeature, } from "@trigger.dev/core"; import { RuntimeEnvironmentType, type Task } from "@trigger.dev/database"; import { generateErrorMessage } from "zod-error"; @@ -18,10 +23,17 @@ import { formatError } from "~/utils/formatErrors.server"; import { safeJsonZodParse } from "~/utils/json"; import { EndpointApi } from "../endpointApi.server"; import { logger } from "../logger.server"; +import { prepareTasksForCaching, prepareTasksForCachingLegacy } from "~/models/task.server"; +import { MAX_RUN_YIELDED_EXECUTIONS } from "~/consts"; +import { ApiEventLog } from "@trigger.dev/core"; +import { RunJobBody } from "@trigger.dev/core"; type FoundRun = NonNullable>>; type FoundTask = FoundRun["tasks"][number]; +// We need to limit the cached tasks to not be too large >3.5MB when serialized +const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000; + export type PerformRunExecutionV2Input = { id: string; reason: "PREPROCESS" | "EXECUTE_JOB"; @@ -230,38 +242,19 @@ export class PerformRunExecutionV2Service { const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext); - const { response, parser, errorParser, durationInMs } = await client.executeJobRequest({ + const executionBody = await this.#createExecutionBody( + run, + [run.tasks, resumedTask].flat().filter(Boolean), + startedAt, + isRetry, + connections.auth, event, - job: { - id: run.version.job.slug, - version: run.version.version, - }, - run: { - id: run.id, - isTest: run.isTest, - startedAt, - isRetry, - }, - environment: { - id: run.environment.id, - slug: run.environment.slug, - type: run.environment.type, - }, - organization: { - id: run.organization.id, - slug: run.organization.slug, - title: run.organization.title, - }, - account: run.externalAccount - ? { - id: run.externalAccount.identifier, - metadata: run.externalAccount.metadata, - } - : undefined, - connections: connections.auth, - source: sourceContext.success ? sourceContext.data : undefined, - tasks: prepareTasksForRun([run.tasks, resumedTask].flat().filter(Boolean)), - }); + sourceContext.success ? sourceContext.data : undefined + ); + + const { response, parser, errorParser, durationInMs } = await client.executeJobRequest( + executionBody + ); if (!response) { return await this.#failRunExecutionWithRetry({ @@ -269,6 +262,25 @@ export class PerformRunExecutionV2Service { }); } + // Update the endpoint version if it has changed + const rawHeaders = Object.fromEntries(response.headers.entries()); + const headers = EndpointHeadersSchema.safeParse(rawHeaders); + + if ( + headers.success && + headers.data["trigger-version"] && + headers.data["trigger-version"] !== run.endpoint.version + ) { + await this.#prismaClient.endpoint.update({ + where: { + id: run.endpoint.id, + }, + data: { + version: headers.data["trigger-version"], + }, + }); + } + const rawBody = await response.text(); if (!response.ok) { @@ -389,6 +401,10 @@ export class PerformRunExecutionV2Service { break; } + case "YIELD_EXECUTION": { + await this.#resumeYieldedRun(run, safeBody.data.key, isRetry, durationInMs, executionCount); + break; + } default: { const _exhaustiveCheck: never = status; throw new Error(`Non-exhaustive match for value: ${status}`); @@ -396,6 +412,91 @@ export class PerformRunExecutionV2Service { } } + async #createExecutionBody( + run: FoundRun, + tasks: FoundTask[], + startedAt: Date, + isRetry: boolean, + connections: Record, + event: ApiEventLog, + source?: RunSourceContext + ): Promise { + if (supportsFeature("lazyLoadedCachedTasks", run.endpoint.version)) { + const preparedTasks = prepareTasksForCaching(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT); + + return { + event, + job: { + id: run.version.job.slug, + version: run.version.version, + }, + run: { + id: run.id, + isTest: run.isTest, + startedAt, + isRetry, + }, + environment: { + id: run.environment.id, + slug: run.environment.slug, + type: run.environment.type, + }, + organization: { + id: run.organization.id, + slug: run.organization.slug, + title: run.organization.title, + }, + account: run.externalAccount + ? { + id: run.externalAccount.identifier, + metadata: run.externalAccount.metadata, + } + : undefined, + connections, + source, + tasks: preparedTasks.tasks, + cachedTaskCursor: preparedTasks.cursor, + noopTasksSet: prepareNoOpTasksBloomFilter(tasks), + yieldedExecutions: run.yieldedExecutions, + }; + } + + const preparedTasks = prepareTasksForCachingLegacy(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT); + + return { + event, + job: { + id: run.version.job.slug, + version: run.version.version, + }, + run: { + id: run.id, + isTest: run.isTest, + startedAt, + isRetry, + }, + environment: { + id: run.environment.id, + slug: run.environment.slug, + type: run.environment.type, + }, + organization: { + id: run.organization.id, + slug: run.organization.slug, + title: run.organization.title, + }, + account: run.externalAccount + ? { + id: run.externalAccount.identifier, + metadata: run.externalAccount.metadata, + } + : undefined, + connections, + source, + tasks: preparedTasks.tasks, + }; + } + async #completeRunWithSuccess(run: FoundRun, data: RunJobSuccess, durationInMs: number) { await this.#prismaClient.jobRun.update({ where: { id: run.id }, @@ -501,6 +602,56 @@ export class PerformRunExecutionV2Service { }); } + async #resumeYieldedRun( + run: FoundRun, + key: string, + isRetry: boolean, + durationInMs: number, + executionCount: number + ) { + await $transaction(this.#prismaClient, async (tx) => { + if (run.yieldedExecutions.length + 1 > MAX_RUN_YIELDED_EXECUTIONS) { + return await this.#failRunExecution( + tx, + "EXECUTE_JOB", + run, + { + message: `Run has yielded too many times, the maximum is ${MAX_RUN_YIELDED_EXECUTIONS}`, + }, + "FAILURE", + durationInMs + ); + } + + await tx.jobRun.update({ + where: { + id: run.id, + }, + data: { + executionDuration: { + increment: durationInMs, + }, + executionCount: { + increment: 1, + }, + yieldedExecutions: { + push: key, + }, + }, + select: { + yieldedExecutions: true, + executionCount: true, + }, + }); + + await enqueueRunExecutionV2(run, tx, { + isRetry, + skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + executionCount, + }); + }); + } + async #retryRunWithTask( run: FoundRun, data: RunJobRetryWithTask, @@ -686,69 +837,16 @@ export class PerformRunExecutionV2Service { } } -function prepareTasksForRun(possibleTasks: FoundTask[]): CachedTask[] { - const tasks = possibleTasks.filter((task) => task.status === "COMPLETED"); +function prepareNoOpTasksBloomFilter(possibleTasks: FoundTask[]): string { + const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && task.noop); - // We need to limit the cached tasks to not be too large >3.5MB when serialized - const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000; + const filter = new BloomFilter(BloomFilter.NOOP_TASK_SET_SIZE); - const cachedTasks = new Map(); // Cache for prepared tasks - const cachedTaskSizes = new Map(); // Cache for calculated task sizes - - // Helper function to get the cached prepared task, or prepare and cache if not already cached - function getCachedTask(task: FoundTask): CachedTask { - const taskId = task.id; - if (!cachedTasks.has(taskId)) { - cachedTasks.set(taskId, prepareTaskForRun(task)); - } - return cachedTasks.get(taskId)!; + for (const task of tasks) { + filter.add(task.idempotencyKey); } - // Helper function to get the cached task size, or calculate and cache if not already cached - function getCachedTaskSize(task: CachedTask): number { - const taskId = task.id; - if (!cachedTaskSizes.has(taskId)) { - cachedTaskSizes.set(taskId, calculateCachedTaskSize(task)); - } - return cachedTaskSizes.get(taskId)!; - } - - // Prepare tasks and calculate their sizes - const availableTasks = tasks.map((task) => { - const cachedTask = getCachedTask(task); - return { task: cachedTask, size: getCachedTaskSize(cachedTask) }; - }); - - // Sort tasks in ascending order by size - availableTasks.sort((a, b) => a.size - b.size); - - // Select tasks using greedy approach - const tasksToRun: CachedTask[] = []; - let remainingSize = TOTAL_CACHED_TASK_BYTE_LIMIT; - - for (const { task, size } of availableTasks) { - if (size <= remainingSize) { - tasksToRun.push(task); - remainingSize -= size; - } - } - - return tasksToRun; -} - -function prepareTaskForRun(task: FoundTask): CachedTask { - return { - id: task.idempotencyKey, // We should eventually move this back to task.id - status: task.status, - idempotencyKey: task.idempotencyKey, - noop: task.noop, - output: task.output as any, - parentId: task.parentId, - }; -} - -function calculateCachedTaskSize(task: CachedTask): number { - return JSON.stringify(task).length; + return filter.serialize(); } async function findRun(prisma: PrismaClientOrTransaction, id: string) { @@ -783,6 +881,9 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) { output: true, parentId: true, }, + orderBy: { + id: "asc", + }, }, event: true, version: { diff --git a/packages/astro/src/index.ts b/packages/astro/src/index.ts index c90ff6ae0..e399aa0d7 100644 --- a/packages/astro/src/index.ts +++ b/packages/astro/src/index.ts @@ -42,6 +42,7 @@ export function createAstroRoute(client: TriggerClient) { // execution's response body return new Response(JSON.stringify(response.body), { status: response.status, + headers: response.headers, }); } catch (err) { return new Response(JSON.stringify({ error: "Internal server error" }), { diff --git a/packages/core/src/bloom.ts b/packages/core/src/bloom.ts new file mode 100644 index 000000000..63428ab76 --- /dev/null +++ b/packages/core/src/bloom.ts @@ -0,0 +1,63 @@ +import { Buffer } from "node:buffer"; + +export class BloomFilter { + private size: number; + private bitArray: Uint8Array; + + constructor(size: number) { + this.size = size; + this.bitArray = new Uint8Array(Math.ceil(size / 8)); + } + + add(item: string): void { + const index = murmurHash3(item) % this.size; + this.bitArray[Math.floor(index / 8)] |= 1 << index % 8; + } + + test(item: string): boolean { + const index = murmurHash3(item) % this.size; + return (this.bitArray[Math.floor(index / 8)] & (1 << index % 8)) !== 0; + } + + // Serialize to a Base64 string + serialize(): string { + return Buffer.from(this.bitArray).toString("base64"); + } + + // Deserialize from a Base64 string + static deserialize(str: string, size: number): BloomFilter { + const filter = new BloomFilter(size); + filter.bitArray = Uint8Array.from(Buffer.from(str, "base64")); + return filter; + } + + static NOOP_TASK_SET_SIZE = 32_768; +} + +function murmurHash3(str: string, seed = 0): number { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 0xcc9e2d51); + h1 = (h1 << 15) | (h1 >>> 17); + h1 = Math.imul(h1, 0x1b873593); + + h2 = Math.imul(h2 ^ ch, 0x85ebca6b); + h2 = (h2 << 13) | (h2 >>> 19); + h2 = Math.imul(h2, 0xc2b2ae35); + } + + h1 ^= str.length; + h2 ^= str.length; + + h1 = Math.imul(h1 ^ (h1 >>> 16), 0x85ebca6b); + h1 = Math.imul(h1 ^ (h1 >>> 13), 0xc2b2ae35); + h1 ^= h1 >>> 16; + + h2 = Math.imul(h2 ^ (h2 >>> 16), 0x85ebca6b); + h2 = Math.imul(h2 ^ (h2 >>> 13), 0xc2b2ae35); + h2 ^= h2 >>> 16; + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 50aee5efa..0810ae835 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,3 +6,30 @@ export * from "./retry"; export * from "./replacements"; export * from "./searchParams"; export * from "./eventFilterMatches"; +export * from "./bloom"; + +export const API_VERSIONS = { + LAZY_LOADED_CACHED_TASKS: "2023-09-29", +} as const; + +export const PLATFORM_FEATURES = { + yieldExecution: API_VERSIONS.LAZY_LOADED_CACHED_TASKS, + lazyLoadedCachedTasks: API_VERSIONS.LAZY_LOADED_CACHED_TASKS, +}; + +export function supportsFeature( + featureName: TFeatureName, + version: string +): boolean { + if (version === "unversioned" || version === "unknown") { + return false; + } + + const supportedVersion = PLATFORM_FEATURES[featureName]; + + if (!supportedVersion) { + return false; + } + + return version >= supportedVersion; +} diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index b5f1f9161..62edc1f15 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -13,7 +13,7 @@ import { RegisterDynamicSchedulePayloadSchema, ScheduleMetadataSchema, } from "./schedules"; -import { CachedTaskSchema, TaskSchema } from "./tasks"; +import { CachedTaskSchema, ServerTaskSchema, TaskSchema } from "./tasks"; import { EventSpecificationSchema, TriggerMetadataSchema } from "./triggers"; import { RunStatusSchema } from "./runs"; import { JobRunStatusRecordSchema } from "./statuses"; @@ -176,11 +176,13 @@ export type HttpSourceRequestHeaders = z.output; export const ValidateSuccessResponseSchema = z.object({ ok: z.literal(true), endpointId: z.string(), + triggerVersion: z.string().optional(), }); export const ValidateErrorResponseSchema = z.object({ ok: z.literal(false), error: z.string(), + triggerVersion: z.string().optional(), }); export const ValidateResponseSchema = z.discriminatedUnion("ok", [ @@ -292,6 +296,10 @@ export const IndexEndpointResponseSchema = z.object({ export type IndexEndpointResponse = z.infer; +export const EndpointHeadersSchema = z.object({ + "trigger-version": z.string().optional(), +}); + export const RawEventSchema = z.object({ /** The `name` property must exactly match any subscriptions you want to trigger. */ @@ -394,6 +402,8 @@ export const RunSourceContextSchema = z.object({ metadata: z.any(), }); +export type RunSourceContext = z.infer; + export const RunJobBodySchema = z.object({ event: ApiEventLogSchema, job: z.object({ @@ -424,7 +434,10 @@ export const RunJobBodySchema = z.object({ .optional(), source: RunSourceContextSchema.optional(), tasks: z.array(CachedTaskSchema).optional(), + cachedTaskCursor: z.string().optional(), + noopTasksSet: z.string().optional(), connections: z.record(ConnectionAuthSchema).optional(), + yieldedExecutions: z.string().array().optional(), }); export type RunJobBody = z.infer; @@ -437,6 +450,13 @@ export const RunJobErrorSchema = z.object({ export type RunJobError = z.infer; +export const RunJobYieldExecutionErrorSchema = z.object({ + status: z.literal("YIELD_EXECUTION"), + key: z.string(), +}); + +export type RunJobYieldExecutionError = z.infer; + export const RunJobInvalidPayloadErrorSchema = z.object({ status: z.literal("INVALID_PAYLOAD"), errors: z.array(SchemaErrorSchema), @@ -482,6 +502,7 @@ export const RunJobSuccessSchema = z.object({ export type RunJobSuccess = z.infer; export const RunJobResponseSchema = z.discriminatedUnion("status", [ + RunJobYieldExecutionErrorSchema, RunJobErrorSchema, RunJobUnresolvedAuthErrorSchema, RunJobInvalidPayloadErrorSchema, @@ -638,6 +659,20 @@ export const RunTaskBodyOutputSchema = RunTaskBodyInputSchema.extend({ export type RunTaskBodyOutput = z.infer; +export const RunTaskResponseWithCachedTasksBodySchema = z.object({ + task: ServerTaskSchema, + cachedTasks: z + .object({ + tasks: z.array(CachedTaskSchema), + cursor: z.string().optional(), + }) + .optional(), +}); + +export type RunTaskResponseWithCachedTasksBody = z.infer< + typeof RunTaskResponseWithCachedTasksBodySchema +>; + export const CompleteTaskBodyInputSchema = RunTaskBodyInputSchema.pick({ properties: true, description: true, diff --git a/packages/database/prisma/migrations/20230929100348_add_yielded_executions_to_job_run/migration.sql b/packages/database/prisma/migrations/20230929100348_add_yielded_executions_to_job_run/migration.sql new file mode 100644 index 000000000..c5ccba086 --- /dev/null +++ b/packages/database/prisma/migrations/20230929100348_add_yielded_executions_to_job_run/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "JobRun" ADD COLUMN "yieldedExecutions" TEXT[]; diff --git a/packages/database/prisma/migrations/20231003092741_add_version_to_endpoint/migration.sql b/packages/database/prisma/migrations/20231003092741_add_version_to_endpoint/migration.sql new file mode 100644 index 000000000..0317eaaef --- /dev/null +++ b/packages/database/prisma/migrations/20231003092741_add_version_to_endpoint/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Endpoint" ADD COLUMN "version" TEXT NOT NULL DEFAULT 'unknown'; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 9e8b66da8..192e0d66a 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -367,6 +367,7 @@ model Endpoint { updatedAt DateTime @updatedAt indexingHookIdentifier String? + version String @default("unknown") jobVersions JobVersion[] jobRuns JobRun[] @@ -713,6 +714,8 @@ model JobRun { isTest Boolean @default(false) preprocess Boolean @default(false) + yieldedExecutions String[] + tasks Task[] runConnections RunConnection[] missingConnections MissingConnection[] diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index fefc2f1d5..bd69a54fc 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -77,6 +77,14 @@ export function createMiddleware(client: TriggerClient, path: string = "/api/tri return; } + if (response.headers) { + for (const [key, value] of Object.entries(response.headers)) { + if (typeof value === "string") { + res.setHeader(key, value); + } + } + } + res.status(response.status).json(response.body); } catch (error) { next(error); diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index b48dd9e87..1f6660cb0 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -14,6 +14,14 @@ export function createPagesRoute(client: TriggerClient) { return; } + if (response.headers) { + for (const [key, value] of Object.entries(response.headers)) { + if (typeof value === "string") { + res.setHeader(key, value); + } + } + } + res.status(response.status).json(response.body); }; @@ -35,7 +43,7 @@ export function createAppRoute(client: TriggerClient) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } - return NextResponse.json(response.body, { status: response.status }); + return NextResponse.json(response.body, { status: response.status, headers: response.headers }); }; return { diff --git a/packages/remix/src/index.ts b/packages/remix/src/index.ts index 347e34da8..04ec567e8 100644 --- a/packages/remix/src/index.ts +++ b/packages/remix/src/index.ts @@ -9,7 +9,7 @@ export function createRemixRoute(client: TriggerClient) { return json({ error: "Not found" }, { status: 404 }); } - return json(response.body, { status: response.status }); + return json(response.body, { status: response.status, headers: response.headers }); }; return { action }; } diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index 744a5cb8c..42a2976a5 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -27,6 +27,8 @@ import { JobRunStatusRecordSchema, StatusUpdate, urlWithSearchParams, + RunTaskResponseWithCachedTasksBodySchema, + API_VERSIONS, } from "@trigger.dev/core"; import fetch, { type RequestInit } from "node-fetch"; @@ -106,22 +108,35 @@ export class ApiClient { return await response.json(); } - async runTask(runId: string, task: RunTaskBodyInput) { + async runTask( + runId: string, + task: RunTaskBodyInput, + options: { cachedTasksCursor?: string } = {} + ) { const apiKey = await this.#apiKey(); this.#logger.debug("Running Task", { task, }); - return await zodfetch(ServerTaskSchema, `${this.#apiUrl}/api/v1/runs/${runId}/tasks`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "Idempotency-Key": task.idempotencyKey, + return await zodfetchWithVersions( + { + [API_VERSIONS.LAZY_LOADED_CACHED_TASKS]: RunTaskResponseWithCachedTasksBodySchema, }, - body: JSON.stringify(task), - }); + ServerTaskSchema, + `${this.#apiUrl}/api/v1/runs/${runId}/tasks`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "Idempotency-Key": task.idempotencyKey, + "X-Cached-Tasks-Cursor": options.cachedTasksCursor ?? "", + "Trigger-Version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, + }, + body: JSON.stringify(task), + } + ); } async completeTask(runId: string, id: string, task: CompleteTaskBodyInput) { @@ -481,15 +496,103 @@ function getApiKey(key?: string) { return { status: "valid" as const, apiKey }; } -async function zodfetch( - schema: z.Schema, +type VersionedResponseBodyMap = { + [key: string]: z.ZodTypeAny; +}; + +// The resulting type should be a discriminating union +// For example, if the TVersions param is { "2023_09_29": z.string() } and the TUnversioned param is z.number(), the resulting type should be: +// type VersionedResponseBody = { version: "2023_09_29"; body: string } | { version: "unversioned"; body: number } +type VersionedResponseBody< + TVersions extends VersionedResponseBodyMap, + TUnversioned extends z.ZodTypeAny, +> = + | { + [TVersion in keyof TVersions]: { + version: TVersion; + body: z.infer; + }; + }[keyof TVersions] + | { + version: "unversioned"; + body: z.infer; + }; + +async function zodfetchWithVersions< + TVersionedResponseBodyMap extends VersionedResponseBodyMap, + TUnversionedResponseBodySchema extends z.ZodTypeAny, + TOptional extends boolean = false, +>( + versionedSchemaMap: TVersionedResponseBodyMap, + unversionedSchema: TUnversionedResponseBodySchema, url: string, requestInit?: RequestInit, options?: { errorMessage?: string; optional?: TOptional; } -): Promise { +): Promise< + TOptional extends true + ? VersionedResponseBody | undefined + : VersionedResponseBody +> { + const response = await fetch(url, requestInit); + + if ( + (!requestInit || requestInit.method === "GET") && + response.status === 404 && + options?.optional + ) { + // @ts-ignore + return; + } + + if (response.status >= 400 && response.status < 500) { + const body = await response.json(); + + throw new Error(body.error); + } + + if (response.status !== 200) { + throw new Error( + options?.errorMessage ?? `Failed to fetch ${url}, got status code ${response.status}` + ); + } + + const jsonBody = await response.json(); + + const version = response.headers.get("trigger-version"); + + if (!version) { + return { + version: "unversioned", + body: unversionedSchema.parse(jsonBody), + }; + } + + const versionedSchema = versionedSchemaMap[version]; + + if (!versionedSchema) { + throw new Error(`Unknown version ${version}`); + } + + return { + version, + body: versionedSchema.parse(jsonBody), + }; +} + +async function zodfetch( + schema: TResponseSchema, + url: string, + requestInit?: RequestInit, + options?: { + errorMessage?: string; + optional?: TOptional; + } +): Promise< + TOptional extends true ? z.infer | undefined : z.infer +> { const response = await fetch(url, requestInit); if ( diff --git a/packages/trigger-sdk/src/errors.ts b/packages/trigger-sdk/src/errors.ts index 5ef025203..9e2aeec0e 100644 --- a/packages/trigger-sdk/src/errors.ts +++ b/packages/trigger-sdk/src/errors.ts @@ -16,6 +16,10 @@ export class CanceledWithTaskError { constructor(public task: ServerTask) {} } +export class YieldExecutionError { + constructor(public key: string) {} +} + export class ParsedPayloadSchemaError { constructor(public schemaErrors: SchemaError[]) {} } @@ -32,6 +36,7 @@ export function isTriggerError( return ( err instanceof ResumeWithTaskError || err instanceof RetryWithTaskError || - err instanceof CanceledWithTaskError + err instanceof CanceledWithTaskError || + err instanceof YieldExecutionError ); } diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts index 9286e7109..58d5a95e9 100644 --- a/packages/trigger-sdk/src/io.ts +++ b/packages/trigger-sdk/src/io.ts @@ -1,4 +1,6 @@ import { + API_VERSIONS, + BloomFilter, CachedTask, ConnectionAuth, CronOptions, @@ -15,6 +17,7 @@ import { SerializableJsonSchema, ServerTask, UpdateTriggerSourceBodyV2, + supportsFeature, } from "@trigger.dev/core"; import { AsyncLocalStorage } from "node:async_hooks"; import { webcrypto } from "node:crypto"; @@ -23,6 +26,7 @@ import { CanceledWithTaskError, ResumeWithTaskError, RetryWithTaskError, + YieldExecutionError, isTriggerError, } from "./errors"; import { calculateRetryAt } from "./retry"; @@ -46,6 +50,10 @@ export type IOOptions = { jobLogger?: Logger; jobLogLevel: LogLevel; cachedTasks?: Array; + cachedTasksCursor?: string; + yieldedExecutions?: Array; + noopTasksSet?: string; + serverVersion?: string | null; }; type JsonPrimitive = string | number | boolean | null | undefined | Date | symbol; @@ -63,6 +71,16 @@ export type RunTaskErrorCallback = ( | undefined | void; +export type IOStats = { + initialCachedTasks: number; + lazyLoadedCachedTasks: number; + executedTasks: number; + cachedTaskHits: number; + cachedTaskMisses: number; + noopCachedTaskHits: number; + noopCachedTaskMisses: number; +}; + export class IO { private _id: string; private _apiClient: ApiClient; @@ -72,7 +90,16 @@ export class IO { private _jobLogLevel: LogLevel; private _cachedTasks: Map; private _taskStorage: AsyncLocalStorage<{ taskId: string }>; + private _cachedTasksCursor?: string; private _context: TriggerContext; + private _yieldedExecutions: Array; + private _noopTasksBloomFilter: BloomFilter | undefined; + private _stats: IOStats; + private _serverVersion: string; + + get stats() { + return this._stats; + } constructor(options: IOOptions) { this._id = options.id; @@ -83,14 +110,37 @@ export class IO { this._jobLogger = options.jobLogger; this._jobLogLevel = options.jobLogLevel; + this._stats = { + initialCachedTasks: 0, + lazyLoadedCachedTasks: 0, + executedTasks: 0, + cachedTaskHits: 0, + cachedTaskMisses: 0, + noopCachedTaskHits: 0, + noopCachedTaskMisses: 0, + }; + if (options.cachedTasks) { options.cachedTasks.forEach((task) => { this._cachedTasks.set(task.idempotencyKey, task); }); + + this._stats.initialCachedTasks = options.cachedTasks.length; } this._taskStorage = new AsyncLocalStorage(); this._context = options.context; + this._yieldedExecutions = options.yieldedExecutions ?? []; + + if (options.noopTasksSet) { + this._noopTasksBloomFilter = BloomFilter.deserialize( + options.noopTasksSet, + BloomFilter.NOOP_TASK_SET_SIZE + ); + } + + this._cachedTasksCursor = options.cachedTasksCursor; + this._serverVersion = options.serverVersion ?? "unversioned"; } /** @internal */ @@ -108,44 +158,48 @@ export class IO { return new IOLogger(async (level, message, data) => { let logLevel: LogLevel = "info"; - switch (level) { - case "LOG": { - this._jobLogger?.log(message, data); - logLevel = "log"; - break; - } - case "DEBUG": { - this._jobLogger?.debug(message, data); - logLevel = "debug"; - break; - } - case "INFO": { - this._jobLogger?.info(message, data); - logLevel = "info"; - break; - } - case "WARN": { - this._jobLogger?.warn(message, data); - logLevel = "warn"; - break; - } - case "ERROR": { - this._jobLogger?.error(message, data); - logLevel = "error"; - break; - } - } - if (Logger.satisfiesLogLevel(logLevel, this._jobLogLevel)) { - await this.runTask([message, level], async (task) => {}, { - name: "log", - icon: "log", - description: message, - params: data, - properties: [{ label: "Level", text: level }], - style: { style: "minimal", variant: level.toLowerCase() }, - noop: true, - }); + await this.runTask( + [message, level], + async (task) => { + switch (level) { + case "LOG": { + this._jobLogger?.log(message, data); + logLevel = "log"; + break; + } + case "DEBUG": { + this._jobLogger?.debug(message, data); + logLevel = "debug"; + break; + } + case "INFO": { + this._jobLogger?.info(message, data); + logLevel = "info"; + break; + } + case "WARN": { + this._jobLogger?.warn(message, data); + logLevel = "warn"; + break; + } + case "ERROR": { + this._jobLogger?.error(message, data); + logLevel = "error"; + break; + } + } + }, + { + name: "log", + icon: "log", + description: message, + params: data, + properties: [{ label: "Level", text: level }], + style: { style: "minimal", variant: level.toLowerCase() }, + noop: true, + } + ); } }); } @@ -549,19 +603,59 @@ export class IO { if (cachedTask && cachedTask.status === "COMPLETED") { this._logger.debug("Using completed cached task", { idempotencyKey, - cachedTask, }); + this._stats.cachedTaskHits++; + return cachedTask.output as T; } - const task = await this._apiClient.runTask(this._id, { - idempotencyKey, - displayKey: typeof key === "string" ? key : key.join("."), - noop: false, - ...(options ?? {}), - parentId, - }); + if (options?.noop && this._noopTasksBloomFilter) { + if (this._noopTasksBloomFilter.test(idempotencyKey)) { + this._logger.debug("task idempotency key exists in noopTasksBloomFilter", { + idempotencyKey, + }); + + this._stats.noopCachedTaskHits++; + + return {} as T; + } + } + + const response = await this._apiClient.runTask( + this._id, + { + idempotencyKey, + displayKey: typeof key === "string" ? key : undefined, + noop: false, + ...(options ?? {}), + parentId, + }, + { + cachedTasksCursor: this._cachedTasksCursor, + } + ); + + const task = + response.version === API_VERSIONS.LAZY_LOADED_CACHED_TASKS + ? response.body.task + : response.body; + + if (response.version === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) { + this._cachedTasksCursor = response.body.cachedTasks?.cursor; + + for (const cachedTask of response.body.cachedTasks?.tasks ?? []) { + if (!this._cachedTasks.has(cachedTask.idempotencyKey)) { + this._cachedTasks.set(cachedTask.idempotencyKey, cachedTask); + + this._logger.debug("Injecting lazy loaded task into task cache", { + idempotencyKey: cachedTask.idempotencyKey, + }); + + this._stats.lazyLoadedCachedTasks++; + } + } + } if (task.status === "CANCELED") { this._logger.debug("Task canceled", { @@ -573,12 +667,20 @@ export class IO { } if (task.status === "COMPLETED") { - this._logger.debug("Using task output", { - idempotencyKey, - task, - }); + if (task.noop) { + this._logger.debug("Noop Task completed", { + idempotencyKey, + }); - this.#addToCachedTasks(task); + this._noopTasksBloomFilter?.add(task.idempotencyKey); + } else { + this._logger.debug("Cache miss", { + idempotencyKey, + }); + + this._stats.cachedTaskMisses++; + this.#addToCachedTasks(task); + } return task.output as T; } @@ -626,6 +728,8 @@ export class IO { properties: task.outputProperties ?? undefined, }); + this._stats.executedTasks++; + if (completedTask.status === "CANCELED") { throw new CanceledWithTaskError(completedTask); } @@ -699,6 +803,30 @@ export class IO { return this._taskStorage.run({ taskId: task.id }, executeTask); } + /** + * `io.yield()` allows you to yield execution of the current run and resume it in a new function execution. Similar to `io.wait()` but does not create a task and resumes execution immediately. + */ + yield(key: string) { + if (!supportsFeature("yieldExecution", this._serverVersion)) { + console.warn( + "[trigger.dev] io.yield() is not support by the version of the Trigger.dev server you are using, you will need to upgrade your self-hosted Trigger.dev instance." + ); + + return; + } + + if (this._yieldedExecutions.includes(key)) { + return; + } + + throw new YieldExecutionError(key); + } + + /** + * `io.brb()` is an alias of `io.yield()` + */ + brb = this.yield.bind(this); + /** `io.try()` allows you to run Tasks and catch any errors that are thrown, it's similar to a normal `try/catch` block but works with [io.runTask()](/sdk/io/runtask). * A regular `try/catch` block on its own won't work as expected with Tasks. Internally `runTask()` throws some special errors to control flow execution. This is necessary to deal with resumability, serverless timeouts, and retrying Tasks. * @param tryCallback The code you wish to run diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index 44c638dcf..55412d89b 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -1,4 +1,5 @@ import { + API_VERSIONS, ConnectionAuth, DeserializedJson, ErrorWithStackSchema, @@ -36,9 +37,10 @@ import { ParsedPayloadSchemaError, ResumeWithTaskError, RetryWithTaskError, + YieldExecutionError, } from "./errors"; import { TriggerIntegration } from "./integrations"; -import { IO } from "./io"; +import { IO, IOStats } from "./io"; import { createIOWithIntegrations } from "./ioWithIntegrations"; import { Job, JobOptions } from "./job"; import { runLocalStorage } from "./runLocalStorage"; @@ -124,7 +126,10 @@ export class TriggerClient { this.id = options.id; this.#options = options; this.#client = new ApiClient(this.#options); - this.#internalLogger = new Logger("trigger.dev", this.#options.verbose ? "debug" : "log"); + this.#internalLogger = new Logger("trigger.dev", this.#options.verbose ? "debug" : "log", [ + "output", + "noopTasksSet", + ]); } async handleRequest(request: Request): Promise { @@ -135,6 +140,7 @@ export class TriggerClient { }); const apiKey = request.headers.get("x-trigger-api-key"); + const triggerVersion = request.headers.get("x-trigger-version"); const authorization = this.authorized(apiKey); @@ -148,6 +154,7 @@ export class TriggerClient { body: { message: "Unauthorized: client missing apiKey", }, + headers: this.#standardResponseHeaders, }; } case "missing-header": { @@ -156,6 +163,7 @@ export class TriggerClient { body: { message: "Unauthorized: missing x-trigger-api-key header", }, + headers: this.#standardResponseHeaders, }; } case "unauthorized": { @@ -164,6 +172,7 @@ export class TriggerClient { body: { message: `Forbidden: client apiKey mismatch: Make sure you are using the correct API Key for your environment`, }, + headers: this.#standardResponseHeaders, }; } } @@ -174,6 +183,7 @@ export class TriggerClient { body: { message: "Method not allowed (only POST is allowed)", }, + headers: this.#standardResponseHeaders, }; } @@ -185,6 +195,7 @@ export class TriggerClient { body: { message: "Missing x-trigger-action header", }, + headers: this.#standardResponseHeaders, }; } @@ -199,6 +210,7 @@ export class TriggerClient { ok: false, error: "Missing endpoint ID", }, + headers: this.#standardResponseHeaders, }; } @@ -209,6 +221,7 @@ export class TriggerClient { ok: false, error: `Endpoint ID mismatch error. Expected ${this.id}, got ${endpointId}`, }, + headers: this.#standardResponseHeaders, }; } @@ -217,6 +230,7 @@ export class TriggerClient { body: { ok: true, }, + headers: this.#standardResponseHeaders, }; } case "INDEX_ENDPOINT": { @@ -241,6 +255,7 @@ export class TriggerClient { return { status: 200, body, + headers: this.#standardResponseHeaders, }; } case "INITIALIZE_TRIGGER": { @@ -270,6 +285,7 @@ export class TriggerClient { return { status: 200, body: dynamicTrigger.registeredTriggerForParams(body.data.params), + headers: this.#standardResponseHeaders, }; } case "EXECUTE_JOB": { @@ -296,11 +312,12 @@ export class TriggerClient { }; } - const results = await this.#executeJob(execution.data, job); + const results = await this.#executeJob(execution.data, job, triggerVersion); return { status: 200, body: results, + headers: this.#standardResponseHeaders, }; } case "PREPROCESS_RUN": { @@ -335,6 +352,7 @@ export class TriggerClient { abort: results.abort, properties: results.properties, }, + headers: this.#standardResponseHeaders, }; } case "DELIVER_HTTP_SOURCE_REQUEST": { @@ -400,6 +418,7 @@ export class TriggerClient { response, metadata, }, + headers: this.#standardResponseHeaders, }; } case "VALIDATE": { @@ -409,6 +428,7 @@ export class TriggerClient { ok: true, endpointId: this.id, }, + headers: this.#standardResponseHeaders, }; } } @@ -418,6 +438,7 @@ export class TriggerClient { body: { message: "Method not allowed", }, + headers: this.#standardResponseHeaders, }; } @@ -664,12 +685,14 @@ export class TriggerClient { async #executeJob( body: RunJobBody, - job: Job, Record> + job: Job, Record>, + triggerVersion: string | null ): Promise { this.#internalLogger.debug("executing job", { execution: body, job: job.id, version: job.version, + triggerVersion, }); const context = this.#createRunContext(body); @@ -677,6 +700,9 @@ export class TriggerClient { const io = new IO({ id: body.run.id, cachedTasks: body.tasks, + cachedTasksCursor: body.cachedTaskCursor, + yieldedExecutions: body.yieldedExecutions ?? [], + noopTasksSet: body.noopTasksSet, apiClient: this.#client, logger: this.#internalLogger, client: this, @@ -685,6 +711,7 @@ export class TriggerClient { jobLogger: this.#options.ioLogLocalEnabled ? new Logger(job.id, job.logLevel ?? this.#options.logLevel ?? "info") : undefined, + serverVersion: triggerVersion, }); const resolvedConnections = await this.#resolveConnections( @@ -715,8 +742,20 @@ export class TriggerClient { ); }); + if (this.#options.verbose) { + this.#logIOStats(io.stats); + } + return { status: "SUCCESS", output }; } catch (error) { + if (this.#options.verbose) { + this.#logIOStats(io.stats); + } + + if (error instanceof YieldExecutionError) { + return { status: "YIELD_EXECUTION", key: error.key }; + } + if (error instanceof ParsedPayloadSchemaError) { return { status: "INVALID_PAYLOAD", errors: error.schemaErrors }; } @@ -1108,6 +1147,18 @@ export class TriggerClient { authSource, }; } + + #logIOStats(stats: IOStats) { + this.#internalLogger.debug("IO stats", { + stats, + }); + } + + get #standardResponseHeaders() { + return { + "Trigger-Version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, + }; + } } function dynamicTriggerRegisterSourceJobId(id: string) { diff --git a/references/job-catalog/src/stressTest.ts b/references/job-catalog/src/stressTest.ts index a6414046c..305dc152d 100644 --- a/references/job-catalog/src/stressTest.ts +++ b/references/job-catalog/src/stressTest.ts @@ -5,7 +5,7 @@ export const client = new TriggerClient({ id: "job-catalog", apiKey: process.env["TRIGGER_API_KEY"], apiUrl: process.env["TRIGGER_API_URL"], - verbose: false, + verbose: true, ioLogLocalEnabled: true, }); @@ -139,4 +139,67 @@ client.defineJob({ }, }); +client.defineJob({ + id: "stress.logs-of-logs", + name: "Lots of Logs", + version: "1.0.0", + trigger: eventTrigger({ + name: "lots.of.logs", + }), + run: async (payload, io, ctx) => { + // Do lots of logs + for (let i = 0; i < payload.iterations; i++) { + await io.logger.info(`before-yield: Iteration ${i} started`); + } + + // Each are 300KB + for (let i = 0; i < payload.iterations; i++) { + await io.runTask( + `before.yield.${i}`, + async (task) => { + return { + i, + extra: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n".repeat( + (300 * payload.size) / 60 + ), + }; + }, + { name: `before-yield: Task ${i}` } + ); + } + + io.yield("yield 1"); + + // Do lots of logs + for (let i = 0; i < payload.iterations; i++) { + await io.logger.info(`after-yield: Iteration ${i} started`); + } + + for (let i = 0; i < payload.iterations; i++) { + await io.runTask( + `after-yield.task.${i}`, + async (task) => { + return { + i, + extra: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n".repeat( + (300 * payload.size) / 60 + ), + }; + }, + { name: `after-yield: Task ${i}` } + ); + } + + await io.wait("wait-1", 10); + + await io.runTask( + `after-wait.task`, + async (task) => { + return { i: 0 }; + }, + { name: `after-wait: Task 0` } + ); + }, +}); + createExpressServer(client); From 81e886a1ba8276466396a1be96bd5c95335bc133 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:48:21 +0100 Subject: [PATCH 02/79] Add typed filters to `Linear` getAll helper (#517) * Fix getAll params type * Changeset * Search param types * Update changeset --- .changeset/sour-jars-punch.md | 5 +++++ integrations/linear/src/index.ts | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/sour-jars-punch.md diff --git a/.changeset/sour-jars-punch.md b/.changeset/sour-jars-punch.md new file mode 100644 index 000000000..28abcd24d --- /dev/null +++ b/.changeset/sour-jars-punch.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/linear": patch +--- + +Fix `getAll` helper and search function params diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index f6318ee7a..dde947e7d 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -182,7 +182,7 @@ export class Linear implements TriggerIntegration { >( task: TTask, key: IntegrationTaskKey, - params: Nullable = {} + params: Parameters[1] = {} ): Promise>["nodes"]> { const boundTask = task.bind(this as any); @@ -695,7 +695,7 @@ export class Linear implements TriggerIntegration { key: IntegrationTaskKey, params: { term: string; - variables?: L.SearchDocumentsQueryVariables; + variables?: Parameters[1]; } ): LinearReturnType { return this.runTask( @@ -862,7 +862,7 @@ export class Linear implements TriggerIntegration { key: IntegrationTaskKey, params: { term: string; - variables?: L.SearchIssuesQueryVariables; + variables?: Parameters[1]; } ): LinearReturnType { return this.runTask( @@ -1273,7 +1273,7 @@ export class Linear implements TriggerIntegration { key: IntegrationTaskKey, params: { term: string; - variables?: L.SearchProjectsQueryVariables; + variables?: Parameters[1]; } ): LinearReturnType { return this.runTask( From 6a992a199551c017e5c65e60037fadd88e60d0fc Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:46:17 +0100 Subject: [PATCH 03/79] `Replicate` integration and remote callbacks (#507) * Support tasks with remote callbacks * Add common integration tsconfig * Add Replicate integration * Basic job catalog example * Integration catalog entry * Check for callbackUrl during executeTask * Fix getAll * Improve JSDoc * Bump version * Remove named queue * Simplify runTask types * Trust the types * Fail tasks on timeout * Callback timeout as param * Mess with types * performRunExecutionV1 * Update runTask docs * Shorten callback task methods * Fix run method return type * Image processing jobs * Replicate docs * Text output example * Changeset * Version bump * Roll back ugly types * Remove missing types * Quicker return when waiting on remote callback * Remote callback example * Bump version * Remove schema parsing * Only schedule positive callback timeout * Decrease callback secret length * Explicit default timeouts * Import deployments tasks * JSDoc * Deployments docs * Fix runTask examples, mention wrappers --------- Co-authored-by: Eric Allam --- .changeset/fair-plums-grin.md | 13 + apps/webapp/app/models/task.server.ts | 1 + ....runs.$runId.tasks.$id.callback.$secret.ts | 124 ++++++++ .../app/routes/api.v1.runs.$runId.tasks.ts | 28 +- .../externalApis/integrationCatalog.server.ts | 2 + .../externalApis/integrations/replicate.ts | 50 ++++ .../runs/performRunExecutionV1.server.ts | 4 +- .../runs/performRunExecutionV2.server.ts | 4 +- .../app/services/sources/utils.server.ts | 4 +- .../services/tasks/processCallbackTimeout.ts | 76 +++++ apps/webapp/app/services/worker.server.ts | 13 + config-packages/tsconfig/integration.json | 17 ++ docs/integrations/apis/replicate.mdx | 170 +++++++++++ docs/integrations/create-tasks.mdx | 2 +- docs/integrations/introduction.mdx | 23 +- docs/mint.json | 1 + docs/sdk/io/runtask.mdx | 65 +++- integrations/airtable/src/index.ts | 2 +- integrations/github/src/index.ts | 2 +- integrations/linear/src/index.ts | 2 +- integrations/replicate/README.md | 1 + integrations/replicate/package.json | 37 +++ integrations/replicate/src/collections.ts | 37 +++ integrations/replicate/src/deployments.ts | 76 +++++ integrations/replicate/src/index.ts | 280 ++++++++++++++++++ integrations/replicate/src/models.ts | 82 +++++ integrations/replicate/src/predictions.ts | 101 +++++++ integrations/replicate/src/trainings.ts | 113 +++++++ integrations/replicate/src/types.ts | 3 + integrations/replicate/src/utils.ts | 58 ++++ integrations/replicate/tsconfig.json | 4 + integrations/replicate/tsup.config.ts | 22 ++ integrations/resend/src/index.ts | 2 +- integrations/sendgrid/src/index.ts | 2 +- integrations/slack/src/index.ts | 2 +- packages/core/src/schemas/api.ts | 16 + packages/core/src/schemas/tasks.ts | 1 + .../migration.sql | 2 + packages/database/prisma/schema.prisma | 1 + packages/trigger-sdk/src/io.ts | 48 +-- references/job-catalog/package.json | 4 +- references/job-catalog/src/replicate.ts | 146 +++++++++ references/job-catalog/tsconfig.json | 6 + 43 files changed, 1600 insertions(+), 47 deletions(-) create mode 100644 .changeset/fair-plums-grin.md create mode 100644 apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts create mode 100644 apps/webapp/app/services/externalApis/integrations/replicate.ts create mode 100644 apps/webapp/app/services/tasks/processCallbackTimeout.ts create mode 100644 config-packages/tsconfig/integration.json create mode 100644 docs/integrations/apis/replicate.mdx create mode 100644 integrations/replicate/README.md create mode 100644 integrations/replicate/package.json create mode 100644 integrations/replicate/src/collections.ts create mode 100644 integrations/replicate/src/deployments.ts create mode 100644 integrations/replicate/src/index.ts create mode 100644 integrations/replicate/src/models.ts create mode 100644 integrations/replicate/src/predictions.ts create mode 100644 integrations/replicate/src/trainings.ts create mode 100644 integrations/replicate/src/types.ts create mode 100644 integrations/replicate/src/utils.ts create mode 100644 integrations/replicate/tsconfig.json create mode 100644 integrations/replicate/tsup.config.ts create mode 100644 packages/database/prisma/migrations/20230925174509_add_callback_url/migration.sql create mode 100644 references/job-catalog/src/replicate.ts diff --git a/.changeset/fair-plums-grin.md b/.changeset/fair-plums-grin.md new file mode 100644 index 000000000..31579ad8c --- /dev/null +++ b/.changeset/fair-plums-grin.md @@ -0,0 +1,13 @@ +--- +"@trigger.dev/replicate": patch +"@trigger.dev/airtable": patch +"@trigger.dev/sendgrid": patch +"@trigger.dev/sdk": patch +"@trigger.dev/github": patch +"@trigger.dev/linear": patch +"@trigger.dev/resend": patch +"@trigger.dev/slack": patch +"@trigger.dev/core": patch +--- + +First release of `@trigger.dev/replicate` integration with remote callback support. diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts index ac468779a..b674dd0a4 100644 --- a/apps/webapp/app/models/task.server.ts +++ b/apps/webapp/app/models/task.server.ts @@ -23,6 +23,7 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask attempts: task.attempts.length, idempotencyKey: task.idempotencyKey, operation: task.operation, + callbackUrl: task.callbackUrl, }; } diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts new file mode 100644 index 000000000..5c1425e2a --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts @@ -0,0 +1,124 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { z } from "zod"; +import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server"; +import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server"; +import { logger } from "~/services/logger.server"; + +const ParamsSchema = z.object({ + runId: z.string(), + id: z.string(), + secret: z.string(), +}); + +export async function action({ request, params }: ActionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + const { runId, id } = ParamsSchema.parse(params); + + // Parse body as JSON (no schema parsing) + const body = await request.json(); + + const service = new CallbackRunTaskService(); + + try { + // Complete task with request body as output + await service.call(runId, id, body, request.url); + + return json({ success: true }); + } catch (error) { + if (error instanceof Error) { + logger.error("Error while processing task callback:", { error }); + } + + return json({ error: "Something went wrong" }, { status: 500 }); + } +} + +export class CallbackRunTaskService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(runId: string, id: string, taskBody: any, callbackUrl: string): Promise { + const task = await findTask(prisma, id); + + if (!task) { + return; + } + + if (task.runId !== runId) { + return; + } + + if (task.status !== "WAITING") { + return; + } + + if (!task.callbackUrl) { + return; + } + + if (new URL(task.callbackUrl).pathname !== new URL(callbackUrl).pathname) { + logger.error("Callback URLs don't match", { runId, taskId: id, callbackUrl }); + return; + } + + logger.debug("CallbackRunTaskService.call()", { task }); + + await this.#resumeTask(task, taskBody); + } + + async #resumeTask(task: NonNullable, output: any) { + await $transaction(this.#prismaClient, async (tx) => { + await tx.taskAttempt.updateMany({ + where: { + taskId: task.id, + status: "PENDING", + }, + data: { + status: "COMPLETED", + }, + }); + + await tx.task.update({ + where: { id: task.id }, + data: { + status: "COMPLETED", + completedAt: new Date(), + output: output ? output : undefined, + }, + }); + + await this.#resumeRunExecution(task, tx); + }); + } + + async #resumeRunExecution(task: NonNullable, prisma: PrismaClientOrTransaction) { + await enqueueRunExecutionV2(task.run, prisma, { + skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + }); + } +} + +type FoundTask = Awaited>; + +async function findTask(prisma: PrismaClientOrTransaction, id: string) { + return prisma.task.findUnique({ + where: { id }, + include: { + run: { + include: { + environment: true, + queue: true, + }, + }, + }, + }); +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 54c964534..94a6ba8ca 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -15,6 +15,8 @@ import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { ulid } from "~/services/ulid.server"; import { workerQueue } from "~/services/worker.server"; +import { generateSecret } from "~/services/sources/utils.server"; +import { env } from "~/env.server"; const ParamsSchema = z.object({ runId: z.string(), @@ -185,10 +187,13 @@ export class RunTaskService { }, }); + const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now(); + const callbackEnabled = taskBody.callback?.enabled; + if (existingTask) { if (existingTask.status === "CANCELED") { const existingTaskStatus = - (taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger + delayUntilInFuture || callbackEnabled || taskBody.trigger ? "WAITING" : taskBody.noop ? "COMPLETED" @@ -233,16 +238,21 @@ export class RunTaskService { status = "CANCELED"; } else { status = - (taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger + delayUntilInFuture || callbackEnabled || taskBody.trigger ? "WAITING" : taskBody.noop ? "COMPLETED" : "RUNNING"; } + const taskId = ulid(); + const callbackUrl = callbackEnabled + ? `${env.APP_ORIGIN}/api/v1/runs/${runId}/tasks/${taskId}/callback/${generateSecret(12)}` + : undefined; + const task = await tx.task.create({ data: { - id: ulid(), + id: taskId, idempotencyKey, displayKey: taskBody.displayKey, runConnection: taskBody.connectionKey @@ -273,6 +283,7 @@ export class RunTaskService { properties: taskBody.properties ?? undefined, redact: taskBody.redact ?? undefined, operation: taskBody.operation, + callbackUrl, style: taskBody.style ?? { style: "normal" }, attempts: { create: { @@ -296,6 +307,17 @@ export class RunTaskService { }, { tx, runAt: task.delayUntil ?? undefined } ); + } else if (task.status === "WAITING" && callbackUrl && taskBody.callback) { + if (taskBody.callback.timeoutInSeconds > 0) { + // We need to schedule the callback timeout + await workerQueue.enqueue( + "processCallbackTimeout", + { + id: task.id, + }, + { tx, runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000) } + ); + } } return task; diff --git a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts index b86c2e496..3c13f154a 100644 --- a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts +++ b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts @@ -3,6 +3,7 @@ import { github } from "./integrations/github"; import { linear } from "./integrations/linear"; import { openai } from "./integrations/openai"; import { plain } from "./integrations/plain"; +import { replicate } from "./integrations/replicate"; import { resend } from "./integrations/resend"; import { sendgrid } from "./integrations/sendgrid"; import { slack } from "./integrations/slack"; @@ -37,6 +38,7 @@ export const integrationCatalog = new IntegrationCatalog({ linear, openai, plain, + replicate, resend, slack, stripe, diff --git a/apps/webapp/app/services/externalApis/integrations/replicate.ts b/apps/webapp/app/services/externalApis/integrations/replicate.ts new file mode 100644 index 000000000..74f20cdaf --- /dev/null +++ b/apps/webapp/app/services/externalApis/integrations/replicate.ts @@ -0,0 +1,50 @@ +import type { HelpSample, Integration } from "../types"; + +function usageSample(hasApiKey: boolean): HelpSample { + const apiKeyPropertyName = "apiKey"; + + return { + title: "Using the client", + code: ` +import { Replicate } from "@trigger.dev/replicate"; + +const replicate = new Replicate({ + id: "__SLUG__",${hasApiKey ? `,\n ${apiKeyPropertyName}: process.env.REPLICATE_API_KEY!` : ""} +}); + +client.defineJob({ + id: "replicate-create-prediction", + name: "Replicate - Create Prediction", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.predict", + schema: z.object({ + prompt: z.string(), + version: z.string(), + }), + }), + run: async (payload, io, ctx) => { + return io.replicate.predictions.createAndAwait("await-prediction", { + version: payload.version, + input: { prompt: payload.prompt }, + }); + }, +}); + `, + }; +} + +export const replicate: Integration = { + identifier: "replicate", + name: "Replicate", + packageName: "@trigger.dev/replicate@latest", + authenticationMethods: { + apikey: { + type: "apikey", + help: { + samples: [usageSample(true)], + }, + }, + }, +}; diff --git a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts index 7cfdf2d96..c67a9dd18 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts @@ -449,7 +449,9 @@ export class PerformRunExecutionV1Service { // If the task has an operation, then the next performRunExecution will occur // when that operation has finished - if (!data.task.operation) { + // Tasks with callbacks enabled will also get processed separately, i.e. when + // they time out, or on valid requests to their callbackUrl + if (!data.task.operation && !data.task.callbackUrl) { const newJobExecution = await tx.jobRunExecution.create({ data: { runId: run.id, diff --git a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts index 1af87b8c3..f22d13edb 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts @@ -530,7 +530,9 @@ export class PerformRunExecutionV2Service { // If the task has an operation, then the next performRunExecution will occur // when that operation has finished - if (!data.task.operation) { + // Tasks with callbacks enabled will also get processed separately, i.e. when + // they time out, or on valid requests to their callbackUrl + if (!data.task.operation && !data.task.callbackUrl) { await enqueueRunExecutionV2(run, tx, { runAt: data.task.delayUntil ?? undefined, resumeTaskId: data.task.id, diff --git a/apps/webapp/app/services/sources/utils.server.ts b/apps/webapp/app/services/sources/utils.server.ts index 127ca4b7a..4c2bc7ae7 100644 --- a/apps/webapp/app/services/sources/utils.server.ts +++ b/apps/webapp/app/services/sources/utils.server.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -export function generateSecret(): string { - return crypto.randomBytes(32).toString("hex"); +export function generateSecret(sizeInBytes = 32): string { + return crypto.randomBytes(sizeInBytes).toString("hex"); } diff --git a/apps/webapp/app/services/tasks/processCallbackTimeout.ts b/apps/webapp/app/services/tasks/processCallbackTimeout.ts new file mode 100644 index 000000000..948691990 --- /dev/null +++ b/apps/webapp/app/services/tasks/processCallbackTimeout.ts @@ -0,0 +1,76 @@ +import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server"; +import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server"; +import { logger } from "../logger.server"; + +type FoundTask = Awaited>; + +export class ProcessCallbackTimeoutService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(id: string) { + const task = await findTask(this.#prismaClient, id); + + if (!task) { + return; + } + + if (task.status !== "WAITING" || !task.callbackUrl) { + return; + } + + logger.debug("ProcessCallbackTimeoutService.call", { task }); + + return await this.#failTask(task, "Remote callback timeout - no requests received"); + } + + async #failTask(task: NonNullable, error: string) { + await $transaction(this.#prismaClient, async (tx) => { + await tx.taskAttempt.updateMany({ + where: { + taskId: task.id, + status: "PENDING", + }, + data: { + status: "ERRORED", + error + }, + }); + + await tx.task.update({ + where: { id: task.id }, + data: { + status: "ERRORED", + completedAt: new Date(), + output: error, + }, + }); + + await this.#resumeRunExecution(task, tx); + }); + } + + async #resumeRunExecution(task: NonNullable, prisma: PrismaClientOrTransaction) { + await enqueueRunExecutionV2(task.run, prisma, { + skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + }); + } +} + +async function findTask(prisma: PrismaClient, id: string) { + return prisma.task.findUnique({ + where: { id }, + include: { + run: { + include: { + environment: true, + queue: true, + }, + }, + }, + }); +} diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 59c5e8b52..07493aab5 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -19,6 +19,7 @@ import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent. import { ActivateSourceService } from "./sources/activateSource.server"; import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server"; import { PerformTaskOperationService } from "./tasks/performTaskOperation.server"; +import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout"; import { addMissingVersionField } from "@trigger.dev/core"; const workerCatalog = { @@ -30,6 +31,9 @@ const workerCatalog = { }), scheduleEmail: DeliverEmailSchema, startRun: z.object({ id: z.string() }), + processCallbackTimeout: z.object({ + id: z.string(), + }), performTaskOperation: z.object({ id: z.string(), }), @@ -240,6 +244,15 @@ function getWorkerQueue() { await service.call(payload.id); }, }, + processCallbackTimeout: { + priority: 0, // smaller number = higher priority + maxAttempts: 3, + handler: async (payload, job) => { + const service = new ProcessCallbackTimeoutService(); + + await service.call(payload.id); + }, + }, performTaskOperation: { priority: 0, // smaller number = higher priority queueName: (payload) => `tasks:${payload.id}`, diff --git a/config-packages/tsconfig/integration.json b/config-packages/tsconfig/integration.json new file mode 100644 index 000000000..753e6091d --- /dev/null +++ b/config-packages/tsconfig/integration.json @@ -0,0 +1,17 @@ +{ + "extends": "./node18.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"], + "@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"] + }, + "declaration": false, + "declarationMap": false, + "baseUrl": ".", + "stripInternal": true + }, + "exclude": ["node_modules"] +} diff --git a/docs/integrations/apis/replicate.mdx b/docs/integrations/apis/replicate.mdx new file mode 100644 index 000000000..978bb51c1 --- /dev/null +++ b/docs/integrations/apis/replicate.mdx @@ -0,0 +1,170 @@ +--- +title: Replicate +description: "Run machine learning tasks easily at scale" +--- + + + +## Installation + +To get started with the Replicate integration on Trigger.dev, you need to install the `@trigger.dev/replicate` package. +You can do this using npm, pnpm, or yarn: + + + +```bash npm +npm install @trigger.dev/replicate@latest +``` + +```bash pnpm +pnpm add @trigger.dev/replicate@latest +``` + +```bash yarn +yarn add @trigger.dev/replicate@latest +``` + + + +## Authentication + +To use the Replicate API with Trigger.dev, you have to provide an API Key. + +### API Key + +You can create an API Key in your [Account Settings](https://replicate.com/account/api-tokens). + +```ts +import { Replicate } from "@trigger.dev/replicate"; + +//this will use the passed in API key (defined in your environment variables) +const replicate = new Replicate({ + id: "replicate", + apiKey: process.env["REPLICATE_API_KEY"], +}); +``` + +## Usage + +Include the Replicate integration in your Trigger.dev job. + +```ts +client.defineJob({ + id: "replicate-cinematic-prompt", + name: "Replicate - Cinematic Prompt", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.cinematic", + schema: z.object({ + prompt: z.string().default("rick astley riding a harley through post-apocalyptic miami"), + version: z + .string() + .default("af1a68a271597604546c09c64aabcd7782c114a63539a4a8d14d1eeda5630c33"), + }), + }), + run: async (payload, io, ctx) => { + //wait for prediction completion (uses remote callbacks internally) + const prediction = await io.replicate.predictions.createAndAwait("await-prediction", { + version: payload.version, + input: { + prompt: `${payload.prompt}, cinematic, 70mm, anamorphic, bokeh`, + width: 1280, + height: 720, + }, + }); + return prediction.output; + }, +}); +``` + +### Pagination + +You can paginate responses: + +- Using the `getAll` helper +- Using the `paginate` helper + +```ts +client.defineJob({ + id: "replicate-pagination", + name: "Replicate Pagination", + version: "0.1.0", + integrations: { + replicate, + }, + trigger: eventTrigger({ + name: "replicate.paginate", + }), + run: async (payload, io, ctx) => { + // getAll - returns an array of all results (uses paginate internally) + const all = await io.replicate.getAll(io.replicate.predictions.list, "get-all"); + + // paginate - returns an async generator, useful to process one page at a time + for await (const predictions of io.replicate.paginate( + io.replicate.predictions.list, + "paginate-all" + )) { + await io.logger.info("stats", { + total: predictions.length, + versions: predictions.map((p) => p.version), + }); + } + + return { count: all.length }; + }, +}); +``` + +## Tasks + +### Collections + +| Function Name | Description | +| ------------------ | ---------------------------------------------------------------------- | +| `collections.get` | Gets a collection. | +| `collections.list` | Returns the first page of all collections. Use with pagination helper. | + +### Deployments + +| Function Name | Description | +| ---------------------------------------- | --------------------------------------------------------- | +| `deployments.predictions.create` | Creates a new prediction with a deployment. | +| `deployments.predictions.createAndAwait` | Creates and waits for a new prediction with a deployment. | + +### Models + +| Function Name | Description | +| ----------------- | ------------------------ | +| `models.get` | Gets a model. | +| `models.versions` | Gets a model version. | +| `models.versions` | Gets all model versions. | + +### Predictions + +| Function Name | Description | +| ---------------------------- | ---------------------------------------------------------------------- | +| `predictions.cancel` | Cancels a prediction. | +| `predictions.create` | Creates a prediction. | +| `predictions.createAndAwait` | Creates and waits for a prediction. | +| `predictions.get` | Gets a prediction. | +| `predictions.list` | Returns the first page of all predictions. Use with pagination helper. | + +### Trainings + +| Function Name | Description | +| -------------------------- | -------------------------------------------------------------------- | +| `trainings.cancel` | Cancels a training. | +| `trainings.create` | Creates a training. | +| `trainings.createAndAwait` | Creates and waits for a training. | +| `trainings.get` | Gets a training. | +| `trainings.list` | Returns the first page of all trainings. Use with pagination helper. | + +### Misc + +| Function Name | Description | +| ------------- | --------------------------------------------------- | +| `getAll` | Pagination helper that returns an array of results. | +| `paginate` | Pagination helper that returns an async generator. | +| `request` | Sends authenticated requests to the Replicate API. | +| `run` | Creates and waits for a prediction. | diff --git a/docs/integrations/create-tasks.mdx b/docs/integrations/create-tasks.mdx index fe5386a18..2ed3b09df 100644 --- a/docs/integrations/create-tasks.mdx +++ b/docs/integrations/create-tasks.mdx @@ -24,7 +24,7 @@ export class Github implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/docs/integrations/introduction.mdx b/docs/integrations/introduction.mdx index 30be9c1e3..688463510 100644 --- a/docs/integrations/introduction.mdx +++ b/docs/integrations/introduction.mdx @@ -30,14 +30,15 @@ description: "Integrations make it easy to authenticate and use APIs." Navigate the menu or select Integrations from the table below. -| API | Description | Webhooks | Tasks | -| --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- | -| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ | -| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ | -| [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ | -| [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ | -| [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ | -| [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | 🕘 | ✅ | -| [Slack](/integrations/apis/slack) | Send Slack messages | 🕘 | ✅ | -| [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | ✅ | ✅ | -| [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | ✅ | ✅ | +| API | Description | Webhooks | Tasks | +| ----------------------------------------- | ---------------------------------------------------------------- | -------- | ----- | +| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ | +| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ | +| [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ | +| [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ | +| [Replicate](/integrations/apis/replicate) | Run machine learning tasks easily at scale | N/A | ✅ | +| [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ | +| [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | 🕘 | ✅ | +| [Slack](/integrations/apis/slack) | Send Slack messages | 🕘 | ✅ | +| [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | ✅ | ✅ | +| [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | ✅ | ✅ | diff --git a/docs/mint.json b/docs/mint.json index 7d7c3bd4b..02e2cf50e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -247,6 +247,7 @@ "integrations/apis/linear", "integrations/apis/openai", "integrations/apis/plain", + "integrations/apis/replicate", "integrations/apis/resend", "integrations/apis/sendgrid", "integrations/apis/slack", diff --git a/docs/sdk/io/runtask.mdx b/docs/sdk/io/runtask.mdx index 579142998..46a8d829d 100644 --- a/docs/sdk/io/runtask.mdx +++ b/docs/sdk/io/runtask.mdx @@ -6,6 +6,8 @@ description: "`io.runTask()` allows you to run a [Task](/documentation/concepts/ A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. +The wrappers at `io.integration.runTask()` expose the underlying Integration client as the first callback parameter (see examples on the right). They will have defaults set for options and `onError` handlers, but should otherwise be considered identical to raw `io.runTask()`. + ## Parameters @@ -112,6 +114,22 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged. + + + An optional object that exposes settings for the remote callback feature. + + Enabling this feature will expose a `callbackUrl` property on the callback's Task parameter. Additionally, `io.runTask()` will now return a Promise that resolves with the body of the first request sent to that URL. + + + + Whether to enable the remote callback feature. + + + The value of the property. + + + + @@ -133,6 +151,8 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged. A Promise that resolves with the returned value of the callback. +If the remote callback feature `options.callback` is enabled, the Promise will instead resolve with the body of the first request sent to `task.callbackUrl`. + ```typescript Run a task @@ -150,11 +170,11 @@ client.defineJob({ }, run: async (payload, io, ctx) => { //runTask - const response = await io.runTask( + const response = await io.github.runTask( "create-card", - async () => { + async (client) => { //create a project card using the underlying GitHub Integration client - return io.github.client.rest.projects.createCard({ + return client.rest.projects.createCard({ column_id: 123, note: "test", }); @@ -201,4 +221,43 @@ client.defineJob({ }); ``` +```typescript Remote callbacks +client.defineJob({ + id: "remote-callback-example", + name: "Remote Callback example", + version: "0.1.1", + trigger: eventTrigger({ name: "predict" }), + integrations: { replicate }, + run: async (payload, io, ctx) => { + //runTask + const prediction = await io.replicate.runTask( + "create-and-await-prediction", + async (client, task) => { + //create a prediction using the underlying Replicate Integration client + await client.predictions.create({ + ...payload, + webhook: task.callbackUrl ?? "", + webhook_events_filter: ["completed"], + }); + //the actual return value will be the data sent to callbackUrl + //cast to the exact data type you expect to receive or `any` if unsure + return {} as Prediction; + }, + { + name: "Create and await Prediction", + icon: "replicate", + //remote callback settings + callback: { + enabled: true, + timeoutInSeconds: 300, + }, + } + ); + + //log the prediction output + await io.logger.info(prediction.output); + }, +}); +``` + diff --git a/integrations/airtable/src/index.ts b/integrations/airtable/src/index.ts index f8c2d99f1..b4d9d542c 100644 --- a/integrations/airtable/src/index.ts +++ b/integrations/airtable/src/index.ts @@ -92,7 +92,7 @@ export class Airtable implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/github/src/index.ts b/integrations/github/src/index.ts index 59e2dc968..0716e849b 100644 --- a/integrations/github/src/index.ts +++ b/integrations/github/src/index.ts @@ -138,7 +138,7 @@ export class Github implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index dde947e7d..0f53f9e7a 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -158,7 +158,7 @@ export class Linear implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/replicate/README.md b/integrations/replicate/README.md new file mode 100644 index 000000000..67a4b88e8 --- /dev/null +++ b/integrations/replicate/README.md @@ -0,0 +1 @@ +# @trigger.dev/replicate diff --git a/integrations/replicate/package.json b/integrations/replicate/package.json new file mode 100644 index 000000000..9478a0a36 --- /dev/null +++ b/integrations/replicate/package.json @@ -0,0 +1,37 @@ +{ + "name": "@trigger.dev/replicate", + "version": "2.1.7", + "description": "Trigger.dev integration for replicate", + "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": { + "@trigger.dev/tsconfig": "workspace:*", + "@types/node": "16.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": { + "@trigger.dev/integration-kit": "workspace:^2.1.0", + "@trigger.dev/sdk": "workspace:^2.1.0", + "replicate": "^0.18.1", + "zod": "3.21.4" + }, + "engines": { + "node": ">=16.8.0" + } +} \ No newline at end of file diff --git a/integrations/replicate/src/collections.ts b/integrations/replicate/src/collections.ts new file mode 100644 index 000000000..b6f1c0100 --- /dev/null +++ b/integrations/replicate/src/collections.ts @@ -0,0 +1,37 @@ +import { IntegrationTaskKey } from "@trigger.dev/sdk"; +import { Page, Collection } from "replicate"; + +import { ReplicateRunTask } from "./index"; +import { ReplicateReturnType } from "./types"; + +export class Collections { + constructor(private runTask: ReplicateRunTask) {} + + /** Fetch a model collection. */ + get(key: IntegrationTaskKey, params: { slug: string }): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.collections.get(params.slug); + }, + { + name: "Get Collection", + params, + properties: [{ label: "Collection Slug", text: params.slug }], + } + ); + } + + /** Fetch a list of model collections. */ + list(key: IntegrationTaskKey): ReplicateReturnType> { + return this.runTask( + key, + (client) => { + return client.collections.list(); + }, + { + name: "List Collections", + } + ); + } +} diff --git a/integrations/replicate/src/deployments.ts b/integrations/replicate/src/deployments.ts new file mode 100644 index 000000000..c5c1508d1 --- /dev/null +++ b/integrations/replicate/src/deployments.ts @@ -0,0 +1,76 @@ +import { IntegrationTaskKey } from "@trigger.dev/sdk"; +import ReplicateClient, { Prediction } from "replicate"; + +import { ReplicateRunTask } from "./index"; +import { callbackProperties, createDeploymentProperties } from "./utils"; +import { CallbackTimeout, ReplicateReturnType } from "./types"; + +export class Deployments { + constructor(private runTask: ReplicateRunTask) {} + + get predictions() { + return new Predictions(this.runTask); + } +} + +class Predictions { + constructor(private runTask: ReplicateRunTask) {} + + /** Create a new prediction with a deployment. */ + create( + key: IntegrationTaskKey, + params: { + deployment_owner: string; + deployment_name: string; + } & Parameters[2] + ): ReplicateReturnType { + return this.runTask( + key, + (client) => { + const { deployment_owner, deployment_name, ...options } = params; + + return client.deployments.predictions.create(deployment_owner, deployment_name, options); + }, + { + name: "Create Prediction With Deployment", + params, + properties: createDeploymentProperties(params), + } + ); + } + + /** Create a new prediction with a deployment and await the result. */ + createAndAwait( + key: IntegrationTaskKey, + params: { + deployment_owner: string; + deployment_name: string; + } & Omit< + Parameters[2], + "webhook" | "webhook_events_filter" + >, + options: CallbackTimeout = { timeoutInSeconds: 3600 } + ): ReplicateReturnType { + return this.runTask( + key, + (client, task) => { + const { deployment_owner, deployment_name, ...options } = params; + + return client.deployments.predictions.create(deployment_owner, deployment_name, { + ...options, + webhook: task.callbackUrl ?? "", + webhook_events_filter: ["completed"], + }); + }, + { + name: "Create And Await Prediction With Deployment", + params, + properties: [...createDeploymentProperties(params), ...callbackProperties(options)], + callback: { + enabled: true, + timeoutInSeconds: options.timeoutInSeconds, + }, + } + ); + } +} diff --git a/integrations/replicate/src/index.ts b/integrations/replicate/src/index.ts new file mode 100644 index 000000000..0093be164 --- /dev/null +++ b/integrations/replicate/src/index.ts @@ -0,0 +1,280 @@ +import { + TriggerIntegration, + RunTaskOptions, + IO, + IOTask, + IntegrationTaskKey, + RunTaskErrorCallback, + Json, + retry, + ConnectionAuth, +} from "@trigger.dev/sdk"; +import ReplicateClient, { Page, Prediction } from "replicate"; + +import { Predictions } from "./predictions"; +import { Models } from "./models"; +import { Trainings } from "./trainings"; +import { Collections } from "./collections"; +import { ReplicateReturnType } from "./types"; +import { Deployments } from "./deployments"; + +export type ReplicateIntegrationOptions = { + id: string; + apiKey: string; +}; + +export type ReplicateRunTask = InstanceType["runTask"]; + +export class Replicate implements TriggerIntegration { + private _options: ReplicateIntegrationOptions; + private _client?: any; + private _io?: IO; + private _connectionKey?: string; + + constructor(private options: ReplicateIntegrationOptions) { + if (Object.keys(options).includes("apiKey") && !options.apiKey) { + throw `Can't create Replicate integration (${options.id}) as apiKey was undefined`; + } + + this._options = options; + } + + get authSource() { + return "LOCAL" as const; + } + + get id() { + return this.options.id; + } + + get metadata() { + return { id: "replicate", name: "Replicate" }; + } + + cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { + const replicate = new Replicate(this._options); + replicate._io = io; + replicate._connectionKey = connectionKey; + replicate._client = this.createClient(auth); + return replicate; + } + + createClient(auth?: ConnectionAuth) { + return new ReplicateClient({ + auth: this._options.apiKey, + }); + } + + runTask | void>( + key: IntegrationTaskKey, + callback: (client: ReplicateClient, task: IOTask, io: IO) => Promise, + options?: RunTaskOptions, + errorCallback?: RunTaskErrorCallback + ): Promise { + if (!this._io) throw new Error("No IO"); + if (!this._connectionKey) throw new Error("No connection key"); + + return this._io.runTask( + key, + (task, io) => { + if (!this._client) throw new Error("No client"); + return callback(this._client, task, io); + }, + { + icon: "replicate", + retry: retry.standardBackoff, + ...(options ?? {}), + connectionKey: this._connectionKey, + }, + errorCallback ?? onError + ); + } + + get collections() { + return new Collections(this.runTask.bind(this)); + } + + get deployments() { + return new Deployments(this.runTask.bind(this)); + } + + get models() { + return new Models(this.runTask.bind(this)); + } + + get predictions() { + return new Predictions(this.runTask.bind(this)); + } + + get trainings() { + return new Trainings(this.runTask.bind(this)); + } + + /** Paginate through a list of results. */ + async *paginate( + task: (key: string) => Promise>, + key: IntegrationTaskKey, + counter: number = 0 + ): AsyncGenerator { + const boundTask = task.bind(this as any); + + const page = await boundTask(`${key}-${counter}`); + yield page.results; + + if (page.next) { + const nextStep = counter++; + + const nextPage = () => { + return this.request>(`${key}-${nextStep}`, { + route: page.next!, + options: { method: "GET" }, + }); + }; + + yield* this.paginate(nextPage, key, nextStep); + } + } + + /** Auto-paginate and return all results. */ + async getAll( + task: (key: string) => Promise>, + key: IntegrationTaskKey + ): ReplicateReturnType { + const allResults: T[] = []; + + for await (const results of this.paginate(task, key)) { + allResults.push(...results); + } + + return allResults; + } + + /** Make a request to the Replicate API. */ + request( + key: IntegrationTaskKey, + params: { + route: string | URL; + options: Parameters[1]; + } + ): ReplicateReturnType { + return this.runTask( + key, + async (client) => { + const response = await client.request(params.route, params.options); + + return response.json(); + }, + { + name: "Send Request", + params, + properties: [ + { label: "Route", text: params.route.toString() }, + ...(params.options.method ? [{ label: "Method", text: params.options.method }] : []), + ], + callback: { enabled: true }, + } + ); + } + + /** Run a model and await the result. */ + run( + key: IntegrationTaskKey, + params: { + identifier: Parameters[0]; + } & Omit< + Parameters[1], + "webhook" | "webhook_events_filter" | "wait" | "signal" + > + ): ReplicateReturnType { + const { identifier, ...paramsWithoutIdentifier } = params; + + // see: https://github.com/replicate/replicate-javascript/blob/4b0d9cb0e226fab3d3d31de5b32261485acf5626/index.js#L102 + + const namePattern = /[a-zA-Z0-9]+(?:(?:[._]|__|[-]*)[a-zA-Z0-9]+)*/; + const pattern = new RegExp( + `^(?${namePattern.source})/(?${namePattern.source}):(?[0-9a-fA-F]+)$` + ); + + const match = identifier.match(pattern); + + if (!match || !match.groups) { + throw new Error('Invalid version. It must be in the format "owner/name:version"'); + } + + const { version } = match.groups; + + return this.predictions.createAndAwait(key, { ...paramsWithoutIdentifier, version }); + } + + // TODO: wait(prediction) - needs polling +} + +class ApiError extends Error { + constructor( + message: string, + readonly request: Request, + readonly response: Response + ) { + super(message); + this.name = "ApiError"; + } +} + +function isReplicateApiError(error: unknown): error is ApiError { + if (typeof error !== "object" || error === null) { + return false; + } + + const apiError = error as ApiError; + + return ( + apiError.name === "ApiError" && + apiError.request instanceof Request && + apiError.response instanceof Response + ); +} + +function shouldRetry(method: string, status: number) { + return status === 429 || (method === "GET" && status >= 500); +} + +export function onError(error: unknown): ReturnType { + if (!isReplicateApiError(error)) { + return; + } + + if (!shouldRetry(error.request.method, error.response.status)) { + return { + skipRetrying: true, + }; + } + + // see: https://github.com/replicate/replicate-javascript/blob/4b0d9cb0e226fab3d3d31de5b32261485acf5626/lib/util.js#L43 + + const retryAfter = error.response.headers.get("retry-after"); + + if (retryAfter) { + const resetDate = new Date(retryAfter); + + if (!Number.isNaN(resetDate.getTime())) { + return { + retryAt: resetDate, + error, + }; + } + } + + const rateLimitRemaining = error.response.headers.get("ratelimit-remaining"); + const rateLimitReset = error.response.headers.get("ratelimit-reset"); + + if (rateLimitRemaining === "0" && rateLimitReset) { + const resetDate = new Date(Number(rateLimitReset) * 1000); + + if (!Number.isNaN(resetDate.getTime())) { + return { + retryAt: resetDate, + error, + }; + } + } +} diff --git a/integrations/replicate/src/models.ts b/integrations/replicate/src/models.ts new file mode 100644 index 000000000..d4b3a78ac --- /dev/null +++ b/integrations/replicate/src/models.ts @@ -0,0 +1,82 @@ +import { IntegrationTaskKey } from "@trigger.dev/sdk"; +import { Model, ModelVersion } from "replicate"; + +import { ReplicateRunTask } from "./index"; +import { modelProperties } from "./utils"; +import { ReplicateReturnType } from "./types"; + +export class Models { + constructor(private runTask: ReplicateRunTask) {} + + /** Get information about a model. */ + get( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + } + ): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.models.get(params.model_owner, params.model_name); + }, + { + name: "Get Model", + params, + properties: modelProperties(params), + } + ); + } + + get versions() { + return new Versions(this.runTask); + } +} + +class Versions { + constructor(private runTask: ReplicateRunTask) {} + + /** Get a specific model version. */ + get( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + version_id: string; + } + ): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.models.versions.get(params.model_owner, params.model_name, params.version_id); + }, + { + name: "Get Model Version", + params, + properties: modelProperties(params), + } + ); + } + + /** List model versions. */ + list( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + } + ): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.models.versions.list(params.model_owner, params.model_name); + }, + { + name: "List Models", + params, + properties: modelProperties(params), + } + ); + } +} diff --git a/integrations/replicate/src/predictions.ts b/integrations/replicate/src/predictions.ts new file mode 100644 index 000000000..9f6c604fd --- /dev/null +++ b/integrations/replicate/src/predictions.ts @@ -0,0 +1,101 @@ +import { IntegrationTaskKey } from "@trigger.dev/sdk"; +import ReplicateClient, { Page, Prediction } from "replicate"; + +import { ReplicateRunTask } from "./index"; +import { CallbackTimeout, ReplicateReturnType } from "./types"; +import { callbackProperties, createPredictionProperties } from "./utils"; + +export class Predictions { + constructor(private runTask: ReplicateRunTask) {} + + /** Cancel a prediction. */ + cancel(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.predictions.cancel(params.id); + }, + { + name: "Cancel Prediction", + params, + properties: [{ label: "Prediction ID", text: params.id }], + } + ); + } + + /** Create a new prediction. */ + create( + key: IntegrationTaskKey, + params: Parameters[0] + ): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.predictions.create(params); + }, + { + name: "Create Prediction", + params, + properties: createPredictionProperties(params), + } + ); + } + + /** Create a new prediction and await the result. */ + createAndAwait( + key: IntegrationTaskKey, + params: Omit< + Parameters[0], + "webhook" | "webhook_events_filter" + >, + options: CallbackTimeout = { timeoutInSeconds: 3600 } + ): ReplicateReturnType { + return this.runTask( + key, + (client, task) => { + return client.predictions.create({ + ...params, + webhook: task.callbackUrl ?? "", + webhook_events_filter: ["completed"], + }); + }, + { + name: "Create And Await Prediction", + params, + properties: [...createPredictionProperties(params), ...callbackProperties(options)], + callback: { + enabled: true, + timeoutInSeconds: options.timeoutInSeconds, + }, + } + ); + } + + /** Fetch a prediction. */ + get(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.predictions.get(params.id); + }, + { + name: "Get Prediction", + params, + properties: [{ label: "Prediction ID", text: params.id }], + } + ); + } + + /** List all predictions. */ + list(key: IntegrationTaskKey): ReplicateReturnType> { + return this.runTask( + key, + (client) => { + return client.predictions.list(); + }, + { + name: "List Predictions", + } + ); + } +} diff --git a/integrations/replicate/src/trainings.ts b/integrations/replicate/src/trainings.ts new file mode 100644 index 000000000..10a3ae576 --- /dev/null +++ b/integrations/replicate/src/trainings.ts @@ -0,0 +1,113 @@ +import { IntegrationTaskKey } from "@trigger.dev/sdk"; +import ReplicateClient, { Page, Training } from "replicate"; + +import { ReplicateRunTask } from "./index"; +import { CallbackTimeout, ReplicateReturnType } from "./types"; +import { callbackProperties, modelProperties } from "./utils"; + +export class Trainings { + constructor(private runTask: ReplicateRunTask) {} + + /** Cancel a training. */ + cancel(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.trainings.cancel(params.id); + }, + { + name: "Cancel Training", + params, + properties: [{ label: "Training ID", text: params.id }], + } + ); + } + + /** Create a new training. */ + create( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + version_id: string; + } & Parameters[3] + ): ReplicateReturnType { + return this.runTask( + key, + (client) => { + const { model_owner, model_name, version_id, ...options } = params; + + return client.trainings.create(model_owner, model_name, version_id, options); + }, + { + name: "Create Training", + params, + properties: modelProperties(params), + } + ); + } + + /** Create a new training and await the result. */ + createAndAwait( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + version_id: string; + } & Omit< + Parameters[3], + "webhook" | "webhook_events_filter" + >, + options: CallbackTimeout = { timeoutInSeconds: 3600 } + ): ReplicateReturnType { + return this.runTask( + key, + (client, task) => { + const { model_owner, model_name, version_id, ...options } = params; + + return client.trainings.create(model_owner, model_name, version_id, { + ...options, + webhook: task.callbackUrl ?? "", + webhook_events_filter: ["completed"], + }); + }, + { + name: "Create And Await Training", + params, + properties: [...modelProperties(params), ...callbackProperties(options)], + callback: { + enabled: true, + timeoutInSeconds: options.timeoutInSeconds, + }, + } + ); + } + + /** Fetch a training. */ + get(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType { + return this.runTask( + key, + (client) => { + return client.trainings.get(params.id); + }, + { + name: "Get Training", + params, + properties: [{ label: "Training ID", text: params.id }], + } + ); + } + + /** List all trainings. */ + list(key: IntegrationTaskKey): ReplicateReturnType> { + return this.runTask( + key, + async (client) => { + return client.trainings.list(); + }, + { + name: "List Trainings", + } + ); + } +} diff --git a/integrations/replicate/src/types.ts b/integrations/replicate/src/types.ts new file mode 100644 index 000000000..d8fafcd1d --- /dev/null +++ b/integrations/replicate/src/types.ts @@ -0,0 +1,3 @@ +export type CallbackTimeout = { timeoutInSeconds?: number }; + +export type ReplicateReturnType = Promise; diff --git a/integrations/replicate/src/utils.ts b/integrations/replicate/src/utils.ts new file mode 100644 index 000000000..0a510690f --- /dev/null +++ b/integrations/replicate/src/utils.ts @@ -0,0 +1,58 @@ +import { CallbackTimeout } from "./types"; + +export const createPredictionProperties = ( + params: Partial<{ + version: string; + stream: boolean; + }> +) => { + return [ + ...(params.version ? [{ label: "Model Version", text: params.version }] : []), + ...streamingProperty(params), + ]; +}; + +export const createDeploymentProperties = ( + params: Partial<{ + deployment_owner: string; + deployment_name: string; + stream: boolean; + }> +) => { + return [ + ...(params.deployment_owner + ? [{ label: "Deployment Owner", text: params.deployment_owner }] + : []), + ...(params.deployment_name ? [{ label: "Deployment Name", text: params.deployment_name }] : []), + ...streamingProperty(params), + ]; +}; + +export const modelProperties = ( + params: Partial<{ + model_owner: string; + model_name: string; + version_id: string; + destination: string; + }> +) => { + return [ + ...(params.model_owner ? [{ label: "Model Owner", text: params.model_owner }] : []), + ...(params.model_name ? [{ label: "Model Name", text: params.model_name }] : []), + ...(params.version_id ? [{ label: "Model Version", text: params.version_id }] : []), + ...(params.destination ? [{ label: "Destination Model", text: params.destination }] : []), + ]; +}; + +export const streamingProperty = (params: { stream?: boolean }) => { + return [{ label: "Streaming Enabled", text: String(!!params.stream) }]; +}; + +export const callbackProperties = (options: CallbackTimeout) => { + return [ + { + label: "Callback Timeout", + text: options.timeoutInSeconds ? `${options.timeoutInSeconds}s` : "default", + }, + ]; +}; diff --git a/integrations/replicate/tsconfig.json b/integrations/replicate/tsconfig.json new file mode 100644 index 000000000..36ae307e4 --- /dev/null +++ b/integrations/replicate/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@trigger.dev/tsconfig/integration.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], +} diff --git a/integrations/replicate/tsup.config.ts b/integrations/replicate/tsup.config.ts new file mode 100644 index 000000000..483aba1d5 --- /dev/null +++ b/integrations/replicate/tsup.config.ts @@ -0,0 +1,22 @@ +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: [], + external: ["http", "https", "util", "events", "tty", "os", "timers"], + }, +]); diff --git a/integrations/resend/src/index.ts b/integrations/resend/src/index.ts index f93be1964..b483fa48c 100644 --- a/integrations/resend/src/index.ts +++ b/integrations/resend/src/index.ts @@ -100,7 +100,7 @@ export class Resend implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/sendgrid/src/index.ts b/integrations/sendgrid/src/index.ts index 49a1a55f2..d08a39379 100644 --- a/integrations/sendgrid/src/index.ts +++ b/integrations/sendgrid/src/index.ts @@ -70,7 +70,7 @@ export class SendGrid implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/slack/src/index.ts b/integrations/slack/src/index.ts index 9a8571d4c..4934a99d1 100644 --- a/integrations/slack/src/index.ts +++ b/integrations/slack/src/index.ts @@ -92,7 +92,7 @@ export class Slack implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index 62edc1f15..ac9828e7a 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -629,6 +629,16 @@ export const RunTaskOptionsSchema = z.object({ params: z.any(), /** The style of the log entry. */ style: StyleSchema.optional(), + /** Allows you to expose a `task.callbackUrl` to use in your tasks. Enabling this feature will cause the task to return the data sent to the callbackUrl instead of the usual async callback result. */ + callback: z + .object({ + /** Causes the task to wait for and return the data of the first request sent to `task.callbackUrl`. */ + enabled: z.boolean(), + /** Time to wait for the first request to `task.callbackUrl`. Default: One hour. */ + timeoutInSeconds: z.number(), + }) + .partial() + .optional(), /** Allows you to link the Integration connection in the logs. This is handled automatically in integrations. */ connectionKey: z.string().optional(), /** An operation you want to perform on the Trigger.dev platform, current only "fetch" is supported. If you wish to `fetch` use [`io.backgroundFetch()`](https://trigger.dev/docs/sdk/io/backgroundfetch) instead. */ @@ -655,6 +665,12 @@ export type RunTaskBodyInput = z.infer; export const RunTaskBodyOutputSchema = RunTaskBodyInputSchema.extend({ params: DeserializedJsonSchema.optional().nullable(), + callback: z + .object({ + enabled: z.boolean(), + timeoutInSeconds: z.number().default(3600), + }) + .optional(), }); export type RunTaskBodyOutput = z.infer; diff --git a/packages/core/src/schemas/tasks.ts b/packages/core/src/schemas/tasks.ts index fe6d43bbd..559dc4bab 100644 --- a/packages/core/src/schemas/tasks.ts +++ b/packages/core/src/schemas/tasks.ts @@ -31,6 +31,7 @@ export const TaskSchema = z.object({ parentId: z.string().optional().nullable(), style: StyleSchema.optional().nullable(), operation: z.string().optional().nullable(), + callbackUrl: z.string().optional().nullable(), }); export const ServerTaskSchema = TaskSchema.extend({ diff --git a/packages/database/prisma/migrations/20230925174509_add_callback_url/migration.sql b/packages/database/prisma/migrations/20230925174509_add_callback_url/migration.sql new file mode 100644 index 000000000..4808101ef --- /dev/null +++ b/packages/database/prisma/migrations/20230925174509_add_callback_url/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Task" ADD COLUMN "callbackUrl" TEXT; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 192e0d66a..0bcdc1547 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -798,6 +798,7 @@ model Task { redact Json? style Json? operation String? + callbackUrl String? startedAt DateTime? completedAt DateTime? diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts index 58d5a95e9..b4dacfc5e 100644 --- a/packages/trigger-sdk/src/io.ts +++ b/packages/trigger-sdk/src/io.ts @@ -694,28 +694,18 @@ export class IO { throw new Error(task.error ?? task?.output ? JSON.stringify(task.output) : "Task errored"); } - if (task.status === "WAITING") { - this._logger.debug("Task waiting", { - idempotencyKey, - task, - }); - - throw new ResumeWithTaskError(task); - } - - if (task.status === "RUNNING" && typeof task.operation === "string") { - this._logger.debug("Task running operation", { - idempotencyKey, - task, - }); - - throw new ResumeWithTaskError(task); - } - const executeTask = async () => { try { const result = await callback(task, this); + if (task.status === "WAITING" && task.callbackUrl) { + this._logger.debug("Waiting for remote callback", { + idempotencyKey, + task, + }); + return {} as T; + } + const output = SerializableJsonSchema.parse(result) as T; this._logger.debug("Completing using output", { @@ -800,6 +790,28 @@ export class IO { } }; + if (task.status === "WAITING") { + this._logger.debug("Task waiting", { + idempotencyKey, + task, + }); + + if (task.callbackUrl) { + await this._taskStorage.run({ taskId: task.id }, executeTask); + } + + throw new ResumeWithTaskError(task); + } + + if (task.status === "RUNNING" && typeof task.operation === "string") { + this._logger.debug("Task running operation", { + idempotencyKey, + task, + }); + + throw new ResumeWithTaskError(task); + } + return this._taskStorage.run({ taskId: task.id }, executeTask); } diff --git a/references/job-catalog/package.json b/references/job-catalog/package.json index ab55bf836..ef34a56c9 100644 --- a/references/job-catalog/package.json +++ b/references/job-catalog/package.json @@ -25,6 +25,7 @@ "status": "nodemon --watch src/status.ts -r tsconfig-paths/register -r dotenv/config src/status.ts", "byo-auth": "nodemon --watch src/byo-auth.ts -r tsconfig-paths/register -r dotenv/config src/byo-auth.ts", "redacted": "nodemon --watch src/redacted.ts -r tsconfig-paths/register -r dotenv/config src/redacted.ts", + "replicate": "nodemon --watch src/replicate.ts -r tsconfig-paths/register -r dotenv/config src/replicate.ts", "dev:trigger": "trigger-cli dev --port 8080" }, "dependencies": { @@ -44,7 +45,8 @@ "@types/node": "20.4.2", "typescript": "5.1.6", "zod": "3.21.4", - "@trigger.dev/linear": "workspace:*" + "@trigger.dev/linear": "workspace:*", + "@trigger.dev/replicate": "workspace:*" }, "trigger.dev": { "endpointId": "job-catalog" diff --git a/references/job-catalog/src/replicate.ts b/references/job-catalog/src/replicate.ts new file mode 100644 index 000000000..5cd416bf0 --- /dev/null +++ b/references/job-catalog/src/replicate.ts @@ -0,0 +1,146 @@ +import { createExpressServer } from "@trigger.dev/express"; +import { TriggerClient, eventTrigger } from "@trigger.dev/sdk"; +import { Replicate } from "@trigger.dev/replicate"; +import { z } from "zod"; + +export const client = new TriggerClient({ + id: "job-catalog", + apiKey: process.env["TRIGGER_API_KEY"], + apiUrl: process.env["TRIGGER_API_URL"], + verbose: false, + ioLogLocalEnabled: true, +}); + +const replicate = new Replicate({ + id: "replicate", + apiKey: process.env["REPLICATE_API_KEY"]!, +}); + +client.defineJob({ + id: "replicate-forge-image", + name: "Replicate - Forge Image", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.bad.forgery", + schema: z.object({ + imageUrl: z + .string() + .url() + .default("https://trigger.dev/blog/supabase-integration/postgres-meme.png"), + }), + }), + run: async (payload, io, ctx) => { + const blipVersion = "2e1dddc8621f72155f24cf2e0adbde548458d3cab9f00c0139eea840d0ac4746"; + const sdVersion = "ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4"; + + const blipPrediction = await io.replicate.run("caption-image", { + identifier: `salesforce/blip:${blipVersion}`, + input: { + image: payload.imageUrl, + }, + }); + + if (typeof blipPrediction.output !== "string") { + throw new Error(`Expected string output, got ${typeof blipPrediction.output}`); + } + + const caption = blipPrediction.output.replace("Caption: ", ""); + + const sdPrediction = await io.replicate.predictions.createAndAwait("draw-image", { + version: sdVersion, + input: { + prompt: caption, + }, + }); + + return { + caption, + output: sdPrediction.output, + }; + }, +}); + +client.defineJob({ + id: "replicate-python-answers", + name: "Replicate - Python Answers", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.serious.monty", + schema: z.object({ + prompt: z.string().default("why are apples not oranges?"), + }), + }), + run: async (payload, io, ctx) => { + const prediction = await io.replicate.run("await-prediction", { + identifier: + "meta/llama-2-13b-chat:f4e2de70d66816a838a89eeeb621910adffb0dd0baba3976c96980970978018d", + input: { + prompt: payload.prompt, + system_prompt: "Answer like John Cleese. Don't be funny.", + max_new_tokens: 200, + }, + }); + + return Array.isArray(prediction.output) ? prediction.output.join("") : prediction.output; + }, +}); + +client.defineJob({ + id: "replicate-cinematic-prompt", + name: "Replicate - Cinematic Prompt", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.cinematic", + schema: z.object({ + prompt: z.string().default("rick astley riding a harley through post-apocalyptic miami"), + version: z + .string() + .default("af1a68a271597604546c09c64aabcd7782c114a63539a4a8d14d1eeda5630c33"), + }), + }), + run: async (payload, io, ctx) => { + const prediction = await io.replicate.predictions.createAndAwait("await-prediction", { + version: payload.version, + input: { + prompt: `${payload.prompt}, cinematic, 70mm, anamorphic, bokeh`, + width: 1280, + height: 720, + }, + }); + return prediction.output; + }, +}); + +client.defineJob({ + id: "replicate-pagination", + name: "Replicate - Pagination", + version: "0.1.0", + integrations: { + replicate, + }, + trigger: eventTrigger({ + name: "replicate.paginate", + }), + run: async (payload, io, ctx) => { + // getAll - returns an array of all results (uses paginate internally) + const all = await io.replicate.getAll(io.replicate.predictions.list, "get-all"); + + // paginate - returns an async generator, useful to process one page at a time + for await (const predictions of io.replicate.paginate( + io.replicate.predictions.list, + "paginate-all" + )) { + await io.logger.info("stats", { + total: predictions.length, + versions: predictions.map((p) => p.version), + }); + } + + return { count: all.length }; + }, +}); + +createExpressServer(client); diff --git a/references/job-catalog/tsconfig.json b/references/job-catalog/tsconfig.json index 6ec3167e6..80823d1a8 100644 --- a/references/job-catalog/tsconfig.json +++ b/references/job-catalog/tsconfig.json @@ -97,6 +97,12 @@ ], "@trigger.dev/linear/*": [ "../../integrations/linear/src/*" + ], + "@trigger.dev/replicate": [ + "../../integrations/replicate/src/index" + ], + "@trigger.dev/replicate/*": [ + "../../integrations/replicate/src/*" ] } } From 5e651d84afe28a64f1130c44534638ee89c4a3a8 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 4 Oct 2023 11:00:55 +0100 Subject: [PATCH 04/79] Update pnpm lock file --- pnpm-lock.yaml | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb63ba5d2..2d8d9b9ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -479,6 +479,29 @@ importers: rimraf: 3.0.2 tsup: 6.6.3 + integrations/replicate: + specifiers: + '@trigger.dev/integration-kit': workspace:^2.1.0 + '@trigger.dev/sdk': workspace:^2.1.0 + '@trigger.dev/tsconfig': workspace:* + '@types/node': 16.x + replicate: ^0.18.1 + rimraf: ^3.0.2 + tsup: 7.1.x + typescript: 4.9.4 + zod: 3.21.4 + dependencies: + '@trigger.dev/integration-kit': link:../../packages/integration-kit + '@trigger.dev/sdk': link:../../packages/trigger-sdk + replicate: 0.18.1 + zod: 3.21.4 + devDependencies: + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + rimraf: 3.0.2 + tsup: 7.1.0_typescript@4.9.4 + typescript: 4.9.4 + integrations/resend: specifiers: '@trigger.dev/integration-kit': workspace:^2.1.7 @@ -1094,6 +1117,7 @@ importers: '@trigger.dev/linear': workspace:* '@trigger.dev/openai': workspace:* '@trigger.dev/plain': workspace:* + '@trigger.dev/replicate': workspace:* '@trigger.dev/resend': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/sendgrid': workspace:* @@ -1118,6 +1142,7 @@ importers: '@trigger.dev/linear': link:../../integrations/linear '@trigger.dev/openai': link:../../integrations/openai '@trigger.dev/plain': link:../../integrations/plain + '@trigger.dev/replicate': link:../../integrations/replicate '@trigger.dev/resend': link:../../integrations/resend '@trigger.dev/sdk': link:../../packages/trigger-sdk '@trigger.dev/sendgrid': link:../../integrations/sendgrid @@ -13411,6 +13436,7 @@ packages: typescript: 5.1.6 transitivePeerDependencies: - supports-color + dev: false /@typescript-eslint/parser/5.59.6_mhvizudgavq6jzvqzwcqcv4nnm: resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} @@ -13430,7 +13456,6 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - supports-color - dev: true /@typescript-eslint/scope-manager/5.59.6: resolution: {integrity: sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==} @@ -13543,7 +13568,6 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - supports-color - dev: true /@typescript-eslint/typescript-estree/5.59.6_typescript@5.0.4: resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} @@ -13585,6 +13609,7 @@ packages: typescript: 5.1.6 transitivePeerDependencies: - supports-color + dev: false /@typescript-eslint/utils/5.59.6_eslint@8.45.0: resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} @@ -17833,7 +17858,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_ko3fmmbeyij36muomfgt2u76xu + '@typescript-eslint/parser': 5.59.6_mhvizudgavq6jzvqzwcqcv4nnm debug: 3.2.7 eslint: 8.45.0 eslint-import-resolver-node: 0.3.7 @@ -17980,7 +18005,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_ko3fmmbeyij36muomfgt2u76xu + '@typescript-eslint/parser': 5.59.6_mhvizudgavq6jzvqzwcqcv4nnm array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -26474,6 +26499,11 @@ packages: engines: {node: '>=0.10'} dev: false + /replicate/0.18.1: + resolution: {integrity: sha512-JFK5qWL7AAajsIjtkW/nTaoX+yKp8zkaANe+pQpBh9RjH5vIy6/tn/sVNlRF1z5bCJLupshJBHFgnHRdWjbKXg==} + engines: {git: '>=2.11.0', node: '>=18.0.0', npm: '>=7.19.0', yarn: '>=1.7.0'} + dev: false + /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -28835,7 +28865,6 @@ packages: dependencies: tslib: 1.14.1 typescript: 4.9.5 - dev: true /tsutils/3.21.0_typescript@5.0.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -28855,6 +28884,7 @@ packages: dependencies: tslib: 1.14.1 typescript: 5.1.6 + dev: false /tsx/3.12.2: resolution: {integrity: sha512-ykAEkoBg30RXxeOMVeZwar+JH632dZn9EUJVyJwhfag62k6UO/dIyJEV58YuLF6e5BTdV/qmbQrpkWqjq9cUnQ==} From b9ed7e2ced67d8a7126ffbb14f13b2a0232ea93c Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 4 Oct 2023 13:21:38 +0100 Subject: [PATCH 05/79] Allow blank issues --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4e9aa8c95..308437901 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: Ask a Question url: https://trigger.dev/discord From 28b05a82d80b23359ee42d88b15cec37e7061ef4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:00:20 +0100 Subject: [PATCH 06/79] chore: Update version for release (#538) Co-authored-by: github-actions[bot] --- .changeset/fair-plums-grin.md | 13 ------------- .changeset/flat-olives-grab.md | 10 ---------- .changeset/sour-jars-punch.md | 5 ----- .changeset/warm-carrots-float.md | 6 ------ integrations/airtable/CHANGELOG.md | 11 +++++++++++ integrations/airtable/package.json | 6 +++--- integrations/github/CHANGELOG.md | 11 +++++++++++ integrations/github/package.json | 6 +++--- integrations/linear/CHANGELOG.md | 12 ++++++++++++ integrations/linear/package.json | 6 +++--- integrations/openai/CHANGELOG.md | 10 ++++++++++ integrations/openai/package.json | 6 +++--- integrations/plain/CHANGELOG.md | 10 ++++++++++ integrations/plain/package.json | 6 +++--- integrations/replicate/CHANGELOG.md | 12 ++++++++++++ integrations/replicate/package.json | 6 +++--- integrations/resend/CHANGELOG.md | 11 +++++++++++ integrations/resend/package.json | 6 +++--- integrations/sendgrid/CHANGELOG.md | 11 +++++++++++ integrations/sendgrid/package.json | 6 +++--- integrations/slack/CHANGELOG.md | 10 ++++++++++ integrations/slack/package.json | 4 ++-- integrations/stripe/CHANGELOG.md | 10 ++++++++++ integrations/stripe/package.json | 6 +++--- integrations/supabase/CHANGELOG.md | 10 ++++++++++ integrations/supabase/package.json | 6 +++--- integrations/typeform/CHANGELOG.md | 10 ++++++++++ integrations/typeform/package.json | 6 +++--- packages/astro/CHANGELOG.md | 10 ++++++++++ packages/astro/package.json | 4 ++-- packages/cli/CHANGELOG.md | 2 ++ packages/cli/package.json | 2 +- packages/core/CHANGELOG.md | 8 ++++++++ packages/core/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 2 ++ packages/eslint-plugin/package.json | 2 +- packages/express/CHANGELOG.md | 10 ++++++++++ packages/express/package.json | 6 +++--- packages/integration-kit/CHANGELOG.md | 2 ++ packages/integration-kit/package.json | 2 +- packages/nextjs/CHANGELOG.md | 10 ++++++++++ packages/nextjs/package.json | 4 ++-- packages/react/CHANGELOG.md | 9 +++++++++ packages/react/package.json | 4 ++-- packages/remix/CHANGELOG.md | 10 ++++++++++ packages/remix/package.json | 4 ++-- packages/testing/CHANGELOG.md | 10 ++++++++++ packages/testing/package.json | 2 +- packages/trigger-sdk/CHANGELOG.md | 12 ++++++++++++ packages/trigger-sdk/package.json | 4 ++-- 50 files changed, 266 insertions(+), 87 deletions(-) delete mode 100644 .changeset/fair-plums-grin.md delete mode 100644 .changeset/flat-olives-grab.md delete mode 100644 .changeset/sour-jars-punch.md delete mode 100644 .changeset/warm-carrots-float.md create mode 100644 integrations/replicate/CHANGELOG.md diff --git a/.changeset/fair-plums-grin.md b/.changeset/fair-plums-grin.md deleted file mode 100644 index 31579ad8c..000000000 --- a/.changeset/fair-plums-grin.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@trigger.dev/replicate": patch -"@trigger.dev/airtable": patch -"@trigger.dev/sendgrid": patch -"@trigger.dev/sdk": patch -"@trigger.dev/github": patch -"@trigger.dev/linear": patch -"@trigger.dev/resend": patch -"@trigger.dev/slack": patch -"@trigger.dev/core": patch ---- - -First release of `@trigger.dev/replicate` integration with remote callback support. diff --git a/.changeset/flat-olives-grab.md b/.changeset/flat-olives-grab.md deleted file mode 100644 index 4e5fe2a30..000000000 --- a/.changeset/flat-olives-grab.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@trigger.dev/sdk": patch -"@trigger.dev/express": patch -"@trigger.dev/nextjs": patch -"@trigger.dev/astro": patch -"@trigger.dev/remix": patch -"@trigger.dev/core": patch ---- - -Send client version back to the server via headers diff --git a/.changeset/sour-jars-punch.md b/.changeset/sour-jars-punch.md deleted file mode 100644 index 28abcd24d..000000000 --- a/.changeset/sour-jars-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/linear": patch ---- - -Fix `getAll` helper and search function params diff --git a/.changeset/warm-carrots-float.md b/.changeset/warm-carrots-float.md deleted file mode 100644 index cf51b6357..000000000 --- a/.changeset/warm-carrots-float.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@trigger.dev/core": patch -"@trigger.dev/sdk": patch ---- - -Better performance when resuming a run, especially one with a large amount of tasks diff --git a/integrations/airtable/CHANGELOG.md b/integrations/airtable/CHANGELOG.md index 9905b5f80..1715a4efa 100644 --- a/integrations/airtable/CHANGELOG.md +++ b/integrations/airtable/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/airtable +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/airtable/package.json b/integrations/airtable/package.json index 87d688f67..aa237de00 100644 --- a/integrations/airtable/package.json +++ b/integrations/airtable/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/airtable", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev integration for airtable", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.7", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "airtable": "^0.12.1", "zod": "3.21.4" }, diff --git a/integrations/github/CHANGELOG.md b/integrations/github/CHANGELOG.md index 0228577ca..976085c29 100644 --- a/integrations/github/CHANGELOG.md +++ b/integrations/github/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/github +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/github/package.json b/integrations/github/package.json index 1354a1277..4995951f3 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/github", - "version": "2.1.7", + "version": "2.1.8", "description": "The official GitHub integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,8 +29,8 @@ "@octokit/request": "^6.2.5", "@octokit/request-error": "^4.0.1", "@octokit/webhooks": "^10.4.0", - "@trigger.dev/sdk": "workspace:^2.1.7", - "@trigger.dev/integration-kit": "workspace:^2.1.7", + "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.8", "octokit": "^2.0.14", "zod": "3.21.4" }, diff --git a/integrations/linear/CHANGELOG.md b/integrations/linear/CHANGELOG.md index 9bccb718a..18734aba1 100644 --- a/integrations/linear/CHANGELOG.md +++ b/integrations/linear/CHANGELOG.md @@ -1,5 +1,17 @@ # @trigger.dev/linear +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- 81e886a1: Fix `getAll` helper and search function params +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/linear/package.json b/integrations/linear/package.json index eabcab053..72ffe7b11 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/linear", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev integration for @linear/sdk", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@linear/sdk": "^8.0.0", - "@trigger.dev/integration-kit": "workspace:^2.1.7", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "zod": "3.21.4" }, "engines": { diff --git a/integrations/openai/CHANGELOG.md b/integrations/openai/CHANGELOG.md index b346fa3ae..63df75549 100644 --- a/integrations/openai/CHANGELOG.md +++ b/integrations/openai/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/slack +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/openai/package.json b/integrations/openai/package.json index ce2ccbbb8..e13607fe3 100644 --- a/integrations/openai/package.json +++ b/integrations/openai/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/openai", - "version": "2.1.7", + "version": "2.1.8", "description": "The official OpenAI integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,8 +25,8 @@ }, "dependencies": { "openai": "^4.2.0", - "@trigger.dev/sdk": "workspace:^2.1.7", - "@trigger.dev/integration-kit": "workspace:^2.1.7" + "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.8" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/plain/CHANGELOG.md b/integrations/plain/CHANGELOG.md index 6baddddcb..94d24d06d 100644 --- a/integrations/plain/CHANGELOG.md +++ b/integrations/plain/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/plain +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/plain/package.json b/integrations/plain/package.json index 8a2e65509..b0c8b0abe 100644 --- a/integrations/plain/package.json +++ b/integrations/plain/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/plain", - "version": "2.1.7", + "version": "2.1.8", "description": "The official Plain.com integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,8 +24,8 @@ "build:tsup": "tsup" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.7", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "@team-plain/typescript-sdk": "^2.7.0" }, "engines": { diff --git a/integrations/replicate/CHANGELOG.md b/integrations/replicate/CHANGELOG.md new file mode 100644 index 000000000..9edd079df --- /dev/null +++ b/integrations/replicate/CHANGELOG.md @@ -0,0 +1,12 @@ +# @trigger.dev/replicate + +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 diff --git a/integrations/replicate/package.json b/integrations/replicate/package.json index 9478a0a36..0995377ec 100644 --- a/integrations/replicate/package.json +++ b/integrations/replicate/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/replicate", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev integration for replicate", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.0", - "@trigger.dev/sdk": "workspace:^2.1.0", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "replicate": "^0.18.1", "zod": "3.21.4" }, diff --git a/integrations/resend/CHANGELOG.md b/integrations/resend/CHANGELOG.md index a7e773f79..1ed135558 100644 --- a/integrations/resend/CHANGELOG.md +++ b/integrations/resend/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/resend +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/resend/package.json b/integrations/resend/package.json index 63af38a55..3caae35d4 100644 --- a/integrations/resend/package.json +++ b/integrations/resend/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/resend", - "version": "2.1.7", + "version": "2.1.8", "description": "The official Resend.com integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,8 +24,8 @@ "build:tsup": "tsup" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.7", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "resend": "^1.0.0" }, "engines": { diff --git a/integrations/sendgrid/CHANGELOG.md b/integrations/sendgrid/CHANGELOG.md index d949d7431..dc96cf7c9 100644 --- a/integrations/sendgrid/CHANGELOG.md +++ b/integrations/sendgrid/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/sendgrid +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/sendgrid/package.json b/integrations/sendgrid/package.json index e5fbca810..82e0eb65c 100644 --- a/integrations/sendgrid/package.json +++ b/integrations/sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sendgrid", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev integration for @sendgrid/mail", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@sendgrid/mail": "^7.7.0", - "@trigger.dev/sdk": "workspace:^2.1.7", - "@trigger.dev/integration-kit": "workspace:^2.1.7" + "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.8" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/slack/CHANGELOG.md b/integrations/slack/CHANGELOG.md index c8f1de0ec..4637acd33 100644 --- a/integrations/slack/CHANGELOG.md +++ b/integrations/slack/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/slack +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/slack/package.json b/integrations/slack/package.json index b14d9295b..3efe16cb0 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/slack", - "version": "2.1.7", + "version": "2.1.8", "description": "The official Slack integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@slack/web-api": "^6.8.1", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/sdk": "workspace:^2.1.8", "zod": "3.21.4" }, "engines": { diff --git a/integrations/stripe/CHANGELOG.md b/integrations/stripe/CHANGELOG.md index 65cea354d..49353899f 100644 --- a/integrations/stripe/CHANGELOG.md +++ b/integrations/stripe/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/stripe +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/stripe/package.json b/integrations/stripe/package.json index a66585039..e27c5a70d 100644 --- a/integrations/stripe/package.json +++ b/integrations/stripe/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/stripe", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev integration for stripe", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.7", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "stripe": "^12.14.0", "zod": "3.21.4" }, diff --git a/integrations/supabase/CHANGELOG.md b/integrations/supabase/CHANGELOG.md index 3c35fd456..ee00ef81e 100644 --- a/integrations/supabase/CHANGELOG.md +++ b/integrations/supabase/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/supabase +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/supabase/package.json b/integrations/supabase/package.json index 4600c57ad..6b54fc361 100644 --- a/integrations/supabase/package.json +++ b/integrations/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/supabase", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev integration for @supabase/supabase-js", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@supabase/supabase-js": "^2.26.0", - "@trigger.dev/integration-kit": "workspace:^2.1.7", - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "supabase-management-js": "^0.1.4", "zod": "3.21.4" }, diff --git a/integrations/typeform/CHANGELOG.md b/integrations/typeform/CHANGELOG.md index d703453ac..c18dfca45 100644 --- a/integrations/typeform/CHANGELOG.md +++ b/integrations/typeform/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/typeform +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/integrations/typeform/package.json b/integrations/typeform/package.json index 8c5a8d306..b0f161675 100644 --- a/integrations/typeform/package.json +++ b/integrations/typeform/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/typeform", - "version": "2.1.7", + "version": "2.1.8", "description": "The official Typeform integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ }, "dependencies": { "@typeform/api-client": "^1.8.0", - "@trigger.dev/sdk": "workspace:^2.1.7", - "@trigger.dev/integration-kit": "workspace:^2.1.7", + "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.8", "zod": "3.21.4" }, "engines": { diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 5f9ca09e8..40b0cc74e 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/astro +## 2.1.8 + +### Patch Changes + +- ab9e4a98: Send client version back to the server via headers +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 328000d23..6dbe60898 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,7 +1,7 @@ { "name": "@trigger.dev/astro", "description": "An Astro-native integration for Trigger.dev background jobs platform", - "version": "2.1.7", + "version": "2.1.8", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ @@ -20,7 +20,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.7" + "@trigger.dev/sdk": "workspace:^2.1.8" }, "devDependencies": { "astro": "^3.0.12", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index f93dc7de8..4a22e3176 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,7 @@ # create-trigger +## 2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 0b7749bd5..b124affdc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/cli", - "version": "2.1.7", + "version": "2.1.8", "description": "The Trigger.dev CLI", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index cb82d87da..92ba3c957 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,13 @@ # internal-platform +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- ab9e4a98: Send client version back to the server via headers +- ab9e4a98: Better performance when resuming a run, especially one with a large amount of tasks + ## 2.1.7 ## 2.1.6 diff --git a/packages/core/package.json b/packages/core/package.json index 868806a71..2daab5ce0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/core", - "version": "2.1.7", + "version": "2.1.8", "description": "Core code used across the Trigger.dev SDK and platform", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 9728ec0a9..e23577542 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,7 @@ # @trigger.dev/eslint-plugin +## 2.1.8 + ## 2.1.7 ## 2.1.6 diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 0d851c58b..654507bbf 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/eslint-plugin", - "version": "2.1.7", + "version": "2.1.8", "description": "ESLint plugin with trigger.dev best practices", "keywords": [ "eslint", diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md index 2aa5ed0fa..b392b4bdc 100644 --- a/packages/express/CHANGELOG.md +++ b/packages/express/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/express +## 2.1.8 + +### Patch Changes + +- ab9e4a98: Send client version back to the server via headers +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/express/package.json b/packages/express/package.json index 3377d07a2..220a02416 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/express", - "version": "2.1.7", + "version": "2.1.8", "description": "Official Express adapter for Trigger.dev", "license": "MIT", "main": "./dist/index.js", @@ -19,7 +19,7 @@ "./package.json": "./package.json" }, "devDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/sdk": "workspace:^2.1.8", "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/express": "^4.17.13", @@ -33,7 +33,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.7" + "@trigger.dev/sdk": "workspace:^2.1.8" }, "dependencies": { "@remix-run/web-fetch": "^4.3.5", diff --git a/packages/integration-kit/CHANGELOG.md b/packages/integration-kit/CHANGELOG.md index c637cf5db..814482804 100644 --- a/packages/integration-kit/CHANGELOG.md +++ b/packages/integration-kit/CHANGELOG.md @@ -1,5 +1,7 @@ # @trigger.dev/integration-kit +## 2.1.8 + ## 2.1.7 ## 2.1.6 diff --git a/packages/integration-kit/package.json b/packages/integration-kit/package.json index 049b50b53..017f60dab 100644 --- a/packages/integration-kit/package.json +++ b/packages/integration-kit/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/integration-kit", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev Integration Kit has helpers to make creating integrations easier", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/nextjs/CHANGELOG.md b/packages/nextjs/CHANGELOG.md index 61d1c6f63..4ff5c4834 100644 --- a/packages/nextjs/CHANGELOG.md +++ b/packages/nextjs/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/nextjs +## 2.1.8 + +### Patch Changes + +- ab9e4a98: Send client version back to the server via headers +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index f3adf42ad..8b1c25a18 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/nextjs", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev Next.js integration", "license": "MIT", "main": "./dist/index.js", @@ -34,7 +34,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/sdk": "workspace:^2.1.8", "next": ">=12.0.0 <14.0.0" }, "dependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index e96c00e73..33cb0628a 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/react +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/core@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/react/package.json b/packages/react/package.json index d45c9dce5..ba77c43e4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/react", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev React SDK", "license": "MIT", "types": "dist/index.d.ts", @@ -27,7 +27,7 @@ }, "dependencies": { "@tanstack/react-query": "5.0.0-beta.2", - "@trigger.dev/core": "workspace:^2.1.7", + "@trigger.dev/core": "workspace:^2.1.8", "debug": "^4.3.4", "zod": "3.21.4" }, diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index 4d7e08f25..f0408bdb2 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/remix +## 2.1.8 + +### Patch Changes + +- ab9e4a98: Send client version back to the server via headers +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/remix/package.json b/packages/remix/package.json index 263d148b8..580e4ad34 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/remix", - "version": "2.1.7", + "version": "2.1.8", "description": "Trigger.dev Remix integration", "license": "MIT", "main": "./dist/index.js", @@ -34,7 +34,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.7", + "@trigger.dev/sdk": "workspace:^2.1.8", "@remix-run/server-runtime": ">1.19.0" }, "dependencies": { diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index df5e27f1c..e51431fea 100644 --- a/packages/testing/CHANGELOG.md +++ b/packages/testing/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/testing +## 2.1.8 + +### Patch Changes + +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/core@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/testing/package.json b/packages/testing/package.json index dcb60e746..724ca43e5 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,7 +1,7 @@ { "name": "@trigger.dev/testing", "description": "A collection of useful tools to write tests for Trigger.dev.", - "version": "2.1.7", + "version": "2.1.8", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-sdk/CHANGELOG.md b/packages/trigger-sdk/CHANGELOG.md index e1249e8ac..70e4785a1 100644 --- a/packages/trigger-sdk/CHANGELOG.md +++ b/packages/trigger-sdk/CHANGELOG.md @@ -1,5 +1,17 @@ # @trigger.dev/sdk +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- ab9e4a98: Send client version back to the server via headers +- ab9e4a98: Better performance when resuming a run, especially one with a large amount of tasks +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/core@2.1.8 + ## 2.1.7 ### Patch Changes diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index b452ae686..6f5f52d76 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sdk", - "version": "2.1.7", + "version": "2.1.8", "description": "trigger.dev Node.JS SDK", "license": "MIT", "main": "./dist/index.js", @@ -25,7 +25,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/core": "workspace:^2.1.7", + "@trigger.dev/core": "workspace:^2.1.8", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", From 90f52de1478aee059d914ffdea95463d5732d999 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 4 Oct 2023 14:01:48 +0100 Subject: [PATCH 07/79] Fix pnpm lock file --- pnpm-lock.yaml | 62 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d8d9b9ad..f9f9b73b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,8 +366,8 @@ importers: integrations/airtable: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x airtable: ^0.12.1 @@ -394,8 +394,8 @@ importers: '@octokit/types': ^9.2.3 '@octokit/webhooks': ^10.4.0 '@octokit/webhooks-types': ^6.10.0 - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' octokit: ^2.0.14 @@ -421,8 +421,8 @@ importers: integrations/linear: specifiers: '@linear/sdk': ^8.0.0 - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x rimraf: ^3.0.2 @@ -443,8 +443,8 @@ importers: integrations/openai: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' openai: ^4.2.0 @@ -463,8 +463,8 @@ importers: integrations/plain: specifiers: '@team-plain/typescript-sdk': ^2.7.0 - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' rimraf: ^3.0.2 @@ -481,8 +481,8 @@ importers: integrations/replicate: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.0 - '@trigger.dev/sdk': workspace:^2.1.0 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x replicate: ^0.18.1 @@ -504,8 +504,8 @@ importers: integrations/resend: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' resend: ^1.0.0 @@ -524,8 +524,8 @@ importers: integrations/sendgrid: specifiers: '@sendgrid/mail': ^7.7.0 - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x rimraf: ^3.0.2 @@ -545,7 +545,7 @@ importers: integrations/slack: specifiers: '@slack/web-api': ^6.8.1 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' rimraf: ^3.0.2 @@ -563,8 +563,8 @@ importers: integrations/stripe: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@types/node': 16.x rimraf: ^3.0.2 stripe: ^12.14.0 @@ -587,8 +587,8 @@ importers: integrations/supabase: specifiers: '@supabase/supabase-js': ^2.26.0 - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/node': 18.x rimraf: ^3.0.2 @@ -611,8 +611,8 @@ importers: integrations/typeform: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.7 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/integration-kit': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.8 '@typeform/api-client': ^1.8.0 '@types/node': 16.x rimraf: ^3.0.2 @@ -839,7 +839,7 @@ importers: packages/express: specifiers: '@remix-run/web-fetch': ^4.3.5 - '@trigger.dev/sdk': workspace:^2.1.7 + '@trigger.dev/sdk': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/express': ^4.17.13 @@ -912,7 +912,7 @@ importers: packages/react: specifiers: '@tanstack/react-query': 5.0.0-beta.2 - '@trigger.dev/core': workspace:^2.1.7 + '@trigger.dev/core': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/react': 18.2.17 @@ -988,7 +988,7 @@ importers: packages/trigger-sdk: specifiers: - '@trigger.dev/core': workspace:^2.1.7 + '@trigger.dev/core': workspace:^2.1.8 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/node': '18' @@ -13436,7 +13436,6 @@ packages: typescript: 5.1.6 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/parser/5.59.6_mhvizudgavq6jzvqzwcqcv4nnm: resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} @@ -13456,6 +13455,7 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/scope-manager/5.59.6: resolution: {integrity: sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==} @@ -13568,6 +13568,7 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/typescript-estree/5.59.6_typescript@5.0.4: resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} @@ -13609,7 +13610,6 @@ packages: typescript: 5.1.6 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/utils/5.59.6_eslint@8.45.0: resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} @@ -17858,7 +17858,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_mhvizudgavq6jzvqzwcqcv4nnm + '@typescript-eslint/parser': 5.59.6_ko3fmmbeyij36muomfgt2u76xu debug: 3.2.7 eslint: 8.45.0 eslint-import-resolver-node: 0.3.7 @@ -18005,7 +18005,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_mhvizudgavq6jzvqzwcqcv4nnm + '@typescript-eslint/parser': 5.59.6_ko3fmmbeyij36muomfgt2u76xu array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -28865,6 +28865,7 @@ packages: dependencies: tslib: 1.14.1 typescript: 4.9.5 + dev: true /tsutils/3.21.0_typescript@5.0.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -28884,7 +28885,6 @@ packages: dependencies: tslib: 1.14.1 typescript: 5.1.6 - dev: false /tsx/3.12.2: resolution: {integrity: sha512-ykAEkoBg30RXxeOMVeZwar+JH632dZn9EUJVyJwhfag62k6UO/dIyJEV58YuLF6e5BTdV/qmbQrpkWqjq9cUnQ==} From 4e358718614b888fb0d8ce6c00c610d19001af41 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 5 Oct 2023 10:38:28 +0100 Subject: [PATCH 08/79] feat: New Test page (#558) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improvements to the shortcut keys * Updated code mirror theme to the new dark mode style * Removed the old test page help panel copy * WIP new structure for the test page * Added some of the side panel options for examples and account ID * Conditionally hide the side panel components if they’re blank * Example with selected state working * Allow selecting the example again to overwrite any edits * Previous run payloads working * Select a recent payload if there’s no example * Created Icon and DetailCell components * The integrations now use the DetailCell * Use DetailCell on the integrations page * Added support for description to DetailCell. With proper variants now * DateTimeAccurate and formatDateTimeAccurate weren’t displaying correctly. fractionalSecondDigits isn’t in the types, but is supported for 92.5% of users * DetailCell now allows label and description to be React components * Using the DetailCell on the Test page * Styled the CodeMirror scrollbars to match elsewhere * Adding copy and clear to the JSONEditor, not working properly yet though * Removed the copy/clear buttons from the Test route * Delete the light color theme * The editor now supports copying/clearing etc * Made it clear when text is copied * Changed the learn link to a tertiary button * Fixed CMD shortcut keys * Run test button now works with ⌘ Enter * CodeMirror now allows ⌘Enter to escape the field * Added JSON linter to the test editor * Fix for buttons not being aligned correctly * Copy/clear buttons now aligned * Made the integration DetailCell text smaller * The test recent payloads now have colored text to help identify the status * DetailCell descriptions are dimmer * Added link to docs for BYOA * Fix for buttons going full width when they shouldn’t --------- Co-authored-by: James Ritchie --- .../webapp/app/components/code/JSONEditor.tsx | 90 ++++- .../app/components/code/codeMirrorSetup.ts | 59 ++-- .../app/components/code/codeMirrorTheme.ts | 202 +++-------- .../helpContent/HelpContentText.tsx | 31 -- .../app/components/primitives/Buttons.tsx | 2 +- .../app/components/primitives/DateTime.tsx | 13 +- .../app/components/primitives/DetailCell.tsx | 95 ++++++ .../webapp/app/components/primitives/Icon.tsx | 37 ++ .../app/components/primitives/ShortcutKey.tsx | 15 +- .../components/stories/DetailCell.stories.tsx | 46 +++ .../components/stories/Shortcuts.stories.tsx | 5 + apps/webapp/app/hooks/useShortcutKeys.tsx | 34 +- .../app/presenters/TestJobPresenter.server.ts | 39 ++- .../route.tsx | 88 +---- .../route.tsx | 321 +++++++++++------- apps/webapp/package.json | 3 +- pnpm-lock.yaml | 35 +- references/job-catalog/src/byo-auth.ts | 2 +- 18 files changed, 635 insertions(+), 482 deletions(-) create mode 100644 apps/webapp/app/components/primitives/DetailCell.tsx create mode 100644 apps/webapp/app/components/primitives/Icon.tsx create mode 100644 apps/webapp/app/components/stories/DetailCell.stories.tsx diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index 4b635904a..7d5ef0c23 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -1,11 +1,13 @@ import { json as jsonLang } from "@codemirror/lang-json"; import type { ViewUpdate } from "@codemirror/view"; +import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid"; import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror"; import { useCodeMirror } from "@uiw/react-codemirror"; -import { useRef, useEffect } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { cn } from "~/utils/cn"; +import { Button } from "../primitives/Buttons"; import { getEditorSetup } from "./codeMirrorSetup"; import { darkTheme } from "./codeMirrorTheme"; -import { cn } from "~/utils/cn"; export interface JSONEditorProps extends Omit { defaultValue?: string; @@ -14,6 +16,8 @@ export interface JSONEditorProps extends Omit { onChange?: (value: string) => void; onUpdate?: (update: ViewUpdate) => void; onBlur?: (code: string) => void; + showCopyButton?: boolean; + showClearButton?: boolean; } const languages = { @@ -38,6 +42,8 @@ export function JSONEditor(opts: JSONEditorProps) { onBlur, basicSetup, autoFocus, + showCopyButton = true, + showClearButton = true, } = { ...defaultProps, ...opts, @@ -65,7 +71,8 @@ export function JSONEditor(opts: JSONEditorProps) { onChange, onUpdate, }; - const { setContainer, state } = useCodeMirror(settings); + const { setContainer, view } = useCodeMirror(settings); + const [copied, setCopied] = useState(false); useEffect(() => { if (editor.current) { @@ -75,24 +82,71 @@ export function JSONEditor(opts: JSONEditorProps) { //if the defaultValue changes update the editor useEffect(() => { - if (state !== undefined) { - state.update({ - changes: { from: 0, to: state.doc.length, insert: defaultValue }, + if (view !== undefined) { + if (view.state.doc.toString() === defaultValue) return; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: defaultValue }, }); } - }, [defaultValue, state]); + }, [defaultValue, view]); + + const clear = useCallback(() => { + if (view === undefined) return; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: undefined }, + }); + onChange?.(""); + }, [view]); + + const copy = useCallback(() => { + if (view === undefined) return; + navigator.clipboard.writeText(view.state.doc.toString()); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1500); + }, [view]); return ( -
{ - if (!onBlur) return; - onBlur(editor.current?.textContent ?? ""); - }} - /> +
+
{ + if (!onBlur) return; + onBlur(editor.current?.textContent ?? ""); + }} + /> +
+ {showClearButton && ( + + )} + {showCopyButton && ( + + )} +
+
); } diff --git a/apps/webapp/app/components/code/codeMirrorSetup.ts b/apps/webapp/app/components/code/codeMirrorSetup.ts index 9e7ff711d..493196727 100644 --- a/apps/webapp/app/components/code/codeMirrorSetup.ts +++ b/apps/webapp/app/components/code/codeMirrorSetup.ts @@ -1,34 +1,19 @@ -import { - highlightSpecialChars, - drawSelection, - highlightActiveLine, - dropCursor, - lineNumbers, - highlightActiveLineGutter, - keymap, -} from "@codemirror/view"; -import type { Extension } from "@codemirror/state"; -import { highlightSelectionMatches } from "@codemirror/search"; -import { json as jsonLang } from "@codemirror/lang-json"; import { closeBrackets } from "@codemirror/autocomplete"; -import { bracketMatching } from "@codemirror/language"; import { indentWithTab } from "@codemirror/commands"; - -export function getPreviewSetup(): Array { - return [ - jsonLang(), - highlightSpecialChars(), - drawSelection(), - dropCursor(), - bracketMatching(), - highlightSelectionMatches(), - lineNumbers(), - ]; -} - -export function getViewerSetup(): Array { - return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()]; -} +import { jsonParseLinter } from "@codemirror/lang-json"; +import { bracketMatching } from "@codemirror/language"; +import { lintGutter, lintKeymap, linter } from "@codemirror/lint"; +import { highlightSelectionMatches } from "@codemirror/search"; +import { Prec, type Extension } from "@codemirror/state"; +import { + drawSelection, + dropCursor, + highlightActiveLine, + highlightActiveLineGutter, + highlightSpecialChars, + keymap, + lineNumbers, +} from "@codemirror/view"; export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array { const options = [ @@ -36,7 +21,21 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A dropCursor(), bracketMatching(), closeBrackets(), - keymap.of([indentWithTab]), + lintGutter(), + linter(jsonParseLinter()), + Prec.highest( + keymap.of([ + { + key: "Mod-Enter", + run: () => { + console.log("Mod-Enter"); + return true; + }, + preventDefault: false, + }, + ]) + ), + keymap.of([indentWithTab, ...lintKeymap]), ]; if (showLineNumbers) { diff --git a/apps/webapp/app/components/code/codeMirrorTheme.ts b/apps/webapp/app/components/code/codeMirrorTheme.ts index 0492b80b9..babcccf09 100644 --- a/apps/webapp/app/components/code/codeMirrorTheme.ts +++ b/apps/webapp/app/components/code/codeMirrorTheme.ts @@ -17,10 +17,15 @@ export function darkTheme(): Extension { violet = "#c678dd", darkBackground = "#21252b", highlightBackground = "rgba(71,85,105,0.2)", - background = "#0f172a", + background = "rgba(11, 16, 24 ,100)", tooltipBackground = "#353a42", selection = "rgb(71 85 105)", - cursor = "#528bff"; + cursor = "#528bff", + scrollbarTrack = "#0E1521", + scrollbarTrackActive = "#131B2B", + scrollbarThumb = "#293649", + scrollbarThumbActive = "#3C4B62", + scrollbarBg = "#0E1521"; const jsonHeroEditorTheme = EditorView.theme( { @@ -94,6 +99,45 @@ export function darkTheme(): Extension { color: ivory, }, }, + ".cm-scroller": { + scrollbarWidth: "thin", + scrollbarColor: `${scrollbarThumb} ${scrollbarTrack}`, + }, + ".cm-scroller::-webkit-scrollbar": { + display: "block", + width: "8px", + height: "8px", + }, + ".cm-scroller::-webkit-scrollbar-track": { + backgroundColor: scrollbarTrack, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-track:hover": { + backgroundColor: scrollbarTrackActive, + }, + ".cm-scroller::-webkit-scrollbar-track:active": { + backgroundColor: scrollbarTrackActive, + }, + ".cm-scroller::-webkit-scrollbar-thumb": { + backgroundColor: scrollbarThumb, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-thumb:hover": { + backgroundColor: scrollbarThumbActive, + }, + ".cm-scroller::-webkit-scrollbar-thumb:active": { + backgroundColor: scrollbarThumbActive, + }, + ".cm-scroller::-webkit-scrollbar-corner": { + backgroundColor: scrollbarBg, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-corner:hover": { + backgroundColor: scrollbarBg, + }, + ".cm-scroller::-webkit-scrollbar-corner:active": { + backgroundColor: scrollbarBg, + }, }, { dark: true } ); @@ -155,157 +199,3 @@ export function darkTheme(): Extension { return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)]; } - -export function lightTheme(): Extension[] { - const stringColor = "text-[#53a053]", - numberColor = "text-[#447bef]", - variableColor = "text-[#a42ea2]", - booleanColor = "text-[#e2574e]", - coral = "text-[#e06c75]", - invalid = "text-[#ffffff]", - ivory = "text-[#abb2bf]", - stone = "text-[#7d8799]", - malibu = "text-[#61afef]", - whiskey = "text-[#d19a66]", - violet = "text-[#c678dd]", - darkBackground = "text-[#21252b]", - highlightBackground = "text-[#D0D0D0]", - background = "text-[#ffffff]", - tooltipBackground = "text-[#353a42]", - selection = "text-[#D0D0D0]", - cursor = "text-[#528bff]"; - - const jsonHeroEditorTheme = EditorView.theme( - { - "&": { - color: ivory, - backgroundColor: background, - }, - - ".cm-content": { - caretColor: cursor, - fontFamily: "monospace", - fontSize: "14px", - }, - - ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { - backgroundColor: selection, - }, - - ".cm-panels": { backgroundColor: darkBackground, color: ivory }, - ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, - ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, - - ".cm-searchMatch": { - backgroundColor: "#72a1ff59", - outline: "1px solid #457dff", - }, - ".cm-searchMatch.cm-searchMatch-selected": { - backgroundColor: "#6199ff2f", - }, - - ".cm-activeLine": { backgroundColor: highlightBackground }, - ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, - - "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { - backgroundColor: "#bad0f847", - outline: "1px solid #515a6b", - }, - - ".cm-gutters": { - backgroundColor: background, - color: stone, - border: "none", - }, - - ".cm-activeLineGutter": { - backgroundColor: highlightBackground, - }, - - ".cm-foldPlaceholder": { - backgroundColor: "transparent", - border: "none", - color: "#ddd", - }, - - ".cm-tooltip": { - border: "none", - backgroundColor: tooltipBackground, - }, - ".cm-tooltip .cm-tooltip-arrow:before": { - borderTopColor: "transparent", - borderBottomColor: "transparent", - }, - ".cm-tooltip .cm-tooltip-arrow:after": { - borderTopColor: tooltipBackground, - borderBottomColor: tooltipBackground, - }, - ".cm-tooltip-autocomplete": { - "& > ul > li[aria-selected]": { - backgroundColor: highlightBackground, - color: ivory, - }, - }, - }, - { dark: false } - ); - - /// The highlighting style for code in the JSON Hero theme. - const jsonHeroHighlightStyle = tagHighlighter([ - { tag: tags.keyword, class: violet }, - { - tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], - class: variableColor, - }, - { - tag: [tags.function(tags.variableName), tags.labelName], - class: malibu, - }, - { - tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], - class: whiskey, - }, - { tag: [tags.definition(tags.name), tags.separator], class: ivory }, - { - tag: [ - tags.typeName, - tags.className, - tags.number, - tags.changed, - tags.annotation, - tags.modifier, - tags.self, - tags.namespace, - ], - class: numberColor, - }, - { - tag: [ - tags.operator, - tags.operatorKeyword, - tags.url, - tags.escape, - tags.regexp, - tags.link, - tags.special(tags.string), - ], - class: stringColor, - }, - { tag: [tags.meta, tags.comment], class: stone }, - - { tag: tags.link, class: stone }, - { tag: tags.heading, class: coral }, - { - tag: [tags.atom, tags.bool, tags.special(tags.variableName)], - class: booleanColor, - }, - { - tag: [tags.processingInstruction, tags.string, tags.inserted], - class: stringColor, - }, - { tag: tags.invalid, class: invalid }, - ]); - - return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)]; -} diff --git a/apps/webapp/app/components/helpContent/HelpContentText.tsx b/apps/webapp/app/components/helpContent/HelpContentText.tsx index 8e9a45871..ce057dae4 100644 --- a/apps/webapp/app/components/helpContent/HelpContentText.tsx +++ b/apps/webapp/app/components/helpContent/HelpContentText.tsx @@ -79,37 +79,6 @@ export function HowToRunYourJob() { ); } -export function HowToRunATest() { - return ( - <> - - - Select the environment you’d like the test to run against. - - - - - - Write your own payload specific to your Job. Some Triggers also provide example payloads - that you can select from. This will populate the code editor below. - - - - - - When you’re happy with the payload, click Run test. - - - Learn more about running tests. - - - ); -} - export function HowToConnectAnIntegration() { return ( <> diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 532d36836..e469c2561 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -144,7 +144,7 @@ export function ButtonContent(props: ButtonContentPropsType) { const textColorClassName = variation.textColor; return ( -
+
{ }; function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string { - const milliseconds = `00${date.getMilliseconds()}`.slice(-3); - const formattedDateTime = new Intl.DateTimeFormat(locales, { + year: "numeric", month: "short", - day: "2-digit", + day: "numeric", hour: "numeric", - minute: "2-digit", - second: "2-digit", + minute: "numeric", + second: "numeric", timeZone, + // @ts-ignore this works in 92.5% of browsers https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_options_parameter_options_fractionalseconddigits_parameter + fractionalSecondDigits: 3, }).format(date); - return `${formatDateTime}.${milliseconds}`; + return formattedDateTime; } diff --git a/apps/webapp/app/components/primitives/DetailCell.tsx b/apps/webapp/app/components/primitives/DetailCell.tsx new file mode 100644 index 000000000..6047cc150 --- /dev/null +++ b/apps/webapp/app/components/primitives/DetailCell.tsx @@ -0,0 +1,95 @@ +import { cn } from "~/utils/cn"; +import { Icon, IconInBox, RenderIcon } from "./Icon"; +import { Paragraph } from "./Paragraph"; + +const variations = { + small: { + label: { + variant: "small" as const, + className: "m-0 leading-[1.1rem]", + }, + description: { + variant: "extra-small" as const, + className: "m-0", + }, + }, + base: { + label: { + variant: "base" as const, + className: "m-0 leading-[1.1rem] ", + }, + description: { + variant: "small" as const, + className: "m-0", + }, + }, +}; + +type DetailCellProps = { + leadingIcon?: RenderIcon; + leadingIconClassName?: string; + trailingIcon?: RenderIcon; + trailingIconClassName?: string; + label: string | React.ReactNode; + description?: string | React.ReactNode; + className?: string; + variant?: keyof typeof variations; +}; + +export function DetailCell({ + leadingIcon, + leadingIconClassName, + trailingIcon, + trailingIconClassName, + label, + description, + className, + variant = "small", +}: DetailCellProps) { + const variation = variations[variant]; + + return ( +
+ +
+ + {label} + + {description && ( + + {description} + + )} +
+
+ +
+
+ ); +} diff --git a/apps/webapp/app/components/primitives/Icon.tsx b/apps/webapp/app/components/primitives/Icon.tsx new file mode 100644 index 000000000..470d4cc80 --- /dev/null +++ b/apps/webapp/app/components/primitives/Icon.tsx @@ -0,0 +1,37 @@ +import { IconNamesOrString, NamedIcon } from "./NamedIcon"; +import { cn } from "~/utils/cn"; + +export type RenderIcon = IconNamesOrString | React.ComponentType; + +type IconProps = { + icon?: RenderIcon; + className?: string; +}; + +/** Use this icon to either render a passed in React component, or a NamedIcon/CompanyIcon */ +export function Icon(props: IconProps) { + if (typeof props.icon === "string") { + return } />; + } + + const Icon = props.icon; + + if (!Icon) { + return <>; + } + + return ; +} + +export function IconInBox({ boxClassName, ...props }: IconProps & { boxClassName?: string }) { + return ( +
+ +
+ ); +} diff --git a/apps/webapp/app/components/primitives/ShortcutKey.tsx b/apps/webapp/app/components/primitives/ShortcutKey.tsx index ff5d4f5f7..8be993c12 100644 --- a/apps/webapp/app/components/primitives/ShortcutKey.tsx +++ b/apps/webapp/app/components/primitives/ShortcutKey.tsx @@ -23,7 +23,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) const isMac = platform === "mac"; let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; const modifiers = relevantShortcut.modifiers ?? []; - const character = relevantShortcut.key; + const character = keyString(relevantShortcut.key, isMac); return ( @@ -35,6 +35,15 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) ); } +function keyString(key: String, isMac: boolean) { + switch (key) { + case "enter": + return isMac ? "↵" : key; + default: + return key; + } +} + function modifierString(modifier: Modifier, isMac: boolean) { switch (modifier) { case "alt": @@ -42,8 +51,10 @@ function modifierString(modifier: Modifier, isMac: boolean) { case "ctrl": return isMac ? "⌃" : "Ctrl+"; case "meta": - return isMac ? "⌘" : "⊞"; + return isMac ? "⌘" : "⊞+"; case "shift": return isMac ? "⇧" : "Shift+"; + case "mod": + return isMac ? "⌘" : "Ctrl+"; } } diff --git a/apps/webapp/app/components/stories/DetailCell.stories.tsx b/apps/webapp/app/components/stories/DetailCell.stories.tsx new file mode 100644 index 000000000..7f70e01a1 --- /dev/null +++ b/apps/webapp/app/components/stories/DetailCell.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { DetailCell } from "../primitives/DetailCell"; +import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline"; +import { DateTime, DateTimeAccurate } from "../primitives/DateTime"; + +const meta: Meta = { + title: "Primitives/DetailCells", +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: () => , +}; + +function Examples() { + return ( +
+ + + } + description="Run #42 complete" + trailingIcon="plus" + trailingIconClassName="text-slate-500 group-hover:text-bright" + /> +
+ ); +} diff --git a/apps/webapp/app/components/stories/Shortcuts.stories.tsx b/apps/webapp/app/components/stories/Shortcuts.stories.tsx index a5ed28866..dc2f381a7 100644 --- a/apps/webapp/app/components/stories/Shortcuts.stories.tsx +++ b/apps/webapp/app/components/stories/Shortcuts.stories.tsx @@ -24,6 +24,8 @@ const shortcuts: ShortcutDefinition[] = [ { key: "f", modifiers: ["meta"] }, { key: "k", modifiers: ["meta"] }, { key: "del", modifiers: ["ctrl", "alt"] }, + { key: "enter", modifiers: ["meta"] }, + { key: "enter", modifiers: ["mod"] }, ]; function Collection() { @@ -67,6 +69,9 @@ function Set({ platform }: { platform: "mac" | "windows" }) { +
))} diff --git a/apps/webapp/app/hooks/useShortcutKeys.tsx b/apps/webapp/app/hooks/useShortcutKeys.tsx index ccbebf618..092e16396 100644 --- a/apps/webapp/app/hooks/useShortcutKeys.tsx +++ b/apps/webapp/app/hooks/useShortcutKeys.tsx @@ -1,12 +1,12 @@ -import { useEffect, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider"; -export type Modifier = "alt" | "ctrl" | "meta" | "shift"; +export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod"; export type Shortcut = { key: string; modifiers?: Modifier[]; + enabledOnInputElements?: boolean; }; export type ShortcutDefinition = @@ -20,19 +20,31 @@ type useShortcutKeysProps = { shortcut: ShortcutDefinition; action: (event: KeyboardEvent) => void; disabled?: boolean; + enabledOnInputElements?: boolean; }; export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) { - const keys = createKeysFromShortcut(shortcut); - useHotkeys(keys, action, { enabled: !disabled }); -} - -function createKeysFromShortcut(shortcut: ShortcutDefinition) { const { platform } = useOperatingSystem(); const isMac = platform === "mac"; - let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; - const modifiers = relevantShortcut.modifiers; - const character = relevantShortcut.key; + const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; - return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character; + const keys = createKeysFromShortcut(relevantShortcut); + useHotkeys( + keys, + (event, hotkeysEvent) => { + action(event); + }, + { + enabled: !disabled, + enableOnFormTags: relevantShortcut.enabledOnInputElements, + enableOnContentEditable: relevantShortcut.enabledOnInputElements, + } + ); +} + +function createKeysFromShortcut(shortcut: Shortcut) { + const modifiers = shortcut.modifiers; + const character = shortcut.key; + + return modifiers ? modifiers.map((k) => k).join("+") + "+" + character : character; } diff --git a/apps/webapp/app/presenters/TestJobPresenter.server.ts b/apps/webapp/app/presenters/TestJobPresenter.server.ts index 3028199e2..c87b035c8 100644 --- a/apps/webapp/app/presenters/TestJobPresenter.server.ts +++ b/apps/webapp/app/presenters/TestJobPresenter.server.ts @@ -4,6 +4,7 @@ import { PrismaClient, prisma } from "~/db.server"; import { Job } from "~/models/job.server"; import { Organization } from "~/models/organization.server"; import { Project } from "~/models/project.server"; +import { EventExample } from "@trigger.dev/core"; export class TestJobPresenter { #prismaClient: PrismaClient; @@ -67,14 +68,22 @@ export class TestJobPresenter { name: "latest", }, }, - _count: { + runs: { select: { - runs: { - where: { - isTest: true, + id: true, + createdAt: true, + number: true, + status: true, + event: { + select: { + payload: true, }, }, }, + orderBy: { + createdAt: "desc", + }, + take: 5, }, }, where: { @@ -97,6 +106,15 @@ export class TestJobPresenter { throw new Error("Job not found"); } + //collect together the examples, we don't care about the environments + const examples = job.aliases.flatMap((alias) => + alias.version.examples.map((example) => ({ + ...example, + icon: example.icon ?? undefined, + payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined, + })) + ); + return { environments: job.aliases.map((alias) => ({ id: alias.environment.id, @@ -104,15 +122,18 @@ export class TestJobPresenter { slug: alias.environment.slug, userId: alias.environment.orgMember?.userId, versionId: alias.version.id, - examples: alias.version.examples.map((example) => ({ - ...example, - payload: JSON.stringify(example.payload, exampleReplacer, 2), - })), hasAuthResolver: alias.version.integrations.some( (i) => i.integration.authSource === "RESOLVER" ), })), - hasTestRuns: job._count.runs > 0, + examples, + runs: job.runs.map((r) => ({ + id: r.id, + number: r.number, + status: r.status, + created: r.createdAt, + payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined, + })), }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx index 1c653370a..8a2535a2b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx @@ -13,6 +13,7 @@ import { BreadcrumbLink } from "~/components/navigation/NavBar"; import { LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateTime } from "~/components/primitives/DateTime"; +import { DetailCell } from "~/components/primitives/DetailCell"; import { Header2 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; import { Input } from "~/components/primitives/Input"; @@ -209,10 +210,12 @@ function PossibleIntegrationsList({ - } @@ -221,10 +224,12 @@ function PossibleIntegrationsList({ Create an Integration -
@@ -482,77 +487,16 @@ function AddIntegrationConnection({ icon?: string; }) { return ( -
- - - {name} - -
- {isIntegration && } - -
-
- ); -} - -function ExternalIntegrationLink({ - name, - label, - trailingIcon, -}: { - name: string; - label: string; - trailingIcon: string; -}) { - return ( - - - - {label} - -
- -
-
+ ); } export function IntegrationIcon() { return ; } - -function InfoLink({ text }: { text: string }) { - return ( -
- - - {text} - -
- -
-
- ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx index 21917e7bf..f3dc9a36f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx @@ -1,6 +1,7 @@ -import { conform, useForm } from "@conform-to/react"; +import { useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { PopoverTrigger } from "@radix-ui/react-popover"; +import { ClipboardIcon } from "@heroicons/react/20/solid"; +import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline"; import { Form, useActionData, useSubmit } from "@remix-run/react"; import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime"; import { useCallback, useRef, useState } from "react"; @@ -8,16 +9,16 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { HowToRunATest } from "~/components/helpContent/HelpContentText"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; -import { Button, ButtonContent } from "~/components/primitives/Buttons"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; +import { DateTime } from "~/components/primitives/DateTime"; +import { DetailCell } from "~/components/primitives/DetailCell"; import { FormError } from "~/components/primitives/FormError"; -import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; +import { Header2 } from "~/components/primitives/Headers"; +import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; -import { Label } from "~/components/primitives/Label"; -import { Popover, PopoverContent } from "~/components/primitives/Popover"; import { Select, SelectContent, @@ -26,6 +27,8 @@ import { SelectTrigger, SelectValue, } from "~/components/primitives/Select"; +import { TextLink } from "~/components/primitives/TextLink"; +import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses"; import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { TestJobPresenter } from "~/presenters/TestJobPresenter.server"; import { TestJobService } from "~/services/jobs/testJob.server"; @@ -39,14 +42,14 @@ export const loader = async ({ request, params }: LoaderArgs) => { const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params); const presenter = new TestJobPresenter(); - const { environments, hasTestRuns } = await presenter.call({ + const { environments, runs, examples } = await presenter.call({ userId, organizationSlug, projectSlug: projectParam, jobSlug: jobParam, }); - return typedjson({ environments, hasTestRuns }); + return typedjson({ environments, runs, examples }); }; const schema = z.object({ @@ -116,22 +119,30 @@ export const handle: Handle = { const startingJson = "{\n\n}"; export default function Page() { + const { environments, runs, examples } = useTypedLoaderData(); + + //form submission const submit = useSubmit(); const lastSubmission = useActionData(); - const [isExamplePopoverOpen, setIsExamplePopoverOpen] = useState(false); - const { environments, hasTestRuns } = useTypedLoaderData(); - const [defaultJson, setDefaultJson] = useState(startingJson); - const currentJson = useRef(defaultJson); + //examples + const [selectedCodeSampleId, setSelectedCodeSampleId] = useState( + examples.at(0)?.id ?? runs.at(0)?.id + ); + const selectedCodeSample = + examples.find((e) => e.id === selectedCodeSampleId)?.payload ?? + runs.find((r) => r.id === selectedCodeSampleId)?.payload; + + const [defaultJson, setDefaultJson] = useState(selectedCodeSample ?? startingJson); + const setCode = useCallback((code: string) => { + setDefaultJson(code); + }, []); + const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments[0].id); - const [currentAccountId, setCurrentAccountId] = useState(undefined); - const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId); - const insertCode = useCallback((code: string) => { - setDefaultJson(code); - setIsExamplePopoverOpen(false); - }, []); + const currentJson = useRef(defaultJson); + const [currentAccountId, setCurrentAccountId] = useState(undefined); const submitForm = useCallback( (e: React.FormEvent) => { @@ -170,120 +181,178 @@ export default function Page() { } return ( - - {(open) => ( -
-
-
submitForm(e)} - > -
-
- - - +
+
+ submitForm(e)} + > +
+
+ { + currentJson.current = v; - {selectedEnvironment && selectedEnvironment.examples.length > 0 && ( - setIsExamplePopoverOpen(open)} + //deselect the example if it's been edited + if (selectedCodeSampleId) { + if (v !== selectedCodeSample) { + setDefaultJson(v); + setSelectedCodeSampleId(undefined); + } + } + }} + height="100%" + min-height="100%" + max-height="100%" + autoFocus + placeholder="Use your schema to enter valid JSON or add one of the example payloads then click 'Run test'" + className="h-full" + /> +
+
+ {examples.length > 0 && ( +
+ Example payloads + {examples.map((example) => ( + - ))} - - - )} + + + ))}
- + )} +
+ Recent payloads + {runs.length === 0 ? ( + + Recent payloads will show here once you've completed a Run. + + ) : ( +
+ {runs.map((run) => ( + + ))} +
+ )}
- - -
- (currentJson.current = v)} - minHeight="150px" - /> -
-
{selectedEnvironment?.hasAuthResolver && ( - - - setCurrentAccountId(e.target.value)} - /> - {accountId.error} - +
+ Account ID + + setCurrentAccountId(e.target.value)} + /> + {accountId.error} + + Learn about testing Jobs with an Account ID in our{" "} + + BYOAuth docs + + + +
)} -
- {payload.error ? ( - {payload.error} - ) : ( -
- )} - -
- +
- - - -
- )} - +
+ + Learn more about running tests + +
+ {payload.error ? ( + {payload.error} + ) : ( +
+ )} + + + + +
+
+ +
+
); } diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 2e90e4b22..a7576143e 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -34,6 +34,7 @@ "@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-json": "^6.0.1", "@codemirror/language": "^6.3.1", + "@codemirror/lint": "^6.4.2", "@codemirror/search": "^6.2.3", "@codemirror/state": "^6.1.3", "@codemirror/view": "^6.5.0", @@ -93,7 +94,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.0", - "react-hotkeys-hook": "^3.4.7", + "react-hotkeys-hook": "^4.4.1", "react-use": "^17.4.0", "recharts": "^2.8.0", "remix-auth": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9f9b73b1..32090baa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,7 @@ importers: '@codemirror/lang-javascript': ^6.1.1 '@codemirror/lang-json': ^6.0.1 '@codemirror/language': ^6.3.1 + '@codemirror/lint': ^6.4.2 '@codemirror/search': ^6.2.3 '@codemirror/state': ^6.1.3 '@codemirror/view': ^6.5.0 @@ -168,7 +169,7 @@ importers: react: ^18.2.0 react-dom: ^18.2.0 react-hot-toast: ^2.4.0 - react-hotkeys-hook: ^3.4.7 + react-hotkeys-hook: ^4.4.1 react-use: ^17.4.0 recharts: ^2.8.0 remix-auth: ^3.2.2 @@ -202,6 +203,7 @@ importers: '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 '@codemirror/language': 6.3.2 + '@codemirror/lint': 6.4.2 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 @@ -232,7 +234,7 @@ importers: '@trigger.dev/core': link:../../packages/core '@trigger.dev/database': link:../../packages/database '@trigger.dev/sdk': link:../../packages/trigger-sdk - '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle + '@uiw/react-codemirror': 4.19.5_th22fcplkuhrqjnlojwclcaim4 class-variance-authority: 0.5.2_typescript@4.9.4 clsx: 1.2.1 compression: 1.7.4 @@ -261,7 +263,7 @@ importers: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y - react-hotkeys-hook: 3.4.7_biqbaboplfbrettd7655fr4n2y + react-hotkeys-hook: 4.4.1_biqbaboplfbrettd7655fr4n2y react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y recharts: 2.8.0_v2m5e27vhdewzwhryxwfaorcca remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y @@ -5357,7 +5359,7 @@ packages: dependencies: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/language': 6.3.2 - '@codemirror/lint': 6.1.0 + '@codemirror/lint': 6.4.2 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 '@lezer/common': 1.0.2 @@ -5382,8 +5384,8 @@ packages: style-mod: 4.0.0 dev: false - /@codemirror/lint/6.1.0: - resolution: {integrity: sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==} + /@codemirror/lint/6.4.2: + resolution: {integrity: sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==} dependencies: '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 @@ -13678,12 +13680,13 @@ packages: '@typescript-eslint/types': 5.59.6 eslint-visitor-keys: 3.4.2 - /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: + /@uiw/codemirror-extensions-basic-setup/4.19.5_o3n2erwajrogdzfxqd6wu4qkza: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' @@ -13691,13 +13694,13 @@ packages: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 - '@codemirror/lint': 6.1.0 + '@codemirror/lint': 6.4.2 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: + /@uiw/react-codemirror/4.19.5_th22fcplkuhrqjnlojwclcaim4: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: '@codemirror/state': '>=6.0.0' @@ -13710,13 +13713,14 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom + '@uiw/codemirror-extensions-basic-setup': 4.19.5_o3n2erwajrogdzfxqd6wu4qkza codemirror: 6.0.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' + - '@codemirror/lint' - '@codemirror/search' dev: false @@ -15779,7 +15783,7 @@ packages: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 - '@codemirror/lint': 6.1.0 + '@codemirror/lint': 6.4.2 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 @@ -20207,10 +20211,6 @@ packages: lru-cache: 7.18.3 dev: false - /hotkeys-js/3.9.4: - resolution: {integrity: sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q==} - dev: false - /hpagent/0.1.2: resolution: {integrity: sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==} requiresBuild: true @@ -25799,13 +25799,12 @@ packages: - csstype dev: false - /react-hotkeys-hook/3.4.7_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ==} + /react-hotkeys-hook/4.4.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' dependencies: - hotkeys-js: 3.9.4 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 dev: false diff --git a/references/job-catalog/src/byo-auth.ts b/references/job-catalog/src/byo-auth.ts index e77786bb1..b739f6912 100644 --- a/references/job-catalog/src/byo-auth.ts +++ b/references/job-catalog/src/byo-auth.ts @@ -23,7 +23,7 @@ const stripe = new Stripe({ }); const slack = new Slack({ id: "slack" }); const openai = new OpenAI({ id: "openai" }); -const github = new Github({ id: "github" }); +const github = new Github({ id: "github-byoa" }); client.defineAuthResolver(resend, async (ctx, integration) => { return { From c0aa6633d2a68b2a2f360509961179811f9b2d04 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:24:09 +0530 Subject: [PATCH 09/79] chore: update doc title for groups (#539) * chore: update doc title for groups * update docs for sdk and integrations --- .../concepts/triggers/introduction.mdx | 21 +++++-------------- docs/documentation/guides/deployment.mdx | 15 ++++--------- docs/documentation/guides/react-hooks.mdx | 3 ++- .../guides/using-integrations.mdx | 4 ++-- docs/documentation/introduction.mdx | 3 ++- .../quickstarts/introduction.mdx | 2 +- docs/integrations/apis/github.mdx | 3 ++- .../apis/supabase/introduction.mdx | 3 ++- docs/integrations/create.mdx | 3 ++- docs/integrations/introduction.mdx | 3 ++- docs/sdk/dynamicschedule/overview.mdx | 3 ++- docs/sdk/dynamictrigger/overview.mdx | 3 ++- docs/sdk/introduction.mdx | 3 ++- docs/sdk/io/overview.mdx | 3 ++- docs/sdk/react/introduction.mdx | 3 ++- docs/sdk/triggerclient/overview.mdx | 3 ++- 16 files changed, 36 insertions(+), 42 deletions(-) diff --git a/docs/documentation/concepts/triggers/introduction.mdx b/docs/documentation/concepts/triggers/introduction.mdx index c626e7852..c4a56cee3 100644 --- a/docs/documentation/concepts/triggers/introduction.mdx +++ b/docs/documentation/concepts/triggers/introduction.mdx @@ -1,30 +1,19 @@ --- -title: Introduction +title: "Triggers: Introduction" +sidebarTitle: "Introduction" description: "A Trigger is what starts a Job Run. It can be a webhook, a schedule, or an event." --- We currently support three types of Triggers: Webhooks, Scheduled, and Events. You can use any of these to start a Job Run. - + Start your Jobs in realtime when events happen in APIs - + Run a Job on a repeating schedule - + Run your Job when you send events with data + This only needs to be done once for each environment @@ -32,11 +29,7 @@ There are two ways to do this: > Manually refresh in your Trigger.dev dashboard - + Automatically refresh by using our webhook diff --git a/docs/documentation/guides/react-hooks.mdx b/docs/documentation/guides/react-hooks.mdx index 648225a15..0132daae7 100644 --- a/docs/documentation/guides/react-hooks.mdx +++ b/docs/documentation/guides/react-hooks.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "React hooks: Overview" +sidebarTitle: "Overview" description: "How to show the live status of Job Runs in your React app" --- diff --git a/docs/documentation/guides/using-integrations.mdx b/docs/documentation/guides/using-integrations.mdx index b2a3dc8bf..45833102e 100644 --- a/docs/documentation/guides/using-integrations.mdx +++ b/docs/documentation/guides/using-integrations.mdx @@ -1,7 +1,7 @@ --- -title: "Integrations Overview" -description: "How to use Trigger.dev Integrations" +title: "Integrations: Overview" sidebarTitle: "Overview" +description: "How to use Trigger.dev Integrations" --- diff --git a/docs/documentation/introduction.mdx b/docs/documentation/introduction.mdx index 69527d8ce..a9f68d1bf 100644 --- a/docs/documentation/introduction.mdx +++ b/docs/documentation/introduction.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Getting Started: Introduction" +sidebarTitle: "Introduction" description: "Welcome to the Trigger.dev documentation." --- diff --git a/docs/documentation/quickstarts/introduction.mdx b/docs/documentation/quickstarts/introduction.mdx index 48891d398..c5aae43c3 100644 --- a/docs/documentation/quickstarts/introduction.mdx +++ b/docs/documentation/quickstarts/introduction.mdx @@ -1,5 +1,5 @@ --- -title: "Introduction" +title: "Quick Starts: Introduction" sidebarTitle: "Introduction" --- diff --git a/docs/integrations/apis/github.mdx b/docs/integrations/apis/github.mdx index c99e71dc0..c001d7da1 100644 --- a/docs/integrations/apis/github.mdx +++ b/docs/integrations/apis/github.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "GitHub: Introduction" +sidebarTitle: "Introduction" --- diff --git a/docs/integrations/apis/supabase/introduction.mdx b/docs/integrations/apis/supabase/introduction.mdx index 92485b6c5..af78305dc 100644 --- a/docs/integrations/apis/supabase/introduction.mdx +++ b/docs/integrations/apis/supabase/introduction.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Supabase: Introduction" +sidebarTitle: "Introduction" --- diff --git a/docs/integrations/create.mdx b/docs/integrations/create.mdx index 5ae14721d..6d56309ca 100644 --- a/docs/integrations/create.mdx +++ b/docs/integrations/create.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Create an Integration: Introduction" +sidebarTitle: "Introduction" description: "You can create Integrations of your own." --- diff --git a/docs/integrations/introduction.mdx b/docs/integrations/introduction.mdx index 688463510..cce19f3a4 100644 --- a/docs/integrations/introduction.mdx +++ b/docs/integrations/introduction.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Integrations: Introduction" +sidebarTitle: "Introduction" description: "Integrations make it easy to authenticate and use APIs." --- diff --git a/docs/sdk/dynamicschedule/overview.mdx b/docs/sdk/dynamicschedule/overview.mdx index f6faaa0a8..2216bc92d 100644 --- a/docs/sdk/dynamicschedule/overview.mdx +++ b/docs/sdk/dynamicschedule/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "DynamicSchedule: Overview" +sidebarTitle: "Overview" description: "`DynamicSchedule` allows you to define a scheduled trigger that can be configured dynamically at runtime." --- diff --git a/docs/sdk/dynamictrigger/overview.mdx b/docs/sdk/dynamictrigger/overview.mdx index b78330cf3..c597b3783 100644 --- a/docs/sdk/dynamictrigger/overview.mdx +++ b/docs/sdk/dynamictrigger/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "DynamicTrigger: Overview" +sidebarTitle: "Overview" description: "`DynamicTrigger` allows you to define a trigger that can be configured dynamically at runtime." --- diff --git a/docs/sdk/introduction.mdx b/docs/sdk/introduction.mdx index 1e2c7076e..0d4b28362 100644 --- a/docs/sdk/introduction.mdx +++ b/docs/sdk/introduction.mdx @@ -1,5 +1,6 @@ --- -title: "Introduction" +title: "SDK: Introduction" +sidebarTitle: "Introduction" description: "The SDK is how you interact with Trigger.dev" --- diff --git a/docs/sdk/io/overview.mdx b/docs/sdk/io/overview.mdx index 3a061e336..3c0eb15c3 100644 --- a/docs/sdk/io/overview.mdx +++ b/docs/sdk/io/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "IO: Overview" +sidebarTitle: "Overview" description: "The second parameter in a Job's `run()` function. It holds Integrations and useful actions you can perform." --- diff --git a/docs/sdk/react/introduction.mdx b/docs/sdk/react/introduction.mdx index 63c67ec93..5c255d15c 100644 --- a/docs/sdk/react/introduction.mdx +++ b/docs/sdk/react/introduction.mdx @@ -1,5 +1,6 @@ --- -title: "Introduction" +title: "React SDK: Introduction" +sidebarTitle: "Introduction" description: "The React SDK allows you to display the status of your Jobs and Runs in your React app." --- diff --git a/docs/sdk/triggerclient/overview.mdx b/docs/sdk/triggerclient/overview.mdx index a91880ef3..6d263dd0b 100644 --- a/docs/sdk/triggerclient/overview.mdx +++ b/docs/sdk/triggerclient/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "TriggerClient: Overview" +sidebarTitle: "Overview" description: "TriggerClient is used to create a client that connects to the Trigger.dev platform" --- From 3e5a97c4b5f3b840a4c87ec6e03cec852c161c2f Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 5 Oct 2023 12:47:40 +0100 Subject: [PATCH 10/79] Add instructions for triggering job runs to the job catalog readme --- references/job-catalog/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/references/job-catalog/README.md b/references/job-catalog/README.md index a1893ce1e..e73669a52 100644 --- a/references/job-catalog/README.md +++ b/references/job-catalog/README.md @@ -28,6 +28,8 @@ cd references/job-catalog pnpm run dev:trigger ``` +Navigate to your trigger.dev instance ([http://localhost:3030](http://localhost:3030/) when running locally, or if you are using the cloud version it's [cloud.trigger.dev](https://cloud.trigger.dev)), to see the jobs. You can use the test feature to trigger them. See our [testing jobs docs](https://trigger.dev/docs/documentation/guides/testing-jobs) for more info. + ### Adding a new file You can add a new file to `src` with it's own `TriggerClient` and set of jobs (e.g. `src/events.ts`) From 9189bdf50fb74963e1db6a5abde7df6fff3beced Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 5 Oct 2023 14:42:29 +0100 Subject: [PATCH 11/79] Updated the Test docs --- docs/documentation/guides/testing-jobs.mdx | 29 ++++++++++++++++++--- docs/images/test-annotated.png | Bin 50739 -> 296695 bytes 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/documentation/guides/testing-jobs.mdx b/docs/documentation/guides/testing-jobs.mdx index 04ce3bf51..d988ba20f 100644 --- a/docs/documentation/guides/testing-jobs.mdx +++ b/docs/documentation/guides/testing-jobs.mdx @@ -14,9 +14,32 @@ There's a tab on the Job page called **Test**. Or you can click the "Test" butto ![Your options on the Test page](/images/test-annotated.png) -1. Select the environment you'd like the test to run against. -2. Some Triggers provide example payloads that you can select from. This will populate the code editor below. -3. When you're happy with the payload, click **Run test**. + + + You will see errors inline if you have any syntax errors. You can use the *Clear* and *Copy* + buttons in the corner. + + + Some Triggers provide example payloads that you can select from. When selected they will + populate the code editor below. + + + If you have previously done Runs, you can select from the most recent payloads. When selected + they will populate the code editor below. + + + If this Job has associated Accounts, enter an Account ID. See [testing with account + ids](/documentation/guides/using-integrations-byo-auth#testing-jobs-with-account-id) for more + information. + + + Select the environment you'd like the test to run against. + + + When you're happy with the payload, click **Run test**. Or press the shortcut key: `Cmd + Enter` + on Mac, `Ctrl + Enter` on Windows. + + ## Identifying test runs diff --git a/docs/images/test-annotated.png b/docs/images/test-annotated.png index a224e3fe493b91adde0f17e3d327975bce91ac41..d34b50d33d241ae18f87b3e1a7be77db67e173fd 100644 GIT binary patch literal 296695 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

&bP7{S2RClET2&{H4-J_RzGF4n} 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

w}MY9ATXobg){amQ*FGam}RIJtcBlw17=? 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%0D&iSd2-~g$Xa*R#y$!e?;6uV6Y8_9Vv}zBT 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�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 E0F|I!-v9sr literal 50739 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

pK2;?R;5cf&9`M`RGz>LH)A+ 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

}Sg2U`}B`G-96F@Xd zM-32Eq){RL4n#NNy%zG~4@igE@}413m+n60S-vztuYW&!pOR6;bFilN^P4jPmwq2+ zXZM*r-fg~aco~3I=s;{VmW|Ib>GPXZRB4~RTgSOx+;sl{%A1Y2pY%fEkJr6`b0#degUtNp2Jgsm>!js;t*stNr_%DFKRw0dp&Ja=kuAhr}d159xoFyAJ1hcW7BM>Bq)0 ziin6yOS6Egev*lcD|5fv?Ng0v-e`aAy~&c;HdT+AEv#LqJItDS`ANYOE~i)LUsvq* zERl$Ne#NYDnU|OptSLp*6p=^O`=MLy2$<5%as}keg7J#h?+6nhd^EV<9`Nbij4ztt z7W4ih*2Wj5$b_CmPZApeGKytZ&*qM7ln?KJ{d%tlg6^&pWaf#D6=&&|zI>S#0)Y^l zJ=DW!5>f0iUeUST1|SE|LSg&e_U}tJH-XzCPY~nKew}8YMQN`k`9o40fk5G}U&ttD-*Y&<1kgWQf1Z?4mxOFvgQ9wRdc5Zh zp{(G|*lt|qAvKSX`n|N~!OKpO6%`eod_Ny zhvbbPnN@CO=allL<5un3_IHp=-~Yh$w|r?Rc_64 zSi&uQ6c@G!0S!`6O`lM1Wo3Yp_Ef{AP6v)U{ zhle+9PB%ErjVnyC>@cYu(qwAhz{+COEl&yG3cofCxS}&QGnx={pR+{;>yL)PQjV%{ zbM|}w52;zCK;YqflK)Cg{#162D2l=yY~~Z(9G8`qVZ~6S!DGU*2@UP>4=TiEC(w32 zF`$5Nq|oWr$B#*)A|i}p2WdT3Ypt7mh)}icJB9d_^s049YnR1>x&9Nqepl=s*Az=n zVIpU|d5^wS{0)ebXo-h|^18?&E?y&-wU3c&5tr7b$ zH)x%753Z!bJNtL;m+?78${Uq@n*h9c>tJPtF=@LYbeI{ltKAYhh1qj&Q6OZAIqprE z`0ozp#IN{qr@t`#P~Lp-p`v+1L(6{#);ha28kxRoc`vWGxmMIF_wg4M7+)t~K|6qkK{J1v?Jrka$ATVcW zywkj&Vi&o2kOEzOHSU-krsH4bGbrR%Le6#A3klUMLUsYlm!;f9M;?BtY+e3P#_6Wx zWZ9fIETUcnhB%nge0m&SG%0?1jZ1&?E1O5NzD)WX3OZ%^)0O@%SjyfHv#l$3=|$#n zrlPvfeh2V}uM`})qSVnhjYvC+un+7OeE0XiY5kacYR?6-xQ_pPAxjI;hb5p0Rq}TyIpfD^K8Isf`l)0y1h)XV)QhWmzUCZ$BOEx=`)hq*GG(N$bI< z^Xfg`{>gLL?kD{x3QCV(>m=8_lWl>MiTHd8V(%=`dY<Onx-&bwnkaM`bq1lm4cjO+lQe3?t|sUxyZgW8Di5Q4$>wp!hCUb;8lmTU9# zsORT3W^4O%%1a>^WcTd%kpsO}vZN;uDgWx-F669-DMMMXa>+tOHIL}J zqU1>MU*$MlQS}k3)n|6pDXBrm+vW!0 zpmIbKXSn4g)OumjL#UFX+nS1~(+cZ(pmHpF6`V~K9qj!sRRT+juGrDefHi9sYkv)! zdy&y*XYaOiHYM7eW5@NVJ;GPz4wh{cz`hmf93SeJ2M{K;H7MW5=p6CAj|$fI2jYvk zSU#8ihTb+zZ15}kiu-B+;Pfb&A}M-3WR}sLHwOoOogIQ zb+T*2iLD)R^}JH)M-#-!s<#d}&7wr>LOSf0hmoaaLzv#NGq`jj0 zy+c81B<(@UKACcus|CFZEy6cbA<>VO77;tBH#%|_5v|NBhkUtq)?`%Tmx2^PiMzgyi%#vCQ`NH3>3 za#9R;Z(+Z&h~U1hbI8Wp_wW=X`zJswGB^K_TamS|LO}e_fI))eC(%kI&rH4m_l7BvtlA zqw)Hoc~$33u=PDk^$B6+bjN+A(H() zeG3MhNSd34*s-^(2ZxeUqYY8?#8qBAW+Q65zwd;^XxqY0ygGkWOtv!nbVLpo+B@)#^L!zv)wETiGLi*p*i~oICrvfxB z`qk02^#5rafhPX<_7MLFJixJkgyEOm{Aau0f1gqQt0n)e3ctAU&rG@r~2eGcnDB3!;}pUI>sTIH;@u>ZqD61g^2&F47@ z<0HOKxS@Jp1-OHzI2TP!Nd8zz6+NbIM$4^OJpI~ucxA!j@I~N*k8*)=N}$pY3Z1&f zTS*WpqaBMjE8@XRx84?`NgL?NFu3?p&k4OL1^nMvLw)=R+q@i3CqvSX-`dxX!q?Y$O zCnRP$XnPDjl2=+QrWQ({wh>^AR3R34%7Y?$EYk9YvMFC;`bnO{#KxrBA3NR^9Ii@NDeO&^K{?VC;50hN@7emvRwWSHaBldf09eW?>zB*UWIR1@tB^0 zm!Gf^)%oE`o``S(Vz%;Jp}-+)`wbQ1Pbt+&tveIQlt7^gG@_Vj(AezIq-#N=5v8Y#-Kib4o{@!thTG z#fx2sFF+hOrk?^QYz(#Qa7JRIe)0FhruAA4t!@O9c4tmVG>Z7bzAQPi@nS4!Cr>() zm?Hf4JyrCif|oX!c=&MdPTqw;*&3pK)1>DSH4mFv)k0ii2B_#-oZtEnZeq4qMuT+3 zqwBzhPe2J{o#y~S3s<+g<4QbUQ`B9i=HU;BdxmhkQ!7f$l+FOi%4bTOdkYD~1e06K z%A_k5rjD`)b&Os8M7drz^dh;?pK}9Cq)7-)Ce;|j=qerLjUdEFXWb8z1J!ab1R5k} zpy`LW!GA|qa@J42Vse^8l6%#UXo<8o|H33s{MLVJwg10O@-XW7X_M-Uz{KFZ?IZ5J zp+ZB;ovR4f$P2)!KW^d^ZO zsiqI4*L<9Kkdm1MbcQX;+7tV|-mQ&FpSzSybN`OAH4|~z+rLjv%3K1^5187e2o zdGkMxJ0T(Eor$Ue-~x!2mqxvOxj-6j#Y=V*f=H25N}YB@4ky3MV&=In?HG3L7uBgi=!tSjk6#yebot6U->gB)Jd^Y6H1Q`*ryEz{nY5 zL5Cl1)O*;@v^{p;diR=!bV!_rgu)AAg{1UVNj2`*5Ls#>3QTcCQ7<;5;yI-FKfeTH Y!MWV?5=q$d!<9ty`mJk)SMLS=508QVDF6Tf From 914745f642ae26c1148d2afa2f64cb77a8c7a3b3 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 5 Oct 2023 16:49:06 +0100 Subject: [PATCH 12/79] =?UTF-8?q?Removed=20a=20console.log=20when=20a=20us?= =?UTF-8?q?er=E2=80=99s=20file=20is=20changed=20that=20CLI=20dev=20is=20li?= =?UTF-8?q?stening=20for?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/thick-pugs-give.md | 5 +++++ packages/cli/src/commands/dev.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/thick-pugs-give.md diff --git a/.changeset/thick-pugs-give.md b/.changeset/thick-pugs-give.md new file mode 100644 index 000000000..1ee60d0c6 --- /dev/null +++ b/.changeset/thick-pugs-give.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +Removed log when a file is changed diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 1984bb15f..517ea9908 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -196,7 +196,6 @@ export async function devCommand(path: string, anyOptions: any) { }); watcher.on("all", (_event, _path) => { - console.log(_event, _path); throttle(refresh, throttleTimeMs); }); From 2e9452ab7c5cb7dbeaefd880e6425f58d2cd7f2a Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:21:59 +0530 Subject: [PATCH 13/79] feat: allow cancelling jobs from trigger-client sdk (#562) * feat: allow cancelling jobs from trigger-client sdk * use presenter instead of service for non-mutating logic --- .changeset/orange-falcons-smile.md | 5 ++ .../app/presenters/ApiRunPresenter.server.ts | 72 +++++++++++++++++++ .../app/routes/api.v1.runs.$runId.cancel.ts | 71 ++++++++++++++++++ apps/webapp/app/routes/api.v1.runs.$runId.ts | 52 +++----------- packages/trigger-sdk/src/apiClient.ts | 16 +++++ packages/trigger-sdk/src/triggerClient.ts | 4 ++ 6 files changed, 176 insertions(+), 44 deletions(-) create mode 100644 .changeset/orange-falcons-smile.md create mode 100644 apps/webapp/app/presenters/ApiRunPresenter.server.ts create mode 100644 apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts diff --git a/.changeset/orange-falcons-smile.md b/.changeset/orange-falcons-smile.md new file mode 100644 index 000000000..7651f714f --- /dev/null +++ b/.changeset/orange-falcons-smile.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": patch +--- + +allow cancelling jobs from trigger-client diff --git a/apps/webapp/app/presenters/ApiRunPresenter.server.ts b/apps/webapp/app/presenters/ApiRunPresenter.server.ts new file mode 100644 index 000000000..6e8ccd3d2 --- /dev/null +++ b/apps/webapp/app/presenters/ApiRunPresenter.server.ts @@ -0,0 +1,72 @@ +import { Job } from "@trigger.dev/database"; +import { PrismaClient, prisma } from "~/db.server"; + +type ApiRunOptions = { + runId: Job["id"]; + maxTasks?: number; + taskDetails?: boolean; + subTasks?: boolean; + cursor?: string; +}; + +export class ApiRunPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + runId, + maxTasks = 20, + taskDetails = false, + subTasks = false, + cursor, + }: ApiRunOptions) { + const take = Math.min(maxTasks, 50); + + return await prisma.jobRun.findUnique({ + where: { + id: runId, + }, + select: { + id: true, + status: true, + startedAt: true, + updatedAt: true, + completedAt: true, + environmentId: true, + output: true, + tasks: { + select: { + id: true, + parentId: true, + displayKey: true, + status: true, + name: true, + icon: true, + startedAt: true, + completedAt: true, + params: taskDetails, + output: taskDetails, + }, + where: { + parentId: subTasks ? undefined : null, + }, + orderBy: { + id: "asc", + }, + take: take + 1, + cursor: cursor + ? { + id: cursor, + } + : undefined, + }, + statuses: { + select: { key: true, label: true, state: true, data: true, history: true }, + }, + }, + }); + } +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts new file mode 100644 index 000000000..876d22d84 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts @@ -0,0 +1,71 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { PrismaErrorSchema } from "~/db.server"; +import { z } from "zod"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { CancelRunService } from "~/services/runs/cancelRun.server"; +import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; + +const ParamsSchema = z.object({ + runId: z.string(), +}); + +export async function action({ request, params }: ActionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + // Authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API Key" }, { status: 401 }); + } + + const parsed = ParamsSchema.safeParse(params); + + if (!parsed.success) { + return json({ error: "Invalid or Missing runId" }, { status: 400 }); + } + + const { runId } = parsed.data; + + const service = new CancelRunService(); + try { + await service.call({ runId }); + } catch (error) { + const prismaError = PrismaErrorSchema.safeParse(error); + // Record not found in the database + if (prismaError.success && prismaError.data.code === "P2005") { + return json({ error: "Run not found" }, { status: 404 }); + } else { + return json({ error: "Internal Server Error" }, { status: 500 }); + } + } + + const presenter = new ApiRunPresenter(); + const jobRun = await presenter.call({ + runId: runId, + }); + + if (!jobRun) { + return json({ message: "Run not found" }, { status: 404 }); + } + + return json({ + id: jobRun.id, + status: jobRun.status, + startedAt: jobRun.startedAt, + updatedAt: jobRun.updatedAt, + completedAt: jobRun.completedAt, + output: jobRun.output, + tasks: jobRun.tasks, + statuses: jobRun.statuses.map((s) => ({ + ...s, + state: s.state ?? undefined, + data: s.data ?? undefined, + history: s.history ?? undefined, + })), + }); +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.ts index e1273654f..12fbaffc9 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.ts @@ -1,7 +1,7 @@ import type { LoaderArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { z } from "zod"; -import { prisma } from "~/db.server"; +import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { apiCors } from "~/utils/apiCors"; import { taskListToTree } from "~/utils/taskListToTree"; @@ -51,51 +51,15 @@ export async function loader({ request, params }: LoaderArgs) { const query = parsedQuery.data; const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE"; - const take = Math.min(query.take, 50); - const jobRun = await prisma.jobRun.findUnique({ - where: { - id: runId, - }, - select: { - id: true, - status: true, - startedAt: true, - updatedAt: true, - completedAt: true, - environmentId: true, - output: true, - tasks: { - select: { - id: true, - parentId: true, - displayKey: true, - status: true, - name: true, - icon: true, - startedAt: true, - completedAt: true, - params: showTaskDetails, - output: showTaskDetails, - }, - where: { - parentId: query.subtasks ? undefined : null, - }, - orderBy: { - id: "asc", - }, - take: take + 1, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, - }, - statuses: { - select: { key: true, label: true, state: true, data: true, history: true }, - }, - }, + const presenter = new ApiRunPresenter(); + const jobRun = await presenter.call({ + runId: runId, + maxTasks: take, + taskDetails: showTaskDetails, + subTasks: query.subtasks, + cursor: query.cursor, }); if (!jobRun) { diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index 42a2976a5..ad003b10a 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -399,6 +399,22 @@ export class ApiClient { ); } + async cancelRun(runId: string) { + const apiKey = await this.#apiKey(); + + this.#logger.debug("Cancelling Run", { + runId, + }); + + return await zodfetch(GetRunSchema, `${this.#apiUrl}/api/v1/runs/${runId}/cancel`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + }); + } + async getRunStatuses(runId: string) { const apiKey = await this.#apiKey(); diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index 55412d89b..b1766f164 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -642,6 +642,10 @@ export class TriggerClient { return this.#client.getRun(runId, options); } + async cancelRun(runId: string) { + return this.#client.cancelRun(runId); + } + async getRuns(jobSlug: string, options?: GetRunsOptions) { return this.#client.getRuns(jobSlug, options); } From 9a187f9edaa24c47848b0a84ba751f6807e89c6c Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:30:33 +0530 Subject: [PATCH 14/79] chore: upgrade zod to 3.22.3 (#570) * chore: upgrade zod to 3.22.3 * add changeset --- .changeset/lazy-parrots-retire.md | 17 +++ apps/webapp/package.json | 2 +- integrations/airtable/package.json | 2 +- integrations/github/package.json | 4 +- integrations/linear/package.json | 2 +- integrations/replicate/package.json | 2 +- integrations/slack/package.json | 2 +- integrations/stripe/package.json | 2 +- integrations/supabase/package.json | 2 +- integrations/typeform/package.json | 6 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/emails/package.json | 4 +- packages/react/package.json | 2 +- packages/testing/package.json | 2 +- packages/trigger-sdk/package.json | 2 +- perf/package.json | 2 +- pnpm-lock.yaml | 160 +++++++++++++---------- references/job-catalog/package.json | 6 +- references/nextjs-reference/package.json | 10 +- references/unit-testing/package.json | 2 +- 21 files changed, 134 insertions(+), 101 deletions(-) create mode 100644 .changeset/lazy-parrots-retire.md diff --git a/.changeset/lazy-parrots-retire.md b/.changeset/lazy-parrots-retire.md new file mode 100644 index 000000000..7b217b63b --- /dev/null +++ b/.changeset/lazy-parrots-retire.md @@ -0,0 +1,17 @@ +--- +"@trigger.dev/replicate": patch +"@trigger.dev/airtable": patch +"@trigger.dev/supabase": patch +"@trigger.dev/typeform": patch +"@trigger.dev/sdk": patch +"@trigger.dev/github": patch +"@trigger.dev/linear": patch +"@trigger.dev/stripe": patch +"@trigger.dev/slack": patch +"@trigger.dev/testing": patch +"@trigger.dev/react": patch +"@trigger.dev/core": patch +"@trigger.dev/cli": patch +--- + +upgrade zod to 3.22.3 diff --git a/apps/webapp/package.json b/apps/webapp/package.json index a7576143e..5c1be72f7 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -111,7 +111,7 @@ "tailwindcss-animate": "^1.0.5", "tiny-invariant": "^1.2.0", "ulid": "^2.3.0", - "zod": "3.21.4", + "zod": "3.22.3", "zod-error": "1.5.0" }, "devDependencies": { diff --git a/integrations/airtable/package.json b/integrations/airtable/package.json index aa237de00..e3eb7b074 100644 --- a/integrations/airtable/package.json +++ b/integrations/airtable/package.json @@ -29,7 +29,7 @@ "@trigger.dev/integration-kit": "workspace:^2.1.8", "@trigger.dev/sdk": "workspace:^2.1.8", "airtable": "^0.12.1", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/github/package.json b/integrations/github/package.json index 4995951f3..fbe5c0a92 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -29,10 +29,10 @@ "@octokit/request": "^6.2.5", "@octokit/request-error": "^4.0.1", "@octokit/webhooks": "^10.4.0", - "@trigger.dev/sdk": "workspace:^2.1.8", "@trigger.dev/integration-kit": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.8", "octokit": "^2.0.14", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/linear/package.json b/integrations/linear/package.json index 72ffe7b11..9514fc862 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -29,7 +29,7 @@ "@linear/sdk": "^8.0.0", "@trigger.dev/integration-kit": "workspace:^2.1.8", "@trigger.dev/sdk": "workspace:^2.1.8", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/replicate/package.json b/integrations/replicate/package.json index 0995377ec..246ee7614 100644 --- a/integrations/replicate/package.json +++ b/integrations/replicate/package.json @@ -29,7 +29,7 @@ "@trigger.dev/integration-kit": "workspace:^2.1.8", "@trigger.dev/sdk": "workspace:^2.1.8", "replicate": "^0.18.1", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/slack/package.json b/integrations/slack/package.json index 3efe16cb0..ae93464d4 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -26,7 +26,7 @@ "dependencies": { "@slack/web-api": "^6.8.1", "@trigger.dev/sdk": "workspace:^2.1.8", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/stripe/package.json b/integrations/stripe/package.json index e27c5a70d..c09c00e87 100644 --- a/integrations/stripe/package.json +++ b/integrations/stripe/package.json @@ -29,7 +29,7 @@ "@trigger.dev/integration-kit": "workspace:^2.1.8", "@trigger.dev/sdk": "workspace:^2.1.8", "stripe": "^12.14.0", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/supabase/package.json b/integrations/supabase/package.json index 6b54fc361..69a736d6e 100644 --- a/integrations/supabase/package.json +++ b/integrations/supabase/package.json @@ -30,7 +30,7 @@ "@trigger.dev/integration-kit": "workspace:^2.1.8", "@trigger.dev/sdk": "workspace:^2.1.8", "supabase-management-js": "^0.1.4", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=18.0.0" diff --git a/integrations/typeform/package.json b/integrations/typeform/package.json index b0f161675..94473d478 100644 --- a/integrations/typeform/package.json +++ b/integrations/typeform/package.json @@ -25,10 +25,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@typeform/api-client": "^1.8.0", - "@trigger.dev/sdk": "workspace:^2.1.8", "@trigger.dev/integration-kit": "workspace:^2.1.8", - "zod": "3.21.4" + "@trigger.dev/sdk": "workspace:^2.1.8", + "@typeform/api-client": "^1.8.0", + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/packages/cli/package.json b/packages/cli/package.json index b124affdc..3fac1f96c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -81,7 +81,7 @@ "terminal-link": "^3.0.0", "tsconfck": "^2.1.2", "url": "^0.11.1", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/packages/core/package.json b/packages/core/package.json index 2daab5ce0..c3f8407b6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "ulid": "^2.3.0", - "zod": "3.21.4", + "zod": "3.22.3", "zod-error": "1.5.0" }, "devDependencies": { diff --git a/packages/emails/package.json b/packages/emails/package.json index 8f2ce6a45..48f855354 100644 --- a/packages/emails/package.json +++ b/packages/emails/package.json @@ -25,12 +25,12 @@ "react-email": "^1.6.1", "resend": "^0.9.1", "tiny-invariant": "^1.2.0", - "zod": "3.21.4" + "zod": "3.22.3" }, "devDependencies": { - "@types/react": "18.2.17", "@trigger.dev/tsconfig": "workspace:*", "@types/node": "16", + "@types/react": "18.2.17", "typescript": "^4.9.4" }, "engines": { diff --git a/packages/react/package.json b/packages/react/package.json index ba77c43e4..7a90957de 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -29,7 +29,7 @@ "@tanstack/react-query": "5.0.0-beta.2", "@trigger.dev/core": "workspace:^2.1.8", "debug": "^4.3.4", - "zod": "3.21.4" + "zod": "3.22.3" }, "devDependencies": { "@trigger.dev/tsconfig": "workspace:*", diff --git a/packages/testing/package.json b/packages/testing/package.json index 724ca43e5..5265e3ae3 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -38,7 +38,7 @@ "@trigger.dev/core": "workspace:*", "@trigger.dev/sdk": "workspace:*", "vitest": "^0.34.3", - "zod": "3.21.4" + "zod": "3.22.3" }, "devDependencies": { "@trigger.dev/stripe": "workspace:*", diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index 6f5f52d76..5a0195d7e 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -39,7 +39,7 @@ "ulid": "^2.3.0", "uuid": "^9.0.0", "ws": "^8.11.0", - "zod": "3.21.4" + "zod": "3.22.3" }, "devDependencies": { "@trigger.dev/tsconfig": "workspace:*", diff --git a/perf/package.json b/perf/package.json index d62f25191..81ccaf5f4 100644 --- a/perf/package.json +++ b/perf/package.json @@ -22,7 +22,7 @@ "@trigger.dev/stripe": "workspace:*", "@trigger.dev/supabase": "workspace:*", "@trigger.dev/typeform": "workspace:*", - "zod": "3.21.4" + "zod": "3.22.3" }, "devDependencies": { "@trigger.dev/cli": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32090baa9..1be0d4157 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: tsconfig-paths: ^3.14.1 typescript: ^4.8.4 ulid: ^2.3.0 - zod: 3.21.4 + zod: 3.22.3 zod-error: 1.5.0 dependencies: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu @@ -208,7 +208,7 @@ importers: '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 '@conform-to/react': 0.6.1_react@18.2.0 - '@conform-to/zod': 0.6.1_zod@3.21.4 + '@conform-to/zod': 0.6.1_zod@3.22.3 '@godaddy/terminus': 4.12.1 '@headlessui/react': 1.7.8_biqbaboplfbrettd7655fr4n2y '@heroicons/react': 2.0.13_react@18.2.0 @@ -270,7 +270,7 @@ importers: remix-auth-email-link: 1.5.2_xmjsiulzsxcc3znmuhq3turs2q remix-auth-github: 1.3.0_xmjsiulzsxcc3znmuhq3turs2q remix-typedjson: 0.1.7_bhtgrpeaoe6kbm4hb4gzl6x7c4 - remix-utils: 6.0.0_c5pntwu5f7mrfmmvuwtiprk4cy + remix-utils: 6.0.0_7krs2yfztxu2oblf77wwc4rpoe semver: 7.5.0 simple-oauth2: 5.0.0 simplur: 3.0.1 @@ -280,7 +280,7 @@ importers: tailwindcss-animate: 1.0.5_tailwindcss@3.3.2 tiny-invariant: 1.3.1 ulid: 2.3.0 - zod: 3.21.4 + zod: 3.22.3 zod-error: 1.5.0 devDependencies: '@remix-run/dev': 1.19.2-pre.0_36n2i74sizt32vwpdxc4husnkq @@ -353,7 +353,7 @@ importers: devDependencies: eslint: 8.31.0 eslint-config-prettier: 8.6.0_eslint@8.31.0 - eslint-config-turbo: 1.10.14_eslint@8.31.0 + eslint-config-turbo: 1.10.15_eslint@8.31.0 eslint-plugin-react: 7.31.8_eslint@8.31.0 typescript: 4.9.4 @@ -376,12 +376,12 @@ importers: rimraf: ^3.0.2 tsup: 7.1.x typescript: 4.9.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk airtable: 0.12.1 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 16.18.11 @@ -403,7 +403,7 @@ importers: octokit: ^2.0.14 rimraf: ^3.0.2 tsup: ^6.5.0 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@octokit/request': 6.2.5 '@octokit/request-error': 4.0.1 @@ -411,7 +411,7 @@ importers: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk octokit: 2.0.14 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@octokit/types': 9.2.3 '@octokit/webhooks-types': 6.10.0 @@ -430,12 +430,12 @@ importers: rimraf: ^3.0.2 tsup: 7.1.x typescript: 4.9.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@linear/sdk': 8.0.0 '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 16.18.11 @@ -491,12 +491,12 @@ importers: rimraf: ^3.0.2 tsup: 7.1.x typescript: 4.9.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk replicate: 0.18.1 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 16.18.11 @@ -552,11 +552,11 @@ importers: '@types/node': '18' rimraf: ^3.0.2 tsup: ^6.5.0 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@slack/web-api': 6.8.1 '@trigger.dev/sdk': link:../../packages/trigger-sdk - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 18.15.13 @@ -573,12 +573,12 @@ importers: stripe-event-types: ^2.4.0 tsup: 7.1.x typescript: 4.9.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk stripe: 12.14.0 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@types/node': 16.18.11 rimraf: 3.0.2 @@ -597,13 +597,13 @@ importers: supabase-management-js: ^0.1.4 tsup: 7.1.x typescript: 4.9.4 - zod: 3.21.4 + zod: 3.22.3 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.4 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 18.15.13 @@ -620,12 +620,12 @@ importers: rimraf: ^3.0.2 tsup: 7.1.x typescript: 4.9.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk '@typeform/api-client': 1.8.0 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@types/node': 16.18.11 rimraf: 3.0.2 @@ -693,7 +693,7 @@ importers: typescript: ^4.9.5 url: ^0.11.1 vitest: ^0.34.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@types/degit': 2.8.3 boxen: 7.1.1 @@ -720,7 +720,7 @@ importers: terminal-link: 3.0.0 tsconfck: 2.1.2_typescript@4.9.5 url: 0.11.1 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/gradient-string': 1.1.2 @@ -745,11 +745,11 @@ importers: tsup: ^7.1.0 typescript: ^4.9.4 ulid: ^2.3.0 - zod: 3.21.4 + zod: 3.22.3 zod-error: 1.5.0 dependencies: ulid: 2.3.0 - zod: 3.21.4 + zod: 3.22.3 zod-error: 1.5.0 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -794,7 +794,7 @@ importers: resend: ^0.9.1 tiny-invariant: ^1.2.0 typescript: ^4.9.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@react-email/button': 0.0.4 '@react-email/container': 0.0.4 @@ -812,7 +812,7 @@ importers: react-email: 1.6.1_react@18.2.0 resend: 0.9.1 tiny-invariant: 1.3.1 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 16.18.11 @@ -926,12 +926,12 @@ importers: tsup: ^7.1.0 tsx: ^3.12.1 typescript: ^4.8.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@tanstack/react-query': 5.0.0-beta.2_react@18.2.0 '@trigger.dev/core': link:../core debug: 4.3.4 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/debug': 4.1.7 @@ -976,12 +976,12 @@ importers: tsup: ^7.2.0 typescript: ^5.2.2 vitest: ^0.34.3 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/core': link:../core '@trigger.dev/sdk': link:../trigger-sdk vitest: 0.34.4 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/stripe': link:../../integrations/stripe '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -1016,7 +1016,7 @@ importers: ulid: ^2.3.0 uuid: ^9.0.0 ws: ^8.11.0 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/core': link:../core chalk: 5.2.0 @@ -1032,7 +1032,7 @@ importers: ulid: 2.3.0 uuid: 9.0.0 ws: 8.12.0 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/debug': 4.1.7 @@ -1069,7 +1069,7 @@ importers: ts-node: ^10.9.1 tsconfig-paths: ^3.14.1 typescript: ^5.1.6 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/express': link:../packages/express '@trigger.dev/github': link:../integrations/github @@ -1082,7 +1082,7 @@ importers: '@trigger.dev/stripe': link:../integrations/stripe '@trigger.dev/supabase': link:../integrations/supabase '@trigger.dev/typeform': link:../integrations/typeform - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/cli': link:../packages/cli '@trigger.dev/tsconfig': link:../config-packages/tsconfig @@ -1135,7 +1135,7 @@ importers: ts-node: ^10.9.1 tsconfig-paths: ^3.14.1 typescript: 5.1.6 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@clerk/backend': 0.29.2 '@trigger.dev/airtable': link:../../integrations/airtable @@ -1154,7 +1154,7 @@ importers: '@trigger.dev/typeform': link:../../integrations/typeform '@types/node': 20.4.2 typescript: 5.1.6 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/cli': link:../../packages/cli '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -1192,7 +1192,7 @@ importers: react-query: ^3.39.3 ts-loader: ^9.4.2 typescript: 5.0.4 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/eslint-plugin': link:../../packages/eslint-plugin '@trigger.dev/github': link:../../integrations/github @@ -1215,7 +1215,7 @@ importers: react-dom: 18.2.0_react@18.2.0 react-query: 3.39.3_biqbaboplfbrettd7655fr4n2y typescript: 5.0.4 - zod: 3.21.4 + zod: 3.22.3 devDependencies: '@trigger.dev/cli': link:../../packages/cli eslint: 8.42.0 @@ -1349,7 +1349,7 @@ importers: tsconfig-paths: ^3.14.1 typescript: ^5.2.2 vitest: ^0.34.3 - zod: 3.21.4 + zod: 3.22.3 dependencies: '@trigger.dev/sdk': link:../../packages/trigger-sdk '@trigger.dev/stripe': link:../../integrations/stripe @@ -1359,7 +1359,7 @@ importers: tsconfig-paths: 3.14.1 typescript: 5.2.2 vitest: 0.34.4 - zod: 3.21.4 + zod: 3.22.3 packages: @@ -5448,13 +5448,13 @@ packages: react: 18.2.0 dev: false - /@conform-to/zod/0.6.1_zod@3.21.4: + /@conform-to/zod/0.6.1_zod@3.22.3: resolution: {integrity: sha512-VYu44VfVeP0VyMrc2sNBagFAS66luZMIeFOfkHndGs1ep+LcR3Z4D+EOEqLZ2ECexmg9Y6nrX8Y2d5UUigEXCg==} peerDependencies: '@conform-to/dom': 0.6.1 zod: ^3.21.0 dependencies: - zod: 3.21.4 + zod: 3.22.3 dev: false /@cspotcode/source-map-support/0.8.1: @@ -12379,7 +12379,7 @@ packages: '@graphql-typed-document-node/core': 3.2.0_graphql@16.6.0 axios: 1.4.0 graphql: 16.6.0 - zod: 3.21.4 + zod: 3.22.3 transitivePeerDependencies: - debug dev: false @@ -12390,7 +12390,7 @@ packages: '@graphql-typed-document-node/core': 3.2.0_graphql@16.6.0 axios: 1.4.0 graphql: 16.6.0 - zod: 3.21.4 + zod: 3.22.4 transitivePeerDependencies: - debug dev: false @@ -13297,8 +13297,8 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@types/yauzl/2.10.0: - resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} + /@types/yauzl/2.10.1: + resolution: {integrity: sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==} requiresBuild: true dependencies: '@types/node': 20.6.0 @@ -14591,7 +14591,7 @@ packages: yargs-parser: 21.1.1 zod: 3.21.1 optionalDependencies: - sharp: 0.32.5 + sharp: 0.32.6 transitivePeerDependencies: - '@types/node' - less @@ -15606,7 +15606,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /chownr/1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -17697,13 +17697,13 @@ packages: eslint: 8.31.0 dev: true - /eslint-config-turbo/1.10.14_eslint@8.31.0: - resolution: {integrity: sha512-ZeB+IcuFXy1OICkLuAplVa0euoYbhK+bMEQd0nH9+Lns18lgZRm33mVz/iSoH9VdUzl/1ZmFmoK+RpZc+8R80A==} + /eslint-config-turbo/1.10.15_eslint@8.31.0: + resolution: {integrity: sha512-76mpx2x818JZE26euen14utYcFDxOahZ9NaWA+6Xa4pY2ezVKVschuOxS96EQz3o3ZRSmcgBOapw/gHbN+EKxQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.31.0 - eslint-plugin-turbo: 1.10.14_eslint@8.31.0 + eslint-plugin-turbo: 1.10.15_eslint@8.31.0 dev: true /eslint-doc-generator/1.4.3_eslint@8.45.0: @@ -18357,8 +18357,8 @@ packages: - typescript dev: true - /eslint-plugin-turbo/1.10.14_eslint@8.31.0: - resolution: {integrity: sha512-sBdBDnYr9AjT1g4lR3PBkZDonTrMnR4TvuGv5W0OiF7z9az1rI68yj2UHJZvjkwwcGu5mazWA1AfB0oaagpmfg==} + /eslint-plugin-turbo/1.10.15_eslint@8.31.0: + resolution: {integrity: sha512-Tv4QSKV/U56qGcTqS/UgOvb9HcKFmWOQcVh3HEaj7of94lfaENgfrtK48E2CckQf7amhKs1i+imhCsNCKjkQyA==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -18937,7 +18937,7 @@ packages: get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: - '@types/yauzl': 2.10.0 + '@types/yauzl': 2.10.1 transitivePeerDependencies: - supports-color dev: false @@ -19453,6 +19453,14 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true + dev: true + optional: true + + /fsevents/2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true optional: true /fstream/1.0.12: @@ -21393,7 +21401,7 @@ packages: micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /jest-haste-map/29.6.2: @@ -21412,7 +21420,7 @@ packages: micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /jest-leak-detector/29.6.2: @@ -26447,7 +26455,7 @@ packages: react: 18.2.0 dev: false - /remix-utils/6.0.0_c5pntwu5f7mrfmmvuwtiprk4cy: + /remix-utils/6.0.0_7krs2yfztxu2oblf77wwc4rpoe: resolution: {integrity: sha512-S7Xec0YHZxGFEDawWpIbU7HQAZC0j51FmAvcCyBRuxjo71aAIMdmez47dgF8T91yxpHV1xlIKPL7LhBzmYsOZw==} engines: {node: '>=14'} peerDependencies: @@ -26464,7 +26472,7 @@ packages: schema-dts: 1.1.0_typescript@4.9.4 type-fest: 2.19.0 uuid: 8.3.2 - zod: 3.21.4 + zod: 3.22.3 transitivePeerDependencies: - typescript dev: false @@ -26772,7 +26780,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup/3.29.1: @@ -26780,7 +26788,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /rtl-css-js/1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} @@ -27064,8 +27072,8 @@ packages: kind-of: 6.0.3 dev: true - /sharp/0.32.5: - resolution: {integrity: sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==} + /sharp/0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} engines: {node: '>=14.15.0'} requiresBuild: true dependencies: @@ -28893,7 +28901,7 @@ packages: '@esbuild-kit/core-utils': 3.0.0 '@esbuild-kit/esm-loader': 2.5.4 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /tty-table/4.1.6: @@ -29811,7 +29819,7 @@ packages: resolve: 1.22.1 rollup: 3.10.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite/4.1.4_@types+node@18.11.18: @@ -29845,7 +29853,7 @@ packages: resolve: 1.22.1 rollup: 3.10.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite/4.1.4_@types+node@18.17.1: @@ -29879,7 +29887,7 @@ packages: resolve: 1.22.1 rollup: 3.10.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite/4.1.4_@types+node@20.6.0: @@ -29913,7 +29921,7 @@ packages: resolve: 1.22.1 rollup: 3.10.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite/4.4.9: @@ -29948,7 +29956,7 @@ packages: postcss: 8.4.29 rollup: 3.29.1 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /vite/4.4.9_@types+node@18.11.18: resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} @@ -29983,7 +29991,7 @@ packages: postcss: 8.4.29 rollup: 3.29.1 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite/4.4.9_@types+node@20.6.0: @@ -30019,7 +30027,7 @@ packages: postcss: 8.4.29 rollup: 3.29.1 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /vitefu/0.2.4_vite@4.4.9: resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} @@ -30708,7 +30716,7 @@ packages: /zod-error/1.5.0: resolution: {integrity: sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==} dependencies: - zod: 3.21.4 + zod: 3.22.3 dev: false /zod/3.21.1: @@ -30716,6 +30724,14 @@ packages: /zod/3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: false + + /zod/3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + + /zod/3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false /zwitch/2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} diff --git a/references/job-catalog/package.json b/references/job-catalog/package.json index ef34a56c9..3f9c45b07 100644 --- a/references/job-catalog/package.json +++ b/references/job-catalog/package.json @@ -33,8 +33,10 @@ "@trigger.dev/airtable": "workspace:*", "@trigger.dev/express": "workspace:*", "@trigger.dev/github": "workspace:*", + "@trigger.dev/linear": "workspace:*", "@trigger.dev/openai": "workspace:*", "@trigger.dev/plain": "workspace:*", + "@trigger.dev/replicate": "workspace:*", "@trigger.dev/resend": "workspace:*", "@trigger.dev/sdk": "workspace:*", "@trigger.dev/sendgrid": "workspace:*", @@ -44,9 +46,7 @@ "@trigger.dev/typeform": "workspace:*", "@types/node": "20.4.2", "typescript": "5.1.6", - "zod": "3.21.4", - "@trigger.dev/linear": "workspace:*", - "@trigger.dev/replicate": "workspace:*" + "zod": "3.22.3" }, "trigger.dev": { "endpointId": "job-catalog" diff --git a/references/nextjs-reference/package.json b/references/nextjs-reference/package.json index f4ea60bcb..5e0b11963 100644 --- a/references/nextjs-reference/package.json +++ b/references/nextjs-reference/package.json @@ -10,6 +10,7 @@ "generate:types": "npx supabase gen types typescript --project-id axtbanoixaztvdntngew --schema public --schema public_2 > src/supabase.types.ts" }, "dependencies": { + "@trigger.dev/eslint-plugin": "workspace:*", "@trigger.dev/github": "workspace:*", "@trigger.dev/nextjs": "workspace:*", "@trigger.dev/openai": "workspace:*", @@ -17,9 +18,11 @@ "@trigger.dev/react": "workspace:*", "@trigger.dev/resend": "workspace:*", "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/sendgrid": "workspace:*", "@trigger.dev/slack": "workspace:*", + "@trigger.dev/stripe": "workspace:*", + "@trigger.dev/supabase": "workspace:*", "@trigger.dev/typeform": "workspace:*", - "@trigger.dev/eslint-plugin": "workspace:*", "@types/node": "18.15.13", "@types/react": "18.2.17", "@types/react-dom": "18.2.7", @@ -28,10 +31,7 @@ "react-dom": "^18.2.0", "react-query": "^3.39.3", "typescript": "5.0.4", - "zod": "3.21.4", - "@trigger.dev/supabase": "workspace:*", - "@trigger.dev/stripe": "workspace:*", - "@trigger.dev/sendgrid": "workspace:*" + "zod": "3.22.3" }, "devDependencies": { "@trigger.dev/cli": "workspace:*", diff --git a/references/unit-testing/package.json b/references/unit-testing/package.json index a827d24bd..bb42c57f6 100644 --- a/references/unit-testing/package.json +++ b/references/unit-testing/package.json @@ -19,6 +19,6 @@ "tsconfig-paths": "^3.14.1", "typescript": "^5.2.2", "vitest": "^0.34.3", - "zod": "3.21.4" + "zod": "3.22.3" } } \ No newline at end of file From c17faabe9fb615ecacdcfa12063a206aed48718b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Oct 2023 11:21:14 +0100 Subject: [PATCH 15/79] Stringify event payload and context before serialization This fixes an issue where an event payload was being serialized through remix-typedjson and was causing issues with incorrect meta keys and so deserialization was failing. See https://github.com/kiliman/remix-typedjson/pull/33 for more --- .../webapp/app/components/run/RunOverview.tsx | 4 +-- apps/webapp/app/components/run/TaskCard.tsx | 4 +-- .../app/components/run/TriggerDetail.tsx | 7 +++-- .../app/presenters/RunPresenter.server.ts | 28 +++++++++++++++---- .../TriggerDetailsPresenter.server.ts | 17 +++++++++-- .../tasks/performTaskOperation.server.ts | 2 -- 6 files changed, 47 insertions(+), 15 deletions(-) diff --git a/apps/webapp/app/components/run/RunOverview.tsx b/apps/webapp/app/components/run/RunOverview.tsx index 79bdfe22b..f9086d3c6 100644 --- a/apps/webapp/app/components/run/RunOverview.tsx +++ b/apps/webapp/app/components/run/RunOverview.tsx @@ -12,7 +12,7 @@ import { import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database"; import { useMemo } from "react"; import { usePathName } from "~/hooks/usePathName"; -import { Run } from "~/presenters/RunPresenter.server"; +import { ViewRun } from "~/presenters/RunPresenter.server"; import { cancelSchema } from "~/routes/resources.runs.$runId.cancel"; import { schema } from "~/routes/resources.runs.$runId.rerun"; import { formatDuration } from "~/utils"; @@ -59,7 +59,7 @@ import { TaskCard } from "./TaskCard"; import { TaskCardSkeleton } from "./TaskCardSkeleton"; type RunOverviewProps = { - run: Run; + run: ViewRun; trigger: { icon: string; title: string; diff --git a/apps/webapp/app/components/run/TaskCard.tsx b/apps/webapp/app/components/run/TaskCard.tsx index c1a399d38..1bce2f482 100644 --- a/apps/webapp/app/components/run/TaskCard.tsx +++ b/apps/webapp/app/components/run/TaskCard.tsx @@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Fragment, useState } from "react"; import simplur from "simplur"; import { Paragraph } from "~/components/primitives/Paragraph"; -import { Task } from "~/presenters/RunPresenter.server"; +import { ViewTask } from "~/presenters/RunPresenter.server"; import { formatDuration } from "~/utils"; import { cn } from "~/utils/cn"; import { @@ -22,7 +22,7 @@ import { } from "./RunCard"; import { TaskStatusIcon } from "./TaskStatus"; -type TaskCardProps = Task & { +type TaskCardProps = ViewTask & { selectedId?: string; selectedTask: (id: string) => void; isLast: boolean; diff --git a/apps/webapp/app/components/run/TriggerDetail.tsx b/apps/webapp/app/components/run/TriggerDetail.tsx index 7f5dd41e8..aee57b22e 100644 --- a/apps/webapp/app/components/run/TriggerDetail.tsx +++ b/apps/webapp/app/components/run/TriggerDetail.tsx @@ -25,7 +25,7 @@ export function TriggerDetail({ }; properties: DisplayProperty[]; }) { - const { id, name, payload, timestamp, deliveredAt } = trigger; + const { id, name, payload, context, timestamp, deliveredAt } = trigger; return ( @@ -45,6 +45,7 @@ export function TriggerDetail({ /> )} + {trigger.externalAccount && ( )} Payload - + + Context +

diff --git a/apps/webapp/app/presenters/RunPresenter.server.ts b/apps/webapp/app/presenters/RunPresenter.server.ts index 369ce882e..34934647d 100644 --- a/apps/webapp/app/presenters/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/RunPresenter.server.ts @@ -13,10 +13,11 @@ type RunOptions = { userId: string; }; -export type Run = NonNullable>>; -export type Task = NonNullable>>["tasks"][number]; -export type Event = NonNullable>>["event"]; +export type ViewRun = NonNullable>>; +export type ViewTask = NonNullable>>["tasks"][number]; +export type ViewEvent = NonNullable>>["event"]; +type QueryEvent = NonNullable>>["event"]; type QueryTask = NonNullable>>["tasks"][number]; export class RunPresenter { @@ -76,7 +77,7 @@ export class RunPresenter { type: run.environment.type, slug: run.environment.slug, }, - event: run.event, + event: this.#prepareEventData(run.event), tasks, runConnections: run.runConnections, missingConnections: run.missingConnections, @@ -84,6 +85,22 @@ export class RunPresenter { }; } + #prepareEventData(event: QueryEvent) { + return { + id: event.eventId, + name: event.name, + payload: JSON.stringify(event.payload), + context: JSON.stringify(event.context), + timestamp: event.timestamp, + deliveredAt: event.deliveredAt, + externalAccount: event.externalAccount + ? { + identifier: event.externalAccount.identifier, + } + : undefined, + }; + } + query({ id, userId }: RunOptions) { return this.#prismaClient.jobRun.findFirst({ select: { @@ -110,9 +127,10 @@ export class RunPresenter { }, event: { select: { - id: true, + eventId: true, name: true, payload: true, + context: true, timestamp: true, deliveredAt: true, externalAccount: { diff --git a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts index d4b6fa6e3..7c420ad5d 100644 --- a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts +++ b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts @@ -17,9 +17,10 @@ export class TriggerDetailsPresenter { select: { event: { select: { - id: true, + eventId: true, name: true, payload: true, + context: true, timestamp: true, deliveredAt: true, externalAccount: { @@ -32,6 +33,18 @@ export class TriggerDetailsPresenter { }, }); - return event; + return { + id: event.eventId, + name: event.name, + payload: JSON.stringify(event.payload), + context: JSON.stringify(event.context), + timestamp: event.timestamp, + deliveredAt: event.deliveredAt, + externalAccount: event.externalAccount + ? { + identifier: event.externalAccount.identifier, + } + : undefined, + }; } } diff --git a/apps/webapp/app/services/tasks/performTaskOperation.server.ts b/apps/webapp/app/services/tasks/performTaskOperation.server.ts index fb5a6304a..987cb56fa 100644 --- a/apps/webapp/app/services/tasks/performTaskOperation.server.ts +++ b/apps/webapp/app/services/tasks/performTaskOperation.server.ts @@ -1,5 +1,3 @@ -import { env } from "process"; -import { Run } from "~/presenters/RunPresenter.server"; import { FetchOperationSchema, FetchRequestInit, From 0df6410561cfe5fd8718ca92f493d8e74cd802d4 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Oct 2023 11:53:12 +0100 Subject: [PATCH 16/79] Need to properly format the payload JSON --- apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts index 7c420ad5d..7a93e77ac 100644 --- a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts +++ b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts @@ -36,8 +36,8 @@ export class TriggerDetailsPresenter { return { id: event.eventId, name: event.name, - payload: JSON.stringify(event.payload), - context: JSON.stringify(event.context), + payload: JSON.stringify(event.payload, null, 2), + context: JSON.stringify(event.context, null, 2), timestamp: event.timestamp, deliveredAt: event.deliveredAt, externalAccount: event.externalAccount From a847b492bc54b41b8b850865195d90554125d680 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 6 Oct 2023 12:27:15 +0100 Subject: [PATCH 17/79] NestJS framework suport (from @H4ad) (#574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: added nestjs package adapter * Latest lockfile * Updated Astro setup docs: env import * Astro docs improvements * Docs: improved the limitations * Bumped package versions to 2.1.3 * Updated the Astro docs with SSR notes * test/368/use vitest instead of jest (#470) * chore: add vitest dependencies * refactor: replace jest by vitest * test: disable broken test * Update pnpm-lock.yaml * Update pnpm-lock.yaml --------- Co-authored-by: Matt Aitken * Created Temp for pull req, bugreport & feature req * A few teaks to the templates * Added instructions for how to do Changeset snapshots * Redirect people to discord to ask a question * Documentation Update: Added instead of triggerdotdev to avoid confusion while cloning the repository (#477) * Added a link to the homepage from the side menu (#479) * Added 2 new named icons * New side menu link to the homepage * Updated lock file * Removed un-used import * Renamed the examples dir to references (true examples are in another repo and this was confusing) * Add a references README * Fixes broken pnpm lock file * Improve the Astro manual setup guide * Fixed cal.com link * Upgrade to the latest remix (pre v2) * React status hooks (#493) * Added stripInternal to SDK tsconfig * Statuses can now be set from a run, and are stored in the database * Added the key to the returned status * Made the test job have an extra step and only pass in some of the options * client.getRunStatuses() and the corresponding endpoint * client.getRun() now includes status info * Fixed circular dependency schema * Translate null to undefined * Added the react package to the nextjs-reference tsconfig * Removed unused OpenAI integration from nextjs-reference project * New hooks for getting the statuses * Disabled most of the nextjs-reference jobs * Updated the hooks UI * Updated the endpoints to deal with null statuses values * The hook is working, with an example * Changeset: “You can create statuses in your Jobs that can then be read using React hooks” * Changeset config is back to the old changelog style * WIP on new React hooks guide * Guide docs for the new hooks * Added the status hooks to the React hooks guide * Removed the links to the status hooks reference for now * Re-ordered the hooks * Fix for an error in the docs * Set a default of a blank array for the GetRunSchema * Fixed dependency * Revert "Upgrade to the latest remix (pre v2)" This reverts commit 4edc7112abd014c1ea37983127a7982e78695823. * Update introduction.mdx (#498) Changed https://github.com/triggerdotdev/examples/tree/main/resend (which was a 404) to https://github.com/triggerdotdev/examples/tree/main/resend-email-form * Use the bell icon for the new status Tasks * Going exponential with `Linear` (#478) * Unleash GPT magic * Clean up after GPT * All the hooks * Provisional integration catalog entry * Sample webhook jobs * Attachments with alpha warnings * Remove some verbose logs * Fix IP restrictions * Remove tunnel * Revert "Remove tunnel" This reverts commit c5b69ce6524e3b40c26b66cdc56e087b8576b8c6. * Resolve event name clashes * Remove circular dependency * Use correct payload uuid * Schema fixes * Fix webhook event name * Start to Linearify catalog entry * Remove todo * More catalog updates * Make OAuth work * Rename webhook helper * Schema juggling * More discrimination * Add Issue SLA event * Simplify triggers * Handle rate limits * Fix Project schema * Payload examples * Improve event props * Remove redundant source metadata * One type to rule them all * Recursive WithoutFunctions type * Linear output serializer * Some tasks * Update catalog entry * Dynamic usage sample * Bump version * Remove tunnel * More tasks * Add optional skipRetrying on runTask errors * Fail fast on user errors * Entity getter tasks * Another couple of tasks * Token to apiKey * Sort tasks * Add filtered issue SLA triggers * Add docs * Type fixes * Job catalog examples * Serialization helper docs * Add changeset * Refactor webhooks * Enhance properties * Pagination helper and docs * Clean up imports * Change misc catalog job --------- Co-authored-by: Matt Aitken * Use absolute image paths (#490) * Update sendevent.mdx * fix: Fail client-side on invalid Stripe event names (#492) * Parse event names * Add changeset * feat: BYO Auth (#491) * feat: BYO Auth Define client-side auth resolvers to be able to supply custom authentication credentials for integrations before a run is performed - Added new defineAuthResolver - Update all integrations to support the new auth resolvers - Strip internal symbols from .d.ts in integrations and trigger-sdk - Added BYO Auth docs - Update Dynamic Schedule to support associated account IDs - Create external accounts just-in-time - Added Account ID field to test job when there are external auth integrations - Show Account ID on run dashboard - Added new Run error state called “Unresolved auth” * Added changeset * Remove @internal from TriggerIntegration public methods * Add void to the result union * DynamicTriggers now work with the new BYO auth system, and added a bunch of docs and docs changes * Add additional key material for registering dynamic trigger task * Add new define* instance methods to the overview * CLI now supports multiple frameworks (with tests) (#480) * Early work defining CLI framework support * WIP moving CLI init logic to the Framework class * Installing files should now work for Nextjs * Some fixes * WIP creating unit tests for Next.js project detection * Delete old jest config * Latest lockfile * Detect use of src directory test * Tests for detection pages/app directory * Correct detection of Next.js project * Renamed test file * Create install files from template files with replacements. With tests * Added multiple uses of the same replacement * Created a test for the install step (it fails right now with JS) * Removed unused import * Another test that should pass but currently fails… * Path alias fixed and now has tests * New pathAlias function used * Nextjs page install tests * Fixed app directory install (with tests) * Removed e2e CLI test, switched to unit testing strategy instead * Latest lockfile * The install files are now actual files that are copied and transformed * Got the template files working correctly after building * Next steps are now framework specific * createFileFromTemplate now works with a path again. Uses mock if specified. * Renamed apiRoute.js to pagesApiRoute.js * Simplified pages file generation * Next.js app API route template * Next.js App routing support, with common files logic shared * Dev command now uses framework default values if they exist and aren’t overridden * Unused import * pathAlias now works for all frameworks * Added a test to detect Next from the next.config.js * WIP on Remix framework support * Tests for Remix install * Replaced references to Next.js * Use a green ✔️ instead of ✅ in the CLI * Support for multiple hostnames * Tunneling can now use the hostname and port * Work on multiple ports * Improved the error messages. Added some extra pots to Next.js * Update the Remix templates to have .server in the imports * Remix updated to use server-runtime instead of node. Node v18+ * Frameworks can specify the watch paths and ignore paths * Define the watch variables above, so we can easily log them for debugging * Don’t wait for outdated package checking when running the dev command * Improved the Remix manual setup guide * Rewriting docs for quickstart * Updated the Next.js quickstart * Remix quick start * Added a changeset * Improved the Next.js manual setup * Tweaked the Linear scopes * Latest lockfile * Fix for getPathAlias typecheck failure * Linear getAll type error (weirdly not in VSCode…) and removed the pagination example that uses the SDK as won’t work with timeouts * Add BYO auth for oauth options * Add back in Job.toJSON to fix the testing package * Removed dynamicTrigger @internal from toJSON * The CLI now checks for a dev server API key in init and dev commands * Remix onboarding now uses the CLI init command * Decouple zod (#500) Zod Schemas is no longer required for validating/inferring event triggers. We’ve taken inspiration from how domain-functions did it: https://github.com/seasonedcc/domain-functions/pull/114 * chore: Update version for release (#481) Co-authored-by: github-actions[bot] * Latest lockfile * feat: Basic usage dashboard to show run volume (#501) * New usage dashboard with static data * Implement org usage dash * Grab chart data for the last 12 months * If no org is found just return undefined so a 404 will be shown * Remove mock data * Fill in missing months with 0s --------- Co-authored-by: James Ritchie * Fixed duplicate end month * hotfix * hotfix 2 * Astro CLI support (#506) * Astro framework CLI support * Changeset: Added Astro automatic installation * Fixed the package name – was remix, now astro * Fixed the export of the example * Added IPv6 localhost to Astro hostnames * Updated Astro onboarding to show the CLI init command, instead of manual instructions * Astro quickstart * Fix for type in Remix quickstart * Next.js framework detection allows different config file extensions and “next” devDependency * Made the dev command port more general so it works with various frameworks * chore: Update version for release (#508) Co-authored-by: github-actions[bot] * Fix for incorrectly named Next.js package in manual setup * Latest lockfile * Increased the intervalTrigger max from 1 day to 30 days * Implement the task output redacting to prevent redacted values from showing in the logs * Express frameworks docs + CLI (#512) * Manual setup docs * Added the onboarding * The emails package now works with Node > 18 * Added CLi support for Express, including custom init command finished messages * Need to actually log out the installation complete message… * Fix for an old Remix reference * Renamed the page export * Use resolvedOptions.triggerUrl * Typo in manual instructions * chore: Update version for release (#513) Co-authored-by: github-actions[bot] * Latest lockfile * Add the STAGING environment by default * Fixed sparodically failed run creations - Use a better way of getting the latest job run number to increment - Make the CreateRunService transaction more reliable - Invoke dispatchers in parallel - No longer swallow prisma errors in $transaction * Swapped out the Homepage link in the side menu for a link to the Changelog * Youtube embedded video fits its aspect ratio instead of going full width * Improved CLI init Next.js middleware detection * CLI init: adds public key as “TRIGGER_PUBLIC_API_KEY” except for Next which overrides this * Updated the docs for the React hooks * chore: Update version for release (#521) Co-authored-by: github-actions[bot] * Display framework logos on the onboarding setup pages (#519) * autofocus the search field on the Job page * Add some documentation around canceling scheduled events * Updated outdated lockfile * Updated docs README.md * Improves the performance of run resuming (#522) * Improves the perform of run resuming When runs resume, we try and make sure that tasks that have already been completed are cached and reused. Worst case scenario the client needs to hit the API server once for a non-cached task that is indeed completed on the server, but this can get pretty expensive when there are a larger number of tasks. This commit does 2 different things to help: - noop tasks are no longer “cached” using the cachedTasks strategy, instead their idempotency keys are shoved into a bloom filter and the client tests for their inclusion in the bloom filter before running them (since they don’t have any concept of output, this works) - Additional cached tasks are lazy loaded when a task is run. This allows us to progressively fetch additional tasks to be cached on the client, which will cut down on cache misses by a decent amount * Create warm-carrots-float.md * Make io.yield backwards compat with older platform versions * Better support old clients connecting to server versions that support lazy loading cached tasks * Fixed type errors when settings headers with unknown value * Better yield not support error message * Rename _version to _serverVersion to be more clear * Add typed filters to `Linear` getAll helper (#517) * Fix getAll params type * Changeset * Search param types * Update changeset * `Replicate` integration and remote callbacks (#507) * Support tasks with remote callbacks * Add common integration tsconfig * Add Replicate integration * Basic job catalog example * Integration catalog entry * Check for callbackUrl during executeTask * Fix getAll * Improve JSDoc * Bump version * Remove named queue * Simplify runTask types * Trust the types * Fail tasks on timeout * Callback timeout as param * Mess with types * performRunExecutionV1 * Update runTask docs * Shorten callback task methods * Fix run method return type * Image processing jobs * Replicate docs * Text output example * Changeset * Version bump * Roll back ugly types * Remove missing types * Quicker return when waiting on remote callback * Remote callback example * Bump version * Remove schema parsing * Only schedule positive callback timeout * Decrease callback secret length * Explicit default timeouts * Import deployments tasks * JSDoc * Deployments docs * Fix runTask examples, mention wrappers --------- Co-authored-by: Eric Allam * Update pnpm lock file * Allow blank issues * chore: Update version for release (#538) Co-authored-by: github-actions[bot] * Fix pnpm lock file * feat: New Test page (#558) * Improvements to the shortcut keys * Updated code mirror theme to the new dark mode style * Removed the old test page help panel copy * WIP new structure for the test page * Added some of the side panel options for examples and account ID * Conditionally hide the side panel components if they’re blank * Example with selected state working * Allow selecting the example again to overwrite any edits * Previous run payloads working * Select a recent payload if there’s no example * Created Icon and DetailCell components * The integrations now use the DetailCell * Use DetailCell on the integrations page * Added support for description to DetailCell. With proper variants now * DateTimeAccurate and formatDateTimeAccurate weren’t displaying correctly. fractionalSecondDigits isn’t in the types, but is supported for 92.5% of users * DetailCell now allows label and description to be React components * Using the DetailCell on the Test page * Styled the CodeMirror scrollbars to match elsewhere * Adding copy and clear to the JSONEditor, not working properly yet though * Removed the copy/clear buttons from the Test route * Delete the light color theme * The editor now supports copying/clearing etc * Made it clear when text is copied * Changed the learn link to a tertiary button * Fixed CMD shortcut keys * Run test button now works with ⌘ Enter * CodeMirror now allows ⌘Enter to escape the field * Added JSON linter to the test editor * Fix for buttons not being aligned correctly * Copy/clear buttons now aligned * Made the integration DetailCell text smaller * The test recent payloads now have colored text to help identify the status * DetailCell descriptions are dimmer * Added link to docs for BYOA * Fix for buttons going full width when they shouldn’t --------- Co-authored-by: James Ritchie * chore: update doc title for groups (#539) * chore: update doc title for groups * update docs for sdk and integrations * Add instructions for triggering job runs to the job catalog readme * Updated the Test docs * Removed a console.log when a user’s file is changed that CLI dev is listening for * feat: allow cancelling jobs from trigger-client sdk (#562) * feat: allow cancelling jobs from trigger-client sdk * use presenter instead of service for non-mutating logic * chore: upgrade zod to 3.22.3 (#570) * chore: upgrade zod to 3.22.3 * add changeset * Created a changeset with the correct starting package version * Added headers to NestJS response * Added some types to the NestJS project * Added fastify types * Removed log from CodeMirror * Added InstallPackages component * Improved the NestJS onboarding instructions * Updated the onboarding instructions * Made all the quickstart framework cards snippets, so the page isn’t a nightmare to edit * NestJS docs updates * Added dotenv to the earlier code sample * Latest lockfile * Moved the nestjs-example to the reference folder --------- Co-authored-by: Vinícius Lourenço Co-authored-by: Wesley <100464352+ologbonowiwi@users.noreply.github.com> Co-authored-by: Vishesh Rawal <92795514+visheshrwl@users.noreply.github.com> Co-authored-by: Eric Allam Co-authored-by: Aniket Bindhani Co-authored-by: James Ritchie Co-authored-by: D-K-P Co-authored-by: Gregory <93215236+gjohnsx@users.noreply.github.com> Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com> Co-authored-by: Eric Allam Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: James Ritchie Co-authored-by: Hemachandar <132386067+hmacr@users.noreply.github.com> --- .changeset/fair-waves-hug.md | 5 + .../app/components/code/InstallPackages.tsx | 44 + .../app/components/code/codeMirrorSetup.ts | 1 - .../frameworks/FrameworkSelector.tsx | 2 +- .../route.tsx | 223 ++- docs/_snippets/frameworks/card-astro.mdx | 64 + docs/_snippets/frameworks/card-express.mdx | 24 + docs/_snippets/frameworks/card-fastify.mdx | 52 + docs/_snippets/frameworks/card-nestjs.mdx | 23 + docs/_snippets/frameworks/card-nextjs.mdx | 22 + docs/_snippets/frameworks/card-nuxt.mdx | 28 + docs/_snippets/frameworks/card-redwood.mdx | 49 + docs/_snippets/frameworks/card-remix.mdx | 206 +++ docs/_snippets/frameworks/card-supabase.mdx | 132 ++ docs/_snippets/frameworks/card-sveltekit.mdx | 36 + docs/_snippets/manual-setup-nestjs.mdx | 264 +++ .../concepts/client-adaptors.mdx | 1 + docs/documentation/guides/manual/nestjs.mdx | 7 + .../quickstarts/introduction.mdx | 262 +-- docs/documentation/quickstarts/nestjs.mdx | 7 + docs/mint.json | 1 + packages/nestjs/LICENSE | 21 + packages/nestjs/README.md | 7 + packages/nestjs/package.json | 48 + packages/nestjs/src/index.ts | 216 +++ packages/nestjs/tsconfig.json | 11 + packages/nestjs/tsup.config.ts | 19 + pnpm-lock.yaml | 1641 ++++++++++++++++- references/nestjs-example/.env.example | 3 + references/nestjs-example/.eslintrc.js | 25 + references/nestjs-example/.gitignore | 38 + references/nestjs-example/.prettierrc | 4 + references/nestjs-example/README.md | 73 + references/nestjs-example/nest-cli.json | 8 + references/nestjs-example/package.json | 72 + .../nestjs-example/src/app.controller.ts | 31 + references/nestjs-example/src/app.module.ts | 24 + references/nestjs-example/src/main.ts | 8 + .../nestjs-example/test/app.e2e-spec.ts | 24 + references/nestjs-example/test/jest-e2e.json | 9 + references/nestjs-example/tsconfig.build.json | 4 + references/nestjs-example/tsconfig.json | 21 + 42 files changed, 3404 insertions(+), 356 deletions(-) create mode 100644 .changeset/fair-waves-hug.md create mode 100644 apps/webapp/app/components/code/InstallPackages.tsx create mode 100644 docs/_snippets/frameworks/card-astro.mdx create mode 100644 docs/_snippets/frameworks/card-express.mdx create mode 100644 docs/_snippets/frameworks/card-fastify.mdx create mode 100644 docs/_snippets/frameworks/card-nestjs.mdx create mode 100644 docs/_snippets/frameworks/card-nextjs.mdx create mode 100644 docs/_snippets/frameworks/card-nuxt.mdx create mode 100644 docs/_snippets/frameworks/card-redwood.mdx create mode 100644 docs/_snippets/frameworks/card-remix.mdx create mode 100644 docs/_snippets/frameworks/card-supabase.mdx create mode 100644 docs/_snippets/frameworks/card-sveltekit.mdx create mode 100644 docs/_snippets/manual-setup-nestjs.mdx create mode 100644 docs/documentation/guides/manual/nestjs.mdx create mode 100644 docs/documentation/quickstarts/nestjs.mdx create mode 100644 packages/nestjs/LICENSE create mode 100644 packages/nestjs/README.md create mode 100644 packages/nestjs/package.json create mode 100644 packages/nestjs/src/index.ts create mode 100644 packages/nestjs/tsconfig.json create mode 100644 packages/nestjs/tsup.config.ts create mode 100644 references/nestjs-example/.env.example create mode 100644 references/nestjs-example/.eslintrc.js create mode 100644 references/nestjs-example/.gitignore create mode 100644 references/nestjs-example/.prettierrc create mode 100644 references/nestjs-example/README.md create mode 100644 references/nestjs-example/nest-cli.json create mode 100644 references/nestjs-example/package.json create mode 100644 references/nestjs-example/src/app.controller.ts create mode 100644 references/nestjs-example/src/app.module.ts create mode 100644 references/nestjs-example/src/main.ts create mode 100644 references/nestjs-example/test/app.e2e-spec.ts create mode 100644 references/nestjs-example/test/jest-e2e.json create mode 100644 references/nestjs-example/tsconfig.build.json create mode 100644 references/nestjs-example/tsconfig.json diff --git a/.changeset/fair-waves-hug.md b/.changeset/fair-waves-hug.md new file mode 100644 index 000000000..68adfefae --- /dev/null +++ b/.changeset/fair-waves-hug.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/nestjs": patch +--- + +First release of NestJS adaptor diff --git a/apps/webapp/app/components/code/InstallPackages.tsx b/apps/webapp/app/components/code/InstallPackages.tsx new file mode 100644 index 000000000..791d101da --- /dev/null +++ b/apps/webapp/app/components/code/InstallPackages.tsx @@ -0,0 +1,44 @@ +import { + ClientTabs, + ClientTabsList, + ClientTabsTrigger, + ClientTabsContent, +} from "../primitives/ClientTabs"; +import { ClipboardField } from "../primitives/ClipboardField"; + +type InstallPackagesProps = { + packages: string[]; +}; + +export function InstallPackages({ packages }: InstallPackagesProps) { + return ( + + + npm + pnpm + yarn + + + + + + + + + + + + ); +} diff --git a/apps/webapp/app/components/code/codeMirrorSetup.ts b/apps/webapp/app/components/code/codeMirrorSetup.ts index 493196727..89988c3d9 100644 --- a/apps/webapp/app/components/code/codeMirrorSetup.ts +++ b/apps/webapp/app/components/code/codeMirrorSetup.ts @@ -28,7 +28,6 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A { key: "Mod-Enter", run: () => { - console.log("Mod-Enter"); return true; }, preventDefault: false, diff --git a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx index 0edbf578e..8302e350d 100644 --- a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx +++ b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx @@ -72,7 +72,7 @@ export function FrameworkSelector() { - +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx index 255f31340..d13b547a0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx @@ -1,21 +1,220 @@ -import { NestjsLogo } from "~/assets/logos/NestjsLogo"; -import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; +import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; +import invariant from "tiny-invariant"; +import { Feedback } from "~/components/Feedback"; +import { PageGradient } from "~/components/PageGradient"; +import { StepContentContainer } from "~/components/StepContentContainer"; +import { InlineCode } from "~/components/code/InlineCode"; +import { InstallPackages } from "~/components/code/InstallPackages"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Header1 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { StepNumber } from "~/components/primitives/StepNumber"; +import { useAppOrigin } from "~/hooks/useAppOrigin"; +import { useDevEnvironment } from "~/hooks/useEnvironments"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { Handle } from "~/utils/handle"; -import { trimTrailingSlash } from "~/utils/pathBuilder"; +import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; +import { CodeBlock } from "../../components/code/CodeBlock"; +import { TriggerDevStep } from "~/components/SetupCommands"; export const handle: Handle = { - breadcrumb: (match) => , + breadcrumb: (match) => , }; -export default function Page() { +const AppModuleCode = ` +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TriggerDevModule } from '@trigger.dev/nestjs'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TriggerDevModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + id: 'my-nest-app', + apiKey: config.getOrThrow('TRIGGER_API_KEY'), + apiUrl: config.getOrThrow('TRIGGER_API_URL'), + verbose: false, + ioLogLocalEnabled: true, + }), + }), + ], +}) +export class AppModule {} +`; + +const JobControllerCode = ` +import { Controller, Get } from '@nestjs/common'; +import { InjectTriggerDevClient } from '@trigger.dev/nestjs'; +import { eventTrigger, TriggerClient } from '@trigger.dev/sdk'; + +@Controller() +export class JobController { + constructor( + @InjectTriggerDevClient() private readonly client: TriggerClient, + ) { + this.client.defineJob({ + id: 'test-job', + name: 'Test Job One', + version: '0.0.1', + trigger: eventTrigger({ + name: 'test.event', + }), + run: async (payload, io, ctx) => { + await io.logger.info('Hello world!', { payload }); + + return { + message: 'Hello world!', + }; + }, + }); + } + + @Get() + getHello(): string { + return \`Running Trigger.dev with client-id \${this.client.id}\`; + } +}`; + +const AppModuleWithControllerCode = ` +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TriggerDevModule } from '@trigger.dev/nestjs'; +import { JobController } from './job.controller'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TriggerDevModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + id: 'my-nest-app', + apiKey: config.getOrThrow('TRIGGER_API_KEY'), + apiUrl: config.getOrThrow('TRIGGER_API_URL'), + verbose: false, + ioLogLocalEnabled: true, + }), + }), + ], + controllers: [ + //...existingControllers, + JobController + ], +}) +export class AppModule {} +`; + +const packageJsonCode = `"trigger.dev": { + "endpointId": "my-nest-app" +}`; + +export default function SetupNestJS() { + const organization = useOrganization(); + const project = useProject(); + useProjectSetupComplete(); + const devEnvironment = useDevEnvironment(); + const appOrigin = useAppOrigin(); + + invariant(devEnvironment, "devEnvironment is required"); + return ( - - - + +
+
+ + Get setup in 2 minutes + +
+ + Choose a different framework + + + I'm stuck! + + } + defaultValue="help" + /> +
+
+ <> + + + + + + + + Inside your .env file, create the following env variables: + + + + + + + Now, go to your app.module.ts and add the{" "} + TriggerDevModule: + + + + + + + Create a controller called{" "} + job.controller.ts and add the following code: + + + + + + + Now, add the new controller to your{" "} + app.module.ts: + + + + + + + Now, add this to the top-level of your package.json: + + + + + + + Finally, run your project with npm run start: + + + + + + + + + This page will automatically refresh. + + +
+
); } diff --git a/docs/_snippets/frameworks/card-astro.mdx b/docs/_snippets/frameworks/card-astro.mdx new file mode 100644 index 000000000..6db902f93 --- /dev/null +++ b/docs/_snippets/frameworks/card-astro.mdx @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/astro" +/> diff --git a/docs/_snippets/frameworks/card-express.mdx b/docs/_snippets/frameworks/card-express.mdx new file mode 100644 index 000000000..3fc9f18c1 --- /dev/null +++ b/docs/_snippets/frameworks/card-express.mdx @@ -0,0 +1,24 @@ + + + + + + + + + + + } + href="/documentation/quickstarts/express" +/> diff --git a/docs/_snippets/frameworks/card-fastify.mdx b/docs/_snippets/frameworks/card-fastify.mdx new file mode 100644 index 000000000..09ad6d051 --- /dev/null +++ b/docs/_snippets/frameworks/card-fastify.mdx @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/fastify" +/> diff --git a/docs/_snippets/frameworks/card-nestjs.mdx b/docs/_snippets/frameworks/card-nestjs.mdx new file mode 100644 index 000000000..84aa84537 --- /dev/null +++ b/docs/_snippets/frameworks/card-nestjs.mdx @@ -0,0 +1,23 @@ + + + + + } + href="/documentation/quickstarts/nestjs" +/> diff --git a/docs/_snippets/frameworks/card-nextjs.mdx b/docs/_snippets/frameworks/card-nextjs.mdx new file mode 100644 index 000000000..9f6759574 --- /dev/null +++ b/docs/_snippets/frameworks/card-nextjs.mdx @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + +} + href="/documentation/quickstarts/nextjs" + +/> diff --git a/docs/_snippets/frameworks/card-nuxt.mdx b/docs/_snippets/frameworks/card-nuxt.mdx new file mode 100644 index 000000000..ec3561cb6 --- /dev/null +++ b/docs/_snippets/frameworks/card-nuxt.mdx @@ -0,0 +1,28 @@ + + + + + + + + + + + + } + href="/documentation/quickstarts/nuxt" +/> diff --git a/docs/_snippets/frameworks/card-redwood.mdx b/docs/_snippets/frameworks/card-redwood.mdx new file mode 100644 index 000000000..f37a1afa2 --- /dev/null +++ b/docs/_snippets/frameworks/card-redwood.mdx @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/redwood" +/> diff --git a/docs/_snippets/frameworks/card-remix.mdx b/docs/_snippets/frameworks/card-remix.mdx new file mode 100644 index 000000000..9de8732f6 --- /dev/null +++ b/docs/_snippets/frameworks/card-remix.mdx @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/remix" +/> diff --git a/docs/_snippets/frameworks/card-supabase.mdx b/docs/_snippets/frameworks/card-supabase.mdx new file mode 100644 index 000000000..84afe8c23 --- /dev/null +++ b/docs/_snippets/frameworks/card-supabase.mdx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/supabase" +/> diff --git a/docs/_snippets/frameworks/card-sveltekit.mdx b/docs/_snippets/frameworks/card-sveltekit.mdx new file mode 100644 index 000000000..b6f5a768a --- /dev/null +++ b/docs/_snippets/frameworks/card-sveltekit.mdx @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + } + href="/documentation/quickstarts/sveltekit" +/> diff --git a/docs/_snippets/manual-setup-nestjs.mdx b/docs/_snippets/manual-setup-nestjs.mdx new file mode 100644 index 000000000..c053c49d4 --- /dev/null +++ b/docs/_snippets/manual-setup-nestjs.mdx @@ -0,0 +1,264 @@ + + Create a blank project by installing the NestJS CLI in your terminal: + +```bash +npm i -g @nestjs/cli +``` + +Then, create an empty project with: + +```bash +nest new project-name +``` + + + +## Installing Required Packages + +To begin, install the necessary packages in your NestJS project directory. You can choose one of the following package managers: + + +```bash npm +npm i @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config +``` + +```bash pnpm +pnpm install @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config +``` + +```bash yarn +yarn add @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config +``` + + + +
+ +Ensure that you execute this command within a NestJS project. + +## Obtaining the Development API Key + +To locate your development API key, login to the [Trigger.dev +dashboard](https://cloud.trigger.dev) and select the Project you want to +connect to. Then click on the Environments & API Keys tab in the left menu. +You can copy your development API Key from the field at the top of this page. +(Your development key will start with `tr_dev_`). + +## Adding Environment Variables + +Create a `.env` file at the root of your project and include your Trigger API key and URL like this: + +```bash +TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE +TRIGGER_API_URL=https://api.trigger.dev # this line is only necessary if you are self-hosting Trigger +``` + +Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step. + + + This configuration only will be loaded if you use [NestJS + Config](https://docs.nestjs.com/techniques/configuration) or + [dotenv](https://github.com/motdotla/dotenv). + + +## Adding TriggerDev Module + +Open your `app.module.ts`, and add the following inside your `imports`: + +```typescript +import { TriggerDevModule } from "@trigger.dev/nestjs"; +import { Module } from "@nestjs/common"; + +//you need to load the environment variables from .env, this is one way to do it +import "dotenv/config"; + +@Module({ + imports: [ + TriggerDevModule.register({ + id: "my-app", + apiKey: process.env.TRIGGER_API_KEY, + apiUrl: process.env.TRIGGER_API_URL, + }), + // if you use NestJS Config, you can do like this: + // TriggerDevModule.registerAsync({ + // useFactory: (configService: ConfigService) => ({ + // id: 'my-app', + // apiKey: configService.get("TRIGGER_API_KEY"), + // apiUrl: configService.get("TRIGGER_API_URL"), + // }), + // inject: [ConfigService], + // }), + ], +}) +export class AppModule { + //... +} +``` + +Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier. + +By following these steps, you'll configure the Trigger Client to work with your project. + +## Creating the Example Job + +When you add `TriggerDevModule` to your project, you will can have access to the `TriggerClient` instance by using the `@InjectTriggerDevClient()` decorator in the constructor. + +Now, let's create an example job to test the integration. + +1. Create a controller named `job.controller.ts` alongside your `app.module.ts` +2. Inside that controller, add the following code: + + + +```typescript job.controller.ts +import { Controller, Get } from "@nestjs/common"; +import { InjectTriggerDevClient } from "@trigger.dev/nestjs"; +import { eventTrigger, TriggerClient } from "@trigger.dev/sdk"; + +@Controller() +export class JobController { + constructor(@InjectTriggerDevClient() private readonly client: TriggerClient) { + this.client.defineJob({ + id: "test-job", + name: "Test Job One", + version: "0.0.1", + trigger: eventTrigger({ + name: "test.event", + }), + run: async (payload, io, ctx) => { + await io.logger.info("Hello world!", { payload }); + + return { + message: "Hello world!", + }; + }, + }); + } + + @Get() + getHello(): string { + return `Running Trigger.dev with client-id ${this.client.id}`; + } +} +``` + +Now, add this controller to your `app.module.ts`: + +```typescript app.module.ts +import { TriggerDevModule } from "@trigger.dev/nestjs"; +import { Module } from "@nestjs/common"; +import { JobController } from "./job.controller"; + +//you need to load the environment variables from .env, this is one way to do it +import "dotenv/config"; + +@Module({ + controllers: [JobController], + imports: [ + TriggerDevModule.register({ + id: "my-app", + apiKey: process.env.TRIGGER_API_KEY, + apiUrl: process.env.TRIGGER_API_URL, + }), + // if you use NestJS Config, you can do like this: + // TriggerDevModule.registerAsync({ + // useFactory: (configService: ConfigService) => ({ + // id: 'my-app', + // apiKey: configService.get("TRIGGER_API_KEY"), + // apiUrl: configService.get("TRIGGER_API_URL"), + // }), + // inject: [ConfigService], + // }), + ], +}) +export class AppModule { + //... +} +``` + + + +
+ + You can import the Trigger.dev client inside any `service` or `controller`, we recommend you to + create specialized `service` for each job you have for a better maintainability. + + +## Adding Configuration to `package.json` + +Inside the `package.json` file, add the following configuration under the root object: + +```json +"trigger.dev": { + "endpointId": "my-app" +} +``` + +Your `package.json` file might look something like this: + +```json +{ + "name": "my-app", + "version": "1.0.0", + "dependencies": { + // ... other dependencies + }, + "trigger.dev": { + "endpointId": "my-app" + } +} +``` + +Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client. + +## Running + +### Run your NestJS app + +Run your NestJS app locally, like you normally would. For example: + + + +```bash npm +npm run start +``` + +```bash pnpm +pnpm run start +``` + +```bash yarn +yarn run start +``` + + + +### Run the CLI 'dev' command + +In a **_separate terminal window or tab_** run: + + + +```bash npm +npx @trigger.dev/cli@latest dev +``` + +```bash pnpm +pnpm dlx @trigger.dev/cli@latest dev +``` + +```bash yarn +yarn dlx @trigger.dev/cli@latest dev +``` + + +
+ + You can optionally pass the port if you're not running on 3000 by adding + `--port 3001` to the end + + + + You can optionally pass the hostname if you're not running on localhost by adding + `--hostname `. Example, in case your Remix is running on 0.0.0.0: `--hostname 0.0.0.0`. + diff --git a/docs/documentation/concepts/client-adaptors.mdx b/docs/documentation/concepts/client-adaptors.mdx index 98fc4dc56..d724284ef 100644 --- a/docs/documentation/concepts/client-adaptors.mdx +++ b/docs/documentation/concepts/client-adaptors.mdx @@ -28,6 +28,7 @@ Each platform has one or more adaptors, see the guides below: | ------------------------------------------------- | -------------------- | | [Next.js](/documentation/guides/platforms/nextjs) | `createPagesRoute()` | | [Next.js](/documentation/guides/platforms/nextjs) | `createAppRoute()` | +| [NestJS](/documentation/guides/manual/nestjs) | `TriggerDevModule` | | [Astro](/documentation/guides/platforms/astro) | `createAstroRoute()` | | [Remix](/documentation/guides/platforms/remix) | `createRemixRoute()` | | Express | Coming soon | diff --git a/docs/documentation/guides/manual/nestjs.mdx b/docs/documentation/guides/manual/nestjs.mdx new file mode 100644 index 000000000..e29c1ae27 --- /dev/null +++ b/docs/documentation/guides/manual/nestjs.mdx @@ -0,0 +1,7 @@ +--- +title: "NestJS" +sidebarTitle: "NestJS" +description: "How to manually setup Trigger.dev in your NestJS project" +--- + + diff --git a/docs/documentation/quickstarts/introduction.mdx b/docs/documentation/quickstarts/introduction.mdx index c5aae43c3..e5e31232f 100644 --- a/docs/documentation/quickstarts/introduction.mdx +++ b/docs/documentation/quickstarts/introduction.mdx @@ -8,261 +8,19 @@ sidebarTitle: "Introduction" ## Select a framework to get started… - - - - - - - - - - - - - - - - -} - href="/documentation/quickstarts/nextjs" - -> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/remix"> - - - - - - - - - - - -} href="/documentation/quickstarts/express"> - - - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/redwood"> - - - - - - - - - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/astro"> - - - - - - - - - - - - -} href="/documentation/quickstarts/nuxt"> - - - - - - - - - - - - - - -} href="/documentation/quickstarts/sveltkit"> - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/fastify"/> - + + + + + + + + + ## Or quickly setup Trigger.dev with… - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - href="/documentation/quickstarts/supabase" - -> - + diff --git a/docs/documentation/quickstarts/nestjs.mdx b/docs/documentation/quickstarts/nestjs.mdx new file mode 100644 index 000000000..46bc1b03b --- /dev/null +++ b/docs/documentation/quickstarts/nestjs.mdx @@ -0,0 +1,7 @@ +--- +title: "NestJS Quick Start" +sidebarTitle: "NestJS" +description: "Start creating Jobs in 5 minutes in your NestJS project." +--- + + diff --git a/docs/mint.json b/docs/mint.json index 02e2cf50e..1d0e4dba8 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -142,6 +142,7 @@ "group": "Manual setup", "pages": [ "documentation/guides/manual/nextjs", + "documentation/guides/manual/nestjs", "documentation/guides/manual/express", "documentation/guides/manual/remix", "documentation/guides/manual/redwood", diff --git a/packages/nestjs/LICENSE b/packages/nestjs/LICENSE new file mode 100644 index 000000000..e51e7b10a --- /dev/null +++ b/packages/nestjs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Trigger.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md new file mode 100644 index 000000000..f0400f90f --- /dev/null +++ b/packages/nestjs/README.md @@ -0,0 +1,7 @@ +# NestJS and Trigger.dev + +Trigger.dev has full support for the NestJS framework. + +For information about the using Trigger.dev in your NestJS app, check out these useful docs links: + +- [Quick start guide for getting setup with Trigger.dev in a NestJS project](https://trigger.dev/docs/documentation/quickstarts/nestjs) diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json new file mode 100644 index 000000000..882c78d9b --- /dev/null +++ b/packages/nestjs/package.json @@ -0,0 +1,48 @@ +{ + "name": "@trigger.dev/nestjs", + "version": "2.1.8", + "description": "Official NestJS adapter for Trigger.dev", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "devDependencies": { + "@trigger.dev/sdk": "workspace:^2.1.0", + "@trigger.dev/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/express": "^4.17.13", + "fastify": "^4.23.2", + "rimraf": "^3.0.2", + "tsup": "^6.5.0", + "tsx": "^3.12.1" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "peerDependencies": { + "@nestjs/common": ">=10.0.0", + "@trigger.dev/sdk": "workspace:^2.1.0" + }, + "dependencies": { + "@nestjs/common": "^10.2.4", + "@remix-run/web-fetch": "^4.3.5", + "debug": "^4.3.4" + }, + "engines": { + "node": ">=16.8.0" + } +} diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts new file mode 100644 index 000000000..e1fbfaebb --- /dev/null +++ b/packages/nestjs/src/index.ts @@ -0,0 +1,216 @@ +import { + Body, + ConfigurableModuleBuilder, + Controller, + DynamicModule, + Head, + Headers, + HttpCode, + Inject, + InjectionToken, + InternalServerErrorException, + Module, + NotFoundException, + Post, + Res, +} from "@nestjs/common"; +import { Headers as StandardHeaders, Request as StandardRequest } from "@remix-run/web-fetch"; +import { TriggerClient, TriggerClientOptions } from "@trigger.dev/sdk"; +import type { Response } from "express"; +import type { FastifyReply } from "fastify"; + +const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = + new ConfigurableModuleBuilder().build(); + +/** + * The injection token to use for the TriggerDev client. + */ +export const TriggerClientRef = Symbol("TriggerClientRef"); + +/** + * Injects the TriggerDev client. + * It will returns an instance of {@link TriggerClient} + */ +export const InjectTriggerDevClient = (customProviderToken: InjectionToken = TriggerClientRef) => + Inject(customProviderToken); + +/** + * The TriggerDev module for NestJS. + * + * Use {@link TriggerDevModule.register} to register the module, or {@link TriggerDevModule.registerAsync} to register it asynchronously. + * + * @example```ts + * import { Module } from '@nestjs/common'; + * import { TriggerDevModule } from '@trigger.dev/nestjs'; + * + * @Module({ + * imports: [ + * TriggerDevModule.register({ + * id: 'my-client', + * apiKey: process.env['TRIGGER_API_KEY']!, + * }), + * // you can also can configure it asynchnously + * TriggerDevModule.registerAsync({ + * inject: [YouConfigService], + * useFactory: (configService: YouConfigService) => { + * return { + * id: 'my-client', + * apiKey: configService.get('TRIGGER_API_KEY'), + * }; + * }, + * }), + * ], + * }) + * export class AppModule {} + * ``` + */ +@Module({}) +export class TriggerDevModule extends ConfigurableModuleClass { + /** + * Register the instance for the TriggerDev client. + * + * Hint: If you want to have multiple instances of the client, you can use the `customProviderToken` to create multiple instances. + * + * @param options The options to use for the client + * @param path The path to use for the controller (default: `/api/trigger`) + * @param customProviderToken The token to use for the provider (default: {@link TriggerClientRef}) + */ + static register( + options: typeof OPTIONS_TYPE, + path: string = "/api/trigger", + customProviderToken: InjectionToken = TriggerClientRef + ): DynamicModule { + const { providers, ...rest } = ConfigurableModuleClass.register(options); + const controller = createControllerByPath(customProviderToken, path); + + return { + ...rest, + controllers: [controller], + providers: [ + ...(providers || []), + { + provide: customProviderToken, + inject: [MODULE_OPTIONS_TOKEN], + useFactory: (options: TriggerClientOptions) => { + return new TriggerClient(options); + }, + }, + ], + exports: [customProviderToken], + }; + } + + /** + * Register the instance for the TriggerDev client asynchronously. + * + * Hint: If you want to have multiple instances of the client, you can use the `customProviderToken` to create multiple instances. + * + * @param options The options to use for the client + * @param path The path to use for the controller (default: `/api/trigger`) + * @param customProviderToken The token to use for the provider (default: {@link TriggerClientRef}) + */ + static registerAsync( + options: typeof ASYNC_OPTIONS_TYPE, + path: string = "/api/trigger", + customProviderToken: InjectionToken = TriggerClientRef + ): DynamicModule { + const { providers, ...rest } = ConfigurableModuleClass.registerAsync(options); + const controller = createControllerByPath(customProviderToken, path); + + return { + ...rest, + controllers: [controller], + providers: [ + ...(providers || []), + { + provide: customProviderToken, + inject: [MODULE_OPTIONS_TOKEN], + useFactory: (options: TriggerClientOptions) => { + return new TriggerClient(options); + }, + }, + ], + exports: [customProviderToken], + }; + } +} + +/** + * Used to create a custom controller for NestJS with specific path to handle TriggerDev requests. + * + * @param customProvider The provider to use to inject the TriggerDev client + * @param path The path to use for the controller + */ +function createControllerByPath(customProvider: InjectionToken, path: string) { + @Controller(path) + class TriggerDevController { + constructor( + @InjectTriggerDevClient(customProvider) + private readonly client: TriggerClient + ) {} + + @Head() + @HttpCode(200) + public empty() {} + + @Post() + public handleRequestPost( + @Res({ passthrough: true }) res: any, + @Headers() headers: unknown, + @Body() body?: unknown + ): Promise { + return this.handleRequest(res, "POST", headers, body); + } + + /** + * Forward the request to the TriggerDev client + */ + public async handleRequest( + res: any, + method: string, + requestHeaders: unknown, + requestBody?: unknown + ): Promise { + // try { + const headers = new StandardHeaders(); + + Object.entries(requestHeaders || {}).forEach(([key, value]) => { + headers.set(key, value as string); + }); + + // Create a new Request object (hardcode the url because it doesn't really matter what it is) + const standardRequest = new StandardRequest("https://nestjs.com/api/trigger", { + headers, + method, + // @ts-ignore + body: requestBody ? JSON.stringify(requestBody) : undefined, + }); + + const response = await this.client.handleRequest(standardRequest); + + if (!response) { + throw new NotFoundException({ error: "Not found" }); + } + + if (typeof res.status === "function") { + // express + (res as Response).status(response.status); + (res as Response).set(response.headers); + } else if (typeof res.code === "function") { + // fastify + (res as FastifyReply).code(response.status); + if (response.headers) { + (res as FastifyReply).headers(response.headers); + } + } else { + throw new InternalServerErrorException( + "Unable to indetify the framework to set the status code, are you using Express or Fastify?" + ); + } + + return response.body; + } + } + + return TriggerDevController; +} diff --git a/packages/nestjs/tsconfig.json b/packages/nestjs/tsconfig.json new file mode 100644 index 000000000..1eea77ed9 --- /dev/null +++ b/packages/nestjs/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@trigger.dev/tsconfig/node18.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "declarationMap": false + }, + "exclude": ["node_modules"] +} diff --git a/packages/nestjs/tsup.config.ts b/packages/nestjs/tsup.config.ts new file mode 100644 index 000000000..74c4f4dc1 --- /dev/null +++ b/packages/nestjs/tsup.config.ts @@ -0,0 +1,19 @@ +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, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + }, +]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1be0d4157..b3b1079fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -888,6 +888,33 @@ importers: tsx: 3.12.2 typescript: 4.9.5 + packages/nestjs: + specifiers: + '@nestjs/common': ^10.2.4 + '@remix-run/web-fetch': ^4.3.5 + '@trigger.dev/sdk': workspace:^2.1.0 + '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 + '@types/express': ^4.17.13 + debug: ^4.3.4 + fastify: ^4.23.2 + rimraf: ^3.0.2 + tsup: ^6.5.0 + tsx: ^3.12.1 + dependencies: + '@nestjs/common': 10.2.7 + '@remix-run/web-fetch': 4.3.6 + debug: 4.3.4 + devDependencies: + '@trigger.dev/sdk': link:../trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 + '@types/express': 4.17.15 + fastify: 4.23.2 + rimraf: 3.0.2 + tsup: 6.6.3 + tsx: 3.12.2 + packages/nextjs: specifiers: '@trigger.dev/tsconfig': workspace:* @@ -1164,6 +1191,69 @@ importers: ts-node: 10.9.1_xj5cs2fmhcigm4w5bhhtewqeja tsconfig-paths: 3.14.1 + references/nestjs-example: + specifiers: + '@nestjs/cli': ^10.0.0 + '@nestjs/common': ^10.0.0 + '@nestjs/config': ^3.0.1 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/schematics': ^10.0.0 + '@nestjs/testing': ^10.0.0 + '@trigger.dev/nestjs': workspace:* + '@trigger.dev/sdk': workspace:* + '@types/express': ^4.17.17 + '@types/jest': ^29.5.2 + '@types/node': ^20.3.1 + '@types/supertest': ^2.0.12 + '@typescript-eslint/eslint-plugin': ^6.0.0 + '@typescript-eslint/parser': ^6.0.0 + eslint: ^8.42.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-prettier: ^5.0.0 + jest: ^29.5.0 + prettier: ^3.0.0 + reflect-metadata: ^0.1.13 + rxjs: ^7.8.1 + source-map-support: ^0.5.21 + supertest: ^6.3.3 + ts-jest: ^29.1.0 + ts-loader: ^9.4.3 + ts-node: ^10.9.1 + tsconfig-paths: ^4.2.0 + typescript: ^5.1.3 + dependencies: + '@nestjs/common': 10.2.7_atc7tu2sld2m3nk4hmwkqn6qde + '@nestjs/config': 3.1.1_xtnakrwl23ehsubvekids4npxm + '@nestjs/core': 10.2.7_5hwp5tvaqax6kxnclaqixhass4 + '@nestjs/platform-express': 10.2.7_pyni7wzdujkbvmfeegxedz3mmy + '@trigger.dev/nestjs': link:../../packages/nestjs + '@trigger.dev/sdk': link:../../packages/trigger-sdk + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + devDependencies: + '@nestjs/cli': 10.1.18 + '@nestjs/schematics': 10.0.2_typescript@5.2.2 + '@nestjs/testing': 10.2.7_6ce6yeqrytunl27g3gdf6ye2ri + '@types/express': 4.17.18 + '@types/jest': 29.5.3 + '@types/node': 20.6.0 + '@types/supertest': 2.0.14 + '@typescript-eslint/eslint-plugin': 6.7.4_ygtxu7ao4w7xzfo6eep522bcem + '@typescript-eslint/parser': 6.7.4_ox3na7ge7wjdarbyztnclevxam + eslint: 8.45.0 + eslint-config-prettier: 9.0.0_eslint@8.45.0 + eslint-plugin-prettier: 5.0.0_jybzfv6jdssomlxkhhfntuvyli + jest: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole + prettier: 3.0.0 + source-map-support: 0.5.21 + supertest: 6.3.3 + ts-jest: 29.1.1_uvjn2wawyirmlmi655a42v3ske + ts-loader: 9.4.4_typescript@5.2.2 + ts-node: 10.9.1_kpuv3buz4xyqturyqxj2gejvma + tsconfig-paths: 4.2.0 + typescript: 5.2.2 + references/nextjs-reference: specifiers: '@trigger.dev/cli': workspace:* @@ -1378,6 +1468,111 @@ packages: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.19 + /@angular-devkit/core/16.1.8: + resolution: {integrity: sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1 + jsonc-parser: 3.2.0 + rxjs: 7.8.1 + source-map: 0.7.4 + dev: true + + /@angular-devkit/core/16.1.8_chokidar@3.5.3: + resolution: {integrity: sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1 + chokidar: 3.5.3 + jsonc-parser: 3.2.0 + rxjs: 7.8.1 + source-map: 0.7.4 + dev: true + + /@angular-devkit/core/16.2.3_chokidar@3.5.3: + resolution: {integrity: sha512-oZLdg2XTx7likYAXRj1CU0XmrsCfe5f2grj3iwuI3OB1LXwwpdbHBztruj03y3yHES+TnO+dIbkvRnvMXs7uAA==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1 + chokidar: 3.5.3 + jsonc-parser: 3.2.0 + picomatch: 2.3.1 + rxjs: 7.8.1 + source-map: 0.7.4 + dev: true + + /@angular-devkit/schematics-cli/16.2.3_chokidar@3.5.3: + resolution: {integrity: sha512-5YQCbQmY9Kc03a9Io4XHOrxGXjnzcVveUuUO64R1m5x2aA5I+mVR8NVvxuoGRAeoI1FWusAKRe9hH8nRCLrelA==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + dependencies: + '@angular-devkit/core': 16.2.3_chokidar@3.5.3 + '@angular-devkit/schematics': 16.2.3_chokidar@3.5.3 + ansi-colors: 4.1.3 + inquirer: 8.2.4 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@angular-devkit/schematics/16.1.8: + resolution: {integrity: sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + dependencies: + '@angular-devkit/core': 16.1.8 + jsonc-parser: 3.2.0 + magic-string: 0.30.0 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@angular-devkit/schematics/16.1.8_chokidar@3.5.3: + resolution: {integrity: sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + dependencies: + '@angular-devkit/core': 16.1.8_chokidar@3.5.3 + jsonc-parser: 3.2.0 + magic-string: 0.30.0 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@angular-devkit/schematics/16.2.3_chokidar@3.5.3: + resolution: {integrity: sha512-+lBiHxi/C9HCfiCbtW25DldwvJDXXXv5oWw+Tg4s18BO/lYZLveGUEaZWu9ZJ5VIJ8GliUi2LohxhDxBkh4Oxg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + dependencies: + '@angular-devkit/core': 16.2.3_chokidar@3.5.3 + jsonc-parser: 3.2.0 + magic-string: 0.30.1 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + dev: true + /@astrojs/compiler/2.1.0: resolution: {integrity: sha512-Mp+qrNhly+27bL/Zq8lGeUY+YrdoU0eDfIlAeGIPrzt0PnI/jGpvPUdCaugv4zbCrDkOUScFfcbeEiYumrdJnw==} @@ -1446,13 +1641,6 @@ packages: default-browser-id: 3.0.0 dev: true - /@babel/code-frame/7.18.6: - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.22.13 - dev: true - /@babel/code-frame/7.21.4: resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} @@ -1517,7 +1705,7 @@ packages: debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true @@ -1555,7 +1743,7 @@ packages: '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 eslint: 8.45.0 eslint-visitor-keys: 2.1.0 - semver: 6.3.0 + semver: 6.3.1 dev: true /@babel/eslint-parser/7.21.8_mxgwiyfjuazeomuuf56n24ufpy: @@ -1569,7 +1757,7 @@ packages: '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 eslint: 8.31.0 eslint-visitor-keys: 2.1.0 - semver: 6.3.0 + semver: 6.3.1 dev: true /@babel/generator/7.21.5: @@ -4689,7 +4877,7 @@ packages: babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.12 babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.12 core-js-compat: 3.27.1 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true @@ -4776,7 +4964,7 @@ packages: babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.21.8 babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.21.8 core-js-compat: 3.27.1 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true @@ -4863,7 +5051,7 @@ packages: babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.22.17 babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.22.17 core-js-compat: 3.27.1 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true @@ -6755,6 +6943,28 @@ packages: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: true + /@fastify/ajv-compiler/3.5.0: + resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1 + fast-uri: 2.2.0 + dev: true + + /@fastify/deepmerge/1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: true + + /@fastify/error/3.4.0: + resolution: {integrity: sha512-e/mafFwbK3MNqxUcFBLgHhgxsF8UT1m8aj0dAlqEa2nJEgPsRtpHTZ3ObgrgkZ2M1eJHPTwgyUl/tXkvabsZdQ==} + dev: true + + /@fastify/fast-json-stringify-compiler/4.3.0: + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + dependencies: + fast-json-stringify: 5.8.0 + dev: true + /@figspec/components/1.0.1: resolution: {integrity: sha512-UvnEamPEAMh9HExViqpobWmX25g1+soA9kcJu+It3VerMa7CeVyaIbQydNf1Gys5v/rxJVdTDRgQ7OXW2zAAig==} dependencies: @@ -7090,6 +7300,49 @@ packages: - ts-node dev: true + /@jest/core/29.6.2_ts-node@10.9.1: + resolution: {integrity: sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.6.2 + '@jest/reporters': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 20.6.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-changed-files: 29.5.0 + jest-config: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole + jest-haste-map: 29.6.2 + jest-message-util: 29.6.2 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-resolve-dependencies: 29.6.2 + jest-runner: 29.6.2 + jest-runtime: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + jest-watcher: 29.6.2 + micromatch: 4.0.5 + pretty-format: 29.6.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/environment/29.6.2: resolution: {integrity: sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7413,6 +7666,10 @@ packages: '@lit-labs/ssr-dom-shim': 1.1.1 dev: true + /@lukeed/csprng/1.1.0: + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + /@manypkg/cli/0.19.2: resolution: {integrity: sha512-DXx/P1lyunNoFWwOj1MWBucUhaIJljoiAGOpO2fE0GKMBCI6EZBZD0Up1+fQZoXBecKXRgV9mGgLvIB2fOQ0KQ==} hasBin: true @@ -7501,6 +7758,194 @@ packages: tar-fs: 2.1.1 dev: true + /@nestjs/cli/10.1.18: + resolution: {integrity: sha512-jQtG47keLsACt7b4YwJbTBYRm90n82gJpMaiR1HGAyQ9pccbctjSYu592eT4bxqkUWxPgBE3mpNynXj7dWAfrw==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + dependencies: + '@angular-devkit/core': 16.2.3_chokidar@3.5.3 + '@angular-devkit/schematics': 16.2.3_chokidar@3.5.3 + '@angular-devkit/schematics-cli': 16.2.3_chokidar@3.5.3 + '@nestjs/schematics': 10.0.2_acogjgahz3bdlhk7may5ikc22y + chalk: 4.1.2 + chokidar: 3.5.3 + cli-table3: 0.6.3 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 8.0.0_dtthwp2bsqb7yvb7hoeealjg4i + inquirer: 8.2.6 + node-emoji: 1.11.0 + ora: 5.4.1 + os-name: 4.0.1 + rimraf: 4.4.1 + shelljs: 0.8.5 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.1.0 + typescript: 5.2.2 + webpack: 5.88.2 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + dev: true + + /@nestjs/common/10.2.7: + resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + iterare: 1.2.1 + tslib: 2.6.2 + uid: 2.0.2 + dev: false + + /@nestjs/common/10.2.7_atc7tu2sld2m3nk4hmwkqn6qde: + resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + iterare: 1.2.1 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + + /@nestjs/config/3.1.1_xtnakrwl23ehsubvekids4npxm: + resolution: {integrity: sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 + dependencies: + '@nestjs/common': 10.2.7_atc7tu2sld2m3nk4hmwkqn6qde + dotenv: 16.3.1 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + reflect-metadata: 0.1.13 + uuid: 9.0.0 + dev: false + + /@nestjs/core/10.2.7_5hwp5tvaqax6kxnclaqixhass4: + resolution: {integrity: sha512-5GSu53QUUcwX17sNmlJPa1I0wIeAZOKbedyVuQx0ZAwWVa9g0wJBbsNP+R4EJ+j5Dkdzt/8xkiZvnKt8RFRR8g==} + requiresBuild: true + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + dependencies: + '@nestjs/common': 10.2.7_atc7tu2sld2m3nk4hmwkqn6qde + '@nestjs/platform-express': 10.2.7_pyni7wzdujkbvmfeegxedz3mmy + '@nuxtjs/opencollective': 0.3.2 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.2.0 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + transitivePeerDependencies: + - encoding + + /@nestjs/platform-express/10.2.7_pyni7wzdujkbvmfeegxedz3mmy: + resolution: {integrity: sha512-p+kp6aJtkgAdVpUrCVmM6MKtOvjsbt7QofBiZMidjYesZkMeG5gZ1D2SK8XzvQ8VXHJfFgEdY2xcKGB+wJLOYQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + dependencies: + '@nestjs/common': 10.2.7_atc7tu2sld2m3nk4hmwkqn6qde + '@nestjs/core': 10.2.7_5hwp5tvaqax6kxnclaqixhass4 + body-parser: 1.20.2 + cors: 2.8.5 + express: 4.18.2 + multer: 1.4.4-lts.1 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + + /@nestjs/schematics/10.0.2_acogjgahz3bdlhk7may5ikc22y: + resolution: {integrity: sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==} + peerDependencies: + typescript: '>=4.8.2' + dependencies: + '@angular-devkit/core': 16.1.8_chokidar@3.5.3 + '@angular-devkit/schematics': 16.1.8_chokidar@3.5.3 + comment-json: 4.2.3 + jsonc-parser: 3.2.0 + pluralize: 8.0.0 + typescript: 5.2.2 + transitivePeerDependencies: + - chokidar + dev: true + + /@nestjs/schematics/10.0.2_typescript@5.2.2: + resolution: {integrity: sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==} + peerDependencies: + typescript: '>=4.8.2' + dependencies: + '@angular-devkit/core': 16.1.8 + '@angular-devkit/schematics': 16.1.8 + comment-json: 4.2.3 + jsonc-parser: 3.2.0 + pluralize: 8.0.0 + typescript: 5.2.2 + transitivePeerDependencies: + - chokidar + dev: true + + /@nestjs/testing/10.2.7_6ce6yeqrytunl27g3gdf6ye2ri: + resolution: {integrity: sha512-d2SIqiJIf/7NSILeNNWSdRvTTpHSouGgisGHwf5PVDC7z4/yXZw/wPO9eJhegnxFlqk6n2LW4QBTmMzbqjAfHA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + dependencies: + '@nestjs/common': 10.2.7_atc7tu2sld2m3nk4hmwkqn6qde + '@nestjs/core': 10.2.7_5hwp5tvaqax6kxnclaqixhass4 + '@nestjs/platform-express': 10.2.7_pyni7wzdujkbvmfeegxedz3mmy + tslib: 2.6.2 + dev: true + /@next/env/13.3.1: resolution: {integrity: sha512-EDtCoedIZC7JlUQ3uaQpSc4aVmyhbLHmQVALg7pFfQgOTjgSnn7mKtA0DiCMkYvvsx6aFb5octGMtWrOtGXW9A==} @@ -7782,6 +8227,17 @@ packages: - supports-color dev: false + /@nuxtjs/opencollective/0.3.2: + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + node-fetch: 2.6.12 + transitivePeerDependencies: + - encoding + /@octokit/app/13.1.2: resolution: {integrity: sha512-Kf+h5sa1SOI33hFsuHvTsWj1jUrjp1x4MuiJBq7U/NicfEGa6nArPUoDnyfP/YTmcQ5cQ5yvOgoIBkbwPg6kzQ==} engines: {node: '>= 14'} @@ -8977,14 +9433,14 @@ packages: dependencies: asn1js: 3.0.5 pvtsutils: 1.3.5 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /@peculiar/json-schema/1.1.12: resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} engines: {node: '>=8.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.6.2 dev: false /@peculiar/webcrypto/1.4.1: @@ -8994,7 +9450,7 @@ packages: '@peculiar/asn1-schema': 2.3.6 '@peculiar/json-schema': 1.1.12 pvtsutils: 1.3.5 - tslib: 2.5.0 + tslib: 2.6.2 webcrypto-core: 1.7.7 dev: false @@ -9027,7 +9483,7 @@ packages: fsevents: 2.3.2 dev: true - /@pmmmwh/react-refresh-webpack-plugin/0.5.10_5p63spmmiprwsput7pcvbh7bwu: + /@pmmmwh/react-refresh-webpack-plugin/0.5.10_2kpgiq4mtlettjqmb64nc4esa4: resolution: {integrity: sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==} engines: {node: '>= 10.13'} peerDependencies: @@ -9061,9 +9517,9 @@ packages: html-entities: 2.3.3 loader-utils: 2.0.4 react-refresh: 0.11.0 - schema-utils: 3.1.2 + schema-utils: 3.3.0 source-map: 0.7.4 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu dev: true /@pnpm/config.env-replace/1.1.0: @@ -10311,7 +10767,7 @@ packages: semver: 7.5.4 sort-package-json: 1.57.0 tar-fs: 2.1.1 - tsconfig-paths: 4.1.2 + tsconfig-paths: 4.2.0 ws: 7.5.9 xdm: 2.1.0 transitivePeerDependencies: @@ -10568,7 +11024,7 @@ packages: /@remix-run/web-blob/3.0.4: resolution: {integrity: sha512-AfegzZvSSDc+LwnXV+SwROTrDtoLiPxeFW+jxgvtDAnkuCX1rrzmVJ6CzqZ1Ai0bVfmJadkG5GxtAfYclpPmgw==} dependencies: - '@remix-run/web-stream': 1.0.3 + '@remix-run/web-stream': 1.0.4 web-encoding: 1.1.5 dev: false @@ -11297,28 +11753,28 @@ packages: '@storybook/theming': 7.0.9_biqbaboplfbrettd7655fr4n2y '@types/node': 16.18.11 '@types/semver': 7.3.13 - babel-loader: 9.1.2_25xk4kalzxoxom6k7ae7wqkwo4 + babel-loader: 9.1.2_ijmuqjuz7epdoeof4qwmt7scdi babel-plugin-named-exports-order: 0.0.2 browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 - css-loader: 6.7.3_webpack@5.80.0 + css-loader: 6.7.3_webpack@5.88.2 express: 4.18.2 - fork-ts-checker-webpack-plugin: 7.3.0_vf4xkga2qinmx3cxkwrybccrqy + fork-ts-checker-webpack-plugin: 7.3.0_vf3ejk3u7gfag4p4x6gqje5yuq fs-extra: 11.1.0 - html-webpack-plugin: 5.5.1_webpack@5.80.0 + html-webpack-plugin: 5.5.1_webpack@5.88.2 path-browserify: 1.0.1 process: 0.11.10 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 semver: 7.5.4 - style-loader: 3.3.2_webpack@5.80.0 - terser-webpack-plugin: 5.3.7_5jgfnkl7fjuhakmzbjzotue6o4 + style-loader: 3.3.2_webpack@5.88.2 + terser-webpack-plugin: 5.3.7_nww33inhqu3uc3cp573wawoccu ts-dedent: 2.2.0 typescript: 4.9.4 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu - webpack-dev-middleware: 5.3.3_webpack@5.80.0 + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu + webpack-dev-middleware: 5.3.3_webpack@5.88.2 webpack-hot-middleware: 2.25.3 webpack-virtual-modules: 0.4.6 transitivePeerDependencies: @@ -11826,12 +12282,12 @@ packages: dependencies: '@babel/preset-flow': 7.18.6 '@babel/preset-react': 7.18.6 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.10_5p63spmmiprwsput7pcvbh7bwu + '@pmmmwh/react-refresh-webpack-plugin': 0.5.10_2kpgiq4mtlettjqmb64nc4esa4 '@storybook/core-webpack': 7.0.9 '@storybook/docs-tools': 7.0.9 '@storybook/node-logger': 7.0.9 '@storybook/react': 7.0.9_o4scbtliisanygemawej7x2d6i - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0_vf4xkga2qinmx3cxkwrybccrqy + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0_vf3ejk3u7gfag4p4x6gqje5yuq '@types/node': 16.18.11 '@types/semver': 7.3.13 babel-plugin-add-react-displayname: 0.0.5 @@ -11842,7 +12298,7 @@ packages: react-refresh: 0.11.0 semver: 7.5.4 typescript: 4.9.4 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu transitivePeerDependencies: - '@swc/core' - '@types/webpack' @@ -11901,7 +12357,7 @@ packages: resolution: {integrity: sha512-09tD+rBWMqBAdVqKhyotO6bTTJlCbVX9uVmc8la4jBoLL1JdE3qkBBmDivEsMDK5AoVaM5Zg2maDO4jm2HyZFw==} dev: true - /@storybook/react-docgen-typescript-plugin/1.0.6--canary.9.0c3f3b7.0_vf4xkga2qinmx3cxkwrybccrqy: + /@storybook/react-docgen-typescript-plugin/1.0.6--canary.9.0c3f3b7.0_vf3ejk3u7gfag4p4x6gqje5yuq: resolution: {integrity: sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q==} peerDependencies: typescript: '>= 4.x' @@ -11915,7 +12371,7 @@ packages: react-docgen-typescript: 2.2.2_typescript@4.9.4 tslib: 2.6.2 typescript: 4.9.4 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu transitivePeerDependencies: - supports-color dev: true @@ -12108,7 +12564,7 @@ packages: dependencies: '@storybook/channels': 7.0.12 '@types/babel__core': 7.20.0 - '@types/express': 4.17.15 + '@types/express': 4.17.18 file-system-cache: 2.1.1 dev: true @@ -12117,7 +12573,7 @@ packages: dependencies: '@storybook/channels': 7.0.9 '@types/babel__core': 7.20.0 - '@types/express': 4.17.15 + '@types/express': 4.17.18 file-system-cache: 2.1.1 dev: true @@ -12399,7 +12855,7 @@ packages: resolution: {integrity: sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.22.13 '@babel/runtime': 7.22.5 '@types/aria-query': 5.0.1 aria-query: 5.1.3 @@ -12642,11 +13098,15 @@ packages: /@types/cookie/0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + /@types/cookiejar/2.1.2: + resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} + dev: true + /@types/cookies/0.7.7: resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==} dependencies: '@types/connect': 3.4.35 - '@types/express': 4.17.15 + '@types/express': 4.17.18 '@types/keygrip': 1.0.2 '@types/node': 20.6.0 dev: false @@ -12757,12 +13217,21 @@ packages: '@types/node': 20.6.0 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 + dev: true + + /@types/express-serve-static-core/4.17.37: + resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} + dependencies: + '@types/node': 20.6.0 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.2 /@types/express/4.17.13: resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==} dependencies: '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.32 + '@types/express-serve-static-core': 4.17.37 '@types/qs': 6.9.7 '@types/serve-static': 1.15.0 dev: false @@ -12774,6 +13243,15 @@ packages: '@types/express-serve-static-core': 4.17.32 '@types/qs': 6.9.7 '@types/serve-static': 1.15.0 + dev: true + + /@types/express/4.17.18: + resolution: {integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.37 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.0 /@types/find-cache-dir/3.2.1: resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} @@ -12928,6 +13406,10 @@ packages: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true + /@types/json-schema/7.0.13: + resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} + dev: true + /@types/json5/0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -13023,6 +13505,9 @@ packages: resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==} dev: true + /@types/mime/1.3.3: + resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} + /@types/mime/3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} @@ -13208,6 +13693,12 @@ packages: /@types/semver/7.5.1: resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==} + /@types/send/0.17.2: + resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==} + dependencies: + '@types/mime': 1.3.3 + '@types/node': 20.6.0 + /@types/serve-static/1.15.0: resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} dependencies: @@ -13236,6 +13727,19 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/superagent/4.1.19: + resolution: {integrity: sha512-McM1mlc7PBZpCaw0fw/36uFqo0YeA6m8JqoyE4OfqXsZCIg0hPP2xdE6FM7r6fdprDZHlJwDpydUj1R++93hCA==} + dependencies: + '@types/cookiejar': 2.1.2 + '@types/node': 20.6.0 + dev: true + + /@types/supertest/2.0.14: + resolution: {integrity: sha512-Q900DeeHNFF3ZYYepf/EyJfZDA2JrnWLaSQ0YNV7+2GTo8IlJzauEnDGhya+hauncpBYTYGpVHwGdssJeAQ7eA==} + dependencies: + '@types/superagent': 4.1.19 + dev: true + /@types/tar/6.1.4: resolution: {integrity: sha512-Cp4oxpfIzWt7mr2pbhHT2OTXGMAL0szYCzuf8lRWyIMCgsx6/Hfc3ubztuhvzXHXgraTQxyOCmmg7TDGIMIJJQ==} dependencies: @@ -13361,6 +13865,35 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin/6.7.4_ygtxu7ao4w7xzfo6eep522bcem: + resolution: {integrity: sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.1 + '@typescript-eslint/parser': 6.7.4_ox3na7ge7wjdarbyztnclevxam + '@typescript-eslint/scope-manager': 6.7.4 + '@typescript-eslint/type-utils': 6.7.4_ox3na7ge7wjdarbyztnclevxam + '@typescript-eslint/utils': 6.7.4_ox3na7ge7wjdarbyztnclevxam + '@typescript-eslint/visitor-keys': 6.7.4 + debug: 4.3.4 + eslint: 8.45.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser/5.59.6_binxsscxvozjxebftqdoazsxm4: resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -13459,6 +13992,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser/6.7.4_ox3na7ge7wjdarbyztnclevxam: + resolution: {integrity: sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.7.4 + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/typescript-estree': 6.7.4_typescript@5.2.2 + '@typescript-eslint/visitor-keys': 6.7.4 + debug: 4.3.4 + eslint: 8.45.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager/5.59.6: resolution: {integrity: sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -13466,6 +14020,14 @@ packages: '@typescript-eslint/types': 5.59.6 '@typescript-eslint/visitor-keys': 5.59.6 + /@typescript-eslint/scope-manager/6.7.4: + resolution: {integrity: sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/visitor-keys': 6.7.4 + dev: true + /@typescript-eslint/type-utils/5.59.6_iukboom6ndih5an6iafl45j2fe: resolution: {integrity: sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -13506,10 +14068,35 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils/6.7.4_ox3na7ge7wjdarbyztnclevxam: + resolution: {integrity: sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.7.4_typescript@5.2.2 + '@typescript-eslint/utils': 6.7.4_ox3na7ge7wjdarbyztnclevxam + debug: 4.3.4 + eslint: 8.45.0 + ts-api-utils: 1.0.3_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types/5.59.6: resolution: {integrity: sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/types/6.7.4: + resolution: {integrity: sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree/5.59.6: resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -13613,6 +14200,27 @@ packages: transitivePeerDependencies: - supports-color + /@typescript-eslint/typescript-estree/6.7.4_typescript@5.2.2: + resolution: {integrity: sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/visitor-keys': 6.7.4 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils/5.59.6_eslint@8.45.0: resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -13673,6 +14281,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils/6.7.4_ox3na7ge7wjdarbyztnclevxam: + resolution: {integrity: sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0_eslint@8.45.0 + '@types/json-schema': 7.0.13 + '@types/semver': 7.5.1 + '@typescript-eslint/scope-manager': 6.7.4 + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/typescript-estree': 6.7.4_typescript@5.2.2 + eslint: 8.45.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys/5.59.6: resolution: {integrity: sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -13680,6 +14307,14 @@ packages: '@typescript-eslint/types': 5.59.6 eslint-visitor-keys: 3.4.2 + /@typescript-eslint/visitor-keys/6.7.4: + resolution: {integrity: sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.7.4 + eslint-visitor-keys: 3.4.2 + dev: true + /@uiw/codemirror-extensions-basic-setup/4.19.5_o3n2erwajrogdzfxqd6wu4qkza: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: @@ -14025,6 +14660,10 @@ packages: resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} dev: false + /abstract-logging/2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: true + /accepts/1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -14032,8 +14671,8 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 - /acorn-import-assertions/1.8.0_acorn@8.10.0: - resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} + /acorn-import-assertions/1.9.0_acorn@8.10.0: + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: acorn: ^8 dependencies: @@ -14223,7 +14862,6 @@ packages: /ansi-colors/4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - dev: false /ansi-escapes/4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -14305,9 +14943,16 @@ packages: resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} dev: true + /append-field/1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + /aproba/2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + /archy/1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + dev: true + /are-we-there-yet/2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} @@ -14406,6 +15051,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /array-timsort/1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + dev: true + /array-union/1.0.2: resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} engines: {node: '>=0.10.0'} @@ -14469,6 +15118,10 @@ packages: engines: {node: '>=8'} dev: false + /asap/2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true + /asn1js/3.0.5: resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} engines: {node: '>=12.0.0'} @@ -14619,6 +15272,11 @@ packages: hasBin: true dev: false + /atomic-sleep/1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: true + /autoprefixer/10.4.13_postcss@8.4.21: resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} engines: {node: ^10 || ^12 || >=14} @@ -14655,6 +15313,16 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + /avvio/8.2.1: + resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} + dependencies: + archy: 1.0.0 + debug: 4.3.4 + fastq: 1.15.0 + transitivePeerDependencies: + - supports-color + dev: true + /axe-core/4.6.2: resolution: {integrity: sha512-b1WlTV8+XKLj9gZy2DZXgQiyDp9xkkoe2a6U6UbYccScq2wgH/YwCeI2/Jq2mgo0HzQxqJOjWZBLeA/mqsk5Mg==} engines: {node: '>=4'} @@ -14736,7 +15404,7 @@ packages: - supports-color dev: true - /babel-loader/9.1.2_25xk4kalzxoxom6k7ae7wqkwo4: + /babel-loader/9.1.2_ijmuqjuz7epdoeof4qwmt7scdi: resolution: {integrity: sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==} engines: {node: '>= 14.15.0'} peerDependencies: @@ -14746,7 +15414,7 @@ packages: '@babel/core': 7.20.12 find-cache-dir: 3.3.2 schema-utils: 4.0.1 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu dev: true /babel-plugin-add-react-displayname/0.0.5: @@ -15032,7 +15700,7 @@ packages: engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dependencies: bytes: 3.1.2 - content-type: 1.0.4 + content-type: 1.0.5 debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 @@ -15046,6 +15714,25 @@ packages: transitivePeerDependencies: - supports-color + /body-parser/1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + /boolbase/1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true @@ -15688,6 +16375,7 @@ packages: /cli-spinners/2.7.0: resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} engines: {node: '>=6'} + dev: false /cli-spinners/2.9.1: resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} @@ -15886,6 +16574,17 @@ packages: engines: {node: ^12.20.0 || >=14} dev: false + /comment-json/4.2.3: + resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} + engines: {node: '>= 6'} + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + dev: true + /common-ancestor-path/1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} @@ -15899,7 +16598,6 @@ packages: /component-emitter/1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} - dev: false /compressible/2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} @@ -15932,7 +16630,6 @@ packages: inherits: 2.0.4 readable-stream: 2.3.7 typedarray: 0.0.6 - dev: true /concurrently/8.2.0: resolution: {integrity: sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==} @@ -15977,6 +16674,9 @@ packages: xdg-basedir: 5.1.0 dev: false + /consola/2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + /console-control-strings/1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -15990,6 +16690,10 @@ packages: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} + /content-type/1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + /convert-source-map/1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -16012,6 +16716,10 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + /cookiejar/2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: true + /copy-descriptor/0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} engines: {node: '>=0.10.0'} @@ -16037,6 +16745,13 @@ packages: /core-util-is/1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + /cors/2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + /cosmiconfig/7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -16186,7 +16901,7 @@ packages: semver: 7.5.4 dev: true - /css-loader/6.7.3_webpack@5.80.0: + /css-loader/6.7.3_webpack@5.88.2: resolution: {integrity: sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -16200,7 +16915,7 @@ packages: postcss-modules-values: 4.0.0_postcss@8.4.29 postcss-value-parser: 4.2.0 semver: 7.5.4 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu dev: true /css-select/4.3.0: @@ -16716,6 +17431,13 @@ packages: /devalue/4.3.2: resolution: {integrity: sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==} + /dezalgo/1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: true + /didyoumean/1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -16859,7 +17581,6 @@ packages: /dotenv-expand/10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} - dev: true /dotenv/16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} @@ -16984,6 +17705,14 @@ packages: graceful-fs: 4.2.10 tapable: 2.2.1 + /enhanced-resolve/5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.10 + tapable: 2.2.1 + dev: true + /enquirer/2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} @@ -17697,6 +18426,15 @@ packages: eslint: 8.31.0 dev: true + /eslint-config-prettier/9.0.0_eslint@8.45.0: + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.45.0 + dev: true + /eslint-config-turbo/1.10.15_eslint@8.31.0: resolution: {integrity: sha512-76mpx2x818JZE26euen14utYcFDxOahZ9NaWA+6Xa4pY2ezVKVschuOxS96EQz3o3ZRSmcgBOapw/gHbN+EKxQ==} peerDependencies: @@ -17958,7 +18696,7 @@ packages: minimatch: 3.1.2 object.values: 1.1.6 resolve: 1.22.2 - semver: 6.3.0 + semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -17991,7 +18729,7 @@ packages: minimatch: 3.1.2 object.values: 1.1.6 resolve: 1.22.2 - semver: 6.3.0 + semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -18024,7 +18762,7 @@ packages: minimatch: 3.1.2 object.values: 1.1.6 resolve: 1.22.2 - semver: 6.3.0 + semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -18119,7 +18857,7 @@ packages: minimatch: 3.1.2 object.entries: 1.1.6 object.fromentries: 2.0.6 - semver: 6.3.0 + semver: 6.3.1 dev: true /eslint-plugin-jsx-a11y/6.7.1_eslint@8.42.0: @@ -18144,7 +18882,7 @@ packages: minimatch: 3.1.2 object.entries: 1.1.6 object.fromentries: 2.0.6 - semver: 6.3.0 + semver: 6.3.1 dev: true /eslint-plugin-jsx-a11y/6.7.1_eslint@8.45.0: @@ -18169,7 +18907,7 @@ packages: minimatch: 3.1.2 object.entries: 1.1.6 object.fromentries: 2.0.6 - semver: 6.3.0 + semver: 6.3.1 /eslint-plugin-node/11.1.0_eslint@8.31.0: resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} @@ -18201,6 +18939,27 @@ packages: semver: 6.3.0 dev: true + /eslint-plugin-prettier/5.0.0_jybzfv6jdssomlxkhhfntuvyli: + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.45.0 + eslint-config-prettier: 9.0.0_eslint@8.45.0 + prettier: 3.0.0 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.5 + dev: true + /eslint-plugin-react-hooks/4.6.0_eslint@8.31.0: resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} @@ -18280,7 +19039,7 @@ packages: object.values: 1.1.6 prop-types: 15.8.1 resolve: 2.0.0-next.4 - semver: 6.3.0 + semver: 6.3.1 string.prototype.matchall: 4.0.8 dev: true @@ -18304,7 +19063,7 @@ packages: object.values: 1.1.6 prop-types: 15.8.1 resolve: 2.0.0-next.4 - semver: 6.3.0 + semver: 6.3.1 string.prototype.matchall: 4.0.8 dev: true @@ -18328,7 +19087,7 @@ packages: object.values: 1.1.6 prop-types: 15.8.1 resolve: 2.0.0-next.4 - semver: 6.3.0 + semver: 6.3.1 string.prototype.matchall: 4.0.8 /eslint-plugin-testing-library/5.11.0_iukboom6ndih5an6iafl45j2fe: @@ -18726,6 +19485,21 @@ packages: tsafe: 1.4.1 dev: false + /execa/4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + /execa/5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -18942,9 +19716,21 @@ packages: - supports-color dev: false + /fast-content-type-parse/1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + dev: true + + /fast-decode-uri-component/1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + dev: true + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + /fast-diff/1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + /fast-equals/5.0.1: resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} engines: {node: '>=6.0.0'} @@ -19014,6 +19800,17 @@ packages: /fast-json-stable-stringify/2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + /fast-json-stringify/5.8.0: + resolution: {integrity: sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ==} + dependencies: + '@fastify/deepmerge': 1.3.0 + ajv: 8.12.0 + ajv-formats: 2.1.1 + fast-deep-equal: 3.1.3 + fast-uri: 2.2.0 + rfdc: 1.3.0 + dev: true + /fast-levenshtein/2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -19025,14 +19822,55 @@ packages: resolution: {integrity: sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==} dev: false + /fast-querystring/1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + dependencies: + fast-decode-uri-component: 1.0.1 + dev: true + + /fast-redact/3.3.0: + resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} + engines: {node: '>=6'} + dev: true + + /fast-safe-stringify/2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + /fast-shallow-equal/1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} dev: false + /fast-uri/2.2.0: + resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} + dev: true + /fastest-stable-stringify/2.0.2: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} dev: false + /fastify/4.23.2: + resolution: {integrity: sha512-WFSxsHES115svC7NrerNqZwwM0UOxbC/P6toT9LRHgAAFvG7o2AN5W+H4ihCtOGuYXjZf4z+2jXC89rVEoPWOA==} + dependencies: + '@fastify/ajv-compiler': 3.5.0 + '@fastify/error': 3.4.0 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.2.1 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.8.0 + find-my-way: 7.6.2 + light-my-request: 5.11.0 + pino: 8.15.6 + process-warning: 2.2.0 + proxy-addr: 2.0.7 + rfdc: 1.3.0 + secure-json-parse: 2.7.0 + semver: 7.5.4 + toad-cache: 3.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /fastq/1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -19158,6 +19996,15 @@ packages: pkg-dir: 4.2.0 dev: true + /find-my-way/7.6.2: + resolution: {integrity: sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 2.0.0 + dev: true + /find-up/3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -19264,7 +20111,7 @@ packages: signal-exit: 4.1.0 dev: false - /fork-ts-checker-webpack-plugin/7.3.0_vf4xkga2qinmx3cxkwrybccrqy: + /fork-ts-checker-webpack-plugin/7.3.0_vf3ejk3u7gfag4p4x6gqje5yuq: resolution: {integrity: sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA==} engines: {node: '>=12.13.0', yarn: '>=1.0.0'} peerDependencies: @@ -19284,11 +20131,34 @@ packages: memfs: 3.5.3 minimatch: 3.1.2 node-abort-controller: 3.1.1 - schema-utils: 3.1.2 + schema-utils: 3.3.0 semver: 7.5.4 tapable: 2.2.1 typescript: 4.9.4 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu + dev: true + + /fork-ts-checker-webpack-plugin/8.0.0_dtthwp2bsqb7yvb7hoeealjg4i: + resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + dependencies: + '@babel/code-frame': 7.22.13 + chalk: 4.1.2 + chokidar: 3.5.3 + cosmiconfig: 7.1.0 + deepmerge: 4.2.2 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.1.2 + semver: 7.5.4 + tapable: 2.2.1 + typescript: 5.2.2 + webpack: 5.88.2 dev: true /form-data-encoder/1.7.2: @@ -19345,6 +20215,15 @@ packages: fetch-blob: 3.2.0 dev: false + /formidable/2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.11.0 + dev: true + /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -19776,6 +20655,16 @@ packages: minimatch: 5.1.2 once: 1.4.0 + /glob/9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.10.1 + dev: true + /global-dirs/3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} @@ -20020,6 +20909,11 @@ packages: is-glob: 3.1.0 dev: false + /has-own-prop/2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + dev: true + /has-property-descriptors/1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: @@ -20194,6 +21088,11 @@ packages: xtend: 4.0.2 dev: false + /hexoid/1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + dev: true + /highlight.run/7.2.0: resolution: {integrity: sha512-vTg0EWiKxHoRO1w9zm+KuPWkOAJN2Mh/8LRfgVqMjrR+sSfdOjuJeKBa9wfmV1GssrGCWNLwnuygDLGIJ4BSCw==} dev: false @@ -20269,7 +21168,7 @@ packages: /html-void-elements/2.0.1: resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} - /html-webpack-plugin/5.5.1_webpack@5.80.0: + /html-webpack-plugin/5.5.1_webpack@5.88.2: resolution: {integrity: sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==} engines: {node: '>=10.13.0'} peerDependencies: @@ -20280,7 +21179,7 @@ packages: lodash: 4.17.21 pretty-error: 4.0.0 tapable: 2.2.1 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu dev: true /htmlparser2/6.1.0: @@ -20381,6 +21280,11 @@ packages: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} dev: false + /human-signals/1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + dev: true + /human-signals/2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -20536,6 +21440,27 @@ packages: fast-loops: 1.1.3 dev: false + /inquirer/8.2.4: + resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + /inquirer/8.2.5: resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} engines: {node: '>=12.0.0'} @@ -20557,6 +21482,27 @@ packages: wrap-ansi: 7.0.0 dev: true + /inquirer/8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + dev: true + /inquirer/9.1.4: resolution: {integrity: sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA==} engines: {node: '>=12.0.0'} @@ -21159,6 +22105,10 @@ packages: istanbul-lib-report: 3.0.0 dev: true + /iterare/1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + /jackspeak/2.3.0: resolution: {integrity: sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg==} engines: {node: '>=14'} @@ -21260,6 +22210,35 @@ packages: - ts-node dev: true + /jest-cli/29.6.2_rqg5pcrfq4i6lanvxwhlyynole: + resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.2_ts-node@10.9.1 + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + import-local: 3.1.0 + jest-config: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole + jest-util: 29.6.2 + jest-validate: 29.6.2 + prompts: 2.4.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config/29.6.2_@types+node@16.18.11: resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -21340,6 +22319,47 @@ packages: - supports-color dev: true + /jest-config/29.6.2_rqg5pcrfq4i6lanvxwhlyynole: + resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.22.17 + '@jest/test-sequencer': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 20.6.0 + babel-jest: 29.6.2_@babel+core@7.22.17 + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 29.6.2 + jest-environment-node: 29.6.2 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-runner: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.6.2 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1_kpuv3buz4xyqturyqxj2gejvma + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff/29.6.2: resolution: {integrity: sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -21680,6 +22700,27 @@ packages: - ts-node dev: true + /jest/29.6.2_rqg5pcrfq4i6lanvxwhlyynole: + resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.2_ts-node@10.9.1 + '@jest/types': 29.6.1 + import-local: 3.1.0 + jest-cli: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jiti/1.18.2: resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} hasBin: true @@ -22027,6 +23068,14 @@ packages: prelude-ls: 1.2.1 type-check: 0.4.0 + /light-my-request/5.11.0: + resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==} + dependencies: + cookie: 0.5.0 + process-warning: 2.2.0 + set-cookie-parser: 2.5.1 + dev: true + /lilconfig/2.0.6: resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} engines: {node: '>=10'} @@ -22258,7 +23307,6 @@ packages: /lru-cache/10.0.1: resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} engines: {node: 14 || >=16.14} - dev: false /lru-cache/4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -22299,6 +23347,25 @@ packages: hasBin: true dev: true + /macos-release/2.5.1: + resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} + engines: {node: '>=6'} + dev: true + + /magic-string/0.30.0: + resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /magic-string/0.30.1: + resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /magic-string/0.30.3: resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} engines: {node: '>=12'} @@ -23116,6 +24183,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch/8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimatch/9.0.0: resolution: {integrity: sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==} engines: {node: '>=16 || 14 >=14.17'} @@ -23195,6 +24269,11 @@ packages: engines: {node: '>=8'} dependencies: yallist: 4.0.0 + dev: true + + /minipass/4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} /minipass/5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} @@ -23204,7 +24283,6 @@ packages: /minipass/7.0.3: resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} engines: {node: '>=16 || 14 >=14.17'} - dev: false /minizlib/2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -23364,6 +24442,18 @@ packages: - supports-color dev: true + /multer/1.4.4-lts.1: + resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + /mute-stream/0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -23662,6 +24752,12 @@ packages: engines: {node: '>=10.5.0'} dev: false + /node-emoji/1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + dependencies: + lodash: 4.17.21 + dev: true + /node-fetch-native/1.0.1: resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==} dev: false @@ -24129,6 +25225,11 @@ packages: - encoding dev: false + /on-exit-leak-free/2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: true + /on-finished/2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -24265,7 +25366,7 @@ packages: bl: 4.1.0 chalk: 4.1.2 cli-cursor: 3.1.0 - cli-spinners: 2.7.0 + cli-spinners: 2.9.1 is-interactive: 1.0.0 is-unicode-supported: 0.1.0 log-symbols: 4.1.0 @@ -24301,6 +25402,14 @@ packages: string-width: 6.1.0 strip-ansi: 7.1.0 + /os-name/4.0.1: + resolution: {integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==} + engines: {node: '>=10'} + dependencies: + macos-release: 2.5.1 + windows-release: 4.0.0 + dev: true + /os-tmpdir/1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -24645,11 +25754,13 @@ packages: dependencies: lru-cache: 10.0.1 minipass: 7.0.3 - dev: false /path-to-regexp/0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + /path-to-regexp/3.2.0: + resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + /path-to-regexp/6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} @@ -24781,6 +25892,34 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + /pino-abstract-transport/1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + dependencies: + readable-stream: 4.4.2 + split2: 4.2.0 + dev: true + + /pino-std-serializers/6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + dev: true + + /pino/8.15.6: + resolution: {integrity: sha512-GuxHr61R0ZFD1npu58tB3a3FSVjuy21OwN/haw4OuKiZBL63Pg11Y51WWeD52RENS2mjwPZOwt+2OQOSkck6kQ==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.3.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.1.0 + pino-std-serializers: 6.2.2 + process-warning: 2.2.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.6.0 + thread-stream: 2.4.1 + dev: true + /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} @@ -24818,6 +25957,11 @@ packages: hasBin: true dev: true + /pluralize/8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + /polished/4.2.2: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'} @@ -25320,6 +26464,13 @@ packages: engines: {node: '>=4'} dev: true + /prettier-linter-helpers/1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + /prettier-plugin-tailwindcss/0.3.0_prettier@2.8.8: resolution: {integrity: sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==} engines: {node: '>=12.17.0'} @@ -25464,6 +26615,10 @@ packages: /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + /process-warning/2.2.0: + resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} + dev: true + /process/0.10.1: resolution: {integrity: sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA==} engines: {node: '>= 0.6.0'} @@ -25658,6 +26813,10 @@ packages: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} optional: true + /quick-format-unescaped/4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: true + /quick-lru/4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} @@ -25690,6 +26849,15 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body/2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + /rc-config-loader/4.1.3: resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==} dependencies: @@ -26096,12 +27264,28 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-stream/4.4.2: + resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: true + /readdirp/3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + /real-require/0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: true + /recast/0.21.5: resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} engines: {node: '>= 4'} @@ -26172,6 +27356,9 @@ packages: postcss-value-parser: 3.3.1 dev: false + /reflect-metadata/0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + /regenerate-unicode-properties/10.1.0: resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} engines: {node: '>=4'} @@ -26504,7 +27691,6 @@ packages: /repeat-string/1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} - dev: false /replicate/0.18.1: resolution: {integrity: sha512-JFK5qWL7AAajsIjtkW/nTaoX+yKp8zkaANe+pQpBh9RjH5vIy6/tn/sVNlRF1z5bCJLupshJBHFgnHRdWjbKXg==} @@ -26696,6 +27882,11 @@ packages: engines: {node: '>=0.12'} dev: false + /ret/0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + dev: true + /retext-latin/3.1.0: resolution: {integrity: sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==} dependencies: @@ -26741,6 +27932,10 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rfdc/1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + /right-align/0.1.3: resolution: {integrity: sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==} engines: {node: '>=0.10.0'} @@ -26767,6 +27962,14 @@ packages: dependencies: glob: 7.2.3 + /rimraf/4.4.1: + resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 9.3.5 + dev: true + /rimraf/5.0.1: resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} engines: {node: '>=14'} @@ -26824,7 +28027,6 @@ packages: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: tslib: 2.6.2 - dev: true /sade/1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} @@ -26855,6 +28057,17 @@ packages: ret: 0.1.15 dev: false + /safe-regex2/2.0.0: + resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + dependencies: + ret: 0.2.2 + dev: true + + /safe-stable-stringify/2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: true + /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -26898,7 +28111,16 @@ packages: resolution: {integrity: sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/json-schema': 7.0.11 + '@types/json-schema': 7.0.13 + ajv: 6.12.6 + ajv-keywords: 3.5.2_ajv@6.12.6 + dev: true + + /schema-utils/3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.13 ajv: 6.12.6 ajv-keywords: 3.5.2_ajv@6.12.6 dev: true @@ -26907,7 +28129,7 @@ packages: resolution: {integrity: sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==} engines: {node: '>= 12.13.0'} dependencies: - '@types/json-schema': 7.0.11 + '@types/json-schema': 7.0.13 ajv: 8.12.0 ajv-formats: 2.1.1 ajv-keywords: 5.1.0_ajv@8.12.0 @@ -26925,6 +28147,10 @@ packages: extend-shallow: 2.0.1 kind-of: 6.0.3 + /secure-json-parse/2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: true + /selderee/0.10.0: resolution: {integrity: sha512-DEL/RW/f4qLw/NrVg97xKaEBC8IpzIG2fvxnzCp3Z4yk4jQ3MXom+Imav9wApjxX2dfS3eW7x0DXafJr85i39A==} dependencies: @@ -26956,6 +28182,7 @@ packages: /semver/6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true + dev: true /semver/6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -27276,7 +28503,7 @@ packages: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /snakecase-keys/5.4.4: @@ -27348,6 +28575,12 @@ packages: ip: 2.0.0 smart-buffer: 4.2.0 + /sonic-boom/3.6.0: + resolution: {integrity: sha512-5Rs7m4IO/mW1WHouC6q6PGJsXO6hSAduwB3ltTsKaDU0Bd7sc5QEUK/jF0YL583g3BG7QV0Dg0rQNZrwZhY6Xg==} + dependencies: + atomic-sleep: 1.0.0 + dev: true + /sort-object-keys/1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} dev: true @@ -27479,7 +28712,6 @@ packages: /split2/4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - dev: false /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -27830,13 +29062,13 @@ packages: webpack: ^5.0.0 dev: true - /style-loader/3.3.2_webpack@5.80.0: + /style-loader/3.3.2_webpack@5.88.2: resolution: {integrity: sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 dependencies: - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu dev: true /style-mod/4.0.0: @@ -27919,6 +29151,34 @@ packages: openapi-fetch: 0.6.2 dev: false + /superagent/8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + dependencies: + component-emitter: 1.3.0 + cookiejar: 2.1.4 + debug: 4.3.4 + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 2.1.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.11.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /supertest/6.3.3: + resolution: {integrity: sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==} + engines: {node: '>=6.4.0'} + dependencies: + methods: 1.1.2 + superagent: 8.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /supports-color/5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -27950,6 +29210,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /symbol-observable/4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + dev: true + /synchronous-promise/2.0.17: resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} dev: true @@ -28125,7 +29390,7 @@ packages: dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 - minipass: 4.0.0 + minipass: 4.2.8 minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 @@ -28172,7 +29437,7 @@ packages: supports-hyperlinks: 2.3.0 dev: false - /terser-webpack-plugin/5.3.7_5jgfnkl7fjuhakmzbjzotue6o4: + /terser-webpack-plugin/5.3.7_nww33inhqu3uc3cp573wawoccu: resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -28192,10 +29457,34 @@ packages: '@swc/core': 1.3.26 esbuild: 0.15.18 jest-worker: 27.5.1 - schema-utils: 3.1.2 + schema-utils: 3.3.0 serialize-javascript: 6.0.1 terser: 5.17.1 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu + dev: true + + /terser-webpack-plugin/5.3.7_webpack@5.88.2: + resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.1 + terser: 5.17.1 + webpack: 5.88.2 dev: true /terser/5.17.1: @@ -28232,6 +29521,12 @@ packages: dependencies: any-promise: 1.3.0 + /thread-stream/2.4.1: + resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + dependencies: + real-require: 0.2.0 + dev: true + /thriftrw/3.11.4: resolution: {integrity: sha512-UcuBd3eanB3T10nXWRRMwfwoaC6VMk7qe3/5YIWP2Jtw+EbHqJ0p1/K3x8ixiR5dozKSSfcg1W+0e33G1Di3XA==} engines: {node: '>= 0.10.x'} @@ -28353,6 +29648,11 @@ packages: safe-regex: 1.1.0 dev: false + /toad-cache/3.3.0: + resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==} + engines: {node: '>=12'} + dev: true + /toggle-selection/1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false @@ -28410,6 +29710,15 @@ packages: /trough/2.1.0: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} + /ts-api-utils/1.0.3_typescript@5.2.2: + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: true + /ts-dedent/2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -28422,6 +29731,39 @@ packages: /ts-interface-checker/0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + /ts-jest/29.1.1_uvjn2wawyirmlmi655a42v3ske: + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole + jest-util: 29.6.2 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.2.2 + yargs-parser: 21.1.1 + dev: true + /ts-jest/29.1.1_xlkreayjyan5lfqjb5gzdqf3my: resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -28469,6 +29811,20 @@ packages: typescript: 5.0.4 dev: true + /ts-loader/9.4.4_typescript@5.2.2: + resolution: {integrity: sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.13.0 + micromatch: 4.0.5 + semver: 7.5.4 + typescript: 5.2.2 + dev: true + /ts-node/10.9.1_fodzh64fuekdilycyvke2qmf2e: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -28531,6 +29887,37 @@ packages: yn: 3.1.1 dev: true + /ts-node/10.9.1_kpuv3buz4xyqturyqxj2gejvma: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 20.6.0 + acorn: 8.10.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.2.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node/10.9.1_xj5cs2fmhcigm4w5bhhtewqeja: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -28590,6 +29977,15 @@ packages: typescript: 4.9.5 dev: false + /tsconfig-paths-webpack-plugin/4.1.0: + resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==} + engines: {node: '>=10.13.0'} + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.13.0 + tsconfig-paths: 4.2.0 + dev: true + /tsconfig-paths/3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} dependencies: @@ -28607,6 +30003,15 @@ packages: strip-bom: 3.0.0 dev: true + /tsconfig-paths/4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + dependencies: + json5: 2.2.3 + minimist: 1.2.7 + strip-bom: 3.0.0 + dev: true + /tsconfig-resolver/3.0.1: resolution: {integrity: sha512-ZHqlstlQF449v8glscGRXzL6l2dZvASPCdXJRWG4gHEZlUVx2Jtmr+a2zeVG4LCsKhDXKRj5R3h0C/98UcVAQg==} dependencies: @@ -29093,7 +30498,6 @@ packages: /typedarray/0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - dev: true /typescript/4.9.4: resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} @@ -29132,6 +30536,12 @@ packages: dev: true optional: true + /uid/2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + dependencies: + '@lukeed/csprng': 1.1.0 + /ulid/2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} hasBin: true @@ -30237,7 +31647,7 @@ packages: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true - /webpack-dev-middleware/5.3.3_webpack@5.80.0: + /webpack-dev-middleware/5.3.3_webpack@5.88.2: resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -30248,7 +31658,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.0.1 - webpack: 5.80.0_uhpfu7q6noim4yjdo6qt2aajgu + webpack: 5.88.2_uhpfu7q6noim4yjdo6qt2aajgu dev: true /webpack-hot-middleware/2.25.3: @@ -30259,6 +31669,11 @@ packages: strip-ansi: 6.0.1 dev: true + /webpack-node-externals/3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + dev: true + /webpack-sources/3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -30268,8 +31683,8 @@ packages: resolution: {integrity: sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==} dev: true - /webpack/5.80.0_uhpfu7q6noim4yjdo6qt2aajgu: - resolution: {integrity: sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==} + /webpack/5.88.2: + resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -30284,10 +31699,10 @@ packages: '@webassemblyjs/wasm-edit': 1.11.5 '@webassemblyjs/wasm-parser': 1.11.5 acorn: 8.10.0 - acorn-import-assertions: 1.8.0_acorn@8.10.0 + acorn-import-assertions: 1.9.0_acorn@8.10.0 browserslist: 4.21.10 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.13.0 + enhanced-resolve: 5.15.0 es-module-lexer: 1.3.1 eslint-scope: 5.1.1 events: 3.3.0 @@ -30297,9 +31712,49 @@ packages: loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 3.1.2 + schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.7_5jgfnkl7fjuhakmzbjzotue6o4 + terser-webpack-plugin: 5.3.7_webpack@5.88.2 + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /webpack/5.88.2_uhpfu7q6noim4yjdo6qt2aajgu: + resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.4 + '@types/estree': 1.0.0 + '@webassemblyjs/ast': 1.11.5 + '@webassemblyjs/wasm-edit': 1.11.5 + '@webassemblyjs/wasm-parser': 1.11.5 + acorn: 8.10.0 + acorn-import-assertions: 1.9.0_acorn@8.10.0 + browserslist: 4.21.10 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.15.0 + es-module-lexer: 1.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.10 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.7_nww33inhqu3uc3cp573wawoccu watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -30433,6 +31888,13 @@ packages: dependencies: string-width: 5.1.2 + /windows-release/4.0.0: + resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==} + engines: {node: '>=10'} + dependencies: + execa: 4.1.0 + dev: true + /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -30452,7 +31914,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: false /wrap-ansi/7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} diff --git a/references/nestjs-example/.env.example b/references/nestjs-example/.env.example new file mode 100644 index 000000000..4fdb8311f --- /dev/null +++ b/references/nestjs-example/.env.example @@ -0,0 +1,3 @@ + +TRIGGER_API_KEY=tr_dev_test-api-key +TRIGGER_API_URL=http://localhost:3030 diff --git a/references/nestjs-example/.eslintrc.js b/references/nestjs-example/.eslintrc.js new file mode 100644 index 000000000..259de13c7 --- /dev/null +++ b/references/nestjs-example/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/references/nestjs-example/.gitignore b/references/nestjs-example/.gitignore new file mode 100644 index 000000000..2d33ffcac --- /dev/null +++ b/references/nestjs-example/.gitignore @@ -0,0 +1,38 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +.env + diff --git a/references/nestjs-example/.prettierrc b/references/nestjs-example/.prettierrc new file mode 100644 index 000000000..dcb72794f --- /dev/null +++ b/references/nestjs-example/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/references/nestjs-example/README.md b/references/nestjs-example/README.md new file mode 100644 index 000000000..f5aa86c5d --- /dev/null +++ b/references/nestjs-example/README.md @@ -0,0 +1,73 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + + Support us + +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Installation + +```bash +$ pnpm install +``` + +## Running the app + +```bash +# development +$ pnpm run start + +# watch mode +$ pnpm run start:dev + +# production mode +$ pnpm run start:prod +``` + +## Test + +```bash +# unit tests +$ pnpm run test + +# e2e tests +$ pnpm run test:e2e + +# test coverage +$ pnpm run test:cov +``` + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](LICENSE). diff --git a/references/nestjs-example/nest-cli.json b/references/nestjs-example/nest-cli.json new file mode 100644 index 000000000..f9aa683b1 --- /dev/null +++ b/references/nestjs-example/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/references/nestjs-example/package.json b/references/nestjs-example/package.json new file mode 100644 index 000000000..78769c86e --- /dev/null +++ b/references/nestjs-example/package.json @@ -0,0 +1,72 @@ +{ + "name": "nestjs-example", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/config": "^3.0.1", + "@nestjs/platform-express": "^10.0.0", + "reflect-metadata": "^0.1.13", + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/nestjs": "workspace:*", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^2.0.12", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/references/nestjs-example/src/app.controller.ts b/references/nestjs-example/src/app.controller.ts new file mode 100644 index 000000000..462bab4c7 --- /dev/null +++ b/references/nestjs-example/src/app.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Get } from '@nestjs/common'; +import { InjectTriggerDevClient } from '@trigger.dev/nestjs'; +import { eventTrigger, TriggerClient } from '@trigger.dev/sdk'; + +@Controller() +export class AppController { + constructor( + @InjectTriggerDevClient() private readonly client: TriggerClient, + ) { + this.client.defineJob({ + id: 'test-job', + name: 'Test Job One', + version: '0.0.1', + trigger: eventTrigger({ + name: 'test.event', + }), + run: async (payload, io) => { + await io.logger.info('Hello world!', { payload }); + + return { + message: 'Hello world!', + }; + }, + }); + } + + @Get() + getHello(): string { + return `Running Trigger.dev with client-id ${this.client.id}`; + } +} diff --git a/references/nestjs-example/src/app.module.ts b/references/nestjs-example/src/app.module.ts new file mode 100644 index 000000000..ad5755575 --- /dev/null +++ b/references/nestjs-example/src/app.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TriggerDevModule } from '@trigger.dev/nestjs'; +import { AppController } from './app.controller'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TriggerDevModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + id: 'my-nest-app', + apiKey: config.getOrThrow('TRIGGER_API_KEY'), + apiUrl: config.getOrThrow('TRIGGER_API_URL'), + verbose: false, + ioLogLocalEnabled: true, + }), + }), + ], + controllers: [AppController], +}) +export class AppModule {} diff --git a/references/nestjs-example/src/main.ts b/references/nestjs-example/src/main.ts new file mode 100644 index 000000000..13cad38cf --- /dev/null +++ b/references/nestjs-example/src/main.ts @@ -0,0 +1,8 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/references/nestjs-example/test/app.e2e-spec.ts b/references/nestjs-example/test/app.e2e-spec.ts new file mode 100644 index 000000000..50cda6233 --- /dev/null +++ b/references/nestjs-example/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/references/nestjs-example/test/jest-e2e.json b/references/nestjs-example/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/references/nestjs-example/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/references/nestjs-example/tsconfig.build.json b/references/nestjs-example/tsconfig.build.json new file mode 100644 index 000000000..64f86c6bd --- /dev/null +++ b/references/nestjs-example/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/references/nestjs-example/tsconfig.json b/references/nestjs-example/tsconfig.json new file mode 100644 index 000000000..95f5641cf --- /dev/null +++ b/references/nestjs-example/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} From 4b641b25a6e23f5c8f8e1efa2232219fe964996d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:54:07 +0100 Subject: [PATCH 18/79] chore: Update version for release (#571) Co-authored-by: github-actions[bot] --- .changeset/fair-waves-hug.md | 5 ----- .changeset/lazy-parrots-retire.md | 17 ----------------- .changeset/orange-falcons-smile.md | 5 ----- .changeset/thick-pugs-give.md | 5 ----- integrations/airtable/CHANGELOG.md | 10 ++++++++++ integrations/airtable/package.json | 6 +++--- integrations/github/CHANGELOG.md | 10 ++++++++++ integrations/github/package.json | 6 +++--- integrations/linear/CHANGELOG.md | 10 ++++++++++ integrations/linear/package.json | 6 +++--- integrations/openai/CHANGELOG.md | 9 +++++++++ integrations/openai/package.json | 6 +++--- integrations/plain/CHANGELOG.md | 9 +++++++++ integrations/plain/package.json | 6 +++--- integrations/replicate/CHANGELOG.md | 10 ++++++++++ integrations/replicate/package.json | 6 +++--- integrations/resend/CHANGELOG.md | 9 +++++++++ integrations/resend/package.json | 6 +++--- integrations/sendgrid/CHANGELOG.md | 9 +++++++++ integrations/sendgrid/package.json | 6 +++--- integrations/slack/CHANGELOG.md | 9 +++++++++ integrations/slack/package.json | 4 ++-- integrations/stripe/CHANGELOG.md | 10 ++++++++++ integrations/stripe/package.json | 6 +++--- integrations/supabase/CHANGELOG.md | 10 ++++++++++ integrations/supabase/package.json | 6 +++--- integrations/typeform/CHANGELOG.md | 10 ++++++++++ integrations/typeform/package.json | 6 +++--- packages/astro/CHANGELOG.md | 8 ++++++++ packages/astro/package.json | 4 ++-- packages/cli/CHANGELOG.md | 7 +++++++ packages/cli/package.json | 2 +- packages/core/CHANGELOG.md | 6 ++++++ packages/core/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 2 ++ packages/eslint-plugin/package.json | 2 +- packages/express/CHANGELOG.md | 8 ++++++++ packages/express/package.json | 6 +++--- packages/integration-kit/CHANGELOG.md | 2 ++ packages/integration-kit/package.json | 2 +- packages/nestjs/CHANGELOG.md | 10 ++++++++++ packages/nestjs/package.json | 6 +++--- packages/nextjs/CHANGELOG.md | 8 ++++++++ packages/nextjs/package.json | 4 ++-- packages/react/CHANGELOG.md | 8 ++++++++ packages/react/package.json | 4 ++-- packages/remix/CHANGELOG.md | 8 ++++++++ packages/remix/package.json | 4 ++-- packages/testing/CHANGELOG.md | 10 ++++++++++ packages/testing/package.json | 2 +- packages/trigger-sdk/CHANGELOG.md | 9 +++++++++ packages/trigger-sdk/package.json | 4 ++-- 52 files changed, 257 insertions(+), 88 deletions(-) delete mode 100644 .changeset/fair-waves-hug.md delete mode 100644 .changeset/lazy-parrots-retire.md delete mode 100644 .changeset/orange-falcons-smile.md delete mode 100644 .changeset/thick-pugs-give.md create mode 100644 packages/nestjs/CHANGELOG.md diff --git a/.changeset/fair-waves-hug.md b/.changeset/fair-waves-hug.md deleted file mode 100644 index 68adfefae..000000000 --- a/.changeset/fair-waves-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/nestjs": patch ---- - -First release of NestJS adaptor diff --git a/.changeset/lazy-parrots-retire.md b/.changeset/lazy-parrots-retire.md deleted file mode 100644 index 7b217b63b..000000000 --- a/.changeset/lazy-parrots-retire.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@trigger.dev/replicate": patch -"@trigger.dev/airtable": patch -"@trigger.dev/supabase": patch -"@trigger.dev/typeform": patch -"@trigger.dev/sdk": patch -"@trigger.dev/github": patch -"@trigger.dev/linear": patch -"@trigger.dev/stripe": patch -"@trigger.dev/slack": patch -"@trigger.dev/testing": patch -"@trigger.dev/react": patch -"@trigger.dev/core": patch -"@trigger.dev/cli": patch ---- - -upgrade zod to 3.22.3 diff --git a/.changeset/orange-falcons-smile.md b/.changeset/orange-falcons-smile.md deleted file mode 100644 index 7651f714f..000000000 --- a/.changeset/orange-falcons-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/sdk": patch ---- - -allow cancelling jobs from trigger-client diff --git a/.changeset/thick-pugs-give.md b/.changeset/thick-pugs-give.md deleted file mode 100644 index 1ee60d0c6..000000000 --- a/.changeset/thick-pugs-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/cli": patch ---- - -Removed log when a file is changed diff --git a/integrations/airtable/CHANGELOG.md b/integrations/airtable/CHANGELOG.md index 1715a4efa..20c708da1 100644 --- a/integrations/airtable/CHANGELOG.md +++ b/integrations/airtable/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/airtable +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/airtable/package.json b/integrations/airtable/package.json index e3eb7b074..ab2a0e303 100644 --- a/integrations/airtable/package.json +++ b/integrations/airtable/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/airtable", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev integration for airtable", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "airtable": "^0.12.1", "zod": "3.22.3" }, diff --git a/integrations/github/CHANGELOG.md b/integrations/github/CHANGELOG.md index 976085c29..8179b15ec 100644 --- a/integrations/github/CHANGELOG.md +++ b/integrations/github/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/github +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/github/package.json b/integrations/github/package.json index fbe5c0a92..2f3a27f90 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/github", - "version": "2.1.8", + "version": "2.1.9", "description": "The official GitHub integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,8 +29,8 @@ "@octokit/request": "^6.2.5", "@octokit/request-error": "^4.0.1", "@octokit/webhooks": "^10.4.0", - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "octokit": "^2.0.14", "zod": "3.22.3" }, diff --git a/integrations/linear/CHANGELOG.md b/integrations/linear/CHANGELOG.md index 18734aba1..14c507d61 100644 --- a/integrations/linear/CHANGELOG.md +++ b/integrations/linear/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/linear +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/linear/package.json b/integrations/linear/package.json index 9514fc862..94c63d85a 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/linear", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev integration for @linear/sdk", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@linear/sdk": "^8.0.0", - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "zod": "3.22.3" }, "engines": { diff --git a/integrations/openai/CHANGELOG.md b/integrations/openai/CHANGELOG.md index 63df75549..ae5647a54 100644 --- a/integrations/openai/CHANGELOG.md +++ b/integrations/openai/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/slack +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/openai/package.json b/integrations/openai/package.json index e13607fe3..50e5dd696 100644 --- a/integrations/openai/package.json +++ b/integrations/openai/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/openai", - "version": "2.1.8", + "version": "2.1.9", "description": "The official OpenAI integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,8 +25,8 @@ }, "dependencies": { "openai": "^4.2.0", - "@trigger.dev/sdk": "workspace:^2.1.8", - "@trigger.dev/integration-kit": "workspace:^2.1.8" + "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.1.9" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/plain/CHANGELOG.md b/integrations/plain/CHANGELOG.md index 94d24d06d..bb7b0877e 100644 --- a/integrations/plain/CHANGELOG.md +++ b/integrations/plain/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/plain +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/plain/package.json b/integrations/plain/package.json index b0c8b0abe..955778510 100644 --- a/integrations/plain/package.json +++ b/integrations/plain/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/plain", - "version": "2.1.8", + "version": "2.1.9", "description": "The official Plain.com integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,8 +24,8 @@ "build:tsup": "tsup" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "@team-plain/typescript-sdk": "^2.7.0" }, "engines": { diff --git a/integrations/replicate/CHANGELOG.md b/integrations/replicate/CHANGELOG.md index 9edd079df..500653bc9 100644 --- a/integrations/replicate/CHANGELOG.md +++ b/integrations/replicate/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/replicate +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/replicate/package.json b/integrations/replicate/package.json index 246ee7614..400c01310 100644 --- a/integrations/replicate/package.json +++ b/integrations/replicate/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/replicate", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev integration for replicate", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "replicate": "^0.18.1", "zod": "3.22.3" }, diff --git a/integrations/resend/CHANGELOG.md b/integrations/resend/CHANGELOG.md index 1ed135558..76b2223f6 100644 --- a/integrations/resend/CHANGELOG.md +++ b/integrations/resend/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/resend +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/resend/package.json b/integrations/resend/package.json index 3caae35d4..a5b68952a 100644 --- a/integrations/resend/package.json +++ b/integrations/resend/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/resend", - "version": "2.1.8", + "version": "2.1.9", "description": "The official Resend.com integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,8 +24,8 @@ "build:tsup": "tsup" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "resend": "^1.0.0" }, "engines": { diff --git a/integrations/sendgrid/CHANGELOG.md b/integrations/sendgrid/CHANGELOG.md index dc96cf7c9..1a7266733 100644 --- a/integrations/sendgrid/CHANGELOG.md +++ b/integrations/sendgrid/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/sendgrid +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/sendgrid/package.json b/integrations/sendgrid/package.json index 82e0eb65c..38c628a98 100644 --- a/integrations/sendgrid/package.json +++ b/integrations/sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sendgrid", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev integration for @sendgrid/mail", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@sendgrid/mail": "^7.7.0", - "@trigger.dev/sdk": "workspace:^2.1.8", - "@trigger.dev/integration-kit": "workspace:^2.1.8" + "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.1.9" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/slack/CHANGELOG.md b/integrations/slack/CHANGELOG.md index 4637acd33..0e52fb368 100644 --- a/integrations/slack/CHANGELOG.md +++ b/integrations/slack/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/slack +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/slack/package.json b/integrations/slack/package.json index ae93464d4..a0b698ddd 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/slack", - "version": "2.1.8", + "version": "2.1.9", "description": "The official Slack integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@slack/web-api": "^6.8.1", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.9", "zod": "3.22.3" }, "engines": { diff --git a/integrations/stripe/CHANGELOG.md b/integrations/stripe/CHANGELOG.md index 49353899f..b52b2c2d8 100644 --- a/integrations/stripe/CHANGELOG.md +++ b/integrations/stripe/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/stripe +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/stripe/package.json b/integrations/stripe/package.json index c09c00e87..02df13289 100644 --- a/integrations/stripe/package.json +++ b/integrations/stripe/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/stripe", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev integration for stripe", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "stripe": "^12.14.0", "zod": "3.22.3" }, diff --git a/integrations/supabase/CHANGELOG.md b/integrations/supabase/CHANGELOG.md index ee00ef81e..70c8fa887 100644 --- a/integrations/supabase/CHANGELOG.md +++ b/integrations/supabase/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/supabase +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/supabase/package.json b/integrations/supabase/package.json index 69a736d6e..aa8be7d0f 100644 --- a/integrations/supabase/package.json +++ b/integrations/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/supabase", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev integration for @supabase/supabase-js", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@supabase/supabase-js": "^2.26.0", - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "supabase-management-js": "^0.1.4", "zod": "3.22.3" }, diff --git a/integrations/typeform/CHANGELOG.md b/integrations/typeform/CHANGELOG.md index c18dfca45..90016bd04 100644 --- a/integrations/typeform/CHANGELOG.md +++ b/integrations/typeform/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/typeform +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/integrations/typeform/package.json b/integrations/typeform/package.json index 94473d478..13eb35c33 100644 --- a/integrations/typeform/package.json +++ b/integrations/typeform/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/typeform", - "version": "2.1.8", + "version": "2.1.9", "description": "The official Typeform integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,8 +25,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.8", - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "@typeform/api-client": "^1.8.0", "zod": "3.22.3" }, diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 40b0cc74e..443e79831 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/astro +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 6dbe60898..e8d60d4cb 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,7 +1,7 @@ { "name": "@trigger.dev/astro", "description": "An Astro-native integration for Trigger.dev background jobs platform", - "version": "2.1.8", + "version": "2.1.9", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ @@ -20,7 +20,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.8" + "@trigger.dev/sdk": "workspace:^2.1.9" }, "devDependencies": { "astro": "^3.0.12", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 4a22e3176..4a61f3d35 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # create-trigger +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- 914745f6: Removed log when a file is changed + ## 2.1.8 ## 2.1.7 diff --git a/packages/cli/package.json b/packages/cli/package.json index 3fac1f96c..b31713fb8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/cli", - "version": "2.1.8", + "version": "2.1.9", "description": "The Trigger.dev CLI", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 92ba3c957..068c67d4c 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # internal-platform +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 + ## 2.1.8 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index c3f8407b6..0e2815c42 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/core", - "version": "2.1.8", + "version": "2.1.9", "description": "Core code used across the Trigger.dev SDK and platform", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index e23577542..1ea6b3bb2 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,7 @@ # @trigger.dev/eslint-plugin +## 2.1.9 + ## 2.1.8 ## 2.1.7 diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 654507bbf..d75dc2b9b 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/eslint-plugin", - "version": "2.1.8", + "version": "2.1.9", "description": "ESLint plugin with trigger.dev best practices", "keywords": [ "eslint", diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md index b392b4bdc..d6426dd2e 100644 --- a/packages/express/CHANGELOG.md +++ b/packages/express/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/express +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/express/package.json b/packages/express/package.json index 220a02416..c98a40c7b 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/express", - "version": "2.1.8", + "version": "2.1.9", "description": "Official Express adapter for Trigger.dev", "license": "MIT", "main": "./dist/index.js", @@ -19,7 +19,7 @@ "./package.json": "./package.json" }, "devDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.9", "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/express": "^4.17.13", @@ -33,7 +33,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.8" + "@trigger.dev/sdk": "workspace:^2.1.9" }, "dependencies": { "@remix-run/web-fetch": "^4.3.5", diff --git a/packages/integration-kit/CHANGELOG.md b/packages/integration-kit/CHANGELOG.md index 814482804..624ef4d38 100644 --- a/packages/integration-kit/CHANGELOG.md +++ b/packages/integration-kit/CHANGELOG.md @@ -1,5 +1,7 @@ # @trigger.dev/integration-kit +## 2.1.9 + ## 2.1.8 ## 2.1.7 diff --git a/packages/integration-kit/package.json b/packages/integration-kit/package.json index 017f60dab..0c1a43307 100644 --- a/packages/integration-kit/package.json +++ b/packages/integration-kit/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/integration-kit", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev Integration Kit has helpers to make creating integrations easier", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/nestjs/CHANGELOG.md b/packages/nestjs/CHANGELOG.md new file mode 100644 index 000000000..4fbd14ca8 --- /dev/null +++ b/packages/nestjs/CHANGELOG.md @@ -0,0 +1,10 @@ +# @trigger.dev/nestjs + +## 2.1.9 + +### Patch Changes + +- a847b492: First release of NestJS adaptor +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 882c78d9b..e837d88b3 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/nestjs", - "version": "2.1.8", + "version": "2.1.9", "description": "Official NestJS adapter for Trigger.dev", "license": "MIT", "main": "./dist/index.js", @@ -19,7 +19,7 @@ "./package.json": "./package.json" }, "devDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.0", + "@trigger.dev/sdk": "workspace:^2.1.9", "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/express": "^4.17.13", @@ -35,7 +35,7 @@ }, "peerDependencies": { "@nestjs/common": ">=10.0.0", - "@trigger.dev/sdk": "workspace:^2.1.0" + "@trigger.dev/sdk": "workspace:^2.1.9" }, "dependencies": { "@nestjs/common": "^10.2.4", diff --git a/packages/nextjs/CHANGELOG.md b/packages/nextjs/CHANGELOG.md index 4ff5c4834..64b5eeb8a 100644 --- a/packages/nextjs/CHANGELOG.md +++ b/packages/nextjs/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/nextjs +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 8b1c25a18..d19ade712 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/nextjs", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev Next.js integration", "license": "MIT", "main": "./dist/index.js", @@ -34,7 +34,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.9", "next": ">=12.0.0 <14.0.0" }, "dependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 33cb0628a..bf9ce8d5f 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/react +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] + - @trigger.dev/core@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/react/package.json b/packages/react/package.json index 7a90957de..0e1abea13 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/react", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev React SDK", "license": "MIT", "types": "dist/index.d.ts", @@ -27,7 +27,7 @@ }, "dependencies": { "@tanstack/react-query": "5.0.0-beta.2", - "@trigger.dev/core": "workspace:^2.1.8", + "@trigger.dev/core": "workspace:^2.1.9", "debug": "^4.3.4", "zod": "3.22.3" }, diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index f0408bdb2..92ad60550 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/remix +## 2.1.9 + +### Patch Changes + +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/remix/package.json b/packages/remix/package.json index 580e4ad34..451a6ddc6 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/remix", - "version": "2.1.8", + "version": "2.1.9", "description": "Trigger.dev Remix integration", "license": "MIT", "main": "./dist/index.js", @@ -34,7 +34,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.8", + "@trigger.dev/sdk": "workspace:^2.1.9", "@remix-run/server-runtime": ">1.19.0" }, "dependencies": { diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index e51431fea..43035fcd5 100644 --- a/packages/testing/CHANGELOG.md +++ b/packages/testing/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/testing +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/core@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/testing/package.json b/packages/testing/package.json index 5265e3ae3..54497ba11 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,7 +1,7 @@ { "name": "@trigger.dev/testing", "description": "A collection of useful tools to write tests for Trigger.dev.", - "version": "2.1.8", + "version": "2.1.9", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-sdk/CHANGELOG.md b/packages/trigger-sdk/CHANGELOG.md index 70e4785a1..df1b8aa22 100644 --- a/packages/trigger-sdk/CHANGELOG.md +++ b/packages/trigger-sdk/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/sdk +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- 2e9452ab: allow cancelling jobs from trigger-client +- Updated dependencies [9a187f9e] + - @trigger.dev/core@2.1.9 + ## 2.1.8 ### Patch Changes diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index 5a0195d7e..a66fd17ba 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sdk", - "version": "2.1.8", + "version": "2.1.9", "description": "trigger.dev Node.JS SDK", "license": "MIT", "main": "./dist/index.js", @@ -25,7 +25,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/core": "workspace:^2.1.8", + "@trigger.dev/core": "workspace:^2.1.9", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", From 5910339bf4758e156a3cdb627336b4bd7c7bccf0 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 6 Oct 2023 12:56:56 +0100 Subject: [PATCH 19/79] Latest lockfile --- pnpm-lock.yaml | 58 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3b1079fa..f50990ab6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,8 +368,8 @@ importers: integrations/airtable: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x airtable: ^0.12.1 @@ -396,8 +396,8 @@ importers: '@octokit/types': ^9.2.3 '@octokit/webhooks': ^10.4.0 '@octokit/webhooks-types': ^6.10.0 - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' octokit: ^2.0.14 @@ -423,8 +423,8 @@ importers: integrations/linear: specifiers: '@linear/sdk': ^8.0.0 - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x rimraf: ^3.0.2 @@ -445,8 +445,8 @@ importers: integrations/openai: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' openai: ^4.2.0 @@ -465,8 +465,8 @@ importers: integrations/plain: specifiers: '@team-plain/typescript-sdk': ^2.7.0 - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' rimraf: ^3.0.2 @@ -483,8 +483,8 @@ importers: integrations/replicate: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x replicate: ^0.18.1 @@ -506,8 +506,8 @@ importers: integrations/resend: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' resend: ^1.0.0 @@ -526,8 +526,8 @@ importers: integrations/sendgrid: specifiers: '@sendgrid/mail': ^7.7.0 - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x rimraf: ^3.0.2 @@ -547,7 +547,7 @@ importers: integrations/slack: specifiers: '@slack/web-api': ^6.8.1 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' rimraf: ^3.0.2 @@ -565,8 +565,8 @@ importers: integrations/stripe: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@types/node': 16.x rimraf: ^3.0.2 stripe: ^12.14.0 @@ -589,8 +589,8 @@ importers: integrations/supabase: specifiers: '@supabase/supabase-js': ^2.26.0 - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/node': 18.x rimraf: ^3.0.2 @@ -613,8 +613,8 @@ importers: integrations/typeform: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.8 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/integration-kit': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.1.9 '@typeform/api-client': ^1.8.0 '@types/node': 16.x rimraf: ^3.0.2 @@ -841,7 +841,7 @@ importers: packages/express: specifiers: '@remix-run/web-fetch': ^4.3.5 - '@trigger.dev/sdk': workspace:^2.1.8 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/express': ^4.17.13 @@ -892,7 +892,7 @@ importers: specifiers: '@nestjs/common': ^10.2.4 '@remix-run/web-fetch': ^4.3.5 - '@trigger.dev/sdk': workspace:^2.1.0 + '@trigger.dev/sdk': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/express': ^4.17.13 @@ -941,7 +941,7 @@ importers: packages/react: specifiers: '@tanstack/react-query': 5.0.0-beta.2 - '@trigger.dev/core': workspace:^2.1.8 + '@trigger.dev/core': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/react': 18.2.17 @@ -1017,7 +1017,7 @@ importers: packages/trigger-sdk: specifiers: - '@trigger.dev/core': workspace:^2.1.8 + '@trigger.dev/core': workspace:^2.1.9 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/node': '18' @@ -18629,7 +18629,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 + '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 debug: 3.2.7 eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 @@ -18714,7 +18714,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 + '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 From a69a5019f102a30125acd94a7a41e9b20d0e2216 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 6 Oct 2023 13:16:40 +0100 Subject: [PATCH 20/79] Removed NestJS example project tests --- references/nestjs-example/package.json | 7 +----- .../nestjs-example/test/app.e2e-spec.ts | 24 ------------------- references/nestjs-example/test/jest-e2e.json | 9 ------- 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 references/nestjs-example/test/app.e2e-spec.ts delete mode 100644 references/nestjs-example/test/jest-e2e.json diff --git a/references/nestjs-example/package.json b/references/nestjs-example/package.json index 78769c86e..53be75e3d 100644 --- a/references/nestjs-example/package.json +++ b/references/nestjs-example/package.json @@ -12,12 +12,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" }, "dependencies": { "@nestjs/common": "^10.0.0", diff --git a/references/nestjs-example/test/app.e2e-spec.ts b/references/nestjs-example/test/app.e2e-spec.ts deleted file mode 100644 index 50cda6233..000000000 --- a/references/nestjs-example/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/references/nestjs-example/test/jest-e2e.json b/references/nestjs-example/test/jest-e2e.json deleted file mode 100644 index e9d912f3e..000000000 --- a/references/nestjs-example/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} From 1f3b423c22ddbcbea12bb0f282cac9735c0f32a6 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 6 Oct 2023 13:18:47 +0100 Subject: [PATCH 21/79] Removed testing dependencies from nestjs example --- pnpm-lock.yaml | 177 +------------------------ references/nestjs-example/package.json | 20 --- 2 files changed, 2 insertions(+), 195 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f50990ab6..af3b34683 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1203,7 +1203,6 @@ importers: '@trigger.dev/nestjs': workspace:* '@trigger.dev/sdk': workspace:* '@types/express': ^4.17.17 - '@types/jest': ^29.5.2 '@types/node': ^20.3.1 '@types/supertest': ^2.0.12 '@typescript-eslint/eslint-plugin': ^6.0.0 @@ -1211,13 +1210,11 @@ importers: eslint: ^8.42.0 eslint-config-prettier: ^9.0.0 eslint-plugin-prettier: ^5.0.0 - jest: ^29.5.0 prettier: ^3.0.0 reflect-metadata: ^0.1.13 rxjs: ^7.8.1 source-map-support: ^0.5.21 supertest: ^6.3.3 - ts-jest: ^29.1.0 ts-loader: ^9.4.3 ts-node: ^10.9.1 tsconfig-paths: ^4.2.0 @@ -1236,7 +1233,6 @@ importers: '@nestjs/schematics': 10.0.2_typescript@5.2.2 '@nestjs/testing': 10.2.7_6ce6yeqrytunl27g3gdf6ye2ri '@types/express': 4.17.18 - '@types/jest': 29.5.3 '@types/node': 20.6.0 '@types/supertest': 2.0.14 '@typescript-eslint/eslint-plugin': 6.7.4_ygtxu7ao4w7xzfo6eep522bcem @@ -1244,11 +1240,9 @@ importers: eslint: 8.45.0 eslint-config-prettier: 9.0.0_eslint@8.45.0 eslint-plugin-prettier: 5.0.0_jybzfv6jdssomlxkhhfntuvyli - jest: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole prettier: 3.0.0 source-map-support: 0.5.21 supertest: 6.3.3 - ts-jest: 29.1.1_uvjn2wawyirmlmi655a42v3ske ts-loader: 9.4.4_typescript@5.2.2 ts-node: 10.9.1_kpuv3buz4xyqturyqxj2gejvma tsconfig-paths: 4.2.0 @@ -7300,49 +7294,6 @@ packages: - ts-node dev: true - /@jest/core/29.6.2_ts-node@10.9.1: - resolution: {integrity: sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/console': 29.6.2 - '@jest/reporters': 29.6.2 - '@jest/test-result': 29.6.2 - '@jest/transform': 29.6.2 - '@jest/types': 29.6.1 - '@types/node': 20.6.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.8.0 - exit: 0.1.2 - graceful-fs: 4.2.10 - jest-changed-files: 29.5.0 - jest-config: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole - jest-haste-map: 29.6.2 - jest-message-util: 29.6.2 - jest-regex-util: 29.4.3 - jest-resolve: 29.6.2 - jest-resolve-dependencies: 29.6.2 - jest-runner: 29.6.2 - jest-runtime: 29.6.2 - jest-snapshot: 29.6.2 - jest-util: 29.6.2 - jest-validate: 29.6.2 - jest-watcher: 29.6.2 - micromatch: 4.0.5 - pretty-format: 29.6.2 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - dev: true - /@jest/environment/29.6.2: resolution: {integrity: sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -18629,7 +18580,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 + '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 debug: 3.2.7 eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 @@ -18714,7 +18665,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 + '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -22210,35 +22161,6 @@ packages: - ts-node dev: true - /jest-cli/29.6.2_rqg5pcrfq4i6lanvxwhlyynole: - resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.6.2_ts-node@10.9.1 - '@jest/test-result': 29.6.2 - '@jest/types': 29.6.1 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole - jest-util: 29.6.2 - jest-validate: 29.6.2 - prompts: 2.4.2 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - dev: true - /jest-config/29.6.2_@types+node@16.18.11: resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -22319,47 +22241,6 @@ packages: - supports-color dev: true - /jest-config/29.6.2_rqg5pcrfq4i6lanvxwhlyynole: - resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.22.17 - '@jest/test-sequencer': 29.6.2 - '@jest/types': 29.6.1 - '@types/node': 20.6.0 - babel-jest: 29.6.2_@babel+core@7.22.17 - chalk: 4.1.2 - ci-info: 3.8.0 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.6.2 - jest-environment-node: 29.6.2 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.6.2 - jest-runner: 29.6.2 - jest-util: 29.6.2 - jest-validate: 29.6.2 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.6.2 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1_kpuv3buz4xyqturyqxj2gejvma - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - dev: true - /jest-diff/29.6.2: resolution: {integrity: sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -22700,27 +22581,6 @@ packages: - ts-node dev: true - /jest/29.6.2_rqg5pcrfq4i6lanvxwhlyynole: - resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.6.2_ts-node@10.9.1 - '@jest/types': 29.6.1 - import-local: 3.1.0 - jest-cli: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - dev: true - /jiti/1.18.2: resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} hasBin: true @@ -29731,39 +29591,6 @@ packages: /ts-interface-checker/0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - /ts-jest/29.1.1_uvjn2wawyirmlmi655a42v3ske: - resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.6.2_rqg5pcrfq4i6lanvxwhlyynole - jest-util: 29.6.2 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.5.4 - typescript: 5.2.2 - yargs-parser: 21.1.1 - dev: true - /ts-jest/29.1.1_xlkreayjyan5lfqjb5gzdqf3my: resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} diff --git a/references/nestjs-example/package.json b/references/nestjs-example/package.json index 53be75e3d..78868e4b0 100644 --- a/references/nestjs-example/package.json +++ b/references/nestjs-example/package.json @@ -29,7 +29,6 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -37,31 +36,12 @@ "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.5.0", "prettier": "^3.0.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", - "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" } } From 63a65dabe4258ca6e32247b3712e97a4350d2a22 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:26:28 +0530 Subject: [PATCH 22/79] chore: add example for eventTrigger() doc (#541) * chore: add example for eventTrigger() doc * add documentation to the options parameter --- docs/sdk/eventtrigger.mdx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/sdk/eventtrigger.mdx b/docs/sdk/eventtrigger.mdx index fe94f8c96..5c7b30c95 100644 --- a/docs/sdk/eventtrigger.mdx +++ b/docs/sdk/eventtrigger.mdx @@ -45,6 +45,26 @@ You can have multiple Jobs that subscribe to the same event, they will all trigg ``` + + Used to provide example payloads that are accepted by the job. + + This will be available in the dashboard and can be used to trigger test runs. + + + + The example's ID. + + + The name that's displayed in the dashboard. + + + The payload that's accepted by the job. + + + The icon to use for this example in the dashboard. + + + @@ -70,6 +90,19 @@ client.defineJob({ filter: { tier: ["pro"], }, + //(optional) example event object + examples: [ + { + id: "issue.opened", + name: "Issue opened", + payload: { + userId: "1234", + tier: "free", + }, + //optional + icon: "github", + }, + ], }), run: async (payload, io, ctx) => { await io.logger.log("New pro user created", { userId: payload.userId }); From be9b51131db181e871354dcbc28b3928bb4d7b35 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:48:58 +0530 Subject: [PATCH 23/79] feat: allow CLI dev to use injected environment variables (#532) * feat: allow CLI dev to use injected environment variables * Remove console.log --------- Co-authored-by: Eric Allam --- .changeset/tidy-toys-turn.md | 5 + packages/cli/src/commands/dev.ts | 6 +- .../cli/src/utils/getTriggerApiDetails.ts | 68 +++--------- packages/cli/src/utils/readEnvVariables.ts | 102 ++++++++++++++++++ 4 files changed, 125 insertions(+), 56 deletions(-) create mode 100644 .changeset/tidy-toys-turn.md create mode 100644 packages/cli/src/utils/readEnvVariables.ts diff --git a/.changeset/tidy-toys-turn.md b/.changeset/tidy-toys-turn.md new file mode 100644 index 000000000..2f9a60e27 --- /dev/null +++ b/.changeset/tidy-toys-turn.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +allow CLI dev to use injected environment variable diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 517ea9908..c6b1a20ad 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -79,8 +79,8 @@ export async function devCommand(path: string, anyOptions: any) { telemetryClient.dev.failed("missing_api_key", resolvedOptions); return; } - const { apiUrl, envFile, apiKey } = apiDetails; - logger.success(`✔️ [trigger.dev] Found API Key in ${envFile} file`); + const { apiUrl, apiKey, apiKeySource } = apiDetails; + logger.success(`✔️ [trigger.dev] Found API Key in ${apiKeySource}`); //verify that the endpoint can be reached const verifiedEndpoint = await verifyEndpoint(resolvedOptions, endpointId, apiKey, framework); @@ -118,7 +118,7 @@ export async function devCommand(path: string, anyOptions: any) { const refreshedEndpointId = await getEndpointIdFromPackageJson(resolvedPath, resolvedOptions); // Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL - const apiDetails = await getTriggerApiDetails(resolvedPath, envFile); + const apiDetails = await getTriggerApiDetails(resolvedPath, resolvedOptions.envFile); if (!apiDetails) { connectingSpinner.fail(`[trigger.dev] Failed to connect: Missing API Key`); diff --git a/packages/cli/src/utils/getTriggerApiDetails.ts b/packages/cli/src/utils/getTriggerApiDetails.ts index 7ce20105c..274a9072b 100644 --- a/packages/cli/src/utils/getTriggerApiDetails.ts +++ b/packages/cli/src/utils/getTriggerApiDetails.ts @@ -1,66 +1,23 @@ -import pathModule from "path"; -import { pathExists, readFile } from "./fileSystem"; import { logger } from "./logger"; -import dotenv from "dotenv"; import { CLOUD_API_URL } from "../consts"; import { checkApiKeyIsDevServer } from "./getApiKeyType"; - -export async function readEnvFilesWithBackups( - path: string, - envFile: string, - backups: string[] -): Promise<{ content: string; fileName: string } | undefined> { - const envFilePath = pathModule.join(path, envFile); - const envFileExists = await pathExists(envFilePath); - - if (envFileExists) { - const content = await readFile(envFilePath); - - return { content, fileName: envFile }; - } - - for (const backup of backups) { - const backupPath = pathModule.join(path, backup); - const backupExists = await pathExists(backupPath); - - if (backupExists) { - const content = await readFile(backupPath); - - return { content, fileName: backup }; - } - } - - return; -} +import { readEnvVariables } from "./readEnvVariables"; export async function getTriggerApiDetails(path: string, envFile: string) { - const resolvedEnvFile = await readEnvFilesWithBackups(path, envFile, [ - ".env", - ".env.local", - ".env.development.local", - ]); + const envVarsToRead = ["TRIGGER_API_KEY", "TRIGGER_API_URL"]; + const resolvedEnvVars = await readEnvVariables(path, envFile, envVarsToRead); - if (!resolvedEnvFile) { - logger.error(`You must add TRIGGER_API_KEY to your ${envFile} file.`); - return; - } - - const parsedEnvFile = dotenv.parse(resolvedEnvFile.content); - - if (!parsedEnvFile) { - logger.error(`You must add TRIGGER_API_KEY to your ${envFile} file.`); - return; - } - - const apiKey = parsedEnvFile.TRIGGER_API_KEY; - const apiUrl = parsedEnvFile.TRIGGER_API_URL; + const apiKey = resolvedEnvVars.TRIGGER_API_KEY; + const apiUrl = resolvedEnvVars.TRIGGER_API_URL; if (!apiKey) { - logger.error(`You must add TRIGGER_API_KEY to your ${envFile} file.`); + logger.error( + `You must add TRIGGER_API_KEY to your ${envFile} file or set as runtime environment variable.` + ); return; } - const result = checkApiKeyIsDevServer(apiKey); + const result = checkApiKeyIsDevServer(apiKey.value); if (!result.success) { if (result.type) { @@ -75,5 +32,10 @@ export async function getTriggerApiDetails(path: string, envFile: string) { return; } - return { apiKey, apiUrl: apiUrl ?? CLOUD_API_URL, envFile: resolvedEnvFile.fileName }; + return { + apiKey: apiKey.value, + apiUrl: apiUrl?.value ?? CLOUD_API_URL, + apiKeySource: + apiKey.source.type === "runtime" ? "process runtime" : `${apiKey.source.name} file`, + }; } diff --git a/packages/cli/src/utils/readEnvVariables.ts b/packages/cli/src/utils/readEnvVariables.ts new file mode 100644 index 000000000..5ef8aa9b5 --- /dev/null +++ b/packages/cli/src/utils/readEnvVariables.ts @@ -0,0 +1,102 @@ +import pathModule from "path"; +import { pathExists, readFile } from "./fileSystem"; +import dotenv from "dotenv"; + +const ENV_FILES_FALLBACK = [".env", ".env.local", ".env.development.local"]; + +export type EnvVarSourceRuntime = { + type: "runtime"; +}; + +export type EnvVarSourceFile = { + type: "file"; + name: string; +}; + +export type EnvVarSource = EnvVarSourceRuntime | EnvVarSourceFile; + +export type EnvironmentVariable = { + value: string; + source: EnvVarSource; +}; + +export type EnvironmentVariables = { + [name: string]: EnvironmentVariable | undefined; +}; + +// Reads `varsToRead` from `process.env` and `envFile` (with fallbacks). +// `process.env` takes precedence over the `envFile`. +export async function readEnvVariables( + path: string, + envFile: string, + varsToRead: string[] +): Promise { + const resolvedEnvFile = await readEnvFilesWithBackups(path, envFile); + const parsedEnvFile = resolvedEnvFile + ? { output: dotenv.parse(resolvedEnvFile.content), filename: resolvedEnvFile.fileName } + : {}; + + return Object.fromEntries( + varsToRead.map((envVar) => [ + envVar, + readFromRuntime(envVar) ?? readFromFile(envVar, parsedEnvFile), + ]) + ); +} + +async function readEnvFilesWithBackups( + path: string, + envFile: string +): Promise<{ content: string; fileName: string } | undefined> { + const envFilePath = pathModule.join(path, envFile); + const envFileExists = await pathExists(envFilePath); + + if (envFileExists) { + const content = await readFile(envFilePath); + + return { content, fileName: envFile }; + } + + for (const fallBack of ENV_FILES_FALLBACK) { + const fallbackPath = pathModule.join(path, fallBack); + const fallbackExists = await pathExists(fallbackPath); + + if (fallbackExists) { + const content = await readFile(fallbackPath); + + return { content, fileName: fallBack }; + } + } + + return; +} + +function readFromRuntime(envVar: string): EnvironmentVariable | undefined { + const val = process.env[envVar]; + if (!val) { + return; + } + return { + value: val, + source: { + type: "runtime", + } as EnvVarSourceRuntime, + }; +} + +function readFromFile( + envVar: string, + parsedEnvFile: { output?: dotenv.DotenvParseOutput; filename?: string } +): EnvironmentVariable | undefined { + const val = parsedEnvFile.output ? parsedEnvFile.output[envVar] : undefined; + if (!val) { + return; + } + return { + value: val, + source: { + type: "file", + name: parsedEnvFile.filename, + } as EnvVarSourceFile, + }; +} From 24d892e322ddb152c66b81ed8e16c4c9d62e59f5 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Oct 2023 15:10:43 +0100 Subject: [PATCH 24/79] Merges 573 with fix for Dockerfile (#578) * update prisma to 5.4.1 (#573) * Get prisma 5.4.1 to work in the Dockerfile --------- Co-authored-by: Karthik --- .../app/services/jobs/registerJob.server.ts | 9 +---- docker/Dockerfile | 3 +- docker/dev-compose.yml | 1 + packages/database/package.json | 4 +-- pnpm-lock.yaml | 34 +++++++++---------- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/apps/webapp/app/services/jobs/registerJob.server.ts b/apps/webapp/app/services/jobs/registerJob.server.ts index 401c69c8a..389808359 100644 --- a/apps/webapp/app/services/jobs/registerJob.server.ts +++ b/apps/webapp/app/services/jobs/registerJob.server.ts @@ -4,14 +4,7 @@ import { SCHEDULED_EVENT, TriggerMetadata, } from "@trigger.dev/core"; -import type { - Endpoint, - Integration, - Job, - JobIntegration, - JobIntegrationPayload, - JobVersion, -} from "@trigger.dev/database"; +import type { Endpoint, Integration, Job, JobIntegration, JobVersion } from "@trigger.dev/database"; import { DEFAULT_MAX_CONCURRENT_RUNS } from "~/consts"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; diff --git a/docker/Dockerfile b/docker/Dockerfile index 2c7d3fc30..cf12ecd6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,7 +32,8 @@ ENV NODE_ENV production RUN pnpm install --prod --no-frozen-lockfile COPY --from=pruner --chown=node:node /triggerdotdev/packages/database/prisma/schema.prisma /triggerdotdev/packages/database/prisma/schema.prisma # RUN pnpm add @prisma/client@5.1.1 -w -RUN pnpx prisma@4.16.0 generate --schema /triggerdotdev/packages/database/prisma/schema.prisma +ENV NPM_CONFIG_IGNORE_WORKSPACE_ROOT_CHECK true +RUN pnpx prisma@5.4.1 generate --schema /triggerdotdev/packages/database/prisma/schema.prisma ## Builder (builds the webapp) FROM base AS builder diff --git a/docker/dev-compose.yml b/docker/dev-compose.yml index b5db14f4b..3f0003a57 100644 --- a/docker/dev-compose.yml +++ b/docker/dev-compose.yml @@ -35,6 +35,7 @@ services: DIRECT_URL: postgres://postgres:postgres@db:5432/postgres?schema=public SESSION_SECRET: secret123 MAGIC_LINK_SECRET: secret123 + ENCRYPTION_KEY: secret123 REMIX_APP_PORT: 3030 PORT: 3030 networks: diff --git a/packages/database/package.json b/packages/database/package.json index dda8fdef4..3fcb97517 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -5,11 +5,11 @@ "main": "./src/index.ts", "types": "./src/index.ts", "dependencies": { - "@prisma/client": "4.16.0", + "@prisma/client": "5.4.1", "typescript": "^4.8.4" }, "devDependencies": { - "prisma": "4.16.0" + "prisma": "5.4.1" }, "scripts": { "generate": "prisma generate", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af3b34683..8f347a871 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -763,14 +763,14 @@ importers: packages/database: specifiers: - '@prisma/client': 4.16.0 - prisma: 4.16.0 + '@prisma/client': 5.4.1 + prisma: 5.4.1 typescript: ^4.8.4 dependencies: - '@prisma/client': 4.16.0_prisma@4.16.0 + '@prisma/client': 5.4.1_prisma@5.4.1 typescript: 4.9.5 devDependencies: - prisma: 4.16.0 + prisma: 5.4.1 packages/emails: specifiers: @@ -9494,9 +9494,9 @@ packages: config-chain: 1.1.13 dev: false - /@prisma/client/4.16.0_prisma@4.16.0: - resolution: {integrity: sha512-CBD+5IdZPiavhLkQokvsz1uz4r9ppixaqY/ajybWs4WXNnsDVMBKEqN3BiPzpSo79jiy22VKj/67pqt4VwIg9w==} - engines: {node: '>=14.17'} + /@prisma/client/5.4.1_prisma@5.4.1: + resolution: {integrity: sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ==} + engines: {node: '>=16.13'} requiresBuild: true peerDependencies: prisma: '*' @@ -9504,16 +9504,16 @@ packages: prisma: optional: true dependencies: - '@prisma/engines-version': 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c - prisma: 4.16.0 + '@prisma/engines-version': 5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f + prisma: 5.4.1 dev: false - /@prisma/engines-version/4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c: - resolution: {integrity: sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==} + /@prisma/engines-version/5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f: + resolution: {integrity: sha512-+nUQM/y8C+1GG5Ioeqcu6itFslCfxvQSAUVSMC9XM2G2Fcq0F4Afnp6m0pXF6X6iUBWen7jZBPmM9Qlq4Nr3/A==} dev: false - /@prisma/engines/4.16.0: - resolution: {integrity: sha512-M6XoMRXnqL0rqZGQS8ZpNiHYG4G1fKBdoqW/oBtHnr1in5UYgerZqal3CXchmd6OBD/770PE9dtjQuqcilZJUA==} + /@prisma/engines/5.4.1: + resolution: {integrity: sha512-vJTdY4la/5V3N7SFvWRmSMUh4mIQnyb/MNoDjzVbh9iLmEC+uEykj/1GPviVsorvfz7DbYSQC4RiwmlEpTEvGA==} requiresBuild: true /@protobufjs/aspromise/1.1.2: @@ -26455,13 +26455,13 @@ packages: react: 18.2.0 dev: false - /prisma/4.16.0: - resolution: {integrity: sha512-kSCwbTm3LCephyGfZMJYqBXpPJXdJStg5xwfzeFmR5C05zfkOURK9pQpJF6uUQvFWm3lI9ZMSNkObmFkAPnB+g==} - engines: {node: '>=14.17'} + /prisma/5.4.1: + resolution: {integrity: sha512-op9PmU8Bcw5dNAas82wBYTG0yHnpq9/O3bhxbDBrNzwZTwBqsVCxxYRLf6wHNh9HVaDGhgjjHlu1+BcW8qdnBg==} + engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 4.16.0 + '@prisma/engines': 5.4.1 /prismjs/1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} From 65ad98c20d75f9420f3d55d2b89c1085bce39247 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Oct 2023 15:13:45 +0100 Subject: [PATCH 25/79] Add docs for yield and brb --- docs/sdk/io/overview.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sdk/io/overview.mdx b/docs/sdk/io/overview.mdx index 3c0eb15c3..c44a34fc6 100644 --- a/docs/sdk/io/overview.mdx +++ b/docs/sdk/io/overview.mdx @@ -64,5 +64,13 @@ If you want to send an event from outside a run (e.g. just from your backend) yo `io.registerTrigger()` allows you to register a [DynamicTrigger](/sdk/dynamictrigger) with the specified trigger data. +### yield() + +`io.yield()` allows you to yield the current run and resume it immediately in a different function execution context. Requires a single argument that defines the yield key which works similar to task keys. + +### brb() + +`io.brb()` is is alias for `io.yield()`. + {/* ### [unregisterTrigger()](/sdk/io/unregistertrigger) */} {/* `io.unregisterTrigger()` allows you to unregister a [DynamicTrigger](/sdk/dynamictrigger) that was previously registered with `io.registerTrigger()`. */} From 1c524c5acc628a8d7b8df17acedad68fe27907cf Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 6 Oct 2023 20:16:09 +0530 Subject: [PATCH 26/79] feat: add internal flag to job-run table (#563) * feat: add internal flag to job-run table * add backfill SQL * Update internal backfill to only update internal jobs --------- Co-authored-by: Eric Allam --- apps/webapp/app/services/runs/createRun.server.ts | 1 + .../20231005064823_add_job_run_internal/migration.sql | 10 ++++++++++ packages/database/prisma/schema.prisma | 1 + 3 files changed, 12 insertions(+) create mode 100644 packages/database/prisma/migrations/20231005064823_add_job_run_internal/migration.sql diff --git a/apps/webapp/app/services/runs/createRun.server.ts b/apps/webapp/app/services/runs/createRun.server.ts index a3c789193..7c1089772 100644 --- a/apps/webapp/app/services/runs/createRun.server.ts +++ b/apps/webapp/app/services/runs/createRun.server.ts @@ -70,6 +70,7 @@ export class CreateRunService { ? eventRecord.externalAccountId : undefined, isTest: eventRecord.isTest, + internal: job.internal, }, }); diff --git a/packages/database/prisma/migrations/20231005064823_add_job_run_internal/migration.sql b/packages/database/prisma/migrations/20231005064823_add_job_run_internal/migration.sql new file mode 100644 index 000000000..f226330db --- /dev/null +++ b/packages/database/prisma/migrations/20231005064823_add_job_run_internal/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable +ALTER TABLE "JobRun" ADD COLUMN "internal" BOOLEAN NOT NULL DEFAULT false; + +/* + Backfill JobRun internal flag +*/ +UPDATE "JobRun" +SET "internal" = "Job"."internal" +FROM "Job" +WHERE "JobRun"."jobId" = "Job"."id" AND "JobRun"."internal" = TRUE; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 0bcdc1547..a1f96966e 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -666,6 +666,7 @@ model EventRecord { model JobRun { id String @id @default(cuid()) number Int + internal Boolean @default(false) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade) jobId String From a7bc5df75490813f72cf9ac77a9e83503e3fdea3 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 6 Oct 2023 16:15:13 +0100 Subject: [PATCH 27/79] use the new JobRun.internal flag in usage stats --- .../presenters/OrgUsagePresenter.server.ts | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/presenters/OrgUsagePresenter.server.ts b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts index 7c08a2468..c43b7fcf8 100644 --- a/apps/webapp/app/presenters/OrgUsagePresenter.server.ts +++ b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts @@ -1,4 +1,5 @@ import { PrismaClient, prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; export class OrgUsagePresenter { #prismaClient: PrismaClient; @@ -33,6 +34,7 @@ export class OrgUsagePresenter { createdAt: { gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1), }, + internal: false, }, }); @@ -44,6 +46,7 @@ export class OrgUsagePresenter { gte: startOfLastMonth, lt: startOfMonth, }, + internal: false, }, }); @@ -63,7 +66,7 @@ export class OrgUsagePresenter { month: string; count: number; }[] - >`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`; + >`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`; const chartData = chartDataRaw.map((obj) => ({ name: obj.month, @@ -139,11 +142,13 @@ export class OrgUsagePresenter { }, }); + const chartDataDisplay = fillInMissingMonthlyData(chartData, 6); + return { id: organization.id, runsCount, runsCountLastMonth, - chartData: fillInMissingMonthlyData(chartData, 6), + chartData: chartDataDisplay, totalJobs, totalJobsLastMonth, totalIntegrations, @@ -166,7 +171,7 @@ function fillInMissingMonthlyData( const startMonth = new Date( new Date(currentMonth).getFullYear(), - new Date(currentMonth).getMonth() - totalNumberOfMonths, + new Date(currentMonth).getMonth() - (totalNumberOfMonths - 2), 1 ) .toISOString() @@ -182,17 +187,36 @@ function fillInMissingMonthlyData( return completeData; } +// Start month will be like 2023-03 and endMonth will be like 2023-10 +// The result should be an array of months between these two months, including the start and end month +// So for example, if startMonth is 2023-03 and endMonth is 2023-10, the result should be: +// ["2023-03", "2023-04", "2023-05", "2023-06", "2023-07", "2023-08", "2023-09", "2023-10"] function getMonthsBetween(startMonth: string, endMonth: string): string[] { - const startDate = new Date(startMonth); - const endDate = new Date(endMonth); + // Initialize result array + const result: string[] = []; - const months = []; - let currentDate = startDate; + // Parse the year and month from startMonth and endMonth + let [startYear, startMonthNum] = startMonth.split("-").map(Number); + let [endYear, endMonthNum] = endMonth.split("-").map(Number); - while (currentDate <= endDate) { - months.push(currentDate.toISOString().slice(0, 7)); - currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1)); + // Loop through each month between startMonth and endMonth + for (let year = startYear; year <= endYear; year++) { + let monthStart = year === startYear ? startMonthNum : 1; + let monthEnd = year === endYear ? endMonthNum : 12; + + for (let month = monthStart; month <= monthEnd; month++) { + // Format the month into a string and add it to the result array + result.push(`${year}-${String(month).padStart(2, "0")}`); + } } - return months; + return result; +} + +function getLastSecondOfMonth(endMonth: string) { + const [year, month] = endMonth.split("-").map(Number); + const nextMonthFirstDay = new Date(year, month, 1); + nextMonthFirstDay.setDate(0); + nextMonthFirstDay.setHours(23, 59, 59); + return nextMonthFirstDay; } From b12bc6042fd6b36bf89d02cdf3ae87658de662d4 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:25:26 +0530 Subject: [PATCH 28/79] (regression) fix: remove invalid envFile response field in sendEvent cmd (#577) * fix: remove invalid envFile response field in sendEvent cmd * add changeset --- .changeset/tiny-candles-type.md | 5 +++++ packages/cli/src/commands/sendEvent.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/tiny-candles-type.md diff --git a/.changeset/tiny-candles-type.md b/.changeset/tiny-candles-type.md new file mode 100644 index 000000000..8afa8b23d --- /dev/null +++ b/.changeset/tiny-candles-type.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +remove unused envFile param in sendEvent.ts cmd diff --git a/packages/cli/src/commands/sendEvent.ts b/packages/cli/src/commands/sendEvent.ts index 843fc71e6..b18b2fcdd 100644 --- a/packages/cli/src/commands/sendEvent.ts +++ b/packages/cli/src/commands/sendEvent.ts @@ -36,7 +36,7 @@ export async function sendEventCommand(path: string, anyOptions: any) { return; } - const { apiUrl, envFile, apiKey } = apiDetails; + const { apiUrl, apiKey } = apiDetails; const parsedPayload = safeJSONParse(options.payload); From 0558b2c595505a201f22210fef100d2b6bedd5a3 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 6 Oct 2023 17:12:52 +0100 Subject: [PATCH 29/79] feat: sveltekit adaptor (#575) * Feat/svelte kit support (#467) * feat: implemented svelte-kit adapter * feat: implemented the @trigger.dev/svelte package * scaffolded a sveltekit example project with trigger.dev * added the convertToStandardRequest function in the sveltekit package * example sveltekit project with the @trigger.dev/svelte package * doc: added the manual setup guide for sveltekit * updated the web app onboard for sveltekit * formatted the manual setup guide for sveltekit * added a link to webapp sveltekit onboarding * added a wait function to the sveltekit example app job * typo fix * removed comments from the @trigger.dev/svelte - commented out the useEventRunDetails * updated package.json to use internal packages * added a .env.example file * updated the example-svelte-app * updated the svelte-example * updated the onboarding page to reflect requested changes * made the manual setup guide more specific * created a .env.example file for svelte-example * added import aliases * updated the tsconfig and svelte.config file * updated to use uint8Array * added missing paths in the tsconfig --------- Co-authored-by: Matt Aitken * Moved examples to references * Tweaked the SveleteKit manual setup guide * Updated zod, and set the svelte/sveltekit package versions to match other packages * Added the headers to the response from the SvelteKit api/trigger route * Changeset for Svelte and SvelteKit packages * Revert change to rawBody * Remove core from changeset packages * Delete svelte .npmrc file * Build svelte package to build, not build/lib * Made the snapshot instructions easier to copy * Delete the svelte package * Latest lockfile * Updated the SvelteKit docs * Attempt to get Svelte to import the package * Updates for port and host options * Updated lockfile from main * Update neat-feet-wait.md --------- Co-authored-by: Chigala <92148630+Chigala@users.noreply.github.com> --- .changeset/neat-feet-wait.md | 5 + CHANGESETS.md | 27 +- apps/webapp/app/components/SetupCommands.tsx | 20 +- .../frameworks/FrameworkSelector.tsx | 2 +- .../route.tsx | 114 ++- docs/_snippets/manual-setup-sveltekit.mdx | 218 ++++- .../concepts/client-adaptors.mdx | 17 +- packages/sveltekit/LICENSE | 19 + packages/sveltekit/README.md | 9 + packages/sveltekit/package.json | 44 + packages/sveltekit/src/index.ts | 42 + packages/sveltekit/tsconfig.json | 18 + packages/sveltekit/tsup.config.ts | 19 + pnpm-lock.yaml | 769 ++++++++++++++++-- references/svelte-example/.env.example | 3 + references/svelte-example/.eslintignore | 13 + references/svelte-example/.eslintrc.cjs | 30 + references/svelte-example/.gitignore | 10 + references/svelte-example/.npmrc | 2 + references/svelte-example/.prettierignore | 13 + references/svelte-example/.prettierrc | 9 + references/svelte-example/README.md | 38 + references/svelte-example/package.json | 38 + references/svelte-example/src/app.d.ts | 12 + references/svelte-example/src/app.html | 12 + references/svelte-example/src/jobs/example.ts | 20 + references/svelte-example/src/lib/index.ts | 1 + .../svelte-example/src/routes/+page.svelte | 2 + .../src/routes/api/trigger/+server.ts | 12 + references/svelte-example/src/trigger.ts | 7 + references/svelte-example/static/favicon.png | Bin 0 -> 1571 bytes references/svelte-example/svelte.config.js | 28 + references/svelte-example/tsconfig.json | 25 + references/svelte-example/vite.config.ts | 6 + 34 files changed, 1482 insertions(+), 122 deletions(-) create mode 100644 .changeset/neat-feet-wait.md create mode 100644 packages/sveltekit/LICENSE create mode 100644 packages/sveltekit/README.md create mode 100644 packages/sveltekit/package.json create mode 100644 packages/sveltekit/src/index.ts create mode 100644 packages/sveltekit/tsconfig.json create mode 100644 packages/sveltekit/tsup.config.ts create mode 100644 references/svelte-example/.env.example create mode 100644 references/svelte-example/.eslintignore create mode 100644 references/svelte-example/.eslintrc.cjs create mode 100644 references/svelte-example/.gitignore create mode 100644 references/svelte-example/.npmrc create mode 100644 references/svelte-example/.prettierignore create mode 100644 references/svelte-example/.prettierrc create mode 100644 references/svelte-example/README.md create mode 100644 references/svelte-example/package.json create mode 100644 references/svelte-example/src/app.d.ts create mode 100644 references/svelte-example/src/app.html create mode 100644 references/svelte-example/src/jobs/example.ts create mode 100644 references/svelte-example/src/lib/index.ts create mode 100644 references/svelte-example/src/routes/+page.svelte create mode 100644 references/svelte-example/src/routes/api/trigger/+server.ts create mode 100644 references/svelte-example/src/trigger.ts create mode 100644 references/svelte-example/static/favicon.png create mode 100644 references/svelte-example/svelte.config.js create mode 100644 references/svelte-example/tsconfig.json create mode 100644 references/svelte-example/vite.config.ts diff --git a/.changeset/neat-feet-wait.md b/.changeset/neat-feet-wait.md new file mode 100644 index 000000000..ab385fe37 --- /dev/null +++ b/.changeset/neat-feet-wait.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sveltekit": patch +--- + +SvelteKit adaptor package diff --git a/CHANGESETS.md b/CHANGESETS.md index 9565e908c..dab130bc8 100644 --- a/CHANGESETS.md +++ b/CHANGESETS.md @@ -32,7 +32,26 @@ Please follow the best-practice of adding changesets in the same commit as the c !MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW! -1. Add changesets as usual `pnpm run changeset:add` -2. Create a snapshot version (replace "dev" with your tag) `pnpm exec changeset version --snapshot dev` -3. Build the packages: `pnpm run build --filter "@trigger.dev/*"` -4. Publish the snapshot (replace "dev" with your tag) `pnpm exec changeset publish --no-git-tag --snapshot --tag dev` +1. Add changesets as usual + +```sh +pnpm run changeset:add +``` + +2. Create a snapshot version (replace "prerelease" with your tag) + +```sh +pnpm exec changeset version --snapshot prerelease +``` + +3. Build the packages: + +```sh +pnpm run build --filter "@trigger.dev/*" +``` + +4. Publish the snapshot (replace "dev" with your tag) + +```sh +pnpm exec changeset publish --no-git-tag --snapshot --tag prerelease +``` diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index 245eeff8b..f04083c7e 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -44,7 +44,7 @@ export function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey: ); } -export function RunDevCommand() { +export function RunDevCommand({ extra }: { extra?: string }) { return ( @@ -53,19 +53,19 @@ export function RunDevCommand() { yarn - + - + - + ); } -export function TriggerDevCommand() { +export function TriggerDevCommand({ extra }: { extra?: string }) { return ( @@ -77,34 +77,34 @@ export function TriggerDevCommand() { ); } -export function TriggerDevStep() { +export function TriggerDevStep({ extra }: { extra?: string }) { return ( <> In a separate terminal window or tab run: - + If you’re not running on the default you can specify the port by adding{" "} --port 3001 to the end. diff --git a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx index 8302e350d..3ce50b1e5 100644 --- a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx +++ b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx @@ -66,7 +66,7 @@ export function FrameworkSelector() { - + diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx index 8083db44b..eb47e5fdf 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx @@ -1,23 +1,113 @@ -import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo"; -import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; +import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; +import invariant from "tiny-invariant"; +import { Feedback } from "~/components/Feedback"; +import { PageGradient } from "~/components/PageGradient"; +import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; +import { StepContentContainer } from "~/components/StepContentContainer"; +import { InlineCode } from "~/components/code/InlineCode"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { Header1 } from "~/components/primitives/Headers"; +import { NamedIcon } from "~/components/primitives/NamedIcon"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { StepNumber } from "~/components/primitives/StepNumber"; +import { useAppOrigin } from "~/hooks/useAppOrigin"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; +import { useDevEnvironment } from "~/hooks/useEnvironments"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; import { Handle } from "~/utils/handle"; -import { trimTrailingSlash } from "~/utils/pathBuilder"; - +import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; +import { Callout } from "~/components/primitives/Callout"; +import { Badge } from "~/components/primitives/Badge"; export const handle: Handle = { breadcrumb: (match) => ( ), }; -export default function Page() { +export default function SetUpSveltekit() { + const organization = useOrganization(); + const project = useProject(); + useProjectSetupComplete(); + const devEnvironment = useDevEnvironment(); + invariant(devEnvironment, "Dev environment must be defined"); return ( - - - + +
+
+ + Get setup in 5 minutes + +
+ + Choose a different framework + + + I'm stuck! + + } + defaultValue="help" + /> +
+
+
+ + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + +
+ + + Copy your server API Key to your clipboard: +
+ Server} + /> +
+ Now follow this guide: + + Manual installation guide + +
+
+ + + + + + + + + + + This page will automatically refresh. + +
+
+
+
); } diff --git a/docs/_snippets/manual-setup-sveltekit.mdx b/docs/_snippets/manual-setup-sveltekit.mdx index 8e299631d..438005689 100644 --- a/docs/_snippets/manual-setup-sveltekit.mdx +++ b/docs/_snippets/manual-setup-sveltekit.mdx @@ -1 +1,217 @@ -We're in the process of building support for the SvelteKit framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues). +## Installing Required Packages + +To begin, install the necessary packages in your Sveltekit project directory. You can choose one of the following package managers: + + + +```bash npm +npm i @trigger.dev/sdk @trigger.dev/sveltekit +``` + +```bash pnpm +pnpm install @trigger.dev/sdk @trigger.dev/sveltekit +``` + +```bash yarn +yarn add @trigger.dev/sdk @trigger.dev/sveltekit +``` + + +
+ +Ensure that you execute this command within a SvelteKit project. +## Obtaining the Development API Key + +To locate your development API key, login to the [Trigger.dev +dashboard](https://cloud.trigger.dev) and select the Project you want to +connect to. Then click on the Environments & API Keys tab in the left menu. +You can copy your development API Key from the field at the top of this page. +(Your development key will start with `tr_dev_`). + +## Adding Environment Variables + +Create a `.env` file at the root of your project and include your Trigger API key and URL like this: + +```bash +TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE +TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting +``` + +Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step. + +## Syncing Environment Variable types (TypeScript) + +You will have type errors for your environment variables unless you run this command: + +```sh +npx svelte-kit sync +``` + +## Configuring the Trigger Client + +Create a file at `/src/trigger.ts` or `/trigger.ts` depending on whether you're using the `src` directory or not. `` represents the root directory of your project. + +Next, add the following code to the file which creates and exports a new `TriggerClient`: + +```typescript src/trigger.(ts/js) +// trigger.ts (for TypeScript) or trigger.js (for JavaScript) + +import { TriggerClient } from "@trigger.dev/sdk"; +import { TRIGGER_API_KEY, TRIGGER_API_URL } from "$env/static/private"; + +export const client = new TriggerClient({ + id: "my-app", + apiKey: TRIGGER_API_KEY, + apiUrl: TRIGGER_API_URL, +}); +``` + +Replace **"my-app"** with an appropriate identifier for your project. + +## Creating the API Route + +To establish an API route for interacting with Trigger.dev, follow these steps based on your project's file type and structure + +Create a new file named `+server.(ts/js)` within the `src/routes/api/trigger` directory, and add the following code: + +```typescript +import { createSvelteRoute } from "@trigger.dev/sveltekit"; +import { client } from "../../../trigger"; + +//import all jobs +import "../../../jobs"; + +// Create the Svelte route handler using the createSvelteRoute function +const svelteRoute = createSvelteRoute(client); + +// Define your API route handler +export const POST = svelteRoute.POST; +``` + +## Creating the Example Job + +1. Create a folder named `jobs` alongside your `src` directory +2. Inside the `jobs` folder, add two files named `example.(ts/js)` and `index.(ts/js)`. + + + +```typescript src/jobs/example.(ts/js) +import { eventTrigger } from "@trigger.dev/sdk"; +import { client } from "../trigger"; + +// your first job +client.defineJob({ + id: "example-job", + name: "Example Job", + version: "0.0.1", + trigger: eventTrigger({ + name: "example.event", + }), + run: async (payload, io, ctx) => { + await io.logger.info("Hello world!", { payload }); + + return { + message: "Hello world!", + }; + }, +}); +``` + +```typescript src/jobs/index.(ts/js) +// export all your job files here +export * from "./example"; +``` + + + +## Additonal Job Definitions + +You can define more job definitions by creating additional files in the `jobs` folder and exporting them in the `src/jobs/index` file. + +For example, in `index.(ts/js)`, you can export other job files like this: + +```typescript +// export all your job files here +export * from "./example"; +export * from "./other-job-file"; +``` + +## Adding Configuration to `package.json` + +Inside the `package.json` file, add the following configuration under the root object: + +```json +"trigger.dev": { + "endpointId": "my-app" +} +``` + +Your `package.json` file might look something like this: + +```json +{ + "name": "my-app", + "version": "1.0.0", + "dependencies": { + // ... other dependencies + }, + "trigger.dev": { + "endpointId": "my-app" + } +} +``` + +Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client. + +## Running + +### Run your Sveltekit app + +Run your Sveltekit app locally. You need to use the `--host` flag to allow the Trigger.dev CLI to connect to your app. + +For example: + + + +```bash npm +npm run dev -- --open --host +``` + +```bash pnpm +pnpm run dev -- --open --host +``` + +```bash yarn +yarn run dev -- --open --host +``` + + + +### Run the CLI 'dev' command + +In a **_separate terminal window or tab_** run: + + + +```bash npm +npx @trigger.dev/cli@latest dev --port 5173 +``` + +```bash pnpm +pnpm dlx @trigger.dev/cli@latest dev --port 5173 +``` + +```bash yarn +yarn dlx @trigger.dev/cli@latest dev --port 5173 +``` + + +
+ + You can optionally pass the port if you're not running on 3000 by adding + `--port 5173` to the end + + + You can optionally pass the hostname if you're not running on localhost by adding + `--hostname `. Example, in case your Sveltekit app is running on 0.0.0.0: `--hostname 0.0.0.0`. + diff --git a/docs/documentation/concepts/client-adaptors.mdx b/docs/documentation/concepts/client-adaptors.mdx index d724284ef..7cd7c3a90 100644 --- a/docs/documentation/concepts/client-adaptors.mdx +++ b/docs/documentation/concepts/client-adaptors.mdx @@ -24,11 +24,12 @@ Adaptors allows Clients to receive data from the Trigger API. They do this by cr Each platform has one or more adaptors, see the guides below: -| Platform | Adaptor | -| ------------------------------------------------- | -------------------- | -| [Next.js](/documentation/guides/platforms/nextjs) | `createPagesRoute()` | -| [Next.js](/documentation/guides/platforms/nextjs) | `createAppRoute()` | -| [NestJS](/documentation/guides/manual/nestjs) | `TriggerDevModule` | -| [Astro](/documentation/guides/platforms/astro) | `createAstroRoute()` | -| [Remix](/documentation/guides/platforms/remix) | `createRemixRoute()` | -| Express | Coming soon | +| Platform | Adaptor | +| ------------------------------------------------------ | --------------------- | +| [Next.js](/documentation/guides/platforms/nextjs) | `createPagesRoute()` | +| [Next.js](/documentation/guides/platforms/nextjs) | `createAppRoute()` | +| [NestJS](/documentation/guides/manual/nestjs) | `TriggerDevModule` | +| [Astro](/documentation/guides/platforms/astro) | `createAstroRoute()` | +| [Remix](/documentation/guides/platforms/remix) | `createRemixRoute()` | +| [Sveltekit](/documentation/guides/platforms/sveltekit) | `createSvelteRoute()` | +| Express | Coming soon | diff --git a/packages/sveltekit/LICENSE b/packages/sveltekit/LICENSE new file mode 100644 index 000000000..9cf106272 --- /dev/null +++ b/packages/sveltekit/LICENSE @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md new file mode 100644 index 000000000..a1c4c89c8 --- /dev/null +++ b/packages/sveltekit/README.md @@ -0,0 +1,9 @@ +# SvelteKit and Trigger.dev + +Trigger.dev provides full support for the SvelteKit framework. + +For information about using Trigger.dev in your SvelteKit app, check out these useful documentation links: + +- [`Quick start guide for setting up Trigger.dev in a SvelteKit project`](https://trigger.dev/docs/documentation/quickstarts/sveltekit) +- [`Manually set up SvelteKit with Trigger.dev`](https://trigger.dev/docs/documentation/guides/manual/sveltekit) +- [`Using Trigger.dev with SvelteKit, handling middleware, and more`](https://trigger.dev/docs/documentation/guides/platforms/sveltekit) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json new file mode 100644 index 000000000..ed6490b75 --- /dev/null +++ b/packages/sveltekit/package.json @@ -0,0 +1,44 @@ +{ + "name": "@trigger.dev/sveltekit", + "version": "2.1.9", + "description": "Trigger.dev svelteKit integration", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "commonjs", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "devDependencies": { + "@sveltejs/kit": "^1.20.4", + "@trigger.dev/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/ws": "^8.5.3", + "rimraf": "^3.0.2", + "tsup": "^6.5.0" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:^2.0.14" + }, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=16.8.0" + } +} diff --git a/packages/sveltekit/src/index.ts b/packages/sveltekit/src/index.ts new file mode 100644 index 000000000..f832d92f7 --- /dev/null +++ b/packages/sveltekit/src/index.ts @@ -0,0 +1,42 @@ +import type { TriggerClient } from "@trigger.dev/sdk"; + +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "@sveltejs/kit"; + +export function createSvelteRoute(client: TriggerClient) { + const POST: RequestHandler = async ({ request }) => { + const standardizedRequest = await convertToStandardRequest(request); + const response = await client.handleRequest(standardizedRequest); + + if (!response) { + return json({ error: "Resource not found" }, { status: 404 }); + } + + return json(response.body, { status: response.status, headers: response.headers }); + }; + return { POST }; +} + +async function convertToStandardRequest(req: Request): Promise { + // Prepare the request to be a fetch-compatible Request object: + const requestHeaders = req.headers; + const requestMethod = req.method; + const responseHeaders = Object.create(null); + + for (const [headerName, headerValue] of requestHeaders.entries()) { + responseHeaders[headerName] = headerValue; + } + + // Create a new Request object to be passed to the TriggerClient + // where we pass the clone the incoming request metadata such as + // headers, method, body. + const request = new Request("https://svelte/api/trigger", { + headers: responseHeaders, + method: requestMethod, + // @ts-ignore + body: req.body ? req.body : req, + duplex: "half", + }); + + return request; +} diff --git a/packages/sveltekit/tsconfig.json b/packages/sveltekit/tsconfig.json new file mode 100644 index 000000000..3421bbad1 --- /dev/null +++ b/packages/sveltekit/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@trigger.dev/tsconfig/node18.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "declarationMap": false, + "lib": ["DOM", "DOM.Iterable"], + "paths": { + "@trigger.dev/sdk": ["../trigger-sdk/src/index"], + "@trigger.dev/sdk/*": ["../trigger-sdk/src/*"], + "@trigger.dev/core": ["../core/src/index"], + "@trigger.dev/core/*": ["../core/src/*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/packages/sveltekit/tsup.config.ts b/packages/sveltekit/tsup.config.ts new file mode 100644 index 000000000..74c4f4dc1 --- /dev/null +++ b/packages/sveltekit/tsup.config.ts @@ -0,0 +1,19 @@ +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, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + }, +]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f347a871..4287788db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -994,6 +994,25 @@ importers: tsx: 3.12.2 typescript: 4.9.5 + packages/sveltekit: + specifiers: + '@sveltejs/kit': ^1.20.4 + '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 + '@types/ws': ^8.5.3 + debug: ^4.3.4 + rimraf: ^3.0.2 + tsup: ^6.5.0 + dependencies: + debug: 4.3.4 + devDependencies: + '@sveltejs/kit': 1.25.1 + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 + '@types/ws': 8.5.4 + rimraf: 3.0.2 + tsup: 6.6.3 + packages/testing: specifiers: '@trigger.dev/core': workspace:* @@ -1424,6 +1443,43 @@ importers: eslint: 8.45.0 typescript: 4.9.5 + references/svelte-example: + specifiers: + '@sveltejs/adapter-auto': ^2.0.0 + '@sveltejs/kit': ^1.20.4 + '@trigger.dev/sdk': workspace:* + '@trigger.dev/sveltekit': workspace:* + '@typescript-eslint/eslint-plugin': ^5.45.0 + '@typescript-eslint/parser': ^5.45.0 + eslint: ^8.28.0 + eslint-config-prettier: ^8.5.0 + eslint-plugin-svelte: ^2.30.0 + prettier: ^2.8.0 + prettier-plugin-svelte: ^2.10.1 + svelte: ^4.0.5 + svelte-check: ^3.4.3 + tslib: ^2.4.1 + typescript: ^5.0.0 + vite: ^4.4.2 + dependencies: + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/sveltekit': link:../../packages/sveltekit + devDependencies: + '@sveltejs/adapter-auto': 2.1.0_@sveltejs+kit@1.25.1 + '@sveltejs/kit': 1.25.1_svelte@4.2.1+vite@4.4.9 + '@typescript-eslint/eslint-plugin': 5.59.6_ltg3s7zeaq5lfn26f6cgiedibm + '@typescript-eslint/parser': 5.59.6_ox3na7ge7wjdarbyztnclevxam + eslint: 8.45.0 + eslint-config-prettier: 8.6.0_eslint@8.45.0 + eslint-plugin-svelte: 2.34.0_eslint@8.45.0+svelte@4.2.1 + prettier: 2.8.8 + prettier-plugin-svelte: 2.10.1_hfafeyo6vw33o2ufgpsfnrywzu + svelte: 4.2.1 + svelte-check: 3.5.2_svelte@4.2.1 + tslib: 2.6.2 + typescript: 5.2.2 + vite: 4.4.9 + references/unit-testing: specifiers: '@trigger.dev/sdk': workspace:* @@ -1461,6 +1517,14 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@ampproject/remapping/2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.19 /@angular-devkit/core/16.1.8: resolution: {integrity: sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==} @@ -1623,7 +1687,7 @@ packages: dset: 3.1.2 is-docker: 3.0.0 is-wsl: 3.0.0 - undici: 5.24.0 + undici: 5.25.4 which-pm-runs: 1.1.0 transitivePeerDependencies: - supports-color @@ -1662,7 +1726,7 @@ packages: resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.0 + '@ampproject/remapping': 2.2.1 '@babel/code-frame': 7.22.13 '@babel/generator': 7.22.15 '@babel/helper-compilation-targets': 7.22.15 @@ -1708,7 +1772,7 @@ packages: resolution: {integrity: sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.0 + '@ampproject/remapping': 2.2.1 '@babel/code-frame': 7.22.13 '@babel/generator': 7.22.15 '@babel/helper-compilation-targets': 7.22.15 @@ -6945,6 +7009,10 @@ packages: fast-uri: 2.2.0 dev: true + /@fastify/busboy/2.0.0: + resolution: {integrity: sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==} + engines: {node: '>=14'} + /@fastify/deepmerge/1.3.0: resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} dev: true @@ -7492,6 +7560,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + dev: true /@jridgewell/gen-mapping/0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} @@ -7682,7 +7751,7 @@ packages: engines: {node: '>=14'} dependencies: '@types/set-cookie-parser': 2.4.2 - set-cookie-parser: 2.5.1 + set-cookie-parser: 2.6.0 dev: true /@mswjs/interceptors/0.17.6: @@ -9494,6 +9563,10 @@ packages: config-chain: 1.1.13 dev: false + /@polka/url/1.0.0-next.23: + resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + dev: true + /@prisma/client/5.4.1_prisma@5.4.1: resolution: {integrity: sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ==} engines: {node: '>=16.13'} @@ -10935,7 +11008,7 @@ packages: '@types/cookie': 0.4.1 '@web3-storage/multipart-parser': 1.0.0 cookie: 0.4.2 - set-cookie-parser: 2.5.1 + set-cookie-parser: 2.6.0 source-map: 0.7.4 /@remix-run/server-runtime/2.0.1_typescript@4.9.5: @@ -12584,6 +12657,137 @@ packages: - supports-color dev: false + /@sveltejs/adapter-auto/2.1.0_@sveltejs+kit@1.25.1: + resolution: {integrity: sha512-o2pZCfATFtA/Gw/BB0Xm7k4EYaekXxaPGER3xGSY3FvzFJGTlJlZjBseaXwYSM94lZ0HniOjTokN3cWaLX6fow==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 + dependencies: + '@sveltejs/kit': 1.25.1_svelte@4.2.1+vite@4.4.9 + import-meta-resolve: 3.0.0 + dev: true + + /@sveltejs/kit/1.25.1: + resolution: {integrity: sha512-pD8XsvNJNgTNkFngNlM60my/X8dXWPKVzN5RghEQr0NjGZmuCjy49AfFu2cGbZjNf5pBcqd2RCNMW912P5fkhA==} + engines: {node: ^16.14 || >=18} + hasBin: true + requiresBuild: true + peerDependencies: + svelte: ^3.54.0 || ^4.0.0-next.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6 + '@types/cookie': 0.5.2 + cookie: 0.5.0 + devalue: 4.3.2 + esm-env: 1.0.0 + kleur: 4.1.5 + magic-string: 0.30.3 + mime: 3.0.0 + sade: 1.8.1 + set-cookie-parser: 2.6.0 + sirv: 2.0.3 + tiny-glob: 0.2.9 + undici: 5.25.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/kit/1.25.1_svelte@4.2.1+vite@4.4.9: + resolution: {integrity: sha512-pD8XsvNJNgTNkFngNlM60my/X8dXWPKVzN5RghEQr0NjGZmuCjy49AfFu2cGbZjNf5pBcqd2RCNMW912P5fkhA==} + engines: {node: ^16.14 || >=18} + hasBin: true + requiresBuild: true + peerDependencies: + svelte: ^3.54.0 || ^4.0.0-next.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6_svelte@4.2.1+vite@4.4.9 + '@types/cookie': 0.5.2 + cookie: 0.5.0 + devalue: 4.3.2 + esm-env: 1.0.0 + kleur: 4.1.5 + magic-string: 0.30.3 + mime: 3.0.0 + sade: 1.8.1 + set-cookie-parser: 2.6.0 + sirv: 2.0.3 + svelte: 4.2.1 + tiny-glob: 0.2.9 + undici: 5.25.4 + vite: 4.4.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte-inspector/1.0.4_25hzhjyralpt5lwm3sm7wb3ghq: + resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.2.0 + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte-inspector/1.0.4_x762n3kazuogrgydgy4ei75iqi: + resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.2.0 + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6_svelte@4.2.1+vite@4.4.9 + debug: 4.3.4 + svelte: 4.2.1 + vite: 4.4.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte/2.4.6: + resolution: {integrity: sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4_25hzhjyralpt5lwm3sm7wb3ghq + debug: 4.3.4 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.3 + svelte-hmr: 0.15.3 + vitefu: 0.2.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte/2.4.6_svelte@4.2.1+vite@4.4.9: + resolution: {integrity: sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4_x762n3kazuogrgydgy4ei75iqi + debug: 4.3.4 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.3 + svelte: 4.2.1 + svelte-hmr: 0.15.3_svelte@4.2.1 + vite: 4.4.9 + vitefu: 0.2.4_vite@4.4.9 + transitivePeerDependencies: + - supports-color + dev: true + /@swc/core-darwin-arm64/1.3.26: resolution: {integrity: sha512-FWWflBfKRYrUJtko2xiedC5XCa31O75IZZqnTWuLpe9g3C5tnUuF3M8LSXZS/dn6wprome1MhtG9GMPkSYkhkg==} engines: {node: '>=10'} @@ -12941,7 +13145,7 @@ packages: /@types/acorn/4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 dev: true /@types/aria-query/5.0.1: @@ -13049,6 +13253,10 @@ packages: /@types/cookie/0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + /@types/cookie/0.5.2: + resolution: {integrity: sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==} + dev: true + /@types/cookiejar/2.1.2: resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} dev: true @@ -13133,7 +13341,7 @@ packages: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: '@types/eslint': 8.4.10 - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 dev: true /@types/eslint/8.4.10: @@ -13146,13 +13354,13 @@ packages: /@types/estree-jsx/0.0.1: resolution: {integrity: sha512-gcLAYiMfQklDCPjQegGn0TBAn9it05ISEsEhlKQUddIk7o2XDokOcTN7HBO8tznM0D9dGezvHEfRZBfZf6me0A==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 dev: true /@types/estree-jsx/1.0.0: resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 dev: true /@types/estree/0.0.51: @@ -13162,6 +13370,10 @@ packages: /@types/estree/1.0.0: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + /@types/estree/1.0.2: + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + dev: true + /@types/express-serve-static-core/4.17.32: resolution: {integrity: sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==} dependencies: @@ -13600,6 +13812,10 @@ packages: /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + /@types/pug/2.0.7: + resolution: {integrity: sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==} + dev: true + /@types/qs/6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} @@ -13816,6 +14032,34 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin/5.59.6_ltg3s7zeaq5lfn26f6cgiedibm: + resolution: {integrity: sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.1 + '@typescript-eslint/parser': 5.59.6_ox3na7ge7wjdarbyztnclevxam + '@typescript-eslint/scope-manager': 5.59.6 + '@typescript-eslint/type-utils': 5.59.6_ox3na7ge7wjdarbyztnclevxam + '@typescript-eslint/utils': 5.59.6_ox3na7ge7wjdarbyztnclevxam + debug: 4.3.4 + eslint: 8.45.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.5.0 + tsutils: 3.21.0_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/eslint-plugin/6.7.4_ygtxu7ao4w7xzfo6eep522bcem: resolution: {integrity: sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -13943,6 +14187,26 @@ packages: - supports-color dev: true + /@typescript-eslint/parser/5.59.6_ox3na7ge7wjdarbyztnclevxam: + resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.59.6 + '@typescript-eslint/types': 5.59.6 + '@typescript-eslint/typescript-estree': 5.59.6_typescript@5.2.2 + debug: 4.3.4 + eslint: 8.45.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser/6.7.4_ox3na7ge7wjdarbyztnclevxam: resolution: {integrity: sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -14019,6 +14283,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils/5.59.6_ox3na7ge7wjdarbyztnclevxam: + resolution: {integrity: sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.59.6_typescript@5.2.2 + '@typescript-eslint/utils': 5.59.6_ox3na7ge7wjdarbyztnclevxam + debug: 4.3.4 + eslint: 8.45.0 + tsutils: 3.21.0_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/type-utils/6.7.4_ox3na7ge7wjdarbyztnclevxam: resolution: {integrity: sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -14151,6 +14435,27 @@ packages: transitivePeerDependencies: - supports-color + /@typescript-eslint/typescript-estree/5.59.6_typescript@5.2.2: + resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.59.6 + '@typescript-eslint/visitor-keys': 5.59.6 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree/6.7.4_typescript@5.2.2: resolution: {integrity: sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -14232,6 +14537,26 @@ packages: - typescript dev: true + /@typescript-eslint/utils/5.59.6_ox3na7ge7wjdarbyztnclevxam: + resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0_eslint@8.45.0 + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.59.6 + '@typescript-eslint/types': 5.59.6 + '@typescript-eslint/typescript-estree': 5.59.6_typescript@5.2.2 + eslint: 8.45.0 + eslint-scope: 5.1.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils/6.7.4_ox3na7ge7wjdarbyztnclevxam: resolution: {integrity: sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -14348,7 +14673,7 @@ packages: lodash: 4.17.21 mlly: 1.1.1 outdent: 0.8.0 - vite: 4.1.4 + vite: 4.4.9 vite-node: 0.28.5 transitivePeerDependencies: - '@types/node' @@ -14375,7 +14700,7 @@ packages: lodash: 4.17.21 mlly: 1.1.1 outdent: 0.8.0 - vite: 4.1.4_@types+node@18.11.18 + vite: 4.4.9_@types+node@18.11.18 vite-node: 0.28.5_@types+node@18.11.18 transitivePeerDependencies: - '@types/node' @@ -14946,6 +15271,12 @@ packages: dependencies: deep-equal: 2.2.0 + /aria-query/5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + /arr-diff/1.1.0: resolution: {integrity: sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==} engines: {node: '>=0.10.0'} @@ -15325,6 +15656,12 @@ packages: dependencies: deep-equal: 2.2.0 + /axobject-query/3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + dependencies: + dequal: 2.0.3 + dev: true + /b4a/1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} optional: true @@ -16416,6 +16753,16 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: true + /code-red/1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + '@types/estree': 1.0.2 + acorn: 8.10.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + dev: true + /codemirror/6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: @@ -16887,6 +17234,14 @@ packages: source-map: 0.6.1 dev: false + /css-tree/2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + /css-unit-converter/1.1.2: resolution: {integrity: sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==} dev: false @@ -17218,6 +17573,11 @@ packages: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} + /deepmerge/4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + /default-browser-id/3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} engines: {node: '>=12'} @@ -17814,6 +18174,10 @@ packages: resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} dev: true + /es6-promise/3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + dev: true + /es6-symbol/3.1.3: resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} dependencies: @@ -18377,6 +18741,15 @@ packages: eslint: 8.31.0 dev: true + /eslint-config-prettier/8.6.0_eslint@8.45.0: + resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.45.0 + dev: true + /eslint-config-prettier/9.0.0_eslint@8.45.0: resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true @@ -19041,6 +19414,34 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 + /eslint-plugin-svelte/2.34.0_eslint@8.45.0+svelte@4.2.1: + resolution: {integrity: sha512-4RYUgNai7wr0v+T/kljMiYSjC/oqwgq5i+cPppawryAayj4C7WK1ixFlWCGmNmBppnoKCl4iA4ZPzPtlHcb4CA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0-0 + svelte: ^3.37.0 || ^4.0.0 + peerDependenciesMeta: + svelte: + optional: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0_eslint@8.45.0 + '@jridgewell/sourcemap-codec': 1.4.15 + debug: 4.3.4 + eslint: 8.45.0 + esutils: 2.0.3 + known-css-properties: 0.28.0 + postcss: 8.4.29 + postcss-load-config: 3.1.4_postcss@8.4.29 + postcss-safe-parser: 6.0.0_postcss@8.4.29 + postcss-selector-parser: 6.0.11 + semver: 7.5.4 + svelte: 4.2.1 + svelte-eslint-parser: 0.33.1_svelte@4.2.1 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /eslint-plugin-testing-library/5.11.0_iukboom6ndih5an6iafl45j2fe: resolution: {integrity: sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} @@ -19286,6 +19687,10 @@ packages: transitivePeerDependencies: - supports-color + /esm-env/1.0.0: + resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + dev: true + /espree/9.4.1: resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -19350,7 +19755,7 @@ packages: /estree-util-attach-comments/2.1.0: resolution: {integrity: sha512-rJz6I4L0GaXYtHpoMScgDIwM0/Vwbu5shbMeER596rB2D1EWF6+Gj0e0UKzJPZrpoOc87+Q2kgVFHfjAymIqmw==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 dev: true /estree-util-build-jsx/2.2.2: @@ -20963,7 +21368,7 @@ packages: /hast-util-to-estree/2.1.0: resolution: {integrity: sha512-Vwch1etMRmm89xGgz+voWXvVHba2iiMdGMKmaMfYt35rbVtFDq8JNwwAIvi8zHMkO6Gvqo9oTMwJTmzVRfXh4g==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 '@types/estree-jsx': 1.0.0 '@types/hast': 2.3.4 '@types/unist': 2.0.6 @@ -22180,7 +22585,7 @@ packages: babel-jest: 29.6.2_@babel+core@7.22.17 chalk: 4.1.2 ci-info: 3.8.0 - deepmerge: 4.2.2 + deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.10 jest-circus: 29.6.2 @@ -22220,7 +22625,7 @@ packages: babel-jest: 29.6.2_@babel+core@7.22.17 chalk: 4.1.2 ci-info: 3.8.0 - deepmerge: 4.2.2 + deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.10 jest-circus: 29.6.2 @@ -22865,6 +23270,10 @@ packages: engines: {node: '>= 8'} dev: true + /known-css-properties/0.28.0: + resolution: {integrity: sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==} + dev: true + /language-subtag-registry/0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -22933,7 +23342,7 @@ packages: dependencies: cookie: 0.5.0 process-warning: 2.2.0 - set-cookie-parser: 2.5.1 + set-cookie-parser: 2.6.0 dev: true /lilconfig/2.0.6: @@ -23038,6 +23447,10 @@ packages: - supports-color dev: false + /locate-character/3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + dev: true + /locate-path/3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -23212,6 +23625,13 @@ packages: engines: {node: '>=6'} dev: true + /magic-string/0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /magic-string/0.30.0: resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} engines: {node: '>=12'} @@ -23560,6 +23980,10 @@ packages: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} dev: false + /mdn-data/2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + /mdurl/1.0.1: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} dev: true @@ -23868,7 +24292,7 @@ packages: resolution: {integrity: sha512-WWp3bf7xT9MppNuw3yPjpnOxa8cj5ACivEzXJKu0WwnjBYfzaBvIAT9KfeyI0Qkll+bfQtfftSwdgTH6QhTOKw==} dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 estree-util-visit: 1.2.0 micromark-util-types: 1.0.2 uvu: 0.5.6 @@ -25666,6 +26090,14 @@ packages: is-reference: 3.0.1 dev: true + /periscopic/3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.0 + estree-walker: 3.0.3 + is-reference: 3.0.1 + dev: true + /pg-connection-string/2.5.0: resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} dev: false @@ -25948,6 +26380,23 @@ packages: yaml: 1.10.2 dev: true + /postcss-load-config/3.1.4_postcss@8.4.29: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.29 + yaml: 1.10.2 + dev: true + /postcss-load-config/4.0.1: resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} @@ -26172,6 +26621,24 @@ packages: postcss-selector-parser: 6.0.11 dev: false + /postcss-safe-parser/6.0.0_postcss@8.4.29: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.29 + dev: true + + /postcss-scss/4.0.9_postcss@8.4.29: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + dependencies: + postcss: 8.4.29 + dev: true + /postcss-selector-parser/6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -26331,6 +26798,16 @@ packages: fast-diff: 1.3.0 dev: true + /prettier-plugin-svelte/2.10.1_hfafeyo6vw33o2ufgpsfnrywzu: + resolution: {integrity: sha512-Wlq7Z5v2ueCubWo0TZzKc9XHcm7TDxqcuzRuGd0gcENfzfT4JZ9yDlCbEgxWgiPmLHkBjfOtpAWkcT28MCDpUQ==} + peerDependencies: + prettier: ^1.16.4 || ^2.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 + dependencies: + prettier: 2.8.8 + svelte: 4.2.1 + dev: true + /prettier-plugin-tailwindcss/0.3.0_prettier@2.8.8: resolution: {integrity: sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==} engines: {node: '>=12.17.0'} @@ -27931,6 +28408,15 @@ packages: /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sander/0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true + /sass-loader/13.2.2: resolution: {integrity: sha512-nrIdVAAte3B9icfBiGWvmMhT/D+eCDwnk+yA7VE/76dp/WkHX+i44Q/pfo71NYbwj0Ap+PGsn0ekOuU1WFJ2AA==} engines: {node: '>= 14.15.0'} @@ -28130,6 +28616,9 @@ packages: /set-cookie-parser/2.5.1: resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} + /set-cookie-parser/2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + /set-harmonic-interval/1.0.1: resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} engines: {node: '>=6.9'} @@ -28314,6 +28803,15 @@ packages: resolution: {integrity: sha512-bBAoTn75tuKh83opmZ1VoyVoQIsvLCKzSxuasAxbnKofrT8eGyOEIaXSuNfhi/hI160+fwsR7ObcbBpOyzDvXg==} dev: false + /sirv/2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.23 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -28441,6 +28939,16 @@ packages: atomic-sleep: 1.0.0 dev: true + /sorcery/0.11.0: + resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} + hasBin: true + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + buffer-crc32: 0.2.13 + minimist: 1.2.7 + sander: 0.5.1 + dev: true + /sort-object-keys/1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} dev: true @@ -29070,6 +29578,132 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /svelte-check/3.5.2_svelte@4.2.1: + resolution: {integrity: sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==} + hasBin: true + peerDependencies: + svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + chokidar: 3.5.3 + fast-glob: 3.3.1 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 4.2.1 + svelte-preprocess: 5.0.4_ihwjmfflvgyqta4xkfhaggvtwe + typescript: 5.2.2 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + dev: true + + /svelte-eslint-parser/0.33.1_svelte@4.2.1: + resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 + peerDependenciesMeta: + svelte: + optional: true + dependencies: + eslint-scope: 7.2.0 + eslint-visitor-keys: 3.4.2 + espree: 9.6.0 + postcss: 8.4.29 + postcss-scss: 4.0.9_postcss@8.4.29 + svelte: 4.2.1 + dev: true + + /svelte-hmr/0.15.3: + resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: ^3.19.0 || ^4.0.0 + dev: true + + /svelte-hmr/0.15.3_svelte@4.2.1: + resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: ^3.19.0 || ^4.0.0 + dependencies: + svelte: 4.2.1 + dev: true + + /svelte-preprocess/5.0.4_ihwjmfflvgyqta4xkfhaggvtwe: + resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} + engines: {node: '>= 14.10.0'} + requiresBuild: true + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 + svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 + typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + dependencies: + '@types/pug': 2.0.7 + detect-indent: 6.1.0 + magic-string: 0.27.0 + sorcery: 0.11.0 + strip-indent: 3.0.0 + svelte: 4.2.1 + typescript: 5.2.2 + dev: true + + /svelte/4.2.1: + resolution: {integrity: sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==} + engines: {node: '>=16'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + acorn: 8.10.0 + aria-query: 5.3.0 + axobject-query: 3.2.1 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.1 + locate-character: 3.0.0 + magic-string: 0.30.3 + periscopic: 3.1.0 + dev: true + /symbol-observable/4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -29525,6 +30159,11 @@ packages: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} dev: true + /totalist/3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /touch/3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true @@ -30125,6 +30764,16 @@ packages: tslib: 1.14.1 typescript: 5.1.6 + /tsutils/3.21.0_typescript@5.2.2: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.2.2 + dev: true + /tsx/3.12.2: resolution: {integrity: sha512-ykAEkoBg30RXxeOMVeZwar+JH632dZn9EUJVyJwhfag62k6UO/dIyJEV58YuLF6e5BTdV/qmbQrpkWqjq9cUnQ==} hasBin: true @@ -30392,6 +31041,12 @@ packages: dependencies: busboy: 1.6.0 + /undici/5.25.4: + resolution: {integrity: sha512-450yJxT29qKMf3aoudzFpIciqpx6Pji3hEWaXqXmanbXF58LTAGCKxcJjxMXWu3iG+Mudgo3ZUfDB6YDFd/dAw==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.0.0 + /unfetch/4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -31026,73 +31681,6 @@ packages: - typescript dev: true - /vite/4.1.4: - resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.16.17 - postcss: 8.4.29 - resolve: 1.22.1 - rollup: 3.10.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vite/4.1.4_@types+node@18.11.18: - resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 18.11.18 - esbuild: 0.16.17 - postcss: 8.4.29 - resolve: 1.22.1 - rollup: 3.10.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vite/4.1.4_@types+node@18.17.1: resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -31266,6 +31854,15 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vitefu/0.2.4: + resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + vite: + optional: true + dev: true + /vitefu/0.2.4_vite@4.4.9: resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} peerDependencies: @@ -31521,7 +32118,7 @@ packages: optional: true dependencies: '@types/eslint-scope': 3.7.4 - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 '@webassemblyjs/ast': 1.11.5 '@webassemblyjs/wasm-edit': 1.11.5 '@webassemblyjs/wasm-parser': 1.11.5 @@ -31561,7 +32158,7 @@ packages: optional: true dependencies: '@types/eslint-scope': 3.7.4 - '@types/estree': 1.0.0 + '@types/estree': 1.0.2 '@webassemblyjs/ast': 1.11.5 '@webassemblyjs/wasm-edit': 1.11.5 '@webassemblyjs/wasm-parser': 1.11.5 diff --git a/references/svelte-example/.env.example b/references/svelte-example/.env.example new file mode 100644 index 000000000..1482c408b --- /dev/null +++ b/references/svelte-example/.env.example @@ -0,0 +1,3 @@ +TRIGGER_API_KEY= +TRIGGER_API_URL= + diff --git a/references/svelte-example/.eslintignore b/references/svelte-example/.eslintignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/references/svelte-example/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/references/svelte-example/.eslintrc.cjs b/references/svelte-example/.eslintrc.cjs new file mode 100644 index 000000000..ebc19589f --- /dev/null +++ b/references/svelte-example/.eslintrc.cjs @@ -0,0 +1,30 @@ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'] + }, + env: { + browser: true, + es2017: true, + node: true + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser' + } + } + ] +}; diff --git a/references/svelte-example/.gitignore b/references/svelte-example/.gitignore new file mode 100644 index 000000000..6635cf554 --- /dev/null +++ b/references/svelte-example/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/references/svelte-example/.npmrc b/references/svelte-example/.npmrc new file mode 100644 index 000000000..0c05da457 --- /dev/null +++ b/references/svelte-example/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +resolution-mode=highest diff --git a/references/svelte-example/.prettierignore b/references/svelte-example/.prettierignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/references/svelte-example/.prettierignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/references/svelte-example/.prettierrc b/references/svelte-example/.prettierrc new file mode 100644 index 000000000..a77fddea9 --- /dev/null +++ b/references/svelte-example/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/references/svelte-example/README.md b/references/svelte-example/README.md new file mode 100644 index 000000000..5c91169b0 --- /dev/null +++ b/references/svelte-example/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/references/svelte-example/package.json b/references/svelte-example/package.json new file mode 100644 index 000000000..92c355be2 --- /dev/null +++ b/references/svelte-example/package.json @@ -0,0 +1,38 @@ +{ + "name": "svelte-example", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.20.4", + "@typescript-eslint/eslint-plugin": "^5.45.0", + "@typescript-eslint/parser": "^5.45.0", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-svelte": "^2.30.0", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.10.1", + "svelte": "^4.0.5", + "svelte-check": "^3.4.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.4.2" + }, + "dependencies": { + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/sveltekit": "workspace:*" + }, + "trigger.dev": { + "endpointId": "sveltekit-example" + }, + "type": "module" +} diff --git a/references/svelte-example/src/app.d.ts b/references/svelte-example/src/app.d.ts new file mode 100644 index 000000000..f59b884c5 --- /dev/null +++ b/references/svelte-example/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/references/svelte-example/src/app.html b/references/svelte-example/src/app.html new file mode 100644 index 000000000..effe0d0d2 --- /dev/null +++ b/references/svelte-example/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/references/svelte-example/src/jobs/example.ts b/references/svelte-example/src/jobs/example.ts new file mode 100644 index 000000000..aad34452b --- /dev/null +++ b/references/svelte-example/src/jobs/example.ts @@ -0,0 +1,20 @@ +import { eventTrigger } from '@trigger.dev/sdk'; +import { client } from '../trigger'; + +// your first job +client.defineJob({ + id: 'test-svelte-job', + name: 'Test sveltekit', + version: '0.0.1', + trigger: eventTrigger({ + name: 'test.event' + }), + run: async (payload, io, ctx) => { + await io.wait("waiting", 5) + await io.logger.info('Hello world!', { payload }); + + return { + message: 'Hello world!' + }; + } +}); diff --git a/references/svelte-example/src/lib/index.ts b/references/svelte-example/src/lib/index.ts new file mode 100644 index 000000000..856f2b6c3 --- /dev/null +++ b/references/svelte-example/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/references/svelte-example/src/routes/+page.svelte b/references/svelte-example/src/routes/+page.svelte new file mode 100644 index 000000000..5982b0ae3 --- /dev/null +++ b/references/svelte-example/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit kit.svelte.dev to read the documentation

diff --git a/references/svelte-example/src/routes/api/trigger/+server.ts b/references/svelte-example/src/routes/api/trigger/+server.ts new file mode 100644 index 000000000..bfe2c7294 --- /dev/null +++ b/references/svelte-example/src/routes/api/trigger/+server.ts @@ -0,0 +1,12 @@ +import { createSvelteRoute } from '@trigger.dev/sveltekit'; + +import { client } from '$trigger'; + +// // Replace this with your own jobs +import '$jobs/example'; + +// Create the Svelte route handler using the createSvelteRoute function +const svelteRoute = createSvelteRoute(client); + +// Define your API route handler +export const POST = svelteRoute.POST; diff --git a/references/svelte-example/src/trigger.ts b/references/svelte-example/src/trigger.ts new file mode 100644 index 000000000..acea0c199 --- /dev/null +++ b/references/svelte-example/src/trigger.ts @@ -0,0 +1,7 @@ +import { TriggerClient } from '@trigger.dev/sdk'; +import { TRIGGER_API_KEY } from '$env/static/private'; + +export const client = new TriggerClient({ + id: 'sveltekit-example', + apiKey: TRIGGER_API_KEY, +}); diff --git a/references/svelte-example/static/favicon.png b/references/svelte-example/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Date: Sun, 8 Oct 2023 17:20:07 +0100 Subject: [PATCH 30/79] Try and determine an error some HTTP requests are causing (#586) --- .../app/routes/api.v1.sources.http.$id.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.sources.http.$id.ts b/apps/webapp/app/routes/api.v1.sources.http.$id.ts index 27092e881..c89a0a891 100644 --- a/apps/webapp/app/routes/api.v1.sources.http.$id.ts +++ b/apps/webapp/app/routes/api.v1.sources.http.$id.ts @@ -6,11 +6,24 @@ import { HandleHttpSourceService } from "~/services/sources/handleHttpSource.ser export async function action({ request, params }: ActionArgs) { logger.info("Handling http source", { url: request.url }); - const { id } = z.object({ id: z.string() }).parse(params); + try { + const { id } = z.object({ id: z.string() }).parse(params); + const service = new HandleHttpSourceService(); + const result = await service.call(id, request); - const service = new HandleHttpSourceService(); - - return await service.call(id, request); + return new Response(undefined, { + status: result.status, + }); + } catch (e) { + if (e instanceof Error) { + logger.error("Error handling http source", { error: e.message }); + } else { + logger.error("Error handling http source", { error: JSON.stringify(e) }); + } + return new Response(undefined, { + status: 500, + }); + } } export async function loader({ request, params }: LoaderArgs) { From 2394fcdf357aa09e0a9cd00b52a3023204e45528 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Mon, 9 Oct 2023 09:58:20 +0100 Subject: [PATCH 31/79] Added line about Stripe Shell to the Stripe docs --- docs/integrations/apis/stripe.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/integrations/apis/stripe.mdx b/docs/integrations/apis/stripe.mdx index 58901892a..87e2a10f5 100644 --- a/docs/integrations/apis/stripe.mdx +++ b/docs/integrations/apis/stripe.mdx @@ -39,6 +39,8 @@ const stripe = new Stripe({ The Stripe integration exposes a number of triggers that can be used on a job, powered by Stripe webhooks. +We recommend testing Stripe payloads using [Stripe Shell](https://stripe.com/docs/stripe-cli?shell=true), Stripe's browser-based shell with the Stripe CLI pre-installed. + ```ts client.defineJob({ id: "stripe-price", From ee033a1fd4a3138492ab41cbfbeb81e8c274652f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 9 Oct 2023 11:21:44 +0100 Subject: [PATCH 32/79] =?UTF-8?q?Onboarding,=20don=E2=80=99t=20show=20unde?= =?UTF-8?q?fined=20if=20there=20are=20no=20extra=20params=20for=20dev=20co?= =?UTF-8?q?mmand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/webapp/app/components/SetupCommands.tsx | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index f04083c7e..5c3469369 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -53,13 +53,25 @@ export function RunDevCommand({ extra }: { extra?: string }) { yarn - + - + - + ); @@ -77,21 +89,21 @@ export function TriggerDevCommand({ extra }: { extra?: string }) { From d0d5a698ec4193e0456dc42524a6713dbd2715a7 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 9 Oct 2023 14:03:17 +0100 Subject: [PATCH 33/79] Added versions to Supabase management docs examples that are missing them --- docs/integrations/apis/supabase/management.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/integrations/apis/supabase/management.mdx b/docs/integrations/apis/supabase/management.mdx index ec3e151d0..286fc93e7 100644 --- a/docs/integrations/apis/supabase/management.mdx +++ b/docs/integrations/apis/supabase/management.mdx @@ -125,6 +125,7 @@ Now, you can use the `db` instance to add a trigger to run a job when a row is i client.defineJob({ id: "supabase-trigger", name: "Supabase Trigger", + version: "1.0.0", trigger: db.onInserted({ table: "todos", }), @@ -140,6 +141,7 @@ You can add additional filters to the trigger by passing a `filter` object: client.defineJob({ id: "supabase-trigger", name: "Supabase Trigger", + version: "1.0.0", trigger: db.onUpdated({ table: "todos", // Only trigger if the todo is marked as completed @@ -164,6 +166,7 @@ You can also listen for multiple different events using the `on` trigger: client.defineJob({ id: "supabase-trigger", name: "Supabase Trigger", + version: "1.0.0", trigger: db.on({ table: "todos", events: ["INSERT", "UPDATE"] // Trigger on both insert and update events @@ -206,6 +209,7 @@ const db = supabase.db("https://.supabase.co"); client.defineJob({ id: "supabase-trigger", name: "Supabase Trigger", + version: "1.0.0", trigger: db.onUpdated({ table: "todos", }), From 4cd97c81ea3b2d23513f6986039a34708082325e Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Mon, 9 Oct 2023 15:20:17 +0100 Subject: [PATCH 34/79] Swapped out the Integration page skeleton state for a simpler message --- .../route.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx index 2193f90a8..35590ffa6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx @@ -8,6 +8,7 @@ import { Callout } from "~/components/primitives/Callout"; import { Header2 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; import { Input } from "~/components/primitives/Input"; +import { Paragraph } from "~/components/primitives/Paragraph"; import { useFilterJobs } from "~/hooks/useFilterJobs"; import { useIntegrationClient } from "~/hooks/useIntegrationClient"; import { JobListPresenter } from "~/presenters/JobListPresenter.server"; @@ -52,10 +53,8 @@ export default function Page() { {(open) => (
-
- {jobs.length === 0 ? ( - Jobs using this integration will appear here - ) : ( +
+ {jobs.length !== 0 && (
{jobs.length === 0 ? ( - <> - - +
+ Jobs using this Integration will appear here. +
) : ( Date: Mon, 9 Oct 2023 16:04:32 +0100 Subject: [PATCH 35/79] Fixes #583 by filtering out properties with missing values (#584) This also improves the retrying behavior of the resend integration by skipping retrying certain client errors --- .changeset/sharp-lobsters-cross.md | 6 ++++ .../app/routes/api.v1.runs.$runId.tasks.ts | 12 +++++++- docs/sdk/io/runtask.mdx | 3 +- integrations/resend/src/index.ts | 9 ++++++ packages/core/src/schemas/api.ts | 1 + references/job-catalog/src/resend.ts | 28 +++++++++++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changeset/sharp-lobsters-cross.md diff --git a/.changeset/sharp-lobsters-cross.md b/.changeset/sharp-lobsters-cross.md new file mode 100644 index 000000000..97f6345cd --- /dev/null +++ b/.changeset/sharp-lobsters-cross.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/resend": patch +"@trigger.dev/core": patch +--- + +Allow task property values to be blank, but strip them out before persisting them diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 94a6ba8ca..1e4dc7736 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -280,7 +280,7 @@ export class RunTaskService { noop: taskBody.noop, delayUntil: taskBody.delayUntil, params: taskBody.params ?? undefined, - properties: taskBody.properties ?? undefined, + properties: this.#filterProperties(taskBody.properties) ?? undefined, redact: taskBody.redact ?? undefined, operation: taskBody.operation, callbackUrl, @@ -325,4 +325,14 @@ export class RunTaskService { return task ? taskWithAttemptsToServerTask(task) : undefined; } + + #filterProperties(properties: RunTaskBodyOutput["properties"]): RunTaskBodyOutput["properties"] { + if (!properties) return; + + return properties.filter((property) => { + if (!property) return false; + + return typeof property.label === "string" && typeof property.text === "string"; + }); + } } diff --git a/docs/sdk/io/runtask.mdx b/docs/sdk/io/runtask.mdx index 46a8d829d..dfd9e1ead 100644 --- a/docs/sdk/io/runtask.mdx +++ b/docs/sdk/io/runtask.mdx @@ -128,6 +128,7 @@ The wrappers at `io.integration.runTask()` expose the underlying Integration cli The value of the property. + @@ -135,7 +136,7 @@ The wrappers at `io.integration.runTask()` expose the underlying Integration cli An optional callback that will be called when the Task fails. You can perform - logic in here and optionally return a custom error object. Returning an object with `{ retryAt: Date, error?: Error }` will retry the Task at the specified Date. You can also just return a new `Error` object to throw a new error. Return nothing to rethrow the original error. + logic in here and optionally return a custom error object. Returning an object with `{ retryAt: Date, error?: Error }` will retry the Task at the specified Date. You can also just return a new `Error` object to throw a new error. Returning `null` or `undefined` will rethrow the original error. If you want to force retrying to be skipped, return `{ skipRetrying: true }`. diff --git a/integrations/resend/src/index.ts b/integrations/resend/src/index.ts index b483fa48c..bd2a663d6 100644 --- a/integrations/resend/src/index.ts +++ b/integrations/resend/src/index.ts @@ -25,6 +25,9 @@ function isRequestError(error: unknown): error is ErrorResponse { return typeof error === "object" && error !== null && "statusCode" in error; } +// See https://resend.com/docs/api-reference/errors +const skipRetryingErrors = [422, 401, 403, 404, 405, 422]; + function onError(error: unknown) { if (!isRequestError(error)) { if (error instanceof Error) { @@ -34,6 +37,12 @@ function onError(error: unknown) { return new Error("Unknown error"); } + if (skipRetryingErrors.includes(error.statusCode)) { + return { + skipRetrying: true, + }; + } + return new Error(error.message); } diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index ac9828e7a..6abac4218 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -664,6 +664,7 @@ export const RunTaskBodyInputSchema = RunTaskOptionsSchema.extend({ export type RunTaskBodyInput = z.infer; export const RunTaskBodyOutputSchema = RunTaskBodyInputSchema.extend({ + properties: z.array(DisplayPropertySchema.partial()).optional(), params: DeserializedJsonSchema.optional().nullable(), callback: z .object({ diff --git a/references/job-catalog/src/resend.ts b/references/job-catalog/src/resend.ts index bbd585854..934529140 100644 --- a/references/job-catalog/src/resend.ts +++ b/references/job-catalog/src/resend.ts @@ -43,4 +43,32 @@ client.defineJob({ }, }); +client.defineJob({ + id: "send-resend-email-from-blank", + name: "Send Resend Email From Blank", + version: "0.1.0", + trigger: eventTrigger({ + name: "send.email", + schema: z.object({ + to: z.union([z.string(), z.array(z.string())]), + subject: z.string(), + text: z.string(), + from: z.string().optional(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.sendEmail("📧", { + to: payload.to, + subject: payload.subject, + text: payload.text, + from: payload.from!, + }); + + await io.logger.info("Sent email", { response }); + }, +}); + createExpressServer(client); From cbc976188e3dc590ec9ba8c4d61ed96200ab76c2 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 9 Oct 2023 16:48:38 +0100 Subject: [PATCH 36/79] GitHub docs improvements (#568) * Updated github-triggers and intro * Edited tasks / triggers and main overview page * Added links to scopes docs and improved copy * Added an overview, side bar title and improved the ordering / wording * Added the underlying Github client section and some formatting updates --------- Co-authored-by: Eric Allam Co-authored-by: Eric Allam --- docs/integrations/apis/github-tasks.mdx | 253 +- docs/integrations/apis/github-triggers.mdx | 3063 +++++++++++++++++++- docs/integrations/apis/github.mdx | 94 +- 3 files changed, 3302 insertions(+), 108 deletions(-) diff --git a/docs/integrations/apis/github-tasks.mdx b/docs/integrations/apis/github-tasks.mdx index 410dc1170..2dbc2e33d 100644 --- a/docs/integrations/apis/github-tasks.mdx +++ b/docs/integrations/apis/github-tasks.mdx @@ -1,54 +1,263 @@ --- -title: Tasks +title: GitHub Tasks +sidebarTitle: Tasks +--- + +Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want. + --- ## All tasks -| Function Name | Description | -| -------------------------------- | ----------------------------------------------------------- | -| `createIssue` | Creates a new issue in a repository. | -| `addIssueAssignees` | Adds assignees to an existing issue. | -| `addIssueLabels` | Adds labels to an existing issue. | -| `createIssueComment` | Creates a new comment on an existing issue. | -| `getRepo` | Retrieves information about a repository. | -| `createIssueCommentWithReaction` | Creates a new comment on an existing issue with a reaction. | -| `addIssueCommentReaction` | Adds a reaction to an existing issue comment. | -| `updateWebhook` | Updates an existing webhook. | -| `createWebhook` | Creates a new webhook. | -| `listWebhooks` | Lists the webhooks for a repository. | -| `updateOrgWebhook` | Updates an existing webhook for an organization. | -| `createOrgWebhook` | Creates a new webhook for an organization. | -| `listOrgWebhooks` | Lists the webhooks for an organization. | +### `createIssue` -## Usage +Creates a new issue in a repository. [Official GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/issues/issues?apiVersion=2022-11-28#create-an-issue). + +```ts example.ts +await io.github.createIssue("create issue", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + title: "", // the title of the issue + body: "", // the contents of the issue +}); +``` + +### `addIssueAssignees` + +Adds assignees to an existing issue. [Official GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/issues/assignees?apiVersion=2022-11-28#add-assignees-to-an-issue). + +```ts example.ts +await io.github.addIssueAssignees("add assignee", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + issueNumber: , // the number of the issue + assignees: [""], // the name(s) of the assignee(s) +}); +``` + +### `addIssueLabels` + +Adds labels to an existing issue. [Official GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/issues/labels?apiVersion=2022-11-28#add-labels-to-an-issue). + +```ts example.ts +await io.github.addIssueLabels("add label", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + issueNumber: , // the number of the issue + labels: [""], // the name(s) of the label(s) +}); +``` + +### `createIssueComment` + +Creates a new comment on an existing issue. [Official GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment). + +```ts example.ts +await io.github.createIssueComment("create comment", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + issueNumber: , // the number of the issue + body: "", // the contents of the comment +}); +``` + +### `getRepo` + +Retrieves information about a repository. [Official GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/repos/repos?apiVersion=2022-11-28#get-a-repository). + +```ts example.ts +const repoInfo = await io.github.getRepo({ + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository +}); +``` + +### `createIssueCommentWithReaction` + +Creates a new comment on an existing issue with a reaction. [Official GitHub docs](https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment). + +```ts example.ts +await io.github.createIssueCommentWithReaction("create comment with reaction", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + issueNumber: , // the number of the issue + body: "", // the contents of the comment + content: "", // the type of reaction +}); +``` + +### `addIssueCommentReaction` + +Adds a reaction to an existing issue comment. [Official GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/reactions/reactions?apiVersion=2022-11-28#create-reaction-for-an-issue-comment). + +```ts example.ts +await io.github.addIssueCommentReaction("add reaction", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + commentId: , // the id of the specific comment + content: "", // the type of reaction +}); +``` + +### `updateWebhook` + +Updates an existing webhook. [Official GitHub docs](https://docs.github.com/en/rest/webhooks/repos?apiVersion=2022-11-28#update-a-repository-webhook). + +```ts example.ts +await io.github.updateWebhook("update webhook", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + webhookId: , // the unique id of the webhook + config: { + url: "", // the url to which payloads will be delivered + contentType: "json", // the media type used to serialize the payloads + secret: "", // If provided, the secret will be used as the key to generate the HMAC hex digest value for delivery signature headers. + }, +}); +``` + +### `createWebhook` + +Creates a new webhook. [Official GitHub docs](https://docs.github.com/en/rest/webhooks/repos?apiVersion=2022-11-28#create-a-repository-webhook). + +```ts example.ts +await io.github.createWebhook("create webhook", { + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository + config: { + url: "", // the url to which payloads will be delivered + contentType: "json", // the media type used to serialize the payloads + secret: "", // If provided, the secret will be used as the key to generate the HMAC hex digest value for delivery signature headers. + }, + events: [""], // the events for which the webhook will trigger +}); +``` + +### `listWebhooks` + +Lists the webhooks for a repository. [Official GitHub docs](https://docs.github.com/en/rest/webhooks/repos?apiVersion=2022-11-28#list-repository-webhooks). + +```ts example.ts +const webhooks = await io.github.listWebhooks({ + owner: "", // the name of the owner of the repository + repo: "", // the name of the repository +}); +``` + +### `updateOrgWebhook` + +Updates an existing webhook for an organization. [Official GitHub docs](https://docs.github.com/en/rest/orgs/webhooks?apiVersion=2022-11-28#update-an-organization-webhook). + +```ts +await io.github.updateOrgWebhook("update org webhook", { + org: "", // the name of the organization + webhookId: , // the unique id of the webhook + config: { + url: "", // the url to which payloads will be delivered + contentType: "json", // the media type used to serialize the payloads + secret: "", // If provided, the secret will be used as the key to generate the HMAC hex digest value for delivery signature headers. + }, +}); +``` + +### `createOrgWebhook` + +Creates a new webhook for an organization. [Official GitHub docs](https://docs.github.com/en/rest/orgs/webhooks?apiVersion=2022-11-28#create-an-organization-webhook). + +```ts example.ts +await io.github.createOrgWebhook("create org webhook", { + org: "", // the name of the organization + config: { + url: "", // the url to which payloads will be delivered + contentType: "json", // the media type used to serialize the payloads + secret: "", // If provided, the secret will be used as the key to generate the HMAC hex digest value for delivery signature headers. + }, + events: [""], // the events for which the webhook will trigger +}); +``` + +### `listOrgWebhooks` + +Lists the webhooks for an organization. [Official GitHub docs](https://docs.github.com/en/rest/orgs/webhooks?apiVersion=2022-11-28#list-organization-webhooks). + +```ts example.ts +const orgWebhooks = await io.github.listOrgWebhooks({ + org: "", // the name of the organization + per-page: , // the number of webhooks to return per page (max 100) + page: , // Page number of the results to fetch. +}); +``` + +## Example usage + +In this example we'll create a task that adds an assignee and a label to an issue when it's opened. ```ts client.defineJob({ id: "github-integration-on-issue-opened", name: "GitHub Integration - On Issue Opened", - version: "0.1.0", + version: "1.0.0", integrations: { github }, trigger: github.triggers.repo({ event: events.onIssueOpened, - owner: "triggerdotdev", - repo: "empty", + owner: "", + repo: "", }), run: async (payload, io, ctx) => { await io.github.addIssueAssignees("add assignee", { owner: payload.repository.owner.login, repo: payload.repository.name, issueNumber: payload.issue.number, - assignees: ["matt-aitken"], + assignees: [""], }); await io.github.addIssueLabels("add label", { owner: payload.repository.owner.login, repo: payload.repository.name, issueNumber: payload.issue.number, - labels: ["bug"], + labels: [""], }); return { payload, ctx }; }, }); ``` + +## Using the underlying GitHub client + +You can access the [Octokit instance](https://github.com/octokit/octokit.js#octokit-api-client) by using the `runTask` method on the integration: + +```ts +const github = new Github({ + id: "github", +}); + +client.defineJob({ + id: "github-example-1", + name: "GitHub Example 1", + version: "0.1.0", + trigger: eventTrigger({ + name: "github.example", + }), + integrations: { + github, + }, + run: async (payload, io, ctx) => { + const contributors = await io.github.runTask( + "get-contributors", + async (octokit, task) => { + const contributors = await octokit.rest.repos.listContributors({ + owner: "", + repo: "", + }); + + return contributors; + }, + //this is optional, it will appear on the Run page + { name: "List Contributors" } + ); + }, +}); +``` + +Make sure to pass the `idempotencyKey` to the underlying client to ensure that the API call is only executed once. This is only needed for mutating API calls. diff --git a/docs/integrations/apis/github-triggers.mdx b/docs/integrations/apis/github-triggers.mdx index d66e62478..3b21ee59e 100644 --- a/docs/integrations/apis/github-triggers.mdx +++ b/docs/integrations/apis/github-triggers.mdx @@ -1,46 +1,3053 @@ --- -title: Triggers +title: GitHub Triggers & Events +sidebarTitle: Triggers & Events --- -## All triggers +You can use these triggers to start a job when a GitHub event occurs. -| Function Name | Description | -| --------------------- | -------------------------------------------------------------------------------- | -| `onIssue` | When any action is performed on an issue. | -| `onIssueOpened` | When an issue is opened. | -| `onIssueAssigned` | When an issue is assigned. | -| `onIssueComment` | When an issue is commented on. | -| `onStar` | When a repo is starred or unstarred. | -| `onNewStar` | When a repo is starred. | -| `onNewRepository` | When a new repo is created. | -| `onNewBranchOrTag` | When a new branch or tag is created. | -| `onNewBranch` | When a new branch is created. | -| `onPush` | When a push is made to a repo. | -| `onPullRequest` | When activity occurs on a pull request (excluding reviews, issues, or comments). | -| `onPullRequestReview` | When a pull request review has activity. | +--- -## Usage +### Repo + +Repo triggers subscribe to a change in a GitHub repo. ```ts -import { Github, events } from "@trigger.dev/github"; - -const github = new Github({ - id: "github", - token: process.env.GITHUB_TOKEN!, +github.triggers.repo({ + event: events.onIssueOpened, + owner: "triggerdotdev", + repo: "trigger.dev", }); +``` + + + + The event to trigger the job on. + + + The owner of the repo. + + + The name of the repo. + + + + +### Org + +Org triggers subscribe to a change across an entire GitHub org. + +```ts +github.triggers.repo({ + event: events.onIssueOpened, + owner: "triggerdotdev", +}); +``` + +}) + + + + + The event to trigger the job on. + + + The owner of the repo. + + + + +## Events + +### `onIssue` + +When any action is performed on an issue. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues). + +```ts usage.ts client.defineJob({ - id: "github-integration-on-issue", - name: "GitHub Integration - On Issue", + id: "", + name: "", version: "0.1.0", trigger: github.triggers.repo({ event: events.onIssue, - owner: "triggerdotdev", - repo: "empty", + owner: "", + repo: "", }), run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - //do stuff + // Add tasks here }, }); ``` + + +````json +{ + "issue": { + "id": 1754473379, + "url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21", + "body": "This is a *big* problem:\r\n\r\n```\r\nconst foo = \"bar\"\r\n```", + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "state": "open", + "title": "This is a sample issue title #20", + "labels": [], + "locked": false, + "number": 21, + "node_id": "I_kwDOI-yZFc5okyOj", + "assignee": null, + "comments": 0, + "html_url": "https://github.com/ericallam/basic-starter-12k/issues/21", + "assignees": [], + "closed_at": null, + "milestone": null, + "reactions": { + "+1": 0, + "-1": 0, + "url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/reactions", + "eyes": 0, + "heart": 0, + "laugh": 0, + "hooray": 0, + "rocket": 0, + "confused": 0, + "total_count": 0 + }, + "created_at": "2023-06-13T09:42:02Z", + "events_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/events", + "labels_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/labels{/name}", + "updated_at": "2023-06-13T09:42:02Z", + "comments_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/comments", + "state_reason": null, + "timeline_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/timeline", + "repository_url": "https://api.github.com/repos/ericallam/basic-starter-12k", + "active_lock_reason": null, + "author_association": "NONE", + "performed_via_github_app": null + }, + "action": "opened", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 602708245, + "url": "https://api.github.com/repos/ericallam/basic-starter-12k", + "fork": false, + "name": "basic-starter-12k", + "size": 0, + "forks": 0, + "owner": { + "id": 534, + "url": "https://api.github.com/users/ericallam", + "type": "User", + "login": "ericallam", + "node_id": "MDQ6VXNlcjUzNA==", + "html_url": "https://github.com/ericallam", + "gists_url": "https://api.github.com/users/ericallam/gists{/gist_id}", + "repos_url": "https://api.github.com/users/ericallam/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/534?v=4", + "events_url": "https://api.github.com/users/ericallam/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/ericallam/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/ericallam/followers", + "following_url": "https://api.github.com/users/ericallam/following{/other_user}", + "organizations_url": "https://api.github.com/users/ericallam/orgs", + "subscriptions_url": "https://api.github.com/users/ericallam/subscriptions", + "received_events_url": "https://api.github.com/users/ericallam/received_events" + }, + "topics": [], + "git_url": "git://github.com/ericallam/basic-starter-12k.git", + "license": null, + "node_id": "R_kgDOI-yZFQ", + "private": false, + "ssh_url": "git@github.com:ericallam/basic-starter-12k.git", + "svn_url": "https://github.com/ericallam/basic-starter-12k", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/ericallam/basic-starter-12k", + "keys_url": "https://api.github.com/repos/ericallam/basic-starter-12k/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/ericallam/basic-starter-12k/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/blobs{/sha}", + "clone_url": "https://github.com/ericallam/basic-starter-12k.git", + "forks_url": "https://api.github.com/repos/ericallam/basic-starter-12k/forks", + "full_name": "ericallam/basic-starter-12k", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/ericallam/basic-starter-12k/hooks", + "pulls_url": "https://api.github.com/repos/ericallam/basic-starter-12k/pulls{/number}", + "pushed_at": "2023-02-16T19:25:20Z", + "teams_url": "https://api.github.com/repos/ericallam/basic-starter-12k/teams", + "trees_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/trees{/sha}", + "created_at": "2023-02-16T19:25:19Z", + "events_url": "https://api.github.com/repos/ericallam/basic-starter-12k/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues{/number}", + "labels_url": "https://api.github.com/repos/ericallam/basic-starter-12k/labels{/name}", + "merges_url": "https://api.github.com/repos/ericallam/basic-starter-12k/merges", + "mirror_url": null, + "updated_at": "2023-02-16T19:25:19Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/ericallam/basic-starter-12k/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/ericallam/basic-starter-12k/commits{/sha}", + "compare_url": "https://api.github.com/repos/ericallam/basic-starter-12k/compare/{base}...{head}", + "description": null, + "forks_count": 0, + "is_template": false, + "open_issues": 21, + "branches_url": "https://api.github.com/repos/ericallam/basic-starter-12k/branches{/branch}", + "comments_url": "https://api.github.com/repos/ericallam/basic-starter-12k/comments{/number}", + "contents_url": "https://api.github.com/repos/ericallam/basic-starter-12k/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/ericallam/basic-starter-12k/releases{/id}", + "statuses_url": "https://api.github.com/repos/ericallam/basic-starter-12k/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/ericallam/basic-starter-12k/assignees{/user}", + "downloads_url": "https://api.github.com/repos/ericallam/basic-starter-12k/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/ericallam/basic-starter-12k/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/ericallam/basic-starter-12k/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/ericallam/basic-starter-12k/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/ericallam/basic-starter-12k/deployments", + "git_commits_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/ericallam/basic-starter-12k/subscribers", + "contributors_url": "https://api.github.com/repos/ericallam/basic-starter-12k/contributors", + "issue_events_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/ericallam/basic-starter-12k/subscription", + "collaborators_url": "https://api.github.com/repos/ericallam/basic-starter-12k/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/ericallam/basic-starter-12k/notifications{?since,all,participating}", + "open_issues_count": 21, + "web_commit_signoff_required": false + } +} +```` + + +### `onIssueOpened` + +Occurs when an issue is opened. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.org({ + event: events.onIssueOpened, + owner: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +````json +{ + "issue": { + "id": 1754473379, + "url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21", + "body": "This is a *big* problem:\r\n\r\n```\r\nconst foo = \"bar\"\r\n```", + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "state": "open", + "title": "This is a sample issue title #20", + "labels": [], + "locked": false, + "number": 21, + "node_id": "I_kwDOI-yZFc5okyOj", + "assignee": null, + "comments": 0, + "html_url": "https://github.com/ericallam/basic-starter-12k/issues/21", + "assignees": [], + "closed_at": null, + "milestone": null, + "reactions": { + "+1": 0, + "-1": 0, + "url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/reactions", + "eyes": 0, + "heart": 0, + "laugh": 0, + "hooray": 0, + "rocket": 0, + "confused": 0, + "total_count": 0 + }, + "created_at": "2023-06-13T09:42:02Z", + "events_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/events", + "labels_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/labels{/name}", + "updated_at": "2023-06-13T09:42:02Z", + "comments_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/comments", + "state_reason": null, + "timeline_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/21/timeline", + "repository_url": "https://api.github.com/repos/ericallam/basic-starter-12k", + "active_lock_reason": null, + "author_association": "NONE", + "performed_via_github_app": null + }, + "action": "opened", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 602708245, + "url": "https://api.github.com/repos/ericallam/basic-starter-12k", + "fork": false, + "name": "basic-starter-12k", + "size": 0, + "forks": 0, + "owner": { + "id": 534, + "url": "https://api.github.com/users/ericallam", + "type": "User", + "login": "ericallam", + "node_id": "MDQ6VXNlcjUzNA==", + "html_url": "https://github.com/ericallam", + "gists_url": "https://api.github.com/users/ericallam/gists{/gist_id}", + "repos_url": "https://api.github.com/users/ericallam/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/534?v=4", + "events_url": "https://api.github.com/users/ericallam/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/ericallam/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/ericallam/followers", + "following_url": "https://api.github.com/users/ericallam/following{/other_user}", + "organizations_url": "https://api.github.com/users/ericallam/orgs", + "subscriptions_url": "https://api.github.com/users/ericallam/subscriptions", + "received_events_url": "https://api.github.com/users/ericallam/received_events" + }, + "topics": [], + "git_url": "git://github.com/ericallam/basic-starter-12k.git", + "license": null, + "node_id": "R_kgDOI-yZFQ", + "private": false, + "ssh_url": "git@github.com:ericallam/basic-starter-12k.git", + "svn_url": "https://github.com/ericallam/basic-starter-12k", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/ericallam/basic-starter-12k", + "keys_url": "https://api.github.com/repos/ericallam/basic-starter-12k/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/ericallam/basic-starter-12k/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/blobs{/sha}", + "clone_url": "https://github.com/ericallam/basic-starter-12k.git", + "forks_url": "https://api.github.com/repos/ericallam/basic-starter-12k/forks", + "full_name": "ericallam/basic-starter-12k", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/ericallam/basic-starter-12k/hooks", + "pulls_url": "https://api.github.com/repos/ericallam/basic-starter-12k/pulls{/number}", + "pushed_at": "2023-02-16T19:25:20Z", + "teams_url": "https://api.github.com/repos/ericallam/basic-starter-12k/teams", + "trees_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/trees{/sha}", + "created_at": "2023-02-16T19:25:19Z", + "events_url": "https://api.github.com/repos/ericallam/basic-starter-12k/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues{/number}", + "labels_url": "https://api.github.com/repos/ericallam/basic-starter-12k/labels{/name}", + "merges_url": "https://api.github.com/repos/ericallam/basic-starter-12k/merges", + "mirror_url": null, + "updated_at": "2023-02-16T19:25:19Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/ericallam/basic-starter-12k/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/ericallam/basic-starter-12k/commits{/sha}", + "compare_url": "https://api.github.com/repos/ericallam/basic-starter-12k/compare/{base}...{head}", + "description": null, + "forks_count": 0, + "is_template": false, + "open_issues": 21, + "branches_url": "https://api.github.com/repos/ericallam/basic-starter-12k/branches{/branch}", + "comments_url": "https://api.github.com/repos/ericallam/basic-starter-12k/comments{/number}", + "contents_url": "https://api.github.com/repos/ericallam/basic-starter-12k/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/ericallam/basic-starter-12k/releases{/id}", + "statuses_url": "https://api.github.com/repos/ericallam/basic-starter-12k/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/ericallam/basic-starter-12k/assignees{/user}", + "downloads_url": "https://api.github.com/repos/ericallam/basic-starter-12k/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/ericallam/basic-starter-12k/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/ericallam/basic-starter-12k/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/ericallam/basic-starter-12k/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/ericallam/basic-starter-12k/deployments", + "git_commits_url": "https://api.github.com/repos/ericallam/basic-starter-12k/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/ericallam/basic-starter-12k/subscribers", + "contributors_url": "https://api.github.com/repos/ericallam/basic-starter-12k/contributors", + "issue_events_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/ericallam/basic-starter-12k/subscription", + "collaborators_url": "https://api.github.com/repos/ericallam/basic-starter-12k/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/ericallam/basic-starter-12k/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/ericallam/basic-starter-12k/notifications{?since,all,participating}", + "open_issues_count": 21, + "web_commit_signoff_required": false + } +} +```` + + +### `onIssueAssigned` + +When an issue is assigned. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onAssigned, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "id": "issue.assigned", + "name": "Issue assigned", + "payload": { + "issue": { + "id": 1767861922, + "url": "https://api.github.com/repos/triggerdotdev/empty/issues/4", + "body": "This is the bod", + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "state": "open", + "title": "Fourth time lucky?", + "labels": [], + "locked": false, + "number": 4, + "node_id": "I_kwDOJyTwbc5pX26i", + "assignee": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM5OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "comments": 1, + "html_url": "https://github.com/triggerdotdev/empty/issues/4", + "assignees": [ + { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + } + ], + "closed_at": null, + "milestone": null, + "reactions": { + "+1": 0, + "-1": 0, + "url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/reactions", + "eyes": 0, + "heart": 0, + "laugh": 0, + "hooray": 0, + "rocket": 0, + "confused": 0, + "total_count": 0 + }, + "created_at": "2023-06-21T15:24:20Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/events", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/labels{/name}", + "updated_at": "2023-06-21T15:36:53Z", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/comments", + "state_reason": null, + "timeline_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/timeline", + "repository_url": "https://api.github.com/repos/triggerdotdev/empty", + "active_lock_reason": null, + "author_association": "MEMBER", + "performed_via_github_app": null + }, + "action": "assigned", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "assignee": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM5OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T14:21:08Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T14:21:08Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + } + } +} +``` + + +### `onIssueComment` + +When an issue is commented on. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onIssueComment, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "payload": { + "issue": { + "id": 1767861922, + "url": "https://api.github.com/repos/triggerdotdev/empty/issues/4", + "body": "This is the bod", + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "state": "open", + "title": "Fourth time lucky?", + "labels": [], + "locked": false, + "number": 4, + "node_id": "I_kwDOJyTwbc5pX26i", + "assignee": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM5OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "comments": 2, + "html_url": "https://github.com/triggerdotdev/empty/issues/4", + "assignees": [ + { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + } + ], + "closed_at": null, + "milestone": null, + "reactions": { + "+1": 0, + "-1": 0, + "url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/reactions", + "eyes": 0, + "heart": 0, + "laugh": 0, + "hooray": 0, + "rocket": 0, + "confused": 0, + "total_count": 0 + }, + "created_at": "2023-06-21T15:24:20Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/events", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/labels{/name}", + "updated_at": "2023-06-21T16:02:28Z", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/comments", + "state_reason": null, + "timeline_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4/timeline", + "repository_url": "https://api.github.com/repos/triggerdotdev/empty", + "active_lock_reason": null, + "author_association": "MEMBER", + "performed_via_github_app": null + }, + "action": "created", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "comment": { + "id": 1601114728, + "url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments/1601114728", + "body": "This is a short comment with short code snippet:\r\n\r\n```\r\nconst rick = \"astley\";\r\n```", + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "node_id": "IC_kwDOJyTwbc5fbxJo", + "html_url": "https://github.com/triggerdotdev/empty/issues/4#issuecomment-1601114728", + "issue_url": "https://api.github.com/repos/triggerdotdev/empty/issues/4", + "reactions": { + "+1": 0, + "-1": 0, + "url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments/1601114728/reactions", + "eyes": 0, + "heart": 0, + "laugh": 0, + "hooray": 0, + "rocket": 0, + "confused": 0, + "total_count": 0 + }, + "created_at": "2023-06-21T16:02:27Z", + "updated_at": "2023-06-21T16:02:27Z", + "author_association": "MEMBER", + "performed_via_github_app": null + }, + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T14:21:08Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T14:21:08Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + } + } +} +``` + + +### `onStar` + +When a repo is starred or unstarred. [Official GitHub docs](https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#list-stargazers). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onStar, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "payload": { + "action": "created", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 1, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T14:21:08Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:22:03Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 1, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 1, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "starred_at": "2023-06-21T17:22:03Z", + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + } + } +} +``` + + +### `onNewStar` + +When a repo is starred. [Official GitHub docs](https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#list-stargazers). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onNewStar, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "payload": { + "action": "created", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 1, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T14:21:08Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:22:03Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 1, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 1, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "starred_at": "2023-06-21T17:22:03Z", + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + } + } +} +``` + + +### `onNewRepository` + +When a new repo is created. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onNewRepository, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + +### `onNewBranchOrTag` + +When a new branch or tag is created. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onNewBranchOrTag, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "ref": "test", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "ref_type": "branch", + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T17:54:25Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "pusher_type": "user", + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + }, + "master_branch": "main" +} +``` + + +### `onNewBranch` + +When a new branch is created. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onNewBranch, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "ref": "test", + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "ref_type": "branch", + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T17:54:25Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "pusher_type": "user", + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + }, + "master_branch": "main" +} +``` + + +### `onPush` + +When a push is made to a repo. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onPush, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "ref": "refs/heads/main", + "after": "1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "before": "7f0b6655803858ac09ae05354a679d6dad03120c", + "forced": false, + "pusher": { + "name": "matt-aitken", + "email": "matt@mattaitken.com" + }, + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "commits": [ + { + "id": "1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "url": "https://github.com/triggerdotdev/empty/commit/1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "added": [], + "author": { + "name": "Matt Aitken", + "email": "matt@mattaitken.com", + "username": "matt-aitken" + }, + "message": "Updated the readme", + "removed": [], + "tree_id": "3a584b2cae2fe34a195e1fe437cc62031ddff447", + "distinct": true, + "modified": ["README.md"], + "committer": { + "name": "Matt Aitken", + "email": "matt@mattaitken.com", + "username": "matt-aitken" + }, + "timestamp": "2023-06-21T19:27:16+01:00" + } + ], + "compare": "https://github.com/triggerdotdev/empty/compare/7f0b66558038...1ca6a91f4a13", + "created": false, + "deleted": false, + "base_ref": null, + "repository": { + "id": 656732269, + "url": "https://github.com/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "name": "triggerdotdev", + "type": "Organization", + "email": "hello@trigger.dev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": 1687372039, + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": 1687357267, + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "stargazers": 0, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 4, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "organization": "triggerdotdev", + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "master_branch": "main", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 4, + "web_commit_signoff_required": false + }, + "head_commit": { + "id": "1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "url": "https://github.com/triggerdotdev/empty/commit/1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "added": [], + "author": { + "name": "Matt Aitken", + "email": "matt@mattaitken.com", + "username": "matt-aitken" + }, + "message": "Updated the readme", + "removed": [], + "tree_id": "3a584b2cae2fe34a195e1fe437cc62031ddff447", + "distinct": true, + "modified": ["README.md"], + "committer": { + "name": "Matt Aitken", + "email": "matt@mattaitken.com", + "username": "matt-aitken" + }, + "timestamp": "2023-06-21T19:27:16+01:00" + }, + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + } +} +``` + + +### `onPullRequest` + +When activity occurs on a pull request (excluding reviews, issues, or comments). [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onPullRequest, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "action": "opened", + "number": 5, + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T18:38:34Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 5, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 5, + "web_commit_signoff_required": false + }, + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + }, + "pull_request": { + "id": 1402223044, + "url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5", + "base": { + "ref": "main", + "sha": "1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "repo": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T18:38:34Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 5, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "allow_auto_merge": false, + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 5, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_squash_merge": true, + "merge_commit_title": "MERGE_MESSAGE", + "allow_update_branch": false, + "merge_commit_message": "PR_TITLE", + "delete_branch_on_merge": false, + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "squash_merge_commit_message": "COMMIT_MESSAGES", + "web_commit_signoff_required": false, + "use_squash_pr_title_as_default": false + }, + "user": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "label": "triggerdotdev:main" + }, + "body": "A bit more added to the readme which could be useful", + "head": { + "ref": "test", + "sha": "073aa42afebca03c4b34361564e3976da5c65d48", + "repo": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T18:38:34Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 5, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "allow_auto_merge": false, + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 5, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_squash_merge": true, + "merge_commit_title": "MERGE_MESSAGE", + "allow_update_branch": false, + "merge_commit_message": "PR_TITLE", + "delete_branch_on_merge": false, + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "squash_merge_commit_message": "COMMIT_MESSAGES", + "web_commit_signoff_required": false, + "use_squash_pr_title_as_default": false + }, + "user": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "label": "triggerdotdev:test" + }, + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "draft": false, + "state": "open", + "title": "Added more to the readme", + "_links": { + "html": { + "href": "https://github.com/triggerdotdev/empty/pull/5" + }, + "self": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5" + }, + "issue": { + "href": "https://api.github.com/repos/triggerdotdev/empty/issues/5" + }, + "commits": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/commits" + }, + "comments": { + "href": "https://api.github.com/repos/triggerdotdev/empty/issues/5/comments" + }, + "statuses": { + "href": "https://api.github.com/repos/triggerdotdev/empty/statuses/073aa42afebca03c4b34361564e3976da5c65d48" + }, + "review_comment": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/comments{/number}" + }, + "review_comments": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/comments" + } + }, + "labels": [], + "locked": false, + "merged": false, + "number": 5, + "commits": 1, + "node_id": "PR_kwDOJyTwbc5TlDnE", + "assignee": null, + "comments": 0, + "diff_url": "https://github.com/triggerdotdev/empty/pull/5.diff", + "html_url": "https://github.com/triggerdotdev/empty/pull/5", + "additions": 2, + "assignees": [], + "closed_at": null, + "deletions": 0, + "issue_url": "https://api.github.com/repos/triggerdotdev/empty/issues/5", + "mergeable": null, + "merged_at": null, + "merged_by": null, + "milestone": null, + "patch_url": "https://github.com/triggerdotdev/empty/pull/5.patch", + "auto_merge": null, + "created_at": "2023-06-21T18:39:13Z", + "rebaseable": null, + "updated_at": "2023-06-21T18:39:13Z", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/commits", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/issues/5/comments", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/073aa42afebca03c4b34361564e3976da5c65d48", + "changed_files": 1, + "mergeable_state": "unknown", + "requested_teams": [], + "review_comments": 0, + "merge_commit_sha": null, + "active_lock_reason": null, + "author_association": "MEMBER", + "review_comment_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/comments{/number}", + "requested_reviewers": [], + "review_comments_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/comments", + "maintainer_can_modify": false + } +} +``` + + +### `onPullRequestReview` + +When a pull request review has activity. [Official GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review). + +```ts usage.ts +client.defineJob({ + id: "", + name: "", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onPullRequestReview, + owner: "", + repo: "", + }), + run: async (payload, io, ctx) => { + // Add tasks here + }, +}); +``` + + +```json +{ + "action": "submitted", + "review": { + "id": 1491475123, + "body": "This needs some work still", + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "state": "commented", + "_links": { + "html": { + "href": "https://github.com/triggerdotdev/empty/pull/5#pullrequestreview-1491475123" + }, + "pull_request": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5" + } + }, + "node_id": "PRR_kwDOJyTwbc5Y5hqz", + "html_url": "https://github.com/triggerdotdev/empty/pull/5#pullrequestreview-1491475123", + "commit_id": "073aa42afebca03c4b34361564e3976da5c65d48", + "submitted_at": "2023-06-21T18:47:47Z", + "pull_request_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5", + "author_association": "MEMBER" + }, + "sender": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "repository": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T18:39:13Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 5, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 5, + "web_commit_signoff_required": false + }, + "organization": { + "id": 95297378, + "url": "https://api.github.com/orgs/triggerdotdev", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks", + "repos_url": "https://api.github.com/orgs/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/orgs/triggerdotdev/events", + "issues_url": "https://api.github.com/orgs/triggerdotdev/issues", + "description": "", + "members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}", + "public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}" + }, + "pull_request": { + "id": 1402223044, + "url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5", + "base": { + "ref": "main", + "sha": "1ca6a91f4a1385e448364a5de1960a3da60d3fe2", + "repo": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T18:39:13Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 5, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "allow_auto_merge": false, + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 5, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_squash_merge": true, + "merge_commit_title": "MERGE_MESSAGE", + "allow_update_branch": false, + "merge_commit_message": "PR_TITLE", + "delete_branch_on_merge": false, + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "squash_merge_commit_message": "COMMIT_MESSAGES", + "web_commit_signoff_required": false, + "use_squash_pr_title_as_default": false + }, + "user": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "label": "triggerdotdev:main" + }, + "body": "A bit more added to the readme which could be useful", + "head": { + "ref": "test", + "sha": "073aa42afebca03c4b34361564e3976da5c65d48", + "repo": { + "id": 656732269, + "url": "https://api.github.com/repos/triggerdotdev/empty", + "fork": false, + "name": "empty", + "size": 0, + "forks": 0, + "owner": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "topics": [], + "git_url": "git://github.com/triggerdotdev/empty.git", + "license": null, + "node_id": "R_kgDOJyTwbQ", + "private": false, + "ssh_url": "git@github.com:triggerdotdev/empty.git", + "svn_url": "https://github.com/triggerdotdev/empty", + "archived": false, + "disabled": false, + "has_wiki": true, + "homepage": null, + "html_url": "https://github.com/triggerdotdev/empty", + "keys_url": "https://api.github.com/repos/triggerdotdev/empty/keys{/key_id}", + "language": null, + "tags_url": "https://api.github.com/repos/triggerdotdev/empty/tags", + "watchers": 0, + "blobs_url": "https://api.github.com/repos/triggerdotdev/empty/git/blobs{/sha}", + "clone_url": "https://github.com/triggerdotdev/empty.git", + "forks_url": "https://api.github.com/repos/triggerdotdev/empty/forks", + "full_name": "triggerdotdev/empty", + "has_pages": false, + "hooks_url": "https://api.github.com/repos/triggerdotdev/empty/hooks", + "pulls_url": "https://api.github.com/repos/triggerdotdev/empty/pulls{/number}", + "pushed_at": "2023-06-21T18:39:13Z", + "teams_url": "https://api.github.com/repos/triggerdotdev/empty/teams", + "trees_url": "https://api.github.com/repos/triggerdotdev/empty/git/trees{/sha}", + "created_at": "2023-06-21T14:21:07Z", + "events_url": "https://api.github.com/repos/triggerdotdev/empty/events", + "has_issues": true, + "issues_url": "https://api.github.com/repos/triggerdotdev/empty/issues{/number}", + "labels_url": "https://api.github.com/repos/triggerdotdev/empty/labels{/name}", + "merges_url": "https://api.github.com/repos/triggerdotdev/empty/merges", + "mirror_url": null, + "updated_at": "2023-06-21T17:23:26Z", + "visibility": "public", + "archive_url": "https://api.github.com/repos/triggerdotdev/empty/{archive_format}{/ref}", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/commits{/sha}", + "compare_url": "https://api.github.com/repos/triggerdotdev/empty/compare/{base}...{head}", + "description": "An empty repo that can be used to test the @trigger.dev/github integration", + "forks_count": 0, + "is_template": false, + "open_issues": 5, + "branches_url": "https://api.github.com/repos/triggerdotdev/empty/branches{/branch}", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/comments{/number}", + "contents_url": "https://api.github.com/repos/triggerdotdev/empty/contents/{+path}", + "git_refs_url": "https://api.github.com/repos/triggerdotdev/empty/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/triggerdotdev/empty/git/tags{/sha}", + "has_projects": true, + "releases_url": "https://api.github.com/repos/triggerdotdev/empty/releases{/id}", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/{sha}", + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/triggerdotdev/empty/assignees{/user}", + "downloads_url": "https://api.github.com/repos/triggerdotdev/empty/downloads", + "has_downloads": true, + "languages_url": "https://api.github.com/repos/triggerdotdev/empty/languages", + "default_branch": "main", + "milestones_url": "https://api.github.com/repos/triggerdotdev/empty/milestones{/number}", + "stargazers_url": "https://api.github.com/repos/triggerdotdev/empty/stargazers", + "watchers_count": 0, + "deployments_url": "https://api.github.com/repos/triggerdotdev/empty/deployments", + "git_commits_url": "https://api.github.com/repos/triggerdotdev/empty/git/commits{/sha}", + "has_discussions": false, + "subscribers_url": "https://api.github.com/repos/triggerdotdev/empty/subscribers", + "allow_auto_merge": false, + "contributors_url": "https://api.github.com/repos/triggerdotdev/empty/contributors", + "issue_events_url": "https://api.github.com/repos/triggerdotdev/empty/issues/events{/number}", + "stargazers_count": 0, + "subscription_url": "https://api.github.com/repos/triggerdotdev/empty/subscription", + "collaborators_url": "https://api.github.com/repos/triggerdotdev/empty/collaborators{/collaborator}", + "issue_comment_url": "https://api.github.com/repos/triggerdotdev/empty/issues/comments{/number}", + "notifications_url": "https://api.github.com/repos/triggerdotdev/empty/notifications{?since,all,participating}", + "open_issues_count": 5, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_squash_merge": true, + "merge_commit_title": "MERGE_MESSAGE", + "allow_update_branch": false, + "merge_commit_message": "PR_TITLE", + "delete_branch_on_merge": false, + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "squash_merge_commit_message": "COMMIT_MESSAGES", + "web_commit_signoff_required": false, + "use_squash_pr_title_as_default": false + }, + "user": { + "id": 95297378, + "url": "https://api.github.com/users/triggerdotdev", + "type": "Organization", + "login": "triggerdotdev", + "node_id": "O_kgDOBa4fYg", + "html_url": "https://github.com/triggerdotdev", + "gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}", + "repos_url": "https://api.github.com/users/triggerdotdev/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4", + "events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/triggerdotdev/followers", + "following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}", + "organizations_url": "https://api.github.com/users/triggerdotdev/orgs", + "subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions", + "received_events_url": "https://api.github.com/users/triggerdotdev/received_events" + }, + "label": "triggerdotdev:test" + }, + "user": { + "id": 10635986, + "url": "https://api.github.com/users/matt-aitken", + "type": "User", + "login": "matt-aitken", + "node_id": "MDQ6VXNlcjEwNjM1OTg2", + "html_url": "https://github.com/matt-aitken", + "gists_url": "https://api.github.com/users/matt-aitken/gists{/gist_id}", + "repos_url": "https://api.github.com/users/matt-aitken/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/10635986?v=4", + "events_url": "https://api.github.com/users/matt-aitken/events{/privacy}", + "site_admin": false, + "gravatar_id": "", + "starred_url": "https://api.github.com/users/matt-aitken/starred{/owner}{/repo}", + "followers_url": "https://api.github.com/users/matt-aitken/followers", + "following_url": "https://api.github.com/users/matt-aitken/following{/other_user}", + "organizations_url": "https://api.github.com/users/matt-aitken/orgs", + "subscriptions_url": "https://api.github.com/users/matt-aitken/subscriptions", + "received_events_url": "https://api.github.com/users/matt-aitken/received_events" + }, + "draft": false, + "state": "open", + "title": "Added more to the readme", + "_links": { + "html": { + "href": "https://github.com/triggerdotdev/empty/pull/5" + }, + "self": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5" + }, + "issue": { + "href": "https://api.github.com/repos/triggerdotdev/empty/issues/5" + }, + "commits": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/commits" + }, + "comments": { + "href": "https://api.github.com/repos/triggerdotdev/empty/issues/5/comments" + }, + "statuses": { + "href": "https://api.github.com/repos/triggerdotdev/empty/statuses/073aa42afebca03c4b34361564e3976da5c65d48" + }, + "review_comment": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/comments{/number}" + }, + "review_comments": { + "href": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/comments" + } + }, + "labels": [], + "locked": false, + "number": 5, + "node_id": "PR_kwDOJyTwbc5TlDnE", + "assignee": null, + "diff_url": "https://github.com/triggerdotdev/empty/pull/5.diff", + "html_url": "https://github.com/triggerdotdev/empty/pull/5", + "assignees": [], + "closed_at": null, + "issue_url": "https://api.github.com/repos/triggerdotdev/empty/issues/5", + "merged_at": null, + "milestone": null, + "patch_url": "https://github.com/triggerdotdev/empty/pull/5.patch", + "auto_merge": null, + "created_at": "2023-06-21T18:39:13Z", + "updated_at": "2023-06-21T18:47:47Z", + "commits_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/commits", + "comments_url": "https://api.github.com/repos/triggerdotdev/empty/issues/5/comments", + "statuses_url": "https://api.github.com/repos/triggerdotdev/empty/statuses/073aa42afebca03c4b34361564e3976da5c65d48", + "requested_teams": [], + "merge_commit_sha": "f146c57c260778db17300c47c366bce0e2dbd53d", + "active_lock_reason": null, + "author_association": "MEMBER", + "review_comment_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/comments{/number}", + "requested_reviewers": [], + "review_comments_url": "https://api.github.com/repos/triggerdotdev/empty/pulls/5/comments" + } +} +``` + diff --git a/docs/integrations/apis/github.mdx b/docs/integrations/apis/github.mdx index c001d7da1..4b00021be 100644 --- a/docs/integrations/apis/github.mdx +++ b/docs/integrations/apis/github.mdx @@ -1,11 +1,21 @@ --- -title: "GitHub: Introduction" -sidebarTitle: "Introduction" +title: GitHub overview & authentication +sidebarTitle: Overview & authentication --- - +## Overview -## Installation +Our GitHub integration allows you to create triggers and tasks that interact with GitHub. For examples of some of the things you can do with it, check out our Jobs Showcase: + + + Check out pre-built GitHub jobs in our showcase. + + +## Installing the GitHub packages @@ -25,25 +35,41 @@ yarn add @trigger.dev/github@latest ## Authentication -GitHub supports Personal Access Tokens and OAuth. +GitHub supports Personal Access Tokens and OAuth. You can use either of these to authenticate with GitHub. -```ts -import { Github } from "@trigger.dev/github"; +### Personal Access Token + +To create a personal access token on GitHub, login and [follow the instructions](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). Information on the required scopes can be found [here](https://docs.github.com/en/developers/apps/scopes-for-oauth-apps). + +```ts my-job.ts +import { GitHub } from "@trigger.dev/github"; //create GitHub client using a token -const github = new Github({ +const github = new GitHub({ id: "github", token: process.env.GITHUB_TOKEN!, }); +... +``` + +### OAuth + +To use OAuth you can connect to GitHub via the Trigger.dev [web app](https://cloud.trigger.dev). Click 'Integrations' in the side panel of any project, and configure GitHub with the ID you want to use in your job and the required [scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps). + +```ts my-job.ts +import { GitHub } from "@trigger.dev/github"; //create GitHub client using OAuth -const github2 = new Github({ - id: "github2", +const github = new GitHub({ + id: "github", }); +... ``` ## Triggers and Tasks +Once you have set up a GitHub client, you can use it to create triggers and tasks. + Trigger Jobs when events happen in GitHub, such as a new commit or a new issue. @@ -52,51 +78,3 @@ const github2 = new Github({ Perform tasks such as creating a new issue or a new comment. - -## Using the underlying client - -You can use the underlying client to do anything Octokit supports. In this example we create a project card when a new issue is opened.. - - - View [the official GitHub docs](https://docs.github.com/en/rest) for everything that is - supported{" "} - - -```ts -import { Github, events } from "@trigger.dev/github"; - -const github = new Github({ - id: "github", - token: process.env.GITHUB_TOKEN!, -}); - -client.defineJob({ - id: "alert-on-new-github-issues", - name: "Alert on new GitHub issues", - version: "0.1.1", - trigger: github.triggers.repo({ - event: events.onIssueOpened, - owner: "triggerdotdev", - repo: "trigger.dev", - }), - integrations: { - github, - }, - run: async (payload, io, ctx) => { - //io.github.runTask allows you to use the underlying SDK client - const { data } = await io.github.runTask( - "create-card", - async (client) => { - return client.rest.projects.createCard({ - column_id: 123, - note: "test", - }); - }, - { name: "Create card" } - ); - - //log the url of the created card - await io.logger.info(data.url); - }, -}); -``` From ca031a72e75877520ef39b72f38cd13c70bff2ea Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:57:28 +0100 Subject: [PATCH 37/79] Add pgAdmin (#533) --- docker/docker-compose.yml | 19 +++++++++++++++++++ docker/pgadmin/servers.json | 13 +++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 docker/pgadmin/servers.json diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 76017f376..6cfd227b2 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -2,6 +2,7 @@ version: "3" volumes: database-data: + pgadmin-data: networks: app_network: @@ -22,3 +23,21 @@ services: - app_network ports: - 5432:5432 + + pgadmin: + container_name: pgadmin + image: dpage/pgadmin4:7 + restart: always + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_DISABLE_POSTFIX: true + volumes: + - pgadmin-data:/var/lib/pgadmin + - ./pgadmin/servers.json:/pgadmin4/servers.json + networks: + - app_network + ports: + - 5480:80 + depends_on: + - database diff --git a/docker/pgadmin/servers.json b/docker/pgadmin/servers.json new file mode 100644 index 000000000..83f7159bb --- /dev/null +++ b/docker/pgadmin/servers.json @@ -0,0 +1,13 @@ +{ + "Servers": { + "1": { + "Name": "Trigger.dev", + "Group": "Trigger.dev", + "Port": 5432, + "Username": "postgres", + "Host": "database", + "SSLMode": "prefer", + "MaintenanceDB": "postgres" + } + } +} From 1c4f9ff1fff895bce62fe7dfb82cc9dd93f8aebd Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 9 Oct 2023 17:14:40 +0100 Subject: [PATCH 38/79] Add pgAdmin instructions to contributing guide --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 471123fd3..231cbdf1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,9 +52,13 @@ branch are tagged into a release monthly. Feel free to update `SESSION_SECRET` and `MAGIC_LINK_SECRET` as well using the same method. 6. Start Docker. This starts the required services like Postgres. If this is your first time using Docker, consider going through this [guide](DOCKER_INSTALLATION.md) + ``` pnpm run docker ``` + + This will also start and run a local instance of [pgAdmin](https://www.pgadmin.org/) on [localhost:5480](http://localhost:5480), preconfigured with email `admin@example.com` and pwd `admin`. Then use `postgres` as the password to the Trigger.dev server. + 7. Migrate the database ``` pnpm run db:migrate From c070e78d06f6e11445930939e3ba54586bbe6ecf Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Mon, 9 Oct 2023 21:45:24 +0530 Subject: [PATCH 39/79] feat: allow CLI update to accept a version for trigger-dev package (#536) * feat: allow CLI update to accept a version for trigger-dev package * remove console log --- .changeset/cool-stingrays-invent.md | 5 +++ packages/cli/src/cli/index.ts | 9 ++-- packages/cli/src/commands/update.ts | 67 +++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 .changeset/cool-stingrays-invent.md diff --git a/.changeset/cool-stingrays-invent.md b/.changeset/cool-stingrays-invent.md new file mode 100644 index 000000000..f283a24fd --- /dev/null +++ b/.changeset/cool-stingrays-invent.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +allow users to update trigger-dev packages to a specific version diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index da8b68590..89829eb4f 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -93,10 +93,13 @@ program program .command("update") - .description("Updates all @trigger.dev/* packages to their latest compatible versions") + .description( + "Updates all @trigger.dev/* packages to their latest compatible versions or the specified version" + ) .argument("[path]", "The path to the directory that contains the package.json file", ".") - .action(async (path) => { - await updateCommand(path); + .option("--to ", "The version to update to (ex: 2.1.4)", "latest") + .action(async (path, options) => { + await updateCommand(path, options); }); program diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index ff6ed559f..e6a81c47d 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -4,8 +4,24 @@ import { run, RunOptions } from "npm-check-updates"; import { installDependencies } from "../utils/installDependencies.js"; import { readJSONFileSync, writeJSONFile } from "../utils/fileSystem.js"; import { logger } from "../utils/logger.js"; +import { z } from "zod"; + +export const UpdateCommandOptionsSchema = z.object({ + to: z.string().optional(), +}); + +export type UpdateCommandOptions = z.infer; + +type NcuRunOptionTarget = "latest" | `@${string}`; + +export async function updateCommand(projectPath: string, anyOptions: any) { + const parseRes = UpdateCommandOptionsSchema.safeParse(anyOptions); + if (!parseRes.success) { + logger.error(parseRes.error.message); + return; + } + const options = parseRes.data; -export async function updateCommand(projectPath: string) { const triggerDevPackage = "@trigger.dev"; const packageJSONPath = path.join(projectPath, "package.json"); const packageData = readJSONFileSync(packageJSONPath); @@ -27,12 +43,14 @@ export async function updateCommand(projectPath: string) { }; }); + const targetVersion = getTargetVersion(options.to); + // Use npm-check-updates to get updated dependency versions const ncuOptions: RunOptions = { packageData, upgrade: true, jsonUpgraded: true, - target: "latest", + target: targetVersion, }; // Can either give a json like package.json or just with deps and their new versions @@ -69,6 +87,39 @@ export async function updateCommand(projectPath: string) { return; } + let applyUpdates = targetVersion !== "latest"; + + if (targetVersion === "latest") { + applyUpdates = await hasUserConfirmed(packagesToUpdate, packageMaps, updatedDependencies); + } + + if (applyUpdates) { + const newPackageJSON = packageData; + packagesToUpdate.forEach((packageName) => { + const tmp = packageMaps[packageName]; + if (tmp) { + newPackageJSON[tmp.type][packageName] = updatedDependencies[packageName]; + } + }); + await writeJSONFile(packageJSONPath, newPackageJSON); + await installDependencies(projectPath); + } +} + +// expects a version number, or latest. +// if version number is specified, prepend it with '@' for ncu. +function getTargetVersion(toVersion?: string): NcuRunOptionTarget { + if (!toVersion) { + return "latest"; + } + return toVersion === "latest" ? "latest" : `@${toVersion}`; +} + +async function hasUserConfirmed( + packagesToUpdate: string[], + packageMaps: { [x: string]: { type: string; version: string } }, + updatedDependencies: { [x: string]: any } +): Promise { // Inform the user of the dependencies that can be updated console.log("\nNewer versions found for the following packages:"); console.table( @@ -86,15 +137,5 @@ export async function updateCommand(projectPath: string) { message: "Do you want to update these packages in package.json and re-install dependencies?", }); - if (confirm) { - const newPackageJSON = packageData; - packagesToUpdate.forEach((packageName) => { - const tmp = packageMaps[packageName]; - if (tmp) { - newPackageJSON[tmp.type][packageName] = updatedDependencies[packageName]; - } - }); - await writeJSONFile(packageJSONPath, newPackageJSON); - await installDependencies(projectPath); - } + return confirm; } From 87ee98aff1004b1b9c03fb2d4c4ca0f05b0e2f1b Mon Sep 17 00:00:00 2001 From: D-K-P Date: Mon, 9 Oct 2023 17:28:49 +0100 Subject: [PATCH 40/79] Added NestJS to the quick starts side menu --- docs/mint.json | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/mint.json b/docs/mint.json index 1d0e4dba8..5abf8aa9e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -76,6 +76,7 @@ "documentation/quickstarts/express", "documentation/quickstarts/remix", "documentation/quickstarts/redwood", + "documentation/quickstarts/nestjs", "documentation/quickstarts/astro", "documentation/quickstarts/nuxt", "documentation/quickstarts/sveltekit", @@ -318,10 +319,7 @@ "sdk/dynamictrigger/constructor", { "group": "Instance methods", - "pages": [ - "sdk/dynamictrigger/register", - "sdk/dynamictrigger/unregister" - ] + "pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"] } ] }, @@ -332,10 +330,7 @@ "sdk/dynamicschedule/constructor", { "group": "Instance methods", - "pages": [ - "sdk/dynamicschedule/register", - "sdk/dynamicschedule/unregister" - ] + "pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"] } ] }, @@ -356,9 +351,7 @@ }, { "group": "Overview", - "pages": [ - "examples/introduction" - ] + "pages": ["examples/introduction"] } ], "footerSocials": { @@ -371,4 +364,4 @@ "apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW" } } -} \ No newline at end of file +} From 97e63d1222f676bff1353c5793580c87d03a1ad1 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:03:08 +0530 Subject: [PATCH 41/79] Event Trigger Example Payload: Fix unsetting icon, and Use fallback icon in case of invalid icon-name. (#576) * fix: unset event-trigger example icon * fix: use fallback icon when an invalid icon is used (example payload page) --- .../route.tsx | 3 ++- apps/webapp/app/services/jobs/registerJob.server.ts | 2 +- apps/webapp/app/utils/icon.ts | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 apps/webapp/app/utils/icon.ts diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx index f3dc9a36f..d109230a3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx @@ -35,6 +35,7 @@ import { TestJobService } from "~/services/jobs/testJob.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { Handle } from "~/utils/handle"; +import { isValidIcon } from "~/utils/icon"; import { JobParamsSchema, jobRunDashboardPath, trimTrailingSlash } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderArgs) => { @@ -228,7 +229,7 @@ export default function Page() { }} > { + if (!icon) { + return false; + } + return namedIcons.includes(icon) || hasIcon(icon); +}; From 8c383bdc6925cb335c354d49ad88861e773de268 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 9 Oct 2023 18:19:06 +0100 Subject: [PATCH 42/79] Add a PR checks github workflow to run typecheck, unit tests, and e2e tests on PRs --- .github/workflows/e2e.yml | 65 ++++++++++++++++++++++++++++++++ .github/workflows/pr_checks.yml | 29 ++++++++++++++ .github/workflows/typecheck.yml | 32 ++++++++++++++++ .github/workflows/unit-tests.yml | 30 +++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 .github/workflows/e2e.yml create mode 100644 .github/workflows/pr_checks.yml create mode 100644 .github/workflows/typecheck.yml create mode 100644 .github/workflows/unit-tests.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..a267eba82 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,65 @@ +name: "🧪 E2E Tests" +on: + workflow_call: +jobs: + e2e: + name: "🧪 E2E Tests" + runs-on: buildjet-4vcpu-ubuntu-2204 + steps: + - name: 🐳 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: ⎔ Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.18 + + - name: ⎔ Setup node + uses: buildjet/setup-node@v3 + with: + node-version: 18 + cache: "pnpm" + + - name: 📥 Download deps + run: pnpm install --frozen-lockfile + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: | + # Setup environment variables + cp ./.env.example ./.env + cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local + + # Build packages + pnpm run build --filter @references/nextjs-test^... + pnpm --filter @trigger.dev/database generate + + # Move trigger-cli bin to correct place + pnpm install --frozen-lockfile + + # Execute tests + pnpm run docker + pnpm run db:migrate + pnpm run db:seed + pnpm run test:e2e + + # Cleanup + pnpm run docker:stop + + - name: Upload Playwright report + uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml new file mode 100644 index 000000000..7ad44f69d --- /dev/null +++ b/.github/workflows/pr_checks.yml @@ -0,0 +1,29 @@ +name: 🤖 PR Checks + +on: + pull_request_target: + branches: + - main + paths-ignore: + - "**.md" + - ".github/CODEOWNERS" + +permissions: + contents: read + id-token: write + +jobs: + type-check: + name: "ʦ TypeScript" + uses: ./.github/workflows/typecheck.yml + secrets: inherit + + unitTests: + name: "🧪 Unit Tests" + uses: ./.github/workflows/unit-tests.yml + secrets: inherit + + e2e: + name: "🧪 E2E Tests" + uses: ./.github/workflows/e2e.yml + secrets: inherit diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 000000000..1d32f6945 --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,32 @@ +name: "ʦ TypeScript" +on: + workflow_call: +jobs: + typecheck: + runs-on: buildjet-4vcpu-ubuntu-2204 + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: ⎔ Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.18 + + - name: ⎔ Setup node + uses: buildjet/setup-node@v3 + with: + node-version: 18 + cache: "pnpm" + + - name: 📥 Download deps + run: pnpm install --frozen-lockfile + + - name: 📀 Generate Prisma Client + run: pnpm run generate + + - name: 🔎 Type check + run: pnpm run typecheck --filter webapp diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 000000000..f90f81fab --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,30 @@ +name: "🧪 Unit Tests" +on: + workflow_call: +jobs: + unitTests: + name: "🧪 Unit Tests" + runs-on: buildjet-4vcpu-ubuntu-2204 + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: ⎔ Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.18 + + - name: ⎔ Setup node + uses: buildjet/setup-node@v3 + with: + node-version: 18 + cache: "pnpm" + + - name: 📥 Download deps + run: pnpm install --frozen-lockfile + + - name: Run Unit Tests + run: | + pnpm run test From c0d5292b0613573928ad3e4d09ad5607c1990935 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 9 Oct 2023 18:22:38 +0100 Subject: [PATCH 43/79] Attempt to fix pgadmin error in tests --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6cfd227b2..41e935745 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -31,7 +31,7 @@ services: environment: PGADMIN_DEFAULT_EMAIL: admin@example.com PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_DISABLE_POSTFIX: true + PGADMIN_DISABLE_POSTFIX: "true" volumes: - pgadmin-data:/var/lib/pgadmin - ./pgadmin/servers.json:/pgadmin4/servers.json From 646bfd37bd47d48860fe8ac1b3d30210d2336e82 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 9 Oct 2023 18:28:49 +0100 Subject: [PATCH 44/79] Pushing a commit to the branch so I can open a PR (#592) * Pushing a commit to the branch so I can open a PR * Couple improvements to the pr checks workflow --- .github/workflows/pr_checks.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 7ad44f69d..6a1cd2acc 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -7,23 +7,25 @@ on: paths-ignore: - "**.md" - ".github/CODEOWNERS" + - ".github/ISSUE_TEMPLATE/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true permissions: contents: read id-token: write jobs: - type-check: - name: "ʦ TypeScript" + typecheck: uses: ./.github/workflows/typecheck.yml secrets: inherit - unitTests: - name: "🧪 Unit Tests" + units: uses: ./.github/workflows/unit-tests.yml secrets: inherit e2e: - name: "🧪 E2E Tests" uses: ./.github/workflows/e2e.yml secrets: inherit From 8c8d1598e377bbf6d22dd0693bad51e864b5c58b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 9 Oct 2023 18:30:55 +0100 Subject: [PATCH 45/79] Reuse common workflow jobs in publish workflow --- .github/workflows/publish.yml | 122 +++------------------------------- 1 file changed, 8 insertions(+), 114 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 74282ef69..3a4f2fb47 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,125 +38,19 @@ env: jobs: typecheck: - name: ʦ TypeScript - runs-on: buildjet-4vcpu-ubuntu-2204 - steps: - - name: ⬇️ Checkout repo - uses: actions/checkout@v3 - with: - fetch-depth: 0 + uses: ./.github/workflows/typecheck.yml + secrets: inherit - - name: ⎔ Setup pnpm - uses: pnpm/action-setup@v2.2.4 - with: - version: 7.18 - - - name: ⎔ Setup node - uses: buildjet/setup-node@v3 - with: - node-version: 18 - cache: "pnpm" - - - name: 📥 Download deps - run: pnpm install --frozen-lockfile - - - name: 📀 Generate Prisma Client - run: pnpm run generate - - - name: 🔎 Type check - run: pnpm run typecheck --filter webapp - - unitTests: - name: Unit Tests - runs-on: buildjet-4vcpu-ubuntu-2204 - steps: - - name: ⬇️ Checkout repo - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: ⎔ Setup pnpm - uses: pnpm/action-setup@v2.2.4 - with: - version: 7.18 - - - name: ⎔ Setup node - uses: buildjet/setup-node@v3 - with: - node-version: 18 - cache: "pnpm" - - - name: 📥 Download deps - run: pnpm install --frozen-lockfile - - - name: Run Unit Tests - run: | - pnpm run test + units: + uses: ./.github/workflows/unit-tests.yml + secrets: inherit e2e: - name: e2e Tests - runs-on: buildjet-4vcpu-ubuntu-2204 - steps: - - name: 🐳 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: ⬇️ Checkout repo - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: ⎔ Setup pnpm - uses: pnpm/action-setup@v2.2.4 - with: - version: 7.18 - - - name: ⎔ Setup node - uses: buildjet/setup-node@v3 - with: - node-version: 18 - cache: "pnpm" - - - name: 📥 Download deps - run: pnpm install --frozen-lockfile - - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - - name: Run Playwright tests - run: | - # Setup environment variables - cp ./.env.example ./.env - cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local - - # Build packages - pnpm run build --filter @references/nextjs-test^... - pnpm --filter @trigger.dev/database generate - - # Move trigger-cli bin to correct place - pnpm install --frozen-lockfile - - # Execute tests - pnpm run docker - pnpm run db:migrate - pnpm run db:seed - pnpm run test:e2e - - # Cleanup - pnpm run docker:stop - - - name: Upload Playwright report - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + uses: ./.github/workflows/e2e.yml + secrets: inherit publish: - needs: [typecheck, unitTests, e2e] + needs: [typecheck, units, e2e] runs-on: buildjet-4vcpu-ubuntu-2204 outputs: version: ${{ steps.get_version.outputs.version }} From d663ba89850321d793caf7232a80cf299878603f Mon Sep 17 00:00:00 2001 From: md <146939900+mohitd404@users.noreply.github.com> Date: Tue, 10 Oct 2023 02:38:32 +0530 Subject: [PATCH 46/79] Fixed #593 Added Contributors Section to readme.md (#594) * Fixed #593 Added Contributors Section to readme.md Fixed #593 Added Contributors Section to readme.md * Update README.md --------- Co-authored-by: Eric Allam --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5f6d3622d..8f813dfb7 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,9 @@ We provide an official trigger.dev docker image you can use to easily self-host ## Development To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md). + +## 🙏 to our contributors + + + + From df4667b7f5cca0f48826a80a21f5fbbcdc37bfde Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Tue, 10 Oct 2023 06:08:53 +0900 Subject: [PATCH 47/79] Fix typo in sse.ts (#591) Uknown -> Unknown --- apps/webapp/app/utils/sse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/utils/sse.ts b/apps/webapp/app/utils/sse.ts index 4588cedc5..105911d7f 100644 --- a/apps/webapp/app/utils/sse.ts +++ b/apps/webapp/app/utils/sse.ts @@ -47,7 +47,7 @@ export function sse({ request, pingInterval = 1000, updateInterval = 348, run }: }); } } else { - logger.debug("Uknown error sending SSE, aborting", { + logger.debug("Unknown error sending SSE, aborting", { error, args, }); From 916ed4e366fd858d9f060c00899cd6e2338c0a54 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Tue, 10 Oct 2023 09:50:43 +0100 Subject: [PATCH 48/79] Updated features table --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f813dfb7..89a5171a4 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ Click the links to join the discussions about our upcoming features. | Dashboard | View every Task in every Run | ✅ | | Serverless | Long-running Jobs on your serverless backend | ✅ | | React hooks | Easily update your UI with Job progress | ✅ | +| React frameworks | Support for Remix, Astro, RedwoodJS & more | ✅ | | [Background tasks](https://github.com/triggerdotdev/trigger.dev/discussions/400) | Offload long or intense Tasks to our infrastructure | 🛠️ | -| [React frameworks](https://github.com/triggerdotdev/trigger.dev/discussions/411) | Support for Remix, Astro, RedwoodJS & more | 🛠️ | | [Long-running servers](https://github.com/triggerdotdev/trigger.dev/discussions/430) | Run Jobs on your long-running backend | 🛠️ | | Polling Triggers | Subscribe to changes without webhooks | 🕝 | | Vercel integration | Easy deploy and preview environment support | 🕝 | From 975c5f1d3c8dd510a91855b8726db765c63f32f8 Mon Sep 17 00:00:00 2001 From: Rutam Prita Mishra Date: Tue, 10 Oct 2023 16:12:02 +0530 Subject: [PATCH 49/79] [TRI-1333] : Upgrade all packages to use Node18 and use fetch instead node-fetch (#581) * [TRI-1333] : Upgrade all packages to use Node18 and use fetch instead of node-fetch * Changeset Added * Update pnpm-lock to compatible pnpm v7.18.1 * Restored pnpm-lock to the last known good point compatible with pnpm v7.18.1 * Update chilled-plants-exercise.md --------- Co-authored-by: Eric Allam --- .changeset/chilled-plants-exercise.md | 14 ++++++++++++++ packages/astro/package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/eslint-plugin/package.json | 2 +- packages/express/package.json | 2 +- packages/integration-kit/package.json | 4 +--- packages/integration-kit/src/file.ts | 4 ++-- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/trigger-sdk/package.json | 4 +--- packages/trigger-sdk/src/apiClient.ts | 1 - pnpm-lock.yaml | 21 --------------------- 14 files changed, 26 insertions(+), 38 deletions(-) create mode 100644 .changeset/chilled-plants-exercise.md diff --git a/.changeset/chilled-plants-exercise.md b/.changeset/chilled-plants-exercise.md new file mode 100644 index 000000000..dd39f4537 --- /dev/null +++ b/.changeset/chilled-plants-exercise.md @@ -0,0 +1,14 @@ +--- +"@trigger.dev/integration-kit": minor +"@trigger.dev/eslint-plugin": minor +"@trigger.dev/sdk": minor +"@trigger.dev/sveltekit": minor +"@trigger.dev/express": minor +"@trigger.dev/nestjs": minor +"@trigger.dev/nextjs": minor +"@trigger.dev/astro": minor +"@trigger.dev/core": minor +"@trigger.dev/cli": minor +--- + +Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. diff --git a/packages/astro/package.json b/packages/astro/package.json index e8d60d4cb..9f3066cd7 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -33,7 +33,7 @@ "typescript": "^4.8.4" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" }, "license": "MIT", "publishConfig": { diff --git a/packages/cli/package.json b/packages/cli/package.json index b31713fb8..0a65964f1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -84,6 +84,6 @@ "zod": "3.22.3" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } diff --git a/packages/core/package.json b/packages/core/package.json index 0e2815c42..16ab2720f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,6 @@ "typescript": "^4.9.4" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } \ No newline at end of file diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index d75dc2b9b..f500bad43 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -29,7 +29,7 @@ "npm-run-all": "^4.1.5" }, "engines": { - "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" + "node": ">=18.0.0" }, "peerDependencies": { "eslint": ">=7" diff --git a/packages/express/package.json b/packages/express/package.json index c98a40c7b..fcdf500da 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -41,6 +41,6 @@ "express": "^4.18.2" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } \ No newline at end of file diff --git a/packages/integration-kit/package.json b/packages/integration-kit/package.json index 0c1a43307..dfd954b40 100644 --- a/packages/integration-kit/package.json +++ b/packages/integration-kit/package.json @@ -21,7 +21,6 @@ "devDependencies": { "@trigger.dev/tsconfig": "workspace:*", "@types/node": "18", - "@types/node-fetch": "2.6.x", "@types/uuid": "^9.0.0", "rimraf": "^3.0.2", "tsup": "^6.5.0", @@ -35,10 +34,9 @@ "typecheck": "tsup --dts-resolve --no-dts" }, "dependencies": { - "node-fetch": "2.6.x", "uuid": "^9.0.0" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } diff --git a/packages/integration-kit/src/file.ts b/packages/integration-kit/src/file.ts index f5ae9f6df..216547e2c 100644 --- a/packages/integration-kit/src/file.ts +++ b/packages/integration-kit/src/file.ts @@ -1,7 +1,6 @@ import fs, { promises } from "fs"; import path from "path"; import { v4 as uuidv4 } from "uuid"; -import fetch from "node-fetch"; export async function fileFromString(contents: string | Buffer, fileName: string): Promise { const directory = path.join("tmp", uuidv4()); @@ -13,7 +12,8 @@ export async function fileFromString(contents: string | Buffer, fileName: string export async function fileFromUrl(url: string) { const response = await fetch(url); - const content = await response.buffer(); + const arrayBuffer = await response.arrayBuffer(); + const content = Buffer.from(arrayBuffer); const fileName = path.basename(url); return fileFromString(content, fileName); diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index e837d88b3..08378889f 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -43,6 +43,6 @@ "debug": "^4.3.4" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index d19ade712..a52f1b9ba 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -41,6 +41,6 @@ "debug": "^4.3.4" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index ed6490b75..6e14e5ae2 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -39,6 +39,6 @@ "debug": "^4.3.4" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index a66fd17ba..f09498891 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -33,7 +33,6 @@ "get-caller-file": "^2.0.5", "git-remote-origin-url": "^4.0.0", "git-repo-info": "^2.1.1", - "node-fetch": "2.6.x", "slug": "^6.0.0", "terminal-link": "^3.0.0", "ulid": "^2.3.0", @@ -45,7 +44,6 @@ "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/node": "18", - "@types/node-fetch": "2.6.x", "@types/slug": "^5.0.3", "@types/uuid": "^9.0.0", "@types/ws": "^8.5.3", @@ -56,6 +54,6 @@ "typescript": "^4.8.4" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } } \ No newline at end of file diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index ad003b10a..2f0efc2e7 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -31,7 +31,6 @@ import { API_VERSIONS, } from "@trigger.dev/core"; -import fetch, { type RequestInit } from "node-fetch"; import { z } from "zod"; export type ApiClientOptions = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4287788db..17000ef6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -867,21 +867,17 @@ importers: specifiers: '@trigger.dev/tsconfig': workspace:* '@types/node': '18' - '@types/node-fetch': 2.6.x '@types/uuid': ^9.0.0 - node-fetch: 2.6.x rimraf: ^3.0.2 tsup: ^6.5.0 tsx: ^3.12.1 typescript: ^4.8.4 uuid: ^9.0.0 dependencies: - node-fetch: 2.6.12 uuid: 9.0.0 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 18.15.13 - '@types/node-fetch': 2.6.2 '@types/uuid': 9.0.0 rimraf: 3.0.2 tsup: 6.6.3_typescript@4.9.5 @@ -1040,7 +1036,6 @@ importers: '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/node': '18' - '@types/node-fetch': 2.6.x '@types/slug': ^5.0.3 '@types/uuid': ^9.0.0 '@types/ws': ^8.5.3 @@ -1052,7 +1047,6 @@ importers: get-caller-file: ^2.0.5 git-remote-origin-url: ^4.0.0 git-repo-info: ^2.1.1 - node-fetch: 2.6.x rimraf: ^3.0.2 slug: ^6.0.0 terminal-link: ^3.0.0 @@ -1072,7 +1066,6 @@ importers: get-caller-file: 2.0.5 git-remote-origin-url: 4.0.0 git-repo-info: 2.1.1 - node-fetch: 2.6.12_encoding@0.1.13 slug: 6.1.0 terminal-link: 3.0.0 ulid: 2.3.0 @@ -1083,7 +1076,6 @@ importers: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/debug': 4.1.7 '@types/node': 18.14.0 - '@types/node-fetch': 2.6.4 '@types/slug': 5.0.3 '@types/uuid': 9.0.0 '@types/ws': 8.5.4 @@ -25073,19 +25065,6 @@ packages: dependencies: whatwg-url: 5.0.0 - /node-fetch/2.6.12_encoding@0.1.13: - resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - encoding: 0.1.13 - whatwg-url: 5.0.0 - dev: false - /node-fetch/2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} From ad71964d30a64610b13a9c1344a9eaab4d549932 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 11:53:42 +0100 Subject: [PATCH 50/79] Upgrade local development Node to v18.18.0 using nvm --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 95c758cad..b714151ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.12.1 \ No newline at end of file +v18.18.0 \ No newline at end of file From 476e2f035f4dd7d505ca02f23ce6b86783bff6f2 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 12:02:01 +0100 Subject: [PATCH 51/79] =?UTF-8?q?Don=E2=80=99t=20typecheck=20the=20workspa?= =?UTF-8?q?ce=20packages=20inside=20the=20webapp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/webapp/package.json | 2 +- apps/webapp/tsconfig.check.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 apps/webapp/tsconfig.check.json diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 5c1be72f7..a9cc3baaa 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -13,7 +13,7 @@ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", "start": "cross-env NODE_ENV=production node --max-old-space-size=8192 ./build/server.js", "start:local": "cross-env node --max-old-space-size=8192 ./build/server.js", - "typecheck": "tsc --noEmit", + "typecheck": "tsc -p ./tsconfig.check.json", "db:seed": "node prisma/seed.js", "db:seed:local": "ts-node prisma/seed.ts", "generate:sourcemaps": "remix build --sourcemap", diff --git a/apps/webapp/tsconfig.check.json b/apps/webapp/tsconfig.check.json new file mode 100644 index 000000000..f1adffe51 --- /dev/null +++ b/apps/webapp/tsconfig.check.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "paths": { + "~/*": ["./app/*"], + "@/*": ["./*"] + } + } +} From d37576f8140b8c19d964059f407a9d720c658a21 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 15:03:30 +0100 Subject: [PATCH 52/79] Update release.yml --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 212fa70d1..34760d90c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,10 @@ on: - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - "turbo.json" + paths-ignore: + - "**.md" + - ".github/CODEOWNERS" + - ".github/ISSUE_TEMPLATE/**" jobs: release: From 624072c23baedc80a0d5163b11872fda0229180e Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 15:04:13 +0100 Subject: [PATCH 53/79] Update release.yml --- .github/workflows/release.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34760d90c..cb954be9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,20 +4,11 @@ on: push: branches: - main - paths: - - ".github/workflows/release.yml" - - "packages/**" - - "!packages/**/*.md" - - ".changeset/**" - - "integrations/**" - - "!integrations/**/*.md" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - - "turbo.json" paths-ignore: - "**.md" - ".github/CODEOWNERS" - ".github/ISSUE_TEMPLATE/**" + jobs: release: From 773765f82dd5d98dd61d718e8633f89a9a025a37 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 15:47:53 +0100 Subject: [PATCH 54/79] Cleanup finished graphile_worker.jobs last run more than 7 days ago --- apps/webapp/app/platform/zodWorker.server.ts | 74 ++++++++++++++++++- .../app/routes/api.v1.runs.$runId.tasks.ts | 2 +- apps/webapp/app/services/logger.server.ts | 7 ++ apps/webapp/app/services/worker.server.ts | 10 ++- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index ea3cafe4b..d73df1627 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -14,7 +14,7 @@ import { run as graphileRun, parseCronItems } from "graphile-worker"; import omit from "lodash.omit"; import { z } from "zod"; import { PrismaClient, PrismaClientOrTransaction } from "~/db.server"; -import { logger } from "~/services/logger.server"; +import { workerLogger as logger } from "~/services/logger.server"; export interface MessageCatalogSchema { [key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion; @@ -81,6 +81,14 @@ export type ZodWorkerDequeueOptions = { tx?: PrismaClientOrTransaction; }; +const CLEANUP_TASK_NAME = "__cleanupOldJobs"; + +export type ZodWorkerCleanupOptions = { + frequencyExpression: string; // cron expression + ttl: number; + taskOptions?: CronItemOptions; +}; + export type ZodWorkerOptions = { name: string; runnerOptions: RunnerOptions; @@ -88,6 +96,7 @@ export type ZodWorkerOptions = { schema: TMessageCatalog; tasks: ZodTasks; recurringTasks?: ZodRecurringTasks; + cleanup?: ZodWorkerCleanupOptions; }; export class ZodWorker { @@ -98,6 +107,7 @@ export class ZodWorker { #tasks: ZodTasks; #recurringTasks?: ZodRecurringTasks; #runner?: GraphileRunner; + #cleanup: ZodWorkerCleanupOptions | undefined; constructor(options: ZodWorkerOptions) { this.#name = options.name; @@ -106,6 +116,7 @@ export class ZodWorker { this.#runnerOptions = options.runnerOptions; this.#tasks = options.tasks; this.#recurringTasks = options.recurringTasks; + this.#cleanup = options.cleanup; } get graphileWorkerSchema() { @@ -337,12 +348,29 @@ export class ZodWorker { taskList[key] = task; } + if (this.#cleanup) { + const task: Task = (payload, helpers) => { + return this.#handleCleanup(payload, helpers); + }; + + taskList[CLEANUP_TASK_NAME] = task; + } + return taskList; } #createCronItemsFromRecurringTasks() { const cronItems: CronItem[] = []; + if (this.#cleanup) { + cronItems.push({ + pattern: this.#cleanup.frequencyExpression, + identifier: CLEANUP_TASK_NAME, + task: CLEANUP_TASK_NAME, + options: this.#cleanup.taskOptions, + }); + } + if (!this.#recurringTasks) { return cronItems; } @@ -434,6 +462,50 @@ export class ZodWorker { } } + async #handleCleanup(rawPayload: unknown, helpers: JobHelpers): Promise { + if (!this.#cleanup) { + return; + } + + const job = helpers.job; + + logger.debug("Received cleanup task", { + payload: rawPayload, + job, + }); + + const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload); + + if (!parsedPayload.success) { + throw new Error( + `Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}` + ); + } + + const payload = parsedPayload.data; + + // Add the this.#cleanup.ttl to the payload._cron.ts + const expirationDate = new Date(payload._cron.ts.getTime() - this.#cleanup.ttl); + + logger.debug("Cleaning up old jobs", { + expirationDate, + payload, + }); + + const rawResults = await this.#prisma.$queryRawUnsafe( + `DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts RETURNING id`, + expirationDate + ); + + const results = Array.isArray(rawResults) ? rawResults : []; + + logger.debug("Cleaned up old jobs", { + count: results.length, + expirationDate, + payload, + }); + } + #logDebug(message: string, args?: any) { logger.debug(`[worker][${this.#name}] ${message}`, args); } diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 1e4dc7736..871a48d92 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -305,7 +305,7 @@ export class RunTaskService { { id: task.id, }, - { tx, runAt: task.delayUntil ?? undefined } + { tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` } ); } else if (task.status === "WAITING" && callbackUrl && taskBody.callback) { if (taskBody.callback.timeoutInSeconds > 0) { diff --git a/apps/webapp/app/services/logger.server.ts b/apps/webapp/app/services/logger.server.ts index fdc886479..75bb91ca1 100644 --- a/apps/webapp/app/services/logger.server.ts +++ b/apps/webapp/app/services/logger.server.ts @@ -8,3 +8,10 @@ export const logger = new Logger( ["examples", "output", "connectionString", "payload"], sensitiveDataReplacer ); + +export const workerLogger = new Logger( + "worker", + (process.env.APP_LOG_LEVEL ?? "debug") as LogLevel, + ["examples", "output", "connectionString"], + sensitiveDataReplacer +); diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 07493aab5..9cc428b75 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -128,6 +128,12 @@ function getWorkerQueue() { return new ZodWorker({ name: "workerQueue", prisma, + cleanup: { + // cleanup once per hour + frequencyExpression: "0 * * * *", + // delete jobs that have been completed for more than 7 days + ttl: 7 * 24 * 60 * 60 * 1000, + }, runnerOptions: { connectionString: env.DATABASE_URL, concurrency: env.WORKER_CONCURRENCY, @@ -229,6 +235,7 @@ function getWorkerQueue() { deliverHttpSourceRequest: { priority: 1, // smaller number = higher priority maxAttempts: 14, + queueName: (payload) => `sources:${payload.id}`, handler: async (payload, job) => { const service = new DeliverHttpSourceRequestService(); @@ -255,7 +262,6 @@ function getWorkerQueue() { }, performTaskOperation: { priority: 0, // smaller number = higher priority - queueName: (payload) => `tasks:${payload.id}`, maxAttempts: 3, handler: async (payload, job) => { const service = new PerformTaskOperationService(); @@ -264,7 +270,6 @@ function getWorkerQueue() { }, }, scheduleEmail: { - queueName: "internal-queue", priority: 100, maxAttempts: 3, handler: async (payload, job) => { @@ -291,7 +296,6 @@ function getWorkerQueue() { }, refreshOAuthToken: { priority: 8, // smaller number = higher priority - queueName: "internal-queue", maxAttempts: 7, handler: async (payload, job) => { await integrationAuthRepository.refreshConnection({ From f54a9ff5fc98c6a02b61b54d485603de65630645 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 16:12:30 +0100 Subject: [PATCH 55/79] Fixed broken link in runTask description --- docs/sdk/io/runtask.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdk/io/runtask.mdx b/docs/sdk/io/runtask.mdx index dfd9e1ead..eca598386 100644 --- a/docs/sdk/io/runtask.mdx +++ b/docs/sdk/io/runtask.mdx @@ -1,10 +1,10 @@ --- title: "io.runTask()" sidebarTitle: "runTask()" -description: "`io.runTask()` allows you to run a [Task](/documentation/concepts/tasks) from inside a Job run." +description: "Creates and runs a Task inside a Run." --- -A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. +A [Task](/documentation/concepts/tasks) is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. The wrappers at `io.integration.runTask()` expose the underlying Integration client as the first callback parameter (see examples on the right). They will have defaults set for options and `onError` handlers, but should otherwise be considered identical to raw `io.runTask()`. From b737f7af921501838ff2f23382e41135e28450b1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 23:06:07 +0100 Subject: [PATCH 56/79] Report worker metrics when WORKER_REPORTER_EMAIL env var is set --- apps/webapp/app/env.server.ts | 1 + apps/webapp/app/platform/zodWorker.server.ts | 85 +++++++++++++++++++- apps/webapp/app/services/email.server.ts | 6 +- apps/webapp/app/services/worker.server.ts | 21 +++-- packages/emails/src/index.tsx | 16 ++++ 5 files changed, 121 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 46f3f9e81..c59e436ec 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({ EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), WORKER_ENABLED: z.string().default("true"), EXECUTION_WORKER_ENABLED: z.string().default("true"), + WORKER_REPORTER_EMAIL: z.string().email().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index d73df1627..6adac7bda 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -82,10 +82,12 @@ export type ZodWorkerDequeueOptions = { }; const CLEANUP_TASK_NAME = "__cleanupOldJobs"; +const REPORTER_TASK_NAME = "__reporter"; export type ZodWorkerCleanupOptions = { frequencyExpression: string; // cron expression ttl: number; + maxCount: number; taskOptions?: CronItemOptions; }; @@ -97,6 +99,7 @@ export type ZodWorkerOptions = { tasks: ZodTasks; recurringTasks?: ZodRecurringTasks; cleanup?: ZodWorkerCleanupOptions; + reporter?: (subject: string, message: string) => Promise; }; export class ZodWorker { @@ -108,6 +111,7 @@ export class ZodWorker { #recurringTasks?: ZodRecurringTasks; #runner?: GraphileRunner; #cleanup: ZodWorkerCleanupOptions | undefined; + #reporter?: (subject: string, message: string) => Promise; constructor(options: ZodWorkerOptions) { this.#name = options.name; @@ -117,6 +121,7 @@ export class ZodWorker { this.#tasks = options.tasks; this.#recurringTasks = options.recurringTasks; this.#cleanup = options.cleanup; + this.#reporter = options.reporter; } get graphileWorkerSchema() { @@ -356,6 +361,14 @@ export class ZodWorker { taskList[CLEANUP_TASK_NAME] = task; } + if (this.#reporter) { + const task: Task = (payload, helpers) => { + return this.#handleReporter(payload, helpers); + }; + + taskList[REPORTER_TASK_NAME] = task; + } + return taskList; } @@ -371,6 +384,14 @@ export class ZodWorker { }); } + if (this.#reporter) { + cronItems.push({ + pattern: "50 * * * *", // Every hour at 50 minutes past the hour + identifier: REPORTER_TASK_NAME, + task: REPORTER_TASK_NAME, + }); + } + if (!this.#recurringTasks) { return cronItems; } @@ -493,8 +514,9 @@ export class ZodWorker { }); const rawResults = await this.#prisma.$queryRawUnsafe( - `DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts RETURNING id`, - expirationDate + `WITH rows AS (SELECT id FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts ORDER BY run_at ASC LIMIT $2) DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE id IN (SELECT id FROM rows) RETURNING id`, + expirationDate, + this.#cleanup.maxCount ); const results = Array.isArray(rawResults) ? rawResults : []; @@ -504,6 +526,65 @@ export class ZodWorker { expirationDate, payload, }); + + if (this.#reporter) { + await this.#reporter( + "Worker Queue Cleanup", + `Cleaned up ${results.length} jobs older than ${expirationDate.toISOString()}` + ); + } + } + + async #handleReporter(rawPayload: unknown, helpers: JobHelpers): Promise { + if (!this.#reporter) { + return; + } + + logger.debug("Received reporter task", { + payload: rawPayload, + }); + + const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload); + + if (!parsedPayload.success) { + throw new Error( + `Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}` + ); + } + + const payload = parsedPayload.data; + + // Subtract an hour from the payload._cron.ts + const startAt = new Date(payload._cron.ts.getTime() - 1000 * 60 * 60); + + const schema = z.array(z.object({ count: z.coerce.number() })); + + // Count the number of jobs that have been added since the startAt date and before the payload._cron.ts date + const rawAddedResults = await this.#prisma.$queryRawUnsafe( + `SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs WHERE created_at > $1 AND created_at < $2`, + startAt, + payload._cron.ts + ); + + const addedCountResults = schema.parse(rawAddedResults)[0]; + + // Count the total number of jobs in the jobs table + const rawTotalResults = await this.#prisma.$queryRawUnsafe( + `SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs` + ); + + const totalCountResults = schema.parse(rawTotalResults)[0]; + + logger.debug("Calculated metrics about the jobs table", { + rawAddedResults, + rawTotalResults, + payload, + }); + + await this.#reporter( + "Worker Queue Metrics", + `Added ${addedCountResults.count} jobs in the last hour, total jobs: ${totalCountResults.count}` + ); } #logDebug(message: string, args?: any) { diff --git a/apps/webapp/app/services/email.server.ts b/apps/webapp/app/services/email.server.ts index bc8014fcc..905c94d56 100644 --- a/apps/webapp/app/services/email.server.ts +++ b/apps/webapp/app/services/email.server.ts @@ -1,4 +1,4 @@ -import type { DeliverEmail } from "emails"; +import type { DeliverEmail, SendPlainTextOptions } from "emails"; import { EmailClient } from "emails"; import type { SendEmailOptions } from "remix-auth-email-link"; import { redirect } from "remix-typedjson"; @@ -27,6 +27,10 @@ export async function sendMagicLinkEmail(options: SendEmailOptions): P }); } +export async function sendPlainTextEmail(options: SendPlainTextOptions) { + return client.sendPlainText(options); +} + export async function scheduleWelcomeEmail(user: User) { //delay for one minute in development, longer in production const delay = process.env.NODE_ENV === "development" ? 1000 * 60 : 1000 * 60 * 22; diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 9cc428b75..9ef140fc1 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; import { ZodWorker } from "~/platform/zodWorker.server"; -import { sendEmail } from "./email.server"; +import { sendEmail, sendPlainTextEmail } from "./email.server"; import { IndexEndpointService } from "./endpoints/indexEndpoint.server"; import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server"; import { DeliverEventService } from "./events/deliverEvent.server"; @@ -21,6 +21,7 @@ import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequ import { PerformTaskOperationService } from "./tasks/performTaskOperation.server"; import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout"; import { addMissingVersionField } from "@trigger.dev/core"; +import { logger } from "./logger.server"; const workerCatalog = { indexEndpoint: z.object({ @@ -129,10 +130,20 @@ function getWorkerQueue() { name: "workerQueue", prisma, cleanup: { - // cleanup once per hour - frequencyExpression: "0 * * * *", - // delete jobs that have been completed for more than 7 days - ttl: 7 * 24 * 60 * 60 * 1000, + frequencyExpression: "13,27,43 * * * *", + ttl: 7 * 24 * 60 * 60 * 1000, // 7 days + maxCount: 1000, + }, + reporter: async (subject, message) => { + logger.info("workerQueue reporter", { workerMessage: message, subject }); + + if (env.WORKER_REPORTER_EMAIL) { + await sendPlainTextEmail({ + to: env.WORKER_REPORTER_EMAIL, + subject: `workerQueue Report: ${subject}`, + text: message, + }); + } }, runnerOptions: { connectionString: env.DATABASE_URL, diff --git a/packages/emails/src/index.tsx b/packages/emails/src/index.tsx index e07ee63c2..da8d2b39f 100644 --- a/packages/emails/src/index.tsx +++ b/packages/emails/src/index.tsx @@ -42,6 +42,8 @@ export const DeliverEmailSchema = z export type DeliverEmail = z.infer; +export type SendPlainTextOptions = { to: string; subject: string; text: string }; + export class EmailClient { #client?: Resend; #imagesBaseUrl: string; @@ -66,6 +68,20 @@ export class EmailClient { }); } + async sendPlainText(options: SendPlainTextOptions) { + if (this.#client) { + await this.#client.sendEmail({ + from: this.#from, + to: options.to, + replyTo: this.#replyTo, + subject: options.subject, + text: options.text, + }); + + return; + } + } + #getTemplate(data: DeliverEmail): { subject: string; component: ReactElement; From f3a7e2aa2003f6a3ce51d7414f61f80eb4d90700 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Oct 2023 23:15:06 +0100 Subject: [PATCH 57/79] Add APP_ENV to worker reporter email --- apps/webapp/app/services/worker.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 9ef140fc1..84d1d41bf 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -140,7 +140,7 @@ function getWorkerQueue() { if (env.WORKER_REPORTER_EMAIL) { await sendPlainTextEmail({ to: env.WORKER_REPORTER_EMAIL, - subject: `workerQueue Report: ${subject}`, + subject: `[${env.APP_ENV}] workerQueue Report: ${subject}`, text: message, }); } From b23696b255b57178f53760d2d3dd847537a17804 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 11 Oct 2023 10:43:37 +0100 Subject: [PATCH 58/79] Move to reporting worker stats through logs --- apps/webapp/app/env.server.ts | 1 - apps/webapp/app/platform/zodWorker.server.ts | 24 ++++++++++++-------- apps/webapp/app/services/worker.server.ts | 12 ++-------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index c59e436ec..46f3f9e81 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,7 +40,6 @@ const EnvironmentSchema = z.object({ EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), WORKER_ENABLED: z.string().default("true"), EXECUTION_WORKER_ENABLED: z.string().default("true"), - WORKER_REPORTER_EMAIL: z.string().email().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index 6adac7bda..564a1fa06 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -91,6 +91,8 @@ export type ZodWorkerCleanupOptions = { taskOptions?: CronItemOptions; }; +type ZodWorkerReporter = (event: string, properties: Record) => Promise; + export type ZodWorkerOptions = { name: string; runnerOptions: RunnerOptions; @@ -99,7 +101,7 @@ export type ZodWorkerOptions = { tasks: ZodTasks; recurringTasks?: ZodRecurringTasks; cleanup?: ZodWorkerCleanupOptions; - reporter?: (subject: string, message: string) => Promise; + reporter?: ZodWorkerReporter; }; export class ZodWorker { @@ -111,7 +113,7 @@ export class ZodWorker { #recurringTasks?: ZodRecurringTasks; #runner?: GraphileRunner; #cleanup: ZodWorkerCleanupOptions | undefined; - #reporter?: (subject: string, message: string) => Promise; + #reporter?: ZodWorkerReporter; constructor(options: ZodWorkerOptions) { this.#name = options.name; @@ -528,10 +530,11 @@ export class ZodWorker { }); if (this.#reporter) { - await this.#reporter( - "Worker Queue Cleanup", - `Cleaned up ${results.length} jobs older than ${expirationDate.toISOString()}` - ); + await this.#reporter("cleanup_stats", { + count: results.length, + expirationDate, + ts: payload._cron.ts, + }); } } @@ -581,10 +584,11 @@ export class ZodWorker { payload, }); - await this.#reporter( - "Worker Queue Metrics", - `Added ${addedCountResults.count} jobs in the last hour, total jobs: ${totalCountResults.count}` - ); + await this.#reporter("queue_metrics", { + addedCount: addedCountResults.count, + totalCount: totalCountResults.count, + ts: payload._cron.ts, + }); } #logDebug(message: string, args?: any) { diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 84d1d41bf..4ce6f2185 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -134,16 +134,8 @@ function getWorkerQueue() { ttl: 7 * 24 * 60 * 60 * 1000, // 7 days maxCount: 1000, }, - reporter: async (subject, message) => { - logger.info("workerQueue reporter", { workerMessage: message, subject }); - - if (env.WORKER_REPORTER_EMAIL) { - await sendPlainTextEmail({ - to: env.WORKER_REPORTER_EMAIL, - subject: `[${env.APP_ENV}] workerQueue Report: ${subject}`, - text: message, - }); - } + reporter: async (event, properties) => { + logger.info("workerQueue report", { event, properties }); }, runnerOptions: { connectionString: env.DATABASE_URL, From 498f56de3eb94b17162b307041276f989689841a Mon Sep 17 00:00:00 2001 From: Arjun S <37960163+arjunindia@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:22:10 +0530 Subject: [PATCH 59/79] Feat: Replaicing react hot toast with Sonner toasts (#600) * Feat: Replaicing react hot toast with Sonner toasts * pnpm lock file fix * Quickfix --- .../app/components/primitives/Toast.tsx | 106 +++++++----------- .../components/stories/ToastUI.stories.tsx | 47 ++++++++ apps/webapp/package.json | 1 + pnpm-lock.yaml | 12 ++ 4 files changed, 101 insertions(+), 65 deletions(-) create mode 100644 apps/webapp/app/components/stories/ToastUI.stories.tsx diff --git a/apps/webapp/app/components/primitives/Toast.tsx b/apps/webapp/app/components/primitives/Toast.tsx index 133bf6507..8a16cc984 100644 --- a/apps/webapp/app/components/primitives/Toast.tsx +++ b/apps/webapp/app/components/primitives/Toast.tsx @@ -1,7 +1,7 @@ import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon } from "@heroicons/react/24/solid"; -import { AnimatePresence, motion } from "framer-motion"; -import toast, { Toaster, resolveValue, useToasterStore } from "react-hot-toast"; +import { Toaster, toast } from "sonner"; + import { useTypedLoaderData } from "remix-typedjson"; import { loader } from "~/root"; import { useEffect } from "react"; @@ -11,79 +11,55 @@ const permanentToastDuration = 60 * 60 * 24 * 1000; export function Toast() { const { toastMessage } = useTypedLoaderData(); - useEffect(() => { if (!toastMessage) { return; } const { message, type, options } = toastMessage; - switch (type) { - case "success": - toast.success(message, { - duration: options.ephemeral ? defaultToastDuration : permanentToastDuration, - }); - break; - case "error": - toast.error(message, { - duration: options.ephemeral ? defaultToastDuration : permanentToastDuration, - }); - break; - default: - throw new Error(`${type} is not handled`); - } + toast.custom((t) => , { + duration: options.ephemeral ? defaultToastDuration : permanentToastDuration, + }); }, [toastMessage]); + return ; +} + +export function ToastUI({ + variant, + message, + t, + toastWidth = 356, // Default width, matches what sonner provides by default +}: { + variant: "error" | "success"; + message: string; + t: string; + toastWidth?: string | number; +}) { return ( - , - }, - error: { - icon: , - }, +
- {(t) => ( - - - {t.icon} - {resolveValue(t.message, t)} - - - - )} - +
+ {variant === "success" ? ( + + ) : ( + + )} + {message} + +
+
); } diff --git a/apps/webapp/app/components/stories/ToastUI.stories.tsx b/apps/webapp/app/components/stories/ToastUI.stories.tsx new file mode 100644 index 000000000..c4a7164d8 --- /dev/null +++ b/apps/webapp/app/components/stories/ToastUI.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Toaster, toast } from "sonner"; +import { ToastUI } from "../primitives/Toast"; +import { Button } from "../primitives/Buttons"; + +const meta: Meta = { + title: "Primitives/Toast", +}; + +export default meta; + +type Story = StoryObj; + +export const Toasts: Story = { + render: () => , +}; + +function Collection() { + return ( +
+ + +
+ + + +
+ ); +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index a9cc3baaa..cf9f0b53e 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -106,6 +106,7 @@ "simple-oauth2": "^5.0.0", "simplur": "^3.0.1", "slug": "^6.0.0", + "sonner": "^1.0.3", "tailwind-merge": "^1.12.0", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss-animate": "^1.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17000ef6b..2f9c3b44c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,6 +182,7 @@ importers: simple-oauth2: ^5.0.0 simplur: ^3.0.1 slug: ^6.0.0 + sonner: ^1.0.3 storybook: ^7.0.7 storybook-addon-designs: 7.0.0-beta.2 storybook-addon-variants: ^0.2.0 @@ -275,6 +276,7 @@ importers: simple-oauth2: 5.0.0 simplur: 3.0.1 slug: 6.1.0 + sonner: 1.0.3_biqbaboplfbrettd7655fr4n2y tailwind-merge: 1.12.0 tailwind-scrollbar-hide: 1.1.7 tailwindcss-animate: 1.0.5_tailwindcss@3.3.2 @@ -28918,6 +28920,16 @@ packages: atomic-sleep: 1.0.0 dev: true + /sonner/1.0.3_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-hBoA2zKuYW3lUnpx4K0vAn8j77YuYiwvP9sLQfieNS2pd5FkT20sMyPTDJnl9S+5T27ZJbwQRPiujwvDBwhZQg==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /sorcery/0.11.0: resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} hasBin: true From 39a332eff1b466aa01c151c417c7da27588e85ba Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:54:24 +0530 Subject: [PATCH 60/79] feat: add filter for active jobs in the dashboard (#601) * feat: add filter for active jobs in the dashboard * use cursor-pointer when hovering Switch label * remove unused import * remove unnecessary span * add flex-gap --- .../app/components/primitives/Switch.tsx | 2 +- apps/webapp/app/hooks/useFilterJobs.ts | 24 ++++++++++++++++--- apps/webapp/app/hooks/useToggleFilter.ts | 21 ++++++++++++++++ .../route.tsx | 20 ++++++++++++---- .../route.tsx | 2 +- 5 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 apps/webapp/app/hooks/useToggleFilter.ts diff --git a/apps/webapp/app/components/primitives/Switch.tsx b/apps/webapp/app/components/primitives/Switch.tsx index fe4d69c46..cf592f8b8 100644 --- a/apps/webapp/app/components/primitives/Switch.tsx +++ b/apps/webapp/app/components/primitives/Switch.tsx @@ -15,7 +15,7 @@ const variations = { container: "flex items-center gap-x-1.5 rounded hover:bg-slate-850 pr-1 py-[0.1rem] pl-1.5", root: "h-3 w-6", thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0", - text: "text-xs text-slate-400 group-hover:text-slate-200 mt-0.5", + text: "text-xs text-slate-400 group-hover:text-slate-200 hover:cursor-pointer", }, }; diff --git a/apps/webapp/app/hooks/useFilterJobs.ts b/apps/webapp/app/hooks/useFilterJobs.ts index 5cad14da3..950e19a43 100644 --- a/apps/webapp/app/hooks/useFilterJobs.ts +++ b/apps/webapp/app/hooks/useFilterJobs.ts @@ -1,9 +1,21 @@ import { ProjectJob } from "./useJobs"; import { useTextFilter } from "./useTextFilter"; +import { useToggleFilter } from "./useToggleFilter"; -export function useFilterJobs(jobs: ProjectJob[]) { - const { filterText, setFilterText, filteredItems } = useTextFilter({ +export function useFilterJobs(jobs: ProjectJob[], onlyActiveJobs = false) { + const toggleFilterRes = useToggleFilter({ items: jobs, + filter: (job, onlyActiveJobs) => { + if (onlyActiveJobs && job.status !== "ACTIVE") { + return false; + } + return true; + }, + defaultValue: onlyActiveJobs, + }); + + const textFilterRes = useTextFilter({ + items: toggleFilterRes.filteredItems, filter: (job, text) => { if (job.slug.toLowerCase().includes(text.toLowerCase())) return true; if (job.title.toLowerCase().includes(text.toLowerCase())) return true; @@ -24,5 +36,11 @@ export function useFilterJobs(jobs: ProjectJob[]) { }, }); - return { filterText, setFilterText, filteredItems }; + return { + filteredItems: textFilterRes.filteredItems, + filterText: textFilterRes.filterText, + setFilterText: textFilterRes.setFilterText, + onlyActiveJobs: toggleFilterRes.isToggleActive, + setOnlyActiveJobs: toggleFilterRes.setToggleActive, + }; } diff --git a/apps/webapp/app/hooks/useToggleFilter.ts b/apps/webapp/app/hooks/useToggleFilter.ts new file mode 100644 index 000000000..2e099a9f9 --- /dev/null +++ b/apps/webapp/app/hooks/useToggleFilter.ts @@ -0,0 +1,21 @@ +import { useMemo, useState } from "react"; + +type ToggleFilterProps = { + items: T[]; + filter: (item: T, isToggleActive: boolean) => boolean; + defaultValue?: boolean; +}; + +export function useToggleFilter({ items, filter, defaultValue = false }: ToggleFilterProps) { + const [isToggleActive, setToggleActive] = useState(defaultValue); + + const filteredItems = useMemo(() => { + return items.filter((item) => filter(item, isToggleActive)); + }, [items, isToggleActive]); + + return { + isToggleActive, + setToggleActive, + filteredItems, + }; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx index 0144db63b..88ce334c3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx @@ -19,6 +19,7 @@ import { PageTitleRow, } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; +import { Switch } from "~/components/primitives/Switch"; import { TextLink } from "~/components/primitives/TextLink"; import { useFilterJobs } from "~/hooks/useFilterJobs"; import { useOrganization } from "~/hooks/useOrganizations"; @@ -62,8 +63,11 @@ export default function Page() { const organization = useOrganization(); const project = useProject(); const { jobs } = useTypedLoaderData(); - const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs); - const hasJobs = jobs.length > 0; + const { filterText, setFilterText, filteredItems, onlyActiveJobs, setOnlyActiveJobs } = + useFilterJobs(jobs); + const totalJobs = jobs.length; + const hasJobs = totalJobs > 0; + const activeJobCount = jobs.filter((j) => j.status === "ACTIVE").length; return ( @@ -74,7 +78,8 @@ export default function Page() { - + + @@ -96,7 +101,7 @@ export default function Page() { )}
-
+
setFilterText(e.target.value)} autoFocus /> +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx index 8a2535a2b..f450aef11 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx @@ -152,7 +152,7 @@ function PossibleIntegrationsList({ onCheckedChange={setOnlyShowIntegrations} variant="small" label={ - + Trigger.dev Integrations } From 203a4313502180eb92942e0ee58a1caa30969bcd Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 11 Oct 2023 11:55:35 +0100 Subject: [PATCH 61/79] Add stripe and airtable to the integrations catalog --- docs/integrations/introduction.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/integrations/introduction.mdx b/docs/integrations/introduction.mdx index cce19f3a4..d307fa403 100644 --- a/docs/integrations/introduction.mdx +++ b/docs/integrations/introduction.mdx @@ -33,6 +33,7 @@ Navigate the menu or select Integrations from the table below. | API | Description | Webhooks | Tasks | | ----------------------------------------- | ---------------------------------------------------------------- | -------- | ----- | +| [Airtable](/integrations/apis/airtable) | Interact with the Airtable API | 🕘 | ✅ | | [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ | | [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ | | [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ | @@ -41,5 +42,6 @@ Navigate the menu or select Integrations from the table below. | [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ | | [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | 🕘 | ✅ | | [Slack](/integrations/apis/slack) | Send Slack messages | 🕘 | ✅ | +| [Stripe](/integrations/apis/stripe) | Interact with the Stripe API | ✅ | ✅ | | [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | ✅ | ✅ | | [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | ✅ | ✅ | From 50e3d9e43a139308ebe19635ea07c2d95098388a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 11 Oct 2023 17:31:19 +0100 Subject: [PATCH 62/79] Indexing errors don't get displayed anywhere (#605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added EndpointIndex status. Default is PENDING, existing rows are SUCCESS * Made it easier to create a migration SQL file * Created a job-catalog file for misconfigured Jobs that should error when running the CLI * EndpointIndex data and state are now optional * The Environments page now shows the status of the last refresh * Improved the UI about endpoints * Added EndpointIndex error column * WIP making the endpoint indexing more robust * Indexing errors are now surfaced * Improved the error show it shows the job id * Use “performEndpointIndexing” when you create your first endpoint from the UI * Use “performEndpointIndexing” for the recurring endpoint checker * Staging is now auto-indexed every 10 mins too * Use “performEndpointIndexing” for the webhook * Moved the throttling to a util * Removed instructional comments * Created a reusable retry system with exponential backoff * Use p-retry for retrying with backoff * The CLI gets indexing results and displays errors * Improved the indexing error messages and display in the console * Use a pre so the Indexing error is correctly split over multiple lines * Use a db transaction for webhook that triggers endpoint indexing * Tidied up imports * Support older versions of the server * Improved the comment on the misconfigured job * Changeset: When indexing user's jobs errors are now stored and displayed --- .changeset/seven-turkeys-worry.md | 6 + .../environments/EndpointIndexStatus.tsx | 74 ++++ .../app/components/primitives/Callout.tsx | 11 +- .../app/components/primitives/FormError.tsx | 13 +- .../webapp/app/models/indexEndpoint.server.ts | 14 - .../EnvironmentsPresenter.server.ts | 20 +- .../ConfigureEndpointSheet.tsx | 60 +++- .../route.tsx | 23 +- .../routes/api.v1.endpointindex.$indexId.ts | 68 ++++ ...endpointSlug.index.$indexHookIdentifier.ts | 80 +++-- ...nvironmentParam.endpoint.$endpointParam.ts | 6 - .../webapp/app/services/endpointApi.server.ts | 40 +-- .../endpoints/createEndpoint.server.ts | 15 +- .../endpoints/indexEndpoint.server.ts | 233 +----------- .../endpoints/performEndpointIndexService.ts | 338 ++++++++++++++++++ .../recurringEndpointIndex.server.ts | 18 +- .../validateCreateEndpoint.server.ts | 11 +- apps/webapp/app/services/worker.server.ts | 20 +- apps/webapp/package.json | 3 +- packages/cli/package.json | 3 + packages/cli/src/commands/dev.ts | 279 +++++++++------ packages/cli/src/commands/init.ts | 2 +- packages/cli/src/commands/whoami.ts | 2 +- packages/cli/src/utils/logger.ts | 3 + packages/cli/src/utils/throttle.ts | 18 + packages/cli/src/utils/triggerApi.ts | 36 +- packages/cli/src/utils/wait.ts | 5 + packages/cli/tsconfig.json | 1 - packages/core/src/schemas/api.ts | 47 +++ packages/database/package.json | 3 +- .../migration.sql | 11 + .../migration.sql | 3 + .../migration.sql | 2 + packages/database/prisma/schema.prisma | 17 +- pnpm-lock.yaml | 59 ++- references/job-catalog/package.json | 3 +- references/job-catalog/src/misconfigured.ts | 35 ++ 37 files changed, 1101 insertions(+), 481 deletions(-) create mode 100644 .changeset/seven-turkeys-worry.md create mode 100644 apps/webapp/app/components/environments/EndpointIndexStatus.tsx delete mode 100644 apps/webapp/app/models/indexEndpoint.server.ts create mode 100644 apps/webapp/app/routes/api.v1.endpointindex.$indexId.ts create mode 100644 apps/webapp/app/services/endpoints/performEndpointIndexService.ts create mode 100644 packages/cli/src/utils/throttle.ts create mode 100644 packages/cli/src/utils/wait.ts create mode 100644 packages/database/prisma/migrations/20231010115840_endpoint_index_status_added/migration.sql create mode 100644 packages/database/prisma/migrations/20231010120458_endpoint_index_data_and_stats_are_now_optional/migration.sql create mode 100644 packages/database/prisma/migrations/20231010135433_endpoint_index_added_error_column/migration.sql create mode 100644 references/job-catalog/src/misconfigured.ts diff --git a/.changeset/seven-turkeys-worry.md b/.changeset/seven-turkeys-worry.md new file mode 100644 index 000000000..9165ca4e2 --- /dev/null +++ b/.changeset/seven-turkeys-worry.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/core": patch +"@trigger.dev/cli": patch +--- + +When indexing user's jobs errors are now stored and displayed diff --git a/apps/webapp/app/components/environments/EndpointIndexStatus.tsx b/apps/webapp/app/components/environments/EndpointIndexStatus.tsx new file mode 100644 index 000000000..67398b0b7 --- /dev/null +++ b/apps/webapp/app/components/environments/EndpointIndexStatus.tsx @@ -0,0 +1,74 @@ +import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/20/solid"; +import { EndpointIndexStatus } from "@trigger.dev/database"; +import { cn } from "~/utils/cn"; +import { Spinner } from "../primitives/Spinner"; + +export function EndpointIndexStatusIcon({ status }: { status: EndpointIndexStatus }) { + switch (status) { + case "PENDING": + return ; + case "STARTED": + return ; + case "SUCCESS": + return ( + + ); + case "FAILURE": + return ; + } +} + +export function EndpointIndexStatusLabel({ status }: { status: EndpointIndexStatus }) { + switch (status) { + case "PENDING": + return ( + + {endpointIndexStatusTitle(status)} + + ); + case "STARTED": + return ( + + {endpointIndexStatusTitle(status)} + + ); + case "SUCCESS": + return ( + + {endpointIndexStatusTitle(status)} + + ); + case "FAILURE": + return ( + + {endpointIndexStatusTitle(status)} + + ); + } +} + +export function endpointIndexStatusTitle(status: EndpointIndexStatus): string { + switch (status) { + case "PENDING": + return "Pending"; + case "STARTED": + return "Started"; + case "SUCCESS": + return "Success"; + case "FAILURE": + return "Failure"; + } +} + +export function endpointIndexStatusClassNameColor(status: EndpointIndexStatus): string { + switch (status) { + case "PENDING": + return "text-dimmed"; + case "STARTED": + return "text-blue-500"; + case "SUCCESS": + return "text-green-500"; + case "FAILURE": + return "text-rose-500"; + } +} diff --git a/apps/webapp/app/components/primitives/Callout.tsx b/apps/webapp/app/components/primitives/Callout.tsx index eb920502e..a4bd20be6 100644 --- a/apps/webapp/app/components/primitives/Callout.tsx +++ b/apps/webapp/app/components/primitives/Callout.tsx @@ -13,6 +13,7 @@ import { import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; import { Paragraph } from "./Paragraph"; +import { Spinner } from "./Spinner"; export const variantClasses = { info: { @@ -51,8 +52,16 @@ export const variantClasses = { textColor: "text-blue-200", linkClassName: "transition hover:bg-blue-400/40", }, + pending: { + className: "border-blue-400/20 bg-blue-800/30", + icon: , + textColor: "text-blue-300", + linkClassName: "transition hover:bg-blue-400/40", + }, } as const; +export type CalloutVariant = keyof typeof variantClasses; + export function Callout({ children, className, @@ -63,7 +72,7 @@ export function Callout({ children?: React.ReactNode; className?: string; icon?: React.ReactNode; - variant: keyof typeof variantClasses; + variant: CalloutVariant; to?: string; }) { const variantDefinition = variantClasses[variant]; diff --git a/apps/webapp/app/components/primitives/FormError.tsx b/apps/webapp/app/components/primitives/FormError.tsx index 79d537d74..ed90f7e99 100644 --- a/apps/webapp/app/components/primitives/FormError.tsx +++ b/apps/webapp/app/components/primitives/FormError.tsx @@ -2,8 +2,17 @@ import type { z } from "zod"; import { Paragraph } from "./Paragraph"; import { NamedIcon } from "./NamedIcon"; import { motion } from "framer-motion"; +import { cn } from "~/utils/cn"; -export function FormError({ children, id }: { children: React.ReactNode; id?: string }) { +export function FormError({ + children, + id, + className, +}: { + children: React.ReactNode; + id?: string; + className?: string; +}) { return ( <> {children && ( @@ -11,7 +20,7 @@ export function FormError({ children, id }: { children: React.ReactNode; id?: st initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} - className="flex items-start gap-0.5" + className={cn("flex items-start gap-0.5", className)} > diff --git a/apps/webapp/app/models/indexEndpoint.server.ts b/apps/webapp/app/models/indexEndpoint.server.ts deleted file mode 100644 index 6a58660de..000000000 --- a/apps/webapp/app/models/indexEndpoint.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod"; - -const IndexEndpointStatsSchema = z.object({ - jobs: z.number(), - sources: z.number(), - dynamicTriggers: z.number(), - dynamicSchedules: z.number(), -}); - -export type IndexEndpointStats = z.infer; - -export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats { - return IndexEndpointStatsSchema.parse(stats); -} diff --git a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts index 477691f62..bd5f4ef87 100644 --- a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts +++ b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts @@ -1,13 +1,19 @@ import { PrismaClient, prisma } from "~/db.server"; -import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server"; import { Project } from "~/models/project.server"; import { User } from "~/models/user.server"; import type { Endpoint, EndpointIndex, + EndpointIndexStatus, RuntimeEnvironment, RuntimeEnvironmentType, } from "@trigger.dev/database"; +import { + EndpointIndexError, + EndpointIndexErrorSchema, + IndexEndpointStats, + parseEndpointIndexStats, +} from "@trigger.dev/core"; export type Client = { slug: string; @@ -34,9 +40,11 @@ export type ClientEndpoint = url: string; indexWebhookPath: string; latestIndex?: { + status: EndpointIndexStatus; source: string; updatedAt: Date; - stats: IndexEndpointStats; + stats?: IndexEndpointStats; + error?: EndpointIndexError; }; environment: { id: string; @@ -81,9 +89,11 @@ export class EnvironmentsPresenter { indexingHookIdentifier: true, indexings: { select: { + status: true, source: true, updatedAt: true, stats: true, + error: true, }, take: 1, orderBy: { @@ -214,7 +224,7 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [ function endpointClient( endpoint: Pick & { - indexings: Pick[]; + indexings: Pick[]; }, environment: Pick, baseUrl: string @@ -227,9 +237,13 @@ function endpointClient( indexWebhookPath: `${baseUrl}/api/v1/endpoints/${environment.id}/${endpoint.slug}/index/${endpoint.indexingHookIdentifier}`, latestIndex: endpoint.indexings[0] ? { + status: endpoint.indexings[0].status, source: endpoint.indexings[0].source, updatedAt: endpoint.indexings[0].updatedAt, stats: parseEndpointIndexStats(endpoint.indexings[0].stats), + error: endpoint.indexings[0].error + ? EndpointIndexErrorSchema.parse(endpoint.indexings[0].error) + : undefined, } : undefined, environment: environment, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/ConfigureEndpointSheet.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/ConfigureEndpointSheet.tsx index 5db9273da..171761967 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/ConfigureEndpointSheet.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/ConfigureEndpointSheet.tsx @@ -6,7 +6,7 @@ import { useEventSource } from "remix-utils"; import { InlineCode } from "~/components/code/InlineCode"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; -import { Callout } from "~/components/primitives/Callout"; +import { Callout, CalloutVariant } from "~/components/primitives/Callout"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { DateTime } from "~/components/primitives/DateTime"; import { FormError } from "~/components/primitives/FormError"; @@ -18,8 +18,14 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { Sheet, SheetBody, SheetContent, SheetHeader } from "~/components/primitives/Sheet"; import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server"; import { endpointStreamingPath } from "~/utils/pathBuilder"; -import { RuntimeEnvironmentType } from "../../../../../packages/database/src"; +import { EndpointIndexStatus, RuntimeEnvironmentType } from "../../../../../packages/database/src"; import { bodySchema } from "../resources.environments.$environmentParam.endpoint"; +import { + EndpointIndexStatusIcon, + EndpointIndexStatusLabel, + endpointIndexStatusTitle, +} from "~/components/environments/EndpointIndexStatus"; +import { CodeBlock } from "~/components/code/CodeBlock"; type ConfigureEndpointSheetProps = { slug: string; @@ -119,15 +125,29 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd method="post" action={`/resources/environments/${endpoint.environment.id}/endpoint/${endpoint.id}`} > - - - Endpoint configured. Last refreshed:{" "} - {endpoint.latestIndex ? ( - - ) : ( - "–" - )} - + + } + className="justiy-between items-center" + > +
+ + + Last refreshed:{" "} + {endpoint.latestIndex ? ( + <> + + + ) : ( + "–" + )} + +
+
@@ -155,3 +180,16 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd ); } + +function calloutVariantFromStatus(status: EndpointIndexStatus): CalloutVariant { + switch (status) { + case "PENDING": + return "pending"; + case "STARTED": + return "pending"; + case "SUCCESS": + return "success"; + case "FAILURE": + return "error"; + } +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx index a335b1411..521fb993a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx @@ -3,11 +3,16 @@ import { LoaderArgs } from "@remix-run/server-runtime"; import { useEffect, useMemo, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { useEventSource } from "remix-utils"; +import { + EndpointIndexStatusIcon, + EndpointIndexStatusLabel, +} from "~/components/environments/EndpointIndexStatus"; import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel"; import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; -import { Button, ButtonContent } from "~/components/primitives/Buttons"; +import { Badge } from "~/components/primitives/Badge"; +import { ButtonContent } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { DateTime } from "~/components/primitives/DateTime"; import { Header2, Header3 } from "~/components/primitives/Headers"; @@ -38,7 +43,6 @@ import { ProjectParamSchema, projectEnvironmentsStreamingPath } from "~/utils/pa import { requestUrl } from "~/utils/requestUrl.server"; import { RuntimeEnvironmentType } from "../../../../../packages/database/src"; import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet"; -import { Badge } from "~/components/primitives/Badge"; import { FirstEndpointSheet } from "./FirstEndpointSheet"; export const loader = async ({ request, params }: LoaderArgs) => { @@ -180,6 +184,7 @@ export default function Page() { Environment Url Last refreshed + Last refresh Status Jobs Go to page @@ -268,7 +273,7 @@ function EndpointRow({
- +
The {environmentTitle({ type })} environment is not configured @@ -290,7 +295,17 @@ function EndpointRow({ {endpoint.latestIndex ? : "–"} - {endpoint.latestIndex?.stats.jobs ?? "–"} + + {endpoint.latestIndex ? ( +
+ + +
+ ) : ( + "–" + )} +
+ {endpoint.latestIndex?.stats?.jobs ?? "–"} ); diff --git a/apps/webapp/app/routes/api.v1.endpointindex.$indexId.ts b/apps/webapp/app/routes/api.v1.endpointindex.$indexId.ts new file mode 100644 index 000000000..fcf271b21 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.endpointindex.$indexId.ts @@ -0,0 +1,68 @@ +import { ActionArgs, json } from "@remix-run/server-runtime"; +import { + EndpointIndexErrorSchema, + GetEndpointIndexResponse, + GetEndpointIndexResponseSchema, +} from "@trigger.dev/core"; +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server"; +import { logger } from "~/services/logger.server"; + +const ParamsSchema = z.object({ + indexId: z.string(), +}); + +export async function loader({ request, params }: ActionArgs) { + if (request.method.toUpperCase() !== "GET") { + return { status: 405, body: "Method Not Allowed" }; + } + + const parsedParams = ParamsSchema.safeParse(params); + + if (!parsedParams.success) { + return json({ error: "Invalid params" }, { status: 400 }); + } + + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const authenticatedEnv = authenticationResult.environment; + + const { indexId } = parsedParams.data; + + const endpointIndex = await prisma.endpointIndex.findUnique({ + where: { + id: indexId, + endpoint: { + environmentId: authenticatedEnv.id, + }, + }, + }); + + if (!endpointIndex) { + logger.info("EndpointIndex not found", { url: request.url }); + return json({ error: "EndpointIndex not found" }, { status: 404 }); + } + + const parsed = GetEndpointIndexResponseSchema.safeParse(endpointIndex); + + if (!parsed.success) { + logger.info("EndpointIndex failed parsing", { errors: parsed.error.issues, endpointIndex }); + const parseFailResult: GetEndpointIndexResponse = { + status: "FAILURE", + error: { + message: "Invalid endpoint index", + }, + updatedAt: new Date(), + }; + return json(parseFailResult, { status: 500 }); + } + + return json(parsed.data); +} diff --git a/apps/webapp/app/routes/api.v1.endpoints.$environmentId.$endpointSlug.index.$indexHookIdentifier.ts b/apps/webapp/app/routes/api.v1.endpoints.$environmentId.$endpointSlug.index.$indexHookIdentifier.ts index 5429b9b21..0ccd1ed99 100644 --- a/apps/webapp/app/routes/api.v1.endpoints.$environmentId.$endpointSlug.index.$indexHookIdentifier.ts +++ b/apps/webapp/app/routes/api.v1.endpoints.$environmentId.$endpointSlug.index.$indexHookIdentifier.ts @@ -1,7 +1,7 @@ import { ActionArgs, LoaderArgs, json } from "@remix-run/server-runtime"; import { RuntimeEnvironmentType } from "@trigger.dev/database"; import { z } from "zod"; -import { PrismaClient, prisma } from "~/db.server"; +import { $transaction, PrismaClient, prisma } from "~/db.server"; import { logger } from "~/services/logger.server"; import { workerQueue } from "~/services/worker.server"; import { safeJsonParse } from "~/utils/json"; @@ -93,43 +93,53 @@ export class TriggerEndpointIndexHookService { body, }); - const endpoint = await this.#prismaClient.endpoint.findUnique({ - where: { - environmentId_slug: { - environmentId, - slug: endpointSlug, + await $transaction(this.#prismaClient, async (tx) => { + const endpoint = await tx.endpoint.findUnique({ + where: { + environmentId_slug: { + environmentId, + slug: endpointSlug, + }, }, - }, - include: { - environment: true, - }, - }); + include: { + environment: true, + }, + }); - if (!endpoint) { - throw new Error("Endpoint not found"); - } - - if (endpoint.indexingHookIdentifier !== indexHookIdentifier) { - throw new Error("Index hook identifier is invalid"); - } - - const reason = parseReasonFromBody(body); - - // Index the endpoint in 5 seconds from now - await workerQueue.enqueue( - "indexEndpoint", - { - id: endpoint.id, - source: "HOOK", - reason, - sourceData: body, - }, - { - runAt: new Date(Date.now() + 5000), - maxAttempts: - endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined, + if (!endpoint) { + throw new Error("Endpoint not found"); } - ); + + if (endpoint.indexingHookIdentifier !== indexHookIdentifier) { + throw new Error("Index hook identifier is invalid"); + } + + const reason = parseReasonFromBody(body); + + const index = await tx.endpointIndex.create({ + data: { + endpointId: endpoint.id, + status: "PENDING", + source: "HOOK", + reason, + sourceData: body, + }, + }); + + // Index the endpoint in 5 seconds from now + await workerQueue.enqueue( + "performEndpointIndexing", + { + id: index.id, + }, + { + runAt: new Date(Date.now() + 5000), + maxAttempts: + endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined, + tx, + } + ); + }); } } diff --git a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts index 297595319..df8fcf927 100644 --- a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts +++ b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts @@ -1,11 +1,5 @@ -import { parse } from "@conform-to/zod"; import { ActionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; -import { prisma } from "~/db.server"; -import { - CreateEndpointError, - CreateEndpointService, -} from "~/services/endpoints/createEndpoint.server"; import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server"; import { requireUserId } from "~/services/session.server"; diff --git a/apps/webapp/app/services/endpointApi.server.ts b/apps/webapp/app/services/endpointApi.server.ts index 494930a07..708a62ee7 100644 --- a/apps/webapp/app/services/endpointApi.server.ts +++ b/apps/webapp/app/services/endpointApi.server.ts @@ -104,6 +104,7 @@ export class EndpointApi { } async indexEndpoint() { + const startTimeInMs = performance.now(); const response = await safeFetch(this.url, { method: "POST", headers: { @@ -113,40 +114,13 @@ export class EndpointApi { }, }); - if (!response) { - throw new Error(`Could not connect to endpoint ${this.url}`); - } - - if (response.status === 401) { - const body = await safeBodyFromResponse(response, ErrorWithStackSchema); - - if (body) { - return { - ok: false, - error: body.message, - } as const; - } - - return { - ok: false, - error: `Trigger API key is invalid`, - } as const; - } - - if (!response.ok) { - throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`); - } - - const anyBody = await response.json(); - - const data = IndexEndpointResponseSchema.parse(anyBody); - const headers = EndpointHeadersSchema.parse(Object.fromEntries(response.headers.entries())); - return { - ok: true, - data, - headers, - } as const; + response, + headerParser: EndpointHeadersSchema, + parser: IndexEndpointResponseSchema, + errorParser: ErrorWithStackSchema, + durationInMs: Math.floor(performance.now() - startTimeInMs), + }; } async executeJobRequest(options: RunJobBody) { diff --git a/apps/webapp/app/services/endpoints/createEndpoint.server.ts b/apps/webapp/app/services/endpoints/createEndpoint.server.ts index 20c62e7ef..a77bb844f 100644 --- a/apps/webapp/app/services/endpoints/createEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/createEndpoint.server.ts @@ -82,12 +82,19 @@ export class CreateEndpointService { }, }); + const endpointIndex = await tx.endpointIndex.create({ + data: { + endpointId: endpoint.id, + status: "PENDING", + source: "INTERNAL", + }, + }); + // Kick off process to fetch the jobs for this endpoint await workerQueue.enqueue( - "indexEndpoint", + "performEndpointIndexing", { - id: endpoint.id, - source: "INTERNAL", + id: endpointIndex.id, }, { tx, @@ -96,7 +103,7 @@ export class CreateEndpointService { } ); - return endpoint; + return { ...endpoint, endpointIndex }; }); return result; diff --git a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts index 2e6da0182..27df22c4f 100644 --- a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts @@ -1,23 +1,9 @@ import type { EndpointIndexSource } from "@trigger.dev/database"; import { PrismaClient, prisma } from "~/db.server"; -import { findEndpoint } from "~/models/endpoint.server"; -import { EndpointApi } from "../endpointApi.server"; -import { RegisterJobService } from "../jobs/registerJob.server"; -import { logger } from "../logger.server"; -import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server"; -import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server"; -import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server"; -import { DisableJobService } from "../jobs/disableJob.server"; -import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server"; +import { PerformEndpointIndexService } from "./performEndpointIndexService"; export class IndexEndpointService { #prismaClient: PrismaClient; - #registerJobService = new RegisterJobService(); - #disableJobService = new DisableJobService(); - #registerSourceServiceV1 = new RegisterSourceServiceV1(); - #registerSourceServiceV2 = new RegisterSourceServiceV2(); - #registerDynamicTriggerService = new RegisterDynamicTriggerService(); - #registerDynamicScheduleService = new RegisterDynamicScheduleService(); constructor(prismaClient: PrismaClient = prisma) { this.#prismaClient = prismaClient; @@ -29,220 +15,17 @@ export class IndexEndpointService { reason?: string, sourceData?: any ) { - const endpoint = await findEndpoint(id); - - // Make a request to the endpoint to fetch a list of jobs - const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url); - - const indexResponse = await client.indexEndpoint(); - - if (!indexResponse.ok) { - throw new Error(indexResponse.error); - } - - const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data; - const { "trigger-version": triggerVersion } = indexResponse.headers; - - logger.debug("Indexing endpoint", { - endpointId: endpoint.id, - endpointUrl: endpoint.url, - endpointSlug: endpoint.slug, - source: source, - sourceData: sourceData, - triggerVersion, - stats: { - jobs: jobs.length, - sources: sources.length, - dynamicTriggers: dynamicTriggers.length, - dynamicSchedules: dynamicSchedules.length, - }, - }); - - if (triggerVersion && triggerVersion !== endpoint.version) { - await this.#prismaClient.endpoint.update({ - where: { - id: endpoint.id, - }, - data: { - version: triggerVersion, - }, - }); - } - - const indexStats = { - jobs: 0, - sources: 0, - dynamicTriggers: 0, - dynamicSchedules: 0, - disabledJobs: 0, - }; - - const existingJobs = await this.#prismaClient.job.findMany({ - where: { - projectId: endpoint.projectId, - deletedAt: null, - }, - include: { - aliases: { - where: { - name: "latest", - environmentId: endpoint.environmentId, - }, - include: { - version: true, - }, - take: 1, - }, - }, - }); - - for (const job of jobs) { - if (!job.enabled) { - const disabledJob = await this.#disableJobService - .call(endpoint, { slug: job.id, version: job.version }) - .catch((error) => { - logger.error("Failed to disable job", { - endpointId: endpoint.id, - job, - error, - }); - - return; - }); - - if (disabledJob) { - indexStats.disabledJobs++; - } - } else { - try { - const registeredVersion = await this.#registerJobService.call(endpoint, job); - - if (registeredVersion) { - indexStats.jobs++; - } - } catch (error) { - logger.error("Failed to register job", { - endpointId: endpoint.id, - job, - error, - }); - } - } - } - - // TODO: we need to do this for sources, dynamic triggers, and dynamic schedules - const missingJobs = existingJobs.filter((job) => { - return !jobs.find((j) => j.id === job.slug); - }); - - if (missingJobs.length > 0) { - logger.debug("Disabling missing jobs", { - endpointId: endpoint.id, - missingJobIds: missingJobs.map((job) => job.slug), - }); - - for (const job of missingJobs) { - const latestVersion = job.aliases[0]?.version; - - if (!latestVersion) { - continue; - } - - const disabledJob = await this.#disableJobService - .call(endpoint, { - slug: job.slug, - version: latestVersion.version, - }) - .catch((error) => { - logger.error("Failed to disable job", { - endpointId: endpoint.id, - job, - error, - }); - - return; - }); - - if (disabledJob) { - indexStats.disabledJobs++; - } - } - } - - for (const source of sources) { - try { - switch (source.version) { - default: - case "1": { - await this.#registerSourceServiceV1.call(endpoint, source); - break; - } - case "2": { - await this.#registerSourceServiceV2.call(endpoint, source); - break; - } - } - - indexStats.sources++; - } catch (error) { - logger.error("Failed to register source", { - endpointId: endpoint.id, - source, - error, - }); - } - } - - for (const dynamicTrigger of dynamicTriggers) { - try { - await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger); - - indexStats.dynamicTriggers++; - } catch (error) { - logger.error("Failed to register dynamic trigger", { - endpointId: endpoint.id, - dynamicTrigger, - error, - }); - } - } - - for (const dynamicSchedule of dynamicSchedules) { - try { - await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule); - - indexStats.dynamicSchedules++; - } catch (error) { - logger.error("Failed to register dynamic schedule", { - endpointId: endpoint.id, - dynamicSchedule, - error, - }); - } - } - - logger.debug("Endpoint indexing complete", { - endpointId: endpoint.id, - indexStats, - source, - sourceData, - reason, - }); - - return await this.#prismaClient.endpointIndex.create({ + const endpointIndex = await this.#prismaClient.endpointIndex.create({ data: { - endpointId: endpoint.id, - stats: indexStats, - data: { - jobs, - sources, - dynamicTriggers, - dynamicSchedules, - }, + endpointId: id, + status: "PENDING", source, - sourceData, reason, + sourceData, }, }); + + const performEndpointIndexService = new PerformEndpointIndexService(); + return await performEndpointIndexService.call(endpointIndex.id); } } diff --git a/apps/webapp/app/services/endpoints/performEndpointIndexService.ts b/apps/webapp/app/services/endpoints/performEndpointIndexService.ts new file mode 100644 index 000000000..ca1c0bcd1 --- /dev/null +++ b/apps/webapp/app/services/endpoints/performEndpointIndexService.ts @@ -0,0 +1,338 @@ +import type { EndpointIndexSource } from "@trigger.dev/database"; +import { PrismaClient, prisma } from "~/db.server"; +import { findEndpoint } from "~/models/endpoint.server"; +import { EndpointApi } from "../endpointApi.server"; +import { RegisterJobService } from "../jobs/registerJob.server"; +import { logger } from "../logger.server"; +import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server"; +import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server"; +import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server"; +import { DisableJobService } from "../jobs/disableJob.server"; +import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server"; +import { EndpointIndexError } from "@trigger.dev/core"; +import { safeBodyFromResponse } from "~/utils/json"; +import { fromZodError } from "zod-validation-error"; +import { IndexEndpointStats } from "@trigger.dev/core"; + +export class PerformEndpointIndexService { + #prismaClient: PrismaClient; + #registerJobService = new RegisterJobService(); + #disableJobService = new DisableJobService(); + #registerSourceServiceV1 = new RegisterSourceServiceV1(); + #registerSourceServiceV2 = new RegisterSourceServiceV2(); + #registerDynamicTriggerService = new RegisterDynamicTriggerService(); + #registerDynamicScheduleService = new RegisterDynamicScheduleService(); + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(id: string) { + const endpointIndex = await this.#prismaClient.endpointIndex.update({ + where: { + id, + }, + data: { + status: "STARTED", + }, + include: { + endpoint: { + include: { + environment: { + include: { + organization: true, + project: true, + }, + }, + }, + }, + }, + }); + + logger.debug("Performing endpoint index", endpointIndex); + + // Make a request to the endpoint to fetch a list of jobs + const client = new EndpointApi( + endpointIndex.endpoint.environment.apiKey, + endpointIndex.endpoint.url + ); + const { response, parser, headerParser, errorParser } = await client.indexEndpoint(); + + if (!response) { + return updateEndpointIndexWithError(this.#prismaClient, id, { + message: `Could not connect to endpoint ${endpointIndex.endpoint.url}`, + }); + } + + if (response.status === 401) { + const body = await safeBodyFromResponse(response, errorParser); + + if (body) { + return updateEndpointIndexWithError(this.#prismaClient, id, { + message: body.message, + }); + } + + return updateEndpointIndexWithError(this.#prismaClient, id, { + message: "Trigger API key is invalid", + }); + } + + if (!response.ok) { + return updateEndpointIndexWithError(this.#prismaClient, id, { + message: `Could not connect to endpoint ${endpointIndex.endpoint.url}. Status code: ${response.status}`, + }); + } + + const anyBody = await response.json(); + const bodyResult = parser.safeParse(anyBody); + + if (!bodyResult.success) { + const issues: string[] = []; + bodyResult.error.issues.forEach((issue) => { + if (issue.path.at(0) === "jobs") { + const jobIndex = issue.path.at(1) as number; + const job = (anyBody as any).jobs[jobIndex]; + + if (job) { + issues.push(`Job "${job.id}": ${issue.message} at "${issue.path.slice(2).join(".")}".`); + } + } + }); + + let friendlyError: string | undefined; + if (issues.length > 0) { + friendlyError = `Your Jobs have issues:\n${issues.map((issue) => `- ${issue}`).join("\n")}`; + } else { + friendlyError = fromZodError(bodyResult.error, { + prefix: "There's an issue with the format of your Jobs", + }).message; + } + + return updateEndpointIndexWithError(this.#prismaClient, id, { + message: friendlyError, + raw: bodyResult.error.issues, + }); + } + + const headerResult = headerParser.safeParse(Object.fromEntries(response.headers.entries())); + if (!headerResult.success) { + const friendlyError = fromZodError(headerResult.error, { + prefix: "Your headers are invalid", + }); + return updateEndpointIndexWithError(this.#prismaClient, id, { + message: friendlyError.message, + raw: headerResult.error.issues, + }); + } + + const { jobs, sources, dynamicTriggers, dynamicSchedules } = bodyResult.data; + const { "trigger-version": triggerVersion } = headerResult.data; + const { endpoint } = endpointIndex; + + if (triggerVersion && triggerVersion !== endpoint.version) { + await this.#prismaClient.endpoint.update({ + where: { + id: endpoint.id, + }, + data: { + version: triggerVersion, + }, + }); + } + + const indexStats: IndexEndpointStats = { + jobs: 0, + sources: 0, + dynamicTriggers: 0, + dynamicSchedules: 0, + disabledJobs: 0, + }; + + const existingJobs = await this.#prismaClient.job.findMany({ + where: { + projectId: endpoint.projectId, + deletedAt: null, + }, + include: { + aliases: { + where: { + name: "latest", + environmentId: endpoint.environmentId, + }, + include: { + version: true, + }, + take: 1, + }, + }, + }); + + for (const job of jobs) { + if (!job.enabled) { + const disabledJob = await this.#disableJobService + .call(endpoint, { slug: job.id, version: job.version }) + .catch((error) => { + logger.error("Failed to disable job", { + endpointId: endpoint.id, + job, + error, + }); + + return; + }); + + if (disabledJob) { + indexStats.disabledJobs++; + } + } else { + try { + const registeredVersion = await this.#registerJobService.call(endpoint, job); + + if (registeredVersion) { + if (!job.internal) { + indexStats.jobs++; + } + } + } catch (error) { + logger.error("Failed to register job", { + endpointId: endpoint.id, + job, + error, + }); + } + } + } + + // TODO: we need to do this for sources, dynamic triggers, and dynamic schedules + const missingJobs = existingJobs.filter((job) => { + return !jobs.find((j) => j.id === job.slug); + }); + + if (missingJobs.length > 0) { + logger.debug("Disabling missing jobs", { + endpointId: endpoint.id, + missingJobIds: missingJobs.map((job) => job.slug), + }); + + for (const job of missingJobs) { + const latestVersion = job.aliases[0]?.version; + + if (!latestVersion) { + continue; + } + + const disabledJob = await this.#disableJobService + .call(endpoint, { + slug: job.slug, + version: latestVersion.version, + }) + .catch((error) => { + logger.error("Failed to disable job", { + endpointId: endpoint.id, + job, + error, + }); + + return; + }); + + if (disabledJob) { + indexStats.disabledJobs++; + } + } + } + + for (const source of sources) { + try { + switch (source.version) { + default: + case "1": { + await this.#registerSourceServiceV1.call(endpoint, source); + break; + } + case "2": { + await this.#registerSourceServiceV2.call(endpoint, source); + break; + } + } + + indexStats.sources++; + } catch (error) { + logger.error("Failed to register source", { + endpointId: endpoint.id, + source, + error, + }); + } + } + + for (const dynamicTrigger of dynamicTriggers) { + try { + await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger); + + indexStats.dynamicTriggers++; + } catch (error) { + logger.error("Failed to register dynamic trigger", { + endpointId: endpoint.id, + dynamicTrigger, + error, + }); + } + } + + for (const dynamicSchedule of dynamicSchedules) { + try { + await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule); + + indexStats.dynamicSchedules++; + } catch (error) { + logger.error("Failed to register dynamic schedule", { + endpointId: endpoint.id, + dynamicSchedule, + error, + }); + } + } + + logger.debug("Endpoint indexing complete", { + endpointId: endpoint.id, + indexStats, + source: endpointIndex.source, + sourceData: endpointIndex.sourceData, + reason: endpointIndex.reason, + }); + + return await this.#prismaClient.endpointIndex.update({ + where: { + id, + }, + data: { + status: "SUCCESS", + stats: indexStats, + data: { + jobs, + sources, + dynamicTriggers, + dynamicSchedules, + }, + }, + }); + } +} + +async function updateEndpointIndexWithError( + prismaClient: PrismaClient, + id: string, + error: EndpointIndexError +) { + return await prismaClient.endpointIndex.update({ + where: { + id, + }, + data: { + status: "FAILURE", + error, + }, + }); +} diff --git a/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts b/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts index 15bc6b015..72658c769 100644 --- a/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts +++ b/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts @@ -17,7 +17,9 @@ export class RecurringEndpointIndexService { const endpoints = await this.#prismaClient.endpoint.findMany({ where: { environment: { - type: RuntimeEnvironmentType.PRODUCTION, + type: { + in: [RuntimeEnvironmentType.PRODUCTION, RuntimeEnvironmentType.STAGING], + }, }, indexings: { none: { @@ -32,12 +34,18 @@ export class RecurringEndpointIndexService { logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", { count: endpoints.length, }); - // Enqueue each endpoint for indexing for (const endpoint of endpoints) { - await workerQueue.enqueue("indexEndpoint", { - id: endpoint.id, - source: "INTERNAL", + const index = await this.#prismaClient.endpointIndex.create({ + data: { + endpointId: endpoint.id, + status: "PENDING", + source: "INTERNAL", + }, + }); + + await workerQueue.enqueue("performEndpointIndexing", { + id: index.id, }); } } diff --git a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts index 4bbe5231f..84baa4c22 100644 --- a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts @@ -66,12 +66,15 @@ export class ValidateCreateEndpointService { }, }); - // Kick off process to fetch the jobs for this endpoint + const index = await tx.endpointIndex.create({ + data: { endpointId: endpoint.id, status: "PENDING", source: "INTERNAL" }, + }); + + // Kick off process to fetch the jobs for this index await workerQueue.enqueue( - "indexEndpoint", + "performEndpointIndexing", { - id: endpoint.id, - source: "INTERNAL", + id: index.id, }, { tx, diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 4ce6f2185..7b6d9e6d8 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -1,16 +1,18 @@ import { DeliverEmailSchema } from "@/../../packages/emails/src"; -import { ScheduledPayloadSchema } from "@trigger.dev/core"; +import { ScheduledPayloadSchema, addMissingVersionField } from "@trigger.dev/core"; import { z } from "zod"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; import { ZodWorker } from "~/platform/zodWorker.server"; -import { sendEmail, sendPlainTextEmail } from "./email.server"; +import { sendEmail } from "./email.server"; import { IndexEndpointService } from "./endpoints/indexEndpoint.server"; +import { PerformEndpointIndexService } from "./endpoints/performEndpointIndexService"; import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server"; import { DeliverEventService } from "./events/deliverEvent.server"; import { InvokeDispatcherService } from "./events/invokeDispatcher.server"; import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server"; import { IntegrationConnectionCreatedService } from "./externalApis/integrationConnectionCreated.server"; +import { logger } from "./logger.server"; import { MissingConnectionCreatedService } from "./runs/missingConnectionCreated.server"; import { PerformRunExecutionV1Service } from "./runs/performRunExecutionV1.server"; import { PerformRunExecutionV2Service } from "./runs/performRunExecutionV2.server"; @@ -20,8 +22,6 @@ import { ActivateSourceService } from "./sources/activateSource.server"; import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server"; import { PerformTaskOperationService } from "./tasks/performTaskOperation.server"; import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout"; -import { addMissingVersionField } from "@trigger.dev/core"; -import { logger } from "./logger.server"; const workerCatalog = { indexEndpoint: z.object({ @@ -30,6 +30,9 @@ const workerCatalog = { sourceData: z.any().optional(), reason: z.string().optional(), }), + performEndpointIndexing: z.object({ + id: z.string(), + }), scheduleEmail: DeliverEmailSchema, startRun: z.object({ id: z.string() }), processCallbackTimeout: z.object({ @@ -284,10 +287,17 @@ function getWorkerQueue() { maxAttempts: 7, handler: async (payload, job) => { const service = new IndexEndpointService(); - await service.call(payload.id, payload.source, payload.reason, payload.sourceData); }, }, + performEndpointIndexing: { + priority: 1, // smaller number = higher priority + maxAttempts: 7, + handler: async (payload, job) => { + const service = new PerformEndpointIndexService(); + await service.call(payload.id); + }, + }, deliverEvent: { priority: 0, // smaller number = higher priority maxAttempts: 5, diff --git a/apps/webapp/package.json b/apps/webapp/package.json index cf9f0b53e..564a85867 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -113,7 +113,8 @@ "tiny-invariant": "^1.2.0", "ulid": "^2.3.0", "zod": "3.22.3", - "zod-error": "1.5.0" + "zod-error": "1.5.0", + "zod-validation-error": "^1.5.0" }, "devDependencies": { "@remix-run/dev": "1.19.2-pre.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 0a65964f1..22f14b8ba 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -56,11 +56,13 @@ "test": "vitest" }, "dependencies": { + "@trigger.dev/core": "workspace:*", "@types/degit": "^2.8.3", "boxen": "^7.1.1", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^9.4.1", + "console-table-printer": "^2.11.2", "degit": "^2.8.4", "dotenv": "^16.3.1", "execa": "^7.0.0", @@ -74,6 +76,7 @@ "npm-check-updates": "^16.12.2", "openai": "^4.5.0", "ora": "^6.1.2", + "p-retry": "^6.1.0", "path-to-regexp": "^6.2.1", "posthog-node": "^3.1.1", "proxy-agent": "^6.3.0", diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index c6b1a20ad..9972ca221 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -1,3 +1,4 @@ +import boxen from "boxen"; import chalk from "chalk"; import childProcess from "child_process"; import chokidar from "chokidar"; @@ -5,10 +6,12 @@ import fs from "fs/promises"; import ngrok from "ngrok"; import { run as ncuRun } from "npm-check-updates"; import ora, { Ora } from "ora"; +import pRetry, { AbortError } from "p-retry"; import pathModule from "path"; import util from "util"; import { z } from "zod"; import { Framework, getFramework } from "../frameworks"; +import { standardWatchFilePaths, standardWatchIgnoreRegex } from "../frameworks/watchConfig"; import { telemetryClient } from "../telemetry/telemetry"; import { getEnvFilename } from "../utils/env"; import fetch from "../utils/fetchUseProxy"; @@ -17,8 +20,9 @@ import { getUserPackageManager } from "../utils/getUserPkgManager"; import { logger } from "../utils/logger"; import { resolvePath } from "../utils/parseNameAndPath"; import { RequireKeys } from "../utils/requiredKeys"; +import { Throttle } from "../utils/throttle"; import { TriggerApi } from "../utils/triggerApi"; -import { standardWatchIgnoreRegex, standardWatchFilePaths } from "../frameworks/watchConfig"; +import { wait } from "../utils/wait"; const asyncExecFile = util.promisify(childProcess.execFile); @@ -86,7 +90,7 @@ export async function devCommand(path: string, anyOptions: any) { const verifiedEndpoint = await verifyEndpoint(resolvedOptions, endpointId, apiKey, framework); if (!verifiedEndpoint) { logger.error( - `✖ [trigger.dev] Failed to find a valid Trigger.dev endpoint. Make sure your app is running and try again.` + `✖ [trigger.dev] Your endpoint couldn't be verified. Make sure your app is running and try again. ${resolvedOptions.handlerPath}` ); logger.info(` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port.`); telemetryClient.dev.failed("no_server_found", resolvedOptions); @@ -107,83 +111,6 @@ export async function devCommand(path: string, anyOptions: any) { const endpointHandlerUrl = `${endpointUrl}${handlerPath}`; telemetryClient.dev.tunnelRunning(path, resolvedOptions); - const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`); - - //refresh function - let hasConnected = false; - let attemptCount = 0; - const refresh = async () => { - connectingSpinner.start(); - - const refreshedEndpointId = await getEndpointIdFromPackageJson(resolvedPath, resolvedOptions); - - // Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL - const apiDetails = await getTriggerApiDetails(resolvedPath, resolvedOptions.envFile); - - if (!apiDetails) { - connectingSpinner.fail(`[trigger.dev] Failed to connect: Missing API Key`); - logger.info(`Will attempt again on the next file change…`); - attemptCount = 0; - return; - } - - const { apiKey, apiUrl } = apiDetails; - const apiClient = new TriggerApi(apiKey, apiUrl); - - const authorizedKey = await apiClient.whoami(apiKey); - if (!authorizedKey) { - logger.error( - `✖ [trigger.dev] The API key you provided is not authorized. Try visiting your dashboard to get a new API key.` - ); - - telemetryClient.dev.failed("invalid_api_key", resolvedOptions); - return; - } - - telemetryClient.identify( - authorizedKey.organization.id, - authorizedKey.project.id, - authorizedKey.userId - ); - - const result = await refreshEndpoint( - apiClient, - refreshedEndpointId ?? endpointId, - endpointHandlerUrl - ); - if (result.success) { - attemptCount = 0; - connectingSpinner.succeed( - `[trigger.dev] 🔄 Refreshed ${refreshedEndpointId ?? endpointId} ${formattedDate.format( - new Date(result.data.updatedAt) - )}` - ); - - if (!hasConnected) { - hasConnected = true; - telemetryClient.dev.connected(path, resolvedOptions); - } - } else { - attemptCount++; - - if (attemptCount === 10 || !result.retryable) { - connectingSpinner.fail(`Failed to connect: ${result.error}`); - logger.info(`Will attempt again on the next file change…`); - attemptCount = 0; - - if (!hasConnected) { - telemetryClient.dev.failed("failed_to_connect", resolvedOptions); - } - return; - } - - const delay = backoff(attemptCount); - // console.log(`Attempt: ${attemptCount}`, delay); - await wait(delay); - refresh(); - } - }; - // Watch for changes to files and refresh endpoints const watchPaths = (framework?.watchFilePaths ?? standardWatchFilePaths).map( (path) => `${resolvedPath}/${path}` @@ -195,12 +122,178 @@ export async function devCommand(path: string, anyOptions: any) { ignoreInitial: true, }); + const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`); + let hasConnected = false; + const abortController = new AbortController(); + + const r = () => { + refresh({ + endpointId, + spinner: connectingSpinner, + path: resolvedPath, + endpointHandlerUrl, + resolvedOptions, + hasConnected, + abortController, + }); + }; + + const throttle = new Throttle(r, throttleTimeMs); + watcher.on("all", (_event, _path) => { - throttle(refresh, throttleTimeMs); + throttle.call(); }); //Do initial refresh - throttle(refresh, throttleTimeMs); + throttle.call(); +} + +type RefreshOptions = { + spinner: Ora; + path: string; + endpointId: string; + endpointHandlerUrl: string; + resolvedOptions: ResolvedOptions; + hasConnected: boolean; + abortController: AbortController; +}; + +async function refresh(options: RefreshOptions) { + //stop any existing refreshes + options.abortController.abort(); + options.abortController = new AbortController(); + + // Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL + const apiDetails = await getTriggerApiDetails(options.path, options.resolvedOptions.envFile); + if (!apiDetails) { + options.spinner.fail("[trigger.dev] Failed to connect: Missing API Key"); + return; + } + + const { apiKey, apiUrl } = apiDetails; + const apiClient = new TriggerApi(apiKey, apiUrl); + + try { + const index = await pRetry(() => startIndexing({ ...options, apiClient }), { + retries: 5, + signal: options.abortController.signal, + maxTimeout: 5000, + }); + options.spinner.text = `[trigger.dev] Refreshing ${formattedDate.format(index.updatedAt)}`; + + if (!options.hasConnected) { + options.hasConnected = true; + telemetryClient.dev.connected(options.path, options.resolvedOptions); + } + + //this is for backwards-compatibility with older servers + if (index.id === undefined) { + options.spinner.succeed(`[trigger.dev] Refreshed ${formattedDate.format(index.updatedAt)}`); + return; + } + + //wait 750ms before attempting to get the indexing result + await wait(750); + + const indexResult = await pRetry(() => fetchIndexResult({ indexId: index.id, apiClient }), { + //this means we're polling, same distance between each attempt + factor: 1, + retries: 10, + signal: options.abortController.signal, + }); + + if (indexResult.status === "FAILURE") { + options.spinner.fail( + `[trigger.dev] Refreshing failed ${formattedDate.format(indexResult.updatedAt)}` + ); + logger.error( + boxen(indexResult.error.message, { + padding: 1, + borderStyle: "double", + }) + ); + return; + } + + options.spinner.succeed( + `[trigger.dev] Refreshed ${formattedDate.format(indexResult.updatedAt)}` + ); + } catch (e) { + if (e instanceof AbortError) { + options.spinner.fail(e.message); + logger.info(` [trigger.dev] Will attempt again on the next file change…`); + return; + } + + let message: string = ""; + if (e instanceof Error) { + message = e.message; + } else { + message = "Unknown error"; + } + + options.spinner.fail(message); + logger.info(` [trigger.dev] Will attempt again on the next file change…`); + + if (!options.hasConnected) { + telemetryClient.dev.failed("failed_to_connect", options.resolvedOptions); + } + } +} + +async function startIndexing({ + spinner, + path, + endpointId, + endpointHandlerUrl, + resolvedOptions, + apiClient, +}: RefreshOptions & { apiClient: TriggerApi }) { + spinner.start(); + + const refreshedEndpointId = await getEndpointIdFromPackageJson(path, resolvedOptions); + + const authorizedKey = await apiClient.whoami(); + if (!authorizedKey) { + telemetryClient.dev.failed("invalid_api_key", resolvedOptions); + throw new AbortError( + "[trigger.dev] The API key you provided is not authorized. Try visiting your dashboard to get a new API key." + ); + } + + telemetryClient.identify( + authorizedKey.organization.id, + authorizedKey.project.id, + authorizedKey.userId + ); + + const result = await refreshEndpoint( + apiClient, + refreshedEndpointId ?? endpointId, + endpointHandlerUrl + ); + + if (!result.success) { + throw new Error(result.error); + } + + return { id: result.data.endpointIndex?.id, updatedAt: new Date(result.data.updatedAt) }; +} + +async function fetchIndexResult({ + indexId, + apiClient, +}: { + indexId: string; + apiClient: TriggerApi; +}) { + const result = await apiClient.getEndpointIndex(indexId); + + if (result.status === "STARTED" || result.status === "PENDING") { + throw new Error("Indexing is still in progress"); + } + + return result; } async function resolveOptions( @@ -209,7 +302,7 @@ async function resolveOptions( unresolvedOptions: DevCommandOptions ): Promise { if (!framework) { - logger.info("Failed to detect framework, using default values"); + logger.info(" [trigger.dev] Failed to detect framework, using default values"); return { port: unresolvedOptions.port ?? 3000, hostname: unresolvedOptions.hostname ?? "localhost", @@ -431,25 +524,3 @@ async function refreshEndpoint(apiClient: TriggerApi, endpointId: string, endpoi } } } - -//wait function -async function wait(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -//throttle function -let throttleTimeout: NodeJS.Timeout | null = null; -function throttle(fn: () => any, delay: number) { - if (throttleTimeout) { - clearTimeout(throttleTimeout); - } - throttleTimeout = setTimeout(fn, delay); -} - -const maximum_backoff = 30; -const initial_backoff = 0.2; -function backoff(attempt: number) { - return Math.min((2 ^ attempt) * initial_backoff, maximum_backoff) * 1000; -} diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index bf934eed1..1c9ecacc5 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -81,7 +81,7 @@ export const initCommand = async (options: InitCommandOptions) => { } const apiClient = new TriggerApi(apiKey, optionsAfterPrompts.apiUrl); - const authorizedKey = await apiClient.whoami(apiKey); + const authorizedKey = await apiClient.whoami(); if (!authorizedKey) { logger.error( diff --git a/packages/cli/src/commands/whoami.ts b/packages/cli/src/commands/whoami.ts index afe71cbad..6a9e11801 100644 --- a/packages/cli/src/commands/whoami.ts +++ b/packages/cli/src/commands/whoami.ts @@ -42,7 +42,7 @@ export async function whoamiCommand(path: string, anyOptions: any) { } const triggerAPI = new TriggerApi(apiDetails.apiKey, apiDetails.apiUrl); - const userData = await triggerAPI.whoami(apiDetails.apiKey); + const userData = await triggerAPI.whoami(); loadingSpinner.stop(); diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index e971b8b95..20f091c94 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -13,4 +13,7 @@ export const logger = { success(...args: unknown[]) { console.log(chalk.green(...args)); }, + table(rows: any) { + console.table(rows); + }, }; diff --git a/packages/cli/src/utils/throttle.ts b/packages/cli/src/utils/throttle.ts new file mode 100644 index 000000000..e86341290 --- /dev/null +++ b/packages/cli/src/utils/throttle.ts @@ -0,0 +1,18 @@ +export class Throttle { + throttleTimeout: NodeJS.Timeout | null = null; + + constructor( + private readonly fn: () => any, + private readonly delay: number + ) { + this.fn = fn; + this.delay = delay; + } + + call() { + if (this.throttleTimeout) { + clearTimeout(this.throttleTimeout); + } + this.throttleTimeout = setTimeout(this.fn, this.delay); + } +} diff --git a/packages/cli/src/utils/triggerApi.ts b/packages/cli/src/utils/triggerApi.ts index 0f776f3ce..d900a00e4 100644 --- a/packages/cli/src/utils/triggerApi.ts +++ b/packages/cli/src/utils/triggerApi.ts @@ -1,5 +1,7 @@ import fetch from "./fetchUseProxy"; import { z } from "zod"; +import core from "@trigger.dev/core"; +const { GetEndpointIndexResponseSchema } = core; export type CreateEndpointOptions = { id: string; @@ -16,6 +18,9 @@ export type EndpointData = { createdAt: string; updatedAt: string; indexingHookIdentifier: string; + endpointIndex: { + id: string; + }; }; export type EndpointResponse = @@ -59,12 +64,12 @@ export class TriggerApi { private baseUrl: string ) {} - async whoami(apiKey: string): Promise { + async whoami(): Promise { const response = await fetch(`${this.baseUrl}/api/v1/whoami`, { method: "GET", headers: { Accept: "application/json", - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${this.apiKey}`, }, }); @@ -155,6 +160,33 @@ export class TriggerApi { data: data as any as EndpointData, }; } + + async getEndpointIndex(indexId: string) { + const response = await fetch(`${this.baseUrl}/api/v1/endpointindex/${indexId}`, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + }); + + if (response.ok) { + const body = await response.json(); + const parsed = GetEndpointIndexResponseSchema.safeParse(body); + + if (parsed.success) { + return parsed.data; + } + } + + return { + status: "FAILURE" as const, + error: { + message: `Bad response from Trigger.dev (${response.status})`, + }, + updatedAt: new Date(), + }; + } } function safeJsonParse(raw: string | null | undefined): unknown { diff --git a/packages/cli/src/utils/wait.ts b/packages/cli/src/utils/wait.ts new file mode 100644 index 000000000..75b9dc68d --- /dev/null +++ b/packages/cli/src/utils/wait.ts @@ -0,0 +1,5 @@ +export async function wait(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 31b1728d4..b765e2ade 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -9,7 +9,6 @@ "resolveJsonModule": true, "allowJs": true, "checkJs": true, - /* EMIT RULES */ "outDir": "./dist", "noEmit": true, // TSUP takes care of emitting js for us, in a MUCH faster way diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index 6abac4218..4e16cbfc5 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -296,6 +296,53 @@ export const IndexEndpointResponseSchema = z.object({ export type IndexEndpointResponse = z.infer; +export const EndpointIndexErrorSchema = z.object({ + message: z.string(), + raw: z.any().optional(), +}); + +export type EndpointIndexError = z.infer; + +const IndexEndpointStatsSchema = z.object({ + jobs: z.number(), + sources: z.number(), + dynamicTriggers: z.number(), + dynamicSchedules: z.number(), + disabledJobs: z.number().default(0), +}); + +export type IndexEndpointStats = z.infer; + +export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats | undefined { + if (stats === null || stats === undefined) { + return; + } + return IndexEndpointStatsSchema.parse(stats); +} + +export const GetEndpointIndexResponseSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("PENDING"), + updatedAt: z.coerce.date(), + }), + z.object({ + status: z.literal("STARTED"), + updatedAt: z.coerce.date(), + }), + z.object({ + status: z.literal("SUCCESS"), + stats: IndexEndpointStatsSchema, + updatedAt: z.coerce.date(), + }), + z.object({ + status: z.literal("FAILURE"), + error: EndpointIndexErrorSchema, + updatedAt: z.coerce.date(), + }), +]); + +export type GetEndpointIndexResponse = z.infer; + export const EndpointHeadersSchema = z.object({ "trigger-version": z.string().optional(), }); diff --git a/packages/database/package.json b/packages/database/package.json index 3fcb97517..6c82f4588 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -14,8 +14,9 @@ "scripts": { "generate": "prisma generate", "db:migrate:dev": "prisma migrate dev", + "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "prisma migrate deploy", "db:studio": "prisma studio", "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/packages/database/prisma/migrations/20231010115840_endpoint_index_status_added/migration.sql b/packages/database/prisma/migrations/20231010115840_endpoint_index_status_added/migration.sql new file mode 100644 index 000000000..746234fca --- /dev/null +++ b/packages/database/prisma/migrations/20231010115840_endpoint_index_status_added/migration.sql @@ -0,0 +1,11 @@ +-- CreateEnum +CREATE TYPE "EndpointIndexStatus" AS ENUM ('PENDING', 'STARTED', 'SUCCESS', 'FAILURE'); + +-- AlterTable +ALTER TABLE "EndpointIndex" +ADD COLUMN "status" "EndpointIndexStatus" NOT NULL DEFAULT 'PENDING'; + +-- Update all existing rows to be SUCCESS. This isn't correct because some of them have failed, but we don't want them to be PENDING. +UPDATE "EndpointIndex" +SET + "status" = 'SUCCESS'; \ No newline at end of file diff --git a/packages/database/prisma/migrations/20231010120458_endpoint_index_data_and_stats_are_now_optional/migration.sql b/packages/database/prisma/migrations/20231010120458_endpoint_index_data_and_stats_are_now_optional/migration.sql new file mode 100644 index 000000000..bbcb52084 --- /dev/null +++ b/packages/database/prisma/migrations/20231010120458_endpoint_index_data_and_stats_are_now_optional/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "EndpointIndex" ALTER COLUMN "data" DROP NOT NULL, +ALTER COLUMN "stats" DROP NOT NULL; diff --git a/packages/database/prisma/migrations/20231010135433_endpoint_index_added_error_column/migration.sql b/packages/database/prisma/migrations/20231010135433_endpoint_index_added_error_column/migration.sql new file mode 100644 index 000000000..6e24e4dca --- /dev/null +++ b/packages/database/prisma/migrations/20231010135433_endpoint_index_added_error_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EndpointIndex" ADD COLUMN "error" JSONB; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index a1f96966e..ddb34534f 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -391,9 +391,11 @@ model EndpointIndex { source EndpointIndexSource @default(MANUAL) sourceData Json? reason String? + status EndpointIndexStatus @default(PENDING) - data Json - stats Json + data Json? + stats Json? + error Json? } enum EndpointIndexSource { @@ -403,6 +405,13 @@ enum EndpointIndexSource { HOOK } +enum EndpointIndexStatus { + PENDING + STARTED + SUCCESS + FAILURE +} + model Job { id String @id @default(cuid()) slug String @@ -664,8 +673,8 @@ model EventRecord { } model JobRun { - id String @id @default(cuid()) - number Int + id String @id @default(cuid()) + number Int internal Boolean @default(false) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f9c3b44c..565a5ecfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,6 +198,7 @@ importers: ulid: ^2.3.0 zod: 3.22.3 zod-error: 1.5.0 + zod-validation-error: ^1.5.0 dependencies: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 @@ -284,6 +285,7 @@ importers: ulid: 2.3.0 zod: 3.22.3 zod-error: 1.5.0 + zod-validation-error: 1.5.0_zod@3.22.3 devDependencies: '@remix-run/dev': 1.19.2-pre.0_36n2i74sizt32vwpdxc4husnkq '@remix-run/eslint-config': 1.19.2-pre.0_ol4nhuzbuflsbzk2mijpqykzba @@ -659,6 +661,7 @@ importers: packages/cli: specifiers: + '@trigger.dev/core': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/degit': ^2.8.3 '@types/gradient-string': ^1.1.2 @@ -670,6 +673,7 @@ importers: chalk: ^5.2.0 chokidar: ^3.5.3 commander: ^9.4.1 + console-table-printer: ^2.11.2 degit: ^2.8.4 dotenv: ^16.3.1 execa: ^7.0.0 @@ -683,6 +687,7 @@ importers: npm-check-updates: ^16.12.2 openai: ^4.5.0 ora: ^6.1.2 + p-retry: ^6.1.0 path-to-regexp: ^6.2.1 posthog-node: ^3.1.1 proxy-agent: ^6.3.0 @@ -697,11 +702,13 @@ importers: vitest: ^0.34.4 zod: 3.22.3 dependencies: + '@trigger.dev/core': link:../core '@types/degit': 2.8.3 boxen: 7.1.1 chalk: 5.2.0 chokidar: 3.5.3 commander: 9.5.0 + console-table-printer: 2.11.2 degit: 2.8.4 dotenv: 16.3.1 execa: 7.0.0 @@ -715,6 +722,7 @@ importers: npm-check-updates: 16.12.3 openai: 4.5.0 ora: 6.1.2 + p-retry: 6.1.0 path-to-regexp: 6.2.1 posthog-node: 3.1.1 proxy-agent: 6.3.0 @@ -13840,6 +13848,10 @@ packages: resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} dev: false + /@types/retry/0.12.2: + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + dev: false + /@types/scheduler/0.16.2: resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} @@ -15614,7 +15626,7 @@ packages: /axios/0.21.4_debug@4.3.2: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2_debug@4.3.2 + follow-redirects: 1.15.2 transitivePeerDependencies: - debug dev: false @@ -16972,6 +16984,12 @@ packages: /console-control-strings/1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + /console-table-printer/2.11.2: + resolution: {integrity: sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==} + dependencies: + simple-wcswidth: 1.0.1 + dev: false + /content-disposition/0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -20411,18 +20429,6 @@ packages: debug: optional: true - /follow-redirects/1.15.2_debug@4.3.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dependencies: - debug: 4.3.2 - dev: false - /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -22174,6 +22180,11 @@ packages: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + /is-network-error/1.0.0: + resolution: {integrity: sha512-P3fxi10Aji2FZmHTrMPSNFbNC6nnp4U5juPAIjXPHkUNubi4+qK7vvdsaNpAUwXslhYm9oyjEYTxs1xd/+Ph0w==} + engines: {node: '>=16'} + dev: false + /is-node-process/1.0.1: resolution: {integrity: sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==} dev: true @@ -25802,6 +25813,15 @@ packages: retry: 0.13.1 dev: false + /p-retry/6.1.0: + resolution: {integrity: sha512-fJLEQ2KqYBJRuaA/8cKMnqhulqNM+bpcjYtXNex2t3mOXKRYPitAJt9NacSf8XAFzcYahSAbKpobiWDSqHSh2g==} + engines: {node: '>=16.17'} + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.0.0 + retry: 0.13.1 + dev: false + /p-timeout/3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} @@ -28780,6 +28800,10 @@ packages: semver: 7.5.4 dev: true + /simple-wcswidth/1.0.1: + resolution: {integrity: sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==} + dev: false + /simplur/3.0.1: resolution: {integrity: sha512-bBAoTn75tuKh83opmZ1VoyVoQIsvLCKzSxuasAxbnKofrT8eGyOEIaXSuNfhi/hI160+fwsR7ObcbBpOyzDvXg==} dev: false @@ -32595,6 +32619,15 @@ packages: zod: 3.22.3 dev: false + /zod-validation-error/1.5.0_zod@3.22.3: + resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==} + engines: {node: '>=16.0.0'} + peerDependencies: + zod: ^3.18.0 + dependencies: + zod: 3.22.3 + dev: false + /zod/3.21.1: resolution: {integrity: sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA==} diff --git a/references/job-catalog/package.json b/references/job-catalog/package.json index 3f9c45b07..0fcca5422 100644 --- a/references/job-catalog/package.json +++ b/references/job-catalog/package.json @@ -26,6 +26,7 @@ "byo-auth": "nodemon --watch src/byo-auth.ts -r tsconfig-paths/register -r dotenv/config src/byo-auth.ts", "redacted": "nodemon --watch src/redacted.ts -r tsconfig-paths/register -r dotenv/config src/redacted.ts", "replicate": "nodemon --watch src/replicate.ts -r tsconfig-paths/register -r dotenv/config src/replicate.ts", + "misconfigured": "nodemon --watch src/misconfigured.ts -r tsconfig-paths/register -r dotenv/config src/misconfigured.ts", "dev:trigger": "trigger-cli dev --port 8080" }, "dependencies": { @@ -60,4 +61,4 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^3.14.1" } -} \ No newline at end of file +} diff --git a/references/job-catalog/src/misconfigured.ts b/references/job-catalog/src/misconfigured.ts new file mode 100644 index 000000000..ba60910ab --- /dev/null +++ b/references/job-catalog/src/misconfigured.ts @@ -0,0 +1,35 @@ +import { createExpressServer } from "@trigger.dev/express"; +import { TriggerClient, intervalTrigger } from "@trigger.dev/sdk"; + +export const client = new TriggerClient({ + id: "job-catalog", + apiKey: process.env["TRIGGER_API_KEY"], + apiUrl: process.env["TRIGGER_API_URL"], + verbose: false, + ioLogLocalEnabled: true, +}); + +// This job is misconfigured because the interval trigger is less than 60s +client.defineJob({ + id: "bad-interval", + name: "Bad Interval", + version: "0.0.2", + trigger: intervalTrigger({ + seconds: 50, + }), + run: async (payload, io, ctx) => {}, +}); + +// This job is misconfigured because it has no name +//@ts-ignore +client.defineJob({ + id: "bad-cron", + // name: "Bad CRON expression", + version: "0.0.2", + trigger: intervalTrigger({ + seconds: 90, + }), + run: async (payload, io, ctx) => {}, +}); + +createExpressServer(client); From 659be8cbc12d5c7c2ab437f2d444734d2b020b7f Mon Sep 17 00:00:00 2001 From: D-K-P Date: Thu, 12 Oct 2023 13:41:15 +0100 Subject: [PATCH 63/79] Added Hacktoberfest info to our readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 89a5171a4..609dfd8ab 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,18 @@
+# ✨🎃 Get involved with Hacktoberfest 2023! 🎃✨ + +All of October we're participating in Hacktoberfest and invite you to join us! We have a bunch of issues labeled `🎃 Hacktoberfest` that are ready for you to work on which will count towards Hacktoberfest. We are also running our own game, earn 💎 points to win swag! + +- Check out our [Hacktoberfest landing page](https://trigger.dev/hacktoberfest) for how to participate and win swag. +- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag. +- Join our [Discord](https://discord.gg/JtBAxBr2m3) and get involved in with the community. + +_New to Hacktober? Check out the [Hacktoberfest website](https://hacktoberfest.digitalocean.com/) for more information._ + +🎃 **Happy Hacking!** 🎃 + # About Trigger.dev Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays. From a25751e8054436a9a932903825ae81fd162cac95 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 12 Oct 2023 16:16:20 +0100 Subject: [PATCH 64/79] Added some better messaging to the login pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Better login page title and subtitle - Better title and new ‘having issues’ panel on the magic link pages --- apps/webapp/app/routes/login._index/route.tsx | 5 ++++- apps/webapp/app/routes/login.magic/route.tsx | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/login._index/route.tsx b/apps/webapp/app/routes/login._index/route.tsx index 20bab4c24..6d3549977 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -60,7 +60,10 @@ export default function LoginPage() { - + + + Create an account or login +
{data.showGithubAuth && ( diff --git a/apps/webapp/app/routes/login.magic/route.tsx b/apps/webapp/app/routes/login.magic/route.tsx index 34bd5462b..4176a64ad 100644 --- a/apps/webapp/app/routes/login.magic/route.tsx +++ b/apps/webapp/app/routes/login.magic/route.tsx @@ -17,7 +17,6 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { authenticator } from "~/services/auth.server"; import { commitSession, getUserSession } from "~/services/sessionStorage.server"; import magicLinkIcon from "./login.magic.svg"; - import type { LoaderType as RootLoader } from "~/root"; import { appEnvTitleTag } from "~/utils"; import { TextLink } from "~/components/primitives/TextLink"; @@ -116,7 +115,10 @@ export default function LoginMagicLinkPage() { ) : ( <> - + + + Create an account or login using your email +
@@ -167,6 +169,22 @@ export default function LoginMagicLinkPage() { )} +
+ + Having login issues? + + + Ensure the Magic Link email isn't in your spam folder. If the problem persists,{" "} + + drop us an email + {" "} + or let us know on{" "} + + Discord + + . + +
From 5d45dea0e16cbef982e1081e23b37319909bb1b7 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Oct 2023 16:45:05 +0100 Subject: [PATCH 65/79] Added some logging when sending magic link emails --- apps/webapp/app/services/email.server.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/services/email.server.ts b/apps/webapp/app/services/email.server.ts index 905c94d56..04672d8be 100644 --- a/apps/webapp/app/services/email.server.ts +++ b/apps/webapp/app/services/email.server.ts @@ -6,6 +6,7 @@ import { env } from "~/env.server"; import type { User } from "~/models/user.server"; import type { AuthUser } from "./authUser"; import { workerQueue } from "./worker.server"; +import { logger } from "./logger.server"; const client = new EmailClient({ apikey: env.RESEND_API_KEY, @@ -20,11 +21,18 @@ export async function sendMagicLinkEmail(options: SendEmailOptions): P throw redirect(options.magicLink); } - return client.send({ - email: "magic_link", - to: options.emailAddress, - magicLink: options.magicLink, - }); + logger.debug("Sending magic link email", { emailAddress: options.emailAddress }); + + try { + return await client.send({ + email: "magic_link", + to: options.emailAddress, + magicLink: options.magicLink, + }); + } catch (error) { + logger.error("Error sending magic link email", { error: JSON.stringify(error) }); + throw error; + } } export async function sendPlainTextEmail(options: SendPlainTextOptions) { From 3d387d602d2c3371724f5345d14be53eaf07e8eb Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Oct 2023 16:48:51 +0100 Subject: [PATCH 66/79] Added additional log after a magic link user has done verification --- apps/webapp/app/services/emailAuth.server.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/services/emailAuth.server.tsx b/apps/webapp/app/services/emailAuth.server.tsx index ff9ea4813..015b6fbc9 100644 --- a/apps/webapp/app/services/emailAuth.server.tsx +++ b/apps/webapp/app/services/emailAuth.server.tsx @@ -5,6 +5,7 @@ import { findOrCreateUser } from "~/models/user.server"; import { env } from "~/env.server"; import { sendMagicLinkEmail } from "~/services/email.server"; import { postAuthentication } from "./postAuth.server"; +import { logger } from "./logger.server"; let secret = env.MAGIC_LINK_SECRET; if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable."); @@ -35,6 +36,7 @@ const emailStrategy = new EmailLinkStrategy( return { userId: user.id }; } catch (error) { + logger.debug("Magic link user failed to authenticate", { error: JSON.stringify(error) }); throw error; } } From 0c207566245d80c2dd2952ad826213f126e7f8f2 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 12 Oct 2023 17:06:06 +0100 Subject: [PATCH 67/79] Remove reporter because of https://github.com/graphile/worker/issues/349 --- apps/webapp/app/services/worker.server.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 7b6d9e6d8..9e048b123 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -137,9 +137,6 @@ function getWorkerQueue() { ttl: 7 * 24 * 60 * 60 * 1000, // 7 days maxCount: 1000, }, - reporter: async (event, properties) => { - logger.info("workerQueue report", { event, properties }); - }, runnerOptions: { connectionString: env.DATABASE_URL, concurrency: env.WORKER_CONCURRENCY, From 6b8b1c7caed4ea18e7ac136169d2251d785f88f9 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 12 Oct 2023 16:31:48 +0100 Subject: [PATCH 68/79] Added data-action tracking to all the login buttons --- apps/webapp/app/routes/login._index/route.tsx | 14 ++++++++++++-- apps/webapp/app/routes/login.magic/route.tsx | 9 ++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/login._index/route.tsx b/apps/webapp/app/routes/login._index/route.tsx index 6d3549977..ed85ab189 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -67,12 +67,22 @@ export default function LoginPage() {
{data.showGithubAuth && ( - )} - + Re-enter email } confirmButton={ - + Log in using another option } @@ -139,6 +144,7 @@ export default function LoginMagicLinkPage() { variant="primary/medium" disabled={isLoading} fullWidth + data-action="send a magic link" > All login options From c6784ba7b879102b0e72c6c085e7d226b320680c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Oct 2023 18:12:15 +0100 Subject: [PATCH 69/79] Added logging when the magic link authentication function is called --- apps/webapp/app/services/emailAuth.server.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/services/emailAuth.server.tsx b/apps/webapp/app/services/emailAuth.server.tsx index 015b6fbc9..e48d405dd 100644 --- a/apps/webapp/app/services/emailAuth.server.tsx +++ b/apps/webapp/app/services/emailAuth.server.tsx @@ -26,6 +26,8 @@ const emailStrategy = new EmailLinkStrategy( form: FormData; magicLinkVerify: boolean; }) => { + logger.info("Magic link user authenticated", { email, magicLinkVerify }); + try { const { user, isNewUser } = await findOrCreateUser({ email, From d6b44d13f78e67dc987ef2fd85ebdc22fa9f0312 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 12 Oct 2023 18:15:46 +0100 Subject: [PATCH 70/79] Show magic link errors in the UI, switch to useNavigation hook, and return using the authenticator --- apps/webapp/app/routes/login.magic/route.tsx | 38 ++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/routes/login.magic/route.tsx b/apps/webapp/app/routes/login.magic/route.tsx index fb1ece192..5bcb380b3 100644 --- a/apps/webapp/app/routes/login.magic/route.tsx +++ b/apps/webapp/app/routes/login.magic/route.tsx @@ -1,6 +1,6 @@ import type { ActionArgs, LoaderArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; -import { Form, useTransition } from "@remix-run/react"; +import { Form, useNavigation, useTransition } from "@remix-run/react"; import { TypedMetaFunction, typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { LogoIcon } from "~/components/LogoIcon"; @@ -20,6 +20,7 @@ import magicLinkIcon from "./login.magic.svg"; import type { LoaderType as RootLoader } from "~/root"; import { appEnvTitleTag } from "~/utils"; import { TextLink } from "~/components/primitives/TextLink"; +import { FormError } from "~/components/primitives/FormError"; export const meta: TypedMetaFunction = ({ parentsData }) => ({ title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`, @@ -31,10 +32,26 @@ export async function loader({ request }: LoaderArgs) { }); const session = await getUserSession(request); + const error = session.get("auth:error"); - return typedjson({ - magicLinkSent: session.has("triggerdotdev:magiclink"), - }); + let magicLinkError: string | undefined; + if (error) { + if ("message" in error) { + magicLinkError = error.message; + } else { + magicLinkError = JSON.stringify(error, null, 2); + } + } + + return typedjson( + { + magicLinkSent: session.has("triggerdotdev:magiclink"), + magicLinkError, + }, + { + headers: { "Set-Cookie": await commitSession(session) }, + } + ); } export async function action({ request }: ActionArgs) { @@ -49,7 +66,7 @@ export async function action({ request }: ActionArgs) { .parse(payload); if (action === "send") { - await authenticator.authenticate("email-link", request, { + return authenticator.authenticate("email-link", request, { successRedirect: "/login/magic", failureRedirect: "/login/magic", }); @@ -66,13 +83,13 @@ export async function action({ request }: ActionArgs) { } export default function LoginMagicLinkPage() { - const { magicLinkSent } = useTypedLoaderData(); - const transition = useTransition(); + const { magicLinkSent, magicLinkError } = useTypedLoaderData(); + const navigate = useNavigation(); const isLoading = - (transition.state === "loading" || transition.state === "submitting") && - transition.type === "actionSubmission" && - transition.submission.formData.get("action") === "send"; + (navigate.state === "loading" || navigate.state === "submitting") && + navigate.formAction !== undefined && + navigate.formData?.get("action") === "send"; return ( @@ -152,6 +169,7 @@ export default function LoginMagicLinkPage() { /> {isLoading ? "Sending…" : "Send a magic link"} + {magicLinkError && {magicLinkError}}
By logging in with your email you agree to our{" "} From 375b788ab091947cd1df933557d71f095ac9c736 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 13 Oct 2023 09:23:05 +0100 Subject: [PATCH 71/79] Add row locking with FOR UPDATE in delete query --- apps/webapp/app/platform/zodWorker.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index 564a1fa06..a275ad9e0 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -516,7 +516,7 @@ export class ZodWorker { }); const rawResults = await this.#prisma.$queryRawUnsafe( - `WITH rows AS (SELECT id FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts ORDER BY run_at ASC LIMIT $2) DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE id IN (SELECT id FROM rows) RETURNING id`, + `WITH rows AS (SELECT id FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts LIMIT $2 FOR UPDATE) DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE id IN (SELECT id FROM rows) RETURNING id`, expirationDate, this.#cleanup.maxCount ); From 29ca09e42df3a8e5a34e00054ccc808826ca645d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 13 Oct 2023 09:38:25 +0100 Subject: [PATCH 72/79] Fixes #611: intervalTriggers of >10 never starting --- .../schedules/nextScheduledEvent.server.ts | 3 ++- .../migration.sql | 2 ++ packages/database/prisma/schema.prisma | 1 + references/job-catalog/src/schedules.ts | 21 +++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/database/prisma/migrations/20231013083144_add_next_event_timestamp_to_schedule_source/migration.sql diff --git a/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts b/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts index 85f0f9811..762227624 100644 --- a/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts +++ b/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts @@ -36,7 +36,7 @@ export class NextScheduledEventService { const scheduleTime = calculateNextScheduledEvent( schedule.data, - scheduleSource.lastEventTimestamp + scheduleSource.lastEventTimestamp ?? scheduleSource.createdAt ); logger.debug("enqueuing scheduled event", { @@ -67,6 +67,7 @@ export class NextScheduledEventService { }, data: { workerJobId: workerJob.id, + nextEventTimestamp: scheduleTime, }, }); diff --git a/packages/database/prisma/migrations/20231013083144_add_next_event_timestamp_to_schedule_source/migration.sql b/packages/database/prisma/migrations/20231013083144_add_next_event_timestamp_to_schedule_source/migration.sql new file mode 100644 index 000000000..ed5a2b48c --- /dev/null +++ b/packages/database/prisma/migrations/20231013083144_add_next_event_timestamp_to_schedule_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ScheduleSource" ADD COLUMN "nextEventTimestamp" TIMESTAMP(3); diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index ddb34534f..7f1d21918 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -1049,6 +1049,7 @@ model ScheduleSource { dispatcherId String lastEventTimestamp DateTime? + nextEventTimestamp DateTime? workerJobId String? diff --git a/references/job-catalog/src/schedules.ts b/references/job-catalog/src/schedules.ts index 501798c2e..89a687cb1 100644 --- a/references/job-catalog/src/schedules.ts +++ b/references/job-catalog/src/schedules.ts @@ -31,6 +31,27 @@ client.defineJob({ }, }); +client.defineJob({ + id: "schedule-example-2", + name: "Schedule Example 2", + version: "1.0.0", + enabled: true, + trigger: intervalTrigger({ + seconds: 60 * 30, // 30 minutes + }), + run: async (payload, io, ctx) => { + await io.runTask("task-example-1", async () => { + return { + message: "Hello World", + }; + }); + + await io.wait("wait-1", 1); + + await io.logger.info("Hello World", { ctx }); + }, +}); + const resend = new Resend({ id: "resend-client", apiKey: process.env.RESEND_API_KEY!, From b697a199547ef77ba991ecd93de89a8caca79a4e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 13:21:10 +0100 Subject: [PATCH 73/79] chore: Update version for release (#579) Co-authored-by: github-actions[bot] --- .changeset/chilled-plants-exercise.md | 14 -------------- .changeset/cool-stingrays-invent.md | 5 ----- .changeset/neat-feet-wait.md | 5 ----- .changeset/seven-turkeys-worry.md | 6 ------ .changeset/sharp-lobsters-cross.md | 6 ------ .changeset/tidy-toys-turn.md | 5 ----- .changeset/tiny-candles-type.md | 5 ----- integrations/airtable/CHANGELOG.md | 8 ++++++++ integrations/airtable/package.json | 6 +++--- integrations/github/CHANGELOG.md | 8 ++++++++ integrations/github/package.json | 6 +++--- integrations/linear/CHANGELOG.md | 8 ++++++++ integrations/linear/package.json | 6 +++--- integrations/openai/CHANGELOG.md | 8 ++++++++ integrations/openai/package.json | 6 +++--- integrations/plain/CHANGELOG.md | 8 ++++++++ integrations/plain/package.json | 6 +++--- integrations/replicate/CHANGELOG.md | 8 ++++++++ integrations/replicate/package.json | 6 +++--- integrations/resend/CHANGELOG.md | 9 +++++++++ integrations/resend/package.json | 6 +++--- integrations/sendgrid/CHANGELOG.md | 8 ++++++++ integrations/sendgrid/package.json | 6 +++--- integrations/slack/CHANGELOG.md | 7 +++++++ integrations/slack/package.json | 4 ++-- integrations/stripe/CHANGELOG.md | 8 ++++++++ integrations/stripe/package.json | 6 +++--- integrations/supabase/CHANGELOG.md | 8 ++++++++ integrations/supabase/package.json | 6 +++--- integrations/typeform/CHANGELOG.md | 8 ++++++++ integrations/typeform/package.json | 6 +++--- packages/astro/CHANGELOG.md | 11 +++++++++++ packages/astro/package.json | 4 ++-- packages/cli/CHANGELOG.md | 17 +++++++++++++++++ packages/cli/package.json | 2 +- packages/core/CHANGELOG.md | 11 +++++++++++ packages/core/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 6 ++++++ packages/eslint-plugin/package.json | 2 +- packages/express/CHANGELOG.md | 11 +++++++++++ packages/express/package.json | 6 +++--- packages/integration-kit/CHANGELOG.md | 6 ++++++ packages/integration-kit/package.json | 2 +- packages/nestjs/CHANGELOG.md | 11 +++++++++++ packages/nestjs/package.json | 6 +++--- packages/nextjs/CHANGELOG.md | 11 +++++++++++ packages/nextjs/package.json | 4 ++-- packages/react/CHANGELOG.md | 9 +++++++++ packages/react/package.json | 4 ++-- packages/remix/CHANGELOG.md | 7 +++++++ packages/remix/package.json | 4 ++-- packages/sveltekit/CHANGELOG.md | 13 +++++++++++++ packages/sveltekit/package.json | 4 ++-- packages/testing/CHANGELOG.md | 10 ++++++++++ packages/testing/package.json | 2 +- packages/trigger-sdk/CHANGELOG.md | 13 +++++++++++++ packages/trigger-sdk/package.json | 4 ++-- 57 files changed, 290 insertions(+), 104 deletions(-) delete mode 100644 .changeset/chilled-plants-exercise.md delete mode 100644 .changeset/cool-stingrays-invent.md delete mode 100644 .changeset/neat-feet-wait.md delete mode 100644 .changeset/seven-turkeys-worry.md delete mode 100644 .changeset/sharp-lobsters-cross.md delete mode 100644 .changeset/tidy-toys-turn.md delete mode 100644 .changeset/tiny-candles-type.md create mode 100644 packages/sveltekit/CHANGELOG.md diff --git a/.changeset/chilled-plants-exercise.md b/.changeset/chilled-plants-exercise.md deleted file mode 100644 index dd39f4537..000000000 --- a/.changeset/chilled-plants-exercise.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@trigger.dev/integration-kit": minor -"@trigger.dev/eslint-plugin": minor -"@trigger.dev/sdk": minor -"@trigger.dev/sveltekit": minor -"@trigger.dev/express": minor -"@trigger.dev/nestjs": minor -"@trigger.dev/nextjs": minor -"@trigger.dev/astro": minor -"@trigger.dev/core": minor -"@trigger.dev/cli": minor ---- - -Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. diff --git a/.changeset/cool-stingrays-invent.md b/.changeset/cool-stingrays-invent.md deleted file mode 100644 index f283a24fd..000000000 --- a/.changeset/cool-stingrays-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/cli": patch ---- - -allow users to update trigger-dev packages to a specific version diff --git a/.changeset/neat-feet-wait.md b/.changeset/neat-feet-wait.md deleted file mode 100644 index ab385fe37..000000000 --- a/.changeset/neat-feet-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/sveltekit": patch ---- - -SvelteKit adaptor package diff --git a/.changeset/seven-turkeys-worry.md b/.changeset/seven-turkeys-worry.md deleted file mode 100644 index 9165ca4e2..000000000 --- a/.changeset/seven-turkeys-worry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@trigger.dev/core": patch -"@trigger.dev/cli": patch ---- - -When indexing user's jobs errors are now stored and displayed diff --git a/.changeset/sharp-lobsters-cross.md b/.changeset/sharp-lobsters-cross.md deleted file mode 100644 index 97f6345cd..000000000 --- a/.changeset/sharp-lobsters-cross.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@trigger.dev/resend": patch -"@trigger.dev/core": patch ---- - -Allow task property values to be blank, but strip them out before persisting them diff --git a/.changeset/tidy-toys-turn.md b/.changeset/tidy-toys-turn.md deleted file mode 100644 index 2f9a60e27..000000000 --- a/.changeset/tidy-toys-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/cli": patch ---- - -allow CLI dev to use injected environment variable diff --git a/.changeset/tiny-candles-type.md b/.changeset/tiny-candles-type.md deleted file mode 100644 index 8afa8b23d..000000000 --- a/.changeset/tiny-candles-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/cli": patch ---- - -remove unused envFile param in sendEvent.ts cmd diff --git a/integrations/airtable/CHANGELOG.md b/integrations/airtable/CHANGELOG.md index 20c708da1..ba4997aab 100644 --- a/integrations/airtable/CHANGELOG.md +++ b/integrations/airtable/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/airtable +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/airtable/package.json b/integrations/airtable/package.json index ab2a0e303..403c2bf95 100644 --- a/integrations/airtable/package.json +++ b/integrations/airtable/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/airtable", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev integration for airtable", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "airtable": "^0.12.1", "zod": "3.22.3" }, diff --git a/integrations/github/CHANGELOG.md b/integrations/github/CHANGELOG.md index 8179b15ec..405bcc8c9 100644 --- a/integrations/github/CHANGELOG.md +++ b/integrations/github/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/github +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/github/package.json b/integrations/github/package.json index 2f3a27f90..d9d9f641d 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/github", - "version": "2.1.9", + "version": "2.2.0", "description": "The official GitHub integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,8 +29,8 @@ "@octokit/request": "^6.2.5", "@octokit/request-error": "^4.0.1", "@octokit/webhooks": "^10.4.0", - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "octokit": "^2.0.14", "zod": "3.22.3" }, diff --git a/integrations/linear/CHANGELOG.md b/integrations/linear/CHANGELOG.md index 14c507d61..25c7d6a61 100644 --- a/integrations/linear/CHANGELOG.md +++ b/integrations/linear/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/linear +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/linear/package.json b/integrations/linear/package.json index 94c63d85a..909c88ee3 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/linear", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev integration for @linear/sdk", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@linear/sdk": "^8.0.0", - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "zod": "3.22.3" }, "engines": { diff --git a/integrations/openai/CHANGELOG.md b/integrations/openai/CHANGELOG.md index ae5647a54..c9fdbb7df 100644 --- a/integrations/openai/CHANGELOG.md +++ b/integrations/openai/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/slack +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/openai/package.json b/integrations/openai/package.json index 50e5dd696..ccd647c9d 100644 --- a/integrations/openai/package.json +++ b/integrations/openai/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/openai", - "version": "2.1.9", + "version": "2.2.0", "description": "The official OpenAI integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,8 +25,8 @@ }, "dependencies": { "openai": "^4.2.0", - "@trigger.dev/sdk": "workspace:^2.1.9", - "@trigger.dev/integration-kit": "workspace:^2.1.9" + "@trigger.dev/sdk": "workspace:^2.2.0", + "@trigger.dev/integration-kit": "workspace:^2.2.0" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/plain/CHANGELOG.md b/integrations/plain/CHANGELOG.md index bb7b0877e..b3beded98 100644 --- a/integrations/plain/CHANGELOG.md +++ b/integrations/plain/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/plain +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/plain/package.json b/integrations/plain/package.json index 955778510..846dc2376 100644 --- a/integrations/plain/package.json +++ b/integrations/plain/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/plain", - "version": "2.1.9", + "version": "2.2.0", "description": "The official Plain.com integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,8 +24,8 @@ "build:tsup": "tsup" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "@team-plain/typescript-sdk": "^2.7.0" }, "engines": { diff --git a/integrations/replicate/CHANGELOG.md b/integrations/replicate/CHANGELOG.md index 500653bc9..be38c5d7b 100644 --- a/integrations/replicate/CHANGELOG.md +++ b/integrations/replicate/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/replicate +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/replicate/package.json b/integrations/replicate/package.json index 400c01310..41c4f76a9 100644 --- a/integrations/replicate/package.json +++ b/integrations/replicate/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/replicate", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev integration for replicate", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "replicate": "^0.18.1", "zod": "3.22.3" }, diff --git a/integrations/resend/CHANGELOG.md b/integrations/resend/CHANGELOG.md index 76b2223f6..a77c82ce2 100644 --- a/integrations/resend/CHANGELOG.md +++ b/integrations/resend/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/resend +## 2.2.0 + +### Patch Changes + +- 59a94c71: Allow task property values to be blank, but strip them out before persisting them +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/resend/package.json b/integrations/resend/package.json index a5b68952a..87a825869 100644 --- a/integrations/resend/package.json +++ b/integrations/resend/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/resend", - "version": "2.1.9", + "version": "2.2.0", "description": "The official Resend.com integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,8 +24,8 @@ "build:tsup": "tsup" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "resend": "^1.0.0" }, "engines": { diff --git a/integrations/sendgrid/CHANGELOG.md b/integrations/sendgrid/CHANGELOG.md index 1a7266733..9086f33bd 100644 --- a/integrations/sendgrid/CHANGELOG.md +++ b/integrations/sendgrid/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/sendgrid +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/sendgrid/package.json b/integrations/sendgrid/package.json index 38c628a98..82a18cd82 100644 --- a/integrations/sendgrid/package.json +++ b/integrations/sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sendgrid", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev integration for @sendgrid/mail", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@sendgrid/mail": "^7.7.0", - "@trigger.dev/sdk": "workspace:^2.1.9", - "@trigger.dev/integration-kit": "workspace:^2.1.9" + "@trigger.dev/sdk": "workspace:^2.2.0", + "@trigger.dev/integration-kit": "workspace:^2.2.0" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/slack/CHANGELOG.md b/integrations/slack/CHANGELOG.md index 0e52fb368..8f52872fa 100644 --- a/integrations/slack/CHANGELOG.md +++ b/integrations/slack/CHANGELOG.md @@ -1,5 +1,12 @@ # @trigger.dev/slack +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/slack/package.json b/integrations/slack/package.json index a0b698ddd..2e6238277 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/slack", - "version": "2.1.9", + "version": "2.2.0", "description": "The official Slack integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@slack/web-api": "^6.8.1", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.2.0", "zod": "3.22.3" }, "engines": { diff --git a/integrations/stripe/CHANGELOG.md b/integrations/stripe/CHANGELOG.md index b52b2c2d8..464c73e3c 100644 --- a/integrations/stripe/CHANGELOG.md +++ b/integrations/stripe/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/stripe +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/stripe/package.json b/integrations/stripe/package.json index 02df13289..75e7ce2f1 100644 --- a/integrations/stripe/package.json +++ b/integrations/stripe/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/stripe", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev integration for stripe", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,8 +26,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "stripe": "^12.14.0", "zod": "3.22.3" }, diff --git a/integrations/supabase/CHANGELOG.md b/integrations/supabase/CHANGELOG.md index 70c8fa887..6206167ed 100644 --- a/integrations/supabase/CHANGELOG.md +++ b/integrations/supabase/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/supabase +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/supabase/package.json b/integrations/supabase/package.json index aa8be7d0f..1bf316418 100644 --- a/integrations/supabase/package.json +++ b/integrations/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/supabase", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev integration for @supabase/supabase-js", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,8 +27,8 @@ }, "dependencies": { "@supabase/supabase-js": "^2.26.0", - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "supabase-management-js": "^0.1.4", "zod": "3.22.3" }, diff --git a/integrations/typeform/CHANGELOG.md b/integrations/typeform/CHANGELOG.md index 90016bd04..9f89d3fae 100644 --- a/integrations/typeform/CHANGELOG.md +++ b/integrations/typeform/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/typeform +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/integration-kit@2.2.0 + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/integrations/typeform/package.json b/integrations/typeform/package.json index 13eb35c33..689ba1ef4 100644 --- a/integrations/typeform/package.json +++ b/integrations/typeform/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/typeform", - "version": "2.1.9", + "version": "2.2.0", "description": "The official Typeform integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -25,8 +25,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.9", - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/integration-kit": "workspace:^2.2.0", + "@trigger.dev/sdk": "workspace:^2.2.0", "@typeform/api-client": "^1.8.0", "zod": "3.22.3" }, diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 443e79831..840cd2888 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/astro +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 9f3066cd7..6f8bf02e2 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,7 +1,7 @@ { "name": "@trigger.dev/astro", "description": "An Astro-native integration for Trigger.dev background jobs platform", - "version": "2.1.9", + "version": "2.2.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ @@ -20,7 +20,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.9" + "@trigger.dev/sdk": "workspace:^2.2.0" }, "devDependencies": { "astro": "^3.0.12", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 4a61f3d35..a2b8fe86b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,22 @@ # create-trigger +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- c070e78d: allow users to update trigger-dev packages to a specific version +- 50e3d9e4: When indexing user's jobs errors are now stored and displayed +- be9b5113: allow CLI dev to use injected environment variable +- b12bc604: remove unused envFile param in sendEvent.ts cmd +- Updated dependencies [975c5f1d] +- Updated dependencies [50e3d9e4] +- Updated dependencies [59a94c71] + - @trigger.dev/core@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 22f14b8ba..23436472d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/cli", - "version": "2.1.9", + "version": "2.2.0", "description": "The Trigger.dev CLI", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 068c67d4c..8cce62069 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,16 @@ # internal-platform +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- 50e3d9e4: When indexing user's jobs errors are now stored and displayed +- 59a94c71: Allow task property values to be blank, but strip them out before persisting them + ## 2.1.9 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 16ab2720f..be937d97f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/core", - "version": "2.1.9", + "version": "2.2.0", "description": "Core code used across the Trigger.dev SDK and platform", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 1ea6b3bb2..9585d5522 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # @trigger.dev/eslint-plugin +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + ## 2.1.9 ## 2.1.8 diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index f500bad43..5cef9a51d 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/eslint-plugin", - "version": "2.1.9", + "version": "2.2.0", "description": "ESLint plugin with trigger.dev best practices", "keywords": [ "eslint", diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md index d6426dd2e..cbf09e178 100644 --- a/packages/express/CHANGELOG.md +++ b/packages/express/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/express +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/express/package.json b/packages/express/package.json index fcdf500da..99f807eb9 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/express", - "version": "2.1.9", + "version": "2.2.0", "description": "Official Express adapter for Trigger.dev", "license": "MIT", "main": "./dist/index.js", @@ -19,7 +19,7 @@ "./package.json": "./package.json" }, "devDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.2.0", "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/express": "^4.17.13", @@ -33,7 +33,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.9" + "@trigger.dev/sdk": "workspace:^2.2.0" }, "dependencies": { "@remix-run/web-fetch": "^4.3.5", diff --git a/packages/integration-kit/CHANGELOG.md b/packages/integration-kit/CHANGELOG.md index 624ef4d38..732ab9845 100644 --- a/packages/integration-kit/CHANGELOG.md +++ b/packages/integration-kit/CHANGELOG.md @@ -1,5 +1,11 @@ # @trigger.dev/integration-kit +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + ## 2.1.9 ## 2.1.8 diff --git a/packages/integration-kit/package.json b/packages/integration-kit/package.json index dfd954b40..461091e3c 100644 --- a/packages/integration-kit/package.json +++ b/packages/integration-kit/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/integration-kit", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev Integration Kit has helpers to make creating integrations easier", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/nestjs/CHANGELOG.md b/packages/nestjs/CHANGELOG.md index 4fbd14ca8..82417c06b 100644 --- a/packages/nestjs/CHANGELOG.md +++ b/packages/nestjs/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/nestjs +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 08378889f..a7a5ee2db 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/nestjs", - "version": "2.1.9", + "version": "2.2.0", "description": "Official NestJS adapter for Trigger.dev", "license": "MIT", "main": "./dist/index.js", @@ -19,7 +19,7 @@ "./package.json": "./package.json" }, "devDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.2.0", "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/express": "^4.17.13", @@ -35,7 +35,7 @@ }, "peerDependencies": { "@nestjs/common": ">=10.0.0", - "@trigger.dev/sdk": "workspace:^2.1.9" + "@trigger.dev/sdk": "workspace:^2.2.0" }, "dependencies": { "@nestjs/common": "^10.2.4", diff --git a/packages/nextjs/CHANGELOG.md b/packages/nextjs/CHANGELOG.md index 64b5eeb8a..35b288f62 100644 --- a/packages/nextjs/CHANGELOG.md +++ b/packages/nextjs/CHANGELOG.md @@ -1,5 +1,16 @@ # @trigger.dev/nextjs +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index a52f1b9ba..c2420baf8 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/nextjs", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev Next.js integration", "license": "MIT", "main": "./dist/index.js", @@ -34,7 +34,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.2.0", "next": ">=12.0.0 <14.0.0" }, "dependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index bf9ce8d5f..3007cc327 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,14 @@ # @trigger.dev/react +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] +- Updated dependencies [50e3d9e4] +- Updated dependencies [59a94c71] + - @trigger.dev/core@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/react/package.json b/packages/react/package.json index 0e1abea13..063966831 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/react", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev React SDK", "license": "MIT", "types": "dist/index.d.ts", @@ -27,7 +27,7 @@ }, "dependencies": { "@tanstack/react-query": "5.0.0-beta.2", - "@trigger.dev/core": "workspace:^2.1.9", + "@trigger.dev/core": "workspace:^2.2.0", "debug": "^4.3.4", "zod": "3.22.3" }, diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index 92ad60550..6da59d203 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -1,5 +1,12 @@ # @trigger.dev/remix +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/remix/package.json b/packages/remix/package.json index 451a6ddc6..d5ef391b7 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/remix", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev Remix integration", "license": "MIT", "main": "./dist/index.js", @@ -34,7 +34,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.2.0", "@remix-run/server-runtime": ">1.19.0" }, "dependencies": { diff --git a/packages/sveltekit/CHANGELOG.md b/packages/sveltekit/CHANGELOG.md new file mode 100644 index 000000000..44af09954 --- /dev/null +++ b/packages/sveltekit/CHANGELOG.md @@ -0,0 +1,13 @@ +# @trigger.dev/sveltekit + +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- 0558b2c5: SvelteKit adaptor package +- Updated dependencies [975c5f1d] + - @trigger.dev/sdk@2.2.0 diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 6e14e5ae2..df5044756 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sveltekit", - "version": "2.1.9", + "version": "2.2.0", "description": "Trigger.dev svelteKit integration", "license": "MIT", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "build:tsup": "tsup" }, "peerDependencies": { - "@trigger.dev/sdk": "workspace:^2.0.14" + "@trigger.dev/sdk": "workspace:^2.2.0" }, "dependencies": { "debug": "^4.3.4" diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index 43035fcd5..fd21caf17 100644 --- a/packages/testing/CHANGELOG.md +++ b/packages/testing/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/testing +## 2.2.0 + +### Patch Changes + +- Updated dependencies [975c5f1d] +- Updated dependencies [50e3d9e4] +- Updated dependencies [59a94c71] + - @trigger.dev/sdk@2.2.0 + - @trigger.dev/core@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/testing/package.json b/packages/testing/package.json index 54497ba11..faa18e8a3 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,7 +1,7 @@ { "name": "@trigger.dev/testing", "description": "A collection of useful tools to write tests for Trigger.dev.", - "version": "2.1.9", + "version": "2.2.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-sdk/CHANGELOG.md b/packages/trigger-sdk/CHANGELOG.md index df1b8aa22..211449219 100644 --- a/packages/trigger-sdk/CHANGELOG.md +++ b/packages/trigger-sdk/CHANGELOG.md @@ -1,5 +1,18 @@ # @trigger.dev/sdk +## 2.2.0 + +### Minor Changes + +- 975c5f1d: Drop support for Node v16, require Node >= 18. This allows us to use native fetch in our SDK which paves the way for multi-platform support. + +### Patch Changes + +- Updated dependencies [975c5f1d] +- Updated dependencies [50e3d9e4] +- Updated dependencies [59a94c71] + - @trigger.dev/core@2.2.0 + ## 2.1.9 ### Patch Changes diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index f09498891..9de7e97a4 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sdk", - "version": "2.1.9", + "version": "2.2.0", "description": "trigger.dev Node.JS SDK", "license": "MIT", "main": "./dist/index.js", @@ -25,7 +25,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/core": "workspace:^2.1.9", + "@trigger.dev/core": "workspace:^2.2.0", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", From ebe78017efdf7bd53c0e4414a8302b7f0ff2648d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 13 Oct 2023 13:21:52 +0100 Subject: [PATCH 74/79] Update pnpm lock file after package release merge --- pnpm-lock.yaml | 68 +++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 565a5ecfb..4b6207a94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -372,8 +372,8 @@ importers: integrations/airtable: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x airtable: ^0.12.1 @@ -400,8 +400,8 @@ importers: '@octokit/types': ^9.2.3 '@octokit/webhooks': ^10.4.0 '@octokit/webhooks-types': ^6.10.0 - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' octokit: ^2.0.14 @@ -427,8 +427,8 @@ importers: integrations/linear: specifiers: '@linear/sdk': ^8.0.0 - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x rimraf: ^3.0.2 @@ -449,8 +449,8 @@ importers: integrations/openai: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' openai: ^4.2.0 @@ -469,8 +469,8 @@ importers: integrations/plain: specifiers: '@team-plain/typescript-sdk': ^2.7.0 - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' rimraf: ^3.0.2 @@ -487,8 +487,8 @@ importers: integrations/replicate: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x replicate: ^0.18.1 @@ -510,8 +510,8 @@ importers: integrations/resend: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' resend: ^1.0.0 @@ -530,8 +530,8 @@ importers: integrations/sendgrid: specifiers: '@sendgrid/mail': ^7.7.0 - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': 16.x rimraf: ^3.0.2 @@ -551,7 +551,7 @@ importers: integrations/slack: specifiers: '@slack/web-api': ^6.8.1 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' rimraf: ^3.0.2 @@ -569,8 +569,8 @@ importers: integrations/stripe: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@types/node': 16.x rimraf: ^3.0.2 stripe: ^12.14.0 @@ -593,8 +593,8 @@ importers: integrations/supabase: specifiers: '@supabase/supabase-js': ^2.26.0 - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/node': 18.x rimraf: ^3.0.2 @@ -617,8 +617,8 @@ importers: integrations/typeform: specifiers: - '@trigger.dev/integration-kit': workspace:^2.1.9 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/integration-kit': workspace:^2.2.0 + '@trigger.dev/sdk': workspace:^2.2.0 '@typeform/api-client': ^1.8.0 '@types/node': 16.x rimraf: ^3.0.2 @@ -851,7 +851,7 @@ importers: packages/express: specifiers: '@remix-run/web-fetch': ^4.3.5 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/express': ^4.17.13 @@ -898,7 +898,7 @@ importers: specifiers: '@nestjs/common': ^10.2.4 '@remix-run/web-fetch': ^4.3.5 - '@trigger.dev/sdk': workspace:^2.1.9 + '@trigger.dev/sdk': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/express': ^4.17.13 @@ -947,7 +947,7 @@ importers: packages/react: specifiers: '@tanstack/react-query': 5.0.0-beta.2 - '@trigger.dev/core': workspace:^2.1.9 + '@trigger.dev/core': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/react': 18.2.17 @@ -1042,7 +1042,7 @@ importers: packages/trigger-sdk: specifiers: - '@trigger.dev/core': workspace:^2.1.9 + '@trigger.dev/core': workspace:^2.2.0 '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/node': '18' @@ -15626,7 +15626,7 @@ packages: /axios/0.21.4_debug@4.3.2: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.2 transitivePeerDependencies: - debug dev: false @@ -20429,6 +20429,18 @@ packages: debug: optional: true + /follow-redirects/1.15.2_debug@4.3.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.2 + dev: false + /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: From 6371dd4f952b45a8073893ff0fed44a8073a5ac9 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:16:35 +0530 Subject: [PATCH 75/79] feat: allow sendEvent to update an existing event (#607) --- .../services/events/ingestSendEvent.server.ts | 148 +++++++++++++----- 1 file changed, 109 insertions(+), 39 deletions(-) diff --git a/apps/webapp/app/services/events/ingestSendEvent.server.ts b/apps/webapp/app/services/events/ingestSendEvent.server.ts index b1de0e2a9..9e4a254bb 100644 --- a/apps/webapp/app/services/events/ingestSendEvent.server.ts +++ b/apps/webapp/app/services/events/ingestSendEvent.server.ts @@ -3,6 +3,25 @@ import { $transaction, PrismaClientOrTransaction, PrismaErrorSchema, prisma } fr import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { workerQueue } from "~/services/worker.server"; import { logger } from "../logger.server"; +import { EventRecord, ExternalAccount } from "@trigger.dev/database"; + +type UpdateEventInput = { + tx: PrismaClientOrTransaction; + existingEventLog: EventRecord; + reqEvent: RawEvent; + deliverAt?: Date; +}; + +type CreateEventInput = { + tx: PrismaClientOrTransaction; + event: RawEvent; + environment: AuthenticatedEnvironment; + deliverAt?: Date; + sourceContext?: { id: string; metadata?: any }; + externalAccount?: ExternalAccount; +}; + +const EVENT_UPDATE_THRESHOLD_WINDOW_IN_MSECS = 5 * 1000; // 5 seconds export class IngestSendEvent { #prismaClient: PrismaClientOrTransaction; @@ -52,34 +71,25 @@ export class IngestSendEvent { }) : undefined; - // Create a new event in the database - const eventLog = await tx.eventRecord.create({ - data: { - organizationId: environment.organizationId, - projectId: environment.projectId, - environmentId: environment.id, - eventId: event.id, - name: event.name, - timestamp: event.timestamp ?? new Date(), - payload: event.payload ?? {}, - context: event.context ?? {}, - source: event.source ?? "trigger.dev", - sourceContext, - deliverAt: deliverAt, - externalAccountId: externalAccount ? externalAccount.id : undefined, + const existingEventLog = await tx.eventRecord.findUnique({ + where: { + eventId_environmentId: { + eventId: event.id, + environmentId: environment.id, + }, }, }); - if (this.deliverEvents) { - // Produce a message to the event bus - await workerQueue.enqueue( - "deliverEvent", - { - id: eventLog.id, - }, - { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` } - ); - } + const eventLog = await (existingEventLog + ? this.updateEvent({ tx, existingEventLog, reqEvent: event, deliverAt }) + : this.createEvent({ + tx, + event, + environment, + deliverAt, + sourceContext, + externalAccount, + })); return eventLog; }); @@ -95,21 +105,81 @@ export class IngestSendEvent { throw error; } - // If the error is a Prisma unique constraint error, it means that the event already exists - if (prismaError.success && prismaError.data.code === "P2002") { - logger.debug("Event already exists, finding and returning", { event, environment }); - - return this.#prismaClient.eventRecord.findUniqueOrThrow({ - where: { - eventId_environmentId: { - eventId: event.id, - environmentId: environment.id, - }, - }, - }); - } - throw error; } } + + private async createEvent({ + tx, + event, + environment, + deliverAt, + sourceContext, + externalAccount, + }: CreateEventInput) { + const eventLog = await tx.eventRecord.create({ + data: { + organizationId: environment.organizationId, + projectId: environment.projectId, + environmentId: environment.id, + eventId: event.id, + name: event.name, + timestamp: event.timestamp ?? new Date(), + payload: event.payload ?? {}, + context: event.context ?? {}, + source: event.source ?? "trigger.dev", + sourceContext, + deliverAt: deliverAt, + externalAccountId: externalAccount ? externalAccount.id : undefined, + }, + }); + + await this.enqueueWorkerEvent(tx, eventLog); + + return eventLog; + } + + private async updateEvent({ tx, existingEventLog, reqEvent, deliverAt }: UpdateEventInput) { + if (!this.shouldUpdateEvent(existingEventLog)) { + logger.debug(`not updating event for event id: ${existingEventLog.eventId}`); + return existingEventLog; + } + + const updatedEventLog = await tx.eventRecord.update({ + where: { + eventId_environmentId: { + eventId: existingEventLog.eventId, + environmentId: existingEventLog.environmentId, + }, + }, + data: { + payload: reqEvent.payload ?? existingEventLog.payload, + context: reqEvent.context ?? existingEventLog.context, + deliverAt: deliverAt ?? new Date(), + }, + }); + + await this.enqueueWorkerEvent(tx, updatedEventLog); + + return updatedEventLog; + } + + private shouldUpdateEvent(eventLog: EventRecord) { + const thresholdTime = new Date(Date.now() + EVENT_UPDATE_THRESHOLD_WINDOW_IN_MSECS); + + return eventLog.deliverAt >= thresholdTime; + } + + private async enqueueWorkerEvent(tx: PrismaClientOrTransaction, eventLog: EventRecord) { + if (this.deliverEvents) { + // Produce a message to the event bus + await workerQueue.enqueue( + "deliverEvent", + { + id: eventLog.id, + }, + { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` } + ); + } + } } From 70410d33af66107fb6c5f1d382cf728dda226c00 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 13 Oct 2023 15:12:05 +0100 Subject: [PATCH 76/79] Ability to change the timeout of the graceful shutdown via env var --- apps/webapp/server.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index 892a99430..55c9fc530 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -62,7 +62,23 @@ if (process.env.HTTP_SERVER_DISABLED !== "true") { }); // Handle shutdowns gracefully - createTerminus(server, { signals: ["SIGINT", "SIGTERM"], timeout: 5000 }); + createTerminus(server, { + signals: ["SIGINT", "SIGTERM"], + timeout: process.env.GRACEFUL_SHUTDOWN_TIMEOUT + ? Number(process.env.GRACEFUL_SHUTDOWN_TIMEOUT) + : 5000, + onSignal: async () => { + console.log("[terminus] onSignal: starting cleanup"); + }, + onShutdown: async () => { + console.log("[terminus] onShutdown: cleanup finished, server is shutting down"); + }, + onSendFailureDuringShutdown: async () => { + console.log( + "[terminus] onSendFailureDuringShutdown: cleanup finished, server is shutting down" + ); + }, + }); } else { console.log(`✅ app ready (skipping http server)`); } From 6769d6b43930317045796cc31af7924f462df07a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 13 Oct 2023 16:20:18 +0100 Subject: [PATCH 77/79] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit c028db229cece2bf7d7acf04e7b1c08fea61798c Author: Matt Aitken Date: Fri Oct 13 16:04:54 2023 +0100 Some fixes for getting the endpoint id commit e31b7ac5f2d451f70e3d42a3fb3e5f718d93485b Author: Matt Aitken Date: Fri Oct 13 15:39:18 2023 +0100 Added a link in the instructions if Node isn’t detected commit 1a59cced775fbeac3e5b94e0c151b490c838d8f8 Author: Matt Aitken Date: Fri Oct 13 15:39:05 2023 +0100 Fixed some conflict issues commit 77263ed26bcf7025071ac0faf15fb867fff245d0 Author: Matt Aitken Date: Fri Oct 13 15:38:56 2023 +0100 Added a changeset commit 1a7df3f37f0c65d68cb9a9fe7ac50328bbb4d879 Author: GuyBorderless <100129659+guy-borderless@users.noreply.github.com> Date: Fri Oct 13 17:31:01 2023 +0300 feat: enable deno (#531) * adding deno reference project and JSRuntime file * accessing runtime specific functions through JsRuntime * adding support for both deno.json and deno.jsonc * no automatic setup for deno projects currently --------- Co-authored-by: Guy Kedem Co-authored-by: Matt Aitken --- .changeset/famous-files-talk.md | 5 + .vscode/extensions.json | 9 + .vscode/settings.json | 3 + packages/cli/src/commands/dev.ts | 64 +- packages/cli/src/commands/init.ts | 14 + packages/cli/src/commands/whoami.ts | 6 +- packages/cli/src/utils/jsRuntime.ts | 137 ++++ packages/cli/src/utils/logger.ts | 1 + .../deno-reference/.vscode/settings.json | 3 + references/deno-reference/deno.json | 8 + references/deno-reference/deno.lock | 684 ++++++++++++++++++ references/deno-reference/main.ts | 41 ++ 12 files changed, 923 insertions(+), 52 deletions(-) create mode 100644 .changeset/famous-files-talk.md create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 packages/cli/src/utils/jsRuntime.ts create mode 100644 references/deno-reference/.vscode/settings.json create mode 100644 references/deno-reference/deno.json create mode 100644 references/deno-reference/deno.lock create mode 100644 references/deno-reference/main.ts diff --git a/.changeset/famous-files-talk.md b/.changeset/famous-files-talk.md new file mode 100644 index 000000000..dad0a1a34 --- /dev/null +++ b/.changeset/famous-files-talk.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +Detects JSRuntime (Node/Deno at the moment). Adds basic Deno support diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..312d6bbd4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "astro-build.astro-vscode", + "denoland.vscode-deno" + ], + "unwantedRecommendations": [ + + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..5f5239032 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enablePaths": ["references/deno-reference"] +} diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 9972ca221..3224c24d3 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -1,22 +1,18 @@ import boxen from "boxen"; -import chalk from "chalk"; import childProcess from "child_process"; import chokidar from "chokidar"; -import fs from "fs/promises"; import ngrok from "ngrok"; -import { run as ncuRun } from "npm-check-updates"; import ora, { Ora } from "ora"; import pRetry, { AbortError } from "p-retry"; -import pathModule from "path"; import util from "util"; import { z } from "zod"; -import { Framework, getFramework } from "../frameworks"; +import { Framework } from "../frameworks"; import { standardWatchFilePaths, standardWatchIgnoreRegex } from "../frameworks/watchConfig"; import { telemetryClient } from "../telemetry/telemetry"; import { getEnvFilename } from "../utils/env"; import fetch from "../utils/fetchUseProxy"; import { getTriggerApiDetails } from "../utils/getTriggerApiDetails"; -import { getUserPackageManager } from "../utils/getUserPkgManager"; +import { JsRuntime, getJsRuntime } from "../utils/jsRuntime"; import { logger } from "../utils/logger"; import { resolvePath } from "../utils/parseNameAndPath"; import { RequireKeys } from "../utils/requiredKeys"; @@ -45,6 +41,8 @@ const formattedDate = new Intl.DateTimeFormat("en", { second: "numeric", }); +let runtime: JsRuntime; + export async function devCommand(path: string, anyOptions: any) { telemetryClient.dev.started(path, anyOptions); @@ -57,12 +55,12 @@ export async function devCommand(path: string, anyOptions: any) { const options = result.data; const resolvedPath = resolvePath(path); - + runtime = await getJsRuntime(resolvedPath, logger); //check for outdated packages, don't await this - checkForOutdatedPackages(resolvedPath); + runtime.checkForOutdatedPackages(); // Read from package.json to get the endpointId - const endpointId = await getEndpointIdFromPackageJson(resolvedPath, options); + const endpointId = await getEndpointId(runtime, options.clientId); if (!endpointId) { logger.error( "You must run the `init` command first to setup the project – you are missing \n'trigger.dev': { 'endpointId': 'your-client-id' } from your package.json file, or pass in the --client-id option to this command" @@ -73,8 +71,8 @@ export async function devCommand(path: string, anyOptions: any) { logger.success(`✔️ [trigger.dev] Detected TriggerClient id: ${endpointId}`); //resolve the options using the detected framework (use default if there isn't a matching framework) - const packageManager = await getUserPackageManager(resolvedPath); - const framework = await getFramework(resolvedPath, packageManager); + const packageManager = await runtime.getUserPackageManager(); + const framework = await runtime.getFramework(); const resolvedOptions = await resolveOptions(framework, resolvedPath, options); // Read from .env.local or .env to get the TRIGGER_API_KEY and TRIGGER_API_URL @@ -250,8 +248,7 @@ async function startIndexing({ apiClient, }: RefreshOptions & { apiClient: TriggerApi }) { spinner.start(); - - const refreshedEndpointId = await getEndpointIdFromPackageJson(path, resolvedOptions); + const refreshedEndpointId = await getEndpointId(runtime, resolvedOptions.clientId); const authorizedKey = await apiClient.whoami(); if (!authorizedKey) { @@ -396,43 +393,10 @@ async function verifyEndpoint( return; } -export async function checkForOutdatedPackages(path: string) { - const updates = (await ncuRun({ - packageFile: `${path}/package.json`, - filter: "/trigger.dev/.+$/", - upgrade: false, - })) as { - [key: string]: string; - }; - - if (typeof updates === "undefined" || Object.keys(updates).length === 0) { - return; - } - - const packageFile = await fs.readFile(`${path}/package.json`); - const data = JSON.parse(Buffer.from(packageFile).toString("utf8")); - const dependencies = data.dependencies; - console.log(chalk.bgYellow("Updates available for trigger.dev packages")); - console.log(chalk.bgBlue("Run npx @trigger.dev/cli@latest update")); - - for (let dep in updates) { - console.log(`${dep} ${dependencies[dep]} → ${updates[dep]}`); - } -} - -export async function getEndpointIdFromPackageJson(path: string, options: DevCommandOptions) { - if (options.clientId) { - return options.clientId; - } - - const pkgJsonPath = pathModule.join(path, "package.json"); - const pkgBuffer = await fs.readFile(pkgJsonPath); - const pkgJson = JSON.parse(pkgBuffer.toString()); - - const value = pkgJson["trigger.dev"]?.endpointId; - if (!value || typeof value !== "string") return; - - return value as string; +export function getEndpointId(runtime: JsRuntime, clientId?: string) { + if (clientId) { + return clientId; + } else return runtime.getEndpointId(); } async function resolveEndpointUrl(apiUrl: string, port: number, hostname: string) { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 1c9ecacc5..8bd7b0fbb 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -22,6 +22,7 @@ import { resolvePath } from "../utils/parseNameAndPath"; import { readPackageJson } from "../utils/readPackageJson"; import { renderTitle } from "../utils/renderTitle"; import { TriggerApi, WhoamiResponse } from "../utils/triggerApi"; +import { getJsRuntime } from "../utils/jsRuntime"; export type InitCommandOptions = { projectPath: string; @@ -38,6 +39,19 @@ export const initCommand = async (options: InitCommandOptions) => { const resolvedPath = resolvePath(options.projectPath); + // assuming nodejs by default + let runtimeId: string = "nodejs"; + try { + runtimeId = (await getJsRuntime(resolvedPath, logger)).id; + } catch {} + if (runtimeId !== "nodejs") { + logger.error( + `We currently only support automatic setup for NodeJS projects. This is a ${runtimeId} project. View our manual installation guides here: https://trigger.dev/docs/documentation/quickstarts/introduction` + ); + telemetryClient.init.failed("not_supported_runtime", options); + return; + } + await renderTitle(resolvedPath); if (options.triggerUrl === CLOUD_TRIGGER_URL) { diff --git a/packages/cli/src/commands/whoami.ts b/packages/cli/src/commands/whoami.ts index 6a9e11801..315617e15 100644 --- a/packages/cli/src/commands/whoami.ts +++ b/packages/cli/src/commands/whoami.ts @@ -2,9 +2,10 @@ import { z } from "zod"; import { logger } from "../utils/logger"; import { resolvePath } from "../utils/parseNameAndPath"; import { TriggerApi } from "../utils/triggerApi"; -import { DevCommandOptions, getEndpointIdFromPackageJson } from "./dev"; +import { DevCommandOptions, getEndpointId } from "./dev"; import ora from "ora"; import { getTriggerApiDetails } from "../utils/getTriggerApiDetails"; +import { getJsRuntime } from "../utils/jsRuntime"; export const WhoAmICommandOptionsSchema = z.object({ envFile: z.string(), @@ -26,7 +27,8 @@ export async function whoamiCommand(path: string, anyOptions: any) { const resolvedPath = resolvePath(path); // Read from package.json to get the endpointId - const endpointId = await getEndpointIdFromPackageJson(resolvedPath, options as DevCommandOptions); + const runtime = await getJsRuntime(resolvedPath, logger); + const endpointId = await getEndpointId(runtime); if (!endpointId) { logger.error( "You must run the `init` command first to setup the project – you are missing \n'trigger.dev': { 'endpointId': 'your-client-id' } from your package.json file, or pass in the --client-id option to this command" diff --git a/packages/cli/src/utils/jsRuntime.ts b/packages/cli/src/utils/jsRuntime.ts new file mode 100644 index 000000000..2e453c2aa --- /dev/null +++ b/packages/cli/src/utils/jsRuntime.ts @@ -0,0 +1,137 @@ +import { Framework, getFramework } from "../frameworks"; +import { PackageManager, getUserPackageManager } from "./getUserPkgManager"; +import { Logger } from "./logger"; +import { run as ncuRun } from "npm-check-updates"; +import chalk from "chalk"; +import fs from "fs/promises"; +import pathModule from "path"; + +export abstract class JsRuntime { + logger: Logger; + projectRootPath: string; + constructor(projectRootPath: string, logger: Logger) { + this.logger = logger; + this.projectRootPath = projectRootPath; + } + abstract get id(): string; + abstract checkForOutdatedPackages(): Promise; + abstract getUserPackageManager(): Promise; + abstract getFramework(): Promise; + abstract getEndpointId(): Promise; +} + +export async function getJsRuntime(projectRootPath: string, logger: Logger): Promise { + if (await NodeJsRuntime.isNodeJsRuntime(projectRootPath)) { + return new NodeJsRuntime(projectRootPath, logger); + } else if (await DenoRuntime.isDenoJsRuntime(projectRootPath)) { + return new DenoRuntime(projectRootPath, logger); + } + throw new Error("Unsupported runtime"); +} + +class NodeJsRuntime extends JsRuntime { + static async isNodeJsRuntime(projectRootPath: string): Promise { + try { + await fs.stat(pathModule.join(projectRootPath, "package.json")); + return true; + } catch { + return false; + } + } + + get id() { + return "nodejs"; + } + get packageJsonPath(): string { + return pathModule.join(this.projectRootPath, "package.json"); + } + + async checkForOutdatedPackages(): Promise { + const updates = (await ncuRun({ + packageFile: `${this.packageJsonPath}`, + filter: "/trigger.dev/.+$/", + upgrade: false, + })) as { + [key: string]: string; + }; + + if (typeof updates === "undefined" || Object.keys(updates).length === 0) { + return; + } + + const packageFile = await fs.readFile(this.packageJsonPath); + const data = JSON.parse(Buffer.from(packageFile).toString("utf8")); + const dependencies = data.dependencies; + console.log(chalk.bgYellow("Updates available for trigger.dev packages")); + console.log(chalk.bgBlue("Run npx @trigger.dev/cli@latest update")); + + for (let dep in updates) { + console.log(`${dep} ${dependencies[dep]} → ${updates[dep]}`); + } + } + + async getUserPackageManager() { + return getUserPackageManager(this.projectRootPath); + } + + async getFramework() { + const userPackageManager = await this.getUserPackageManager(); + return getFramework(this.projectRootPath, userPackageManager); + } + async getEndpointId() { + const pkgJsonPath = pathModule.join(this.projectRootPath, "package.json"); + const pkgBuffer = await fs.readFile(pkgJsonPath); + const pkgJson = JSON.parse(pkgBuffer.toString()); + const value = pkgJson["trigger.dev"]?.endpointId; + if (!value || typeof value !== "string") return undefined; + return value; + } +} + +class DenoRuntime extends JsRuntime { + getDenoJsonPath(): Promise { + try { + return fs + .stat(pathModule.join(this.projectRootPath, "deno.json")) + .then(() => pathModule.join(this.projectRootPath, "deno.json")); + } catch { + return fs + .stat(pathModule.join(this.projectRootPath, "deno.jsonc")) + .then(() => pathModule.join(this.projectRootPath, "deno.jsonc")); + } + } + + get id() { + return "deno"; + } + + static async isDenoJsRuntime(projectRootPath: string): Promise { + try { + try { + await fs.stat(pathModule.join(projectRootPath, "deno.json")); + } catch (e) { + await fs.stat(pathModule.join(projectRootPath, "deno.jsonc")); + } + return true; + } catch { + return false; + } + } + + async checkForOutdatedPackages() { + // not implemented currently + } + async getUserPackageManager() { + return undefined; + } + async getFramework() { + // not implemented currently + return undefined; + } + async getEndpointId() { + const pkgJsonPath = await this.getDenoJsonPath(); + const pkgBuffer = await fs.readFile(pkgJsonPath); + const pkgJson = JSON.parse(pkgBuffer.toString()); + return pkgJson["trigger.dev"]?.endpointId; + } +} diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index 20f091c94..69b33fcef 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; +export type Logger = typeof logger; export const logger = { error(...args: unknown[]) { console.log(chalk.red(...args)); diff --git a/references/deno-reference/.vscode/settings.json b/references/deno-reference/.vscode/settings.json new file mode 100644 index 000000000..b943dbc7a --- /dev/null +++ b/references/deno-reference/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/references/deno-reference/deno.json b/references/deno-reference/deno.json new file mode 100644 index 000000000..26c4ebaee --- /dev/null +++ b/references/deno-reference/deno.json @@ -0,0 +1,8 @@ +{ + "trigger.dev": { + "endpointId": "borderless" + }, + "tasks": { + "dev": "deno run --watch main.ts" + } +} diff --git a/references/deno-reference/deno.lock b/references/deno-reference/deno.lock new file mode 100644 index 000000000..0d0405f0d --- /dev/null +++ b/references/deno-reference/deno.lock @@ -0,0 +1,684 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "npm:@trigger.dev/express": "npm:@trigger.dev/express@2.1.7_@trigger.dev+sdk@2.1.7", + "npm:@trigger.dev/sdk": "npm:@trigger.dev/sdk@2.1.7" + }, + "npm": { + "@remix-run/web-blob@3.1.0": { + "integrity": "sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==", + "dependencies": { + "@remix-run/web-stream": "@remix-run/web-stream@1.1.0", + "web-encoding": "web-encoding@1.1.5" + } + }, + "@remix-run/web-fetch@4.4.1": { + "integrity": "sha512-xMceEGn2kvfeWS91nHSOhEQHPGgjFnmDVpWFZrbWPVdiTByMZIn421/tdSF6Kd1RsNsY+5Iwt3JFEKZHAcMQHw==", + "dependencies": { + "@remix-run/web-blob": "@remix-run/web-blob@3.1.0", + "@remix-run/web-file": "@remix-run/web-file@3.1.0", + "@remix-run/web-form-data": "@remix-run/web-form-data@3.1.0", + "@remix-run/web-stream": "@remix-run/web-stream@1.1.0", + "@web3-storage/multipart-parser": "@web3-storage/multipart-parser@1.0.0", + "abort-controller": "abort-controller@3.0.0", + "data-uri-to-buffer": "data-uri-to-buffer@3.0.1", + "mrmime": "mrmime@1.0.1" + } + }, + "@remix-run/web-file@3.1.0": { + "integrity": "sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==", + "dependencies": { + "@remix-run/web-blob": "@remix-run/web-blob@3.1.0" + } + }, + "@remix-run/web-form-data@3.1.0": { + "integrity": "sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==", + "dependencies": { + "web-encoding": "web-encoding@1.1.5" + } + }, + "@remix-run/web-stream@1.1.0": { + "integrity": "sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==", + "dependencies": { + "web-streams-polyfill": "web-streams-polyfill@3.2.1" + } + }, + "@trigger.dev/core@2.1.7": { + "integrity": "sha512-Ts0xMFiWi4ph4da6BIbuegz2mMSVYJxWEh5S2mhM5vc+S6cl+MEbTdh+neUVa/0N4qq3Io07e9yDTXnTfiifzg==", + "dependencies": { + "ulid": "ulid@2.3.0", + "zod": "zod@3.21.4", + "zod-error": "zod-error@1.5.0" + } + }, + "@trigger.dev/express@2.1.7_@trigger.dev+sdk@2.1.7": { + "integrity": "sha512-uMoSDpOZJdYs+UXwEkhWYCYm8T6e+cu33LXmXhoF2tpm7m8PritFgR7LPX4H9UdIddRATdsmWgOqPMOQneEtDg==", + "dependencies": { + "@remix-run/web-fetch": "@remix-run/web-fetch@4.4.1", + "@trigger.dev/sdk": "@trigger.dev/sdk@2.1.7", + "debug": "debug@4.3.4", + "express": "express@4.18.2" + } + }, + "@trigger.dev/sdk@2.1.7": { + "integrity": "sha512-t3pbXj6+I38a5LNGJg9Ed09NCt0AWnw7hJtUCZyuEfBQulRnVSekNLsxCyGlgiTPiyRvbyefzPrX1GhXh0MY1w==", + "dependencies": { + "@trigger.dev/core": "@trigger.dev/core@2.1.7", + "chalk": "chalk@5.3.0", + "cronstrue": "cronstrue@2.32.0", + "debug": "debug@4.3.4", + "evt": "evt@2.5.3", + "get-caller-file": "get-caller-file@2.0.5", + "git-remote-origin-url": "git-remote-origin-url@4.0.0", + "git-repo-info": "git-repo-info@2.1.1", + "node-fetch": "node-fetch@2.6.13", + "slug": "slug@6.1.0", + "terminal-link": "terminal-link@3.0.0", + "ulid": "ulid@2.3.0", + "uuid": "uuid@9.0.1", + "ws": "ws@8.13.0", + "zod": "zod@3.21.4" + } + }, + "@web3-storage/multipart-parser@1.0.0": { + "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==", + "dependencies": {} + }, + "@zxing/text-encoding@0.9.0": { + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "dependencies": {} + }, + "abort-controller@3.0.0": { + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "event-target-shim@5.0.1" + } + }, + "accepts@1.3.8": { + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "mime-types@2.1.35", + "negotiator": "negotiator@0.6.3" + } + }, + "ansi-escapes@5.0.0": { + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dependencies": { + "type-fest": "type-fest@1.4.0" + } + }, + "array-flatten@1.1.1": { + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dependencies": {} + }, + "available-typed-arrays@1.0.5": { + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dependencies": {} + }, + "body-parser@1.20.1": { + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "bytes@3.1.2", + "content-type": "content-type@1.0.5", + "debug": "debug@2.6.9", + "depd": "depd@2.0.0", + "destroy": "destroy@1.2.0", + "http-errors": "http-errors@2.0.0", + "iconv-lite": "iconv-lite@0.4.24", + "on-finished": "on-finished@2.4.1", + "qs": "qs@6.11.0", + "raw-body": "raw-body@2.5.1", + "type-is": "type-is@1.6.18", + "unpipe": "unpipe@1.0.0" + } + }, + "bytes@3.1.2": { + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dependencies": {} + }, + "call-bind@1.0.2": { + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "function-bind@1.1.1", + "get-intrinsic": "get-intrinsic@1.2.1" + } + }, + "chalk@5.3.0": { + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dependencies": {} + }, + "content-disposition@0.5.4": { + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "content-type@1.0.5": { + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dependencies": {} + }, + "cookie-signature@1.0.6": { + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dependencies": {} + }, + "cookie@0.5.0": { + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dependencies": {} + }, + "cronstrue@2.32.0": { + "integrity": "sha512-dmNflOCNJL6lZEj0dp2YhGIPY83VTjFue6d9feFhnNtrER6mAjBrUvSgK95j3IB/xNGpLjaZDIDG6ACKTZr9Yw==", + "dependencies": {} + }, + "data-uri-to-buffer@3.0.1": { + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "dependencies": {} + }, + "debug@2.6.9": { + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "ms@2.0.0" + } + }, + "debug@4.3.4": { + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "ms@2.1.2" + } + }, + "depd@2.0.0": { + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dependencies": {} + }, + "destroy@1.2.0": { + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dependencies": {} + }, + "ee-first@1.1.1": { + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dependencies": {} + }, + "encodeurl@1.0.2": { + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dependencies": {} + }, + "escape-html@1.0.3": { + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dependencies": {} + }, + "etag@1.8.1": { + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dependencies": {} + }, + "event-target-shim@5.0.1": { + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dependencies": {} + }, + "evt@2.5.3": { + "integrity": "sha512-wZKx0JgXaTOVOXI2saNVxINU6VToOHDowMwb3NRcU6l+C59eW3w9dZgNxjokiM8rvMgc7/11yFG0cSDxn4qxgA==", + "dependencies": { + "minimal-polyfills": "minimal-polyfills@2.2.3", + "run-exclusive": "run-exclusive@2.2.19", + "tsafe": "tsafe@1.6.5" + } + }, + "express@4.18.2": { + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "accepts@1.3.8", + "array-flatten": "array-flatten@1.1.1", + "body-parser": "body-parser@1.20.1", + "content-disposition": "content-disposition@0.5.4", + "content-type": "content-type@1.0.5", + "cookie": "cookie@0.5.0", + "cookie-signature": "cookie-signature@1.0.6", + "debug": "debug@2.6.9", + "depd": "depd@2.0.0", + "encodeurl": "encodeurl@1.0.2", + "escape-html": "escape-html@1.0.3", + "etag": "etag@1.8.1", + "finalhandler": "finalhandler@1.2.0", + "fresh": "fresh@0.5.2", + "http-errors": "http-errors@2.0.0", + "merge-descriptors": "merge-descriptors@1.0.1", + "methods": "methods@1.1.2", + "on-finished": "on-finished@2.4.1", + "parseurl": "parseurl@1.3.3", + "path-to-regexp": "path-to-regexp@0.1.7", + "proxy-addr": "proxy-addr@2.0.7", + "qs": "qs@6.11.0", + "range-parser": "range-parser@1.2.1", + "safe-buffer": "safe-buffer@5.2.1", + "send": "send@0.18.0", + "serve-static": "serve-static@1.15.0", + "setprototypeof": "setprototypeof@1.2.0", + "statuses": "statuses@2.0.1", + "type-is": "type-is@1.6.18", + "utils-merge": "utils-merge@1.0.1", + "vary": "vary@1.1.2" + } + }, + "finalhandler@1.2.0": { + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "debug@2.6.9", + "encodeurl": "encodeurl@1.0.2", + "escape-html": "escape-html@1.0.3", + "on-finished": "on-finished@2.4.1", + "parseurl": "parseurl@1.3.3", + "statuses": "statuses@2.0.1", + "unpipe": "unpipe@1.0.0" + } + }, + "for-each@0.3.3": { + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "is-callable@1.2.7" + } + }, + "forwarded@0.2.0": { + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dependencies": {} + }, + "fresh@0.5.2": { + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dependencies": {} + }, + "function-bind@1.1.1": { + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dependencies": {} + }, + "get-caller-file@2.0.5": { + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dependencies": {} + }, + "get-intrinsic@1.2.1": { + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "function-bind@1.1.1", + "has": "has@1.0.3", + "has-proto": "has-proto@1.0.1", + "has-symbols": "has-symbols@1.0.3" + } + }, + "git-remote-origin-url@4.0.0": { + "integrity": "sha512-EAxDksNdjuWgmVW9pVvA9jQDi/dmTaiDONktIy7qiRRhBZUI4FQK1YvBvteuTSX24aNKg9lfgxNYJEeeSXe6DA==", + "dependencies": { + "gitconfiglocal": "gitconfiglocal@2.1.0" + } + }, + "git-repo-info@2.1.1": { + "integrity": "sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==", + "dependencies": {} + }, + "gitconfiglocal@2.1.0": { + "integrity": "sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==", + "dependencies": { + "ini": "ini@1.3.8" + } + }, + "gopd@1.0.1": { + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "get-intrinsic@1.2.1" + } + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dependencies": {} + }, + "has-proto@1.0.1": { + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dependencies": {} + }, + "has-symbols@1.0.3": { + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dependencies": {} + }, + "has-tostringtag@1.0.0": { + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "has-symbols@1.0.3" + } + }, + "has@1.0.3": { + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "function-bind@1.1.1" + } + }, + "http-errors@2.0.0": { + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "depd@2.0.0", + "inherits": "inherits@2.0.4", + "setprototypeof": "setprototypeof@1.2.0", + "statuses": "statuses@2.0.1", + "toidentifier": "toidentifier@1.0.1" + } + }, + "iconv-lite@0.4.24": { + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": "safer-buffer@2.1.2" + } + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dependencies": {} + }, + "ini@1.3.8": { + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dependencies": {} + }, + "ipaddr.js@1.9.1": { + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dependencies": {} + }, + "is-arguments@1.1.1": { + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "call-bind@1.0.2", + "has-tostringtag": "has-tostringtag@1.0.0" + } + }, + "is-callable@1.2.7": { + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dependencies": {} + }, + "is-generator-function@1.0.10": { + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "has-tostringtag@1.0.0" + } + }, + "is-typed-array@1.1.12": { + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dependencies": { + "which-typed-array": "which-typed-array@1.1.11" + } + }, + "media-typer@0.3.0": { + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dependencies": {} + }, + "merge-descriptors@1.0.1": { + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dependencies": {} + }, + "methods@1.1.2": { + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dependencies": {} + }, + "mime-db@1.52.0": { + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dependencies": {} + }, + "mime-types@2.1.35": { + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "mime-db@1.52.0" + } + }, + "mime@1.6.0": { + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dependencies": {} + }, + "minimal-polyfills@2.2.3": { + "integrity": "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw==", + "dependencies": {} + }, + "mrmime@1.0.1": { + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dependencies": {} + }, + "ms@2.0.0": { + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dependencies": {} + }, + "ms@2.1.2": { + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "negotiator@0.6.3": { + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dependencies": {} + }, + "node-fetch@2.6.13": { + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "object-inspect@1.12.3": { + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dependencies": {} + }, + "on-finished@2.4.1": { + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "ee-first@1.1.1" + } + }, + "parseurl@1.3.3": { + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dependencies": {} + }, + "path-to-regexp@0.1.7": { + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dependencies": {} + }, + "proxy-addr@2.0.7": { + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "forwarded@0.2.0", + "ipaddr.js": "ipaddr.js@1.9.1" + } + }, + "qs@6.11.0": { + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "side-channel@1.0.4" + } + }, + "range-parser@1.2.1": { + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dependencies": {} + }, + "raw-body@2.5.1": { + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "bytes@3.1.2", + "http-errors": "http-errors@2.0.0", + "iconv-lite": "iconv-lite@0.4.24", + "unpipe": "unpipe@1.0.0" + } + }, + "run-exclusive@2.2.19": { + "integrity": "sha512-K3mdoAi7tjJ/qT7Flj90L7QyPozwUaAG+CVhkdDje4HLKXUYC3N/Jzkau3flHVDLQVhiHBtcimVodMjN9egYbA==", + "dependencies": { + "minimal-polyfills": "minimal-polyfills@2.2.3" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dependencies": {} + }, + "send@0.18.0": { + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "debug@2.6.9", + "depd": "depd@2.0.0", + "destroy": "destroy@1.2.0", + "encodeurl": "encodeurl@1.0.2", + "escape-html": "escape-html@1.0.3", + "etag": "etag@1.8.1", + "fresh": "fresh@0.5.2", + "http-errors": "http-errors@2.0.0", + "mime": "mime@1.6.0", + "ms": "ms@2.1.3", + "on-finished": "on-finished@2.4.1", + "range-parser": "range-parser@1.2.1", + "statuses": "statuses@2.0.1" + } + }, + "serve-static@1.15.0": { + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "encodeurl@1.0.2", + "escape-html": "escape-html@1.0.3", + "parseurl": "parseurl@1.3.3", + "send": "send@0.18.0" + } + }, + "setprototypeof@1.2.0": { + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dependencies": {} + }, + "side-channel@1.0.4": { + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "call-bind@1.0.2", + "get-intrinsic": "get-intrinsic@1.2.1", + "object-inspect": "object-inspect@1.12.3" + } + }, + "slug@6.1.0": { + "integrity": "sha512-x6vLHCMasg4DR2LPiyFGI0gJJhywY6DTiGhCrOMzb3SOk/0JVLIaL4UhyFSHu04SD3uAavrKY/K3zZ3i6iRcgA==", + "dependencies": {} + }, + "statuses@2.0.1": { + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dependencies": {} + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "has-flag@4.0.0" + } + }, + "supports-hyperlinks@2.3.0": { + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dependencies": { + "has-flag": "has-flag@4.0.0", + "supports-color": "supports-color@7.2.0" + } + }, + "terminal-link@3.0.0": { + "integrity": "sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==", + "dependencies": { + "ansi-escapes": "ansi-escapes@5.0.0", + "supports-hyperlinks": "supports-hyperlinks@2.3.0" + } + }, + "toidentifier@1.0.1": { + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "tsafe@1.6.5": { + "integrity": "sha512-895zss8xqqHKTc28sHGIfZKnt3C5jrstB1DyPr/h3/flK0zojsZUMQL1/W4ytdDW6KI4Oth62nb9rrxmA3s3Iw==", + "dependencies": {} + }, + "type-fest@1.4.0": { + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dependencies": {} + }, + "type-is@1.6.18": { + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "media-typer@0.3.0", + "mime-types": "mime-types@2.1.35" + } + }, + "ulid@2.3.0": { + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", + "dependencies": {} + }, + "unpipe@1.0.0": { + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dependencies": {} + }, + "util@0.12.5": { + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "inherits@2.0.4", + "is-arguments": "is-arguments@1.1.1", + "is-generator-function": "is-generator-function@1.0.10", + "is-typed-array": "is-typed-array@1.1.12", + "which-typed-array": "which-typed-array@1.1.11" + } + }, + "utils-merge@1.0.1": { + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dependencies": {} + }, + "uuid@9.0.1": { + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dependencies": {} + }, + "vary@1.1.2": { + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dependencies": {} + }, + "web-encoding@1.1.5": { + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dependencies": { + "@zxing/text-encoding": "@zxing/text-encoding@0.9.0", + "util": "util@0.12.5" + } + }, + "web-streams-polyfill@3.2.1": { + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "which-typed-array@1.1.11": { + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dependencies": { + "available-typed-arrays": "available-typed-arrays@1.0.5", + "call-bind": "call-bind@1.0.2", + "for-each": "for-each@0.3.3", + "gopd": "gopd@1.0.1", + "has-tostringtag": "has-tostringtag@1.0.0" + } + }, + "ws@8.13.0": { + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dependencies": {} + }, + "zod-error@1.5.0": { + "integrity": "sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==", + "dependencies": { + "zod": "zod@3.21.4" + } + }, + "zod@3.21.4": { + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "dependencies": {} + } + } + }, + "remote": {} +} diff --git a/references/deno-reference/main.ts b/references/deno-reference/main.ts new file mode 100644 index 000000000..de9336dea --- /dev/null +++ b/references/deno-reference/main.ts @@ -0,0 +1,41 @@ +import { TriggerClient } from "npm:@trigger.dev/sdk"; +import { eventTrigger } from "npm:@trigger.dev/sdk"; + +export const triggerClient = new TriggerClient({ + id: "borderless", + apiKey: "...", +}); + +// your first job +triggerClient.defineJob({ + id: "example-job", + name: "Example Job", + version: "0.0.1", + trigger: eventTrigger({ + name: "example.event", + }), + run: async (payload, io, ctx) => { + await io.logger.info("Hello world!", { payload }); + + return { + message: "Hello world!", + }; + }, +}); + +Deno.serve(async (req) => { + const response = await triggerClient.handleRequest(req); + if (!response) { + return Response.json( + { error: "Not found" }, + { + status: 404, + } + ); + } + + return Response.json(response.body, { + status: response.status, + headers: response.headers, + }); +}); From 9df93d079831243a38fd036538793d92f8d40634 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:55:22 +0100 Subject: [PATCH 78/79] CLI create-integration templates and shared configs (#511) * Add tsup config package * Add integration tsconfig to extend from * Return correct version via cli -v * Make create-integration use templates * Add changeset * Switch to liquidjs templates --------- Co-authored-by: Matt Aitken --- .changeset/gorgeous-panthers-run.md | 5 + config-packages/tsconfig/integration.json | 2 + config-packages/tsup/package.json | 9 + config-packages/tsup/src/index.ts | 3 + config-packages/tsup/src/integration.ts | 22 ++ config-packages/tsup/src/utils.ts | 32 ++ packages/cli/package.json | 1 + packages/cli/src/cli/index.ts | 5 +- .../cli/src/commands/createIntegration.ts | 220 +++++++------ .../src/templates/integration/README.md.j2 | 1 + .../src/templates/integration/events.js.j2 | 118 +++++++ .../cli/src/templates/integration/index.js.j2 | 244 ++++++++++++++ .../src/templates/integration/models.js.j2 | 79 +++++ .../src/templates/integration/package.json.j2 | 40 +++ .../integration/payload-examples/index.js.j2 | 40 +++ .../src/templates/integration/schemas.js.j2 | 120 +++++++ .../integration/tsconfig-external.json.j2 | 27 ++ .../integration/tsconfig-internal.json.j2 | 4 + .../integration/tsup.config-external.js.j2 | 20 ++ .../integration/tsup.config-internal.js.j2 | 7 + .../cli/src/templates/integration/types.js.j2 | 28 ++ .../cli/src/templates/integration/utils.js.j2 | 77 +++++ .../src/templates/integration/webhooks.js.j2 | 297 ++++++++++++++++++ .../createIntegrationFileFromTemplate.ts | 61 ++++ packages/cli/src/utils/parseNameAndPath.ts | 5 + 25 files changed, 1369 insertions(+), 98 deletions(-) create mode 100644 .changeset/gorgeous-panthers-run.md create mode 100644 config-packages/tsup/package.json create mode 100644 config-packages/tsup/src/index.ts create mode 100644 config-packages/tsup/src/integration.ts create mode 100644 config-packages/tsup/src/utils.ts create mode 100644 packages/cli/src/templates/integration/README.md.j2 create mode 100644 packages/cli/src/templates/integration/events.js.j2 create mode 100644 packages/cli/src/templates/integration/index.js.j2 create mode 100644 packages/cli/src/templates/integration/models.js.j2 create mode 100644 packages/cli/src/templates/integration/package.json.j2 create mode 100644 packages/cli/src/templates/integration/payload-examples/index.js.j2 create mode 100644 packages/cli/src/templates/integration/schemas.js.j2 create mode 100644 packages/cli/src/templates/integration/tsconfig-external.json.j2 create mode 100644 packages/cli/src/templates/integration/tsconfig-internal.json.j2 create mode 100644 packages/cli/src/templates/integration/tsup.config-external.js.j2 create mode 100644 packages/cli/src/templates/integration/tsup.config-internal.js.j2 create mode 100644 packages/cli/src/templates/integration/types.js.j2 create mode 100644 packages/cli/src/templates/integration/utils.js.j2 create mode 100644 packages/cli/src/templates/integration/webhooks.js.j2 create mode 100644 packages/cli/src/utils/createIntegrationFileFromTemplate.ts diff --git a/.changeset/gorgeous-panthers-run.md b/.changeset/gorgeous-panthers-run.md new file mode 100644 index 000000000..63a677380 --- /dev/null +++ b/.changeset/gorgeous-panthers-run.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +Improve create-integration output. Use templates and shared configs. diff --git a/config-packages/tsconfig/integration.json b/config-packages/tsconfig/integration.json index 753e6091d..ff9d795e5 100644 --- a/config-packages/tsconfig/integration.json +++ b/config-packages/tsconfig/integration.json @@ -3,6 +3,8 @@ "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2019"], "paths": { + "@trigger.dev/tsup/*": ["../../config-packages/tsup/src/*"], + "@trigger.dev/tsup": ["../../config-packages/tsup/src/index"], "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], "@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"], diff --git a/config-packages/tsup/package.json b/config-packages/tsup/package.json new file mode 100644 index 000000000..8af32c7ae --- /dev/null +++ b/config-packages/tsup/package.json @@ -0,0 +1,9 @@ +{ + "name": "@trigger.dev/tsup", + "version": "0.0.0", + "private": true, + "license": "MIT", + "devDependencies": { + "tsup": "7.1.x" + } +} diff --git a/config-packages/tsup/src/index.ts b/config-packages/tsup/src/index.ts new file mode 100644 index 000000000..661e53cca --- /dev/null +++ b/config-packages/tsup/src/index.ts @@ -0,0 +1,3 @@ +export { defineConfig } from "tsup"; +export { deepMergeOptions } from "./utils"; +export { options as integrationOptions } from "./integration"; diff --git a/config-packages/tsup/src/integration.ts b/config-packages/tsup/src/integration.ts new file mode 100644 index 000000000..0fb2a3d7d --- /dev/null +++ b/config-packages/tsup/src/integration.ts @@ -0,0 +1,22 @@ +import { Options, defineConfig } from "tsup"; + +export const options: Options = { + 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: [], + external: ["http", "https", "util", "events", "tty", "os", "timers"], +}; + +export default defineConfig(options); diff --git a/config-packages/tsup/src/utils.ts b/config-packages/tsup/src/utils.ts new file mode 100644 index 000000000..6e46ff742 --- /dev/null +++ b/config-packages/tsup/src/utils.ts @@ -0,0 +1,32 @@ +import { Options } from "tsup"; + +export const deepMergeOptions = deepMergeRecords; + +function deepMergeRecords>(...options: TRecord[]): TRecord { + const result = {} as TRecord; + + for (const option of options) { + for (const key in option) { + if (option.hasOwnProperty(key)) { + const optionValue = option[key]; + const existingValue = result[key]; + + if ( + existingValue && + typeof existingValue === "object" && + typeof optionValue === "object" && + !Array.isArray(existingValue) && + !Array.isArray(optionValue) && + existingValue !== null && + optionValue !== null + ) { + result[key] = deepMergeRecords(existingValue, optionValue); + } else { + result[key] = optionValue; + } + } + } + } + + return result; +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 23436472d..b87ac8dbe 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,6 +68,7 @@ "execa": "^7.0.0", "gradient-string": "^2.0.2", "inquirer": "^9.1.4", + "liquidjs": "^10.9.2", "localtunnel": "^2.0.2", "mock-fs": "^5.2.0", "nanoid": "^4.0.2", diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index 89829eb4f..cc3bc8713 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -14,7 +14,10 @@ import { checkApiKeyIsDevServer } from "../utils/getApiKeyType"; export const program = new Command(); -program.name(COMMAND_NAME).description("The Trigger.dev CLI").version("0.0.1"); +program + .name(COMMAND_NAME) + .description("The Trigger.dev CLI") + .version(getVersion(), "-v, --version", "Display the version number"); program .command("init") diff --git a/packages/cli/src/commands/createIntegration.ts b/packages/cli/src/commands/createIntegration.ts index 312daab0f..ebe1f0c27 100644 --- a/packages/cli/src/commands/createIntegration.ts +++ b/packages/cli/src/commands/createIntegration.ts @@ -9,7 +9,8 @@ import { generateIntegrationFiles } from "../utils/generateIntegrationFiles"; import { getPackageName } from "../utils/getPackagName"; import { installDependencies } from "../utils/installDependencies"; import { logger } from "../utils/logger"; -import { resolvePath } from "../utils/parseNameAndPath"; +import { relativePath, resolvePath } from "../utils/parseNameAndPath"; +import { createIntegrationFileFromTemplate } from "../utils/createIntegrationFileFromTemplate"; const CLIOptionsSchema = z.object({ packageName: z.string().optional(), @@ -91,108 +92,107 @@ export async function createIntegrationCommand(path: string, cliOptions: any) { process.exit(1); } - // Create the package.json - const packageJson = { - name: resolvedOptions.packageName, - version: "0.0.1", - description: `Trigger.dev integration for ${resolvedOptions.sdkPackage}`, - 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": "16.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: { - [latestVersion.name]: `^${latestVersion.version}`, - [sdkVersion.name]: sdkVersion.version, - [integrationKitVersion.name]: integrationKitVersion.version, - }, - engines: { - node: ">=16.8.0", - }, + const integrationVersion = await getInternalOrExternalPackageVersion({ + path: "integrations/github", + packageName: "@trigger.dev/github", + tag: "latest", + monorepoPath: triggerMonorepoPath, + prependWorkspace: false, + }); + + if (!integrationVersion) { + logger.error( + `Could not find the latest version of @trigger.dev/github. Please try again later.` + ); + + process.exit(1); + } + + const baseVariables = { + packageName: resolvedOptions.packageName, + sdkPackage: resolvedOptions.sdkPackage, + integrationVersion: integrationVersion, + latestVersion: latestVersion, + sdkVersion: sdkVersion, + integrationKitVersion: integrationKitVersion, + triggerMonorepoPath, }; - await createFileInPath(resolvedPath, "package.json", JSON.stringify(packageJson, null, 2)); + const getOutputPath = (relativePath: string) => pathModule.join(resolvedPath, relativePath); - // Create the tsconfig.json - const tsconfigJson = { - compilerOptions: { - composite: false, - declaration: false, - declarationMap: false, - esModuleInterop: true, - forceConsistentCasingInFileNames: true, - inlineSources: false, - isolatedModules: true, - moduleResolution: "node16", - noUnusedLocals: false, - noUnusedParameters: false, - preserveWatchOutput: true, - skipLibCheck: true, - strict: true, - experimentalDecorators: true, - emitDecoratorMetadata: true, - sourceMap: true, - resolveJsonModule: true, - lib: ["es2019"], - module: "commonjs", - target: "es2021", + const miscIntegrationFiles = [ + { + relativeTemplatePath: "package.json.j2", + outputPath: getOutputPath("package.json"), }, - include: ["./src/**/*.ts", "tsup.config.ts"], - exclude: ["node_modules"], - }; - - await createFileInPath(resolvedPath, "tsconfig.json", JSON.stringify(tsconfigJson, null, 2)); - - const readme = ` -# ${resolvedOptions.packageName} - `; - - await createFileInPath(resolvedPath, "README.md", readme); - - // Create the tsup.config.ts - const tsupConfig = ` -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", + { + // use `tsc --showConfig` to update external tsconfig + relativeTemplatePath: `tsconfig-${triggerMonorepoPath ? "internal" : "external"}.json.j2`, + outputPath: getOutputPath("tsconfig.json"), }, - esbuildPlugins: [], - external: ["http", "https", "util", "events", "tty", "os", "timers"], - }, -]); + { + relativeTemplatePath: `tsup.config-${triggerMonorepoPath ? "internal" : "external"}.js.j2`, + outputPath: getOutputPath("tsup.config.ts"), + }, + { + relativeTemplatePath: "README.md.j2", + outputPath: getOutputPath("README.md"), + }, + ]; -`; - - await createFileInPath(resolvedPath, "tsup.config.ts", tsupConfig); + await createIntegrationFiles(miscIntegrationFiles, baseVariables); + // create src/* if (resolvedOptions.skipGeneratingCode) { - await createFileInPath(resolvedPath, "src/index.ts", "export {}"); + const getSrcOutputPath = (relativePath: string) => + getOutputPath(pathModule.join("src", relativePath)); + + const srcIntegrationFiles = [ + { + relativeTemplatePath: pathModule.join("payload-examples", "index.js.j2"), + outputPath: getSrcOutputPath(pathModule.join("payload-examples", "index.ts")), + }, + { + relativeTemplatePath: "events.js.j2", + outputPath: getSrcOutputPath("events.ts"), + }, + { + relativeTemplatePath: "index.js.j2", + outputPath: getSrcOutputPath("index.ts"), + }, + { + relativeTemplatePath: "models.js.j2", + outputPath: getSrcOutputPath("models.ts"), + }, + { + relativeTemplatePath: "schemas.js.j2", + outputPath: getSrcOutputPath("schemas.ts"), + }, + { + relativeTemplatePath: "types.js.j2", + outputPath: getSrcOutputPath("types.ts"), + }, + { + relativeTemplatePath: "utils.js.j2", + outputPath: getSrcOutputPath("utils.ts"), + }, + { + relativeTemplatePath: "webhooks.js.j2", + outputPath: getSrcOutputPath("webhooks.ts"), + }, + ]; + + const validIdentifier = pathModule + .basename(path) + .replace(/[^a-zA-Z0-9]+/g, "") + .replace(/^[0-9]+/g, ""); + + await createIntegrationFiles(srcIntegrationFiles, { + ...baseVariables, + apiKeyPropertyName: "apiKey", // TODO: prompt for this + authMethod: resolvedOptions.authMethod, + identifier: validIdentifier.length ? validIdentifier : "packageName", + }); } else { await attemptToGenerateIntegrationFiles(pathModule.join(resolvedPath, "src"), resolvedOptions); } @@ -295,8 +295,9 @@ const resolveOptionsWithPrompts = async ( resolvedOptions.skipGeneratingCode = true; } + resolvedOptions.authMethod = await promptAuthMethod(); + if (!resolvedOptions.skipGeneratingCode) { - resolvedOptions.authMethod = await promptAuthMethod(); resolvedOptions.extraInfo = await promptExtraInfo(); } } catch (err) { @@ -448,11 +449,13 @@ async function getInternalOrExternalPackageVersion({ tag, path, monorepoPath, + prependWorkspace = true, }: { packageName: string; tag: string; path: string; monorepoPath?: string; + prependWorkspace?: boolean; }): Promise<{ name: string; version: string } | undefined> { if (!monorepoPath) { return await getLatestPackageVersion(packageName, tag); @@ -470,7 +473,7 @@ async function getInternalOrExternalPackageVersion({ return { name: packageJson.name, - version: `workspace:^${packageJson.version}`, + version: `${prependWorkspace ? "workspace:^" : ""}${packageJson.version}`, }; } @@ -550,3 +553,26 @@ async function updateJobCatalogWithNewIntegration( }; await writeJSONFile(tsConfigPath, newTsConfig); } + +const createIntegrationFiles = async ( + files: { + relativeTemplatePath: string; + outputPath: string; + }[], + variables?: Record +) => { + for (const file of files) { + const result = await createIntegrationFileFromTemplate({ ...file, variables }); + handleCreateResult(file.outputPath, result); + } +}; + +const handleCreateResult = ( + outputPath: string, + result: Awaited> +) => { + if (!result.success) { + throw new Error(`Failed to create ${pathModule.basename(outputPath)}: ${result.error}`); + } + logger.success(`✔ Created ${pathModule.basename(outputPath)} at ${relativePath(outputPath)}`); +}; diff --git a/packages/cli/src/templates/integration/README.md.j2 b/packages/cli/src/templates/integration/README.md.j2 new file mode 100644 index 000000000..f4e2541a2 --- /dev/null +++ b/packages/cli/src/templates/integration/README.md.j2 @@ -0,0 +1 @@ +# {{ packageName }} diff --git a/packages/cli/src/templates/integration/events.js.j2 b/packages/cli/src/templates/integration/events.js.j2 new file mode 100644 index 000000000..77d100907 --- /dev/null +++ b/packages/cli/src/templates/integration/events.js.j2 @@ -0,0 +1,118 @@ +import { EventSpecification } from "@trigger.dev/sdk"; +import { CommentEvent, IssueEvent } from "./schemas"; +import { Get{{ identifier | capitalize }}Payload } from "./types"; +import { + commentCreated, + commentRemoved, + commentUpdated, + issueCreated, + issueRemoved, + issueUpdated, +} from "./payload-examples"; +import { onCommentProperties, onIssueProperties, updatedFromProperties } from "./utils"; + +export const onComment: EventSpecification> = { + name: "Comment", + title: "On Comment", + source: "linear.app", + icon: "linear", + examples: [commentCreated, commentRemoved, commentUpdated], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + ...onCommentProperties(payload), + ...updatedFromProperties(payload), + ], +}; + +export const onCommentCreated: EventSpecification> = { + name: "Comment", + title: "On Comment Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [commentCreated], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => onCommentProperties(payload), +}; + +export const onCommentRemoved: EventSpecification> = { + name: "Comment", + title: "On Comment Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [commentRemoved], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => onCommentProperties(payload), +}; + +export const onCommentUpdated: EventSpecification> = { + name: "Comment", + title: "On Comment Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [commentUpdated], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)], +}; + +export const onIssue: EventSpecification> = { + name: "Issue", + title: "On Issue", + source: "linear.app", + icon: "linear", + examples: [issueCreated, issueRemoved, issueUpdated], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + ...onIssueProperties(payload), + ...updatedFromProperties(payload), + ], +}; + +export const onIssueCreated: EventSpecification> = { + name: "Issue", + title: "On Issue Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [issueCreated], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => onIssueProperties(payload), +}; + +export const onIssueRemoved: EventSpecification> = { + name: "Issue", + title: "On Issue Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [issueRemoved], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => onIssueProperties(payload), +}; + +export const onIssueUpdated: EventSpecification> = { + name: "Issue", + title: "On Issue Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [issueUpdated], + parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload, + runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)], +}; diff --git a/packages/cli/src/templates/integration/index.js.j2 b/packages/cli/src/templates/integration/index.js.j2 new file mode 100644 index 000000000..289de3ee1 --- /dev/null +++ b/packages/cli/src/templates/integration/index.js.j2 @@ -0,0 +1,244 @@ +import { + TriggerIntegration, + RunTaskOptions, + IO, + IOTask, + IntegrationTaskKey, + RunTaskErrorCallback, + Json, + retry, + ConnectionAuth, + Prettify, +} from "@trigger.dev/sdk"; +import {{ identifier | capitalize }}Client from "{{ sdkPackage }}"; + +import * as events from "./events"; +import { {{ identifier | capitalize }}ReturnType, Serialized{{ identifier | capitalize }}Output } from "./types"; +import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; +import { Models } from "./models"; + +export type {{ identifier | capitalize }}IntegrationOptions = { + id: string; + {{ apiKeyPropertyName }}: string; +}; + +export type {{ identifier | capitalize }}RunTask = InstanceType["runTask"]; + +export class {{ identifier | capitalize }}{{ " " }} implements TriggerIntegration { + private _options: {{ identifier | capitalize }}IntegrationOptions; + private _client?: any; + private _io?: IO; + private _connectionKey?: string; + + constructor(private options: {{ identifier | capitalize }}IntegrationOptions) { + if (Object.keys(options).includes("{{ apiKeyPropertyName }}") && !options.{{ apiKeyPropertyName }}) { + throw `Can't create {{ identifier | capitalize }} integration (${options.id}) as {{ apiKeyPropertyName }} was undefined`; + } + + this._options = options; + } + + get authSource() { + {% case authMethod %} + {% when "api-key" %} + return "LOCAL" as const; + {% when "oauth" %} + return "HOSTED" as const; + {% when "both-methods" %} + return this._options.{{ apiKeyPropertyName }} ? "LOCAL" : "HOSTED"; + {% endcase %} + } + + get id() { + return this.options.id; + } + + get metadata() { + return { id: "{{ identifier }}", name: "{{ identifier | capitalize }}" }; + } + + get source() { + return createWebhookEventSource(this); + } + + cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { + const {{ identifier }} = new {{ identifier | capitalize }}(this._options); + {{ identifier }}._io = io; + {{ identifier }}._connectionKey = connectionKey; + {{ identifier }}._client = this.createClient(auth); + return {{ identifier }}; + } + + createClient(auth?: ConnectionAuth) { + // oauth + if (auth) { + return new {{ identifier | capitalize }}Client({ + auth: auth.accessToken, + }); + } + + // apiKey auth + if (this._options.{{ apiKeyPropertyName }}) { + return new {{ identifier | capitalize }}Client({ + apiKey: this._options.{{ apiKeyPropertyName }}, + }); + } + + throw new Error("No auth"); + } + + runTask | void>( + key: IntegrationTaskKey, + callback: (client: {{ identifier | capitalize }}Client, task: IOTask, io: IO) => Promise, + options?: RunTaskOptions, + errorCallback?: RunTaskErrorCallback + ): Promise { + if (!this._io) throw new Error("No IO"); + if (!this._connectionKey) throw new Error("No connection key"); + + return this._io.runTask( + key, + (task, io) => { + if (!this._client) throw new Error("No client"); + return callback(this._client, task, io); + }, + { + icon: "{{ identifier }}", + retry: retry.standardBackoff, + ...(options ?? {}), + connectionKey: this._connectionKey, + }, + errorCallback ?? onError + ); + } + + // top-level task + + request( + key: IntegrationTaskKey, + params: { + route: string | URL; + options: Parameters<{{ identifier | capitalize }}Client["request"]>[1]; + } + ): {{ identifier | capitalize }}ReturnType { + return this.runTask( + key, + async (client) => { + const response = await client.request(params.route, params.options); + + return response.json(); + }, + { + name: "Send Request", + params, + properties: [ + { label: "Route", text: params.route.toString() }, + ...(params.options.method ? [{ label: "Method", text: params.options.method }] : []), + ], + callback: { enabled: true }, + } + ); + } + + // nested tasks + + get models() { + return new Models(this.runTask.bind(this)); + } + + // events + + onComment(params: TriggerParams = {}) { + return createTrigger(this.source, events.onComment, params); + } + + onCommentCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCommentCreated, params); + } + + onCommentRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCommentRemoved, params); + } + + onCommentUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCommentUpdated, params); + } + + // triggers (webhooks) + + // private, just here to keep webhook logic in a separate file + get #webhooks() { + return new Webhooks(this.runTask.bind(this)); + } + + webhook = this.#webhooks.webhook; + webhooks = this.#webhooks.webhooks; + + createWebhook = this.#webhooks.createWebhook; + deleteWebhook = this.#webhooks.deleteWebhook; + updateWebhook = this.#webhooks.updateWebhook; +} + +class {{ identifier | capitalize }}ApiError extends Error { + constructor( + message: string, + readonly request: Request, + readonly response: Response + ) { + super(message); + this.name = "{{ identifier | capitalize }}ApiError"; + } +} + +function is{{ identifier | capitalize }}ApiError(error: unknown): error is {{ identifier | capitalize }}ApiError { + if (typeof error !== "object" || error === null) { + return false; + } + + const apiError = error as {{ identifier | capitalize }}ApiError; + + return ( + apiError.name === "{{ identifier | capitalize }}ApiError" && + apiError.request instanceof Request && + apiError.response instanceof Response + ); +} + +function shouldRetry(method: string, status: number) { + return status === 429 || (method === "GET" && status >= 500); +} + +export function onError(error: unknown): ReturnType { + if (!is{{ identifier | capitalize }}ApiError(error)) { + return; + } + + if (!shouldRetry(error.request.method, error.response.status)) { + return { + skipRetrying: true, + }; + } + + const rateLimitRemaining = error.response.headers.get("ratelimit-remaining"); + const rateLimitReset = error.response.headers.get("ratelimit-reset"); + + if (rateLimitRemaining === "0" && rateLimitReset) { + const resetDate = new Date(Number(rateLimitReset) * 1000); + + if (!Number.isNaN(resetDate.getTime())) { + return { + retryAt: resetDate, + error, + }; + } + } +} + +export const serialize{{ identifier | capitalize }}Output = (obj: T): Prettify> => { + return JSON.parse(JSON.stringify(obj), (key, value) => { + if (typeof value === "function" || key.startsWith("_")) { + return undefined; + } + return value; + }); +}; diff --git a/packages/cli/src/templates/integration/models.js.j2 b/packages/cli/src/templates/integration/models.js.j2 new file mode 100644 index 000000000..0d67c7952 --- /dev/null +++ b/packages/cli/src/templates/integration/models.js.j2 @@ -0,0 +1,79 @@ +import { IntegrationTaskKey } from "@trigger.dev/sdk"; +import { Model, ModelVersion } from "{{ sdkPackage }}"; + +import { {{ capitalizedIdentifier }}RunTask } from "./index"; +import { modelProperties } from "./utils"; +import { {{ capitalizedIdentifier }}ReturnType } from "./types"; + +export class Models { + constructor(private runTask: {{ capitalizedIdentifier }}RunTask) {} + + get( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + } + ): {{ capitalizedIdentifier }}ReturnType { + return this.runTask( + key, + (client) => { + return client.models.get(params.model_owner, params.model_name); + }, + { + name: "Get Model", + params, + properties: modelProperties(params), + } + ); + } + + get versions() { + return new Versions(this.runTask); + } +} + +class Versions { + constructor(private runTask: {{ capitalizedIdentifier }}RunTask) {} + + get( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + version_id: string; + } + ): {{ capitalizedIdentifier }}ReturnType { + return this.runTask( + key, + (client) => { + return client.models.versions.get(params.model_owner, params.model_name, params.version_id); + }, + { + name: "Get Model Version", + params, + properties: modelProperties(params), + } + ); + } + + list( + key: IntegrationTaskKey, + params: { + model_owner: string; + model_name: string; + } + ): {{ capitalizedIdentifier }}ReturnType { + return this.runTask( + key, + (client) => { + return client.models.versions.list(params.model_owner, params.model_name); + }, + { + name: "List Models", + params, + properties: modelProperties(params), + } + ); + } +} diff --git a/packages/cli/src/templates/integration/package.json.j2 b/packages/cli/src/templates/integration/package.json.j2 new file mode 100644 index 000000000..6e1391c90 --- /dev/null +++ b/packages/cli/src/templates/integration/package.json.j2 @@ -0,0 +1,40 @@ +{ + "name": "{{ packageName }}", + "version": "{{ integrationVersion.version }}", + "description": "Trigger.dev integration for {{ sdkPackage }}", + "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": { + {% if triggerMonorepoPath %} + "@trigger.dev/tsconfig": "workspace:*", + "@trigger.dev/tsup": "workspace:*", + {% endif %} + "@types/node": "16.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": { + "{{ latestVersion.name }}": "^{{ latestVersion.version }}", + "{{ sdkVersion.name }}": "{{ sdkVersion.version }}", + "{{ integrationKitVersion.name }}": "{{ integrationKitVersion.version }}", + "zod": "3.21.4" + }, + "engines": { + "node": ">=16.8.0" + } +} diff --git a/packages/cli/src/templates/integration/payload-examples/index.js.j2 b/packages/cli/src/templates/integration/payload-examples/index.js.j2 new file mode 100644 index 000000000..a35e18d3e --- /dev/null +++ b/packages/cli/src/templates/integration/payload-examples/index.js.j2 @@ -0,0 +1,40 @@ +import { EventSpecificationExample } from "@trigger.dev/sdk"; + +import CommentCreated from "./CommentCreated.json" +import CommentRemoved from "./CommentRemoved.json" +import CommentUpdated from "./CommentUpdated.json" +import IssueCreated from "./IssueCreated.json" +import IssueRemoved from "./IssueRemoved.json" +import IssueUpdated from "./IssueUpdated.json" + +export const commentCreated: EventSpecificationExample = { + id: "CommentCreated", + name: "Comment created", + payload: CommentCreated, +}; +export const commentRemoved: EventSpecificationExample = { + id: "CommentRemoved", + name: "Comment removed", + payload: CommentRemoved, +}; +export const commentUpdated: EventSpecificationExample = { + id: "CommentUpdated", + name: "Comment updated", + payload: CommentUpdated, +}; + +export const issueCreated: EventSpecificationExample = { + id: "IssueCreated", + name: "Issue created", + payload: IssueCreated, +}; +export const issueRemoved: EventSpecificationExample = { + id: "IssueRemoved", + name: "Issue removed", + payload: IssueRemoved, +}; +export const issueUpdated: EventSpecificationExample = { + id: "IssueUpdated", + name: "Issue updated", + payload: IssueUpdated, +}; diff --git a/packages/cli/src/templates/integration/schemas.js.j2 b/packages/cli/src/templates/integration/schemas.js.j2 new file mode 100644 index 000000000..9c51092e8 --- /dev/null +++ b/packages/cli/src/templates/integration/schemas.js.j2 @@ -0,0 +1,120 @@ +import { z } from "zod"; + +export const WebhookResourceTypeSchema = z.union([ + z.literal("Comment"), + z.literal("Issue"), +]); +export type WebhookResourceType = z.infer; + +export const WebhookActionTypeSchema = z.union([ + z.literal("create"), + z.literal("remove"), + z.literal("update"), +]); +export type WebhookActionType = z.infer; + +const IssueLabelDataSchema = z.object({ + archivedAt: z.coerce.date().optional().nullable(), + color: z.string(), + createdAt: z.coerce.date(), + creatorId: z.string().optional().nullable(), + description: z.string().optional().nullable(), + id: z.string(), + name: z.string(), + organizationId: z.string(), + parentId: z.string().optional().nullable(), + teamId: z.string().optional().nullable(), + updatedAt: z.coerce.date(), +}); + +const IssueDataSchema = z.object({ + archivedAt: z.coerce.date().optional().nullable(), + assignee: z.object({ id: z.string(), name: z.string() }).optional().nullable(), + assigneeId: z.string().optional().nullable(), + id: z.string(), + labelIds: z.array(z.string()), + labels: z.array(IssueLabelDataSchema.pick({ id: true, color: true, name: true })), + number: z.number(), + parentId: z.string().optional().nullable(), + previousIdentifiers: z.array(z.string()), + priority: z.number(), + priorityLabel: z.string(), + projectId: z.string().optional().nullable(), + sortOrder: z.number(), + team: z.object({ id: z.string(), key: z.string(), name: z.string() }), + teamId: z.string(), + title: z.string(), + trashed: z.boolean().optional().nullable(), + triagedAt: z.coerce.date().optional().nullable(), + updatedAt: z.coerce.date(), +}); + +const CommentDataSchema = z.object({ + archivedAt: z.coerce.date().optional().nullable(), + body: z.string(), + botActorId: z.string().optional().nullable(), + createdAt: z.coerce.date(), + editedAt: z.string().optional().nullable(), + id: z.string(), + issue: IssueDataSchema.pick({ id: true, title: true }), + issueId: z.string(), + parentId: z.string().optional().nullable(), + reactionData: z.array(z.object({}).passthrough()), + updatedAt: z.coerce.date(), + userId: z.string().optional().nullable(), +}); + +export const WebhookPayloadBaseSchema = z.object({ + createdAt: z.coerce.date(), + organizationId: z.string().optional().nullable(), + url: z.string().url().optional().nullable(), + webhookId: z.string(), + webhookTimestamp: z.coerce.date(), +}); + +const CREATE = z.literal("create"); +const REMOVE = z.literal("remove"); +const UPDATE = z.literal("update"); + +export const CommentEventBaseSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Comment"), + data: CommentDataSchema, +}); +export const CommentEventSchema = z.discriminatedUnion("action", [ + CommentEventBaseSchema.extend({ + action: CREATE, + }), + CommentEventBaseSchema.extend({ + action: REMOVE, + }), + CommentEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: CommentDataSchema.partial(), + }), +]); +export type CommentEvent = z.infer; + +export const IssueEventBaseSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Issue"), + data: IssueDataSchema, +}); +export const IssueEventSchema = z.discriminatedUnion("action", [ + IssueEventBaseSchema.extend({ + action: CREATE, + }), + IssueEventBaseSchema.extend({ + action: REMOVE, + }), + IssueEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: IssueDataSchema.partial(), + }), +]); +export type IssueEvent = z.infer; + +export const WebhookPayloadSchema = z.union([ + CommentEventSchema, + IssueEventSchema, +]); + +export type WebhookPayload = z.infer; diff --git a/packages/cli/src/templates/integration/tsconfig-external.json.j2 b/packages/cli/src/templates/integration/tsconfig-external.json.j2 new file mode 100644 index 000000000..2a94d35fa --- /dev/null +++ b/packages/cli/src/templates/integration/tsconfig-external.json.j2 @@ -0,0 +1,27 @@ +{ + "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": ["dom", "dom.iterable", "es2019"], + "module": "commonjs", + "target": "es2021", + "stripInternal": true + }, + "include": ["./src/**/*.ts", "tsup.config.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/cli/src/templates/integration/tsconfig-internal.json.j2 b/packages/cli/src/templates/integration/tsconfig-internal.json.j2 new file mode 100644 index 000000000..26ae70a15 --- /dev/null +++ b/packages/cli/src/templates/integration/tsconfig-internal.json.j2 @@ -0,0 +1,4 @@ +{ + "extends": "@trigger.dev/tsconfig/integration.json", + "include": ["./src/**/*.ts", "tsup.config.ts"] +} diff --git a/packages/cli/src/templates/integration/tsup.config-external.js.j2 b/packages/cli/src/templates/integration/tsup.config-external.js.j2 new file mode 100644 index 000000000..d1145e2b9 --- /dev/null +++ b/packages/cli/src/templates/integration/tsup.config-external.js.j2 @@ -0,0 +1,20 @@ +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: [], + external: ["http", "https", "util", "events", "tty", "os", "timers"], +}); diff --git a/packages/cli/src/templates/integration/tsup.config-internal.js.j2 b/packages/cli/src/templates/integration/tsup.config-internal.js.j2 new file mode 100644 index 000000000..3071b229a --- /dev/null +++ b/packages/cli/src/templates/integration/tsup.config-internal.js.j2 @@ -0,0 +1,7 @@ +import { defineConfig, deepMergeOptions, integrationOptions } from "@trigger.dev/tsup"; + +const options = deepMergeOptions(integrationOptions, { + // extend base config here +}); + +export default defineConfig(options); diff --git a/packages/cli/src/templates/integration/types.js.j2 b/packages/cli/src/templates/integration/types.js.j2 new file mode 100644 index 000000000..1a7ff133a --- /dev/null +++ b/packages/cli/src/templates/integration/types.js.j2 @@ -0,0 +1,28 @@ +import { Request } from "{{ sdkPackage }}"; +import { WebhookActionType, WebhookPayload } from "./schemas"; + +export type Get{{ identifier | capitalize }}Payload< + TPayload extends WebhookPayload, + TAction extends any = any, +> = TAction extends WebhookActionType ? Extract : TPayload; + +type FunctionKeys = { + [K in keyof T]: T[K] extends Function ? K : never; +}[keyof T]; + +export type Serialized{{ identifier | capitalize }}Output = T extends object + ? T extends Array + ? Array> + : { [K in keyof T as Exclude | `_${string}`>]: Serialized{{ identifier | capitalize }}Output } + : T; + +export type {{ identifier | capitalize }}ReturnType< + TPayload extends Omit, + K extends unknown = unknown, +> = Promise< + Awaited>> +>; + +export type AwaitNested = Omit & { + [key in K]: Awaited; +}; diff --git a/packages/cli/src/templates/integration/utils.js.j2 b/packages/cli/src/templates/integration/utils.js.j2 new file mode 100644 index 000000000..f3fe00dc9 --- /dev/null +++ b/packages/cli/src/templates/integration/utils.js.j2 @@ -0,0 +1,77 @@ +import { CommentEvent, IssueEvent, WebhookPayload } from "./schemas"; +import { Get{{ identifier | capitalize }}Payload } from "./types"; + +export type QueryVariables = { + after: string; + before: string; + first: number; + includeArchived: boolean; + last: number; +}; + +export type Nullable = Partial<{ + [K in keyof T]: T[K] | null; +}>; + +export const onCommentProperties = (payload: Get{{ identifier | capitalize }}Payload) => { + return [ + { label: "Comment ID", text: payload.data.id }, + { label: "Issue ID", text: payload.data.issueId }, + { label: "Issue Title", text: payload.data.issue.title, url: payload.url ?? undefined }, + ]; +}; + +export const onIssueProperties = (payload: Get{{ identifier | capitalize }}Payload) => { + return [ + { label: "Issue ID", text: payload.data.id }, + { + label: "Issue", + text: `[${payload.data.team.key}-${payload.data.number}] ${payload.data.title}`, + url: payload.url ?? undefined, + }, + ]; +}; + +export const queryProperties = (query: Nullable) => { + return [ + ...(query.after ? [{ label: "After", text: query.after }] : []), + ...(query.before ? [{ label: "Before", text: query.before }] : []), + ...(query.first ? [{ label: "First", text: String(query.first) }] : []), + ...(query.last ? [{ label: "Last", text: String(query.last) }] : []), + ...(query.includeArchived + ? [{ label: "Include archived", text: String(query.includeArchived) }] + : []), + ]; +}; + +export const updatedFromProperties = (payload: WebhookPayload) => { + if (payload.action !== "update") return []; + return [ + { + label: "Updated Keys", + text: Object.keys(payload.updatedFrom) + .filter((key) => !["editedAt", "updatedAt"].includes(key)) + .join(", "), + }, + ]; +}; + +export const modelProperties = ( + params: Partial<{ + model_owner: string; + model_name: string; + version_id: string; + destination: string; + }> +) => { + return [ + ...(params.model_owner ? [{ label: "Model Owner", text: params.model_owner }] : []), + ...(params.model_name ? [{ label: "Model Name", text: params.model_name }] : []), + ...(params.version_id ? [{ label: "Model Version", text: params.version_id }] : []), + ...(params.destination ? [{ label: "Destination Model", text: params.destination }] : []), + ]; +}; + +export const streamingProperty = (params: { stream?: boolean }) => { + return [{ label: "Streaming Enabled", text: String(!!params.stream) }]; +}; diff --git a/packages/cli/src/templates/integration/webhooks.js.j2 b/packages/cli/src/templates/integration/webhooks.js.j2 new file mode 100644 index 000000000..9bb55e514 --- /dev/null +++ b/packages/cli/src/templates/integration/webhooks.js.j2 @@ -0,0 +1,297 @@ +import { + EventFilter, + ExternalSource, + ExternalSourceTrigger, + HandlerEvent, + IntegrationTaskKey, + Logger, +} from "@trigger.dev/sdk"; +import { + Document, + {{ identifier | capitalize }}Webhooks, + WebhookPayload, + DeletePayload, + Webhook, +} from "{{ sdkPackage }}"; +import { z } from "zod"; +import * as events from "./events"; +import { {{ identifier | capitalize }}, {{ identifier | capitalize }}RunTask, serialize{{ identifier | capitalize }}Output } from "./index"; +import { WebhookPayloadSchema } from "./schemas"; +import { {{ identifier | capitalize }}ReturnType } from "./types"; +import { queryProperties } from "./utils"; + +export class Webhooks { + runTask: {{ identifier | capitalize }}RunTask; + + constructor(runTask: {{ identifier | capitalize }}RunTask) { + this.runTask = runTask; + } + + webhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType { + return this.runTask( + key, + async (client, task, io) => { + return serialize{{ identifier | capitalize }}Output(await client.webhook(params.id)); + }, + { + name: "Get Webhook", + params, + properties: [{ label: "Webhook ID", text: params.id }], + } + ); + } + + webhooks(key: IntegrationTaskKey, params?: Document.WebhooksQueryVariables): {{ identifier | capitalize }}ReturnType { + return this.runTask( + key, + async (client, task, io) => { + let connections = await client.webhooks(params); + const hooks = connections.nodes; + while (connections.pageInfo.hasNextPage) { + connections = await connections.fetchNext(); + hooks.push(...connections.nodes); + } + return serialize{{ identifier | capitalize }}Output(hooks); + }, + { + name: "List Webhooks", + params, + properties: queryProperties(params ?? {}), + } + ); + } + + createWebhook( + key: IntegrationTaskKey, + params: Document.WebhookCreateInput + ): {{ identifier | capitalize }}ReturnType & { webhook: Webhook | undefined }> { + return this.runTask( + key, + async (client, task, io) => { + const payload = await client.createWebhook({ ...params, allPublicTeams: !params.teamId }); + return serialize{{ identifier | capitalize }}Output({ + ...payload, + webhook: await payload.webhook, + }); + }, + { + name: "Create Webhook", + params, + properties: [ + { label: "Webhook URL", text: params.url }, + { label: "Resource Types", text: params.resourceTypes.join(", ") }, + ], + } + ); + } + + deleteWebhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType { + return this.runTask( + key, + async (client, task, io) => { + return serialize{{ identifier | capitalize }}Output(await client.deleteWebhook(params.id)); + }, + { + name: "Delete Webhook", + params, + properties: [{ label: "Webhook ID", text: params.id }], + } + ); + } + + updateWebhook( + key: IntegrationTaskKey, + params: { id: string; input: Document.WebhookUpdateInput } + ): {{ identifier | capitalize }}ReturnType & { webhook: Webhook | undefined }> { + return this.runTask( + key, + async (client, task) => { + const payload = await client.updateWebhook(params.id, params.input); + return serialize{{ identifier | capitalize }}Output({ + ...payload, + webhook: await payload.webhook, + }); + }, + { + name: "Update Webhook", + params, + properties: [ + { label: "Webhook ID", text: params.id }, + ...(params.input.url ? [{ label: "Webhook URL", text: params.input.url }] : []), + ...(params.input.resourceTypes + ? [{ label: "Resource Types", text: params.input.resourceTypes.join(", ") }] + : []), + ], + } + ); + } +} + +type {{ identifier | capitalize }}Events = (typeof events)[keyof typeof events]; + +export type TriggerParams = { + teamId?: string; + filter?: EventFilter; +}; + +type CreateTriggersResult = ExternalSourceTrigger< + TEventSpecification, + ReturnType +>; + +export function createTrigger( + source: ReturnType, + event: TEventSpecification, + params: TriggerParams +): CreateTriggersResult { + return new ExternalSourceTrigger({ + event, + params, + source, + options: {}, + }); +} + +const WebhookRegistrationDataSchema = z.object({ + success: z.literal(true), + webhook: z.object({ + id: z.string(), + enabled: z.boolean(), + }), +}); + +export function createWebhookEventSource( + integration: {{ identifier | capitalize }} +): ExternalSource<{{ identifier | capitalize }}, TriggerParams, "HTTP", {}> { + return new ExternalSource("HTTP", { + id: "{{ identifier }}.webhook", + schema: z.object({ + teamId: z.string().optional(), + }), + version: "0.1.0", + integration, + key: (params) => `${params.teamId ? params.teamId : "all"}`, + handler: webhookHandler, + register: async (event, io, ctx) => { + const { params, source: httpSource, options } = event; + + // (key-specific) stored data, undefined if not registered yet + const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data); + + // set of events to register + const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing])); + const registeredOptions = { + event: allEvents, + }; + + // easily identify webhooks on {{ identifier }} + const label = `trigger.${params.teamId ? params.teamId : "all"}`; + + if (httpSource.active && webhookData.success) { + const hasMissingOptions = Object.values(options).some( + (option) => option.missing.length > 0 + ); + if (!hasMissingOptions) return; + + const updatedWebhook = await io.integration.updateWebhook("update-webhook", { + id: webhookData.data.webhook.id, + input: { + label, + resourceTypes: allEvents, + secret: httpSource.secret, + url: httpSource.url, + }, + }); + + return { + data: WebhookRegistrationDataSchema.parse(updatedWebhook), + options: registeredOptions, + }; + } + + // check for existing hooks that match url + const listResponse = await io.integration.webhooks("list-webhooks"); + const existingWebhook = listResponse.find((w) => w.url === httpSource.url); + + if (existingWebhook) { + const updatedWebhook = await io.integration.updateWebhook("update-webhook", { + id: existingWebhook.id, + input: { + label, + resourceTypes: allEvents, + secret: httpSource.secret, + url: httpSource.url, + }, + }); + + return { + data: WebhookRegistrationDataSchema.parse(updatedWebhook), + options: registeredOptions, + }; + } + + const createPayload = await io.integration.createWebhook("create-webhook", { + label, + resourceTypes: allEvents, + secret: httpSource.secret, + teamId: params.teamId, + url: httpSource.url, + }); + + return { + data: WebhookRegistrationDataSchema.parse(createPayload), + secret: (await createPayload.webhook)?.secret, + options: registeredOptions, + }; + }, + }); +} + +async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: {{ identifier | capitalize }}) { + logger.debug("[@trigger.dev/{{ identifier }}] Handling webhook payload"); + + const { rawEvent: request, source } = event; + + const payloadUuid = request.headers.get("{{ identifier | capitalize }}-Delivery"); + const payloadEvent = request.headers.get("{{ identifier | capitalize }}-Event"); + + if (!payloadUuid || !payloadEvent) { + logger.debug("[@trigger.dev/{{ identifier }}] Missing required {{ identifier | capitalize }} headers"); + return { events: [] }; + } + + if (!request.body) { + logger.debug("[@trigger.dev/{{ identifier }}] No body found"); + return { events: [] }; + } + + const signature = request.headers.get("WEBHOOK_SIGNATURE_HEADER"); + + if (!signature) { + logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, no signature found"); + throw Error("[@trigger.dev/{{ identifier }}] No signature found"); + } + + const rawBody = await request.text(); + const body = JSON.parse(rawBody); + const webhookHelper = new {{ identifier | capitalize }}Webhooks(source.secret); + + if (!webhookHelper.verify(Buffer.from(rawBody), signature)) { + logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, they don't match"); + throw Error("[@trigger.dev/{{ identifier }}] Invalid signature"); + } + + const webhookPayload = WebhookPayloadSchema.parse(body); + + return { + events: [ + { + id: payloadUuid, + name: payloadEvent, + source: "{{ identifier }}.app", + payload: webhookPayload, + context: {}, + }, + ], + }; +} diff --git a/packages/cli/src/utils/createIntegrationFileFromTemplate.ts b/packages/cli/src/utils/createIntegrationFileFromTemplate.ts new file mode 100644 index 000000000..8ac261a53 --- /dev/null +++ b/packages/cli/src/utils/createIntegrationFileFromTemplate.ts @@ -0,0 +1,61 @@ +import fs from "fs/promises"; +import { Liquid } from "liquidjs"; +import path from "path"; + +import { pathExists } from "./fileSystem"; +import { templatesPath } from "../paths"; + +type Result = + | { + success: true; + alreadyExisted: boolean; + } + | { + success: false; + error: string; + }; + +const templatesDir = path.join(templatesPath(), "integration"); + +const liquid = new Liquid({ + root: templatesDir, + trimTagRight: true, + trimOutputRight: true, +}); + +export async function createIntegrationFileFromTemplate(params: { + relativeTemplatePath: string; + variables?: Record; + outputPath: string; +}): Promise { + if (await pathExists(params.outputPath)) { + return { + success: true, + alreadyExisted: true, + }; + } + + try { + const output = await liquid.renderFile(params.relativeTemplatePath, params.variables); + + const directoryName = path.dirname(params.outputPath); + await fs.mkdir(directoryName, { recursive: true }); + await fs.writeFile(params.outputPath, output); + + return { + success: true, + alreadyExisted: false, + }; + } catch (e) { + if (e instanceof Error) { + return { + success: false, + error: e.message, + }; + } + return { + success: false, + error: JSON.stringify(e), + }; + } +} diff --git a/packages/cli/src/utils/parseNameAndPath.ts b/packages/cli/src/utils/parseNameAndPath.ts index 400a534cf..e6016c0d9 100644 --- a/packages/cli/src/utils/parseNameAndPath.ts +++ b/packages/cli/src/utils/parseNameAndPath.ts @@ -4,3 +4,8 @@ import pathModule from "path"; export const resolvePath = (input: string) => { return pathModule.resolve(process.cwd(), input); }; + +// Takes an absolute path and derives the relative path from the current working directory +export const relativePath = (input: string) => { + return pathModule.relative(process.cwd(), input); +}; From b9eba68078214c71716abe8ba9c39cf9f7149bb2 Mon Sep 17 00:00:00 2001 From: Ravan <69167444+RamK777-stack@users.noreply.github.com> Date: Fri, 13 Oct 2023 22:14:05 +0530 Subject: [PATCH 79/79] feat: export base file (#610) * feat: export base file * Added a change set --------- Co-authored-by: Matt Aitken --- .changeset/warm-elephants-battle.md | 5 +++++ integrations/airtable/src/index.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/warm-elephants-battle.md diff --git a/.changeset/warm-elephants-battle.md b/.changeset/warm-elephants-battle.md new file mode 100644 index 000000000..58e9abcd5 --- /dev/null +++ b/.changeset/warm-elephants-battle.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/airtable": patch +--- + +Export Base and Table diff --git a/integrations/airtable/src/index.ts b/integrations/airtable/src/index.ts index b4d9d542c..bc9ff2402 100644 --- a/integrations/airtable/src/index.ts +++ b/integrations/airtable/src/index.ts @@ -15,6 +15,7 @@ import { Base } from "./base"; import { Webhooks, createWebhookEventSource } from "./webhooks"; export * from "./types"; +export * from "./base"; export type AirtableIntegrationOptions = { /** An ID for this client */