Files
triggerdotdev--trigger.dev/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts
T
nicktrn 52c9d485f8 v3: cli build command, prod runs, checkpoints (#919)
* fix trailing slash in api url

* add maybe platform down error

* shorten config path

* extend protobuf compiler install instructions

* build command and image model

* new image triggers task indexing

* index support for prod builds

* shorten example provider secret

* multi-stage task prod build

* lock prod tasks to node 18 image

* extract shared build and dev command libs

* pull out more shared deps

* add coordinator and providers

* fix core-apps build

* add dev builds for new apps

* fix cwd

* enable corepack

* some build fixes

* enable buildkit for old docker versions

* coordinator image fixes

* update provider containerfile

* build dev images in parallel

* upgrade pgadmin

* fix prod facade build

* prod runs

* fix merge

* don't knock out platform on invalid attempt id

* fix prod facade

* rename to build.ts

* fix prod builds

* set to executing after fetching payload

* make prod worker listen on random port if in use

* remove experimental warnings in dev

* prod resume

* prevent execution after completion

* exit prod worker after completion

* always restart otel collector

* docker checkpoints and prod runtime messaging

* don't retry indexing without chance of success

* make platform checkpoint aware

* deploy with existing hash sets latest worker

* log restore requests

* only try to checkpoint long waits

* tidying up

* lockfile

* fix build

* prod worker merge fixes

* fix prod complete and cancel

* fix lua nil checks

* socket namespace abstraction

* cleanup

* make all build args optional

* add build script

* don't require env vars for dev

* fix schema

* prod merge

* small fix

* bind correct logger

* fix v3 ref catalog entry

* resume prod batch

* pass socket to error and disconnect handlers

* fix non-batch resume

* fix batch resume

* send connection env vars when not in dev

* create worker via socket

* move api client back into v3 cli

* fix lockfile

* fix resume with failures

* marqs replace message

* typecheck prior to build

* don't define api url in prod builds

* support prod retries after resume

* skip typecheck option
2024-03-04 13:10:07 +00:00

552 lines
14 KiB
TypeScript

import { Prisma, PrismaClient } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, prisma } from "~/db.server";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { EnvironmentVariable, ProjectEnvironmentVariable, Repository, Result } from "./repository";
import { env } from "~/env.server";
function secretKeyProjectPrefix(projectId: string) {
return `environmentvariable:${projectId}:`;
}
function secretKeyEnvironmentPrefix(projectId: string, environmentId: string) {
return `${secretKeyProjectPrefix(projectId)}${environmentId}:`;
}
function secretKey(projectId: string, environmentId: string, key: string) {
return `${secretKeyEnvironmentPrefix(projectId, environmentId)}${key}`;
}
function parseSecretKey(key: string) {
const parts = key.split(":");
return {
projectId: parts[1],
environmentId: parts[2],
key: parts[3],
};
}
const SecretValue = z.object({ secret: z.string() });
export class EnvironmentVariablesRepository implements Repository {
constructor(private prismaClient: PrismaClient = prisma) {}
async create(
projectId: string,
userId: string,
options: { key: string; values: { value: string; environmentId: string }[] }
): Promise<Result> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
if (options.values.every((v) => !project.environments.some((e) => e.id === v.environmentId))) {
return { success: false as const, error: `Environment not found` };
}
//get rid of empty strings
const values = options.values.filter((v) => v.value.trim() !== "");
if (values.length === 0) {
return { success: false as const, error: `You must set at least one value` };
}
try {
const result = await $transaction(this.prismaClient, async (tx) => {
const environmentVariable = await tx.environmentVariable.create({
data: {
key: options.key,
friendlyId: generateFriendlyId("envvar"),
project: {
connect: {
id: projectId,
},
},
},
});
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
//create the secret values and references
for (const value of values) {
const key = secretKey(projectId, value.environmentId, options.key);
//create the secret reference
const secretReference = await tx.secretReference.create({
data: {
key,
provider: "DATABASE",
},
});
const variableValue = await tx.environmentVariableValue.create({
data: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
valueReferenceId: secretReference.id,
},
});
await secretStore.setSecret<{ secret: string }>(key, {
secret: value.value,
});
}
});
return {
success: true as const,
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// The error code for unique constraint violation in Prisma is P2002
if (error.code === "P2002") {
return {
success: false as const,
error: `There's already an environment variable called ${options.key}.`,
};
}
}
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
async edit(
projectId: string,
userId: string,
options: { values: { value: string; environmentId: string }[]; id: string }
): Promise<Result> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
where: {
OR: [
{
orgMember: null,
},
{
orgMember: {
userId,
},
},
],
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
if (options.values.every((v) => !project.environments.some((e) => e.id === v.environmentId))) {
return { success: false as const, error: `Environment not found` };
}
//get rid of empty strings
let values = options.values.filter((v) => v.value.trim() !== "");
//add in empty values for environments that don't have a value
const environmentIds = project.environments.map((e) => e.id);
for (const environmentId of environmentIds) {
if (!values.some((v) => v.environmentId === environmentId)) {
values.push({
environmentId,
value: "",
});
}
}
const environmentVariable = await this.prismaClient.environmentVariable.findUnique({
select: {
id: true,
key: true,
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
try {
await $transaction(this.prismaClient, async (tx) => {
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
//create the secret values and references
for (const value of values) {
const key = secretKey(projectId, value.environmentId, environmentVariable.key);
const existingValue = await tx.environmentVariableValue.findUnique({
where: {
variableId_environmentId: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
},
},
});
if (existingValue && existingValue.valueReferenceId) {
if (value.value === "") {
//delete the value
await secretStore.deleteSecret(key);
await tx.secretReference.delete({
where: {
id: existingValue.valueReferenceId,
},
});
await tx.environmentVariableValue.delete({
where: {
variableId_environmentId: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
},
},
});
} else {
await secretStore.setSecret<{ secret: string }>(key, {
secret: value.value,
});
}
continue;
}
//create the secret reference
const secretReference = await tx.secretReference.create({
data: {
key,
provider: "DATABASE",
},
});
const variableValue = await tx.environmentVariableValue.create({
data: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
valueReferenceId: secretReference.id,
},
});
await secretStore.setSecret<{ secret: string }>(key, {
secret: value.value,
});
}
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
async getProject(projectId: string, userId: string): Promise<ProjectEnvironmentVariable[]> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
type: true,
},
},
},
});
if (!project) {
return [];
}
const secretStore = getSecretStore("DATABASE", {
prismaClient: this.prismaClient,
});
const secrets = await secretStore.getSecrets(SecretValue, secretKeyProjectPrefix(projectId));
const values = secrets.map((secret) => {
const { projectId, environmentId, key } = parseSecretKey(secret.key);
return {
projectId,
environmentId,
key,
value: secret.value.secret,
};
});
//now group the values together by key and environment ID into ProjectEnvironmentVariable[]
//and add the type of environment to the result
const results: ProjectEnvironmentVariable[] = [];
for (const value of values) {
const environment = project.environments.find((e) => e.id === value.environmentId);
if (!environment) {
throw new Error("Environment not found");
}
const existing = results.find((r) => r.key === value.key);
if (existing) {
existing.values.push({
value: value.value,
environment: {
id: value.environmentId,
type: environment.type,
},
});
} else {
results.push({
key: value.key,
values: [
{
value: value.value,
environment: {
id: value.environmentId,
type: environment.type,
},
},
],
});
}
}
return results;
}
async getEnvironment(
projectId: string,
userId: string,
environmentId: string
): Promise<EnvironmentVariable[]> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
where: {
id: environmentId,
},
},
},
});
if (!project || project.environments.length === 0) {
return [];
}
return this.getEnvironmentVariables(projectId, environmentId);
}
async #getTriggerEnvironmentVariables(environmentId: string): Promise<EnvironmentVariable[]> {
const environment = await this.prismaClient.runtimeEnvironment.findFirst({
where: {
id: environmentId,
},
});
if (!environment) {
return [];
}
if (environment.type === "DEVELOPMENT") {
return [];
}
return [
{
key: "TRIGGER_API_KEY",
value: environment.apiKey,
},
{
key: "TRIGGER_API_URL",
value: env.APP_ORIGIN,
},
];
}
async #getSecretEnvironmentVariables(
projectId: string,
environmentId: string
): Promise<EnvironmentVariable[]> {
const secretStore = getSecretStore("DATABASE", {
prismaClient: this.prismaClient,
});
const secrets = await secretStore.getSecrets(
SecretValue,
secretKeyEnvironmentPrefix(projectId, environmentId)
);
return secrets.map((secret) => {
const { key } = parseSecretKey(secret.key);
return {
key,
value: secret.value.secret,
};
});
}
async getEnvironmentVariables(
projectId: string,
environmentId: string
): Promise<EnvironmentVariable[]> {
const secretEnvVars = await this.#getSecretEnvironmentVariables(projectId, environmentId);
const triggerEnvVars = await this.#getTriggerEnvironmentVariables(environmentId);
return [...secretEnvVars, ...triggerEnvVars];
}
async delete(projectId: string, userId: string, options: { id: string }): Promise<Result> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
where: {
OR: [
{
orgMember: null,
},
{
orgMember: {
userId,
},
},
],
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
const environmentVariable = await this.prismaClient.environmentVariable.findUnique({
select: {
id: true,
key: true,
values: {
select: {
id: true,
environmentId: true,
},
},
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
try {
await $transaction(this.prismaClient, async (tx) => {
await tx.environmentVariable.delete({
where: {
id: options.id,
},
});
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
//create the secret values and references
for (const value of environmentVariable.values) {
const key = secretKey(projectId, value.environmentId, environmentVariable.key);
await secretStore.deleteSecret(key);
}
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
}