bee59de3a0
* Don't use the organization max concurrency anymore * Early draft of the concurrency page * WIP adding a new stepper input component * Move stepper to be alphabetical * When max value is reached, disabled the + button * Show placeholder if you delete all numbers * Make all the html input values available to the component * Adds size variants * Move stepper into its own component * Work on showing the extra concurrency * The purchase form styling and functionality (minus actually purchasing) * New style for outline input fields * Concurrency purchasing working * Purchasing concurrency and quota emails working * Improvements to the modal * Show cost breakdown in the modal * Fix for allocated concurrency including DEV * Improved types * Allocating concurrency is working * Live updates total env concurrency * Implemented reset * Fix for concurrency allocation editing across multiple projects * Tabular numbers * Added an error from allocating concurrency * Fixes for allocating concurrency where it didn't calculate correctly * "Increase limit" link to concurrency page * Indent environments * Added Preview limit when updating concurrency for an org * Show error when changing plan fails * Added maximumProjectCount column to Org * Limit project count and display a rich error toast (with title and button now) * Added title and button to toasts. Use it for new project error * @trigger.dev/platform 1.0.20 * Allow submitting zero concurrency so you can downgrade back to nothing * Use the server as the truth for omitted environments * Updated the pricing panels --------- Co-authored-by: James Ritchie <james@trigger.dev>
160 lines
3.9 KiB
TypeScript
160 lines
3.9 KiB
TypeScript
import { nanoid, customAlphabet } from "nanoid";
|
|
import slug from "slug";
|
|
import { prisma } from "~/db.server";
|
|
import type { Project } from "@trigger.dev/database";
|
|
import { Organization, createEnvironment } from "./organization.server";
|
|
import { env } from "~/env.server";
|
|
import { projectCreated } from "~/services/platform.v3.server";
|
|
export type { Project } from "@trigger.dev/database";
|
|
|
|
const externalRefGenerator = customAlphabet("abcdefghijklmnopqrstuvwxyz", 20);
|
|
|
|
type Options = {
|
|
organizationSlug: string;
|
|
name: string;
|
|
userId: string;
|
|
version: "v2" | "v3";
|
|
};
|
|
|
|
export class ExceededProjectLimitError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "ExceededProjectLimitError";
|
|
}
|
|
}
|
|
|
|
export async function createProject(
|
|
{ organizationSlug, name, userId, version }: 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,
|
|
v3Enabled: 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.v3Enabled) {
|
|
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,
|
|
},
|
|
attemptCount + 1
|
|
);
|
|
}
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name,
|
|
slug: uniqueProjectSlug,
|
|
organization: {
|
|
connect: {
|
|
slug: organizationSlug,
|
|
},
|
|
},
|
|
externalRef: `proj_${externalRefGenerator()}`,
|
|
version: version === "v3" ? "V3" : "V2",
|
|
},
|
|
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",
|
|
isBranchableEnvironment: false,
|
|
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 prisma.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 prisma.project.findFirst({
|
|
where: {
|
|
externalRef,
|
|
organization: {
|
|
members: { some: { userId } },
|
|
},
|
|
},
|
|
});
|
|
}
|