From c56200e0304b14919c68070d7b92dc88a63aeba4 Mon Sep 17 00:00:00 2001
From: Mateusz Kwasniewski <kwasniewski.mateusz@gmail.com>
Date: Fri, 3 Jan 2025 15:38:10 +0100
Subject: [PATCH] feat: ability to delete single legal values (#9058)

Co-authored-by: Thomas Heartman <thomas@getunleash.io>
---
 src/lib/features/context/context-service.ts | 30 +++++++++++++-
 src/lib/features/context/context.test.ts    | 37 ++++++++++++++++++
 src/lib/features/context/context.ts         | 43 +++++++++++++++++++--
 3 files changed, 106 insertions(+), 4 deletions(-)

diff --git a/src/lib/features/context/context-service.ts b/src/lib/features/context/context-service.ts
index db4dd29444..b96b1e680d 100644
--- a/src/lib/features/context/context-service.ts
+++ b/src/lib/features/context/context-service.ts
@@ -140,7 +140,7 @@ class ContextService {
         });
     }
 
-    async updateContextFieldLegalValue(
+    async updateLegalValue(
         contextFieldLegalValue: { name: string; legalValue: LegalValueSchema },
         auditUser: IAuditUser,
     ): Promise<void> {
@@ -179,6 +179,34 @@ class ContextService {
         });
     }
 
+    async deleteLegalValue(
+        contextFieldLegalValue: { name: string; legalValue: string },
+        auditUser: IAuditUser,
+    ): Promise<void> {
+        const contextField = await this.contextFieldStore.get(
+            contextFieldLegalValue.name,
+        );
+
+        const newContextField = {
+            ...contextField,
+            legalValues: contextField.legalValues?.filter(
+                (legalValue) =>
+                    legalValue.value !== contextFieldLegalValue.legalValue,
+            ),
+        };
+
+        await this.contextFieldStore.update(newContextField);
+
+        await this.eventService.storeEvent({
+            type: CONTEXT_FIELD_UPDATED,
+            createdBy: auditUser.username,
+            createdByUserId: auditUser.id,
+            ip: auditUser.ip,
+            preData: contextField,
+            data: newContextField,
+        });
+    }
+
     async deleteContextField(
         name: string,
         auditUser: IAuditUser,
diff --git a/src/lib/features/context/context.test.ts b/src/lib/features/context/context.test.ts
index a1cb282acd..f5e5334379 100644
--- a/src/lib/features/context/context.test.ts
+++ b/src/lib/features/context/context.test.ts
@@ -204,6 +204,43 @@ test('should add and update a single context field with new legal values', async
     });
 });
 
+test('should delete a single context field legal value', async () => {
+    expect.assertions(1);
+
+    // add a new context field legal value
+    await request
+        .post(`${base}/api/admin/context/environment/legal-values`)
+        .send({
+            value: 'valueA',
+        })
+        .set('Content-Type', 'application/json')
+        .expect(200);
+
+    await request
+        .post(`${base}/api/admin/context/environment/legal-values`)
+        .send({
+            value: 'valueB',
+        })
+        .set('Content-Type', 'application/json')
+        .expect(200);
+
+    await request
+        .delete(`${base}/api/admin/context/environment/legal-values/valueB`)
+        .expect(200);
+
+    const { body } = await request.get(`${base}/api/admin/context/environment`);
+
+    expect(body).toMatchObject({
+        name: 'environment',
+        legalValues: [{ value: 'valueA' }],
+    });
+
+    // verify delete is idempotent
+    await request
+        .delete(`${base}/api/admin/context/environment/legal-values/valueB`)
+        .expect(200);
+});
+
 test('should not delete a unknown context field', () => {
     expect.assertions(0);
 
diff --git a/src/lib/features/context/context.ts b/src/lib/features/context/context.ts
index 20e28f062b..ca21648ac2 100644
--- a/src/lib/features/context/context.ts
+++ b/src/lib/features/context/context.ts
@@ -45,6 +45,10 @@ interface ContextParam {
     contextField: string;
 }
 
+interface DeleteLegalValueParam extends ContextParam {
+    legalValue: string;
+}
+
 export class ContextController extends Controller {
     private contextService: ContextService;
 
@@ -172,7 +176,7 @@ export class ContextController extends Controller {
         this.route({
             method: 'post',
             path: '/:contextField/legal-values',
-            handler: this.updateContextFieldLegalValue,
+            handler: this.updateLegalValue,
             permission: UPDATE_CONTEXT_FIELD,
             middleware: [
                 openApiService.validPath({
@@ -188,6 +192,25 @@ export class ContextController extends Controller {
             ],
         });
 
+        this.route({
+            method: 'delete',
+            path: '/:contextField/legal-values/:legalValue',
+            handler: this.deleteLegalValue,
+            acceptAnyContentType: true,
+            permission: UPDATE_CONTEXT_FIELD,
+            middleware: [
+                openApiService.validPath({
+                    tags: ['Context'],
+                    summary: 'Delete legal value for the context field',
+                    description: `Removes the specified custom context field legal value. Does not validate that the legal value is not in use and does not remove usage from constraints that use it.`,
+                    operationId: 'deleteContextFieldLegalValue',
+                    responses: {
+                        200: emptyResponse,
+                    },
+                }),
+            ],
+        });
+
         this.route({
             method: 'delete',
             path: '/:contextField',
@@ -291,14 +314,28 @@ export class ContextController extends Controller {
         res.status(200).end();
     }
 
-    async updateContextFieldLegalValue(
+    async updateLegalValue(
         req: IAuthRequest<ContextParam, void, LegalValueSchema>,
         res: Response,
     ): Promise<void> {
         const name = req.params.contextField;
         const legalValue = req.body;
 
-        await this.contextService.updateContextFieldLegalValue(
+        await this.contextService.updateLegalValue(
+            { name, legalValue },
+            req.audit,
+        );
+        res.status(200).end();
+    }
+
+    async deleteLegalValue(
+        req: IAuthRequest<DeleteLegalValueParam, void>,
+        res: Response,
+    ): Promise<void> {
+        const name = req.params.contextField;
+        const legalValue = req.params.legalValue;
+
+        await this.contextService.deleteLegalValue(
             { name, legalValue },
             req.audit,
         );