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 @@ -4567,6 +4567,64 @@
"summary": "Create deployment"
}
},
"/v1/workspaces/{workspaceId}/deployments/name/{name}": {
"get": {
"operationId": "getDeploymentByName",
"parameters": [
{
"description": "ID of the workspace",
"in": "path",
"name": "workspaceId",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Name of the deployment",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeploymentWithVariablesAndSystems"
}
}
},
"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 deployment by name"
}
},
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}": {
"delete": {
"operationId": "requestDeploymentDeletion",
Expand Down
13 changes: 13 additions & 0 deletions apps/api/openapi/paths/deployments.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ local openapi = import '../lib/openapi.libsonnet';
+ openapi.conflictResponse('Deployment name already exists in this workspace'),
},
},
'/v1/workspaces/{workspaceId}/deployments/name/{name}': {
get: {
summary: 'Get deployment by name',
operationId: 'getDeploymentByName',
parameters: [
openapi.workspaceIdParam(),
openapi.stringParam('name', 'Name of the deployment'),
],
responses: openapi.okResponse(openapi.schemaRef('DeploymentWithVariablesAndSystems'))
+ openapi.notFoundResponse()
+ openapi.badRequestResponse(),
},
},
'/v1/workspaces/{workspaceId}/deployments/{deploymentId}': {
get: {
summary: 'Get deployment',
Expand Down
61 changes: 43 additions & 18 deletions apps/api/src/routes/v1/workspaces/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,34 +89,22 @@ const listDeployments: AsyncTypedHandler<
res.status(200).json(data);
};

const getDeployment: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
"get"
> = async (req, res) => {
const { workspaceId, deploymentId } = req.params;

const dep = await db.query.deployment.findFirst({
where: and(
eq(schema.deployment.id, deploymentId),
eq(schema.deployment.workspaceId, workspaceId),
),
});

if (dep == null) throw new ApiError("Deployment not found", 404);

const getDeploymentWithVariablesAndSystems = async (
dep: typeof schema.deployment.$inferSelect,
) => {
const systemRows = await db
.select({ system: schema.system })
.from(schema.systemDeployment)
.innerJoin(
schema.system,
eq(schema.systemDeployment.systemId, schema.system.id),
)
.where(eq(schema.systemDeployment.deploymentId, deploymentId));
.where(eq(schema.systemDeployment.deploymentId, dep.id));

const variables = await db
.select()
.from(schema.deploymentVariable)
.where(eq(schema.deploymentVariable.deploymentId, deploymentId));
.where(eq(schema.deploymentVariable.deploymentId, dep.id));

const variableIds = variables.map((v) => v.id);
const variableValues =
Expand All @@ -142,7 +130,7 @@ const getDeployment: AsyncTypedHandler<
valuesByVariableId.set(val.deploymentVariableId, arr);
}

res.status(200).json({
return {
deployment: formatDeployment(dep),
systems: systemRows.map((r) => formatSystem(r.system)),
variables: variables.map((v) => ({
Expand All @@ -161,7 +149,43 @@ const getDeployment: AsyncTypedHandler<
resourceSelector: parseSelector(val.resourceSelector),
})),
})),
};
};

const getDeployment: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
"get"
> = async (req, res) => {
const { workspaceId, deploymentId } = req.params;

const dep = await db.query.deployment.findFirst({
where: and(
eq(schema.deployment.id, deploymentId),
eq(schema.deployment.workspaceId, workspaceId),
),
});

if (dep == null) throw new ApiError("Deployment not found", 404);

res.status(200).json(await getDeploymentWithVariablesAndSystems(dep));
};

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

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

if (dep == null) throw new ApiError("Deployment not found", 404);

res.status(200).json(await getDeploymentWithVariablesAndSystems(dep));
};

const postDeployment: AsyncTypedHandler<
Expand Down Expand Up @@ -505,6 +529,7 @@ const getDeploymentPlan: AsyncTypedHandler<
export const deploymentsRouter = Router({ mergeParams: true })
.get("/", asyncHandler(listDeployments))
.post("/", asyncHandler(postDeployment))
.get("/name/:name", asyncHandler(getDeploymentByName))
.get("/:deploymentId", asyncHandler(getDeployment))
.put("/:deploymentId", asyncHandler(upsertDeployment))
.delete("/:deploymentId", asyncHandler(deleteDeployment))
Expand Down
60 changes: 60 additions & 0 deletions apps/api/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/deployments/name/{name}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get deployment by name */
get: operations["getDeploymentByName"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}": {
parameters: {
query?: never;
Expand Down Expand Up @@ -3055,6 +3072,49 @@ export interface operations {
};
};
};
getDeploymentByName: {
parameters: {
query?: never;
header?: never;
path: {
/** @description ID of the workspace */
workspaceId: string;
/** @description Name of the deployment */
name: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DeploymentWithVariablesAndSystems"];
};
};
/** @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"];
};
};
};
};
getDeployment: {
parameters: {
query?: never;
Expand Down
49 changes: 49 additions & 0 deletions e2e/tests/api/deployments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,55 @@ test.describe("Deployment API", () => {
]);
});

test("should get a deployment by name", async ({ api, workspace }) => {
const name = `deploy-by-name-${faker.string.alphanumeric(8)}`;
const createRes = await api.POST(
"/v1/workspaces/{workspaceId}/deployments",
{
params: { path: { workspaceId: workspace.id } },
body: { name, slug: name, description: "Fetch-by-name target" },
},
);
expect(createRes.response.status).toBe(202);
const deploymentId = createRes.data!.id;

const getRes = await api.GET(
"/v1/workspaces/{workspaceId}/deployments/name/{name}",
{
params: { path: { workspaceId: workspace.id, name } },
},
);

expect(getRes.response.status).toBe(200);
expect(getRes.data!.deployment.id).toBe(deploymentId);
expect(getRes.data!.deployment.name).toBe(name);
expect(getRes.data!.deployment.description).toBe("Fetch-by-name target");

await api.DELETE(
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
{ params: { path: { workspaceId: workspace.id, deploymentId } } },
);
});

test("should return 404 when getting a deployment by a non-existent name", async ({
api,
workspace,
}) => {
const getRes = await api.GET(
"/v1/workspaces/{workspaceId}/deployments/name/{name}",
{
params: {
path: {
workspaceId: workspace.id,
name: `missing-${faker.string.alphanumeric(12)}`,
},
},
},
);

expect(getRes.response.status).toBe(404);
});

test("should return all deployments when no CEL filter is provided", async ({
api,
workspace,
Expand Down
Loading