diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index aa7d2fc510..b09447340e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -385,6 +385,10 @@ export const PROJECTS = { GET: { workspaceId: "The ID of the project." }, + GET_ID: { + slug: "The slug of the project to get", + name: "The name of the project to get" + }, UPDATE: { workspaceId: "The ID of the project to update.", name: "The new name of the project.", diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 7619171daf..2734cd6099 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; +import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -511,4 +512,52 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { serviceTokenData }; } }); + + server.route({ + method: "GET", + url: "/id", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + slug: z.string().trim().optional().describe(PROJECTS.GET_ID.slug), + name: z.string().trim().optional().describe(PROJECTS.GET_ID.name) + }), + response: { + 200: z.object({ + id: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + if (!req.query.slug && !req.query.name) { + throw new BadRequestError({ + name: "Project name or slug is required." + }); + } + + const workspace = await server.services.project.getAProject({ + filter: { + ...(req.query.slug + ? { + type: ProjectFilterType.SLUG, + slug: req.query.slug + } + : { + type: ProjectFilterType.NAME, + name: req.query.name as string + }), + orgId: req.permission.orgId + }, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + + return { id: workspace.id }; + } + }); }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index ce7f6324e1..0eb6914722 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -244,6 +244,55 @@ export const projectDALFactory = (db: TDbClient) => { } }; + const findProjectByName = async (name: string, orgId: string | undefined) => { + try { + if (!orgId) { + throw new BadRequestError({ message: "Organization ID is required when querying with slugs" }); + } + + const projects = await db + .replicaNode()(TableName.Project) + .where(`${TableName.Project}.name`, name) + .where(`${TableName.Project}.orgId`, orgId) + .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .select( + selectAllTableCols(TableName.Project), + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + .orderBy([ + { column: `${TableName.Project}.name`, order: "asc" }, + { column: `${TableName.Environment}.position`, order: "asc" } + ]); + + const project = sqlNestRelationships({ + data: projects, + key: "id", + parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }), + childrenMapper: [ + { + key: "envId", + label: "environments" as const, + mapper: ({ envId, envSlug, envName }) => ({ + id: envId, + slug: envSlug, + name: envName + }) + } + ] + })?.[0]; + + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + return project; + } catch (error) { + throw new DatabaseError({ error, name: "Find project by slug" }); + } + }; + const findProjectByFilter = async (filter: Filter) => { try { if (filter.type === ProjectFilterType.ID) { @@ -258,6 +307,17 @@ export const projectDALFactory = (db: TDbClient) => { return await findProjectBySlug(filter.slug, filter.orgId); } + + if (filter.type === ProjectFilterType.NAME) { + if (!filter.orgId) { + throw new BadRequestError({ + message: "Organization ID is required when querying with names" + }); + } + + return await findProjectByName(filter.name, filter.orgId); + } + throw new BadRequestError({ message: "Invalid filter type" }); } catch (error) { if (error instanceof BadRequestError) { diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 1c2279c7a6..2e77ec0dd1 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -7,7 +7,8 @@ import { KmsType } from "../kms/kms-types"; export enum ProjectFilterType { ID = "id", - SLUG = "slug" + SLUG = "slug", + NAME = "name" } export type Filter = @@ -19,6 +20,11 @@ export type Filter = type: ProjectFilterType.SLUG; slug: string; orgId: string | undefined; + } + | { + type: ProjectFilterType.NAME; + name: string; + orgId: string | undefined; }; export type TCreateProjectDTO = { diff --git a/docs/api-reference/endpoints/workspaces/get-workspace-id.mdx b/docs/api-reference/endpoints/workspaces/get-workspace-id.mdx new file mode 100644 index 0000000000..52611efe59 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/get-workspace-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Project ID" +openapi: "GET /api/v1/workspace/id" +--- diff --git a/docs/mint.json b/docs/mint.json index ed824284b0..28957d2a38 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -538,6 +538,7 @@ "api-reference/endpoints/workspaces/create-workspace", "api-reference/endpoints/workspaces/delete-workspace", "api-reference/endpoints/workspaces/get-workspace", + "api-reference/endpoints/workspaces/get-workspace-id", "api-reference/endpoints/workspaces/update-workspace", "api-reference/endpoints/workspaces/secret-snapshots", "api-reference/endpoints/workspaces/rollback-snapshot"