fd4f02b2f8
## What & why Two related fixes to how new cloud organizations get onboarded onto the Free plan. ### 1. Route new cloud orgs through plan selection New cloud organizations were created already activated, so they skipped the plan-selection step and went straight to creating projects — which meant their plan and usage limits were never set up. They're now created deactivated and routed through plan selection, which activates them once a plan is chosen. Self-hosted installs have no plan-selection step, so they're activated immediately on creation and are unaffected. The `Organization.v3Enabled` field is renamed to `isActivated` to better describe what it now gates. It's mapped to the existing `v3Enabled` column, so there's no data migration — only a schema/code rename. ### 2. Allow selecting the Free plan without GitHub verification Choosing the Free plan no longer requires connecting and verifying a GitHub account. The plan is applied immediately when selected. This removes: - the "Connect to GitHub" dialog and the GitHub-verified badge from the plan picker - the account-rejected state - the now-unreachable GitHub-connect return routes ## Notes - These changes pair with the corresponding change in the billing service that applies the Free plan directly; they should be released together. ## Testing Verified locally end to end: a new cloud org is routed to plan selection, the Free plan applies in one click with no GitHub step, the org is activated, its usage allowance is provisioned, and it lands on the new-project page. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
164 lines
4.1 KiB
TypeScript
164 lines
4.1 KiB
TypeScript
import type { Prisma, Project } from "@trigger.dev/database";
|
|
import { customAlphabet, nanoid } from "nanoid";
|
|
import slug from "slug";
|
|
import { $replica, prisma } from "~/db.server";
|
|
import { projectCreated } from "~/services/projectCreated.server";
|
|
import { type Organization, createEnvironment } from "./organization.server";
|
|
export type { Project } from "@trigger.dev/database";
|
|
|
|
const externalRefGenerator = customAlphabet("abcdefghijklmnopqrstuvwxyz", 20);
|
|
|
|
type Options = {
|
|
organizationSlug: string;
|
|
name: string;
|
|
userId: string;
|
|
version: "v2" | "v3";
|
|
onboardingData?: Prisma.InputJsonValue;
|
|
};
|
|
|
|
export class ExceededProjectLimitError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "ExceededProjectLimitError";
|
|
}
|
|
}
|
|
|
|
export async function createProject(
|
|
{ organizationSlug, name, userId, version, onboardingData }: Options,
|
|
attemptCount = 0
|
|
): Promise<Project & { organization: Organization }> {
|
|
//check the user has permissions to do this
|
|
const organization = await prisma.organization.findFirst({
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
isActivated: true,
|
|
maximumConcurrencyLimit: true,
|
|
maximumProjectCount: true,
|
|
},
|
|
where: {
|
|
slug: organizationSlug,
|
|
members: { some: { userId } },
|
|
},
|
|
});
|
|
|
|
if (!organization) {
|
|
throw new Error(
|
|
`User ${userId} does not have permission to create a project in organization ${organizationSlug}`
|
|
);
|
|
}
|
|
|
|
if (version === "v3") {
|
|
if (!organization.isActivated) {
|
|
throw new Error(`Organization can't create v3 projects.`);
|
|
}
|
|
}
|
|
|
|
const projectCount = await prisma.project.count({
|
|
where: {
|
|
organizationId: organization.id,
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
|
|
if (projectCount >= organization.maximumProjectCount) {
|
|
throw new ExceededProjectLimitError(
|
|
`This organization has reached the maximum number of projects (${organization.maximumProjectCount}).`
|
|
);
|
|
}
|
|
|
|
//ensure the slug is globally unique
|
|
const uniqueProjectSlug = `${slug(name)}-${nanoid(4)}`;
|
|
const projectWithSameSlug = await prisma.project.findFirst({
|
|
where: { slug: uniqueProjectSlug },
|
|
});
|
|
|
|
if (attemptCount > 100) {
|
|
throw new Error(`Unable to create project with slug ${uniqueProjectSlug} after 100 attempts`);
|
|
}
|
|
|
|
if (projectWithSameSlug) {
|
|
return createProject(
|
|
{
|
|
organizationSlug,
|
|
name,
|
|
userId,
|
|
version,
|
|
onboardingData,
|
|
},
|
|
attemptCount + 1
|
|
);
|
|
}
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name,
|
|
slug: uniqueProjectSlug,
|
|
organization: {
|
|
connect: {
|
|
slug: organizationSlug,
|
|
},
|
|
},
|
|
externalRef: `proj_${externalRefGenerator()}`,
|
|
version: version === "v3" ? "V3" : "V2",
|
|
onboardingData,
|
|
},
|
|
include: {
|
|
organization: {
|
|
include: {
|
|
members: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create the dev and prod environments
|
|
await createEnvironment({
|
|
organization,
|
|
project,
|
|
type: "PRODUCTION",
|
|
isBranchableEnvironment: false,
|
|
});
|
|
|
|
for (const member of project.organization.members) {
|
|
await createEnvironment({
|
|
organization,
|
|
project,
|
|
type: "DEVELOPMENT",
|
|
// We set this true but no backfill (yet!?) so never used
|
|
// for dev environments
|
|
isBranchableEnvironment: true,
|
|
member,
|
|
});
|
|
}
|
|
|
|
await projectCreated(organization, project);
|
|
|
|
return project;
|
|
}
|
|
|
|
export async function findProjectBySlug(orgSlug: string, projectSlug: string, userId: string) {
|
|
// Find the project scoped to the organization, making sure the user belongs to that org
|
|
return await $replica.project.findFirst({
|
|
where: {
|
|
slug: projectSlug,
|
|
organization: {
|
|
slug: orgSlug,
|
|
members: { some: { userId } },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function findProjectByRef(externalRef: string, userId: string) {
|
|
// Find the project scoped to the organization, making sure the user belongs to that org
|
|
return await $replica.project.findFirst({
|
|
where: {
|
|
externalRef,
|
|
organization: {
|
|
members: { some: { userId } },
|
|
},
|
|
},
|
|
});
|
|
}
|