Skip to content

v3: env var management API #1116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/five-toes-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"trigger.dev": patch
"@trigger.dev/core": patch
---

v3: Environment variable management API and SDK, along with resolveEnvVars CLI hook
8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@
"cwd": "${workspaceFolder}/references/v3-catalog",
"sourceMaps": true
},
{
"type": "node-terminal",
"request": "launch",
"name": "Debug V3 Management",
"command": "pnpm run management",
"cwd": "${workspaceFolder}/references/v3-catalog",
"sourceMaps": true
},
{
"type": "node",
"request": "attach",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class EnvironmentVariablesPresenter {
);

const repository = new EnvironmentVariablesRepository(this.#prismaClient);
const variables = await repository.getProject(project.id, userId);
const variables = await repository.getProject(project.id);

return {
environmentVariables: environmentVariables.map((environmentVariable) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const Variable = z.object({
type Variable = z.infer<typeof Variable>;

const schema = z.object({
overwrite: z.preprocess((i) => {
override: z.preprocess((i) => {
if (i === "true") return true;
if (i === "false") return false;
return;
Expand Down Expand Up @@ -115,6 +115,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
const project = await prisma.project.findUnique({
where: {
slug: params.projectParam,
organization: {
members: {
some: {
userId,
},
},
},
},
select: {
id: true,
Expand All @@ -126,7 +133,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
}

const repository = new EnvironmentVariablesRepository(prisma);
const result = await repository.create(project.id, userId, submission.value);
const result = await repository.create(project.id, submission.value);

if (!result.success) {
if (result.variableErrors) {
Expand Down Expand Up @@ -249,18 +256,18 @@ export default function Page() {
type="submit"
variant="primary/small"
disabled={isLoading}
name="overwrite"
name="override"
value="false"
>
{isLoading ? "Saving" : "Save"}
</Button>
<Button
variant="secondary/small"
disabled={isLoading}
name="overwrite"
name="override"
value="true"
>
{isLoading ? "Overwriting" : "Overwrite"}
{isLoading ? "Overriding" : "Override"}
</Button>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
const project = await prisma.project.findUnique({
where: {
slug: params.projectParam,
organization: {
members: {
some: {
userId,
},
},
},
},
select: {
id: true,
Expand All @@ -119,7 +126,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
switch (submission.value.action) {
case "edit": {
const repository = new EnvironmentVariablesRepository(prisma);
const result = await repository.edit(project.id, userId, submission.value);
const result = await repository.edit(project.id, submission.value);

if (!result.success) {
submission.error.key = result.error;
Expand All @@ -138,7 +145,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
}
case "delete": {
const repository = new EnvironmentVariablesRepository(prisma);
const result = await repository.delete(project.id, userId, submission.value);
const result = await repository.delete(project.id, submission.value);

if (!result.success) {
submission.error.key = result.error;
Expand Down Expand Up @@ -334,6 +341,7 @@ function EditEnvironmentVariablePanel({
name={`values[${index}].value`}
placeholder="Not set"
defaultValue={value}
type="password"
/>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { UpdateEnvironmentVariableRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import {
authenticateProjectApiKeyOrPersonalAccessToken,
authenticatedEnvironmentForAuthentication,
} from "~/services/apiAuth.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";

const ParamsSchema = z.object({
projectRef: z.string(),
slug: z.string(),
name: z.string(),
});

export async function action({ params, request }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);

if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}

const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}

const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);

// Find the environment variable
const variable = await prisma.environmentVariable.findFirst({
where: {
key: parsedParams.data.name,
projectId: environment.project.id,
},
});

if (!variable) {
return json({ error: "Environment variable not found" }, { status: 404 });
}

const repository = new EnvironmentVariablesRepository();

switch (request.method.toUpperCase()) {
case "DELETE": {
const result = await repository.deleteValue(environment.project.id, {
id: variable.id,
environmentId: environment.id,
});

if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error }, { status: 400 });
}
}
case "PUT":
case "POST": {
const jsonBody = await request.json();

const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody);

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

const result = await repository.edit(environment.project.id, {
values: [
{
value: body.data.value,
environmentId: environment.id,
},
],
id: variable.id,
keepEmptyValues: true,
});

if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error }, { status: 400 });
}
}
}
}

export async function loader({ params, request }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);

if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}

const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}

const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);

// Find the environment variable
const variable = await prisma.environmentVariable.findFirst({
where: {
key: parsedParams.data.name,
projectId: environment.project.id,
},
});

if (!variable) {
return json({ error: "Environment variable not found" }, { status: 404 });
}

const repository = new EnvironmentVariablesRepository();

const variables = await repository.getEnvironment(environment.project.id, environment.id, true);

const environmentVariable = variables.find((v) => v.key === parsedParams.data.name);

if (!environmentVariable) {
return json({ error: "Environment variable not found" }, { status: 404 });
}

return json({
value: environmentVariable.value,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { ImportEnvironmentVariablesRequestBody } from "@trigger.dev/core/v3";
import { parse } from "dotenv";
import { z } from "zod";
import {
authenticateProjectApiKeyOrPersonalAccessToken,
authenticatedEnvironmentForAuthentication,
} from "~/services/apiAuth.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";

const ParamsSchema = z.object({
projectRef: z.string(),
slug: z.string(),
});

export async function action({ params, request }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);

if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}

const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}

const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);

const repository = new EnvironmentVariablesRepository();

const body = await parseImportBody(request);

const result = await repository.create(environment.project.id, {
override: typeof body.override === "boolean" ? body.override : false,
environmentIds: [environment.id],
variables: Object.entries(body.variables).map(([key, value]) => ({
key,
value,
})),
});

if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error, variableErrors: result.variableErrors }, { status: 400 });
}
}

async function parseImportBody(request: Request): Promise<ImportEnvironmentVariablesRequestBody> {
const contentType = request.headers.get("content-type") ?? "application/json";

if (contentType.includes("multipart/form-data")) {
const formData = await request.formData();

const file = formData.get("variables");
const override = formData.get("override") === "true";

if (file instanceof File) {
const buffer = await file.arrayBuffer();

const variables = parse(Buffer.from(buffer));

return { variables, override };
} else {
throw json({ error: "Invalid file" }, { status: 400 });
}
} else {
const rawBody = await request.json();

const body = ImportEnvironmentVariablesRequestBody.safeParse(rawBody);

if (!body.success) {
throw json({ error: "Invalid body" }, { status: 400 });
}

return body.data;
}
}
Loading
Loading