Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/api/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5362,6 +5362,64 @@
"summary": "Create environment"
}
},
"/v1/workspaces/{workspaceId}/environments/name/{name}": {
"get": {
"operationId": "getEnvironmentByName",
"parameters": [
{
"description": "ID of the workspace",
"in": "path",
"name": "workspaceId",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Name of the environment",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EnvironmentWithSystems"
}
}
},
"description": "OK response"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Invalid request"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Resource not found"
}
},
"summary": "Get environment by name"
}
},
"/v1/workspaces/{workspaceId}/environments/{environmentId}": {
"delete": {
"operationId": "requestEnvironmentDeletion",
Expand Down
13 changes: 13 additions & 0 deletions apps/api/openapi/paths/environments.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ local openapi = import '../lib/openapi.libsonnet';
+ openapi.conflictResponse('Environment name already exists in this workspace'),
},
},
'/v1/workspaces/{workspaceId}/environments/name/{name}': {
get: {
summary: 'Get environment by name',
operationId: 'getEnvironmentByName',
parameters: [
openapi.workspaceIdParam(),
openapi.stringParam('name', 'Name of the environment'),
],
responses: openapi.okResponse(openapi.schemaRef('EnvironmentWithSystems'))
+ openapi.notFoundResponse()
+ openapi.badRequestResponse(),
},
},
'/v1/workspaces/{workspaceId}/environments/{environmentId}': {
get: {
summary: 'Get environment',
Expand Down
59 changes: 42 additions & 17 deletions apps/api/src/routes/v1/workspaces/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,17 @@ const listEnvironments: AsyncTypedHandler<
});
};

const getEnvironment: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/environments/{environmentId}",
"get"
> = async (req, res) => {
const { workspaceId, environmentId } = req.params;

const env = await db.query.environment.findFirst({
where: and(
eq(schema.environment.id, environmentId),
eq(schema.environment.workspaceId, workspaceId),
),
});

if (env == null) throw new ApiError("Environment not found", 404);

const getEnvironmentWithSystems = async (
env: typeof schema.environment.$inferSelect,
) => {
const systemRows = await db
.select({ system: schema.system })
.from(schema.systemEnvironment)
.innerJoin(
schema.system,
eq(schema.systemEnvironment.systemId, schema.system.id),
)
.where(eq(schema.systemEnvironment.environmentId, environmentId));
.where(eq(schema.systemEnvironment.environmentId, env.id));

const systems = systemRows.map((r) => ({
id: r.system.id,
Expand All @@ -91,7 +79,43 @@ const getEnvironment: AsyncTypedHandler<
metadata: r.system.metadata,
}));

res.status(200).json({ ...formatEnvironment(env), systems });
return { ...formatEnvironment(env), systems };
};

const getEnvironment: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/environments/{environmentId}",
"get"
> = async (req, res) => {
const { workspaceId, environmentId } = req.params;

const env = await db.query.environment.findFirst({
where: and(
eq(schema.environment.id, environmentId),
eq(schema.environment.workspaceId, workspaceId),
),
});

if (env == null) throw new ApiError("Environment not found", 404);

res.status(200).json(await getEnvironmentWithSystems(env));
};

const getEnvironmentByName: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/environments/name/{name}",
"get"
> = async (req, res) => {
const { workspaceId, name } = req.params;

const env = await db.query.environment.findFirst({
where: and(
eq(schema.environment.name, name),
eq(schema.environment.workspaceId, workspaceId),
),
});

if (env == null) throw new ApiError("Environment not found", 404);

res.status(200).json(await getEnvironmentWithSystems(env));
Comment on lines +103 to +118
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET /environments/name/:name uses the raw environment name as a path segment, but environment names are not validated/normalized (create/upsert accept any string). This means some valid names (e.g. containing / or other reserved characters) cannot be reliably fetched via this endpoint. Consider either (a) enforcing a URL-safe name/slug format at create/upsert (and documenting it in OpenAPI with a pattern), or (b) changing this lookup to use a query parameter instead of a path parameter.

Copilot uses AI. Check for mistakes.
};

const deleteEnvironment: AsyncTypedHandler<
Expand Down Expand Up @@ -207,6 +231,7 @@ export const upsertEnvironmentById: AsyncTypedHandler<
export const environmentsRouter = Router({ mergeParams: true })
.get("/", asyncHandler(listEnvironments))
.post("/", asyncHandler(createEnvironment))
.get("/name/:name", asyncHandler(getEnvironmentByName))
.get("/:environmentId", asyncHandler(getEnvironment))
.put("/:environmentId", asyncHandler(upsertEnvironmentById))
.delete("/:environmentId", asyncHandler(deleteEnvironment));
60 changes: 60 additions & 0 deletions apps/api/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/environments/name/{name}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get environment by name */
get: operations["getEnvironmentByName"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/environments/{environmentId}": {
parameters: {
query?: never;
Expand Down Expand Up @@ -3527,6 +3544,49 @@ export interface operations {
};
};
};
getEnvironmentByName: {
parameters: {
query?: never;
header?: never;
path: {
/** @description ID of the workspace */
workspaceId: string;
/** @description Name of the environment */
name: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["EnvironmentWithSystems"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
getEnvironment: {
parameters: {
query?: never;
Expand Down
Loading
Loading