diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index 8bfc912af..429faa844 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -64,37 +64,35 @@ export async function createOrganization( { title, userId, - desiredSlug, projectName, }: Pick & { userId: User["id"]; - desiredSlug?: string; projectName: string; }, attemptCount = 0 ): Promise { - if (desiredSlug === undefined) { - desiredSlug = slug(title); - } + const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`; + const uniqueProjectSlug = `${slug(projectName)}-${nanoid(4)}`; - const uniqueSlug = `${desiredSlug}-${nanoid(4)}`; + const orgWithSameSlug = await prisma.organization.findFirst({ + where: { slug: uniqueOrgSlug }, + }); - const withSameSlug = await prisma.organization.findFirst({ - where: { slug: uniqueSlug }, + const projectWithSameSlug = await prisma.project.findFirst({ + where: { slug: uniqueProjectSlug }, }); if (attemptCount > 100) { throw new Error( - `Unable to create organization with slug ${uniqueSlug} after 100 attempts` + `Unable to create organization with slug ${uniqueOrgSlug} after 100 attempts` ); } - if (withSameSlug) { + if (orgWithSameSlug || projectWithSameSlug) { return createOrganization( { title, userId, - desiredSlug, projectName, }, attemptCount + 1 @@ -104,7 +102,7 @@ export async function createOrganization( const organization = await prisma.organization.create({ data: { title, - slug: uniqueSlug, + slug: uniqueOrgSlug, members: { create: { userId: userId, @@ -114,6 +112,7 @@ export async function createOrganization( projects: { create: { name: projectName, + slug: uniqueProjectSlug, }, }, }, diff --git a/apps/webapp/app/presenters/jobList.server.ts b/apps/webapp/app/presenters/jobList.server.ts new file mode 100644 index 000000000..e349e0a57 --- /dev/null +++ b/apps/webapp/app/presenters/jobList.server.ts @@ -0,0 +1,61 @@ +import { prisma } from "~/db.server"; + +type JobListOptions = { + userId: string; + projectSlug: string; + environmentId: string; +}; + +export async function jobList({ userId, projectSlug }: JobListOptions) { + const jobs = await prisma.job.findMany({ + select: { + id: true, + slug: true, + title: true, + projectId: true, + aliases: { + select: { + version: { + select: { + version: true, + eventSpecification: true, + runs: { + select: { + createdAt: true, + status: true, + }, + take: 1, + orderBy: [{ createdAt: "desc" }], + }, + integrations: { + select: { + metadata: true, + }, + }, + }, + }, + environment: { + select: { + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + where: { + name: "latest", + }, + }, + }, + where: { + organization: { members: { some: { userId } } }, + project: { slug: projectSlug }, + internal: false, + }, + orderBy: [{ title: "asc" }], + }); + + return jobs; +} diff --git a/apps/webapp/app/presenters/runList.server.ts b/apps/webapp/app/presenters/runList.server.ts new file mode 100644 index 000000000..f8bfa0bc7 --- /dev/null +++ b/apps/webapp/app/presenters/runList.server.ts @@ -0,0 +1,61 @@ +import { prisma } from "~/db.server"; + +type RunListOptions = { + userId: string; + jobId: string; + cursor?: string; +}; + +const PAGE_SIZE = 20; + +export async function runList({ userId, jobId, cursor }: RunListOptions) { + const runs = await prisma.jobRun.findMany({ + //todo change to a select + include: { + environment: { + select: { + type: true, + }, + }, + version: { + select: { + version: true, + }, + }, + }, + where: { + jobId, + organization: { members: { some: { userId } } }, + environment: { + OR: [ + { + orgMember: null, + }, + { + orgMember: { + userId, + }, + }, + ], + }, + }, + orderBy: [{ id: "desc" }], + //take an extra page to tell if there are more + take: PAGE_SIZE + 1, + //skip the cursor if there is one + skip: cursor ? 1 : 0, + cursor: cursor + ? { + id: cursor, + } + : undefined, + }); + + const hasMore = runs.length > PAGE_SIZE; + return { + runs: runs.slice(0, PAGE_SIZE), + hasMore, + //todo look at Stripe for how to structure the object, needs previous cursor too + cursor: hasMore ? runs[PAGE_SIZE - 1].id : undefined, + }; +} diff --git a/apps/webapp/prisma/migrations/20230525111943_project_slug_added/migration.sql b/apps/webapp/prisma/migrations/20230525111943_project_slug_added/migration.sql new file mode 100644 index 000000000..58af4ba20 --- /dev/null +++ b/apps/webapp/prisma/migrations/20230525111943_project_slug_added/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[slug]` on the table `Project` will be added. If there are existing duplicate values, this will fail. + - Added the required column `slug` to the `Project` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "slug" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma index 2fd71d938..c380e7dbc 100644 --- a/apps/webapp/prisma/schema.prisma +++ b/apps/webapp/prisma/schema.prisma @@ -220,6 +220,7 @@ enum RuntimeEnvironmentType { model Project { id String @id @default(cuid()) + slug String @unique name String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@ -595,7 +596,7 @@ enum SecretStoreProvider { AWS_PARAM_STORE } -/// Used when the provider = "database". Not recommended outside of local development. */ +/// Used when the provider = "database". Not recommended outside of local development. model SecretStore { key String @unique value Json