179 lines
5.4 KiB
TypeScript
179 lines
5.4 KiB
TypeScript
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
|
import {
|
|
ApiDeploymentListSearchParams,
|
|
InitializeDeploymentRequestBody,
|
|
type InitializeDeploymentResponseBody,
|
|
} from "@trigger.dev/core/v3";
|
|
import { $replica } from "~/db.server";
|
|
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
|
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
|
import { InitializeDeploymentService } from "~/v3/services/initializeDeployment.server";
|
|
|
|
export async function action({ request, params }: ActionFunctionArgs) {
|
|
// Ensure this is a POST request
|
|
if (request.method.toUpperCase() !== "POST") {
|
|
return { status: 405, body: "Method Not Allowed" };
|
|
}
|
|
|
|
// Next authenticate the request
|
|
const authenticationResult = await authenticateApiRequest(request);
|
|
|
|
if (!authenticationResult) {
|
|
logger.info("Invalid or missing api key", { url: request.url });
|
|
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
|
}
|
|
|
|
const rawBody = await request.json();
|
|
const body = InitializeDeploymentRequestBody.safeParse(rawBody);
|
|
|
|
if (!body.success) {
|
|
return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 });
|
|
}
|
|
|
|
const authenticatedEnv = authenticationResult.environment;
|
|
|
|
const service = new InitializeDeploymentService();
|
|
|
|
try {
|
|
const { deployment, imageRef, eventStream } = await service.call(authenticatedEnv, body.data);
|
|
|
|
const responseBody: InitializeDeploymentResponseBody = {
|
|
id: deployment.friendlyId,
|
|
contentHash: deployment.contentHash,
|
|
shortCode: deployment.shortCode,
|
|
version: deployment.version,
|
|
externalBuildData:
|
|
deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
|
|
imageTag: imageRef,
|
|
imagePlatform: deployment.imagePlatform,
|
|
eventStream,
|
|
};
|
|
|
|
return json(responseBody, { status: 200 });
|
|
} catch (error) {
|
|
if (error instanceof ServiceValidationError) {
|
|
return json({ error: error.message }, { status: 400 });
|
|
}
|
|
|
|
logger.error("Error initializing deployment", { error });
|
|
return json({ error: "Internal server error" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export const loader = createLoaderApiRoute(
|
|
{
|
|
searchParams: ApiDeploymentListSearchParams,
|
|
allowJWT: true,
|
|
corsStrategy: "none",
|
|
authorization: {
|
|
action: "read",
|
|
resource: () => ({ type: "deployments", id: "list" }),
|
|
},
|
|
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
|
|
},
|
|
async ({ searchParams, authentication }) => {
|
|
const limit = Math.max(Math.min(searchParams["page[size]"] ?? 20, 100), 5);
|
|
|
|
const afterDeployment = searchParams["page[after]"]
|
|
? await $replica.workerDeployment.findFirst({
|
|
where: {
|
|
friendlyId: searchParams["page[after]"],
|
|
environmentId: authentication.environment.id,
|
|
},
|
|
})
|
|
: undefined;
|
|
|
|
const deployments = await $replica.workerDeployment.findMany({
|
|
where: {
|
|
environmentId: authentication.environment.id,
|
|
...(afterDeployment ? { id: { lt: afterDeployment.id } } : {}),
|
|
...getCreatedAtFilter(searchParams),
|
|
...(searchParams.status ? { status: searchParams.status } : {}),
|
|
},
|
|
orderBy: {
|
|
id: "desc",
|
|
},
|
|
take: limit + 1,
|
|
});
|
|
|
|
const hasMore = deployments.length > limit;
|
|
const nextCursor = hasMore ? deployments[limit - 1].friendlyId : undefined;
|
|
const data = hasMore ? deployments.slice(0, limit) : deployments;
|
|
|
|
return json({
|
|
data: data.map((deployment) => ({
|
|
id: deployment.friendlyId,
|
|
createdAt: deployment.createdAt,
|
|
shortCode: deployment.shortCode,
|
|
version: deployment.version.toString(),
|
|
runtime: deployment.runtime,
|
|
runtimeVersion: deployment.runtimeVersion,
|
|
status: deployment.status,
|
|
deployedAt: deployment.deployedAt,
|
|
git: deployment.git,
|
|
error: deployment.errorData ?? undefined,
|
|
})),
|
|
pagination: {
|
|
next: nextCursor,
|
|
},
|
|
});
|
|
}
|
|
);
|
|
|
|
import parseDuration from "parse-duration";
|
|
import { parseDate } from "@trigger.dev/core/v3/isomorphic";
|
|
|
|
function getCreatedAtFilter(searchParams: ApiDeploymentListSearchParams) {
|
|
if (searchParams.period) {
|
|
const duration = parseDuration(searchParams.period, "ms");
|
|
|
|
if (!duration) {
|
|
throw new ServiceValidationError(
|
|
`Invalid search query parameter: period=${searchParams.period}`,
|
|
400
|
|
);
|
|
}
|
|
|
|
return {
|
|
createdAt: {
|
|
gte: new Date(Date.now() - duration),
|
|
lte: new Date(),
|
|
},
|
|
};
|
|
}
|
|
|
|
if (searchParams.from && searchParams.to) {
|
|
const fromDate = safeDateFromString(searchParams.from, "from");
|
|
const toDate = safeDateFromString(searchParams.to, "to");
|
|
|
|
return {
|
|
createdAt: {
|
|
gte: fromDate,
|
|
lte: toDate,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (searchParams.from) {
|
|
const fromDate = safeDateFromString(searchParams.from, "from");
|
|
return {
|
|
createdAt: {
|
|
gte: fromDate,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
function safeDateFromString(value: string, paramName: string) {
|
|
const date = parseDate(value);
|
|
|
|
if (!date) {
|
|
throw new ServiceValidationError(`Invalid search query parameter: ${paramName}=${value}`, 400);
|
|
}
|
|
return date;
|
|
}
|