Skip to content

Commit 451e4f6

Browse files
chore: deployment by name endpoint (#1063)
1 parent 8fd85f4 commit 451e4f6

5 files changed

Lines changed: 223 additions & 18 deletions

File tree

apps/api/openapi/openapi.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4567,6 +4567,64 @@
45674567
"summary": "Create deployment"
45684568
}
45694569
},
4570+
"/v1/workspaces/{workspaceId}/deployments/name/{name}": {
4571+
"get": {
4572+
"operationId": "getDeploymentByName",
4573+
"parameters": [
4574+
{
4575+
"description": "ID of the workspace",
4576+
"in": "path",
4577+
"name": "workspaceId",
4578+
"required": true,
4579+
"schema": {
4580+
"type": "string"
4581+
}
4582+
},
4583+
{
4584+
"description": "Name of the deployment",
4585+
"in": "path",
4586+
"name": "name",
4587+
"required": true,
4588+
"schema": {
4589+
"type": "string"
4590+
}
4591+
}
4592+
],
4593+
"responses": {
4594+
"200": {
4595+
"content": {
4596+
"application/json": {
4597+
"schema": {
4598+
"$ref": "#/components/schemas/DeploymentWithVariablesAndSystems"
4599+
}
4600+
}
4601+
},
4602+
"description": "OK response"
4603+
},
4604+
"400": {
4605+
"content": {
4606+
"application/json": {
4607+
"schema": {
4608+
"$ref": "#/components/schemas/ErrorResponse"
4609+
}
4610+
}
4611+
},
4612+
"description": "Invalid request"
4613+
},
4614+
"404": {
4615+
"content": {
4616+
"application/json": {
4617+
"schema": {
4618+
"$ref": "#/components/schemas/ErrorResponse"
4619+
}
4620+
}
4621+
},
4622+
"description": "Resource not found"
4623+
}
4624+
},
4625+
"summary": "Get deployment by name"
4626+
}
4627+
},
45704628
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}": {
45714629
"delete": {
45724630
"operationId": "requestDeploymentDeletion",

apps/api/openapi/paths/deployments.jsonnet

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ local openapi = import '../lib/openapi.libsonnet';
3232
+ openapi.conflictResponse('Deployment name already exists in this workspace'),
3333
},
3434
},
35+
'/v1/workspaces/{workspaceId}/deployments/name/{name}': {
36+
get: {
37+
summary: 'Get deployment by name',
38+
operationId: 'getDeploymentByName',
39+
parameters: [
40+
openapi.workspaceIdParam(),
41+
openapi.stringParam('name', 'Name of the deployment'),
42+
],
43+
responses: openapi.okResponse(openapi.schemaRef('DeploymentWithVariablesAndSystems'))
44+
+ openapi.notFoundResponse()
45+
+ openapi.badRequestResponse(),
46+
},
47+
},
3548
'/v1/workspaces/{workspaceId}/deployments/{deploymentId}': {
3649
get: {
3750
summary: 'Get deployment',

apps/api/src/routes/v1/workspaces/deployments.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,34 +89,22 @@ const listDeployments: AsyncTypedHandler<
8989
res.status(200).json(data);
9090
};
9191

92-
const getDeployment: AsyncTypedHandler<
93-
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
94-
"get"
95-
> = async (req, res) => {
96-
const { workspaceId, deploymentId } = req.params;
97-
98-
const dep = await db.query.deployment.findFirst({
99-
where: and(
100-
eq(schema.deployment.id, deploymentId),
101-
eq(schema.deployment.workspaceId, workspaceId),
102-
),
103-
});
104-
105-
if (dep == null) throw new ApiError("Deployment not found", 404);
106-
92+
const getDeploymentWithVariablesAndSystems = async (
93+
dep: typeof schema.deployment.$inferSelect,
94+
) => {
10795
const systemRows = await db
10896
.select({ system: schema.system })
10997
.from(schema.systemDeployment)
11098
.innerJoin(
11199
schema.system,
112100
eq(schema.systemDeployment.systemId, schema.system.id),
113101
)
114-
.where(eq(schema.systemDeployment.deploymentId, deploymentId));
102+
.where(eq(schema.systemDeployment.deploymentId, dep.id));
115103

116104
const variables = await db
117105
.select()
118106
.from(schema.deploymentVariable)
119-
.where(eq(schema.deploymentVariable.deploymentId, deploymentId));
107+
.where(eq(schema.deploymentVariable.deploymentId, dep.id));
120108

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

145-
res.status(200).json({
133+
return {
146134
deployment: formatDeployment(dep),
147135
systems: systemRows.map((r) => formatSystem(r.system)),
148136
variables: variables.map((v) => ({
@@ -161,7 +149,43 @@ const getDeployment: AsyncTypedHandler<
161149
resourceSelector: parseSelector(val.resourceSelector),
162150
})),
163151
})),
152+
};
153+
};
154+
155+
const getDeployment: AsyncTypedHandler<
156+
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
157+
"get"
158+
> = async (req, res) => {
159+
const { workspaceId, deploymentId } = req.params;
160+
161+
const dep = await db.query.deployment.findFirst({
162+
where: and(
163+
eq(schema.deployment.id, deploymentId),
164+
eq(schema.deployment.workspaceId, workspaceId),
165+
),
164166
});
167+
168+
if (dep == null) throw new ApiError("Deployment not found", 404);
169+
170+
res.status(200).json(await getDeploymentWithVariablesAndSystems(dep));
171+
};
172+
173+
const getDeploymentByName: AsyncTypedHandler<
174+
"/v1/workspaces/{workspaceId}/deployments/name/{name}",
175+
"get"
176+
> = async (req, res) => {
177+
const { workspaceId, name } = req.params;
178+
179+
const dep = await db.query.deployment.findFirst({
180+
where: and(
181+
eq(schema.deployment.name, name),
182+
eq(schema.deployment.workspaceId, workspaceId),
183+
),
184+
});
185+
186+
if (dep == null) throw new ApiError("Deployment not found", 404);
187+
188+
res.status(200).json(await getDeploymentWithVariablesAndSystems(dep));
165189
};
166190

167191
const postDeployment: AsyncTypedHandler<
@@ -505,6 +529,7 @@ const getDeploymentPlan: AsyncTypedHandler<
505529
export const deploymentsRouter = Router({ mergeParams: true })
506530
.get("/", asyncHandler(listDeployments))
507531
.post("/", asyncHandler(postDeployment))
532+
.get("/name/:name", asyncHandler(getDeploymentByName))
508533
.get("/:deploymentId", asyncHandler(getDeployment))
509534
.put("/:deploymentId", asyncHandler(upsertDeployment))
510535
.delete("/:deploymentId", asyncHandler(deleteDeployment))

apps/api/src/types/openapi.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ export interface paths {
173173
patch?: never;
174174
trace?: never;
175175
};
176+
"/v1/workspaces/{workspaceId}/deployments/name/{name}": {
177+
parameters: {
178+
query?: never;
179+
header?: never;
180+
path?: never;
181+
cookie?: never;
182+
};
183+
/** Get deployment by name */
184+
get: operations["getDeploymentByName"];
185+
put?: never;
186+
post?: never;
187+
delete?: never;
188+
options?: never;
189+
head?: never;
190+
patch?: never;
191+
trace?: never;
192+
};
176193
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}": {
177194
parameters: {
178195
query?: never;
@@ -3055,6 +3072,49 @@ export interface operations {
30553072
};
30563073
};
30573074
};
3075+
getDeploymentByName: {
3076+
parameters: {
3077+
query?: never;
3078+
header?: never;
3079+
path: {
3080+
/** @description ID of the workspace */
3081+
workspaceId: string;
3082+
/** @description Name of the deployment */
3083+
name: string;
3084+
};
3085+
cookie?: never;
3086+
};
3087+
requestBody?: never;
3088+
responses: {
3089+
/** @description OK response */
3090+
200: {
3091+
headers: {
3092+
[name: string]: unknown;
3093+
};
3094+
content: {
3095+
"application/json": components["schemas"]["DeploymentWithVariablesAndSystems"];
3096+
};
3097+
};
3098+
/** @description Invalid request */
3099+
400: {
3100+
headers: {
3101+
[name: string]: unknown;
3102+
};
3103+
content: {
3104+
"application/json": components["schemas"]["ErrorResponse"];
3105+
};
3106+
};
3107+
/** @description Resource not found */
3108+
404: {
3109+
headers: {
3110+
[name: string]: unknown;
3111+
};
3112+
content: {
3113+
"application/json": components["schemas"]["ErrorResponse"];
3114+
};
3115+
};
3116+
};
3117+
};
30583118
getDeployment: {
30593119
parameters: {
30603120
query?: never;

e2e/tests/api/deployments.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,55 @@ test.describe("Deployment API", () => {
719719
]);
720720
});
721721

722+
test("should get a deployment by name", async ({ api, workspace }) => {
723+
const name = `deploy-by-name-${faker.string.alphanumeric(8)}`;
724+
const createRes = await api.POST(
725+
"/v1/workspaces/{workspaceId}/deployments",
726+
{
727+
params: { path: { workspaceId: workspace.id } },
728+
body: { name, slug: name, description: "Fetch-by-name target" },
729+
},
730+
);
731+
expect(createRes.response.status).toBe(202);
732+
const deploymentId = createRes.data!.id;
733+
734+
const getRes = await api.GET(
735+
"/v1/workspaces/{workspaceId}/deployments/name/{name}",
736+
{
737+
params: { path: { workspaceId: workspace.id, name } },
738+
},
739+
);
740+
741+
expect(getRes.response.status).toBe(200);
742+
expect(getRes.data!.deployment.id).toBe(deploymentId);
743+
expect(getRes.data!.deployment.name).toBe(name);
744+
expect(getRes.data!.deployment.description).toBe("Fetch-by-name target");
745+
746+
await api.DELETE(
747+
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
748+
{ params: { path: { workspaceId: workspace.id, deploymentId } } },
749+
);
750+
});
751+
752+
test("should return 404 when getting a deployment by a non-existent name", async ({
753+
api,
754+
workspace,
755+
}) => {
756+
const getRes = await api.GET(
757+
"/v1/workspaces/{workspaceId}/deployments/name/{name}",
758+
{
759+
params: {
760+
path: {
761+
workspaceId: workspace.id,
762+
name: `missing-${faker.string.alphanumeric(12)}`,
763+
},
764+
},
765+
},
766+
);
767+
768+
expect(getRes.response.status).toBe(404);
769+
});
770+
722771
test("should return all deployments when no CEL filter is provided", async ({
723772
api,
724773
workspace,

0 commit comments

Comments
 (0)