Skip to content

Commit 3a1b0c4

Browse files
authored
v3: env var management API (#1116)
* WIP env var management API * Add import env var API endpoint * Adding docs and support for using both API keys and PATs when interacting with the env var endpoints * WIP envvar SDK * Uploading env vars in a variety of formats now works * Finish env var endpoints and add resolveEnvVars hook * Add changeset
1 parent 1f462ea commit 3a1b0c4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3271
-1090
lines changed

.changeset/five-toes-destroy.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"trigger.dev": patch
4+
"@trigger.dev/core": patch
5+
---
6+
7+
v3: Environment variable management API and SDK, along with resolveEnvVars CLI hook

.vscode/launch.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
"cwd": "${workspaceFolder}/references/v3-catalog",
4646
"sourceMaps": true
4747
},
48+
{
49+
"type": "node-terminal",
50+
"request": "launch",
51+
"name": "Debug V3 Management",
52+
"command": "pnpm run management",
53+
"cwd": "${workspaceFolder}/references/v3-catalog",
54+
"sourceMaps": true
55+
},
4856
{
4957
"type": "node",
5058
"request": "attach",

apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export class EnvironmentVariablesPresenter {
8787
);
8888

8989
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
90-
const variables = await repository.getProject(project.id, userId);
90+
const variables = await repository.getProject(project.id);
9191

9292
return {
9393
environmentVariables: environmentVariables.map((environmentVariable) => {

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.environment-variables.new/route.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const Variable = z.object({
7070
type Variable = z.infer<typeof Variable>;
7171

7272
const schema = z.object({
73-
overwrite: z.preprocess((i) => {
73+
override: z.preprocess((i) => {
7474
if (i === "true") return true;
7575
if (i === "false") return false;
7676
return;
@@ -115,6 +115,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
115115
const project = await prisma.project.findUnique({
116116
where: {
117117
slug: params.projectParam,
118+
organization: {
119+
members: {
120+
some: {
121+
userId,
122+
},
123+
},
124+
},
118125
},
119126
select: {
120127
id: true,
@@ -126,7 +133,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
126133
}
127134

128135
const repository = new EnvironmentVariablesRepository(prisma);
129-
const result = await repository.create(project.id, userId, submission.value);
136+
const result = await repository.create(project.id, submission.value);
130137

131138
if (!result.success) {
132139
if (result.variableErrors) {
@@ -249,18 +256,18 @@ export default function Page() {
249256
type="submit"
250257
variant="primary/small"
251258
disabled={isLoading}
252-
name="overwrite"
259+
name="override"
253260
value="false"
254261
>
255262
{isLoading ? "Saving" : "Save"}
256263
</Button>
257264
<Button
258265
variant="secondary/small"
259266
disabled={isLoading}
260-
name="overwrite"
267+
name="override"
261268
value="true"
262269
>
263-
{isLoading ? "Overwriting" : "Overwrite"}
270+
{isLoading ? "Overriding" : "Override"}
264271
</Button>
265272
</div>
266273
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.environment-variables/route.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
106106
const project = await prisma.project.findUnique({
107107
where: {
108108
slug: params.projectParam,
109+
organization: {
110+
members: {
111+
some: {
112+
userId,
113+
},
114+
},
115+
},
109116
},
110117
select: {
111118
id: true,
@@ -119,7 +126,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
119126
switch (submission.value.action) {
120127
case "edit": {
121128
const repository = new EnvironmentVariablesRepository(prisma);
122-
const result = await repository.edit(project.id, userId, submission.value);
129+
const result = await repository.edit(project.id, submission.value);
123130

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

143150
if (!result.success) {
144151
submission.error.key = result.error;
@@ -334,6 +341,7 @@ function EditEnvironmentVariablePanel({
334341
name={`values[${index}].value`}
335342
placeholder="Not set"
336343
defaultValue={value}
344+
type="password"
337345
/>
338346
</Fragment>
339347
);
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { UpdateEnvironmentVariableRequestBody } from "@trigger.dev/core/v3";
3+
import { z } from "zod";
4+
import { prisma } from "~/db.server";
5+
import {
6+
authenticateProjectApiKeyOrPersonalAccessToken,
7+
authenticatedEnvironmentForAuthentication,
8+
} from "~/services/apiAuth.server";
9+
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
10+
11+
const ParamsSchema = z.object({
12+
projectRef: z.string(),
13+
slug: z.string(),
14+
name: z.string(),
15+
});
16+
17+
export async function action({ params, request }: ActionFunctionArgs) {
18+
const parsedParams = ParamsSchema.safeParse(params);
19+
20+
if (!parsedParams.success) {
21+
return json({ error: "Invalid params" }, { status: 400 });
22+
}
23+
24+
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
25+
26+
if (!authenticationResult) {
27+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
28+
}
29+
30+
const environment = await authenticatedEnvironmentForAuthentication(
31+
authenticationResult,
32+
parsedParams.data.projectRef,
33+
parsedParams.data.slug
34+
);
35+
36+
// Find the environment variable
37+
const variable = await prisma.environmentVariable.findFirst({
38+
where: {
39+
key: parsedParams.data.name,
40+
projectId: environment.project.id,
41+
},
42+
});
43+
44+
if (!variable) {
45+
return json({ error: "Environment variable not found" }, { status: 404 });
46+
}
47+
48+
const repository = new EnvironmentVariablesRepository();
49+
50+
switch (request.method.toUpperCase()) {
51+
case "DELETE": {
52+
const result = await repository.deleteValue(environment.project.id, {
53+
id: variable.id,
54+
environmentId: environment.id,
55+
});
56+
57+
if (result.success) {
58+
return json({ success: true });
59+
} else {
60+
return json({ error: result.error }, { status: 400 });
61+
}
62+
}
63+
case "PUT":
64+
case "POST": {
65+
const jsonBody = await request.json();
66+
67+
const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody);
68+
69+
if (!body.success) {
70+
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
71+
}
72+
73+
const result = await repository.edit(environment.project.id, {
74+
values: [
75+
{
76+
value: body.data.value,
77+
environmentId: environment.id,
78+
},
79+
],
80+
id: variable.id,
81+
keepEmptyValues: true,
82+
});
83+
84+
if (result.success) {
85+
return json({ success: true });
86+
} else {
87+
return json({ error: result.error }, { status: 400 });
88+
}
89+
}
90+
}
91+
}
92+
93+
export async function loader({ params, request }: LoaderFunctionArgs) {
94+
const parsedParams = ParamsSchema.safeParse(params);
95+
96+
if (!parsedParams.success) {
97+
return json({ error: "Invalid params" }, { status: 400 });
98+
}
99+
100+
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
101+
102+
if (!authenticationResult) {
103+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
104+
}
105+
106+
const environment = await authenticatedEnvironmentForAuthentication(
107+
authenticationResult,
108+
parsedParams.data.projectRef,
109+
parsedParams.data.slug
110+
);
111+
112+
// Find the environment variable
113+
const variable = await prisma.environmentVariable.findFirst({
114+
where: {
115+
key: parsedParams.data.name,
116+
projectId: environment.project.id,
117+
},
118+
});
119+
120+
if (!variable) {
121+
return json({ error: "Environment variable not found" }, { status: 404 });
122+
}
123+
124+
const repository = new EnvironmentVariablesRepository();
125+
126+
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
127+
128+
const environmentVariable = variables.find((v) => v.key === parsedParams.data.name);
129+
130+
if (!environmentVariable) {
131+
return json({ error: "Environment variable not found" }, { status: 404 });
132+
}
133+
134+
return json({
135+
value: environmentVariable.value,
136+
});
137+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { ImportEnvironmentVariablesRequestBody } from "@trigger.dev/core/v3";
3+
import { parse } from "dotenv";
4+
import { z } from "zod";
5+
import {
6+
authenticateProjectApiKeyOrPersonalAccessToken,
7+
authenticatedEnvironmentForAuthentication,
8+
} from "~/services/apiAuth.server";
9+
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
10+
11+
const ParamsSchema = z.object({
12+
projectRef: z.string(),
13+
slug: z.string(),
14+
});
15+
16+
export async function action({ params, request }: ActionFunctionArgs) {
17+
const parsedParams = ParamsSchema.safeParse(params);
18+
19+
if (!parsedParams.success) {
20+
return json({ error: "Invalid params" }, { status: 400 });
21+
}
22+
23+
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
24+
25+
if (!authenticationResult) {
26+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
27+
}
28+
29+
const environment = await authenticatedEnvironmentForAuthentication(
30+
authenticationResult,
31+
parsedParams.data.projectRef,
32+
parsedParams.data.slug
33+
);
34+
35+
const repository = new EnvironmentVariablesRepository();
36+
37+
const body = await parseImportBody(request);
38+
39+
const result = await repository.create(environment.project.id, {
40+
override: typeof body.override === "boolean" ? body.override : false,
41+
environmentIds: [environment.id],
42+
variables: Object.entries(body.variables).map(([key, value]) => ({
43+
key,
44+
value,
45+
})),
46+
});
47+
48+
if (result.success) {
49+
return json({ success: true });
50+
} else {
51+
return json({ error: result.error, variableErrors: result.variableErrors }, { status: 400 });
52+
}
53+
}
54+
55+
async function parseImportBody(request: Request): Promise<ImportEnvironmentVariablesRequestBody> {
56+
const contentType = request.headers.get("content-type") ?? "application/json";
57+
58+
if (contentType.includes("multipart/form-data")) {
59+
const formData = await request.formData();
60+
61+
const file = formData.get("variables");
62+
const override = formData.get("override") === "true";
63+
64+
if (file instanceof File) {
65+
const buffer = await file.arrayBuffer();
66+
67+
const variables = parse(Buffer.from(buffer));
68+
69+
return { variables, override };
70+
} else {
71+
throw json({ error: "Invalid file" }, { status: 400 });
72+
}
73+
} else {
74+
const rawBody = await request.json();
75+
76+
const body = ImportEnvironmentVariablesRequestBody.safeParse(rawBody);
77+
78+
if (!body.success) {
79+
throw json({ error: "Invalid body" }, { status: 400 });
80+
}
81+
82+
return body.data;
83+
}
84+
}

0 commit comments

Comments
 (0)