chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import type {
|
||||
Organization,
|
||||
OrgMember,
|
||||
Prisma,
|
||||
Project,
|
||||
RuntimeEnvironment,
|
||||
User,
|
||||
} from "@trigger.dev/database";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { generate } from "random-words";
|
||||
import slug from "slug";
|
||||
import { $replica, prisma, type PrismaClientOrTransaction } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { featuresForUrl } from "~/features.server";
|
||||
import { createApiKeyForEnv, createPkApiKeyForEnv, envSlug } from "./api-key.server";
|
||||
import { getDefaultEnvironmentConcurrencyLimit } from "~/services/platform.v3.server";
|
||||
import { enqueueAttioWorkspaceSync } from "~/services/attio.server";
|
||||
import {
|
||||
applyBillingLimitPauseAfterEnvCreate,
|
||||
getInitialEnvPauseStateForBillingLimit,
|
||||
} from "~/v3/services/billingLimit/getInitialEnvPauseStateForBillingLimit.server";
|
||||
export type { Organization };
|
||||
|
||||
const nanoid = customAlphabet("1234567890abcdef", 4);
|
||||
|
||||
/**
|
||||
* Resolve an organization id from its slug for use as an RBAC auth scope.
|
||||
* Reads the replica first (the common case) and falls back to the primary on a
|
||||
* miss, so replica lag never leaves a real org unresolved, which the dashboard
|
||||
* route builder treats as an unauthorized request.
|
||||
*/
|
||||
export async function resolveOrgIdFromSlug(slug: string): Promise<string | null> {
|
||||
const fromReplica = await $replica.organization.findFirst({
|
||||
where: { slug },
|
||||
select: { id: true },
|
||||
});
|
||||
if (fromReplica) {
|
||||
return fromReplica.id;
|
||||
}
|
||||
|
||||
const fromPrimary = await prisma.organization.findFirst({
|
||||
where: { slug },
|
||||
select: { id: true },
|
||||
});
|
||||
return fromPrimary?.id ?? null;
|
||||
}
|
||||
|
||||
export async function createOrganization(
|
||||
{
|
||||
title,
|
||||
userId,
|
||||
companySize,
|
||||
onboardingData,
|
||||
avatar,
|
||||
}: Pick<Organization, "title" | "companySize"> & {
|
||||
userId: User["id"];
|
||||
onboardingData?: Prisma.InputJsonValue;
|
||||
avatar?: Prisma.InputJsonValue;
|
||||
},
|
||||
attemptCount = 0
|
||||
): Promise<Organization> {
|
||||
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
|
||||
throw new Error("Organization could not be created.");
|
||||
}
|
||||
|
||||
const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`;
|
||||
|
||||
const orgWithSameSlug = await prisma.organization.findFirst({
|
||||
where: { slug: uniqueOrgSlug },
|
||||
});
|
||||
|
||||
if (attemptCount > 100) {
|
||||
throw new Error(`Unable to create organization with slug ${uniqueOrgSlug} after 100 attempts`);
|
||||
}
|
||||
|
||||
if (orgWithSameSlug) {
|
||||
return createOrganization(
|
||||
{
|
||||
title,
|
||||
userId,
|
||||
companySize,
|
||||
onboardingData,
|
||||
avatar,
|
||||
},
|
||||
attemptCount + 1
|
||||
);
|
||||
}
|
||||
|
||||
const features = featuresForUrl(new URL(env.APP_ORIGIN));
|
||||
|
||||
const organization = await prisma.organization.create({
|
||||
data: {
|
||||
title,
|
||||
slug: uniqueOrgSlug,
|
||||
companySize,
|
||||
onboardingData: onboardingData ?? undefined,
|
||||
avatar: avatar ?? undefined,
|
||||
maximumConcurrencyLimit: env.DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT,
|
||||
members: {
|
||||
create: {
|
||||
userId: userId,
|
||||
role: "ADMIN",
|
||||
},
|
||||
},
|
||||
// Managed-cloud orgs start deactivated so they're routed through
|
||||
// select-plan, which provisions their billing entitlement and activates
|
||||
// them. Self-hosters have no billing gate, so they're active immediately.
|
||||
isActivated: !features.isManagedCloud,
|
||||
},
|
||||
include: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Fire-and-forget; never blocks org creation.
|
||||
void enqueueAttioWorkspaceSync({
|
||||
orgId: organization.id,
|
||||
title: organization.title,
|
||||
slug: organization.slug,
|
||||
companySize: organization.companySize,
|
||||
createdAt: organization.createdAt,
|
||||
adminUserId: userId,
|
||||
});
|
||||
|
||||
return { ...organization };
|
||||
}
|
||||
|
||||
export async function createEnvironment({
|
||||
organization,
|
||||
project,
|
||||
type,
|
||||
isBranchableEnvironment = false,
|
||||
member,
|
||||
prismaClient = prisma,
|
||||
/** When set, skips billing lookup — caller must supply the limit for this org + type. */
|
||||
maximumConcurrencyLimit,
|
||||
}: {
|
||||
organization: Pick<Organization, "id" | "maximumConcurrencyLimit">;
|
||||
project: Pick<Project, "id">;
|
||||
type: RuntimeEnvironment["type"];
|
||||
isBranchableEnvironment?: boolean;
|
||||
member?: OrgMember;
|
||||
prismaClient?: PrismaClientOrTransaction;
|
||||
maximumConcurrencyLimit?: number;
|
||||
}) {
|
||||
const slug = envSlug(type);
|
||||
const apiKey = createApiKeyForEnv(type);
|
||||
const pkApiKey = createPkApiKeyForEnv(type);
|
||||
const shortcode = createShortcode().join("-");
|
||||
|
||||
const limit =
|
||||
maximumConcurrencyLimit ?? (await getDefaultEnvironmentConcurrencyLimit(organization.id, type));
|
||||
const billingPause = await getInitialEnvPauseStateForBillingLimit(organization.id, type);
|
||||
|
||||
const environment = await prismaClient.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug,
|
||||
apiKey,
|
||||
pkApiKey,
|
||||
shortcode,
|
||||
autoEnableInternalSources: type !== "DEVELOPMENT",
|
||||
maximumConcurrencyLimit: limit,
|
||||
paused: billingPause.paused,
|
||||
pauseSource: billingPause.pauseSource,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
connect: {
|
||||
id: project.id,
|
||||
},
|
||||
},
|
||||
orgMember: member ? { connect: { id: member.id } } : undefined,
|
||||
type,
|
||||
isBranchableEnvironment,
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
await applyBillingLimitPauseAfterEnvCreate(environment);
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
function createShortcode() {
|
||||
return generate({ exactly: 2 });
|
||||
}
|
||||
Reference in New Issue
Block a user