Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API environment endpoints #2529

Merged
merged 15 commits into from
May 30, 2024
170 changes: 170 additions & 0 deletions packages/back-end/generated/spec.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/back-end/src/api/api.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import experimentsRouter from "./experiments/experiments.router";
import metricsRouter from "./metrics/metrics.router";
import segmentsRouter from "./segments/segments.router";
import projectsRouter from "./projects/projects.router";
import environmentsRouter from "./environments/environments.router";
import savedGroupsRouter from "./saved-groups/saved-groups.router";
import sdkConnectionsRouter from "./sdk-connections/sdk-connections.router";
import sdkPayloadRouter from "./sdk-payload/sdk-payload.router";
Expand Down Expand Up @@ -82,6 +83,7 @@ router.use("/metrics", metricsRouter);
router.use("/segments", segmentsRouter);
router.use("/dimensions", dimensionsRouter);
router.use("/projects", projectsRouter);
router.use("/environments", environmentsRouter);
router.use("/sdk-connections", sdkConnectionsRouter);
router.use("/data-sources", dataSourcesRouter);
router.use("/visual-changesets", visualChangesetsRouter);
Expand Down
46 changes: 46 additions & 0 deletions packages/back-end/src/api/environments/deleteEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { DeleteEnvironmentResponse } from "../../../types/openapi";
import { createApiRequestHandler } from "../../util/handler";
import { deleteEnvironmentValidator } from "../../validators/openapi";
import { updateOrganization } from "../../models/OrganizationModel";
import { OrganizationInterface } from "../../../types/organization";
import { auditDetailsDelete } from "../../services/audit";

export const deleteEnvironment = createApiRequestHandler(
deleteEnvironmentValidator
)(
async (req): Promise<DeleteEnvironmentResponse> => {
const id = req.params.id;
const org = req.context.org;
const environments = org.settings?.environments || [];

const environment = environments.find((env) => env.id === id);
if (!environment) {
throw Error(`Environment ${id} does not exists!`);
}

if (!req.context.permissions.canDeleteEnvironment(environment))
throw Error("You do not have permission to delete this environment!");
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have a specific permissions error that we can use that provides better categorization of errors.

E.G.

if (!context.permissions.canDeleteEnvironment(environment)) {
context.permissions.throwPermissionError();
}


const updates: Partial<OrganizationInterface> = {
settings: {
...org.settings,
environments: [...environments.filter((env) => env.id !== id)],
},
};

await updateOrganization(org.id, updates);

await req.audit({
event: "environment.delete",
entity: {
object: "environment",
id: environment.id,
},
details: auditDetailsDelete(environment),
});

return {
deletedId: id,
};
}
);
14 changes: 14 additions & 0 deletions packages/back-end/src/api/environments/environments.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Router } from "express";
import { listEnvironments } from "./listEnvironments";
import { putEnvironment } from "./putEnvironment";
import { postEnvironment } from "./postEnvironment";
import { deleteEnvironment } from "./deleteEnvironment";

const router = Router();

router.get("/:environments", listEnvironments);
router.post("/environments", postEnvironment);
router.put("/environments/:environmentId", putEnvironment);
router.delete("/environments/:environmentId", deleteEnvironment);

export default router;
41 changes: 41 additions & 0 deletions packages/back-end/src/api/environments/listEnvironments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ListEnvironmentsResponse } from "../../../types/openapi";
import { createApiRequestHandler } from "../../util/handler";
import { findOrganizationById } from "../../models/OrganizationModel";
import { listEnvironmentsValidator } from "../../validators/openapi";

export const listEnvironments = createApiRequestHandler(
listEnvironmentsValidator
)(
async (req): Promise<ListEnvironmentsResponse> => {
const id = req.params.id;

const org = await findOrganizationById(id);
if (!org) {
throw Error("Organization not found");
}
romain-growthbook marked this conversation as resolved.
Show resolved Hide resolved

const environments = (
org.settings?.environments || []
).filter((environment) =>
req.context.permissions.canReadMultiProjectResource(environment.projects)
);

return {
environments: environments.map(
({
id,
description = "",
toggleOnList = false,
defaultState = false,
projects = [],
}) => ({
id,
projects,
description,
defaultState,
toggleOnList,
})
),
};
}
);
46 changes: 46 additions & 0 deletions packages/back-end/src/api/environments/postEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { PostEnvironmentResponse } from "../../../types/openapi";
import { createApiRequestHandler } from "../../util/handler";
import { postEnvironmentValidator } from "../../validators/openapi";
import { updateOrganization } from "../../models/OrganizationModel";
import { OrganizationInterface } from "../../../types/organization";
import { auditDetailsCreate } from "../../services/audit";
import { validatePayload } from "./validations";

export const postEnvironment = createApiRequestHandler(
postEnvironmentValidator
)(
async (req): Promise<PostEnvironmentResponse> => {
const environment = await validatePayload(req.context, req.body);

const org = req.context.org;

if (org.settings?.environments?.some((env) => env.id === environment.id)) {
throw Error(`Environment ${environment.id} already exists!`);
}

if (!req.context.permissions.canCreateOrUpdateEnvironment(environment))
throw new Error("You don't have permission to update this environment!");

const updates: Partial<OrganizationInterface> = {
settings: {
...org.settings,
environments: [...(org.settings?.environments || []), environment],
},
};

await updateOrganization(org.id, updates);

await req.audit({
event: "environment.create",
entity: {
object: "environment",
id: environment.id,
},
details: auditDetailsCreate(environment),
});

return {
environment,
};
}
);