v3: Runs List Management SDK and SDK improvements (#1133)
* Improved the existing runs API
* WIP next runs API
* Improve the returned ApiPromise to add ability to return response
* More WIP
* WI{
* Added offset/limit pagination stuff like the cursor one, and converted all API methods to use ApiPromise
* More run API stuff
- Adding schedule output from the retrieveRun endpoint
- Ability to filter by schedule and isTest
* Remove env from retrieve run in openAPI
* prefer duplication over merge
* WIP docs
* Use spread to DRY up some run API schemas
* Finish the overview docs
* Adding changeset
* Fixed typecheck errors
* Typo fix
* Re-export zodfetch from core so the v3 CLI can use it
* Fixed type errors
---------
Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@trigger.dev/sdk": patch
|
||||||
|
"@trigger.dev/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Management SDK overhaul and adding the runs.list API
|
||||||
@@ -170,6 +170,7 @@ const EnvironmentSchema = z.object({
|
|||||||
MAX_SEQUENTIAL_INDEX_FAILURE_COUNT: z.coerce.number().default(96),
|
MAX_SEQUENTIAL_INDEX_FAILURE_COUNT: z.coerce.number().default(96),
|
||||||
|
|
||||||
LOOPS_API_KEY: z.string().optional(),
|
LOOPS_API_KEY: z.string().optional(),
|
||||||
|
MARQS_DISABLE_REBALANCING: z.coerce.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
|
|||||||
select: {
|
select: {
|
||||||
id: true;
|
id: true;
|
||||||
type: true;
|
type: true;
|
||||||
|
slug: true;
|
||||||
orgMember: {
|
orgMember: {
|
||||||
select: {
|
select: {
|
||||||
user: {
|
user: {
|
||||||
@@ -155,6 +156,7 @@ export function displayableEnvironment(
|
|||||||
return {
|
return {
|
||||||
id: environment.id,
|
id: environment.id,
|
||||||
type: environment.type,
|
type: environment.type,
|
||||||
|
slug: environment.slug,
|
||||||
userName,
|
userName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { AttemptStatus, RetrieveRunResponse, RunStatus, logger } from "@trigger.dev/core/v3";
|
import {
|
||||||
import { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
|
AttemptStatus,
|
||||||
|
RetrieveRunResponse,
|
||||||
|
RunStatus,
|
||||||
|
SerializedError,
|
||||||
|
TaskRunError,
|
||||||
|
conditionallyImportPacket,
|
||||||
|
createJsonErrorObject,
|
||||||
|
logger,
|
||||||
|
parsePacket,
|
||||||
|
} from "@trigger.dev/core/v3";
|
||||||
|
import { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
|
||||||
import assertNever from "assert-never";
|
import assertNever from "assert-never";
|
||||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||||
import { BasePresenter } from "./basePresenter.server";
|
import { BasePresenter } from "./basePresenter.server";
|
||||||
@@ -23,6 +33,7 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
lockedToVersion: true,
|
lockedToVersion: true,
|
||||||
|
schedule: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,14 +43,68 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let $payload: any;
|
||||||
|
let $output: any;
|
||||||
|
|
||||||
|
if (showSecretDetails) {
|
||||||
|
const payloadPacket = await conditionallyImportPacket({
|
||||||
|
data: taskRun.payload,
|
||||||
|
dataType: taskRun.payloadType,
|
||||||
|
});
|
||||||
|
|
||||||
|
$payload =
|
||||||
|
payloadPacket.dataType === "application/json"
|
||||||
|
? await parsePacket(payloadPacket)
|
||||||
|
: payloadPacket.data;
|
||||||
|
|
||||||
|
if (taskRun.status === "COMPLETED_SUCCESSFULLY") {
|
||||||
|
const completedAttempt = taskRun.attempts.find(
|
||||||
|
(a) => a.status === "COMPLETED" && typeof a.output !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (completedAttempt && completedAttempt.output) {
|
||||||
|
const outputPacket = await conditionallyImportPacket({
|
||||||
|
data: completedAttempt.output,
|
||||||
|
dataType: completedAttempt.outputType,
|
||||||
|
});
|
||||||
|
|
||||||
|
$output = await parsePacket(outputPacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiStatus = ApiRetrieveRunPresenter.apiStatusFromRunStatus(taskRun.status);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: taskRun.friendlyId,
|
id: taskRun.friendlyId,
|
||||||
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(taskRun.status),
|
status: apiStatus,
|
||||||
taskIdentifier: taskRun.taskIdentifier,
|
taskIdentifier: taskRun.taskIdentifier,
|
||||||
idempotencyKey: taskRun.idempotencyKey ?? undefined,
|
idempotencyKey: taskRun.idempotencyKey ?? undefined,
|
||||||
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
|
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
|
||||||
createdAt: taskRun.createdAt ?? undefined,
|
createdAt: taskRun.createdAt ?? undefined,
|
||||||
updatedAt: taskRun.updatedAt ?? undefined,
|
updatedAt: taskRun.updatedAt ?? undefined,
|
||||||
|
startedAt: taskRun.lockedAt ?? undefined,
|
||||||
|
finishedAt: ApiRetrieveRunPresenter.isStatusFinished(apiStatus)
|
||||||
|
? taskRun.updatedAt
|
||||||
|
: undefined,
|
||||||
|
payload: $payload,
|
||||||
|
output: $output,
|
||||||
|
isTest: taskRun.isTest,
|
||||||
|
schedule: taskRun.schedule
|
||||||
|
? {
|
||||||
|
id: taskRun.schedule.friendlyId,
|
||||||
|
externalId: taskRun.schedule.externalId ?? undefined,
|
||||||
|
deduplicationKey: taskRun.schedule.userProvidedDeduplicationKey
|
||||||
|
? taskRun.schedule.deduplicationKey
|
||||||
|
: undefined,
|
||||||
|
generator: {
|
||||||
|
type: "CRON",
|
||||||
|
expression: taskRun.schedule.generatorExpression,
|
||||||
|
description: taskRun.schedule.generatorDescription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
...ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(apiStatus),
|
||||||
attempts: !showSecretDetails
|
attempts: !showSecretDetails
|
||||||
? []
|
? []
|
||||||
: taskRun.attempts.map((a) => ({
|
: taskRun.attempts.map((a) => ({
|
||||||
@@ -49,34 +114,68 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
updatedAt: a.updatedAt ?? undefined,
|
updatedAt: a.updatedAt ?? undefined,
|
||||||
startedAt: a.startedAt ?? undefined,
|
startedAt: a.startedAt ?? undefined,
|
||||||
completedAt: a.completedAt ?? undefined,
|
completedAt: a.completedAt ?? undefined,
|
||||||
|
error: ApiRetrieveRunPresenter.apiErrorFromError(a.error),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static apiErrorFromError(error: Prisma.JsonValue): SerializedError | undefined {
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorData = TaskRunError.safeParse(error);
|
||||||
|
|
||||||
|
if (errorData.success) {
|
||||||
|
return createJsonErrorObject(errorData.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static isStatusFinished(status: RunStatus) {
|
||||||
|
return (
|
||||||
|
status === "COMPLETED" ||
|
||||||
|
status === "FAILED" ||
|
||||||
|
status === "CANCELED" ||
|
||||||
|
status === "INTERRUPTED" ||
|
||||||
|
status === "CRASHED" ||
|
||||||
|
status === "SYSTEM_FAILURE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
|
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "WAITING_FOR_DEPLOY":
|
case "WAITING_FOR_DEPLOY": {
|
||||||
case "PENDING": {
|
return "WAITING_FOR_DEPLOY";
|
||||||
return "PENDING";
|
}
|
||||||
|
case "PENDING": {
|
||||||
|
return "QUEUED";
|
||||||
|
}
|
||||||
|
case "PAUSED":
|
||||||
|
case "WAITING_TO_RESUME": {
|
||||||
|
return "FROZEN";
|
||||||
|
}
|
||||||
|
case "RETRYING_AFTER_FAILURE": {
|
||||||
|
return "REATTEMPTING";
|
||||||
}
|
}
|
||||||
case "RETRYING_AFTER_FAILURE":
|
|
||||||
case "EXECUTING": {
|
case "EXECUTING": {
|
||||||
return "EXECUTING";
|
return "EXECUTING";
|
||||||
}
|
}
|
||||||
case "WAITING_TO_RESUME":
|
|
||||||
case "PAUSED": {
|
|
||||||
return "PAUSED";
|
|
||||||
}
|
|
||||||
case "CANCELED": {
|
case "CANCELED": {
|
||||||
return "CANCELED";
|
return "CANCELED";
|
||||||
}
|
}
|
||||||
case "COMPLETED_SUCCESSFULLY": {
|
case "COMPLETED_SUCCESSFULLY": {
|
||||||
return "COMPLETED";
|
return "COMPLETED";
|
||||||
}
|
}
|
||||||
case "SYSTEM_FAILURE":
|
case "SYSTEM_FAILURE": {
|
||||||
case "INTERRUPTED":
|
return "SYSTEM_FAILURE";
|
||||||
case "CRASHED":
|
}
|
||||||
|
case "INTERRUPTED": {
|
||||||
|
return "INTERRUPTED";
|
||||||
|
}
|
||||||
|
case "CRASHED": {
|
||||||
|
return "CRASHED";
|
||||||
|
}
|
||||||
case "COMPLETED_WITH_ERRORS": {
|
case "COMPLETED_WITH_ERRORS": {
|
||||||
return "FAILED";
|
return "FAILED";
|
||||||
}
|
}
|
||||||
@@ -86,6 +185,30 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static apiBooleanHelpersFromRunStatus(status: RunStatus) {
|
||||||
|
const isQueued = status === "QUEUED" || status === "WAITING_FOR_DEPLOY";
|
||||||
|
const isExecuting = status === "EXECUTING" || status === "REATTEMPTING" || status === "FROZEN";
|
||||||
|
const isCompleted =
|
||||||
|
status === "COMPLETED" ||
|
||||||
|
status === "CANCELED" ||
|
||||||
|
status === "FAILED" ||
|
||||||
|
status === "CRASHED" ||
|
||||||
|
status === "INTERRUPTED" ||
|
||||||
|
status === "SYSTEM_FAILURE";
|
||||||
|
const isFailed = isCompleted && status !== "COMPLETED";
|
||||||
|
const isSuccess = isCompleted && status === "COMPLETED";
|
||||||
|
const isCancelled = status === "CANCELED";
|
||||||
|
|
||||||
|
return {
|
||||||
|
isQueued,
|
||||||
|
isExecuting,
|
||||||
|
isCompleted,
|
||||||
|
isFailed,
|
||||||
|
isSuccess,
|
||||||
|
isCancelled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static apiStatusFromAttemptStatus(status: TaskRunAttemptStatus): AttemptStatus {
|
static apiStatusFromAttemptStatus(status: TaskRunAttemptStatus): AttemptStatus {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "PENDING": {
|
case "PENDING": {
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { ListRunResponse, ListRunResponseItem, RunStatus } from "@trigger.dev/core/v3";
|
||||||
|
import { Project, RuntimeEnvironment, TaskRunStatus } from "@trigger.dev/database";
|
||||||
|
import assertNever from "assert-never";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
import { logger } from "~/services/logger.server";
|
||||||
|
import { ApiRetrieveRunPresenter } from "./ApiRetrieveRunPresenter.server";
|
||||||
|
import { RunListOptions, RunListPresenter } from "./RunListPresenter.server";
|
||||||
|
import { BasePresenter } from "./basePresenter.server";
|
||||||
|
|
||||||
|
const SearchParamsSchema = z.object({
|
||||||
|
"page[size]": z.coerce.number().int().positive().min(1).max(100).optional(),
|
||||||
|
"page[after]": z.string().optional(),
|
||||||
|
"page[before]": z.string().optional(),
|
||||||
|
"filter[status]": z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value, ctx) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses = value.split(",");
|
||||||
|
const parsedStatuses = statuses.map((status) => RunStatus.safeParse(status));
|
||||||
|
|
||||||
|
if (parsedStatuses.some((result) => !result.success)) {
|
||||||
|
const invalidStatuses: string[] = [];
|
||||||
|
|
||||||
|
for (const [index, result] of parsedStatuses.entries()) {
|
||||||
|
if (!result.success) {
|
||||||
|
invalidStatuses.push(statuses[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid status values: ${invalidStatuses.join(", ")}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return z.NEVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $statuses = parsedStatuses
|
||||||
|
.map((result) => (result.success ? result.data : undefined))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return Array.from(new Set($statuses));
|
||||||
|
}),
|
||||||
|
"filter[env]": z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => {
|
||||||
|
return value ? value.split(",") : undefined;
|
||||||
|
}),
|
||||||
|
"filter[taskIdentifier]": z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => {
|
||||||
|
return value ? value.split(",") : undefined;
|
||||||
|
}),
|
||||||
|
"filter[version]": z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => {
|
||||||
|
return value ? value.split(",") : undefined;
|
||||||
|
}),
|
||||||
|
"filter[bulkAction]": z.string().optional(),
|
||||||
|
"filter[schedule]": z.string().optional(),
|
||||||
|
"filter[isTest]": z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value, ctx) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "true") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "false") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid value for isTest: ${value}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return z.NEVER;
|
||||||
|
}),
|
||||||
|
"filter[createdAt][from]": z.coerce.date().optional(),
|
||||||
|
"filter[createdAt][to]": z.coerce.date().optional(),
|
||||||
|
"filter[createdAt][period]": z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type SearchParamsSchema = z.infer<typeof SearchParamsSchema>;
|
||||||
|
|
||||||
|
export class ApiRunListPresenter extends BasePresenter {
|
||||||
|
public async call(
|
||||||
|
project: Project,
|
||||||
|
searchParams: URLSearchParams,
|
||||||
|
environment?: RuntimeEnvironment
|
||||||
|
): Promise<ListRunResponse> {
|
||||||
|
return this.trace("call", async (span) => {
|
||||||
|
const rawSearchParams = Object.fromEntries(searchParams.entries());
|
||||||
|
const $searchParams = SearchParamsSchema.safeParse(rawSearchParams);
|
||||||
|
|
||||||
|
if (!$searchParams.success) {
|
||||||
|
logger.error("Invalid search params", {
|
||||||
|
searchParams: rawSearchParams,
|
||||||
|
errors: $searchParams.error.errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw fromZodError($searchParams.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Valid search params", { searchParams: $searchParams.data });
|
||||||
|
|
||||||
|
const options: RunListOptions = {
|
||||||
|
projectId: project.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// pagination
|
||||||
|
if ($searchParams.data["page[size]"]) {
|
||||||
|
options.pageSize = $searchParams.data["page[size]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["page[after]"]) {
|
||||||
|
options.cursor = $searchParams.data["page[after]"];
|
||||||
|
options.direction = "forward";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["page[before]"]) {
|
||||||
|
options.cursor = $searchParams.data["page[before]"];
|
||||||
|
options.direction = "backward";
|
||||||
|
}
|
||||||
|
|
||||||
|
// filters
|
||||||
|
if (environment) {
|
||||||
|
options.environments = [environment.id];
|
||||||
|
} else {
|
||||||
|
if ($searchParams.data["filter[env]"]) {
|
||||||
|
const environments = await this._prisma.runtimeEnvironment.findMany({
|
||||||
|
where: {
|
||||||
|
projectId: project.id,
|
||||||
|
slug: {
|
||||||
|
in: $searchParams.data["filter[env]"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
options.environments = environments.map((env) => env.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[status]"]) {
|
||||||
|
options.statuses = $searchParams.data["filter[status]"].flatMap((status) =>
|
||||||
|
ApiRunListPresenter.apiStatusToRunStatuses(status)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[taskIdentifier]"]) {
|
||||||
|
options.tasks = $searchParams.data["filter[taskIdentifier]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[version]"]) {
|
||||||
|
options.versions = $searchParams.data["filter[version]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[bulkAction]"]) {
|
||||||
|
options.bulkId = $searchParams.data["filter[bulkAction]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[schedule]"]) {
|
||||||
|
options.scheduleId = $searchParams.data["filter[schedule]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[createdAt][from]"]) {
|
||||||
|
options.from = $searchParams.data["filter[createdAt][from]"].getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[createdAt][to]"]) {
|
||||||
|
options.to = $searchParams.data["filter[createdAt][to]"].getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchParams.data["filter[createdAt][period]"]) {
|
||||||
|
options.period = $searchParams.data["filter[createdAt][period]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof $searchParams.data["filter[isTest]"] === "boolean") {
|
||||||
|
options.isTest = $searchParams.data["filter[isTest]"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const presenter = new RunListPresenter();
|
||||||
|
|
||||||
|
logger.debug("Calling RunListPresenter", { options });
|
||||||
|
|
||||||
|
const results = await presenter.call(options);
|
||||||
|
|
||||||
|
const data: ListRunResponseItem[] = results.runs.map((run) => {
|
||||||
|
return {
|
||||||
|
id: run.friendlyId,
|
||||||
|
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(run.status),
|
||||||
|
taskIdentifier: run.taskIdentifier,
|
||||||
|
idempotencyKey: run.idempotencyKey,
|
||||||
|
version: run.version ?? undefined,
|
||||||
|
createdAt: new Date(run.createdAt),
|
||||||
|
updatedAt: new Date(run.updatedAt),
|
||||||
|
startedAt: run.startedAt ? new Date(run.startedAt) : undefined,
|
||||||
|
finishedAt: run.finishedAt ? new Date(run.finishedAt) : undefined,
|
||||||
|
isTest: run.isTest,
|
||||||
|
env: {
|
||||||
|
id: run.environment.id,
|
||||||
|
name: run.environment.slug,
|
||||||
|
user: run.environment.userName,
|
||||||
|
},
|
||||||
|
...ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(
|
||||||
|
ApiRetrieveRunPresenter.apiStatusFromRunStatus(run.status)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
pagination: {
|
||||||
|
next: results.pagination.next,
|
||||||
|
previous: results.pagination.previous,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static apiStatusToRunStatuses(status: RunStatus): TaskRunStatus[] | TaskRunStatus {
|
||||||
|
switch (status) {
|
||||||
|
case "WAITING_FOR_DEPLOY": {
|
||||||
|
return "WAITING_FOR_DEPLOY";
|
||||||
|
}
|
||||||
|
case "QUEUED": {
|
||||||
|
return "PENDING";
|
||||||
|
}
|
||||||
|
case "EXECUTING": {
|
||||||
|
return "EXECUTING";
|
||||||
|
}
|
||||||
|
case "REATTEMPTING": {
|
||||||
|
return "RETRYING_AFTER_FAILURE";
|
||||||
|
}
|
||||||
|
case "FROZEN": {
|
||||||
|
return ["PAUSED", "WAITING_TO_RESUME"];
|
||||||
|
}
|
||||||
|
case "CANCELED": {
|
||||||
|
return "CANCELED";
|
||||||
|
}
|
||||||
|
case "COMPLETED": {
|
||||||
|
return "COMPLETED_SUCCESSFULLY";
|
||||||
|
}
|
||||||
|
case "SYSTEM_FAILURE": {
|
||||||
|
return "SYSTEM_FAILURE";
|
||||||
|
}
|
||||||
|
case "INTERRUPTED": {
|
||||||
|
return "INTERRUPTED";
|
||||||
|
}
|
||||||
|
case "CRASHED": {
|
||||||
|
return "CRASHED";
|
||||||
|
}
|
||||||
|
case "FAILED": {
|
||||||
|
return "COMPLETED_WITH_ERRORS";
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
|||||||
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
|
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
|
||||||
import { BasePresenter } from "./basePresenter.server";
|
import { BasePresenter } from "./basePresenter.server";
|
||||||
|
|
||||||
type RunListOptions = {
|
export type RunListOptions = {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
//filters
|
//filters
|
||||||
tasks?: string[];
|
tasks?: string[];
|
||||||
versions?: string[];
|
versions?: string[];
|
||||||
@@ -20,6 +20,7 @@ type RunListOptions = {
|
|||||||
bulkId?: string;
|
bulkId?: string;
|
||||||
from?: number;
|
from?: number;
|
||||||
to?: number;
|
to?: number;
|
||||||
|
isTest?: boolean;
|
||||||
//pagination
|
//pagination
|
||||||
direction?: Direction;
|
direction?: Direction;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
@@ -35,7 +36,7 @@ export type RunListAppliedFilters = RunList["filters"];
|
|||||||
export class RunListPresenter extends BasePresenter {
|
export class RunListPresenter extends BasePresenter {
|
||||||
public async call({
|
public async call({
|
||||||
userId,
|
userId,
|
||||||
projectSlug,
|
projectId,
|
||||||
tasks,
|
tasks,
|
||||||
versions,
|
versions,
|
||||||
statuses,
|
statuses,
|
||||||
@@ -43,6 +44,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
scheduleId,
|
scheduleId,
|
||||||
period,
|
period,
|
||||||
bulkId,
|
bulkId,
|
||||||
|
isTest,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
direction = "forward",
|
direction = "forward",
|
||||||
@@ -59,7 +61,9 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
(period !== undefined && period !== "all") ||
|
(period !== undefined && period !== "all") ||
|
||||||
(bulkId !== undefined && bulkId !== "") ||
|
(bulkId !== undefined && bulkId !== "") ||
|
||||||
from !== undefined ||
|
from !== undefined ||
|
||||||
to !== undefined;
|
to !== undefined ||
|
||||||
|
(scheduleId !== undefined && scheduleId !== "") ||
|
||||||
|
typeof isTest === "boolean";
|
||||||
|
|
||||||
// Find the project scoped to the organization
|
// Find the project scoped to the organization
|
||||||
const project = await this._replica.project.findFirstOrThrow({
|
const project = await this._replica.project.findFirstOrThrow({
|
||||||
@@ -85,7 +89,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
slug: projectSlug,
|
id: projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -156,6 +160,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
isTest: boolean;
|
isTest: boolean;
|
||||||
spanId: string;
|
spanId: string;
|
||||||
|
idempotencyKey: string | null;
|
||||||
}[]
|
}[]
|
||||||
>`
|
>`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -170,7 +175,8 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
tr."lockedAt" AS "lockedAt",
|
tr."lockedAt" AS "lockedAt",
|
||||||
tr."updatedAt" AS "updatedAt",
|
tr."updatedAt" AS "updatedAt",
|
||||||
tr."isTest" AS "isTest",
|
tr."isTest" AS "isTest",
|
||||||
tr."spanId" AS "spanId"
|
tr."spanId" AS "spanId",
|
||||||
|
tr."idempotencyKey" AS "idempotencyKey"
|
||||||
FROM
|
FROM
|
||||||
${sqlDatabaseSchema}."TaskRun" tr
|
${sqlDatabaseSchema}."TaskRun" tr
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -210,6 +216,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
: Prisma.empty
|
: Prisma.empty
|
||||||
}
|
}
|
||||||
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
||||||
|
${typeof isTest === "boolean" ? Prisma.sql`AND tr."isTest" = ${isTest}` : Prisma.empty}
|
||||||
${
|
${
|
||||||
periodMs
|
periodMs
|
||||||
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
||||||
@@ -270,6 +277,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
friendlyId: run.runFriendlyId,
|
friendlyId: run.runFriendlyId,
|
||||||
number: Number(run.number),
|
number: Number(run.number),
|
||||||
createdAt: run.createdAt.toISOString(),
|
createdAt: run.createdAt.toISOString(),
|
||||||
|
updatedAt: run.updatedAt.toISOString(),
|
||||||
startedAt: run.lockedAt ? run.lockedAt.toISOString() : undefined,
|
startedAt: run.lockedAt ? run.lockedAt.toISOString() : undefined,
|
||||||
hasFinished,
|
hasFinished,
|
||||||
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
|
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
|
||||||
@@ -281,6 +289,7 @@ export class RunListPresenter extends BasePresenter {
|
|||||||
isReplayable: true,
|
isReplayable: true,
|
||||||
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
||||||
environment: displayableEnvironment(environment, userId),
|
environment: displayableEnvironment(environment, userId),
|
||||||
|
idempotencyKey: run.idempotencyKey ? run.idempotencyKey : undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class ViewSchedulePresenter {
|
|||||||
taskIdentifier: true,
|
taskIdentifier: true,
|
||||||
project: {
|
project: {
|
||||||
select: {
|
select: {
|
||||||
slug: true,
|
id: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instances: {
|
instances: {
|
||||||
@@ -39,6 +39,7 @@ export class ViewSchedulePresenter {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
type: true,
|
type: true,
|
||||||
|
slug: true,
|
||||||
orgMember: {
|
orgMember: {
|
||||||
select: {
|
select: {
|
||||||
user: {
|
user: {
|
||||||
@@ -71,8 +72,9 @@ export class ViewSchedulePresenter {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const runPresenter = new RunListPresenter(this.#prismaClient);
|
const runPresenter = new RunListPresenter(this.#prismaClient);
|
||||||
|
|
||||||
const { runs } = await runPresenter.call({
|
const { runs } = await runPresenter.call({
|
||||||
projectSlug: schedule.project.slug,
|
projectId: schedule.project.id,
|
||||||
scheduleId: schedule.id,
|
scheduleId: schedule.id,
|
||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,4 +34,26 @@ export abstract class BasePresenter {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async trace<T>(trace: string, fn: (span: Span) => Promise<T>): Promise<T> {
|
||||||
|
return tracer.startActiveSpan(
|
||||||
|
`${this.constructor.name}.${trace}`,
|
||||||
|
{ kind: SpanKind.SERVER },
|
||||||
|
async (span) => {
|
||||||
|
try {
|
||||||
|
return await fn(span);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
span.recordException(e);
|
||||||
|
} else {
|
||||||
|
span.recordException(new Error(String(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -39,6 +39,7 @@ import { cn } from "~/utils/cn";
|
|||||||
import { ProjectParamSchema, v3ProjectPath, v3RunsPath, v3TestPath } from "~/utils/pathBuilder";
|
import { ProjectParamSchema, v3ProjectPath, v3RunsPath, v3TestPath } from "~/utils/pathBuilder";
|
||||||
import { ListPagination } from "../../components/ListPagination";
|
import { ListPagination } from "../../components/ListPagination";
|
||||||
import { BULK_ACTION_RUN_LIMIT } from "~/consts";
|
import { BULK_ACTION_RUN_LIMIT } from "~/consts";
|
||||||
|
import { findProjectBySlug } from "~/models/project.server";
|
||||||
|
|
||||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||||
const userId = await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
@@ -57,10 +58,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||||||
const { tasks, versions, statuses, environments, period, bulkId, from, to, cursor, direction } =
|
const { tasks, versions, statuses, environments, period, bulkId, from, to, cursor, direction } =
|
||||||
TaskRunListSearchFilters.parse(s);
|
TaskRunListSearchFilters.parse(s);
|
||||||
|
|
||||||
|
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("Project not found");
|
||||||
|
}
|
||||||
|
|
||||||
const presenter = new RunListPresenter();
|
const presenter = new RunListPresenter();
|
||||||
const list = presenter.call({
|
const list = presenter.call({
|
||||||
userId,
|
userId,
|
||||||
projectSlug: projectParam,
|
projectId: project.id,
|
||||||
tasks,
|
tasks,
|
||||||
versions,
|
versions,
|
||||||
statuses,
|
statuses,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { json } from "@remix-run/server-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { ValidationError } from "zod-validation-error";
|
||||||
|
import { findProjectByRef } from "~/models/project.server";
|
||||||
|
import { ApiRunListPresenter } from "~/presenters/v3/ApiRunListPresenter.server";
|
||||||
|
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||||
|
import { apiCors } from "~/utils/apiCors";
|
||||||
|
|
||||||
|
const ParamsSchema = z.object({
|
||||||
|
projectRef: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
|
if (request.method.toUpperCase() === "OPTIONS") {
|
||||||
|
return apiCors(request, json({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
|
||||||
|
|
||||||
|
if (!authenticationResult) {
|
||||||
|
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const $params = ParamsSchema.safeParse(params);
|
||||||
|
|
||||||
|
if (!$params.success) {
|
||||||
|
return json({ error: "Invalid params" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await findProjectByRef($params.data.projectRef, authenticationResult.userId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return json({ error: "Project not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const presenter = new ApiRunListPresenter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await presenter.call(project, url.searchParams);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return apiCors(request, json({ data: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiCors(request, json(result));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return apiCors(
|
||||||
|
request,
|
||||||
|
json({ error: "Query Error", details: error.details }, { status: 400 })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return apiCors(
|
||||||
|
request,
|
||||||
|
json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { json } from "@remix-run/server-runtime";
|
||||||
|
import { ValidationError } from "zod-validation-error";
|
||||||
|
import { ApiRunListPresenter } from "~/presenters/v3/ApiRunListPresenter.server";
|
||||||
|
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||||
|
import { apiCors } from "~/utils/apiCors";
|
||||||
|
|
||||||
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
|
if (request.method.toUpperCase() === "OPTIONS") {
|
||||||
|
return apiCors(request, json({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticationResult = await authenticateApiRequest(request, {
|
||||||
|
allowPublicKey: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authenticationResult) {
|
||||||
|
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticatedEnv = authenticationResult.environment;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const presenter = new ApiRunListPresenter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await presenter.call(
|
||||||
|
authenticatedEnv.project,
|
||||||
|
url.searchParams,
|
||||||
|
authenticatedEnv
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return apiCors(request, json({ data: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiCors(request, json(result));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
return apiCors(
|
||||||
|
request,
|
||||||
|
json({ error: "Query Error", details: error.details }, { status: 400 })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return apiCors(
|
||||||
|
request,
|
||||||
|
json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,5 +48,5 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
|||||||
return json({ error: "Internal Server Error" }, { status: 500 });
|
return json({ error: "Internal Server Error" }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({ message: "Run cancelled" }, { status: 200 });
|
return json({ id: runParam }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type MarQSOptions = {
|
|||||||
keysProducer: MarQSKeyProducer;
|
keysProducer: MarQSKeyProducer;
|
||||||
queuePriorityStrategy: MarQSQueuePriorityStrategy;
|
queuePriorityStrategy: MarQSQueuePriorityStrategy;
|
||||||
envQueuePriorityStrategy: MarQSQueuePriorityStrategy;
|
envQueuePriorityStrategy: MarQSQueuePriorityStrategy;
|
||||||
|
enableRebalancing?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -707,6 +708,10 @@ export class MarQS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#startRebalanceWorkers() {
|
#startRebalanceWorkers() {
|
||||||
|
if (!this.options.enableRebalancing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Start a new worker to rebalance parent queues periodically
|
// Start a new worker to rebalance parent queues periodically
|
||||||
for (let i = 0; i < this.options.workers; i++) {
|
for (let i = 0; i < this.options.workers; i++) {
|
||||||
const worker = new AsyncWorker(this.#rebalanceParentQueues.bind(this), 60_000);
|
const worker = new AsyncWorker(this.#rebalanceParentQueues.bind(this), 60_000);
|
||||||
@@ -1597,6 +1602,7 @@ function getMarQSClient() {
|
|||||||
defaultEnvConcurrency: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT,
|
defaultEnvConcurrency: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT,
|
||||||
defaultOrgConcurrency: env.DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT,
|
defaultOrgConcurrency: env.DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT,
|
||||||
visibilityTimeoutInMs: 120 * 1000, // 2 minutes,
|
visibilityTimeoutInMs: 120 * 1000, // 2 minutes,
|
||||||
|
enableRebalancing: !env.MARQS_DISABLE_REBALANCING,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
+25
-17
@@ -5,7 +5,7 @@
|
|||||||
"versions": ["v3 (Developer Preview)", "v2"],
|
"versions": ["v3 (Developer Preview)", "v2"],
|
||||||
"api": {
|
"api": {
|
||||||
"playground": {
|
"playground": {
|
||||||
"mode": "hide"
|
"mode": "simple"
|
||||||
},
|
},
|
||||||
"maintainOrder": true
|
"maintainOrder": true
|
||||||
},
|
},
|
||||||
@@ -165,35 +165,43 @@
|
|||||||
"group": "API reference",
|
"group": "API reference",
|
||||||
"version": "v3 (Developer Preview)",
|
"version": "v3 (Developer Preview)",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
"v3/management/overview",
|
||||||
{
|
{
|
||||||
"group": "Runs API",
|
"group": "Runs API",
|
||||||
"pages": [
|
"pages": [
|
||||||
"v3/management-retrieve-run",
|
"v3/management/runs/list",
|
||||||
"v3/management-replay-run",
|
"v3/management/runs/retrieve",
|
||||||
"v3/management-cancel-run"
|
"v3/management/runs/replay",
|
||||||
|
"v3/management/runs/cancel"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Schedules API",
|
"group": "Schedules API",
|
||||||
"pages": [
|
"pages": [
|
||||||
"v3/management-list-schedules",
|
"v3/management/schedules/list",
|
||||||
"v3/management-create-schedule",
|
"v3/management/schedules/create",
|
||||||
"v3/management-retrieve-schedule",
|
"v3/management/schedules/retrieve",
|
||||||
"v3/management-update-schedule",
|
"v3/management/schedules/update",
|
||||||
"v3/management-delete-schedule",
|
"v3/management/schedules/delete",
|
||||||
"v3/management-deactivate-schedule",
|
"v3/management/schedules/deactivate",
|
||||||
"v3/management-activate-schedule"
|
"v3/management/schedules/activate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Env Vars API",
|
"group": "Env Vars API",
|
||||||
"pages": [
|
"pages": [
|
||||||
"v3/management-envvars-list",
|
"v3/management/envvars/list",
|
||||||
"v3/management-envvars-import",
|
"v3/management/envvars/import",
|
||||||
"v3/management-envvars-create",
|
"v3/management/envvars/create",
|
||||||
"v3/management-envvars-retrieve",
|
"v3/management/envvars/retrieve",
|
||||||
"v3/management-envvars-update",
|
"v3/management/envvars/update",
|
||||||
"v3/management-envvars-delete"
|
"v3/management/envvars/delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Projects API",
|
||||||
|
"pages": [
|
||||||
|
"v3/management/projects/runs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+553
-56
@@ -39,7 +39,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -79,7 +79,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -114,7 +114,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -157,7 +157,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -189,7 +189,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -223,7 +223,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -257,7 +257,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- schedules
|
- schedules
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -265,20 +265,14 @@ paths:
|
|||||||
|
|
||||||
const schedule = await schedules.activate(scheduleId);
|
const schedule = await schedules.activate(scheduleId);
|
||||||
|
|
||||||
"/api/v1/runs/{run_id}/replay":
|
"/api/v1/runs/{runId}/replay":
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/runId"
|
||||||
post:
|
post:
|
||||||
operationId: replay_run_v1
|
operationId: replay_run_v1
|
||||||
summary: Replay a run
|
summary: Replay a run
|
||||||
description: Creates a new run with the same payload and options as the original
|
description: Creates a new run with the same payload and options as the original
|
||||||
run.
|
run.
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: run_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The ID of an existing run. When you trigger a run you will get
|
|
||||||
an id in the response.
|
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful request
|
description: Successful request
|
||||||
@@ -325,9 +319,9 @@ paths:
|
|||||||
enum:
|
enum:
|
||||||
- Run not found
|
- Run not found
|
||||||
tags:
|
tags:
|
||||||
- run
|
- runs
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -335,19 +329,14 @@ paths:
|
|||||||
|
|
||||||
const handle = await runs.replay("run_1234");
|
const handle = await runs.replay("run_1234");
|
||||||
|
|
||||||
"/api/v1/runs/{run_id}/cancel":
|
"/api/v2/runs/{runId}/cancel":
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/runId"
|
||||||
post:
|
post:
|
||||||
operationId: cancel_run_v1
|
operationId: cancel_run_v1
|
||||||
description: Cancels a run.
|
|
||||||
summary: Cancel a run
|
summary: Cancel a run
|
||||||
parameters:
|
description: Cancels an in-progress run. If the run is already completed, this
|
||||||
- in: path
|
will have no effect.
|
||||||
name: run_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The ID of an existing run. When you trigger a run you will get
|
|
||||||
an id in the response.
|
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful request
|
description: Successful request
|
||||||
@@ -356,9 +345,10 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
message:
|
id:
|
||||||
type: string
|
type: string
|
||||||
description: Confirmation message that the run was canceled.
|
description: The ID of the run that was canceled.
|
||||||
|
example: run_1234
|
||||||
"400":
|
"400":
|
||||||
description: Invalid request
|
description: Invalid request
|
||||||
content:
|
content:
|
||||||
@@ -394,9 +384,9 @@ paths:
|
|||||||
enum:
|
enum:
|
||||||
- Run not found
|
- Run not found
|
||||||
tags:
|
tags:
|
||||||
- run
|
- runs
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
@@ -404,19 +394,14 @@ paths:
|
|||||||
|
|
||||||
await runs.cancel("run_1234");
|
await runs.cancel("run_1234");
|
||||||
|
|
||||||
"/api/v3/runs/{run_id}":
|
"/api/v3/runs/{runId}":
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/runId"
|
||||||
get:
|
get:
|
||||||
operationId: retrieve_run_v1
|
operationId: retrieve_run_v1
|
||||||
description: Retrieve a run
|
|
||||||
summary: Retrieve a run
|
summary: Retrieve a run
|
||||||
parameters:
|
description: |
|
||||||
- in: path
|
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
|
||||||
name: run_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The ID of an existing run. When you trigger a run you will get
|
|
||||||
an id in the response.
|
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful request
|
description: Successful request
|
||||||
@@ -460,13 +445,182 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- run
|
- run
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
source: |-
|
source: |-
|
||||||
import { runs } from "@trigger.dev/sdk/v3";
|
import { runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
await runs.retrieve("run_1234");
|
const result = await runs.retrieve("run_1234");
|
||||||
|
|
||||||
|
// We include boolean helpers to check the status of the run
|
||||||
|
// (isSuccess, isFailed, isCompleted, etc.)
|
||||||
|
if (result.isSuccess) {
|
||||||
|
console.log("Run was successful with output", result.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// You also have access to the run status that includes more granular information
|
||||||
|
console.log("Run status:", result.status);
|
||||||
|
|
||||||
|
// You can access the payload and output
|
||||||
|
console.log("Payload:", result.payload);
|
||||||
|
console.log("Output:", result.output);
|
||||||
|
|
||||||
|
// You can also access the attempts, which will give you information about errors (if they exist)
|
||||||
|
for (const attempt of result.attempts) {
|
||||||
|
if (attempt.status === "FAILED") {
|
||||||
|
console.log("Attempt failed with error:", attempt.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"/api/v1/runs":
|
||||||
|
get:
|
||||||
|
operationId: list_runs_v1
|
||||||
|
summary: List runs
|
||||||
|
description: List runs in a specific environment. You can filter the runs by status, created at, task identifier, version, and more.
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/cursorPagination"
|
||||||
|
- $ref: "#/components/parameters/runsFilter"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ListRunsResult"
|
||||||
|
"400":
|
||||||
|
description: Invalid query parameters
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorWithDetailsResponse"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized request
|
||||||
|
tags:
|
||||||
|
- runs
|
||||||
|
security:
|
||||||
|
- secretKey: []
|
||||||
|
x-codeSamples:
|
||||||
|
- lang: typescript
|
||||||
|
label: List runs
|
||||||
|
source: |-
|
||||||
|
import { runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
// Get the first page of runs
|
||||||
|
let page = await runs.list({ limit: 20 });
|
||||||
|
|
||||||
|
for (const run of page.data) {
|
||||||
|
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods are provided for manually paginating:
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
// Do something with the next page of runs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-paginate through all runs
|
||||||
|
const allRuns = [];
|
||||||
|
|
||||||
|
for await (const run of runs.list({ limit: 20 })) {
|
||||||
|
allRuns.push(run);
|
||||||
|
}
|
||||||
|
- lang: typescript
|
||||||
|
label: Filter runs
|
||||||
|
source: |-
|
||||||
|
import { runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
const response = await runs.list({
|
||||||
|
status: ["QUEUED", "EXECUTING"],
|
||||||
|
taskIdentifier: ["my-task", "my-other-task"],
|
||||||
|
from: new Date("2024-04-01T00:00:00Z"),
|
||||||
|
to: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const run of response.data) {
|
||||||
|
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
"/api/v1/projects/{projectRef}/runs":
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/projectRef"
|
||||||
|
get:
|
||||||
|
operationId: list_project_runs_v1
|
||||||
|
summary: List project runs
|
||||||
|
description: List runs in a project, across multiple environments, using Personal Access Token auth. You can filter the runs by status, created at, task identifier, version, and more.
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/cursorPagination"
|
||||||
|
- $ref: "#/components/parameters/runsFilterWithEnv"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ListRunsResult"
|
||||||
|
"400":
|
||||||
|
description: Invalid request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorWithDetailsResponse"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized request
|
||||||
|
tags:
|
||||||
|
- runs
|
||||||
|
security:
|
||||||
|
- personalAccessToken: []
|
||||||
|
x-codeSamples:
|
||||||
|
- lang: typescript
|
||||||
|
label: List runs
|
||||||
|
source: |-
|
||||||
|
import { runs, configure } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
configure({
|
||||||
|
secretKey: "tr_pat_1234" // always use an environment variable for this
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the first page of runs
|
||||||
|
let page = await runs.list("proj_1234", { limit: 20 });
|
||||||
|
|
||||||
|
for (const run of page.data) {
|
||||||
|
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods are provided for manually paginating:
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
// Do something with the next page of runs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-paginate through all runs
|
||||||
|
const allRuns = [];
|
||||||
|
|
||||||
|
for await (const run of runs.list("proj_1234", { limit: 20 })) {
|
||||||
|
allRuns.push(run);
|
||||||
|
}
|
||||||
|
- lang: typescript
|
||||||
|
label: Filter runs
|
||||||
|
source: |-
|
||||||
|
import { runs, configure } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
configure({
|
||||||
|
secretKey: "tr_pat_1234" // always use an environment variable for this
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await runs.list("proj_1234", {
|
||||||
|
env: ["prod", "staging"],
|
||||||
|
status: ["QUEUED", "EXECUTING"],
|
||||||
|
taskIdentifier: ["my-task", "my-other-task"],
|
||||||
|
from: new Date("2024-04-01T00:00:00Z"),
|
||||||
|
to: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const run of response.data) {
|
||||||
|
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"/api/v1/projects/{projectRef}/envvars/{env}":
|
"/api/v1/projects/{projectRef}/envvars/{env}":
|
||||||
parameters:
|
parameters:
|
||||||
@@ -504,7 +658,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- envvars
|
- envvars
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
- personalAccessToken: []
|
- personalAccessToken: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
@@ -577,7 +731,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- envvars
|
- envvars
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
- personalAccessToken: []
|
- personalAccessToken: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
@@ -674,7 +828,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- envvars
|
- envvars
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
- personalAccessToken: []
|
- personalAccessToken: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
@@ -772,7 +926,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- envvars
|
- envvars
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
- personalAccessToken: []
|
- personalAccessToken: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
@@ -830,7 +984,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- envvars
|
- envvars
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
- personalAccessToken: []
|
- personalAccessToken: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
@@ -889,7 +1043,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- envvars
|
- envvars
|
||||||
security:
|
security:
|
||||||
- apiKey: []
|
- secretKey: []
|
||||||
- personalAccessToken: []
|
- personalAccessToken: []
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: typescript
|
- lang: typescript
|
||||||
@@ -918,6 +1072,67 @@ paths:
|
|||||||
|
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
|
runsFilterWithEnv:
|
||||||
|
in: query
|
||||||
|
name: filter
|
||||||
|
style: deepObject
|
||||||
|
explode: true
|
||||||
|
description: |
|
||||||
|
Use this parameter to filter the runs. You can filter by created at, environment, status, task identifier, and version.
|
||||||
|
|
||||||
|
For array fields, you can provide multiple values to filter by using a comma-separated list. For example, to get QUEUED and EXECUTING runs, you can use `filter[status]=QUEUED,EXECUTING`.
|
||||||
|
|
||||||
|
For object fields, you should use the "form" encoding style. For example, to filter by the period, you can use `filter[createdAt][period]=1d`.
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/CommonRunsFilter"
|
||||||
|
- $ref: "#/components/schemas/EnvFilter"
|
||||||
|
runsFilter:
|
||||||
|
in: query
|
||||||
|
name: filter
|
||||||
|
style: deepObject
|
||||||
|
explode: true
|
||||||
|
description: |
|
||||||
|
Use this parameter to filter the runs. You can filter by created at, status, task identifier, and version.
|
||||||
|
|
||||||
|
For array fields, you can provide multiple values to filter by using a comma-separated list. For example, to get QUEUED and EXECUTING runs, you can use `filter[status]=QUEUED,EXECUTING`.
|
||||||
|
|
||||||
|
For object fields, you should use the "form" encoding style. For example, to filter by the period, you can use `filter[createdAt][period]=1d`.
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CommonRunsFilter"
|
||||||
|
cursorPagination:
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
style: deepObject
|
||||||
|
explode: true
|
||||||
|
description: |
|
||||||
|
Use this parameter to paginate the results. You can specify the number of runs per page, and the ID of the run to start the page after or before.
|
||||||
|
|
||||||
|
For object fields like `page`, you should use the "form" encoding style. For example, to get the next page of runs, you can use `page[after]=run_1234`.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
maximum: 100
|
||||||
|
minimum: 10
|
||||||
|
default: 25
|
||||||
|
description: Number of runs per page. Maximum is 100.
|
||||||
|
after:
|
||||||
|
type: string
|
||||||
|
description: The ID of the run to start the page after. This will set the direction of the pagination to forward.
|
||||||
|
before:
|
||||||
|
type: string
|
||||||
|
description: The ID of the run to start the page before. This will set the direction of the pagination to backward.
|
||||||
|
runId:
|
||||||
|
in: path
|
||||||
|
name: runId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The ID of an run, starts with `run_`. The run ID will be returned when you trigger a run on a task.
|
||||||
|
example: run_1234
|
||||||
projectRef:
|
projectRef:
|
||||||
in: path
|
in: path
|
||||||
name: projectRef
|
name: projectRef
|
||||||
@@ -944,7 +1159,7 @@ components:
|
|||||||
description: The name of the environment variable.
|
description: The name of the environment variable.
|
||||||
example: SLACK_API_KEY
|
example: SLACK_API_KEY
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
apiKey:
|
secretKey:
|
||||||
type: http
|
type: http
|
||||||
scheme: bearer
|
scheme: bearer
|
||||||
description: |
|
description: |
|
||||||
@@ -974,6 +1189,173 @@ components:
|
|||||||
configure({ secretKey: "tr_pat_1234" });
|
configure({ secretKey: "tr_pat_1234" });
|
||||||
```
|
```
|
||||||
schemas:
|
schemas:
|
||||||
|
EnvFilter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
env:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: The environment of the project
|
||||||
|
enum:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- prod
|
||||||
|
CommonRunsFilter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
createdAt:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
from:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The start date to filter the runs by
|
||||||
|
to:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The end date to filter the runs by
|
||||||
|
period:
|
||||||
|
type: string
|
||||||
|
description: The period to filter the runs by
|
||||||
|
example: 1d
|
||||||
|
status:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: The status of the run
|
||||||
|
enum:
|
||||||
|
- WAITING_FOR_DEPLOY
|
||||||
|
- QUEUED
|
||||||
|
- EXECUTING
|
||||||
|
- REATTEMPTING
|
||||||
|
- FROZEN
|
||||||
|
- COMPLETED
|
||||||
|
- CANCELED
|
||||||
|
- FAILED
|
||||||
|
- CRASHED
|
||||||
|
- INTERRUPTED
|
||||||
|
- SYSTEM_FAILURE
|
||||||
|
taskIdentifier:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: The identifier of the task that was run
|
||||||
|
version:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: The version of the worker that executed the run
|
||||||
|
|
||||||
|
bulkAction:
|
||||||
|
type: string
|
||||||
|
description: The bulk action ID to filter the runs by
|
||||||
|
example: bulk_1234
|
||||||
|
schedule:
|
||||||
|
type: string
|
||||||
|
description: The schedule ID to filter the runs by
|
||||||
|
example: schedule_1234
|
||||||
|
isTest:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the run is a test run or not
|
||||||
|
example: false
|
||||||
|
ListRunsResult:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
"$ref": "#/components/schemas/ListRunItem"
|
||||||
|
pagination:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
next:
|
||||||
|
type: string
|
||||||
|
description: The run ID to start the next page after. This should be used as the `page[after]` parameter in the next request.
|
||||||
|
example: run_1234
|
||||||
|
previous:
|
||||||
|
type: string
|
||||||
|
description: The run ID to start the previous page before. This should be used as the `page[before]` parameter in the next request.
|
||||||
|
example: run_5678
|
||||||
|
ListRunItem:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- status
|
||||||
|
- taskIdentifier
|
||||||
|
- createdAt
|
||||||
|
- updatedAt
|
||||||
|
- isTest
|
||||||
|
- env
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: The unique ID of the run, prefixed with `run_`
|
||||||
|
example: run_1234
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: The status of the run
|
||||||
|
enum:
|
||||||
|
- WAITING_FOR_DEPLOY
|
||||||
|
- QUEUED
|
||||||
|
- EXECUTING
|
||||||
|
- REATTEMPTING
|
||||||
|
- FROZEN
|
||||||
|
- COMPLETED
|
||||||
|
- CANCELED
|
||||||
|
- FAILED
|
||||||
|
- CRASHED
|
||||||
|
- INTERRUPTED
|
||||||
|
- SYSTEM_FAILURE
|
||||||
|
taskIdentifier:
|
||||||
|
type: string
|
||||||
|
description: The identifier of the task that was run
|
||||||
|
example: my-task
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
example: 20240523.1
|
||||||
|
description: The version of the worker that executed the run
|
||||||
|
env:
|
||||||
|
type: object
|
||||||
|
description: The environment of the run
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: The unique ID of the environment
|
||||||
|
example: cl1234
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the environment
|
||||||
|
example: dev
|
||||||
|
user:
|
||||||
|
type: string
|
||||||
|
description: If this is a dev environment, the username of the user represented by this environment
|
||||||
|
example: Anna
|
||||||
|
idempotencyKey:
|
||||||
|
type: string
|
||||||
|
description: The idempotency key used to prevent creating duplicate runs, if provided
|
||||||
|
example: idempotency_key_1234
|
||||||
|
isTest:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the run is a test run or not
|
||||||
|
example: false
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
updatedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
startedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The time the run started
|
||||||
|
finishedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The time the run finished
|
||||||
InvalidEnvVarsRequestResponse:
|
InvalidEnvVarsRequestResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -999,6 +1381,35 @@ components:
|
|||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
required: ["error"]
|
required: ["error"]
|
||||||
|
ErrorWithDetailsResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
example: Query Error
|
||||||
|
details:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: The error code
|
||||||
|
example: custom
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: The error message
|
||||||
|
example: "Invalid status values: FOOBAR"
|
||||||
|
path:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: The relevant path in the request
|
||||||
|
example: ["filter[status]"]
|
||||||
|
required: ["error"]
|
||||||
ListEnvironmentVariablesResponse:
|
ListEnvironmentVariablesResponse:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -1032,27 +1443,95 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
|
description: The unique ID of the run, prefixed with `run_`
|
||||||
|
example: run_1234
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
description: The status of the run
|
||||||
enum:
|
enum:
|
||||||
- PENDING
|
- WAITING_FOR_DEPLOY
|
||||||
|
- QUEUED
|
||||||
- EXECUTING
|
- EXECUTING
|
||||||
- PAUSED
|
- REATTEMPTING
|
||||||
|
- FROZEN
|
||||||
- COMPLETED
|
- COMPLETED
|
||||||
- FAILED
|
|
||||||
- CANCELED
|
- CANCELED
|
||||||
|
- FAILED
|
||||||
|
- CRASHED
|
||||||
|
- INTERRUPTED
|
||||||
|
- SYSTEM_FAILURE
|
||||||
taskIdentifier:
|
taskIdentifier:
|
||||||
type: string
|
type: string
|
||||||
idempotencyKey:
|
description: The identifier of the task that was run
|
||||||
type: string
|
example: my-task
|
||||||
version:
|
version:
|
||||||
type: string
|
type: string
|
||||||
|
example: 20240523.1
|
||||||
|
description: The version of the worker that executed the run
|
||||||
|
payload:
|
||||||
|
type: object
|
||||||
|
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
|
||||||
|
example: {"foo": "bar"}
|
||||||
|
output:
|
||||||
|
type: object
|
||||||
|
description: The output of the run. Will be omitted if the request was made with a Public API key
|
||||||
|
example: {"foo": "bar"}
|
||||||
|
idempotencyKey:
|
||||||
|
type: string
|
||||||
|
description: The idempotency key used to prevent creating duplicate runs, if provided
|
||||||
|
example: idempotency_key_1234
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
updatedAt:
|
updatedAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
isTest:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the run is a test run or not
|
||||||
|
example: false
|
||||||
|
startedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The time the run started
|
||||||
|
finishedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The time the run finished
|
||||||
|
schedule:
|
||||||
|
type: object
|
||||||
|
description: The schedule that triggered the run. Will be omitted if the run was not triggered by a schedule
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- generator
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: The unique ID of the schedule, prefixed with `sched_`
|
||||||
|
example: sched_1234
|
||||||
|
externalId:
|
||||||
|
type: string
|
||||||
|
description: The external ID of the schedule. Can be anything that is useful to you (e.g., user ID, org ID, etc.)
|
||||||
|
example: user_1234
|
||||||
|
deduplicationKey:
|
||||||
|
type: string
|
||||||
|
description: The deduplication key used to prevent creating duplicate schedules
|
||||||
|
example: dedup_key_1234
|
||||||
|
generator:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- CRON
|
||||||
|
expression:
|
||||||
|
type: string
|
||||||
|
description: The cron expression used to generate the schedule
|
||||||
|
example: 0 0 * * *
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: The description of the generator in plain english
|
||||||
|
example: Every day at midnight
|
||||||
attempts:
|
attempts:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -1065,6 +1544,8 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
|
description: The unique ID of the attempt, prefixed with `attempt_`
|
||||||
|
example: attempt_1234
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -1074,6 +1555,8 @@ components:
|
|||||||
- COMPLETED
|
- COMPLETED
|
||||||
- FAILED
|
- FAILED
|
||||||
- CANCELED
|
- CANCELED
|
||||||
|
error:
|
||||||
|
$ref: "#/components/schemas/SerializedError"
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -1174,3 +1657,17 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
userName:
|
userName:
|
||||||
type: string
|
type: string
|
||||||
|
SerializedError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Something went wrong
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: Error
|
||||||
|
stackTrace:
|
||||||
|
type: string
|
||||||
|
example: "Error: Something went wrong"
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Cancel run"
|
|
||||||
openapi: "v3-openapi POST /api/v1/runs/{run_id}/cancel"
|
|
||||||
---
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Get run"
|
|
||||||
description: "Get a run using the Task id."
|
|
||||||
---
|
|
||||||
|
|
||||||
<Snippet file="incomplete-docs.mdx" />
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Get runs"
|
|
||||||
description: "Get runs using a Task id."
|
|
||||||
---
|
|
||||||
|
|
||||||
<Snippet file="incomplete-docs.mdx" />
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Replay run"
|
|
||||||
openapi: "v3-openapi POST /api/v1/runs/{run_id}/replay"
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Retrieve run"
|
|
||||||
openapi: "v3-openapi GET /api/v3/runs/{run_id}"
|
|
||||||
---
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Start run"
|
|
||||||
description: "Start a run using the Task id, payload and options."
|
|
||||||
---
|
|
||||||
|
|
||||||
<Snippet file="incomplete-docs.mdx" />
|
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
---
|
||||||
|
title: Overview & Authentication
|
||||||
|
sidebarTitle: Overview & Authentication
|
||||||
|
description: Using the Trigger.dev v3 management API
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The management API is available through the same `@trigger.dev/sdk` package used in defining and triggering tasks. If you have already installed the package in your project, you can skip this step.
|
||||||
|
|
||||||
|
<Note>Make sure you use the `beta` tag when installing, as v3 is still in Developer Preview.</Note>
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
|
||||||
|
```bash npm
|
||||||
|
npm i @trigger.dev/sdk@beta
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash pnpm
|
||||||
|
pnpm add @trigger.dev/sdk@beta
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash yarn
|
||||||
|
yarn add @trigger.dev/sdk@beta
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
All `v3` functionality is provided through the `@trigger.dev/sdk/v3` module. You can import the entire module or individual resources as needed.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { configure, runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
configure({
|
||||||
|
// this is the default and if the `TRIGGER_SECRET_KEY` environment variable is set, can omit calling configure
|
||||||
|
secretKey: process.env["TRIGGER_SECRET_KEY"],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const runs = await runs.list({
|
||||||
|
limit: 10,
|
||||||
|
status: ["COMPLETED"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
There are two methods of authenticating with the management API: using a secret key associated with a specific environment in a project (`secretKey`), or using a personal access token (`personalAccessToken`). Both methods should only be used in a backend server, as they provide full access to the project.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Support for client-side authentication is coming soon to v3 but is not available at the time of
|
||||||
|
writing.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Certain API functions work with both authentication methods, but require different arguments depending on the method used. For example, the `runs.list` function can be called using either a `secretKey` or a `personalAccessToken`, but the `projectRef` argument is required when using a `personalAccessToken`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { configure, runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
// Using secretKey authentication
|
||||||
|
configure({
|
||||||
|
secretKey: process.env["TRIGGER_SECRET_KEY"], // starts with tr_dev_ or tr_prod_
|
||||||
|
});
|
||||||
|
|
||||||
|
function secretKeyExample() {
|
||||||
|
return runs.list({
|
||||||
|
limit: 10,
|
||||||
|
status: ["COMPLETED"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using personalAccessToken authentication
|
||||||
|
configure({
|
||||||
|
secretKey: process.env["TRIGGER_ACCESS_TOKEN"], // starts with tr_pat_
|
||||||
|
});
|
||||||
|
|
||||||
|
function personalAccessTokenExample() {
|
||||||
|
// Notice the projectRef argument is required when using a personalAccessToken
|
||||||
|
return runs.list("prof_1234", {
|
||||||
|
limit: 10,
|
||||||
|
status: ["COMPLETED"],
|
||||||
|
projectRef: "tr_proj_1234567890",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Accordion title="View endpoint support">
|
||||||
|
Consult the following table to see which endpoints support each authentication method.
|
||||||
|
|
||||||
|
| Endpoint | Secret key | Personal Access Token |
|
||||||
|
| ---------------------- | ---------- | --------------------- |
|
||||||
|
| `runs.list` | ✅ | ✅ |
|
||||||
|
| `runs.retrieve` | ✅ | |
|
||||||
|
| `runs.cancel` | ✅ | |
|
||||||
|
| `runs.replay` | ✅ | |
|
||||||
|
| `envvars.list` | ✅ | ✅ |
|
||||||
|
| `envvars.retrieve` | ✅ | ✅ |
|
||||||
|
| `envvars.upload` | ✅ | ✅ |
|
||||||
|
| `envvars.create` | ✅ | ✅ |
|
||||||
|
| `envvars.update` | ✅ | ✅ |
|
||||||
|
| `envvars.del` | ✅ | ✅ |
|
||||||
|
| `schedules.list` | ✅ | |
|
||||||
|
| `schedules.create` | ✅ | |
|
||||||
|
| `schedules.retrieve` | ✅ | |
|
||||||
|
| `schedules.update` | ✅ | |
|
||||||
|
| `schedules.activate` | ✅ | |
|
||||||
|
| `schedules.deactivate` | ✅ | |
|
||||||
|
| `schedules.del` | ✅ | |
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
### Secret key
|
||||||
|
|
||||||
|
Secret key authentication scopes the API access to a specific environment in a project, and works with certain endpoints. You can read our [API Keys guide](/v3/apikeys) for more information.
|
||||||
|
|
||||||
|
### Personal Access Token (PAT)
|
||||||
|
|
||||||
|
A PAT is a token associated with a specific user, and gives access to all the orgs, projects, and environments that the user has access to. You can identify a PAT by the `tr_pat_` prefix. Because a PAT does not scope access to a specific environment, you must provide the `projectRef` argument when using a PAT (and sometimes the environment as well).
|
||||||
|
|
||||||
|
For example, when uploading environment variables using a PAT, you must provide the `projectRef` and `environment` arguments:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { configure, envvars } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
configure({
|
||||||
|
secretKey: process.env["TRIGGER_ACCESS_TOKEN"], // starts with tr_pat_
|
||||||
|
});
|
||||||
|
|
||||||
|
await envvars.upload("proj_1234", "dev", {
|
||||||
|
variables: {
|
||||||
|
MY_ENV_VAR: "MY_ENV_VAR_VALUE",
|
||||||
|
},
|
||||||
|
override: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling errors
|
||||||
|
|
||||||
|
When the SDK method is unable to connect to the API server, or the API server returns a non-successful response, the SDK will throw an `ApiError` that you can catch and handle:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { runs, APIError } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const run = await runs.retrieve("run_1234");
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
console.error(`API error: ${error.status}, ${error.headers}, ${error.body}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-pagination
|
||||||
|
|
||||||
|
All list endpoints in the management API support auto-pagination.
|
||||||
|
You can use `for await … of` syntax to iterate through items across all pages:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
async function fetchAllRuns() {
|
||||||
|
const runs = [];
|
||||||
|
|
||||||
|
for await (const run of runs.list({ limit: 10 })) {
|
||||||
|
runs.push(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
return runs;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use helpers on the return value from any `list` method to get the next/previous page of results:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let page = await runs.list({ limit: 10 });
|
||||||
|
|
||||||
|
for (const run of page.data) {
|
||||||
|
console.log(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
// ... do something with the next page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced usage
|
||||||
|
|
||||||
|
### Accessing raw HTTP responses
|
||||||
|
|
||||||
|
All API methods return a `Promise` subclass `ApiPromise` that includes helpers for accessing the underlying HTTP response:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { runs } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { data: run, response: raw } = await runs.retrieve("run_1234").withResponse();
|
||||||
|
|
||||||
|
console.log(raw.status);
|
||||||
|
console.log(raw.headers);
|
||||||
|
|
||||||
|
const response = await runs.retrieve("run_1234").asResponse(); // Returns a Response object
|
||||||
|
|
||||||
|
console.log(response.status);
|
||||||
|
console.log(response.headers);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List runs"
|
||||||
|
openapi: "v3-openapi GET /api/v1/projects/{projectRef}/runs"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Cancel run"
|
||||||
|
openapi: "v3-openapi POST /api/v2/runs/{runId}/cancel"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List runs"
|
||||||
|
openapi: "v3-openapi GET /api/v1/runs"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Replay run"
|
||||||
|
openapi: "v3-openapi POST /api/v1/runs/{runId}/replay"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve run"
|
||||||
|
openapi: "v3-openapi GET /api/v3/runs/{runId}"
|
||||||
|
---
|
||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
ImportEnvironmentVariablesRequestBody,
|
ImportEnvironmentVariablesRequestBody,
|
||||||
EnvironmentVariableResponseBody,
|
EnvironmentVariableResponseBody,
|
||||||
TaskRunExecution,
|
TaskRunExecution,
|
||||||
APIError,
|
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import { zodfetch } from "@trigger.dev/core/v3/zodfetch";
|
import { zodfetch, ApiError } from "@trigger.dev/core/v3/zodfetch";
|
||||||
|
|
||||||
export class CliApiClient {
|
export class CliApiClient {
|
||||||
private readonly apiURL: string;
|
private readonly apiURL: string;
|
||||||
@@ -265,7 +264,7 @@ async function wrapZodFetch<T extends z.ZodTypeAny>(
|
|||||||
data: response,
|
data: response,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof APIError) {
|
if (error instanceof ApiError) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
|
|||||||
@@ -0,0 +1,687 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
import { ApiConnectionError, ApiError } from "./errors";
|
||||||
|
import { RetryOptions } from "../schemas";
|
||||||
|
import { calculateNextRetryDelay } from "../utils/retries";
|
||||||
|
import { FormDataEncoder } from "form-data-encoder";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
import {
|
||||||
|
CursorPage,
|
||||||
|
CursorPageParams,
|
||||||
|
CursorPageResponse,
|
||||||
|
OffsetLimitPage,
|
||||||
|
OffsetLimitPageParams,
|
||||||
|
OffsetLimitPageResponse,
|
||||||
|
} from "./pagination";
|
||||||
|
|
||||||
|
export const defaultRetryOptions = {
|
||||||
|
maxAttempts: 3,
|
||||||
|
factor: 2,
|
||||||
|
minTimeoutInMs: 1000,
|
||||||
|
maxTimeoutInMs: 60000,
|
||||||
|
randomize: false,
|
||||||
|
} satisfies RetryOptions;
|
||||||
|
|
||||||
|
export type ZodFetchOptions = {
|
||||||
|
retry?: RetryOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FetchCursorPageParams extends CursorPageParams {
|
||||||
|
query?: URLSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchOffsetLimitPageParams extends OffsetLimitPageParams {
|
||||||
|
query?: URLSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zodfetch<TResponseBodySchema extends z.ZodTypeAny>(
|
||||||
|
schema: TResponseBodySchema,
|
||||||
|
url: string,
|
||||||
|
requestInit?: RequestInit,
|
||||||
|
options?: ZodFetchOptions
|
||||||
|
): ApiPromise<z.output<TResponseBodySchema>> {
|
||||||
|
return new ApiPromise(_doZodFetch(schema, url, requestInit, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zodfetchCursorPage<TItemSchema extends z.ZodTypeAny>(
|
||||||
|
schema: TItemSchema,
|
||||||
|
url: string,
|
||||||
|
params: FetchCursorPageParams,
|
||||||
|
requestInit?: RequestInit,
|
||||||
|
options?: ZodFetchOptions
|
||||||
|
) {
|
||||||
|
const query = new URLSearchParams(params.query);
|
||||||
|
|
||||||
|
if (params.limit) {
|
||||||
|
query.set("page[size]", String(params.limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.after) {
|
||||||
|
query.set("page[after]", params.after);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.before) {
|
||||||
|
query.set("page[before]", params.before);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursorPageSchema = z.object({
|
||||||
|
data: z.array(schema),
|
||||||
|
pagination: z.object({
|
||||||
|
next: z.string().optional(),
|
||||||
|
previous: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const $url = new URL(url);
|
||||||
|
$url.search = query.toString();
|
||||||
|
|
||||||
|
const fetchResult = _doZodFetch(cursorPageSchema, $url.href, requestInit, options);
|
||||||
|
|
||||||
|
return new CursorPagePromise(fetchResult, schema, url, params, requestInit, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zodfetchOffsetLimitPage<TItemSchema extends z.ZodTypeAny>(
|
||||||
|
schema: TItemSchema,
|
||||||
|
url: string,
|
||||||
|
params: FetchOffsetLimitPageParams,
|
||||||
|
requestInit?: RequestInit,
|
||||||
|
options?: ZodFetchOptions
|
||||||
|
) {
|
||||||
|
const query = new URLSearchParams(params.query);
|
||||||
|
|
||||||
|
if (params.limit) {
|
||||||
|
query.set("perPage", String(params.limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.page) {
|
||||||
|
query.set("page", String(params.page));
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetLimitPageSchema = z.object({
|
||||||
|
data: z.array(schema),
|
||||||
|
pagination: z.object({
|
||||||
|
currentPage: z.coerce.number(),
|
||||||
|
totalPages: z.coerce.number(),
|
||||||
|
count: z.coerce.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const $url = new URL(url);
|
||||||
|
$url.search = query.toString();
|
||||||
|
|
||||||
|
const fetchResult = _doZodFetch(offsetLimitPageSchema, $url.href, requestInit, options);
|
||||||
|
|
||||||
|
return new OffsetLimitPagePromise(fetchResult, schema, url, params, requestInit, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zodupload<
|
||||||
|
TResponseBodySchema extends z.ZodTypeAny,
|
||||||
|
TBody = Record<string, unknown>,
|
||||||
|
>(
|
||||||
|
schema: TResponseBodySchema,
|
||||||
|
url: string,
|
||||||
|
body: TBody,
|
||||||
|
requestInit?: RequestInit,
|
||||||
|
options?: ZodFetchOptions
|
||||||
|
): ApiPromise<z.output<TResponseBodySchema>> {
|
||||||
|
const finalRequestInit = createMultipartFormRequestInit(body, requestInit);
|
||||||
|
|
||||||
|
return new ApiPromise(_doZodFetch(schema, url, finalRequestInit, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMultipartFormRequestInit<TBody = Record<string, unknown>>(
|
||||||
|
body: TBody,
|
||||||
|
requestInit?: RequestInit
|
||||||
|
): Promise<RequestInit> {
|
||||||
|
const form = await createForm(body);
|
||||||
|
const encoder = new FormDataEncoder(form);
|
||||||
|
|
||||||
|
const finalHeaders: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(requestInit?.headers || {})) {
|
||||||
|
finalHeaders[key] = value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(encoder.headers)) {
|
||||||
|
finalHeaders[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalHeaders["Content-Length"] = String(encoder.contentLength);
|
||||||
|
|
||||||
|
const finalRequestInit: RequestInit = {
|
||||||
|
...requestInit,
|
||||||
|
headers: finalHeaders,
|
||||||
|
body: Readable.from(encoder) as any,
|
||||||
|
// @ts-expect-error
|
||||||
|
duplex: "half",
|
||||||
|
};
|
||||||
|
|
||||||
|
return finalRequestInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createForm = async <T = Record<string, unknown>>(body: T | undefined): Promise<FormData> => {
|
||||||
|
const form = new FormData();
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))
|
||||||
|
);
|
||||||
|
return form;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ZodFetchResult<T> = {
|
||||||
|
data: T;
|
||||||
|
response: Response;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromiseOrValue<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
async function _doZodFetch<TResponseBodySchema extends z.ZodTypeAny>(
|
||||||
|
schema: TResponseBodySchema,
|
||||||
|
url: string,
|
||||||
|
requestInit?: PromiseOrValue<RequestInit>,
|
||||||
|
options?: ZodFetchOptions,
|
||||||
|
attempt = 1
|
||||||
|
): Promise<ZodFetchResult<z.output<TResponseBodySchema>>> {
|
||||||
|
try {
|
||||||
|
const $requestInit = await requestInit;
|
||||||
|
|
||||||
|
const response = await fetch(url, requestInitWithCache($requestInit));
|
||||||
|
|
||||||
|
const responseHeaders = createResponseHeaders(response.headers);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const retryResult = shouldRetry(response, attempt, options?.retry);
|
||||||
|
|
||||||
|
if (retryResult.retry) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, retryResult.delay));
|
||||||
|
|
||||||
|
return await _doZodFetch(schema, url, requestInit, options, attempt + 1);
|
||||||
|
} else {
|
||||||
|
const errText = await response.text().catch((e) => castToError(e).message);
|
||||||
|
const errJSON = safeJsonParse(errText);
|
||||||
|
const errMessage = errJSON ? undefined : errText;
|
||||||
|
|
||||||
|
throw ApiError.generate(response.status, errJSON, errMessage, responseHeaders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonBody = await response.json();
|
||||||
|
const parsedResult = schema.safeParse(jsonBody);
|
||||||
|
|
||||||
|
if (parsedResult.success) {
|
||||||
|
return { data: parsedResult.data, response };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw fromZodError(parsedResult.error);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.retry) {
|
||||||
|
const retry = { ...defaultRetryOptions, ...options.retry };
|
||||||
|
|
||||||
|
const delay = calculateNextRetryDelay(retry, attempt);
|
||||||
|
|
||||||
|
if (delay) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
|
||||||
|
return await _doZodFetch(schema, url, requestInit, options, attempt + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiConnectionError({ cause: castToError(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function castToError(err: any): Error {
|
||||||
|
if (err instanceof Error) return err;
|
||||||
|
return new Error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShouldRetryResult =
|
||||||
|
| {
|
||||||
|
retry: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
retry: true;
|
||||||
|
delay: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function shouldRetry(
|
||||||
|
response: Response,
|
||||||
|
attempt: number,
|
||||||
|
retryOptions?: RetryOptions
|
||||||
|
): ShouldRetryResult {
|
||||||
|
function shouldRetryForOptions(): ShouldRetryResult {
|
||||||
|
const retry = { ...defaultRetryOptions, ...retryOptions };
|
||||||
|
|
||||||
|
const delay = calculateNextRetryDelay(retry, attempt);
|
||||||
|
|
||||||
|
if (delay) {
|
||||||
|
return { retry: true, delay };
|
||||||
|
} else {
|
||||||
|
return { retry: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note this is not a standard header.
|
||||||
|
const shouldRetryHeader = response.headers.get("x-should-retry");
|
||||||
|
|
||||||
|
// If the server explicitly says whether or not to retry, obey.
|
||||||
|
if (shouldRetryHeader === "true") return shouldRetryForOptions();
|
||||||
|
if (shouldRetryHeader === "false") return { retry: false };
|
||||||
|
|
||||||
|
// Retry on request timeouts.
|
||||||
|
if (response.status === 408) return shouldRetryForOptions();
|
||||||
|
|
||||||
|
// Retry on lock timeouts.
|
||||||
|
if (response.status === 409) return shouldRetryForOptions();
|
||||||
|
|
||||||
|
// Retry on rate limits.
|
||||||
|
if (response.status === 429) return shouldRetryForOptions();
|
||||||
|
|
||||||
|
// Retry internal errors.
|
||||||
|
if (response.status >= 500) return shouldRetryForOptions();
|
||||||
|
|
||||||
|
return { retry: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJsonParse(text: string): any {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResponseHeaders(headers: Response["headers"]): Record<string, string> {
|
||||||
|
return new Proxy(
|
||||||
|
Object.fromEntries(
|
||||||
|
// @ts-ignore
|
||||||
|
headers.entries()
|
||||||
|
),
|
||||||
|
{
|
||||||
|
get(target, name) {
|
||||||
|
const key = name.toString();
|
||||||
|
return target[key.toLowerCase()] || target[key];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestInitWithCache(requestInit?: RequestInit): RequestInit {
|
||||||
|
try {
|
||||||
|
const withCache: RequestInit = {
|
||||||
|
...requestInit,
|
||||||
|
cache: "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
const _ = new Request("http://localhost", withCache);
|
||||||
|
|
||||||
|
return withCache;
|
||||||
|
} catch (error) {
|
||||||
|
return requestInit ?? {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (value == null) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make nested formats configurable
|
||||||
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||||
|
form.append(key, String(value));
|
||||||
|
} else if (
|
||||||
|
isUploadable(value) ||
|
||||||
|
isBlobLike(value) ||
|
||||||
|
value instanceof Buffer ||
|
||||||
|
value instanceof ArrayBuffer
|
||||||
|
) {
|
||||||
|
const file = await toFile(value);
|
||||||
|
form.append(key, file as File);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
await Promise.all(value.map((entry) => addFormValue(form, key + "[]", entry)));
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new TypeError(
|
||||||
|
`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToFileInput = Uploadable | Exclude<BlobLikePart, string> | AsyncIterable<BlobLikePart>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
|
||||||
|
* @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
|
||||||
|
* @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
|
||||||
|
* @param {Object=} options additional properties
|
||||||
|
* @param {string=} options.type the MIME type of the content
|
||||||
|
* @param {number=} options.lastModified the last modified timestamp
|
||||||
|
* @returns a {@link File} with the given properties
|
||||||
|
*/
|
||||||
|
export async function toFile(
|
||||||
|
value: ToFileInput | PromiseLike<ToFileInput>,
|
||||||
|
name?: string | null | undefined,
|
||||||
|
options?: FilePropertyBag | undefined
|
||||||
|
): Promise<FileLike> {
|
||||||
|
// If it's a promise, resolve it.
|
||||||
|
value = await value;
|
||||||
|
|
||||||
|
// Use the file's options if there isn't one provided
|
||||||
|
options ??= isFileLike(value) ? { lastModified: value.lastModified, type: value.type } : {};
|
||||||
|
|
||||||
|
if (isResponseLike(value)) {
|
||||||
|
const blob = await value.blob();
|
||||||
|
name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? "unknown_file";
|
||||||
|
|
||||||
|
return new File([blob as any], name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bits = await getBytes(value);
|
||||||
|
|
||||||
|
name ||= getName(value) ?? "unknown_file";
|
||||||
|
|
||||||
|
if (!options.type) {
|
||||||
|
const type = (bits[0] as any)?.type;
|
||||||
|
if (typeof type === "string") {
|
||||||
|
options = { ...options, type };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File(bits, name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getName(value: any): string | undefined {
|
||||||
|
return (
|
||||||
|
getStringFromMaybeBuffer(value.name) ||
|
||||||
|
getStringFromMaybeBuffer(value.filename) ||
|
||||||
|
// For fs.ReadStream
|
||||||
|
getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStringFromMaybeBuffer = (x: string | Buffer | unknown): string | undefined => {
|
||||||
|
if (typeof x === "string") return x;
|
||||||
|
if (typeof Buffer !== "undefined" && x instanceof Buffer) return String(x);
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getBytes(value: ToFileInput): Promise<Array<BlobPart>> {
|
||||||
|
let parts: Array<BlobPart> = [];
|
||||||
|
if (
|
||||||
|
typeof value === "string" ||
|
||||||
|
ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
|
||||||
|
value instanceof ArrayBuffer
|
||||||
|
) {
|
||||||
|
parts.push(value);
|
||||||
|
} else if (isBlobLike(value)) {
|
||||||
|
parts.push(await value.arrayBuffer());
|
||||||
|
} else if (
|
||||||
|
isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
|
||||||
|
) {
|
||||||
|
for await (const chunk of value) {
|
||||||
|
parts.push(chunk as BlobPart); // TODO, consider validating?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected data type: ${typeof value}; constructor: ${value?.constructor
|
||||||
|
?.name}; props: ${propsForError(value)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function propsForError(value: any): string {
|
||||||
|
const props = Object.getOwnPropertyNames(value);
|
||||||
|
return `[${props.map((p) => `"${p}"`).join(", ")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unknown> =>
|
||||||
|
value != null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended to match web.Blob, node.Blob, node-fetch.Blob, etc.
|
||||||
|
*/
|
||||||
|
export interface BlobLike {
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
|
||||||
|
readonly size: number;
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
|
||||||
|
readonly type: string;
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
|
||||||
|
text(): Promise<string>;
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
|
||||||
|
slice(start?: number, end?: number): BlobLike;
|
||||||
|
// unfortunately @types/node-fetch@^2.6.4 doesn't type the arrayBuffer method
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended to match web.File, node.File, node-fetch.File, etc.
|
||||||
|
*/
|
||||||
|
export interface FileLike extends BlobLike {
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
|
||||||
|
readonly lastModified: number;
|
||||||
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
|
||||||
|
readonly name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended to match web.Response, node.Response, node-fetch.Response, etc.
|
||||||
|
*/
|
||||||
|
export interface ResponseLike {
|
||||||
|
url: string;
|
||||||
|
blob(): Promise<BlobLike>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Uploadable = FileLike | ResponseLike | Readable;
|
||||||
|
|
||||||
|
export const isResponseLike = (value: any): value is ResponseLike =>
|
||||||
|
value != null &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
typeof value.url === "string" &&
|
||||||
|
typeof value.blob === "function";
|
||||||
|
|
||||||
|
export const isFileLike = (value: any): value is FileLike =>
|
||||||
|
value != null &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
typeof value.name === "string" &&
|
||||||
|
typeof value.lastModified === "number" &&
|
||||||
|
isBlobLike(value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The BlobLike type omits arrayBuffer() because @types/node-fetch@^2.6.4 lacks it; but this check
|
||||||
|
* adds the arrayBuffer() method type because it is available and used at runtime
|
||||||
|
*/
|
||||||
|
export const isBlobLike = (
|
||||||
|
value: any
|
||||||
|
): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
|
||||||
|
value != null &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
typeof value.size === "number" &&
|
||||||
|
typeof value.type === "string" &&
|
||||||
|
typeof value.text === "function" &&
|
||||||
|
typeof value.slice === "function" &&
|
||||||
|
typeof value.arrayBuffer === "function";
|
||||||
|
|
||||||
|
export const isFsReadStream = (value: any): value is Readable => value instanceof Readable;
|
||||||
|
|
||||||
|
export const isUploadable = (value: any): value is Uploadable => {
|
||||||
|
return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlobLikePart =
|
||||||
|
| string
|
||||||
|
| ArrayBuffer
|
||||||
|
| ArrayBufferView
|
||||||
|
| BlobLike
|
||||||
|
| Uint8Array
|
||||||
|
| DataView;
|
||||||
|
|
||||||
|
export const isRecordLike = (value: any): value is Record<string, string> =>
|
||||||
|
value != null &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
Object.keys(value).length > 0 &&
|
||||||
|
Object.keys(value).every((key) => typeof key === "string" && typeof value[key] === "string");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subclass of `Promise` providing additional helper methods
|
||||||
|
* for interacting with the SDK.
|
||||||
|
*/
|
||||||
|
export class ApiPromise<T> extends Promise<T> {
|
||||||
|
constructor(private responsePromise: Promise<ZodFetchResult<T>>) {
|
||||||
|
super((resolve) => {
|
||||||
|
// this is maybe a bit weird but this has to be a no-op to not implicitly
|
||||||
|
// parse the response body; instead .then, .catch, .finally are overridden
|
||||||
|
// to parse the response
|
||||||
|
resolve(null as any);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the raw `Response` instance instead of parsing the response
|
||||||
|
* data.
|
||||||
|
*
|
||||||
|
* If you want to parse the response body but still get the `Response`
|
||||||
|
* instance, you can use {@link withResponse()}.
|
||||||
|
*/
|
||||||
|
asResponse(): Promise<Response> {
|
||||||
|
return this.responsePromise.then((p) => p.response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the parsed response data and the raw `Response` instance.
|
||||||
|
*
|
||||||
|
* If you just want to get the raw `Response` instance without parsing it,
|
||||||
|
* you can use {@link asResponse()}.
|
||||||
|
*/
|
||||||
|
async withResponse(): Promise<{ data: T; response: Response }> {
|
||||||
|
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
|
||||||
|
return { data, response };
|
||||||
|
}
|
||||||
|
|
||||||
|
private parse(): Promise<T> {
|
||||||
|
return this.responsePromise.then((result) => result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
override then<TResult1 = T, TResult2 = never>(
|
||||||
|
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||||
|
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
||||||
|
): Promise<TResult1 | TResult2> {
|
||||||
|
return this.parse().then(onfulfilled, onrejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
override catch<TResult = never>(
|
||||||
|
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
|
||||||
|
): Promise<T | TResult> {
|
||||||
|
return this.parse().catch(onrejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
|
||||||
|
return this.parse().finally(onfinally);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CursorPagePromise<TItemSchema extends z.ZodTypeAny>
|
||||||
|
extends ApiPromise<CursorPage<z.output<TItemSchema>>>
|
||||||
|
implements AsyncIterable<z.output<TItemSchema>>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
result: Promise<ZodFetchResult<CursorPageResponse<z.output<TItemSchema>>>>,
|
||||||
|
private schema: TItemSchema,
|
||||||
|
private url: string,
|
||||||
|
private params: FetchCursorPageParams,
|
||||||
|
private requestInit?: RequestInit,
|
||||||
|
private options?: ZodFetchOptions
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
result.then((result) => ({
|
||||||
|
data: new CursorPage(result.data.data, result.data.pagination, this.#fetchPage.bind(this)),
|
||||||
|
response: result.response,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetchPage(params: Omit<CursorPageParams, "limit">): Promise<CursorPage<z.output<TItemSchema>>> {
|
||||||
|
return zodfetchCursorPage(
|
||||||
|
this.schema,
|
||||||
|
this.url,
|
||||||
|
{ ...this.params, ...params },
|
||||||
|
this.requestInit,
|
||||||
|
this.options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow auto-paginating iteration on an unawaited list call, eg:
|
||||||
|
*
|
||||||
|
* for await (const item of client.items.list()) {
|
||||||
|
* console.log(item)
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
const page = await this;
|
||||||
|
for await (const item of page) {
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OffsetLimitPagePromise<TItemSchema extends z.ZodTypeAny>
|
||||||
|
extends ApiPromise<OffsetLimitPage<z.output<TItemSchema>>>
|
||||||
|
implements AsyncIterable<z.output<TItemSchema>>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
result: Promise<ZodFetchResult<OffsetLimitPageResponse<z.output<TItemSchema>>>>,
|
||||||
|
private schema: TItemSchema,
|
||||||
|
private url: string,
|
||||||
|
private params: FetchOffsetLimitPageParams,
|
||||||
|
private requestInit?: RequestInit,
|
||||||
|
private options?: ZodFetchOptions
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
result.then((result) => ({
|
||||||
|
data: new OffsetLimitPage(
|
||||||
|
result.data.data,
|
||||||
|
result.data.pagination,
|
||||||
|
this.#fetchPage.bind(this)
|
||||||
|
),
|
||||||
|
response: result.response,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetchPage(
|
||||||
|
params: Omit<FetchOffsetLimitPageParams, "limit">
|
||||||
|
): Promise<OffsetLimitPage<z.output<TItemSchema>>> {
|
||||||
|
return zodfetchOffsetLimitPage(
|
||||||
|
this.schema,
|
||||||
|
this.url,
|
||||||
|
{ ...this.params, ...params },
|
||||||
|
this.requestInit,
|
||||||
|
this.options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow auto-paginating iteration on an unawaited list call, eg:
|
||||||
|
*
|
||||||
|
* for await (const item of client.items.list()) {
|
||||||
|
* console.log(item)
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
const page = await this;
|
||||||
|
for await (const item of page) {
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export type APIHeaders = Record<string, string | null | undefined>;
|
export type APIHeaders = Record<string, string | null | undefined>;
|
||||||
|
|
||||||
export class APIError extends Error {
|
export class ApiError extends Error {
|
||||||
readonly status: number | undefined;
|
readonly status: number | undefined;
|
||||||
readonly headers: APIHeaders | undefined;
|
readonly headers: APIHeaders | undefined;
|
||||||
readonly error: Object | undefined;
|
readonly error: Object | undefined;
|
||||||
@@ -15,7 +15,7 @@ export class APIError extends Error {
|
|||||||
message: string | undefined,
|
message: string | undefined,
|
||||||
headers: APIHeaders | undefined
|
headers: APIHeaders | undefined
|
||||||
) {
|
) {
|
||||||
super(`${APIError.makeMessage(status, error, message)}`);
|
super(`${ApiError.makeMessage(status, error, message)}`);
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export class APIError extends Error {
|
|||||||
headers: APIHeaders | undefined
|
headers: APIHeaders | undefined
|
||||||
) {
|
) {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return new APIConnectionError({ cause: castToError(errorResponse) });
|
return new ApiConnectionError({ cause: castToError(errorResponse) });
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = (errorResponse as Record<string, any>)?.["error"];
|
const error = (errorResponse as Record<string, any>)?.["error"];
|
||||||
@@ -91,11 +91,11 @@ export class APIError extends Error {
|
|||||||
return new InternalServerError(status, error, message, headers);
|
return new InternalServerError(status, error, message, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new APIError(status, error, message, headers);
|
return new ApiError(status, error, message, headers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class APIConnectionError extends APIError {
|
export class ApiConnectionError extends ApiError {
|
||||||
override readonly status: undefined = undefined;
|
override readonly status: undefined = undefined;
|
||||||
|
|
||||||
constructor({ message, cause }: { message?: string; cause?: Error | undefined }) {
|
constructor({ message, cause }: { message?: string; cause?: Error | undefined }) {
|
||||||
@@ -106,35 +106,35 @@ export class APIConnectionError extends APIError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BadRequestError extends APIError {
|
export class BadRequestError extends ApiError {
|
||||||
override readonly status: 400 = 400;
|
override readonly status: 400 = 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthenticationError extends APIError {
|
export class AuthenticationError extends ApiError {
|
||||||
override readonly status: 401 = 401;
|
override readonly status: 401 = 401;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PermissionDeniedError extends APIError {
|
export class PermissionDeniedError extends ApiError {
|
||||||
override readonly status: 403 = 403;
|
override readonly status: 403 = 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotFoundError extends APIError {
|
export class NotFoundError extends ApiError {
|
||||||
override readonly status: 404 = 404;
|
override readonly status: 404 = 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConflictError extends APIError {
|
export class ConflictError extends ApiError {
|
||||||
override readonly status: 409 = 409;
|
override readonly status: 409 = 409;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnprocessableEntityError extends APIError {
|
export class UnprocessableEntityError extends ApiError {
|
||||||
override readonly status: 422 = 422;
|
override readonly status: 422 = 422;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RateLimitError extends APIError {
|
export class RateLimitError extends ApiError {
|
||||||
override readonly status: 429 = 429;
|
override readonly status: 429 = 429;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InternalServerError extends APIError {}
|
export class InternalServerError extends ApiError {}
|
||||||
|
|
||||||
function castToError(err: any): Error {
|
function castToError(err: any): Error {
|
||||||
if (err instanceof Error) return err;
|
if (err instanceof Error) return err;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { context, propagation } from "@opentelemetry/api";
|
import { context, propagation } from "@opentelemetry/api";
|
||||||
import { version } from "../../../package.json";
|
import { version } from "../../../package.json";
|
||||||
import { APIError } from "../apiErrors";
|
|
||||||
import {
|
import {
|
||||||
BatchTaskRunExecutionResult,
|
BatchTaskRunExecutionResult,
|
||||||
BatchTriggerTaskRequestBody,
|
BatchTriggerTaskRequestBody,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
EnvironmentVariableResponseBody,
|
EnvironmentVariableResponseBody,
|
||||||
EnvironmentVariableValue,
|
EnvironmentVariableValue,
|
||||||
EnvironmentVariables,
|
EnvironmentVariables,
|
||||||
|
ListRunResponseItem,
|
||||||
ListScheduleOptions,
|
ListScheduleOptions,
|
||||||
ListSchedulesResult,
|
ListSchedulesResult,
|
||||||
ReplayRunResponse,
|
ReplayRunResponse,
|
||||||
@@ -25,16 +25,27 @@ import {
|
|||||||
UpdateScheduleOptions,
|
UpdateScheduleOptions,
|
||||||
} from "../schemas";
|
} from "../schemas";
|
||||||
import { taskContext } from "../task-context-api";
|
import { taskContext } from "../task-context-api";
|
||||||
import { ZodFetchOptions, isRecordLike, zodfetch, zodupload } from "../zodfetch";
|
|
||||||
import {
|
import {
|
||||||
ImportEnvironmentVariablesParams,
|
CursorPagePromise,
|
||||||
|
ZodFetchOptions,
|
||||||
|
isRecordLike,
|
||||||
|
zodfetch,
|
||||||
|
zodfetchCursorPage,
|
||||||
|
zodfetchOffsetLimitPage,
|
||||||
|
zodupload,
|
||||||
|
} from "./core";
|
||||||
|
import { ApiError } from "./errors";
|
||||||
|
import {
|
||||||
CreateEnvironmentVariableParams,
|
CreateEnvironmentVariableParams,
|
||||||
|
ImportEnvironmentVariablesParams,
|
||||||
|
ListProjectRunsQueryParams,
|
||||||
|
ListRunsQueryParams,
|
||||||
UpdateEnvironmentVariableParams,
|
UpdateEnvironmentVariableParams,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ImportEnvironmentVariablesParams,
|
|
||||||
CreateEnvironmentVariableParams,
|
CreateEnvironmentVariableParams,
|
||||||
|
ImportEnvironmentVariablesParams,
|
||||||
UpdateEnvironmentVariableParams,
|
UpdateEnvironmentVariableParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,7 +88,7 @@ export class ApiClient {
|
|||||||
zodFetchOptions
|
zodFetchOptions
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof APIError) {
|
if (error instanceof ApiError) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,56 @@ export class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listRuns(query?: ListRunsQueryParams): CursorPagePromise<typeof ListRunResponseItem> {
|
||||||
|
const searchParams = createSearchQueryForListRuns(query);
|
||||||
|
|
||||||
|
return zodfetchCursorPage(
|
||||||
|
ListRunResponseItem,
|
||||||
|
`${this.baseUrl}/api/v1/runs`,
|
||||||
|
{
|
||||||
|
query: searchParams,
|
||||||
|
limit: query?.limit,
|
||||||
|
after: query?.after,
|
||||||
|
before: query?.before,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: this.#getHeaders(false),
|
||||||
|
},
|
||||||
|
zodFetchOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listProjectRuns(
|
||||||
|
projectRef: string,
|
||||||
|
query?: ListProjectRunsQueryParams
|
||||||
|
): CursorPagePromise<typeof ListRunResponseItem> {
|
||||||
|
const searchParams = createSearchQueryForListRuns(query);
|
||||||
|
|
||||||
|
if (query?.env) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[env]",
|
||||||
|
Array.isArray(query.env) ? query.env.join(",") : query.env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodfetchCursorPage(
|
||||||
|
ListRunResponseItem,
|
||||||
|
`${this.baseUrl}/api/v1/projects/${projectRef}/runs`,
|
||||||
|
{
|
||||||
|
query: searchParams,
|
||||||
|
limit: query?.limit,
|
||||||
|
after: query?.after,
|
||||||
|
before: query?.before,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: this.#getHeaders(false),
|
||||||
|
},
|
||||||
|
zodFetchOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
replayRun(runId: string) {
|
replayRun(runId: string) {
|
||||||
return zodfetch(
|
return zodfetch(
|
||||||
ReplayRunResponse,
|
ReplayRunResponse,
|
||||||
@@ -204,9 +265,13 @@ export class ApiClient {
|
|||||||
searchParams.append("perPage", options.perPage.toString());
|
searchParams.append("perPage", options.perPage.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(
|
return zodfetchOffsetLimitPage(
|
||||||
ListSchedulesResult,
|
ScheduleObject,
|
||||||
`${this.baseUrl}/api/v1/schedules${searchParams.size > 0 ? `?${searchParams}` : ""}`,
|
`${this.baseUrl}/api/v1/schedules`,
|
||||||
|
{
|
||||||
|
page: options?.page,
|
||||||
|
limit: options?.perPage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: this.#getHeaders(false),
|
headers: this.#getHeaders(false),
|
||||||
@@ -356,3 +421,62 @@ export class ApiClient {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchParams {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
if (query.status) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[status]",
|
||||||
|
Array.isArray(query.status) ? query.status.join(",") : query.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.taskIdentifier) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[taskIdentifier]",
|
||||||
|
Array.isArray(query.taskIdentifier) ? query.taskIdentifier.join(",") : query.taskIdentifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.version) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[version]",
|
||||||
|
Array.isArray(query.version) ? query.version.join(",") : query.version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.bulkAction) {
|
||||||
|
searchParams.append("filter[bulkAction]", query.bulkAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.schedule) {
|
||||||
|
searchParams.append("filter[schedule]", query.schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof query.isTest === "boolean") {
|
||||||
|
searchParams.append("filter[isTest]", String(query.isTest));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.from) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[createdAt][from]",
|
||||||
|
query.from instanceof Date ? query.from.getTime().toString() : query.from.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.to) {
|
||||||
|
searchParams.append(
|
||||||
|
"filter[createdAt][to]",
|
||||||
|
query.to instanceof Date ? query.to.getTime().toString() : query.to.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.period) {
|
||||||
|
searchParams.append("filter[createdAt][period]", query.period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
export interface CursorPageParams {
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OffsetLimitPageParams {
|
||||||
|
limit?: number;
|
||||||
|
page?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageResponse<Item> {
|
||||||
|
data: Array<Item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CursorPageResponse<Item> extends PageResponse<Item> {
|
||||||
|
pagination: {
|
||||||
|
next?: string;
|
||||||
|
previous?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OffsetLimitPageResponse<Item> extends PageResponse<Item> {
|
||||||
|
pagination: {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Page<Item> {
|
||||||
|
getPaginatedItems(): Item[];
|
||||||
|
hasNextPage(): boolean;
|
||||||
|
hasPreviousPage(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CursorPage<Item> implements CursorPageResponse<Item>, Page<Item>, AsyncIterable<Item> {
|
||||||
|
data: Array<Item>;
|
||||||
|
pagination: { next?: string; previous?: string };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
data: Array<Item>,
|
||||||
|
pagination: { next?: string; previous?: string },
|
||||||
|
private pageFetcher: (params: Omit<CursorPageParams, "limit">) => Promise<CursorPage<Item>>
|
||||||
|
) {
|
||||||
|
this.data = data;
|
||||||
|
this.pagination = pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaginatedItems(): Item[] {
|
||||||
|
return this.data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextPage(): boolean {
|
||||||
|
return !!this.pagination.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPreviousPage(): boolean {
|
||||||
|
return !!this.pagination.previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPage(): Promise<CursorPage<Item>> {
|
||||||
|
if (!this.pagination.next) {
|
||||||
|
throw new Error("No next page available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pageFetcher({ after: this.pagination.next });
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPage(): Promise<CursorPage<Item>> {
|
||||||
|
if (!this.pagination.previous) {
|
||||||
|
throw new Error("No previous page available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pageFetcher({ before: this.pagination.previous });
|
||||||
|
}
|
||||||
|
|
||||||
|
async *iterPages() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
let page: CursorPage<Item> = this;
|
||||||
|
yield page;
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
yield page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
for await (const page of this.iterPages()) {
|
||||||
|
for (const item of page.getPaginatedItems()) {
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OffsetLimitPage<Item>
|
||||||
|
implements OffsetLimitPageResponse<Item>, Page<Item>, AsyncIterable<Item>
|
||||||
|
{
|
||||||
|
data: Array<Item>;
|
||||||
|
pagination: { currentPage: number; totalPages: number; count: number };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
data: Array<Item>,
|
||||||
|
pagination: { currentPage: number; totalPages: number; count: number },
|
||||||
|
private pageFetcher: (
|
||||||
|
params: Omit<OffsetLimitPageParams, "limit">
|
||||||
|
) => Promise<OffsetLimitPage<Item>>
|
||||||
|
) {
|
||||||
|
this.data = data;
|
||||||
|
this.pagination = pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaginatedItems(): Item[] {
|
||||||
|
return this.data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextPage(): boolean {
|
||||||
|
return this.pagination.currentPage < this.pagination.totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPreviousPage(): boolean {
|
||||||
|
return this.pagination.currentPage > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPage(): Promise<OffsetLimitPage<Item>> {
|
||||||
|
if (!this.hasNextPage()) {
|
||||||
|
throw new Error("No next page available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pageFetcher({
|
||||||
|
page: this.pagination.currentPage + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPage(): Promise<OffsetLimitPage<Item>> {
|
||||||
|
if (!this.hasPreviousPage()) {
|
||||||
|
throw new Error("No previous page available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pageFetcher({
|
||||||
|
page: this.pagination.currentPage - 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async *iterPages() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
let page: OffsetLimitPage<Item> = this;
|
||||||
|
yield page;
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
yield page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
for await (const page of this.iterPages()) {
|
||||||
|
for (const item of page.getPaginatedItems()) {
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { BlobLikePart, Uploadable } from "../zodfetch";
|
import { RunStatus } from "../schemas";
|
||||||
|
import { BlobLikePart, Uploadable } from "./core";
|
||||||
|
import { CursorPageParams } from "./pagination";
|
||||||
|
|
||||||
export interface ImportEnvironmentVariablesParams {
|
export interface ImportEnvironmentVariablesParams {
|
||||||
/**
|
/**
|
||||||
@@ -22,3 +24,19 @@ export interface CreateEnvironmentVariableParams {
|
|||||||
export interface UpdateEnvironmentVariableParams {
|
export interface UpdateEnvironmentVariableParams {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListRunsQueryParams extends CursorPageParams {
|
||||||
|
status?: Array<RunStatus> | RunStatus;
|
||||||
|
taskIdentifier?: Array<string> | string;
|
||||||
|
version?: Array<string> | string;
|
||||||
|
from?: Date | number;
|
||||||
|
to?: Date | number;
|
||||||
|
period?: string;
|
||||||
|
bulkAction?: string;
|
||||||
|
schedule?: string;
|
||||||
|
isTest?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListProjectRunsQueryParams extends CursorPageParams, ListRunsQueryParams {
|
||||||
|
env?: Array<"dev" | "staging" | "prod"> | "dev" | "staging" | "prod";
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,15 @@ export function createErrorTaskError(error: TaskRunError): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createJsonErrorObject(error: TaskRunError) {
|
export const SerializedError = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
stackTrace: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SerializedError = z.infer<typeof SerializedError>;
|
||||||
|
|
||||||
|
export function createJsonErrorObject(error: TaskRunError): SerializedError {
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case "BUILT_IN_ERROR": {
|
case "BUILT_IN_ERROR": {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export * from "./apiClient";
|
export * from "./apiClient";
|
||||||
export * from "./apiErrors";
|
export * from "./apiClient/types";
|
||||||
|
export * from "./apiClient/pagination";
|
||||||
|
export type { ApiPromise } from "./apiClient/core";
|
||||||
|
export * from "./apiClient/errors";
|
||||||
export * from "./clock-api";
|
export * from "./clock-api";
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
export * from "./limits";
|
export * from "./limits";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { BackgroundWorkerMetadata, ImageDetailsMetadata } from "./resources";
|
import { BackgroundWorkerMetadata, ImageDetailsMetadata } from "./resources";
|
||||||
import { QueueOptions } from "./schemas";
|
import { QueueOptions } from "./schemas";
|
||||||
|
import { SerializedError } from "../errors";
|
||||||
|
|
||||||
export const WhoAmIResponseSchema = z.object({
|
export const WhoAmIResponseSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
@@ -210,7 +211,7 @@ export const ReplayRunResponse = z.object({
|
|||||||
export type ReplayRunResponse = z.infer<typeof ReplayRunResponse>;
|
export type ReplayRunResponse = z.infer<typeof ReplayRunResponse>;
|
||||||
|
|
||||||
export const CanceledRunResponse = z.object({
|
export const CanceledRunResponse = z.object({
|
||||||
message: z.string(),
|
id: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CanceledRunResponse = z.infer<typeof CanceledRunResponse>;
|
export type CanceledRunResponse = z.infer<typeof CanceledRunResponse>;
|
||||||
@@ -272,17 +273,21 @@ export const UpdateScheduleOptions = CreateScheduleOptions;
|
|||||||
|
|
||||||
export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;
|
export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;
|
||||||
|
|
||||||
|
export const ScheduleGenerator = z.object({
|
||||||
|
type: z.literal("CRON"),
|
||||||
|
expression: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ScheduleGenerator = z.infer<typeof ScheduleGenerator>;
|
||||||
|
|
||||||
export const ScheduleObject = z.object({
|
export const ScheduleObject = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
task: z.string(),
|
task: z.string(),
|
||||||
active: z.boolean(),
|
active: z.boolean(),
|
||||||
deduplicationKey: z.string().nullish(),
|
deduplicationKey: z.string().nullish(),
|
||||||
externalId: z.string().nullish(),
|
externalId: z.string().nullish(),
|
||||||
generator: z.object({
|
generator: ScheduleGenerator,
|
||||||
type: z.literal("CRON"),
|
|
||||||
expression: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
}),
|
|
||||||
nextRun: z.coerce.date().nullish(),
|
nextRun: z.coerce.date().nullish(),
|
||||||
environments: z.array(
|
environments: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -320,12 +325,28 @@ export const ListScheduleOptions = z.object({
|
|||||||
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
|
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
|
||||||
|
|
||||||
export const RunStatus = z.enum([
|
export const RunStatus = z.enum([
|
||||||
"PENDING",
|
/// Task hasn't been deployed yet but is waiting to be executed
|
||||||
|
"WAITING_FOR_DEPLOY",
|
||||||
|
/// Task is waiting to be executed by a worker
|
||||||
|
"QUEUED",
|
||||||
|
/// Task is currently being executed by a worker
|
||||||
"EXECUTING",
|
"EXECUTING",
|
||||||
"PAUSED",
|
/// Task has failed and is waiting to be retried
|
||||||
|
"REATTEMPTING",
|
||||||
|
/// Task has been paused by the system, and will be resumed by the system
|
||||||
|
"FROZEN",
|
||||||
|
/// Task has been completed successfully
|
||||||
"COMPLETED",
|
"COMPLETED",
|
||||||
"FAILED",
|
/// Task has been canceled by the user
|
||||||
"CANCELED",
|
"CANCELED",
|
||||||
|
/// Task has been completed with errors
|
||||||
|
"FAILED",
|
||||||
|
/// Task has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage
|
||||||
|
"CRASHED",
|
||||||
|
/// Task was interrupted during execution, mostly this happens in development environments
|
||||||
|
"INTERRUPTED",
|
||||||
|
/// Task has failed to complete, due to an error in the system
|
||||||
|
"SYSTEM_FAILURE",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type RunStatus = z.infer<typeof RunStatus>;
|
export type RunStatus = z.infer<typeof RunStatus>;
|
||||||
@@ -341,14 +362,47 @@ export const AttemptStatus = z.enum([
|
|||||||
|
|
||||||
export type AttemptStatus = z.infer<typeof AttemptStatus>;
|
export type AttemptStatus = z.infer<typeof AttemptStatus>;
|
||||||
|
|
||||||
export const RetrieveRunResponse = z.object({
|
export const RunEnvironmentDetails = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
user: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RunEnvironmentDetails = z.infer<typeof RunEnvironmentDetails>;
|
||||||
|
|
||||||
|
export const RunScheduleDetails = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
externalId: z.string().optional(),
|
||||||
|
deduplicationKey: z.string().optional(),
|
||||||
|
generator: ScheduleGenerator,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RunScheduleDetails = z.infer<typeof RunScheduleDetails>;
|
||||||
|
|
||||||
|
const CommonRunFields = {
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
status: RunStatus,
|
status: RunStatus,
|
||||||
taskIdentifier: z.string(),
|
taskIdentifier: z.string(),
|
||||||
idempotencyKey: z.string().optional(),
|
idempotencyKey: z.string().optional(),
|
||||||
version: z.string().optional(),
|
version: z.string().optional(),
|
||||||
|
isQueued: z.boolean(),
|
||||||
|
isExecuting: z.boolean(),
|
||||||
|
isCompleted: z.boolean(),
|
||||||
|
isSuccess: z.boolean(),
|
||||||
|
isFailed: z.boolean(),
|
||||||
|
isCancelled: z.boolean(),
|
||||||
|
isTest: z.boolean(),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
updatedAt: z.coerce.date(),
|
updatedAt: z.coerce.date(),
|
||||||
|
startedAt: z.coerce.date().optional(),
|
||||||
|
finishedAt: z.coerce.date().optional(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RetrieveRunResponse = z.object({
|
||||||
|
...CommonRunFields,
|
||||||
|
payload: z.any().optional(),
|
||||||
|
output: z.any().optional(),
|
||||||
|
schedule: RunScheduleDetails.optional(),
|
||||||
attempts: z.array(
|
attempts: z.array(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
@@ -358,6 +412,7 @@ export const RetrieveRunResponse = z.object({
|
|||||||
updatedAt: z.coerce.date(),
|
updatedAt: z.coerce.date(),
|
||||||
startedAt: z.coerce.date().optional(),
|
startedAt: z.coerce.date().optional(),
|
||||||
completedAt: z.coerce.date().optional(),
|
completedAt: z.coerce.date().optional(),
|
||||||
|
error: SerializedError.optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
),
|
),
|
||||||
@@ -365,6 +420,23 @@ export const RetrieveRunResponse = z.object({
|
|||||||
|
|
||||||
export type RetrieveRunResponse = z.infer<typeof RetrieveRunResponse>;
|
export type RetrieveRunResponse = z.infer<typeof RetrieveRunResponse>;
|
||||||
|
|
||||||
|
export const ListRunResponseItem = z.object({
|
||||||
|
...CommonRunFields,
|
||||||
|
env: RunEnvironmentDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListRunResponseItem = z.infer<typeof ListRunResponseItem>;
|
||||||
|
|
||||||
|
export const ListRunResponse = z.object({
|
||||||
|
data: z.array(ListRunResponseItem),
|
||||||
|
pagination: z.object({
|
||||||
|
next: z.string().optional(),
|
||||||
|
previous: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListRunResponse = z.infer<typeof ListRunResponse>;
|
||||||
|
|
||||||
export const CreateEnvironmentVariableRequestBody = z.object({
|
export const CreateEnvironmentVariableRequestBody = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
|
|||||||
@@ -176,7 +176,16 @@ export const TaskFileMetadata = z.object({
|
|||||||
|
|
||||||
export type TaskFileMetadata = z.infer<typeof TaskFileMetadata>;
|
export type TaskFileMetadata = z.infer<typeof TaskFileMetadata>;
|
||||||
|
|
||||||
export const TaskMetadataWithFilePath = TaskMetadata.merge(TaskFileMetadata);
|
export const TaskMetadataWithFilePath = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
packageVersion: z.string(),
|
||||||
|
queue: QueueOptions.optional(),
|
||||||
|
retry: RetryOptions.optional(),
|
||||||
|
machine: Machine.partial().optional(),
|
||||||
|
triggerSource: z.string().optional(),
|
||||||
|
filePath: z.string(),
|
||||||
|
exportName: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export type TaskMetadataWithFilePath = z.infer<typeof TaskMetadataWithFilePath>;
|
export type TaskMetadataWithFilePath = z.infer<typeof TaskMetadataWithFilePath>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,437 +1,3 @@
|
|||||||
import { z } from "zod";
|
export * from "./apiClient/core";
|
||||||
import { fromZodError } from "zod-validation-error";
|
export * from "./apiClient/errors";
|
||||||
import { APIConnectionError, APIError } from "./apiErrors";
|
export * from "./apiClient/pagination";
|
||||||
import { RetryOptions } from "./schemas";
|
|
||||||
import { calculateNextRetryDelay } from "./utils/retries";
|
|
||||||
import { FormDataEncoder } from "form-data-encoder";
|
|
||||||
import { Readable } from "node:stream";
|
|
||||||
|
|
||||||
export const defaultRetryOptions = {
|
|
||||||
maxAttempts: 3,
|
|
||||||
factor: 2,
|
|
||||||
minTimeoutInMs: 1000,
|
|
||||||
maxTimeoutInMs: 60000,
|
|
||||||
randomize: false,
|
|
||||||
} satisfies RetryOptions;
|
|
||||||
|
|
||||||
export type ZodFetchOptions = {
|
|
||||||
retry?: RetryOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function zodfetch<TResponseBodySchema extends z.ZodTypeAny>(
|
|
||||||
schema: TResponseBodySchema,
|
|
||||||
url: string,
|
|
||||||
requestInit?: RequestInit,
|
|
||||||
options?: ZodFetchOptions
|
|
||||||
): Promise<z.output<TResponseBodySchema>> {
|
|
||||||
return await _doZodFetch(schema, url, requestInit, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MultipartBody {
|
|
||||||
constructor(public body: any) {}
|
|
||||||
get [Symbol.toStringTag](): string {
|
|
||||||
return "MultipartBody";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function zodupload<
|
|
||||||
TResponseBodySchema extends z.ZodTypeAny,
|
|
||||||
TBody = Record<string, unknown>,
|
|
||||||
>(
|
|
||||||
schema: TResponseBodySchema,
|
|
||||||
url: string,
|
|
||||||
body: TBody,
|
|
||||||
requestInit?: RequestInit,
|
|
||||||
options?: ZodFetchOptions
|
|
||||||
): Promise<z.output<TResponseBodySchema>> {
|
|
||||||
const form = await createForm(body);
|
|
||||||
const encoder = new FormDataEncoder(form);
|
|
||||||
|
|
||||||
const finalHeaders: Record<string, string> = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(requestInit?.headers || {})) {
|
|
||||||
finalHeaders[key] = value as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(encoder.headers)) {
|
|
||||||
finalHeaders[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
finalHeaders["Content-Length"] = String(encoder.contentLength);
|
|
||||||
|
|
||||||
const finalRequestInit: RequestInit = {
|
|
||||||
...requestInit,
|
|
||||||
headers: finalHeaders,
|
|
||||||
body: Readable.from(encoder) as any,
|
|
||||||
// @ts-expect-error
|
|
||||||
duplex: "half",
|
|
||||||
};
|
|
||||||
|
|
||||||
return await _doZodFetch(schema, url, finalRequestInit, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createForm = async <T = Record<string, unknown>>(
|
|
||||||
body: T | undefined
|
|
||||||
): Promise<FormData> => {
|
|
||||||
const form = new FormData();
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))
|
|
||||||
);
|
|
||||||
return form;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function _doZodFetch<TResponseBodySchema extends z.ZodTypeAny>(
|
|
||||||
schema: TResponseBodySchema,
|
|
||||||
url: string,
|
|
||||||
requestInit?: RequestInit,
|
|
||||||
options?: ZodFetchOptions,
|
|
||||||
attempt = 1
|
|
||||||
): Promise<z.output<TResponseBodySchema>> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, requestInitWithCache(requestInit));
|
|
||||||
|
|
||||||
const responseHeaders = createResponseHeaders(response.headers);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const retryResult = shouldRetry(response, attempt, options?.retry);
|
|
||||||
|
|
||||||
if (retryResult.retry) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, retryResult.delay));
|
|
||||||
|
|
||||||
return await _doZodFetch(schema, url, requestInit, options, attempt + 1);
|
|
||||||
} else {
|
|
||||||
const errText = await response.text().catch((e) => castToError(e).message);
|
|
||||||
const errJSON = safeJsonParse(errText);
|
|
||||||
const errMessage = errJSON ? undefined : errText;
|
|
||||||
|
|
||||||
throw APIError.generate(response.status, errJSON, errMessage, responseHeaders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonBody = await response.json();
|
|
||||||
const parsedResult = schema.safeParse(jsonBody);
|
|
||||||
|
|
||||||
if (parsedResult.success) {
|
|
||||||
return parsedResult.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw fromZodError(parsedResult.error);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof APIError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.retry) {
|
|
||||||
const retry = { ...defaultRetryOptions, ...options.retry };
|
|
||||||
|
|
||||||
const delay = calculateNextRetryDelay(retry, attempt);
|
|
||||||
|
|
||||||
if (delay) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
|
|
||||||
return await _doZodFetch(schema, url, requestInit, options, attempt + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new APIConnectionError({ cause: castToError(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function castToError(err: any): Error {
|
|
||||||
if (err instanceof Error) return err;
|
|
||||||
return new Error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShouldRetryResult =
|
|
||||||
| {
|
|
||||||
retry: false;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
retry: true;
|
|
||||||
delay: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function shouldRetry(
|
|
||||||
response: Response,
|
|
||||||
attempt: number,
|
|
||||||
retryOptions?: RetryOptions
|
|
||||||
): ShouldRetryResult {
|
|
||||||
function shouldRetryForOptions(): ShouldRetryResult {
|
|
||||||
const retry = { ...defaultRetryOptions, ...retryOptions };
|
|
||||||
|
|
||||||
const delay = calculateNextRetryDelay(retry, attempt);
|
|
||||||
|
|
||||||
if (delay) {
|
|
||||||
return { retry: true, delay };
|
|
||||||
} else {
|
|
||||||
return { retry: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note this is not a standard header.
|
|
||||||
const shouldRetryHeader = response.headers.get("x-should-retry");
|
|
||||||
|
|
||||||
// If the server explicitly says whether or not to retry, obey.
|
|
||||||
if (shouldRetryHeader === "true") return shouldRetryForOptions();
|
|
||||||
if (shouldRetryHeader === "false") return { retry: false };
|
|
||||||
|
|
||||||
// Retry on request timeouts.
|
|
||||||
if (response.status === 408) return shouldRetryForOptions();
|
|
||||||
|
|
||||||
// Retry on lock timeouts.
|
|
||||||
if (response.status === 409) return shouldRetryForOptions();
|
|
||||||
|
|
||||||
// Retry on rate limits.
|
|
||||||
if (response.status === 429) return shouldRetryForOptions();
|
|
||||||
|
|
||||||
// Retry internal errors.
|
|
||||||
if (response.status >= 500) return shouldRetryForOptions();
|
|
||||||
|
|
||||||
return { retry: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeJsonParse(text: string): any {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createResponseHeaders(headers: Response["headers"]): Record<string, string> {
|
|
||||||
return new Proxy(
|
|
||||||
Object.fromEntries(
|
|
||||||
// @ts-ignore
|
|
||||||
headers.entries()
|
|
||||||
),
|
|
||||||
{
|
|
||||||
get(target, name) {
|
|
||||||
const key = name.toString();
|
|
||||||
return target[key.toLowerCase()] || target[key];
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestInitWithCache(requestInit?: RequestInit): RequestInit {
|
|
||||||
try {
|
|
||||||
const withCache: RequestInit = {
|
|
||||||
...requestInit,
|
|
||||||
cache: "no-cache",
|
|
||||||
};
|
|
||||||
|
|
||||||
const _ = new Request("http://localhost", withCache);
|
|
||||||
|
|
||||||
return withCache;
|
|
||||||
} catch (error) {
|
|
||||||
return requestInit ?? {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
|
|
||||||
if (value === undefined) return;
|
|
||||||
if (value == null) {
|
|
||||||
throw new TypeError(
|
|
||||||
`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make nested formats configurable
|
|
||||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
||||||
form.append(key, String(value));
|
|
||||||
} else if (
|
|
||||||
isUploadable(value) ||
|
|
||||||
isBlobLike(value) ||
|
|
||||||
value instanceof Buffer ||
|
|
||||||
value instanceof ArrayBuffer
|
|
||||||
) {
|
|
||||||
const file = await toFile(value);
|
|
||||||
form.append(key, file as File);
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
await Promise.all(value.map((entry) => addFormValue(form, key + "[]", entry)));
|
|
||||||
} else if (typeof value === "object") {
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new TypeError(
|
|
||||||
`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ToFileInput = Uploadable | Exclude<BlobLikePart, string> | AsyncIterable<BlobLikePart>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
|
|
||||||
* @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
|
|
||||||
* @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
|
|
||||||
* @param {Object=} options additional properties
|
|
||||||
* @param {string=} options.type the MIME type of the content
|
|
||||||
* @param {number=} options.lastModified the last modified timestamp
|
|
||||||
* @returns a {@link File} with the given properties
|
|
||||||
*/
|
|
||||||
export async function toFile(
|
|
||||||
value: ToFileInput | PromiseLike<ToFileInput>,
|
|
||||||
name?: string | null | undefined,
|
|
||||||
options?: FilePropertyBag | undefined
|
|
||||||
): Promise<FileLike> {
|
|
||||||
// If it's a promise, resolve it.
|
|
||||||
value = await value;
|
|
||||||
|
|
||||||
// Use the file's options if there isn't one provided
|
|
||||||
options ??= isFileLike(value) ? { lastModified: value.lastModified, type: value.type } : {};
|
|
||||||
|
|
||||||
if (isResponseLike(value)) {
|
|
||||||
const blob = await value.blob();
|
|
||||||
name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? "unknown_file";
|
|
||||||
|
|
||||||
return new File([blob as any], name, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bits = await getBytes(value);
|
|
||||||
|
|
||||||
name ||= getName(value) ?? "unknown_file";
|
|
||||||
|
|
||||||
if (!options.type) {
|
|
||||||
const type = (bits[0] as any)?.type;
|
|
||||||
if (typeof type === "string") {
|
|
||||||
options = { ...options, type };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new File(bits, name, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getName(value: any): string | undefined {
|
|
||||||
return (
|
|
||||||
getStringFromMaybeBuffer(value.name) ||
|
|
||||||
getStringFromMaybeBuffer(value.filename) ||
|
|
||||||
// For fs.ReadStream
|
|
||||||
getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStringFromMaybeBuffer = (x: string | Buffer | unknown): string | undefined => {
|
|
||||||
if (typeof x === "string") return x;
|
|
||||||
if (typeof Buffer !== "undefined" && x instanceof Buffer) return String(x);
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getBytes(value: ToFileInput): Promise<Array<BlobPart>> {
|
|
||||||
let parts: Array<BlobPart> = [];
|
|
||||||
if (
|
|
||||||
typeof value === "string" ||
|
|
||||||
ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
|
|
||||||
value instanceof ArrayBuffer
|
|
||||||
) {
|
|
||||||
parts.push(value);
|
|
||||||
} else if (isBlobLike(value)) {
|
|
||||||
parts.push(await value.arrayBuffer());
|
|
||||||
} else if (
|
|
||||||
isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
|
|
||||||
) {
|
|
||||||
for await (const chunk of value) {
|
|
||||||
parts.push(chunk as BlobPart); // TODO, consider validating?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Unexpected data type: ${typeof value}; constructor: ${value?.constructor
|
|
||||||
?.name}; props: ${propsForError(value)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function propsForError(value: any): string {
|
|
||||||
const props = Object.getOwnPropertyNames(value);
|
|
||||||
return `[${props.map((p) => `"${p}"`).join(", ")}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unknown> =>
|
|
||||||
value != null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intended to match web.Blob, node.Blob, node-fetch.Blob, etc.
|
|
||||||
*/
|
|
||||||
export interface BlobLike {
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
|
|
||||||
readonly size: number;
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
|
|
||||||
readonly type: string;
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
|
|
||||||
text(): Promise<string>;
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
|
|
||||||
slice(start?: number, end?: number): BlobLike;
|
|
||||||
// unfortunately @types/node-fetch@^2.6.4 doesn't type the arrayBuffer method
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intended to match web.File, node.File, node-fetch.File, etc.
|
|
||||||
*/
|
|
||||||
export interface FileLike extends BlobLike {
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
|
|
||||||
readonly lastModified: number;
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
|
|
||||||
readonly name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intended to match web.Response, node.Response, node-fetch.Response, etc.
|
|
||||||
*/
|
|
||||||
export interface ResponseLike {
|
|
||||||
url: string;
|
|
||||||
blob(): Promise<BlobLike>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Uploadable = FileLike | ResponseLike | Readable;
|
|
||||||
|
|
||||||
export const isResponseLike = (value: any): value is ResponseLike =>
|
|
||||||
value != null &&
|
|
||||||
typeof value === "object" &&
|
|
||||||
typeof value.url === "string" &&
|
|
||||||
typeof value.blob === "function";
|
|
||||||
|
|
||||||
export const isFileLike = (value: any): value is FileLike =>
|
|
||||||
value != null &&
|
|
||||||
typeof value === "object" &&
|
|
||||||
typeof value.name === "string" &&
|
|
||||||
typeof value.lastModified === "number" &&
|
|
||||||
isBlobLike(value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The BlobLike type omits arrayBuffer() because @types/node-fetch@^2.6.4 lacks it; but this check
|
|
||||||
* adds the arrayBuffer() method type because it is available and used at runtime
|
|
||||||
*/
|
|
||||||
export const isBlobLike = (
|
|
||||||
value: any
|
|
||||||
): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
|
|
||||||
value != null &&
|
|
||||||
typeof value === "object" &&
|
|
||||||
typeof value.size === "number" &&
|
|
||||||
typeof value.type === "string" &&
|
|
||||||
typeof value.text === "function" &&
|
|
||||||
typeof value.slice === "function" &&
|
|
||||||
typeof value.arrayBuffer === "function";
|
|
||||||
|
|
||||||
export const isFsReadStream = (value: any): value is Readable => value instanceof Readable;
|
|
||||||
|
|
||||||
export const isUploadable = (value: any): value is Uploadable => {
|
|
||||||
return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BlobLikePart =
|
|
||||||
| string
|
|
||||||
| ArrayBuffer
|
|
||||||
| ArrayBufferView
|
|
||||||
| BlobLike
|
|
||||||
| Uint8Array
|
|
||||||
| DataView;
|
|
||||||
|
|
||||||
export const isRecordLike = (value: any): value is Record<string, string> =>
|
|
||||||
value != null &&
|
|
||||||
typeof value === "object" &&
|
|
||||||
!Array.isArray(value) &&
|
|
||||||
Object.keys(value).length > 0 &&
|
|
||||||
Object.keys(value).every((key) => typeof key === "string" && typeof value[key] === "string");
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import type {
|
import type {
|
||||||
ImportEnvironmentVariablesParams,
|
ApiPromise,
|
||||||
EnvironmentVariableResponseBody,
|
|
||||||
EnvironmentVariables,
|
|
||||||
CreateEnvironmentVariableParams,
|
CreateEnvironmentVariableParams,
|
||||||
|
EnvironmentVariableResponseBody,
|
||||||
EnvironmentVariableValue,
|
EnvironmentVariableValue,
|
||||||
|
EnvironmentVariables,
|
||||||
|
ImportEnvironmentVariablesParams,
|
||||||
UpdateEnvironmentVariableParams,
|
UpdateEnvironmentVariableParams,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import { SemanticInternalAttributes, apiClientManager, taskContext } from "@trigger.dev/core/v3";
|
import { apiClientManager, taskContext } from "@trigger.dev/core/v3";
|
||||||
import { apiClientMissingError } from "./shared";
|
import { apiClientMissingError } from "./shared";
|
||||||
import { tracer } from "./tracer";
|
|
||||||
|
|
||||||
export type { ImportEnvironmentVariablesParams, CreateEnvironmentVariableParams };
|
export type { CreateEnvironmentVariableParams, ImportEnvironmentVariablesParams };
|
||||||
|
|
||||||
export async function upload(
|
export function upload(
|
||||||
projectRef: string,
|
projectRef: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
params: ImportEnvironmentVariablesParams
|
params: ImportEnvironmentVariablesParams
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function upload(
|
export function upload(
|
||||||
params: ImportEnvironmentVariablesParams
|
params: ImportEnvironmentVariablesParams
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function upload(
|
export function upload(
|
||||||
projectRefOrParams: string | ImportEnvironmentVariablesParams,
|
projectRefOrParams: string | ImportEnvironmentVariablesParams,
|
||||||
slug?: string,
|
slug?: string,
|
||||||
params?: ImportEnvironmentVariablesParams
|
params?: ImportEnvironmentVariablesParams
|
||||||
): Promise<EnvironmentVariableResponseBody> {
|
): ApiPromise<EnvironmentVariableResponseBody> {
|
||||||
let $projectRef: string;
|
let $projectRef: string;
|
||||||
let $params: ImportEnvironmentVariablesParams;
|
let $params: ImportEnvironmentVariablesParams;
|
||||||
let $slug: string;
|
let $slug: string;
|
||||||
@@ -68,22 +68,12 @@ export async function upload(
|
|||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tracer.startActiveSpan(
|
return apiClient.importEnvVars($projectRef, $slug, $params);
|
||||||
"envvars.upload",
|
|
||||||
async (span) => {
|
|
||||||
return await apiClient.importEnvVars($projectRef, $slug, $params);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[SemanticInternalAttributes.STYLE_ICON]: "file-upload",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function list(projectRef: string, slug: string): Promise<EnvironmentVariables>;
|
export function list(projectRef: string, slug: string): ApiPromise<EnvironmentVariables>;
|
||||||
export async function list(): Promise<EnvironmentVariables>;
|
export function list(): ApiPromise<EnvironmentVariables>;
|
||||||
export async function list(projectRef?: string, slug?: string): Promise<EnvironmentVariables> {
|
export function list(projectRef?: string, slug?: string): ApiPromise<EnvironmentVariables> {
|
||||||
const $projectRef = projectRef ?? taskContext.ctx?.project.ref;
|
const $projectRef = projectRef ?? taskContext.ctx?.project.ref;
|
||||||
const $slug = slug ?? taskContext.ctx?.environment.slug;
|
const $slug = slug ?? taskContext.ctx?.environment.slug;
|
||||||
|
|
||||||
@@ -101,32 +91,22 @@ export async function list(projectRef?: string, slug?: string): Promise<Environm
|
|||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tracer.startActiveSpan(
|
return apiClient.listEnvVars($projectRef, $slug);
|
||||||
"envvars.list",
|
|
||||||
async (span) => {
|
|
||||||
return await apiClient.listEnvVars($projectRef, $slug);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[SemanticInternalAttributes.STYLE_ICON]: "id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(
|
export function create(
|
||||||
projectRef: string,
|
projectRef: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
params: CreateEnvironmentVariableParams
|
params: CreateEnvironmentVariableParams
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function create(
|
export function create(
|
||||||
params: CreateEnvironmentVariableParams
|
params: CreateEnvironmentVariableParams
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function create(
|
export function create(
|
||||||
projectRefOrParams: string | CreateEnvironmentVariableParams,
|
projectRefOrParams: string | CreateEnvironmentVariableParams,
|
||||||
slug?: string,
|
slug?: string,
|
||||||
params?: CreateEnvironmentVariableParams
|
params?: CreateEnvironmentVariableParams
|
||||||
): Promise<EnvironmentVariableResponseBody> {
|
): ApiPromise<EnvironmentVariableResponseBody> {
|
||||||
let $projectRef: string;
|
let $projectRef: string;
|
||||||
let $slug: string;
|
let $slug: string;
|
||||||
let $params: CreateEnvironmentVariableParams;
|
let $params: CreateEnvironmentVariableParams;
|
||||||
@@ -170,30 +150,20 @@ export async function create(
|
|||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tracer.startActiveSpan(
|
return apiClient.createEnvVar($projectRef, $slug, $params);
|
||||||
"envvars.create",
|
|
||||||
async (span) => {
|
|
||||||
return await apiClient.createEnvVar($projectRef, $slug, $params);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[SemanticInternalAttributes.STYLE_ICON]: "id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retrieve(
|
export function retrieve(
|
||||||
projectRef: string,
|
projectRef: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
name: string
|
name: string
|
||||||
): Promise<EnvironmentVariableValue>;
|
): ApiPromise<EnvironmentVariableValue>;
|
||||||
export async function retrieve(name: string): Promise<EnvironmentVariableValue>;
|
export function retrieve(name: string): ApiPromise<EnvironmentVariableValue>;
|
||||||
export async function retrieve(
|
export function retrieve(
|
||||||
projectRefOrName: string,
|
projectRefOrName: string,
|
||||||
slug?: string,
|
slug?: string,
|
||||||
name?: string
|
name?: string
|
||||||
): Promise<EnvironmentVariableValue> {
|
): ApiPromise<EnvironmentVariableValue> {
|
||||||
let $projectRef: string;
|
let $projectRef: string;
|
||||||
let $slug: string;
|
let $slug: string;
|
||||||
let $name: string;
|
let $name: string;
|
||||||
@@ -222,30 +192,20 @@ export async function retrieve(
|
|||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tracer.startActiveSpan(
|
return apiClient.retrieveEnvVar($projectRef, $slug, $name);
|
||||||
"envvars.retrieve",
|
|
||||||
async (span) => {
|
|
||||||
return await apiClient.retrieveEnvVar($projectRef, $slug, $name);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[SemanticInternalAttributes.STYLE_ICON]: "id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function del(
|
export function del(
|
||||||
projectRef: string,
|
projectRef: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
name: string
|
name: string
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function del(name: string): Promise<EnvironmentVariableResponseBody>;
|
export function del(name: string): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function del(
|
export function del(
|
||||||
projectRefOrName: string,
|
projectRefOrName: string,
|
||||||
slug?: string,
|
slug?: string,
|
||||||
name?: string
|
name?: string
|
||||||
): Promise<EnvironmentVariableResponseBody> {
|
): ApiPromise<EnvironmentVariableResponseBody> {
|
||||||
let $projectRef: string;
|
let $projectRef: string;
|
||||||
let $slug: string;
|
let $slug: string;
|
||||||
let $name: string;
|
let $name: string;
|
||||||
@@ -274,35 +234,25 @@ export async function del(
|
|||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tracer.startActiveSpan(
|
return apiClient.deleteEnvVar($projectRef, $slug, $name);
|
||||||
"envvars.delete",
|
|
||||||
async (span) => {
|
|
||||||
return await apiClient.deleteEnvVar($projectRef, $slug, $name);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[SemanticInternalAttributes.STYLE_ICON]: "id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(
|
export function update(
|
||||||
projectRef: string,
|
projectRef: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
name: string,
|
name: string,
|
||||||
params: UpdateEnvironmentVariableParams
|
params: UpdateEnvironmentVariableParams
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function update(
|
export function update(
|
||||||
name: string,
|
name: string,
|
||||||
params: UpdateEnvironmentVariableParams
|
params: UpdateEnvironmentVariableParams
|
||||||
): Promise<EnvironmentVariableResponseBody>;
|
): ApiPromise<EnvironmentVariableResponseBody>;
|
||||||
export async function update(
|
export function update(
|
||||||
projectRefOrName: string,
|
projectRefOrName: string,
|
||||||
slugOrParams: string | UpdateEnvironmentVariableParams,
|
slugOrParams: string | UpdateEnvironmentVariableParams,
|
||||||
name?: string,
|
name?: string,
|
||||||
params?: UpdateEnvironmentVariableParams
|
params?: UpdateEnvironmentVariableParams
|
||||||
): Promise<EnvironmentVariableResponseBody> {
|
): ApiPromise<EnvironmentVariableResponseBody> {
|
||||||
let $projectRef: string;
|
let $projectRef: string;
|
||||||
let $slug: string;
|
let $slug: string;
|
||||||
let $name: string;
|
let $name: string;
|
||||||
@@ -350,15 +300,5 @@ export async function update(
|
|||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tracer.startActiveSpan(
|
return apiClient.updateEnvVar($projectRef, $slug, $name, $params);
|
||||||
"envvars.update",
|
|
||||||
async (span) => {
|
|
||||||
return await apiClient.updateEnvVar($projectRef, $slug, $name, $params);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[SemanticInternalAttributes.STYLE_ICON]: "id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { apiClientManager } from "@trigger.dev/core/v3";
|
|||||||
export type { ApiClientConfiguration };
|
export type { ApiClientConfiguration };
|
||||||
|
|
||||||
export {
|
export {
|
||||||
APIError,
|
ApiError,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
ConflictError,
|
ConflictError,
|
||||||
|
|||||||
@@ -1,43 +1,74 @@
|
|||||||
import {
|
import {
|
||||||
|
ApiPromise,
|
||||||
CanceledRunResponse,
|
CanceledRunResponse,
|
||||||
|
ListRunResponseItem,
|
||||||
ReplayRunResponse,
|
ReplayRunResponse,
|
||||||
RetrieveRunResponse,
|
RetrieveRunResponse,
|
||||||
apiClientManager,
|
apiClientManager,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
|
import type { ListProjectRunsQueryParams, ListRunsQueryParams } from "@trigger.dev/core/v3";
|
||||||
import { apiClientMissingError } from "./shared";
|
import { apiClientMissingError } from "./shared";
|
||||||
|
import { CursorPagePromise } from "@trigger.dev/core/v3/apiClient/core";
|
||||||
|
|
||||||
|
export type RetrieveRunResult = RetrieveRunResponse;
|
||||||
|
|
||||||
export const runs = {
|
export const runs = {
|
||||||
replay: replayRun,
|
replay: replayRun,
|
||||||
cancel: cancelRun,
|
cancel: cancelRun,
|
||||||
retrieve: retrieveRun,
|
retrieve: retrieveRun,
|
||||||
|
list: listRuns,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function retrieveRun(runId: string): Promise<RetrieveRunResponse> {
|
export type ListRunsItem = ListRunResponseItem;
|
||||||
|
|
||||||
|
function listRuns(
|
||||||
|
projectRef: string,
|
||||||
|
params?: ListProjectRunsQueryParams
|
||||||
|
): CursorPagePromise<typeof ListRunResponseItem>;
|
||||||
|
function listRuns(params?: ListRunsQueryParams): CursorPagePromise<typeof ListRunResponseItem>;
|
||||||
|
function listRuns(
|
||||||
|
paramsOrProjectRef?: ListRunsQueryParams | string,
|
||||||
|
params?: ListRunsQueryParams | ListProjectRunsQueryParams
|
||||||
|
): CursorPagePromise<typeof ListRunResponseItem> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await apiClient.retrieveRun(runId);
|
if (typeof paramsOrProjectRef === "string") {
|
||||||
|
return apiClient.listProjectRuns(paramsOrProjectRef, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.listRuns(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replayRun(runId: string): Promise<ReplayRunResponse> {
|
function retrieveRun(runId: string): ApiPromise<RetrieveRunResult> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await apiClient.replayRun(runId);
|
return apiClient.retrieveRun(runId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelRun(runId: string): Promise<CanceledRunResponse> {
|
function replayRun(runId: string): ApiPromise<ReplayRunResponse> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
throw apiClientMissingError();
|
throw apiClientMissingError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await apiClient.cancelRun(runId);
|
return apiClient.replayRun(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRun(runId: string): ApiPromise<CanceledRunResponse> {
|
||||||
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
|
if (!apiClient) {
|
||||||
|
throw apiClientMissingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.cancelRun(runId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
|
ApiPromise,
|
||||||
DeletedScheduleObject,
|
DeletedScheduleObject,
|
||||||
InitOutput,
|
InitOutput,
|
||||||
ListSchedulesResult,
|
|
||||||
ScheduleObject,
|
ScheduleObject,
|
||||||
apiClientManager,
|
apiClientManager,
|
||||||
taskCatalog,
|
taskCatalog,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared";
|
import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared";
|
||||||
import * as SchedulesAPI from "./api";
|
import * as SchedulesAPI from "./api";
|
||||||
|
import { OffsetLimitPagePromise } from "@trigger.dev/core/v3/apiClient/core";
|
||||||
|
|
||||||
export function task<TOutput, TInitOutput extends InitOutput>(
|
export function task<TOutput, TInitOutput extends InitOutput>(
|
||||||
params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
|
params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
|
||||||
@@ -30,7 +31,7 @@ export function task<TOutput, TInitOutput extends InitOutput>(
|
|||||||
* @param options.deduplicationKey - An optional deduplication key for the schedule
|
* @param options.deduplicationKey - An optional deduplication key for the schedule
|
||||||
* @returns The created schedule
|
* @returns The created schedule
|
||||||
*/
|
*/
|
||||||
export async function create(options: SchedulesAPI.CreateScheduleOptions): Promise<ScheduleObject> {
|
export function create(options: SchedulesAPI.CreateScheduleOptions): ApiPromise<ScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
@@ -45,7 +46,7 @@ export async function create(options: SchedulesAPI.CreateScheduleOptions): Promi
|
|||||||
* @param scheduleId - The ID of the schedule to retrieve
|
* @param scheduleId - The ID of the schedule to retrieve
|
||||||
* @returns The retrieved schedule
|
* @returns The retrieved schedule
|
||||||
*/
|
*/
|
||||||
export async function retrieve(scheduleId: string): Promise<ScheduleObject> {
|
export function retrieve(scheduleId: string): ApiPromise<ScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
@@ -64,10 +65,10 @@ export async function retrieve(scheduleId: string): Promise<ScheduleObject> {
|
|||||||
* @param options.externalId - An optional external identifier for the schedule
|
* @param options.externalId - An optional external identifier for the schedule
|
||||||
* @returns The updated schedule
|
* @returns The updated schedule
|
||||||
*/
|
*/
|
||||||
export async function update(
|
export function update(
|
||||||
scheduleId: string,
|
scheduleId: string,
|
||||||
options: SchedulesAPI.UpdateScheduleOptions
|
options: SchedulesAPI.UpdateScheduleOptions
|
||||||
): Promise<ScheduleObject> {
|
): ApiPromise<ScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
@@ -81,7 +82,7 @@ export async function update(
|
|||||||
* Deletes a schedule
|
* Deletes a schedule
|
||||||
* @param scheduleId - The ID of the schedule to delete
|
* @param scheduleId - The ID of the schedule to delete
|
||||||
*/
|
*/
|
||||||
export async function del(scheduleId: string): Promise<DeletedScheduleObject> {
|
export function del(scheduleId: string): ApiPromise<DeletedScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
@@ -95,7 +96,7 @@ export async function del(scheduleId: string): Promise<DeletedScheduleObject> {
|
|||||||
* Deactivates a schedule
|
* Deactivates a schedule
|
||||||
* @param scheduleId - The ID of the schedule to deactivate
|
* @param scheduleId - The ID of the schedule to deactivate
|
||||||
*/
|
*/
|
||||||
export async function deactivate(scheduleId: string): Promise<ScheduleObject> {
|
export function deactivate(scheduleId: string): ApiPromise<ScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
@@ -109,7 +110,7 @@ export async function deactivate(scheduleId: string): Promise<ScheduleObject> {
|
|||||||
* Activates a schedule
|
* Activates a schedule
|
||||||
* @param scheduleId - The ID of the schedule to activate
|
* @param scheduleId - The ID of the schedule to activate
|
||||||
*/
|
*/
|
||||||
export async function activate(scheduleId: string): Promise<ScheduleObject> {
|
export function activate(scheduleId: string): ApiPromise<ScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
@@ -126,9 +127,9 @@ export async function activate(scheduleId: string): Promise<ScheduleObject> {
|
|||||||
* @param options.perPage - The number of schedules per page
|
* @param options.perPage - The number of schedules per page
|
||||||
* @returns The list of schedules
|
* @returns The list of schedules
|
||||||
*/
|
*/
|
||||||
export async function list(
|
export function list(
|
||||||
options?: SchedulesAPI.ListScheduleOptions
|
options?: SchedulesAPI.ListScheduleOptions
|
||||||
): Promise<ListSchedulesResult> {
|
): OffsetLimitPagePromise<typeof ScheduleObject> {
|
||||||
const apiClient = apiClientManager.client;
|
const apiClient = apiClientManager.client;
|
||||||
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { tracer } from "./tracer";
|
import { configure, envvars, runs, schedules, ApiError } from "@trigger.dev/sdk/v3";
|
||||||
import { APIError, configure, runs, schedules, envvars } from "@trigger.dev/sdk/v3";
|
|
||||||
import { simpleChildTask } from "./trigger/subtasks";
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { firstScheduledTask } from "./trigger/scheduled";
|
|
||||||
import { createReadStream } from "node:fs";
|
import { createReadStream } from "node:fs";
|
||||||
|
import { firstScheduledTask } from "./trigger/scheduled";
|
||||||
|
import { simpleChildTask } from "./trigger/subtasks";
|
||||||
|
import { taskThatErrors } from "./trigger/retries";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
async function uploadEnvVars() {
|
async function doEnvVars() {
|
||||||
configure({
|
configure({
|
||||||
secretKey: process.env.TRIGGER_ACCESS_TOKEN,
|
secretKey: process.env.TRIGGER_ACCESS_TOKEN,
|
||||||
});
|
});
|
||||||
@@ -88,37 +88,113 @@ async function uploadEnvVars() {
|
|||||||
console.log("response6", response6);
|
console.log("response6", response6);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run() {
|
async function doRuns() {
|
||||||
await tracer.startActiveSpan("run", async (span) => {
|
|
||||||
try {
|
|
||||||
const run = await simpleChildTask.trigger({ message: "Hello, World!" });
|
const run = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||||
|
|
||||||
const retrievedRun = await runs.retrieve(run.id);
|
const retrievedRun = await runs.retrieve(run.id);
|
||||||
console.log("retrieved run", retrievedRun);
|
console.log("retrieved run", retrievedRun);
|
||||||
|
|
||||||
const canceled = await runs.cancel(run.id);
|
const completedRun = await waitForRunToComplete(run.id);
|
||||||
console.log("canceled run", canceled);
|
console.log("completed run", completedRun);
|
||||||
|
|
||||||
const replayed = await runs.replay(run.id);
|
const failingRun = await taskThatErrors.trigger({ message: "Hello, World!" });
|
||||||
console.log("replayed run", replayed);
|
const failedRun = await waitForRunToComplete(failingRun.id);
|
||||||
|
|
||||||
const run2 = await simpleChildTask.trigger(
|
console.log("failed run", failedRun);
|
||||||
{ message: "Hello, World!" },
|
|
||||||
{
|
const replayableRun = await runs.replay(failedRun.id);
|
||||||
idempotencyKey: "mmvlgwcidiklyeygen4",
|
const replayedRun = await waitForRunToExecute(replayableRun.id);
|
||||||
|
|
||||||
|
console.log("replayed run", replayedRun);
|
||||||
|
|
||||||
|
const canceledRun = await runs.cancel(replayedRun.id);
|
||||||
|
const canceledRunResult = await waitForRunToComplete(canceledRun.id);
|
||||||
|
|
||||||
|
console.log("canceled run", canceledRunResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doListRuns() {
|
||||||
|
let pageCount = 0;
|
||||||
|
|
||||||
|
let page = await runs.list({
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`run page #${++pageCount}`);
|
||||||
|
|
||||||
|
// Convenience methods are provided for manually paginating:
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
console.log(`run page #${++pageCount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while (page.hasPreviousPage()) {
|
||||||
|
page = await page.getPreviousPage();
|
||||||
|
console.log(`run page #${--pageCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const run of runs.list({
|
||||||
|
status: ["COMPLETED"],
|
||||||
|
period: "1y",
|
||||||
|
})) {
|
||||||
|
console.log(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
let withResponse = await runs
|
||||||
|
.list({
|
||||||
|
limit: 100,
|
||||||
|
})
|
||||||
|
.withResponse();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"withResponse",
|
||||||
|
withResponse.response.status,
|
||||||
|
withResponse.response.headers,
|
||||||
|
withResponse.data.data.length
|
||||||
);
|
);
|
||||||
|
|
||||||
const run3 = await simpleChildTask.trigger(
|
configure({
|
||||||
{ message: "Hello, World again!" },
|
secretKey: process.env.TRIGGER_ACCESS_TOKEN,
|
||||||
{
|
});
|
||||||
idempotencyKey: "mmvlgwcidiklyeygen4",
|
|
||||||
|
for await (const run of runs.list("yubjwjsfkxnylobaqvqz", {
|
||||||
|
status: ["COMPLETED"],
|
||||||
|
period: "1y",
|
||||||
|
env: ["dev", "staging", "prod"],
|
||||||
|
})) {
|
||||||
|
console.log(run.env.name, run.isTest, run.id, run.status, run.createdAt);
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
console.log("run2", run2);
|
async function waitForRunToComplete(runId: string) {
|
||||||
console.log("run3", run3);
|
let run = await runs.retrieve(runId);
|
||||||
|
|
||||||
|
while (!run.isCompleted) {
|
||||||
|
console.log("run is not completed, waiting...", run);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
run = await runs.retrieve(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("run is completed", run);
|
||||||
|
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForRunToExecute(runId: string) {
|
||||||
|
let run = await runs.retrieve(runId);
|
||||||
|
|
||||||
|
while (!run.isExecuting) {
|
||||||
|
console.log("run is not executing, waiting...", run);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
run = await runs.retrieve(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("run is executing", run);
|
||||||
|
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSchedules() {
|
||||||
const allSchedules = await schedules.list();
|
const allSchedules = await schedules.list();
|
||||||
|
|
||||||
console.log("all schedules", allSchedules);
|
console.log("all schedules", allSchedules);
|
||||||
@@ -156,19 +232,37 @@ export async function run() {
|
|||||||
const deletedSchedule = await schedules.del(createdSchedule.id);
|
const deletedSchedule = await schedules.del(createdSchedule.id);
|
||||||
|
|
||||||
console.log("deleted schedule", deletedSchedule);
|
console.log("deleted schedule", deletedSchedule);
|
||||||
} catch (error) {
|
|
||||||
span.recordException(error as Error);
|
|
||||||
|
|
||||||
if (error instanceof APIError) {
|
|
||||||
console.error("APIError", error);
|
|
||||||
} else {
|
|
||||||
console.error("Unknown error", error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
span.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// run();
|
async function doScheduleLists() {
|
||||||
uploadEnvVars().catch(console.error);
|
let pageCount = 0;
|
||||||
|
|
||||||
|
let page = await schedules.list({
|
||||||
|
perPage: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`schedule page #${++pageCount}`);
|
||||||
|
|
||||||
|
// Convenience methods are provided for manually paginating:
|
||||||
|
while (page.hasNextPage()) {
|
||||||
|
page = await page.getNextPage();
|
||||||
|
console.log(`schedule page #${++pageCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (page.hasPreviousPage()) {
|
||||||
|
page = await page.getPreviousPage();
|
||||||
|
console.log(`schedule page #${--pageCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const schedule of schedules.list({
|
||||||
|
perPage: 2,
|
||||||
|
})) {
|
||||||
|
console.log(schedule.id, schedule.task, schedule.generator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRuns().catch(console.error);
|
||||||
|
doListRuns().catch(console.error);
|
||||||
|
// doScheduleLists().catch(console.error);
|
||||||
|
// doSchedules().catch(console.error);
|
||||||
|
// doEnvVars().catch(console.error);
|
||||||
|
|||||||
Reference in New Issue
Block a user