Skip to content

Commit

Permalink
wip: delete category
Browse files Browse the repository at this point in the history
  • Loading branch information
OddTomBrooks committed Dec 11, 2024
1 parent 582091e commit edd4844
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 2 deletions.
125 changes: 124 additions & 1 deletion pkg/graph/generated/generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ type ComplexityRoot struct {
CreatePlanDocumentSolutionLinks func(childComplexity int, solutionID uuid.UUID, documentIDs []uuid.UUID) int
CreatePlanTdl func(childComplexity int, input model.PlanTDLCreateInput) int
CreateStandardCategories func(childComplexity int, modelPlanID uuid.UUID) int
DeleteMTOCategory func(childComplexity int, id uuid.UUID) int
DeleteMTOMilestone func(childComplexity int, id uuid.UUID) int
DeleteOperationalSolutionSubtask func(childComplexity int, id uuid.UUID) int
DeletePlanCollaborator func(childComplexity int, id uuid.UUID) int
Expand Down Expand Up @@ -2422,6 +2423,7 @@ type MutationResolver interface {
RenameMTOCategory(ctx context.Context, id uuid.UUID, name string) (*models.MTOCategory, error)
ReorderMTOCategory(ctx context.Context, id uuid.UUID, newOrder *int, parentID *uuid.UUID) (*models.MTOCategory, error)
CreateStandardCategories(ctx context.Context, modelPlanID uuid.UUID) (bool, error)
DeleteMTOCategory(ctx context.Context, id uuid.UUID) (bool, error)
CreateMTOMilestoneCustom(ctx context.Context, modelPlanID uuid.UUID, name string, mtoCategoryID *uuid.UUID) (*models.MTOMilestone, error)
CreateMTOMilestoneCommon(ctx context.Context, modelPlanID uuid.UUID, commonMilestoneKey models.MTOCommonMilestoneKey, commonSolutions []models.MTOCommonSolutionKey) (*models.MTOMilestone, error)
UpdateMTOMilestone(ctx context.Context, id uuid.UUID, changes map[string]interface{}) (*models.MTOMilestone, error)
Expand Down Expand Up @@ -5170,6 +5172,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

return e.complexity.Mutation.CreateStandardCategories(childComplexity, args["modelPlanID"].(uuid.UUID)), true

case "Mutation.deleteMTOCategory":
if e.complexity.Mutation.DeleteMTOCategory == nil {
break
}

args, err := ec.field_Mutation_deleteMTOCategory_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}

return e.complexity.Mutation.DeleteMTOCategory(childComplexity, args["id"].(uuid.UUID)), true

case "Mutation.deleteMTOMilestone":
if e.complexity.Mutation.DeleteMTOMilestone == nil {
break
Expand Down Expand Up @@ -16961,7 +16975,7 @@ type MTOCategories {

extend type Mutation {
"""
Allows you to create an MTOCategory or Subcategory if you provide a parent ID.
Allows you to create an MTOCategory or Subcategory if you provide a parent ID.
Note, the parent must belong to the same model plan, or this will return an error
"""
createMTOCategory(modelPlanID: UUID!, name: String!, parentID: UUID): MTOCategory!
Expand All @@ -16988,6 +17002,14 @@ extend type Mutation {
"""
createStandardCategories(modelPlanID: UUID!): Boolean!
@hasRole(role: MINT_USER)

"""
Deletes an MTO category. If the category has subcategories, it will delete them as well.
If the target category is a subcategory, it will only delete the subcategory and redirect
references to the parent category.
"""
deleteMTOCategory(id: UUID!): Boolean!
@hasRole(role: MINT_USER)
}`, BuiltIn: false},
{Name: "../schema/types/mto_category_translation.graphql", Input: `
"""
Expand Down Expand Up @@ -21787,6 +21809,21 @@ func (ec *executionContext) field_Mutation_createStandardCategories_args(ctx con
return args, nil
}

func (ec *executionContext) field_Mutation_deleteMTOCategory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 uuid.UUID
if tmp, ok := rawArgs["id"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
arg0, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, tmp)
if err != nil {
return nil, err
}
}
args["id"] = arg0
return args, nil
}

func (ec *executionContext) field_Mutation_deleteMTOMilestone_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
Expand Down Expand Up @@ -41314,6 +41351,85 @@ func (ec *executionContext) fieldContext_Mutation_createStandardCategories(ctx c
return fc, nil
}

func (ec *executionContext) _Mutation_deleteMTOCategory(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_deleteMTOCategory(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().DeleteMTOCategory(rctx, fc.Args["id"].(uuid.UUID))
}
directive1 := func(ctx context.Context) (interface{}, error) {
role, err := ec.unmarshalNRole2githubᚗcomᚋcmsᚑenterpriseᚋmintᚑappᚋpkgᚋgraphᚋmodelᚐRole(ctx, "MINT_USER")
if err != nil {
return nil, err
}
if ec.directives.HasRole == nil {
return nil, errors.New("directive hasRole is not implemented")
}
return ec.directives.HasRole(ctx, nil, directive0, role)
}

tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(bool); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be bool`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_Mutation_deleteMTOCategory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_deleteMTOCategory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}

func (ec *executionContext) _Mutation_createMTOMilestoneCustom(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createMTOMilestoneCustom(ctx, field)
if err != nil {
Expand Down Expand Up @@ -139607,6 +139723,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "deleteMTOCategory":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_deleteMTOCategory(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createMTOMilestoneCustom":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createMTOMilestoneCustom(ctx, field)
Expand Down
23 changes: 23 additions & 0 deletions pkg/graph/resolvers/mto_category.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ func MTOCategoryCreate(ctx context.Context, logger *zap.Logger, principal authen
return storage.MTOCategoryCreate(store, logger, category)
}

// MTOCategoryDelete removes an MTOCategory or SubCategory
func MTOCategoryDelete(ctx context.Context, logger *zap.Logger, principal authentication.Principal, store *storage.Store,
id uuid.UUID,
) error {
principalAccount := principal.Account()
if principalAccount == nil {
return fmt.Errorf("principal doesn't have an account, username %s", principal.String())
}

existing, err := storage.MTOCategoryGetByID(store, logger, id)
if err != nil {
return fmt.Errorf("unable to delete MTO category. Err %w", err)
}

// Just check access, don't apply changes here
err = BaseStructPreDelete(logger, existing, principal, store, true)
if err != nil {
return fmt.Errorf("unable to delete MTO category. Err %w", err)
}

return storage.MTOCategoryDelete(store, logger, id)
}

// MTOCategoryRename updates the name of MTOCategory or SubCategory
func MTOCategoryRename(ctx context.Context, logger *zap.Logger, principal authentication.Principal, store *storage.Store,
id uuid.UUID,
Expand Down
10 changes: 10 additions & 0 deletions pkg/graph/resolvers/mto_category.resolvers.go

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

10 changes: 9 additions & 1 deletion pkg/graph/schema/types/mto_category.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type MTOCategories {

extend type Mutation {
"""
Allows you to create an MTOCategory or Subcategory if you provide a parent ID.
Allows you to create an MTOCategory or Subcategory if you provide a parent ID.
Note, the parent must belong to the same model plan, or this will return an error
"""
createMTOCategory(modelPlanID: UUID!, name: String!, parentID: UUID): MTOCategory!
Expand All @@ -60,4 +60,12 @@ extend type Mutation {
"""
createStandardCategories(modelPlanID: UUID!): Boolean!
@hasRole(role: MINT_USER)

"""
Deletes an MTO category. If the category has subcategories, it will delete them as well.
If the target category is a subcategory, it will only delete the subcategory and redirect
references to the parent category.
"""
deleteMTOCategory(id: UUID!): Boolean!
@hasRole(role: MINT_USER)
}
105 changes: 105 additions & 0 deletions pkg/sqlqueries/SQL/mto/category/delete.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
-- TODO: Move function creation to a migration, keeping it here for testing purposes

/*
This SQL script sets up a trigger and a supporting function to handle custom cascading
deletion behavior for categories in the `mto_category` table, per the application requirements.
Updated Assumption (per user's note):
-------------------------------------
Instead of assigning milestones to a designated "Uncategorized" category with a known UUID,
we will set their `mto_category_id` to NULL (a "nil" UUID), indicating they are now uncategorized.
Requirements:
- Deleting a subcategory:
* All milestones referencing that subcategory should be reassigned to its parent category.
- Deleting a top-level category:
* All milestones referencing the deleted top-level category or its direct subcategories
should have `mto_category_id` set to NULL, indicating they are now uncategorized.
* All direct subcategories of the deleted category should be deleted.
Why the "CASCADE" Keyword Isn't Used:
-------------------------------------
The standard ON DELETE CASCADE simply removes dependent rows. Here, we need to reassign
references rather than just delete them. Since this behavior is more complex, we implement
custom logic in a trigger and a PL/pgSQL function rather than relying on cascade deletes.
Usage:
- Once this script is applied, a `DELETE FROM mto_category WHERE id = 'some-category-uuid';`
will trigger the logic to reassign milestones and remove subcategories as defined.
Assumptions:
- When deleting a top-level category, milestones are "uncategorized" by setting their
`mto_category_id` to NULL.
- For subcategories, milestones are moved up to the parent category.
- This handles one level of subcategories. If a deeper hierarchy is required,
the logic should be extended to recursively handle all descendants.
*/

-- Drop existing trigger and function to allow clean re-creation
DROP TRIGGER IF EXISTS mto_category_before_delete ON mto_category;
DROP FUNCTION IF EXISTS rebalance_milestones_before_category_delete();

-- Create the trigger function that implements the custom cascading logic
CREATE OR REPLACE FUNCTION rebalance_milestones_before_category_delete()
RETURNS TRIGGER AS $$
DECLARE
cat_model_plan_id UUID;
is_top_level BOOLEAN;
BEGIN
-- Determine if the category is top-level or a subcategory
SELECT model_plan_id, (parent_id IS NULL) INTO cat_model_plan_id, is_top_level
FROM mto_category
WHERE id = OLD.id;

IF is_top_level THEN
-- TOP-LEVEL CATEGORY DELETION LOGIC:
-- Reassign all milestones referencing this category or its direct subcategories
-- to NULL, marking them as uncategorized.

UPDATE mto_milestone
SET mto_category_id = NULL
WHERE mto_category_id IN (
SELECT id FROM mto_category
WHERE parent_id = OLD.id
UNION ALL
SELECT OLD.id
);

-- Delete all direct subcategories of this category
DELETE FROM mto_category
WHERE parent_id = OLD.id;

-- The category itself (OLD.id) will be deleted by the original DELETE statement
ELSE
-- SUBCATEGORY DELETION LOGIC:
-- Move all milestones from this subcategory to its parent category
UPDATE mto_milestone
SET mto_category_id = OLD.parent_id
WHERE mto_category_id = OLD.id;

-- The subcategory (OLD.id) will be deleted by the original DELETE statement
END IF;

RETURN OLD; -- Allow the DELETE to proceed after adjustments
END;
$$ LANGUAGE plpgsql;

-- Create the trigger to invoke the above function before any category deletion
CREATE TRIGGER mto_category_before_delete
BEFORE DELETE ON mto_category
FOR EACH ROW
EXECUTE FUNCTION rebalance_milestones_before_category_delete();


DELETE FROM mto_category
WHERE id = :id
RETURNING
id,
name,
parent_id,
model_plan_id,
position,
created_by,
created_dts,
modified_by,
modified_dts;
5 changes: 5 additions & 0 deletions pkg/sqlqueries/mto_category.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import _ "embed"
//go:embed SQL/mto/category/create.sql
var mtoCategoryCreateSQL string

//go:embed SQL/mto/category/delete.sql
var mtoCategoryDeleteSQL string

//go:embed SQL/mto/category/create_allow_conflicts.sql
var mtoCategoryCreateAllowConflictsSQL string

Expand All @@ -28,6 +31,7 @@ var mtoCategoryGetByParentIDLoaderSQL string

type mtoCategoryScripts struct {
Create string
Delete string
CreateAllowConflicts string
Update string
GetByID string
Expand All @@ -44,6 +48,7 @@ type mtoCategoryScripts struct {
// MTOCategory houses all the sql for getting data for mto category from the database
var MTOCategory = mtoCategoryScripts{
Create: mtoCategoryCreateSQL,
Delete: mtoCategoryDeleteSQL,
CreateAllowConflicts: mtoCategoryCreateAllowConflictsSQL,
Update: mtoCategoryUpdateSQL,
GetByID: mtoCategoryGetByIDSQL,
Expand Down
Loading

0 comments on commit edd4844

Please sign in to comment.