From 6198f3e45dc93b3930b2d6f05194fa099a0fb3d4 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Mon, 11 Mar 2024 10:26:52 -0700 Subject: [PATCH 01/19] feat: user notification user userIDRelation --- pkg/models/added_as_collaborator_meta.go | 63 ++++++++++++++++++++++++ pkg/models/user_notification.go | 13 ++--- 2 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 pkg/models/added_as_collaborator_meta.go diff --git a/pkg/models/added_as_collaborator_meta.go b/pkg/models/added_as_collaborator_meta.go new file mode 100644 index 0000000000..42dd827424 --- /dev/null +++ b/pkg/models/added_as_collaborator_meta.go @@ -0,0 +1,63 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + + "github.com/google/uuid" +) + +// AddedAsCollaboratorMeta represents the notification data that is relevant to being added as a collaborator +type AddedAsCollaboratorMeta struct { + ActivityMetaBaseStruct + modelPlanRelation +} + +// newNewPlanDiscussionActivityMeta creates a New NewPlanDiscussionActivityMeta +func newAddedAsCollaboratorMeta(modelPlanID uuid.UUID) *AddedAsCollaboratorMeta { + version := 0 //iterate this if this type ever updates + return &AddedAsCollaboratorMeta{ + ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussion, version), + modelPlanRelation: NewModelPlanRelation(modelPlanID), + } + +} + +// NewAddedAsCollaboratorActivity creates a New Added as Collaborator type of Activity +func NewAddedAsCollaboratorActivity(actorID uuid.UUID, modelPlanID uuid.UUID, collaboratorID uuid.UUID) *Activity { + return &Activity{ + baseStruct: NewBaseStruct(actorID), + ActorID: actorID, + EntityID: collaboratorID, + ActivityType: ActivityAddedAsCollaborator, + MetaData: newAddedAsCollaboratorMeta(modelPlanID), + } +} + +// Future Enhancement: --> Refactor these all to have a generic scan / value + +// Value allows us to satisfy the valuer interface so we can write to the database +// We need to do a specific implementation instead of relying on the implementation of the embedded struct, as that will only serialize the common data +func (d AddedAsCollaboratorMeta) Value() (driver.Value, error) { + + j, err := json.Marshal(d) + return j, err +} + +// Scan implements the scanner interface so we can translate the JSONb from the db to an object in GO +func (d *AddedAsCollaboratorMeta) Scan(src interface{}) error { + if src == nil { + return nil + } + source, ok := src.([]byte) + if !ok { + return errors.New("type assertion .([]byte) failed") + } + err := json.Unmarshal(source, d) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/models/user_notification.go b/pkg/models/user_notification.go index af0931cd79..26b60867f9 100644 --- a/pkg/models/user_notification.go +++ b/pkg/models/user_notification.go @@ -9,7 +9,7 @@ import ( type UserNotification struct { baseStruct // The id of the user this notification is for - UserID uuid.UUID `json:"userID" db:"user_id"` + userIDRelation // The if of the entity this notification is about ActivityID uuid.UUID `json:"activityID" db:"activity_id"` IsRead bool `json:"isRead" db:"is_read"` @@ -27,11 +27,12 @@ func NewUserNotification( emailNotification bool, ) *UserNotification { return &UserNotification{ - baseStruct: NewBaseStruct(userID), - ActivityID: activityID, - IsRead: !inAppNotification, // set to read if user doesn't want notifications - InAppSent: inAppNotification, // set to archived if user doesn't want notifications - EmailSent: emailNotification, // set that an email should be sent for this + baseStruct: NewBaseStruct(userID), + userIDRelation: NewUserIDRelation(userID), + ActivityID: activityID, + IsRead: !inAppNotification, // set to read if user doesn't want notifications + InAppSent: inAppNotification, // set to archived if user doesn't want notifications + EmailSent: emailNotification, // set that an email should be sent for this } } From 5963534c5be3a73d3886aba95ece17ac5ce19555 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Mon, 11 Mar 2024 10:57:36 -0700 Subject: [PATCH 02/19] feat: preliminary added as a collaboorator meta data type --- pkg/graph/generated/generated.go | 542 +++++++++++++++++++++- pkg/graph/resolvers/activity.resolvers.go | 18 + pkg/graph/resolvers/plan_collaborator.go | 1 + pkg/graph/schema/types/activity.graphql | 10 +- pkg/models/added_as_collaborator_meta.go | 16 +- pkg/models/model_plan_relation.go | 3 + src/gql/gen/graphql.ts | 14 +- src/gql/gen/types/GetNotifications.ts | 2 +- 8 files changed, 594 insertions(+), 12 deletions(-) diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index 34c487607f..fa3e892b2c 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -42,6 +42,7 @@ type Config struct { type ResolverRoot interface { Activity() ActivityResolver + AddedAsCollaboratorMeta() AddedAsCollaboratorMetaResolver AuditChange() AuditChangeResolver CurrentUser() CurrentUserResolver DiscussionReply() DiscussionReplyResolver @@ -98,6 +99,15 @@ type ComplexityRoot struct { Version func(childComplexity int) int } + AddedAsCollaboratorMeta struct { + Collaborator func(childComplexity int) int + CollaboratorID func(childComplexity int) int + ModelPlan func(childComplexity int) int + ModelPlanID func(childComplexity int) int + Type func(childComplexity int) int + Version func(childComplexity int) int + } + AuditChange struct { Action func(childComplexity int) int Fields func(childComplexity int) int @@ -1099,6 +1109,11 @@ type ComplexityRoot struct { type ActivityResolver interface { ActorUserAccount(ctx context.Context, obj *models.Activity) (*authentication.UserAccount, error) } +type AddedAsCollaboratorMetaResolver interface { + ModelPlan(ctx context.Context, obj *models.AddedAsCollaboratorMeta) (*models.ModelPlan, error) + + Collaborator(ctx context.Context, obj *models.AddedAsCollaboratorMeta) (*models.PlanCollaborator, error) +} type AuditChangeResolver interface { Fields(ctx context.Context, obj *models.AuditChange) (map[string]interface{}, error) } @@ -1496,6 +1511,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ActivityMetaBaseStruct.Version(childComplexity), true + case "AddedAsCollaboratorMeta.collaborator": + if e.complexity.AddedAsCollaboratorMeta.Collaborator == nil { + break + } + + return e.complexity.AddedAsCollaboratorMeta.Collaborator(childComplexity), true + + case "AddedAsCollaboratorMeta.collaboratorID": + if e.complexity.AddedAsCollaboratorMeta.CollaboratorID == nil { + break + } + + return e.complexity.AddedAsCollaboratorMeta.CollaboratorID(childComplexity), true + + case "AddedAsCollaboratorMeta.modelPlan": + if e.complexity.AddedAsCollaboratorMeta.ModelPlan == nil { + break + } + + return e.complexity.AddedAsCollaboratorMeta.ModelPlan(childComplexity), true + + case "AddedAsCollaboratorMeta.modelPlanID": + if e.complexity.AddedAsCollaboratorMeta.ModelPlanID == nil { + break + } + + return e.complexity.AddedAsCollaboratorMeta.ModelPlanID(childComplexity), true + + case "AddedAsCollaboratorMeta.type": + if e.complexity.AddedAsCollaboratorMeta.Type == nil { + break + } + + return e.complexity.AddedAsCollaboratorMeta.Type(childComplexity), true + + case "AddedAsCollaboratorMeta.version": + if e.complexity.AddedAsCollaboratorMeta.Version == nil { + break + } + + return e.complexity.AddedAsCollaboratorMeta.Version(childComplexity), true + case "AuditChange.action": if e.complexity.AuditChange.Action == nil { break @@ -9390,7 +9447,15 @@ enum ActivityType { """ ActivityMetaData is a type that represents all the data that can be captured in an Activity """ -union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta +union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | AddedAsCollaboratorMeta +type AddedAsCollaboratorMeta { + version: Int! + type: ActivityType! + modelPlanID: UUID! + modelPlan: ModelPlan! + collaboratorID: UUID! + collaborator: PlanCollaborator! +} type TaggedInPlanDiscussionActivityMeta { version: Int! @@ -12829,6 +12894,350 @@ func (ec *executionContext) fieldContext_ActivityMetaBaseStruct_type(ctx context return fc, nil } +func (ec *executionContext) _AddedAsCollaboratorMeta_version(ctx context.Context, field graphql.CollectedField, obj *models.AddedAsCollaboratorMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AddedAsCollaboratorMeta_version(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) { + ctx = rctx // use context from middleware stack in children + return obj.Version, nil + }) + 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.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AddedAsCollaboratorMeta_version(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AddedAsCollaboratorMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AddedAsCollaboratorMeta_type(ctx context.Context, field graphql.CollectedField, obj *models.AddedAsCollaboratorMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AddedAsCollaboratorMeta_type(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) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + 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.(models.ActivityType) + fc.Result = res + return ec.marshalNActivityType2githubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐActivityType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AddedAsCollaboratorMeta_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AddedAsCollaboratorMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ActivityType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AddedAsCollaboratorMeta_modelPlanID(ctx context.Context, field graphql.CollectedField, obj *models.AddedAsCollaboratorMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AddedAsCollaboratorMeta_modelPlanID(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) { + ctx = rctx // use context from middleware stack in children + return obj.ModelPlanID, nil + }) + 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.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AddedAsCollaboratorMeta_modelPlanID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AddedAsCollaboratorMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AddedAsCollaboratorMeta_modelPlan(ctx context.Context, field graphql.CollectedField, obj *models.AddedAsCollaboratorMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AddedAsCollaboratorMeta_modelPlan(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) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.AddedAsCollaboratorMeta().ModelPlan(rctx, obj) + }) + 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.(*models.ModelPlan) + fc.Result = res + return ec.marshalNModelPlan2ᚖgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐModelPlan(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AddedAsCollaboratorMeta_modelPlan(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AddedAsCollaboratorMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ModelPlan_id(ctx, field) + case "modelName": + return ec.fieldContext_ModelPlan_modelName(ctx, field) + case "abbreviation": + return ec.fieldContext_ModelPlan_abbreviation(ctx, field) + case "archived": + return ec.fieldContext_ModelPlan_archived(ctx, field) + case "createdBy": + return ec.fieldContext_ModelPlan_createdBy(ctx, field) + case "createdByUserAccount": + return ec.fieldContext_ModelPlan_createdByUserAccount(ctx, field) + case "createdDts": + return ec.fieldContext_ModelPlan_createdDts(ctx, field) + case "modifiedBy": + return ec.fieldContext_ModelPlan_modifiedBy(ctx, field) + case "modifiedByUserAccount": + return ec.fieldContext_ModelPlan_modifiedByUserAccount(ctx, field) + case "modifiedDts": + return ec.fieldContext_ModelPlan_modifiedDts(ctx, field) + case "basics": + return ec.fieldContext_ModelPlan_basics(ctx, field) + case "generalCharacteristics": + return ec.fieldContext_ModelPlan_generalCharacteristics(ctx, field) + case "participantsAndProviders": + return ec.fieldContext_ModelPlan_participantsAndProviders(ctx, field) + case "beneficiaries": + return ec.fieldContext_ModelPlan_beneficiaries(ctx, field) + case "opsEvalAndLearning": + return ec.fieldContext_ModelPlan_opsEvalAndLearning(ctx, field) + case "collaborators": + return ec.fieldContext_ModelPlan_collaborators(ctx, field) + case "documents": + return ec.fieldContext_ModelPlan_documents(ctx, field) + case "discussions": + return ec.fieldContext_ModelPlan_discussions(ctx, field) + case "payments": + return ec.fieldContext_ModelPlan_payments(ctx, field) + case "status": + return ec.fieldContext_ModelPlan_status(ctx, field) + case "isFavorite": + return ec.fieldContext_ModelPlan_isFavorite(ctx, field) + case "isCollaborator": + return ec.fieldContext_ModelPlan_isCollaborator(ctx, field) + case "crs": + return ec.fieldContext_ModelPlan_crs(ctx, field) + case "tdls": + return ec.fieldContext_ModelPlan_tdls(ctx, field) + case "prepareForClearance": + return ec.fieldContext_ModelPlan_prepareForClearance(ctx, field) + case "nameHistory": + return ec.fieldContext_ModelPlan_nameHistory(ctx, field) + case "operationalNeeds": + return ec.fieldContext_ModelPlan_operationalNeeds(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ModelPlan", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _AddedAsCollaboratorMeta_collaboratorID(ctx context.Context, field graphql.CollectedField, obj *models.AddedAsCollaboratorMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AddedAsCollaboratorMeta_collaboratorID(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) { + ctx = rctx // use context from middleware stack in children + return obj.CollaboratorID, nil + }) + 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.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AddedAsCollaboratorMeta_collaboratorID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AddedAsCollaboratorMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AddedAsCollaboratorMeta_collaborator(ctx context.Context, field graphql.CollectedField, obj *models.AddedAsCollaboratorMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AddedAsCollaboratorMeta_collaborator(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) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.AddedAsCollaboratorMeta().Collaborator(rctx, obj) + }) + 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.(*models.PlanCollaborator) + fc.Result = res + return ec.marshalNPlanCollaborator2ᚖgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐPlanCollaborator(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AddedAsCollaboratorMeta_collaborator(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AddedAsCollaboratorMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_PlanCollaborator_id(ctx, field) + case "modelPlanID": + return ec.fieldContext_PlanCollaborator_modelPlanID(ctx, field) + case "userID": + return ec.fieldContext_PlanCollaborator_userID(ctx, field) + case "userAccount": + return ec.fieldContext_PlanCollaborator_userAccount(ctx, field) + case "teamRoles": + return ec.fieldContext_PlanCollaborator_teamRoles(ctx, field) + case "createdBy": + return ec.fieldContext_PlanCollaborator_createdBy(ctx, field) + case "createdByUserAccount": + return ec.fieldContext_PlanCollaborator_createdByUserAccount(ctx, field) + case "createdDts": + return ec.fieldContext_PlanCollaborator_createdDts(ctx, field) + case "modifiedBy": + return ec.fieldContext_PlanCollaborator_modifiedBy(ctx, field) + case "modifiedByUserAccount": + return ec.fieldContext_PlanCollaborator_modifiedByUserAccount(ctx, field) + case "modifiedDts": + return ec.fieldContext_PlanCollaborator_modifiedDts(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PlanCollaborator", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _AuditChange_id(ctx context.Context, field graphql.CollectedField, obj *models.AuditChange) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AuditChange_id(ctx, field) if err != nil { @@ -60157,6 +60566,11 @@ func (ec *executionContext) _ActivityMetaData(ctx context.Context, sel ast.Selec return graphql.Null } return ec._TaggedInDiscussionReplyActivityMeta(ctx, sel, obj) + case *models.AddedAsCollaboratorMeta: + if obj == nil { + return graphql.Null + } + return ec._AddedAsCollaboratorMeta(ctx, sel, obj) default: panic(fmt.Errorf("unexpected type %T", obj)) } @@ -60434,6 +60848,132 @@ func (ec *executionContext) _ActivityMetaBaseStruct(ctx context.Context, sel ast return out } +var addedAsCollaboratorMetaImplementors = []string{"AddedAsCollaboratorMeta", "ActivityMetaData"} + +func (ec *executionContext) _AddedAsCollaboratorMeta(ctx context.Context, sel ast.SelectionSet, obj *models.AddedAsCollaboratorMeta) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, addedAsCollaboratorMetaImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AddedAsCollaboratorMeta") + case "version": + out.Values[i] = ec._AddedAsCollaboratorMeta_version(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "type": + out.Values[i] = ec._AddedAsCollaboratorMeta_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "modelPlanID": + out.Values[i] = ec._AddedAsCollaboratorMeta_modelPlanID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "modelPlan": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._AddedAsCollaboratorMeta_modelPlan(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "collaboratorID": + out.Values[i] = ec._AddedAsCollaboratorMeta_collaboratorID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "collaborator": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._AddedAsCollaboratorMeta_collaborator(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var auditChangeImplementors = []string{"AuditChange"} func (ec *executionContext) _AuditChange(ctx context.Context, sel ast.SelectionSet, obj *models.AuditChange) graphql.Marshaler { diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index b435e276b2..cd51fbf996 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -18,6 +18,18 @@ func (r *activityResolver) ActorUserAccount(ctx context.Context, obj *models.Act return UserAccountGetByIDLOADER(ctx, obj.ActorID) } +// ModelPlan is the resolver for the modelPlan field. +func (r *addedAsCollaboratorMetaResolver) ModelPlan(ctx context.Context, obj *models.AddedAsCollaboratorMeta) (*models.ModelPlan, error) { + return ModelPlanGetByIDLOADER(ctx, obj.ModelPlanID) +} + +// Collaborator is the resolver for the collaborator field. +func (r *addedAsCollaboratorMetaResolver) Collaborator(ctx context.Context, obj *models.AddedAsCollaboratorMeta) (*models.PlanCollaborator, error) { + logger := appcontext.ZLogger(ctx) + // TODO: EASI-(EASI-3945) Consider making this a data loader. The naming could be unified as well. + return FetchCollaboratorByID(logger, obj.CollaboratorID, r.store) +} + // ModelPlan is the resolver for the modelPlan field. func (r *taggedInDiscussionReplyActivityMetaResolver) ModelPlan(ctx context.Context, obj *models.TaggedInDiscussionReplyActivityMeta) (*models.ModelPlan, error) { return ModelPlanGetByIDLOADER(ctx, obj.ModelPlanID) @@ -49,6 +61,11 @@ func (r *taggedInPlanDiscussionActivityMetaResolver) Discussion(ctx context.Cont // Activity returns generated.ActivityResolver implementation. func (r *Resolver) Activity() generated.ActivityResolver { return &activityResolver{r} } +// AddedAsCollaboratorMeta returns generated.AddedAsCollaboratorMetaResolver implementation. +func (r *Resolver) AddedAsCollaboratorMeta() generated.AddedAsCollaboratorMetaResolver { + return &addedAsCollaboratorMetaResolver{r} +} + // TaggedInDiscussionReplyActivityMeta returns generated.TaggedInDiscussionReplyActivityMetaResolver implementation. func (r *Resolver) TaggedInDiscussionReplyActivityMeta() generated.TaggedInDiscussionReplyActivityMetaResolver { return &taggedInDiscussionReplyActivityMetaResolver{r} @@ -60,5 +77,6 @@ func (r *Resolver) TaggedInPlanDiscussionActivityMeta() generated.TaggedInPlanDi } type activityResolver struct{ *Resolver } +type addedAsCollaboratorMetaResolver struct{ *Resolver } type taggedInDiscussionReplyActivityMetaResolver struct{ *Resolver } type taggedInPlanDiscussionActivityMetaResolver struct{ *Resolver } diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index ef44d1c873..6adfbed777 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -165,6 +165,7 @@ func PlanCollaboratorGetByModelPlanIDLOADER(ctx context.Context, modelPlanID uui // FetchCollaboratorByID implements resolver logic to fetch a plan collaborator by ID func FetchCollaboratorByID(logger *zap.Logger, id uuid.UUID, store *storage.Store) (*models.PlanCollaborator, error) { + //TODO: EASI-(EASI-3945) Consider making this a data loader. The naming could be unified as well. collaborator, err := store.PlanCollaboratorFetchByID(id) return collaborator, err } diff --git a/pkg/graph/schema/types/activity.graphql b/pkg/graph/schema/types/activity.graphql index 00e4a3014e..4742ca721a 100644 --- a/pkg/graph/schema/types/activity.graphql +++ b/pkg/graph/schema/types/activity.graphql @@ -13,7 +13,15 @@ enum ActivityType { """ ActivityMetaData is a type that represents all the data that can be captured in an Activity """ -union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta +union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | AddedAsCollaboratorMeta +type AddedAsCollaboratorMeta { + version: Int! + type: ActivityType! + modelPlanID: UUID! + modelPlan: ModelPlan! + collaboratorID: UUID! + collaborator: PlanCollaborator! +} type TaggedInPlanDiscussionActivityMeta { version: Int! diff --git a/pkg/models/added_as_collaborator_meta.go b/pkg/models/added_as_collaborator_meta.go index 42dd827424..0185113363 100644 --- a/pkg/models/added_as_collaborator_meta.go +++ b/pkg/models/added_as_collaborator_meta.go @@ -12,14 +12,16 @@ import ( type AddedAsCollaboratorMeta struct { ActivityMetaBaseStruct modelPlanRelation + CollaboratorID uuid.UUID `json:"collaboratorID"` } // newNewPlanDiscussionActivityMeta creates a New NewPlanDiscussionActivityMeta -func newAddedAsCollaboratorMeta(modelPlanID uuid.UUID) *AddedAsCollaboratorMeta { +func newAddedAsCollaboratorMeta(modelPlanID uuid.UUID, collaboratorID uuid.UUID) *AddedAsCollaboratorMeta { version := 0 //iterate this if this type ever updates return &AddedAsCollaboratorMeta{ - ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussion, version), + ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityAddedAsCollaborator, version), modelPlanRelation: NewModelPlanRelation(modelPlanID), + CollaboratorID: collaboratorID, } } @@ -31,7 +33,7 @@ func NewAddedAsCollaboratorActivity(actorID uuid.UUID, modelPlanID uuid.UUID, co ActorID: actorID, EntityID: collaboratorID, ActivityType: ActivityAddedAsCollaborator, - MetaData: newAddedAsCollaboratorMeta(modelPlanID), + MetaData: newAddedAsCollaboratorMeta(modelPlanID, collaboratorID), } } @@ -39,14 +41,14 @@ func NewAddedAsCollaboratorActivity(actorID uuid.UUID, modelPlanID uuid.UUID, co // Value allows us to satisfy the valuer interface so we can write to the database // We need to do a specific implementation instead of relying on the implementation of the embedded struct, as that will only serialize the common data -func (d AddedAsCollaboratorMeta) Value() (driver.Value, error) { +func (cm AddedAsCollaboratorMeta) Value() (driver.Value, error) { - j, err := json.Marshal(d) + j, err := json.Marshal(cm) return j, err } // Scan implements the scanner interface so we can translate the JSONb from the db to an object in GO -func (d *AddedAsCollaboratorMeta) Scan(src interface{}) error { +func (cm *AddedAsCollaboratorMeta) Scan(src interface{}) error { if src == nil { return nil } @@ -54,7 +56,7 @@ func (d *AddedAsCollaboratorMeta) Scan(src interface{}) error { if !ok { return errors.New("type assertion .([]byte) failed") } - err := json.Unmarshal(source, d) + err := json.Unmarshal(source, cm) if err != nil { return err } diff --git a/pkg/models/model_plan_relation.go b/pkg/models/model_plan_relation.go index b59cbed758..b36af0d27a 100644 --- a/pkg/models/model_plan_relation.go +++ b/pkg/models/model_plan_relation.go @@ -23,3 +23,6 @@ func NewModelPlanRelation(modelPlanID uuid.UUID) modelPlanRelation { func (m modelPlanRelation) GetModelPlanID() uuid.UUID { return m.ModelPlanID } + +// Future Enhancement: Consider adding a ModelPlan() method like we do for user accounts etc to return a ModelPlan for any relation. +// This would remove the need to implement it in the resolvers diff --git a/src/gql/gen/graphql.ts b/src/gql/gen/graphql.ts index 7192e85bcb..ee6d6f1663 100644 --- a/src/gql/gen/graphql.ts +++ b/src/gql/gen/graphql.ts @@ -61,7 +61,7 @@ export type ActivityMetaBaseStruct = { }; /** ActivityMetaData is a type that represents all the data that can be captured in an Activity */ -export type ActivityMetaData = ActivityMetaBaseStruct | TaggedInDiscussionReplyActivityMeta | TaggedInPlanDiscussionActivityMeta; +export type ActivityMetaData = ActivityMetaBaseStruct | AddedAsCollaboratorMeta | TaggedInDiscussionReplyActivityMeta | TaggedInPlanDiscussionActivityMeta; /** ActivityType represents the possible activities that happen in application that might result in a notification */ export enum ActivityType { @@ -73,6 +73,16 @@ export enum ActivityType { TAGGED_IN_DISCUSSION_REPLY = 'TAGGED_IN_DISCUSSION_REPLY' } +export type AddedAsCollaboratorMeta = { + __typename: 'AddedAsCollaboratorMeta'; + collaborator: PlanCollaborator; + collaboratorID: Scalars['UUID']['output']; + modelPlan: ModelPlan; + modelPlanID: Scalars['UUID']['output']; + type: ActivityType; + version: Scalars['Int']['output']; +}; + export enum AgencyOrStateHelpType { NO = 'NO', OTHER = 'OTHER', @@ -3117,7 +3127,7 @@ export type GetNotificationSettingsQuery = { __typename: 'Query', currentUser: { export type GetNotificationsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetNotificationsQuery = { __typename: 'Query', currentUser: { __typename: 'CurrentUser', notifications: { __typename: 'UserNotifications', numUnreadNotifications: number, notifications: Array<{ __typename: 'UserNotification', id: UUID, isRead: boolean, inAppSent: boolean, emailSent: boolean, createdDts: Time, activity: { __typename: 'Activity', activityType: ActivityType, entityID: UUID, actorID: UUID, actorUserAccount: { __typename: 'UserAccount', commonName: string }, metaData: { __typename: 'ActivityMetaBaseStruct' } | { __typename: 'TaggedInDiscussionReplyActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, replyID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'TaggedInPlanDiscussionActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } } }> } } }; +export type GetNotificationsQuery = { __typename: 'Query', currentUser: { __typename: 'CurrentUser', notifications: { __typename: 'UserNotifications', numUnreadNotifications: number, notifications: Array<{ __typename: 'UserNotification', id: UUID, isRead: boolean, inAppSent: boolean, emailSent: boolean, createdDts: Time, activity: { __typename: 'Activity', activityType: ActivityType, entityID: UUID, actorID: UUID, actorUserAccount: { __typename: 'UserAccount', commonName: string }, metaData: { __typename: 'ActivityMetaBaseStruct' } | { __typename: 'AddedAsCollaboratorMeta' } | { __typename: 'TaggedInDiscussionReplyActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, replyID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'TaggedInPlanDiscussionActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } } }> } } }; export type GetPollNotificationsQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/src/gql/gen/types/GetNotifications.ts b/src/gql/gen/types/GetNotifications.ts index 36b8954741..26074cadf0 100644 --- a/src/gql/gen/types/GetNotifications.ts +++ b/src/gql/gen/types/GetNotifications.ts @@ -15,7 +15,7 @@ export interface GetNotifications_currentUser_notifications_notifications_activi } export interface GetNotifications_currentUser_notifications_notifications_activity_metaData_ActivityMetaBaseStruct { - __typename: "ActivityMetaBaseStruct"; + __typename: "ActivityMetaBaseStruct" | "AddedAsCollaboratorMeta"; } export interface GetNotifications_currentUser_notifications_notifications_activity_metaData_TaggedInPlanDiscussionActivityMeta_modelPlan { From 4456694187a5ed4a89dc70217a5c2e66c5dd45e5 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Mon, 11 Mar 2024 11:45:59 -0700 Subject: [PATCH 03/19] feat: ActivityAddedAsCollaboratorCreate and unit test --- pkg/notifications/activity.go | 8 +++ .../added_as_collaborator_meta.go.go | 43 ++++++++++++++++ .../added_as_collaborator_meta_test.go | 50 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 pkg/notifications/added_as_collaborator_meta.go.go create mode 100644 pkg/notifications/added_as_collaborator_meta_test.go diff --git a/pkg/notifications/activity.go b/pkg/notifications/activity.go index 81bccc657c..675c7c2bd6 100644 --- a/pkg/notifications/activity.go +++ b/pkg/notifications/activity.go @@ -93,6 +93,14 @@ func parseRawActivityMetaData(activityType models.ActivityType, rawMetaDataJSON return nil, err } return &meta, nil + case models.ActivityAddedAsCollaborator: + // Deserialize the raw JSON into AddedAsCollaboratorMeta + meta := models.AddedAsCollaboratorMeta{} + if err := json.Unmarshal(rawData, &meta); err != nil { + + return nil, err + } + return &meta, nil // Add cases for other ActivityTypes as needed diff --git a/pkg/notifications/added_as_collaborator_meta.go.go b/pkg/notifications/added_as_collaborator_meta.go.go new file mode 100644 index 0000000000..9c4013cb48 --- /dev/null +++ b/pkg/notifications/added_as_collaborator_meta.go.go @@ -0,0 +1,43 @@ +package notifications + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/cmsgov/mint-app/pkg/models" + "github.com/cmsgov/mint-app/pkg/sqlutils" +) + +// ActivityTAddedAsCollaboratorMetaCreate creates an activity for when a User is added as a collaborator for a model plan. +// It also creates all the relevant notifications +func ActivityAddedAsCollaboratorCreate( + ctx context.Context, + np sqlutils.NamedPreparer, + actorID uuid.UUID, + modelPlanID uuid.UUID, + collaboratorID uuid.UUID, + collaboratorAccountID uuid.UUID, + getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { + + activity := models.NewAddedAsCollaboratorActivity(actorID, modelPlanID, collaboratorID) + + retActivity, actErr := activityCreate(ctx, np, activity) + if actErr != nil { + return nil, actErr + } + + pref, err := getPreferencesFunc(ctx, collaboratorAccountID) + if err != nil { + return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) + } + + _, err = userNotificationCreate(ctx, np, retActivity, collaboratorAccountID, pref.AddedAsCollaborator) + if err != nil { + return nil, err + } + + return retActivity, nil + +} diff --git a/pkg/notifications/added_as_collaborator_meta_test.go b/pkg/notifications/added_as_collaborator_meta_test.go new file mode 100644 index 0000000000..5325013e40 --- /dev/null +++ b/pkg/notifications/added_as_collaborator_meta_test.go @@ -0,0 +1,50 @@ +package notifications + +import ( + "context" + + "github.com/google/uuid" + + "github.com/cmsgov/mint-app/pkg/models" +) + +func (suite *NotificationsSuite) TestActivityAddedAsCollaboratorMetaCreate() { + + // we are just choosing a valid UUID to set for the entityID + modelPlanID := uuid.New() + // we are just choosing a valid UUID to set for the entityID + collaboratorID := uuid.New() + actorID := suite.testConfigs.Principal.Account().ID + + collabEUA := "FAKE" + collabPrincipal, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, collabEUA) + suite.NoError(err) + collaboratorAccountID := collabPrincipal.Account().ID + + mockFunc := func(ctx context.Context, user_id uuid.UUID) (*models.UserNotificationPreferences, error) { + // Return mock data, all notifications enabled + return models.NewUserNotificationPreferences(user_id), nil + } + + testActivity, err := ActivityAddedAsCollaboratorCreate(suite.testConfigs.Context, suite.testConfigs.Store, actorID, modelPlanID, collaboratorID, collaboratorAccountID, mockFunc) + + suite.NoError(err) + suite.NotNil(testActivity) + suite.EqualValues(models.ActivityAddedAsCollaborator, testActivity.ActivityType) + // Assert meta data is not deserialized here + suite.Nil(testActivity.MetaData) + //Assert meta data can be deserialized + suite.NotNil(testActivity.MetaDataRaw) + meta, err := parseRawActivityMetaData(testActivity.ActivityType, testActivity.MetaDataRaw) + suite.NoError(err) + suite.NotNil(meta) + + actorNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Principal) + suite.NoError(err) + suite.EqualValues(0, actorNots.NumUnreadNotifications()) + + collabNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, collabPrincipal) + suite.NoError(err) + suite.EqualValues(1, collabNots.NumUnreadNotifications()) + +} From 3c93a51a206981c8a85a19cd8f6353ad5172b125 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Mon, 11 Mar 2024 11:52:53 -0700 Subject: [PATCH 04/19] feat: preliminary connection of added as collaborator notification --- pkg/graph/resolvers/plan_collaborator.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index 6adfbed777..66cc3a5cfe 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -2,11 +2,13 @@ package resolvers import ( "context" + "fmt" "github.com/google/uuid" "go.uber.org/zap" "github.com/cmsgov/mint-app/pkg/email" + "github.com/cmsgov/mint-app/pkg/notifications" "github.com/cmsgov/mint-app/pkg/shared/oddmail" "github.com/cmsgov/mint-app/pkg/sqlutils" "github.com/cmsgov/mint-app/pkg/storage/loaders" @@ -66,7 +68,19 @@ func CreatePlanCollaborator( return retCollaborator, nil, err } - if emailService != nil && emailTemplateService != nil { + _, notificationError := notifications.ActivityAddedAsCollaboratorCreate(ctx, np, principal.Account().ID, modelPlan.ID, retCollaborator.ID, collabAccount.ID, loaders.UserNotificationPreferencesGetByUserID) + if notificationError != nil { + return nil, nil, notificationError + } + // TODO: EASI-(EASI-3945) Double check, do we need to insure that the user who makes the model plan doesn't get notified about being added a collaborator? + // If so, we might need to revisit this logic. + pref, err := loaders.UserNotificationPreferencesGetByUserID(ctx, collabAccount.ID) + if err != nil { + return nil, nil, fmt.Errorf("issue creating collaborator, couldn't get collaborator preferences. Err: %w", err) + + } + + if emailService != nil && emailTemplateService != nil && pref.AddedAsCollaborator.SendEmail() { err = sendCollaboratorAddedEmail(emailService, emailTemplateService, addressBook, collabAccount.Email, modelPlan) if err != nil { return retCollaborator, planFavorite, err From f0f53bf36350875b34c4a40ea2845b0cfda2f581 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Mon, 11 Mar 2024 13:46:40 -0700 Subject: [PATCH 05/19] feat: add parameter to create collaborator to send a notification or not (instead of inferrring from email service) --- cmd/dbseed/resolver_wrappers.go | 1 + pkg/graph/resolvers/added_as_collaborator_email_test.go | 5 ++--- pkg/graph/resolvers/model_plan.go | 1 + pkg/graph/resolvers/plan_collaborator.go | 9 ++++++++- pkg/graph/resolvers/plan_collaborator_test.go | 3 ++- pkg/graph/resolvers/resolver_test.go | 5 +++-- pkg/graph/resolvers/schema.resolvers.go | 1 + pkg/worker/worker_test.go | 1 + 8 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cmd/dbseed/resolver_wrappers.go b/cmd/dbseed/resolver_wrappers.go index c9c652807a..de20f7b9be 100644 --- a/cmd/dbseed/resolver_wrappers.go +++ b/cmd/dbseed/resolver_wrappers.go @@ -122,6 +122,7 @@ func (s *Seeder) addPlanCollaborator( princ, true, userhelpers.GetUserInfoAccountInfoWrapperFunc(stubFetchUserInfo), + true, ) if err != nil { panic(err) diff --git a/pkg/graph/resolvers/added_as_collaborator_email_test.go b/pkg/graph/resolvers/added_as_collaborator_email_test.go index 770e2b090e..a4a027be0b 100644 --- a/pkg/graph/resolvers/added_as_collaborator_email_test.go +++ b/pkg/graph/resolvers/added_as_collaborator_email_test.go @@ -1,8 +1,6 @@ package resolvers import ( - "context" - "github.com/golang/mock/gomock" "github.com/cmsgov/mint-app/pkg/email" @@ -61,7 +59,7 @@ func (s *ResolverSuite) TestAddedAsCollaboratorEmail() { AnyTimes() _, _, err := CreatePlanCollaborator( - context.Background(), + s.testConfigs.Context, s.testConfigs.Store, s.testConfigs.Store, s.testConfigs.Logger, @@ -72,6 +70,7 @@ func (s *ResolverSuite) TestAddedAsCollaboratorEmail() { s.testConfigs.Principal, false, userhelpers.GetUserInfoAccountInfoWrapperFunc(s.stubFetchUserInfo), + true, ) s.NoError(err) mockController.Finish() diff --git a/pkg/graph/resolvers/model_plan.go b/pkg/graph/resolvers/model_plan.go index 57fe54ebaf..3de23b3fca 100644 --- a/pkg/graph/resolvers/model_plan.go +++ b/pkg/graph/resolvers/model_plan.go @@ -126,6 +126,7 @@ func ModelPlanCreate( principal, false, getAccountInformation, + false, ) if err != nil { return nil, err diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index 66cc3a5cfe..4915f0f379 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -38,7 +38,9 @@ func CreatePlanCollaborator( input *model.PlanCollaboratorCreateInput, principal authentication.Principal, checkAccess bool, - getAccountInformation userhelpers.GetAccountInfoFunc) (*models.PlanCollaborator, *models.PlanFavorite, error) { + getAccountInformation userhelpers.GetAccountInfoFunc, + createNotification bool, +) (*models.PlanCollaborator, *models.PlanFavorite, error) { //TODO make these clustered with store methods? isMacUser := false @@ -67,6 +69,11 @@ func CreatePlanCollaborator( if err != nil { return retCollaborator, nil, err } + // If a this is false, we return without creating a notification or an email. + if !createNotification { + + return retCollaborator, planFavorite, nil + } _, notificationError := notifications.ActivityAddedAsCollaboratorCreate(ctx, np, principal.Account().ID, modelPlan.ID, retCollaborator.ID, collabAccount.ID, loaders.UserNotificationPreferencesGetByUserID) if notificationError != nil { diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index 9bcd613634..63f34cb3ba 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -69,7 +69,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaborator() { AnyTimes() collaborator, _, err := CreatePlanCollaborator( - context.Background(), + suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Store, suite.testConfigs.Logger, @@ -80,6 +80,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaborator() { suite.testConfigs.Principal, false, userhelpers.GetUserInfoAccountInfoWrapperFunc(suite.stubFetchUserInfo), + true, ) account, uAccountErr := storage.UserAccountGetByUsername(suite.testConfigs.Store, collaboratorInput.UserName) diff --git a/pkg/graph/resolvers/resolver_test.go b/pkg/graph/resolvers/resolver_test.go index 395f0056cb..c875b17ec8 100644 --- a/pkg/graph/resolvers/resolver_test.go +++ b/pkg/graph/resolvers/resolver_test.go @@ -45,7 +45,7 @@ func (suite *ResolverSuite) stubFetchUserInfo(ctx context.Context, username stri func (suite *ResolverSuite) createModelPlan(planName string) *models.ModelPlan { mp, err := ModelPlanCreate( - context.Background(), + suite.testConfigs.Context, suite.testConfigs.Logger, nil, nil, @@ -156,7 +156,7 @@ func (suite *ResolverSuite) createPlanCollaborator(mp *models.ModelPlan, userNam AnyTimes() collaborator, _, err := CreatePlanCollaborator( - context.Background(), + suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Store, suite.testConfigs.Logger, @@ -167,6 +167,7 @@ func (suite *ResolverSuite) createPlanCollaborator(mp *models.ModelPlan, userNam suite.testConfigs.Principal, false, userhelpers.GetUserInfoAccountInfoWrapperFunc(suite.stubFetchUserInfo), + true, ) suite.NoError(err) return collaborator diff --git a/pkg/graph/resolvers/schema.resolvers.go b/pkg/graph/resolvers/schema.resolvers.go index fbc11a94a5..cd550d9686 100644 --- a/pkg/graph/resolvers/schema.resolvers.go +++ b/pkg/graph/resolvers/schema.resolvers.go @@ -55,6 +55,7 @@ func (r *mutationResolver) CreatePlanCollaborator(ctx context.Context, input mod principal, true, userhelpers.GetUserInfoAccountInfoWrapperFunc(r.service.FetchUserInfo), + true, ) return planCollaborator, err } diff --git a/pkg/worker/worker_test.go b/pkg/worker/worker_test.go index 23ef21a7ff..18cc43cf66 100644 --- a/pkg/worker/worker_test.go +++ b/pkg/worker/worker_test.go @@ -117,6 +117,7 @@ func (suite *WorkerSuite) createPlanCollaborator( suite.testConfigs.Principal, false, userhelpers.GetUserInfoAccountInfoWrapperFunc(suite.stubFetchUserInfo), + false, // This needs to be false because there are no data loaders on the worker package ) suite.NoError(err) return collaborator From 10d30246d639c8c6de64853b969a7e04ae87ad10 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 07:43:10 -0700 Subject: [PATCH 06/19] feat: assert that a notification is made in the collaborator unit test --- pkg/graph/resolvers/plan_collaborator.go | 6 ++---- pkg/graph/resolvers/plan_collaborator_test.go | 9 +++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index 4915f0f379..9affe1a65d 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -71,16 +71,14 @@ func CreatePlanCollaborator( } // If a this is false, we return without creating a notification or an email. if !createNotification { - return retCollaborator, planFavorite, nil } - + // Note, we could pass the get preferences function to CreatePlanCollaborator, but instead we assume that every method that calls this will have data loaders on context _, notificationError := notifications.ActivityAddedAsCollaboratorCreate(ctx, np, principal.Account().ID, modelPlan.ID, retCollaborator.ID, collabAccount.ID, loaders.UserNotificationPreferencesGetByUserID) if notificationError != nil { return nil, nil, notificationError } - // TODO: EASI-(EASI-3945) Double check, do we need to insure that the user who makes the model plan doesn't get notified about being added a collaborator? - // If so, we might need to revisit this logic. + pref, err := loaders.UserNotificationPreferencesGetByUserID(ctx, collabAccount.ID) if err != nil { return nil, nil, fmt.Errorf("issue creating collaborator, couldn't get collaborator preferences. Err: %w", err) diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index 63f34cb3ba..bf0287394d 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "golang.org/x/sync/errgroup" + "github.com/cmsgov/mint-app/pkg/notifications" "github.com/cmsgov/mint-app/pkg/shared/oddmail" "github.com/cmsgov/mint-app/pkg/storage" "github.com/cmsgov/mint-app/pkg/userhelpers" @@ -83,6 +84,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaborator() { true, ) + //Asset that making a collaborator also creates an account account, uAccountErr := storage.UserAccountGetByUsername(suite.testConfigs.Store, collaboratorInput.UserName) suite.NoError(uAccountErr) suite.NotNil(account) @@ -93,6 +95,13 @@ func (suite *ResolverSuite) TestCreatePlanCollaborator() { suite.EqualValues(pq.StringArray{string(models.TeamRoleLeadership)}, collaborator.TeamRoles) suite.EqualValues(suite.testConfigs.Principal.Account().ID, collaborator.CreatedBy) suite.Nil(collaborator.ModifiedBy) + + // Assert that a notification was generated for the collaborator + collabPrinc := getTestPrincipal(suite.testConfigs.Store, collaboratorInput.UserName) + collabNots, err := notifications.UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, collabPrinc) + suite.NoError(err) + suite.EqualValues(1, collabNots.NumUnreadNotifications()) + mockController.Finish() } From 3cdffb2c8c9e3d084ca27b02db6271c27aaa7ae3 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 08:35:49 -0700 Subject: [PATCH 07/19] feat: add a unit test to assert a notification is not created when sendNotification is false --- pkg/graph/resolvers/plan_collaborator_test.go | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index bf0287394d..6837f9cc1d 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -20,7 +20,92 @@ import ( "github.com/cmsgov/mint-app/pkg/models" ) -func (suite *ResolverSuite) TestCreatePlanCollaborator() { +func (suite *ResolverSuite) TestCreatePlanCollaboratorWithoutNotification() { + mockController := gomock.NewController(suite.T()) + mockEmailService := oddmail.NewMockEmailService(mockController) + mockEmailTemplateService := email.NewMockTemplateService(mockController) + + planName := "Plan For Milestones" + plan := suite.createModelPlan(planName) + + collaboratorInput := &model.PlanCollaboratorCreateInput{ + ModelPlanID: plan.ID, + UserName: "CLAB", + TeamRoles: []models.TeamRole{models.TeamRoleLeadership}, + } + expectedEmail := "CLAB.doe@local.fake" //comes from stubFetchUserInfo + + testTemplate, expectedSubject, expectedBody := createAddedAsCollaboratorTemplateCacheHelper(planName, plan) + + mockEmailTemplateService. + EXPECT(). + GetEmailTemplate(gomock.Eq(email.AddedAsCollaboratorTemplateName)). + Return(testTemplate, nil). + MaxTimes(0) + + addressBook := email.AddressBook{ + DefaultSender: "unit-test-execution@mint.cms.gov", + } + + emailServiceConfig := &oddmail.GoSimpleMailServiceConfig{ + ClientAddress: "http://localhost:3005", + } + + mockEmailService. + EXPECT(). + GetConfig(). + Return(emailServiceConfig). + AnyTimes() + + mockEmailService. + EXPECT(). + Send( + gomock.Eq("unit-test-execution@mint.cms.gov"), + gomock.Eq([]string{expectedEmail}), + gomock.Any(), + gomock.Eq(expectedSubject), + gomock.Any(), + gomock.Eq(expectedBody), + ). + MaxTimes(0) + + collaborator, _, err := CreatePlanCollaborator( + suite.testConfigs.Context, + suite.testConfigs.Store, + suite.testConfigs.Store, + suite.testConfigs.Logger, + mockEmailService, + mockEmailTemplateService, + addressBook, + collaboratorInput, + suite.testConfigs.Principal, + false, + userhelpers.GetUserInfoAccountInfoWrapperFunc(suite.stubFetchUserInfo), + false, + ) + + //Asset that making a collaborator also creates an account + account, uAccountErr := storage.UserAccountGetByUsername(suite.testConfigs.Store, collaboratorInput.UserName) + suite.NoError(uAccountErr) + suite.NotNil(account) + + suite.NoError(err) + suite.EqualValues(plan.ID, collaborator.ModelPlanID) + suite.EqualValues(account.ID, collaborator.UserID) + suite.EqualValues(pq.StringArray{string(models.TeamRoleLeadership)}, collaborator.TeamRoles) + suite.EqualValues(suite.testConfigs.Principal.Account().ID, collaborator.CreatedBy) + suite.Nil(collaborator.ModifiedBy) + + // Assert that a notification was not generated for the collaborator + collabPrinc := getTestPrincipal(suite.testConfigs.Store, collaboratorInput.UserName) + collabNots, err := notifications.UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, collabPrinc) + suite.NoError(err) + suite.EqualValues(0, collabNots.NumUnreadNotifications()) + + mockController.Finish() +} + +func (suite *ResolverSuite) TestCreatePlanCollaboratorWithNotification() { mockController := gomock.NewController(suite.T()) mockEmailService := oddmail.NewMockEmailService(mockController) mockEmailTemplateService := email.NewMockTemplateService(mockController) From d35014fa088618e87b7df07a6c2354c963e1e9b3 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 09:44:01 -0700 Subject: [PATCH 08/19] feat: new data loader WIP, collaborator sql files moved to SQL queries --- pkg/graph/resolvers/plan_collaborator.go | 2 +- .../SQL/plan_collaborator/create.sql | 0 .../SQL/plan_collaborator/delete.sql | 0 .../SQL/plan_collaborator/fetch_by_id.sql | 0 .../plan_collaborator/get_by_id_LOADER.sql | 19 ++++++ .../get_by_model_plan_id_LOADER.sql | 0 .../SQL/plan_collaborator/update.sql | 0 pkg/sqlqueries/plan_collaborator.go | 43 +++++++++++++ pkg/storage/loaders/data_loaders.go | 8 ++- .../loaders/plan_collaborator_loader.go | 61 +++++++++++++++++++ pkg/storage/plan_collaboratorStore.go | 44 ++++++------- 11 files changed, 153 insertions(+), 24 deletions(-) rename pkg/{storage => sqlqueries}/SQL/plan_collaborator/create.sql (100%) rename pkg/{storage => sqlqueries}/SQL/plan_collaborator/delete.sql (100%) rename pkg/{storage => sqlqueries}/SQL/plan_collaborator/fetch_by_id.sql (100%) create mode 100644 pkg/sqlqueries/SQL/plan_collaborator/get_by_id_LOADER.sql rename pkg/{storage => sqlqueries}/SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql (100%) rename pkg/{storage => sqlqueries}/SQL/plan_collaborator/update.sql (100%) create mode 100644 pkg/sqlqueries/plan_collaborator.go diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index 9affe1a65d..413b0f198c 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -168,7 +168,7 @@ func DeletePlanCollaborator(logger *zap.Logger, id uuid.UUID, principal authenti // PlanCollaboratorGetByModelPlanIDLOADER implements resolver logic to get Plan Collaborator by a model plan ID using a data loader func PlanCollaboratorGetByModelPlanIDLOADER(ctx context.Context, modelPlanID uuid.UUID) ([]*models.PlanCollaborator, error) { allLoaders := loaders.Loaders(ctx) - collabLoader := allLoaders.PlanCollaboratorLoader + collabLoader := allLoaders.PlanCollaboratorByModelPlanLoader key := loaders.NewKeyArgs() key.Args["model_plan_id"] = modelPlanID diff --git a/pkg/storage/SQL/plan_collaborator/create.sql b/pkg/sqlqueries/SQL/plan_collaborator/create.sql similarity index 100% rename from pkg/storage/SQL/plan_collaborator/create.sql rename to pkg/sqlqueries/SQL/plan_collaborator/create.sql diff --git a/pkg/storage/SQL/plan_collaborator/delete.sql b/pkg/sqlqueries/SQL/plan_collaborator/delete.sql similarity index 100% rename from pkg/storage/SQL/plan_collaborator/delete.sql rename to pkg/sqlqueries/SQL/plan_collaborator/delete.sql diff --git a/pkg/storage/SQL/plan_collaborator/fetch_by_id.sql b/pkg/sqlqueries/SQL/plan_collaborator/fetch_by_id.sql similarity index 100% rename from pkg/storage/SQL/plan_collaborator/fetch_by_id.sql rename to pkg/sqlqueries/SQL/plan_collaborator/fetch_by_id.sql diff --git a/pkg/sqlqueries/SQL/plan_collaborator/get_by_id_LOADER.sql b/pkg/sqlqueries/SQL/plan_collaborator/get_by_id_LOADER.sql new file mode 100644 index 0000000000..5e96d89232 --- /dev/null +++ b/pkg/sqlqueries/SQL/plan_collaborator/get_by_id_LOADER.sql @@ -0,0 +1,19 @@ +WITH QUERIED_IDS AS ( + /*Translate the input to a table */ + SELECT id + FROM + JSON_TO_RECORDSET(:paramTableJSON) + AS x("id" UUID) --noqa +) + +SELECT + collab.id, + collab.model_plan_id, + collab.user_id, + collab.team_roles, + collab.created_by, + collab.created_dts, + collab.modified_by, + collab.modified_dts +FROM plan_collaborator AS collab +INNER JOIN QUERIED_IDS AS qIDs ON collab.id = qIDs.id; diff --git a/pkg/storage/SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql b/pkg/sqlqueries/SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql similarity index 100% rename from pkg/storage/SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql rename to pkg/sqlqueries/SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql diff --git a/pkg/storage/SQL/plan_collaborator/update.sql b/pkg/sqlqueries/SQL/plan_collaborator/update.sql similarity index 100% rename from pkg/storage/SQL/plan_collaborator/update.sql rename to pkg/sqlqueries/SQL/plan_collaborator/update.sql diff --git a/pkg/sqlqueries/plan_collaborator.go b/pkg/sqlqueries/plan_collaborator.go new file mode 100644 index 0000000000..b4c88e7ac8 --- /dev/null +++ b/pkg/sqlqueries/plan_collaborator.go @@ -0,0 +1,43 @@ +package sqlqueries + +import _ "embed" + +//go:embed SQL/plan_collaborator/create.sql +var planCollaboratorCreateSQL string + +//go:embed SQL/plan_collaborator/update.sql +var planCollaboratorUpdateSQL string + +//go:embed SQL/plan_collaborator/delete.sql +var planCollaboratorDeleteSQL string + +//go:embed SQL/plan_collaborator/fetch_by_id.sql +var planCollaboratorFetchByIDSQL string + +//go:embed SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql +var planCollaboratorGetByModelPlanIDLoaderSQL string + +type planCollaboratorScripts struct { + // Holds the SQL query to create an PlanCollaborator + Create string + // Holds the SQL query to update an PlanCollaborator + Update string + // Holds the SQL query to delete an PlanCollaborator + Delete string + // Holds the SQL query to get a single PlanCollaborator + GetByID string + // Holds the SQL query to return all PlanCollaborators for a provided JSONArray of ids + CollectionGetByIDLoader string + // Holds the SQL query to return all PlanCollaborator for a given ModelPlanID. + CollectionGetByModelPlanIDLoader string +} + +// planCollaborator holds all the SQL scrips related to the planCollaborator Entity +var PlanCollaborator = planCollaboratorScripts{ + Create: planCollaboratorCreateSQL, + Update: planCollaboratorUpdateSQL, + Delete: planCollaboratorDeleteSQL, + GetByID: planCollaboratorFetchByIDSQL, + CollectionGetByIDLoader: planCollaboratorFetchByIDSQL, + CollectionGetByModelPlanIDLoader: planCollaboratorGetByModelPlanIDLoaderSQL, +} diff --git a/pkg/storage/loaders/data_loaders.go b/pkg/storage/loaders/data_loaders.go index dfdcba20f9..b0c6cd7c99 100644 --- a/pkg/storage/loaders/data_loaders.go +++ b/pkg/storage/loaders/data_loaders.go @@ -11,7 +11,9 @@ type DataLoaders struct { BeneficiariesLoader *WrappedDataLoader OperationsEvaluationAndLearningLoader *WrappedDataLoader PaymentLoader *WrappedDataLoader - PlanCollaboratorLoader *WrappedDataLoader + PlanCollaboratorByModelPlanLoader *WrappedDataLoader + + PlanCollaboratorByIDLoader *WrappedDataLoader DiscussionLoader *WrappedDataLoader DiscussionReplyLoader *WrappedDataLoader @@ -45,7 +47,9 @@ func NewDataLoaders(store *storage.Store) *DataLoaders { loaders.BeneficiariesLoader = newWrappedDataLoader(loaders.GetPlanBeneficiariesByModelPlanID) loaders.OperationsEvaluationAndLearningLoader = newWrappedDataLoader(loaders.GetPlanOpsEvalAndLearningByModelPlanID) loaders.PaymentLoader = newWrappedDataLoader(loaders.GetPlanPaymentsByModelPlanID) - loaders.PlanCollaboratorLoader = newWrappedDataLoader(loaders.GetPlanCollaboratorByModelPlanID) + loaders.PlanCollaboratorByModelPlanLoader = newWrappedDataLoader(loaders.GetPlanCollaboratorByModelPlanID) + + loaders.PlanCollaboratorByIDLoader = newWrappedDataLoader(loaders.getPlanCollaboratorByIDBatch) loaders.DiscussionLoader = newWrappedDataLoader(loaders.GetPlanDiscussionByModelPlanID) loaders.DiscussionReplyLoader = newWrappedDataLoader(loaders.GetDiscussionReplyByModelPlanID) diff --git a/pkg/storage/loaders/plan_collaborator_loader.go b/pkg/storage/loaders/plan_collaborator_loader.go index 3e6d3cc6d6..dbad19ea2d 100644 --- a/pkg/storage/loaders/plan_collaborator_loader.go +++ b/pkg/storage/loaders/plan_collaborator_loader.go @@ -4,11 +4,19 @@ import ( "context" "fmt" + "github.com/google/uuid" "github.com/graph-gophers/dataloader" + "github.com/samber/lo" "go.uber.org/zap" "github.com/cmsgov/mint-app/pkg/appcontext" "github.com/cmsgov/mint-app/pkg/models" + "github.com/cmsgov/mint-app/pkg/storage" +) + +const ( + // DLIDKey is the key used to store and retrieve an id + DLIDKey string = "id" ) // GetPlanCollaboratorByModelPlanID uses a DataLoader to aggreggate a SQL call and return all Plan Collaborator in one query @@ -58,3 +66,56 @@ func (loaders *DataLoaders) GetPlanCollaboratorByModelPlanID(ctx context.Context return output } + +// getPlanCollaboratorByIDBatch uses a DataLoader to aggregate a SQL call and return all Plan Collaborators for a collection of IDS in one query +func (loaders *DataLoaders) getPlanCollaboratorByIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { + jsonParams, err := CovertToJSONArray(keys) + if err != nil { + return []*dataloader.Result{{Data: nil, Error: fmt.Errorf("issue converting keys to json for PlanCollaboratorByIDLoader, %w", err)}} + } + collaborators, err := storage.PlanCollaboratorGetIDLOADER(loaders.DataReader.Store, jsonParams) + if err != nil { //TODO: EASI-(EASI-3945), this might make a problem, because the count doesn't match. Look at the user account loader for how a result is generated for each record. Maybe make a helper function? + return []*dataloader.Result{{Data: nil, Error: err}} + } + output := make([]*dataloader.Result, len(keys)) + + collaboratorByID := lo.Associate(collaborators, func(collab *models.PlanCollaborator) (string, *models.PlanCollaborator) { //TRANSLATE TO MAP + return collab.ID.String(), collab + }) + // RETURN IN THE SAME ORDER REQUESTED + + for index, key := range keys { + ck, ok := key.Raw().(KeyArgs) + if ok { + resKey := fmt.Sprint(ck.Args[DLIDKey]) + user, ok := collaboratorByID[resKey] + if ok { + output[index] = &dataloader.Result{Data: user, Error: nil} + } else { + err := fmt.Errorf("plan collaborator not found for id %s", resKey) + output[index] = &dataloader.Result{Data: nil, Error: err} + } + } else { + err := fmt.Errorf("could not retrieve key from %s", key.String()) + output[index] = &dataloader.Result{Data: nil, Error: err} + } + } + return output +} + +// GetPlanCollaboratorByID returns the Plan Collaborator data loader, loads it, and returns the correct result +func GetPlanCollaboratorByID(ctx context.Context, id uuid.UUID) (*models.PlanCollaborator, error) { + allLoaders := Loaders(ctx) + collabByIDLoader := allLoaders.PlanCollaboratorByIDLoader + key := NewKeyArgs() + + key.Args[DLIDKey] = id + thunk := collabByIDLoader.Loader.Load(ctx, key) + + result, err := thunk() + if err != nil { + return nil, err + } + return result.(*models.PlanCollaborator), nil + +} diff --git a/pkg/storage/plan_collaboratorStore.go b/pkg/storage/plan_collaboratorStore.go index 92ebc4a505..8e8a9b89f6 100644 --- a/pkg/storage/plan_collaboratorStore.go +++ b/pkg/storage/plan_collaboratorStore.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/cmsgov/mint-app/pkg/shared/utilitySQL" + "github.com/cmsgov/mint-app/pkg/sqlqueries" "github.com/cmsgov/mint-app/pkg/sqlutils" "github.com/google/uuid" @@ -14,21 +15,6 @@ import ( "github.com/cmsgov/mint-app/pkg/shared/utilityUUID" ) -//go:embed SQL/plan_collaborator/create.sql -var planCollaboratorCreateSQL string - -//go:embed SQL/plan_collaborator/update.sql -var planCollaboratorUpdateSQL string - -//go:embed SQL/plan_collaborator/delete.sql -var planCollaboratorDeleteSQL string - -//go:embed SQL/plan_collaborator/fetch_by_id.sql -var planCollaboratorFetchByIDSQL string - -//go:embed SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql -var planCollaboratorGetByModelPlanIDLoaderSQL string - // PlanCollaboratorGetByModelPlanIDLOADER returns the plan GeneralCharacteristics for a slice of model plan ids func (s *Store) PlanCollaboratorGetByModelPlanIDLOADER( _ *zap.Logger, @@ -36,8 +22,7 @@ func (s *Store) PlanCollaboratorGetByModelPlanIDLOADER( ) ([]*models.PlanCollaborator, error) { var collabSlice []*models.PlanCollaborator - - stmt, err := s.db.PrepareNamed(planCollaboratorGetByModelPlanIDLoaderSQL) + stmt, err := s.db.PrepareNamed(sqlqueries.PlanCollaborator.CollectionGetByModelPlanIDLoader) if err != nil { return nil, err } @@ -56,6 +41,23 @@ func (s *Store) PlanCollaboratorGetByModelPlanIDLOADER( return collabSlice, nil } +// PlanCollaboratorGetIDLOADER returns the plan collaborators corresponding to an array of plan collaborator IDs stored in JSON array +func PlanCollaboratorGetIDLOADER( + np sqlutils.NamedPreparer, + paramTableJSON string, +) ([]*models.PlanCollaborator, error) { + arg := map[string]interface{}{ + "paramTableJSON": paramTableJSON, + } + + retCollaborators, err := sqlutils.SelectProcedure[models.PlanCollaborator](np, sqlqueries.PlanCollaborator.CollectionGetByIDLoader, arg) + if err != nil { + return nil, fmt.Errorf("issue selecting plan collaborators by ids with the data loader, %w", err) + } + + return retCollaborators, nil +} + // PlanCollaboratorCreate creates a new plan collaborator func (s *Store) PlanCollaboratorCreate( np sqlutils.NamedPreparer, @@ -65,7 +67,7 @@ func (s *Store) PlanCollaboratorCreate( collaborator.ID = utilityUUID.ValueOrNewUUID(collaborator.ID) - stmt, err := np.PrepareNamed(planCollaboratorCreateSQL) + stmt, err := np.PrepareNamed(sqlqueries.PlanCollaborator.Create) if err != nil { return nil, err } @@ -88,7 +90,7 @@ func (s *Store) PlanCollaboratorUpdate( collaborator *models.PlanCollaborator, ) (*models.PlanCollaborator, error) { - stmt, err := s.db.PrepareNamed(planCollaboratorUpdateSQL) + stmt, err := s.db.PrepareNamed(sqlqueries.PlanCollaborator.Update) if err != nil { return nil, err } @@ -117,7 +119,7 @@ func (s *Store) PlanCollaboratorDelete( return nil, err } - stmt, err := tx.PrepareNamed(planCollaboratorDeleteSQL) + stmt, err := tx.PrepareNamed(sqlqueries.PlanCollaborator.Delete) if err != nil { return nil, err } @@ -140,7 +142,7 @@ func (s *Store) PlanCollaboratorDelete( // PlanCollaboratorFetchByID returns a plan collaborator for a given database ID, or nil if none found func (s *Store) PlanCollaboratorFetchByID(id uuid.UUID) (*models.PlanCollaborator, error) { - stmt, err := s.db.PrepareNamed(planCollaboratorFetchByIDSQL) + stmt, err := s.db.PrepareNamed(sqlqueries.PlanCollaborator.GetByID) if err != nil { return nil, err } From 342690372501d9a2c740a1922925f49853216759 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 09:51:20 -0700 Subject: [PATCH 09/19] feat: standardize collaborator method naming conventions --- cmd/dbseed/resolver_wrappers.go | 2 +- pkg/graph/resolvers/activity.resolvers.go | 2 +- .../added_as_collaborator_email_test.go | 2 +- pkg/graph/resolvers/base_struct_test.go | 2 +- pkg/graph/resolvers/model_plan.go | 2 +- pkg/graph/resolvers/plan_collaborator.go | 21 ++++++++++--------- pkg/graph/resolvers/plan_collaborator_test.go | 20 +++++++++--------- pkg/graph/resolvers/resolver_test.go | 2 +- pkg/graph/resolvers/schema.resolvers.go | 8 +++---- .../{fetch_by_id.sql => get_by_id.sql} | 0 pkg/sqlqueries/plan_collaborator.go | 8 +++---- pkg/storage/plan_collaboratorStore.go | 5 +++-- pkg/worker/worker_test.go | 2 +- 13 files changed, 39 insertions(+), 37 deletions(-) rename pkg/sqlqueries/SQL/plan_collaborator/{fetch_by_id.sql => get_by_id.sql} (100%) diff --git a/cmd/dbseed/resolver_wrappers.go b/cmd/dbseed/resolver_wrappers.go index de20f7b9be..3e925122c0 100644 --- a/cmd/dbseed/resolver_wrappers.go +++ b/cmd/dbseed/resolver_wrappers.go @@ -110,7 +110,7 @@ func (s *Seeder) addPlanCollaborator( ) *models.PlanCollaborator { princ := s.getTestPrincipalByUUID(mp.CreatedBy) - collaborator, _, err := resolvers.CreatePlanCollaborator( + collaborator, _, err := resolvers.PlanCollaboratorCreate( context.Background(), s.Config.Store, s.Config.Store, diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index cd51fbf996..6b8ee095ff 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -27,7 +27,7 @@ func (r *addedAsCollaboratorMetaResolver) ModelPlan(ctx context.Context, obj *mo func (r *addedAsCollaboratorMetaResolver) Collaborator(ctx context.Context, obj *models.AddedAsCollaboratorMeta) (*models.PlanCollaborator, error) { logger := appcontext.ZLogger(ctx) // TODO: EASI-(EASI-3945) Consider making this a data loader. The naming could be unified as well. - return FetchCollaboratorByID(logger, obj.CollaboratorID, r.store) + return PlanCollaboratorGetByID(logger, obj.CollaboratorID, r.store) } // ModelPlan is the resolver for the modelPlan field. diff --git a/pkg/graph/resolvers/added_as_collaborator_email_test.go b/pkg/graph/resolvers/added_as_collaborator_email_test.go index a4a027be0b..5a8f5ec47a 100644 --- a/pkg/graph/resolvers/added_as_collaborator_email_test.go +++ b/pkg/graph/resolvers/added_as_collaborator_email_test.go @@ -58,7 +58,7 @@ func (s *ResolverSuite) TestAddedAsCollaboratorEmail() { Return(emailServiceConfig). AnyTimes() - _, _, err := CreatePlanCollaborator( + _, _, err := PlanCollaboratorCreate( s.testConfigs.Context, s.testConfigs.Store, s.testConfigs.Store, diff --git a/pkg/graph/resolvers/base_struct_test.go b/pkg/graph/resolvers/base_struct_test.go index adc26159d4..692f5685fb 100644 --- a/pkg/graph/resolvers/base_struct_test.go +++ b/pkg/graph/resolvers/base_struct_test.go @@ -19,7 +19,7 @@ func (suite *ResolverSuite) TestModifiedByUserAccount() { nilModifiedAccount := colab.ModifiedByUserAccount(suite.testConfigs.Context) suite.Nil(nilModifiedAccount) - updatedCollab, err := UpdatePlanCollaborator( + updatedCollab, err := PlanCollaboratorUpdate( suite.testConfigs.Logger, colab.ID, []models.TeamRole{models.TeamRoleITLead}, diff --git a/pkg/graph/resolvers/model_plan.go b/pkg/graph/resolvers/model_plan.go index 3de23b3fca..833e35092b 100644 --- a/pkg/graph/resolvers/model_plan.go +++ b/pkg/graph/resolvers/model_plan.go @@ -110,7 +110,7 @@ func ModelPlanCreate( } // Create an initial collaborator for the plan - _, _, err = CreatePlanCollaborator( + _, _, err = PlanCollaboratorCreate( ctx, tx, store, diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index 413b0f198c..bd1d44e8ff 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -27,7 +27,7 @@ import ( // // A plan favorite is created for the collaborating user when the user is added as a collaborator // The transaction object does not commit or rollback in the scope of this function -func CreatePlanCollaborator( +func PlanCollaboratorCreate( ctx context.Context, np sqlutils.NamedPreparer, store *storage.Store, @@ -134,10 +134,10 @@ func sendCollaboratorAddedEmail( return nil } -// UpdatePlanCollaborator implements resolver logic to update a plan collaborator -func UpdatePlanCollaborator(logger *zap.Logger, id uuid.UUID, newRoles []models.TeamRole, principal authentication.Principal, store *storage.Store) (*models.PlanCollaborator, error) { +// PlanCollaboratorUpdate implements resolver logic to update a plan collaborator +func PlanCollaboratorUpdate(logger *zap.Logger, id uuid.UUID, newRoles []models.TeamRole, principal authentication.Principal, store *storage.Store) (*models.PlanCollaborator, error) { // Get existing collaborator - existingCollaborator, err := store.PlanCollaboratorFetchByID(id) + existingCollaborator, err := store.PlanCollaboratorGetByID(id) if err != nil { return nil, err } @@ -151,9 +151,9 @@ func UpdatePlanCollaborator(logger *zap.Logger, id uuid.UUID, newRoles []models. return store.PlanCollaboratorUpdate(logger, existingCollaborator) } -// DeletePlanCollaborator implements resolver logic to delete a plan collaborator -func DeletePlanCollaborator(logger *zap.Logger, id uuid.UUID, principal authentication.Principal, store *storage.Store) (*models.PlanCollaborator, error) { - existingCollaborator, err := store.PlanCollaboratorFetchByID(id) +// PlanCollaboratorDelete implements resolver logic to delete a plan collaborator +func PlanCollaboratorDelete(logger *zap.Logger, id uuid.UUID, principal authentication.Principal, store *storage.Store) (*models.PlanCollaborator, error) { + existingCollaborator, err := store.PlanCollaboratorGetByID(id) if err != nil { return nil, err } @@ -182,14 +182,15 @@ func PlanCollaboratorGetByModelPlanIDLOADER(ctx context.Context, modelPlanID uui return result.([]*models.PlanCollaborator), nil } -// FetchCollaboratorByID implements resolver logic to fetch a plan collaborator by ID -func FetchCollaboratorByID(logger *zap.Logger, id uuid.UUID, store *storage.Store) (*models.PlanCollaborator, error) { +// PlanCollaboratorGetByID implements resolver logic to fetch a plan collaborator by ID +func PlanCollaboratorGetByID(logger *zap.Logger, id uuid.UUID, store *storage.Store) (*models.PlanCollaborator, error) { //TODO: EASI-(EASI-3945) Consider making this a data loader. The naming could be unified as well. - collaborator, err := store.PlanCollaboratorFetchByID(id) + collaborator, err := store.PlanCollaboratorGetByID(id) return collaborator, err } // IsPlanCollaborator checks if a user is a collaborator on model plan is a favorite. func IsPlanCollaborator(logger *zap.Logger, principal authentication.Principal, store *storage.Store, modelPlanID uuid.UUID) (bool, error) { + // Future Enhancement: Consider making this a dataloader. return store.CheckIfCollaborator(logger, principal.Account().ID, modelPlanID) } diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index 6837f9cc1d..94cc1af72d 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -69,7 +69,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaboratorWithoutNotification() { ). MaxTimes(0) - collaborator, _, err := CreatePlanCollaborator( + collaborator, _, err := PlanCollaboratorCreate( suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Store, @@ -154,7 +154,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaboratorWithNotification() { ). AnyTimes() - collaborator, _, err := CreatePlanCollaborator( + collaborator, _, err := PlanCollaboratorCreate( suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Store, @@ -196,7 +196,7 @@ func (suite *ResolverSuite) TestUpdatePlanCollaborator() { suite.Nil(collaborator.ModifiedBy) suite.Nil(collaborator.ModifiedDts) - updatedCollaborator, err := UpdatePlanCollaborator( + updatedCollaborator, err := PlanCollaboratorUpdate( suite.testConfigs.Logger, collaborator.ID, []models.TeamRole{models.TeamRoleEvaluation}, @@ -223,7 +223,7 @@ func (suite *ResolverSuite) TestUpdatePlanCollaboratorLastModelLead() { suite.NoError(err) collaborator := collaborators[0] - updatedPlanCollaborator, err := UpdatePlanCollaborator( + updatedPlanCollaborator, err := PlanCollaboratorUpdate( suite.testConfigs.Logger, collaborator.ID, []models.TeamRole{models.TeamRoleEvaluation}, @@ -243,7 +243,7 @@ func (suite *ResolverSuite) TestUpdateMultipleRolesToModelLeadOnly() { []models.TeamRole{models.TeamRoleModelLead, models.TeamRoleLearning}, ) - updatedCollaborator, err := UpdatePlanCollaborator( + updatedCollaborator, err := PlanCollaboratorUpdate( suite.testConfigs.Logger, collaborator.ID, []models.TeamRole{models.TeamRoleModelLead}, @@ -260,7 +260,7 @@ func (suite *ResolverSuite) TestAttemptToAddDuplicateRoles() { plan := suite.createModelPlan("Duplicate Roles Plan") collaborator := suite.createPlanCollaborator(plan, "CLAB", []models.TeamRole{models.TeamRoleModelLead}) - updatedCollaborator, err := UpdatePlanCollaborator( + updatedCollaborator, err := PlanCollaboratorUpdate( suite.testConfigs.Logger, collaborator.ID, []models.TeamRole{models.TeamRoleModelLead, models.TeamRoleModelLead}, @@ -290,7 +290,7 @@ func (suite *ResolverSuite) TestFetchCollaboratorByID() { plan := suite.createModelPlan("Plan For Milestones") collaborator := suite.createPlanCollaborator(plan, "SCND", []models.TeamRole{models.TeamRoleLeadership}) - collaboratorByID, err := FetchCollaboratorByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) + collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) suite.NoError(err) suite.EqualValues(collaboratorByID, collaborator) } @@ -300,12 +300,12 @@ func (suite *ResolverSuite) TestDeletePlanCollaborator() { collaborator := suite.createPlanCollaborator(plan, "SCND", []models.TeamRole{models.TeamRoleLeadership}) // Delete the 2nd collaborator - deletedCollaborator, err := DeletePlanCollaborator(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Principal, suite.testConfigs.Store) + deletedCollaborator, err := PlanCollaboratorDelete(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Principal, suite.testConfigs.Store) suite.NoError(err) suite.EqualValues(deletedCollaborator, collaborator) // Ensure we get an error when we try fetch it - collaboratorByID, err := FetchCollaboratorByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) + collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) suite.Error(err) suite.Nil(collaboratorByID) } @@ -317,7 +317,7 @@ func (suite *ResolverSuite) TestDeletePlanCollaboratorLastModelLead() { suite.NoError(err) collaborator := collaborators[0] - deletedPlanCollaborator, err := DeletePlanCollaborator(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Principal, suite.testConfigs.Store) + deletedPlanCollaborator, err := PlanCollaboratorDelete(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Principal, suite.testConfigs.Store) suite.Error(err) suite.EqualValues("pq: There must be at least one MODEL_LEAD assigned to each model plan", err.Error()) suite.Nil(deletedPlanCollaborator) diff --git a/pkg/graph/resolvers/resolver_test.go b/pkg/graph/resolvers/resolver_test.go index c875b17ec8..51ae0dc87f 100644 --- a/pkg/graph/resolvers/resolver_test.go +++ b/pkg/graph/resolvers/resolver_test.go @@ -155,7 +155,7 @@ func (suite *ResolverSuite) createPlanCollaborator(mp *models.ModelPlan, userNam ). AnyTimes() - collaborator, _, err := CreatePlanCollaborator( + collaborator, _, err := PlanCollaboratorCreate( suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Store, diff --git a/pkg/graph/resolvers/schema.resolvers.go b/pkg/graph/resolvers/schema.resolvers.go index cd550d9686..eddece1efa 100644 --- a/pkg/graph/resolvers/schema.resolvers.go +++ b/pkg/graph/resolvers/schema.resolvers.go @@ -43,7 +43,7 @@ func (r *mutationResolver) CreatePlanCollaborator(ctx context.Context, input mod principal := appcontext.Principal(ctx) logger := appcontext.ZLogger(ctx) - planCollaborator, _, err := CreatePlanCollaborator( + planCollaborator, _, err := PlanCollaboratorCreate( ctx, r.store, r.store, @@ -65,7 +65,7 @@ func (r *mutationResolver) UpdatePlanCollaborator(ctx context.Context, id uuid.U principal := appcontext.Principal(ctx) logger := appcontext.ZLogger(ctx) - return UpdatePlanCollaborator(logger, id, newRoles, principal, r.store) + return PlanCollaboratorUpdate(logger, id, newRoles, principal, r.store) } // DeletePlanCollaborator is the resolver for the deletePlanCollaborator field. @@ -73,7 +73,7 @@ func (r *mutationResolver) DeletePlanCollaborator(ctx context.Context, id uuid.U principal := appcontext.Principal(ctx) logger := appcontext.ZLogger(ctx) - return DeletePlanCollaborator(logger, id, principal, r.store) + return PlanCollaboratorDelete(logger, id, principal, r.store) } // UpdatePlanBeneficiaries is the resolver for the updatePlanBeneficiaries field. @@ -516,7 +516,7 @@ func (r *queryResolver) SearchOktaUsers(ctx context.Context, searchTerm string) // PlanCollaboratorByID is the resolver for the planCollaboratorByID field. func (r *queryResolver) PlanCollaboratorByID(ctx context.Context, id uuid.UUID) (*models.PlanCollaborator, error) { logger := appcontext.ZLogger(ctx) - return FetchCollaboratorByID(logger, id, r.store) + return PlanCollaboratorGetByID(logger, id, r.store) } // TaskListSectionLocks is the resolver for the taskListSectionLocks field. diff --git a/pkg/sqlqueries/SQL/plan_collaborator/fetch_by_id.sql b/pkg/sqlqueries/SQL/plan_collaborator/get_by_id.sql similarity index 100% rename from pkg/sqlqueries/SQL/plan_collaborator/fetch_by_id.sql rename to pkg/sqlqueries/SQL/plan_collaborator/get_by_id.sql diff --git a/pkg/sqlqueries/plan_collaborator.go b/pkg/sqlqueries/plan_collaborator.go index b4c88e7ac8..cbbc72e0aa 100644 --- a/pkg/sqlqueries/plan_collaborator.go +++ b/pkg/sqlqueries/plan_collaborator.go @@ -11,8 +11,8 @@ var planCollaboratorUpdateSQL string //go:embed SQL/plan_collaborator/delete.sql var planCollaboratorDeleteSQL string -//go:embed SQL/plan_collaborator/fetch_by_id.sql -var planCollaboratorFetchByIDSQL string +//go:embed SQL/plan_collaborator/get_by_id.sql +var planCollaboratorGetByIDSQL string //go:embed SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql var planCollaboratorGetByModelPlanIDLoaderSQL string @@ -37,7 +37,7 @@ var PlanCollaborator = planCollaboratorScripts{ Create: planCollaboratorCreateSQL, Update: planCollaboratorUpdateSQL, Delete: planCollaboratorDeleteSQL, - GetByID: planCollaboratorFetchByIDSQL, - CollectionGetByIDLoader: planCollaboratorFetchByIDSQL, + GetByID: planCollaboratorGetByIDSQL, + CollectionGetByIDLoader: planCollaboratorGetByIDSQL, CollectionGetByModelPlanIDLoader: planCollaboratorGetByModelPlanIDLoaderSQL, } diff --git a/pkg/storage/plan_collaboratorStore.go b/pkg/storage/plan_collaboratorStore.go index 8e8a9b89f6..88673b87a4 100644 --- a/pkg/storage/plan_collaboratorStore.go +++ b/pkg/storage/plan_collaboratorStore.go @@ -139,8 +139,9 @@ func (s *Store) PlanCollaboratorDelete( return collaborator, nil } -// PlanCollaboratorFetchByID returns a plan collaborator for a given database ID, or nil if none found -func (s *Store) PlanCollaboratorFetchByID(id uuid.UUID) (*models.PlanCollaborator, error) { +// PlanCollaboratorGetByID returns a plan collaborator for a given database ID, or nil if none found +// Note: The dataloader method should be preferred over this method. +func (s *Store) PlanCollaboratorGetByID(id uuid.UUID) (*models.PlanCollaborator, error) { stmt, err := s.db.PrepareNamed(sqlqueries.PlanCollaborator.GetByID) if err != nil { diff --git a/pkg/worker/worker_test.go b/pkg/worker/worker_test.go index 18cc43cf66..2a17b6ef9c 100644 --- a/pkg/worker/worker_test.go +++ b/pkg/worker/worker_test.go @@ -105,7 +105,7 @@ func (suite *WorkerSuite) createPlanCollaborator( TeamRoles: teamRoles, } - collaborator, _, err := resolvers.CreatePlanCollaborator( + collaborator, _, err := resolvers.PlanCollaboratorCreate( context.Background(), suite.testConfigs.Store, suite.testConfigs.Store, From ba0a6fe4d056303380cef9a1ced29b613f1a7d20 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 10:02:30 -0700 Subject: [PATCH 10/19] feat: plan collaborator get now utilizes dataloader. Correct wiring for dataloader --- pkg/graph/resolvers/activity.resolvers.go | 4 +--- pkg/graph/resolvers/plan_collaborator.go | 8 +++----- pkg/graph/resolvers/plan_collaborator_test.go | 8 ++++---- pkg/graph/resolvers/schema.resolvers.go | 3 +-- pkg/sqlqueries/plan_collaborator.go | 5 ++++- pkg/storage/loaders/plan_collaborator_loader.go | 4 ++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 6b8ee095ff..eee757e437 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -25,9 +25,7 @@ func (r *addedAsCollaboratorMetaResolver) ModelPlan(ctx context.Context, obj *mo // Collaborator is the resolver for the collaborator field. func (r *addedAsCollaboratorMetaResolver) Collaborator(ctx context.Context, obj *models.AddedAsCollaboratorMeta) (*models.PlanCollaborator, error) { - logger := appcontext.ZLogger(ctx) - // TODO: EASI-(EASI-3945) Consider making this a data loader. The naming could be unified as well. - return PlanCollaboratorGetByID(logger, obj.CollaboratorID, r.store) + return PlanCollaboratorGetByID(ctx, obj.CollaboratorID) } // ModelPlan is the resolver for the modelPlan field. diff --git a/pkg/graph/resolvers/plan_collaborator.go b/pkg/graph/resolvers/plan_collaborator.go index bd1d44e8ff..015c9f8de0 100644 --- a/pkg/graph/resolvers/plan_collaborator.go +++ b/pkg/graph/resolvers/plan_collaborator.go @@ -182,11 +182,9 @@ func PlanCollaboratorGetByModelPlanIDLOADER(ctx context.Context, modelPlanID uui return result.([]*models.PlanCollaborator), nil } -// PlanCollaboratorGetByID implements resolver logic to fetch a plan collaborator by ID -func PlanCollaboratorGetByID(logger *zap.Logger, id uuid.UUID, store *storage.Store) (*models.PlanCollaborator, error) { - //TODO: EASI-(EASI-3945) Consider making this a data loader. The naming could be unified as well. - collaborator, err := store.PlanCollaboratorGetByID(id) - return collaborator, err +// PlanCollaboratorGetByID implements resolver logic to fetch a plan collaborator by ID. It requires the ctx to have a DataLoader embedded. +func PlanCollaboratorGetByID(ctx context.Context, id uuid.UUID) (*models.PlanCollaborator, error) { + return loaders.PlanCollaboratorByID(ctx, id) } // IsPlanCollaborator checks if a user is a collaborator on model plan is a favorite. diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index 94cc1af72d..459f46165b 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -286,11 +286,11 @@ func (suite *ResolverSuite) TestFetchCollaboratorsByModelPlanID() { // } } -func (suite *ResolverSuite) TestFetchCollaboratorByID() { +func (suite *ResolverSuite) TestPlanCollaboratorGetByID() { plan := suite.createModelPlan("Plan For Milestones") collaborator := suite.createPlanCollaborator(plan, "SCND", []models.TeamRole{models.TeamRoleLeadership}) - collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) + collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Context, collaborator.ID) suite.NoError(err) suite.EqualValues(collaboratorByID, collaborator) } @@ -305,12 +305,12 @@ func (suite *ResolverSuite) TestDeletePlanCollaborator() { suite.EqualValues(deletedCollaborator, collaborator) // Ensure we get an error when we try fetch it - collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) + collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Context, collaborator.ID) suite.Error(err) suite.Nil(collaboratorByID) } -func (suite *ResolverSuite) TestDeletePlanCollaboratorLastModelLead() { +func (suite *ResolverSuite) TestPlanCollaboratorDeleteLastModelLead() { plan := suite.createModelPlan("Plan For Milestones") collaborators, err := PlanCollaboratorGetByModelPlanIDLOADER(suite.testConfigs.Context, plan.ID) diff --git a/pkg/graph/resolvers/schema.resolvers.go b/pkg/graph/resolvers/schema.resolvers.go index eddece1efa..a4455d3dd5 100644 --- a/pkg/graph/resolvers/schema.resolvers.go +++ b/pkg/graph/resolvers/schema.resolvers.go @@ -515,8 +515,7 @@ func (r *queryResolver) SearchOktaUsers(ctx context.Context, searchTerm string) // PlanCollaboratorByID is the resolver for the planCollaboratorByID field. func (r *queryResolver) PlanCollaboratorByID(ctx context.Context, id uuid.UUID) (*models.PlanCollaborator, error) { - logger := appcontext.ZLogger(ctx) - return PlanCollaboratorGetByID(logger, id, r.store) + return PlanCollaboratorGetByID(ctx, id) } // TaskListSectionLocks is the resolver for the taskListSectionLocks field. diff --git a/pkg/sqlqueries/plan_collaborator.go b/pkg/sqlqueries/plan_collaborator.go index cbbc72e0aa..d983b53cea 100644 --- a/pkg/sqlqueries/plan_collaborator.go +++ b/pkg/sqlqueries/plan_collaborator.go @@ -14,6 +14,9 @@ var planCollaboratorDeleteSQL string //go:embed SQL/plan_collaborator/get_by_id.sql var planCollaboratorGetByIDSQL string +//go:embed SQL/plan_collaborator/get_by_id_loader.sql +var planCollaboratorGetByIDLoaderSQL string + //go:embed SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql var planCollaboratorGetByModelPlanIDLoaderSQL string @@ -38,6 +41,6 @@ var PlanCollaborator = planCollaboratorScripts{ Update: planCollaboratorUpdateSQL, Delete: planCollaboratorDeleteSQL, GetByID: planCollaboratorGetByIDSQL, - CollectionGetByIDLoader: planCollaboratorGetByIDSQL, + CollectionGetByIDLoader: planCollaboratorGetByIDLoaderSQL, CollectionGetByModelPlanIDLoader: planCollaboratorGetByModelPlanIDLoaderSQL, } diff --git a/pkg/storage/loaders/plan_collaborator_loader.go b/pkg/storage/loaders/plan_collaborator_loader.go index dbad19ea2d..3edb8893d8 100644 --- a/pkg/storage/loaders/plan_collaborator_loader.go +++ b/pkg/storage/loaders/plan_collaborator_loader.go @@ -103,8 +103,8 @@ func (loaders *DataLoaders) getPlanCollaboratorByIDBatch(ctx context.Context, ke return output } -// GetPlanCollaboratorByID returns the Plan Collaborator data loader, loads it, and returns the correct result -func GetPlanCollaboratorByID(ctx context.Context, id uuid.UUID) (*models.PlanCollaborator, error) { +// PlanCollaboratorByID returns the Plan Collaborator data loader, loads it, and returns the correct result +func PlanCollaboratorByID(ctx context.Context, id uuid.UUID) (*models.PlanCollaborator, error) { allLoaders := Loaders(ctx) collabByIDLoader := allLoaders.PlanCollaboratorByIDLoader key := NewKeyArgs() From 0d7d83fc72e54a872ea856e82ab04153532ad62f Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 12:23:17 -0700 Subject: [PATCH 11/19] feat: unit test for planCollaborator by ID data loader --- pkg/graph/resolvers/plan_collaborator_test.go | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index 459f46165b..8f8ce3fb39 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -339,7 +339,7 @@ func (suite *ResolverSuite) TestIsPlanCollaborator() { suite.EqualValues(false, isCollabFalseCase) } -func (suite *ResolverSuite) TestPlanCollaboratorDataLoader() { +func (suite *ResolverSuite) TestPlanCollaboratorGetByModelPlanIDDataLoader() { plan1 := suite.createModelPlan("Plan For Collab 1") suite.createPlanCollaborator(plan1, "SCND", []models.TeamRole{models.TeamRoleLeadership}) suite.createPlanCollaborator(plan1, "BLOB", []models.TeamRole{models.TeamRoleLeadership}) @@ -352,16 +352,16 @@ func (suite *ResolverSuite) TestPlanCollaboratorDataLoader() { g, ctx := errgroup.WithContext(suite.testConfigs.Context) g.Go(func() error { - return verifyPlanCollaboratorLoader(ctx, plan1.ID) + return verifyPlanCollaboratorGetByModelPlanIDLoader(ctx, plan1.ID) }) g.Go(func() error { - return verifyPlanCollaboratorLoader(ctx, plan2.ID) + return verifyPlanCollaboratorGetByModelPlanIDLoader(ctx, plan2.ID) }) err := g.Wait() suite.NoError(err) } -func verifyPlanCollaboratorLoader(ctx context.Context, modelPlanID uuid.UUID) error { +func verifyPlanCollaboratorGetByModelPlanIDLoader(ctx context.Context, modelPlanID uuid.UUID) error { collab, err := PlanCollaboratorGetByModelPlanIDLOADER(ctx, modelPlanID) if err != nil { @@ -373,3 +373,57 @@ func verifyPlanCollaboratorLoader(ctx context.Context, modelPlanID uuid.UUID) er } return nil } + +func (suite *ResolverSuite) TestPlanCollaboratorGetByIDDataLoader() { + plan1 := suite.createModelPlan("Plan For Collab 1") + collab1 := suite.createPlanCollaborator(plan1, "SCND", []models.TeamRole{models.TeamRoleLeadership}) + collab2 := suite.createPlanCollaborator(plan1, "BLOB", []models.TeamRole{models.TeamRoleLeadership}) + collab3 := suite.createPlanCollaborator(plan1, "MIKE", []models.TeamRole{models.TeamRoleLeadership}) + plan2 := suite.createModelPlan("Plan For Collab 2") + + collab4 := suite.createPlanCollaborator(plan2, "BIBS", []models.TeamRole{models.TeamRoleLeadership}) + collab5 := suite.createPlanCollaborator(plan2, "BOBS", []models.TeamRole{models.TeamRoleLeadership}) + collab6 := suite.createPlanCollaborator(plan2, "LUKE", []models.TeamRole{models.TeamRoleLeadership}) + + g, ctx := errgroup.WithContext(suite.testConfigs.Context) + + g.Go(func() error { + retCollab, err := PlanCollaboratorGetByID(ctx, collab1.ID) + suite.NoError(err) + suite.EqualValues(collab1.ID, retCollab.ID) + return nil + }) + g.Go(func() error { + retCollab, err := PlanCollaboratorGetByID(ctx, collab2.ID) + suite.NoError(err) + suite.EqualValues(collab2.ID, retCollab.ID) + return nil + }) + g.Go(func() error { + retCollab, err := PlanCollaboratorGetByID(ctx, collab3.ID) + suite.NoError(err) + suite.EqualValues(collab3.ID, retCollab.ID) + return nil + }) + g.Go(func() error { + retCollab, err := PlanCollaboratorGetByID(ctx, collab4.ID) + suite.NoError(err) + suite.EqualValues(collab4.ID, retCollab.ID) + return nil + }) + g.Go(func() error { + retCollab, err := PlanCollaboratorGetByID(ctx, collab5.ID) + suite.NoError(err) + suite.EqualValues(collab5.ID, retCollab.ID) + return nil + }) + g.Go(func() error { + retCollab, err := PlanCollaboratorGetByID(ctx, collab6.ID) + suite.NoError(err) + suite.EqualValues(collab6.ID, retCollab.ID) + return nil + }) + err := g.Wait() + suite.NoError(err) + +} From f0af77c1be906b4b8aeb5120a6b64991a2a1e793 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 12:31:05 -0700 Subject: [PATCH 12/19] feat: utility error handling function for data loaders --- pkg/storage/loaders/data_loader_utilities.go | 12 ++++++++++++ pkg/storage/loaders/plan_collaborator_loader.go | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 pkg/storage/loaders/data_loader_utilities.go diff --git a/pkg/storage/loaders/data_loader_utilities.go b/pkg/storage/loaders/data_loader_utilities.go new file mode 100644 index 0000000000..6d46ff85a4 --- /dev/null +++ b/pkg/storage/loaders/data_loader_utilities.go @@ -0,0 +1,12 @@ +package loaders + +import "github.com/graph-gophers/dataloader" + +// setEachOutputToError iterates through each dataloader result and sets an error message +// this is useful in situations where the same error message applies to every result +func setEachOutputToError(err error, output []*dataloader.Result) { + for _, result := range output { + result.Error = err + result.Data = nil + } +} diff --git a/pkg/storage/loaders/plan_collaborator_loader.go b/pkg/storage/loaders/plan_collaborator_loader.go index 3edb8893d8..9cf757591e 100644 --- a/pkg/storage/loaders/plan_collaborator_loader.go +++ b/pkg/storage/loaders/plan_collaborator_loader.go @@ -59,7 +59,7 @@ func (loaders *DataLoaders) GetPlanCollaboratorByModelPlanID(ctx context.Context output[index] = &dataloader.Result{Data: nil, Error: err} } } else { - err := fmt.Errorf("could not retrive key from %s", key.String()) + err := fmt.Errorf("could not retrieve key from %s", key.String()) output[index] = &dataloader.Result{Data: nil, Error: err} } } @@ -70,14 +70,16 @@ func (loaders *DataLoaders) GetPlanCollaboratorByModelPlanID(ctx context.Context // getPlanCollaboratorByIDBatch uses a DataLoader to aggregate a SQL call and return all Plan Collaborators for a collection of IDS in one query func (loaders *DataLoaders) getPlanCollaboratorByIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { jsonParams, err := CovertToJSONArray(keys) + output := make([]*dataloader.Result, len(keys)) if err != nil { - return []*dataloader.Result{{Data: nil, Error: fmt.Errorf("issue converting keys to json for PlanCollaboratorByIDLoader, %w", err)}} + setEachOutputToError(fmt.Errorf("issue converting keys to json for PlanCollaboratorByIDLoader, %w", err), output) + return output } collaborators, err := storage.PlanCollaboratorGetIDLOADER(loaders.DataReader.Store, jsonParams) if err != nil { //TODO: EASI-(EASI-3945), this might make a problem, because the count doesn't match. Look at the user account loader for how a result is generated for each record. Maybe make a helper function? - return []*dataloader.Result{{Data: nil, Error: err}} + setEachOutputToError(err, output) + return output } - output := make([]*dataloader.Result, len(keys)) collaboratorByID := lo.Associate(collaborators, func(collab *models.PlanCollaborator) (string, *models.PlanCollaborator) { //TRANSLATE TO MAP return collab.ID.String(), collab From c46a02d2deef24f2074015a4bca4c45f128c14ad Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 12:32:28 -0700 Subject: [PATCH 13/19] chore: remove irrelevant comment --- pkg/storage/loaders/plan_collaborator_loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/storage/loaders/plan_collaborator_loader.go b/pkg/storage/loaders/plan_collaborator_loader.go index 9cf757591e..e6513e0cb7 100644 --- a/pkg/storage/loaders/plan_collaborator_loader.go +++ b/pkg/storage/loaders/plan_collaborator_loader.go @@ -76,7 +76,7 @@ func (loaders *DataLoaders) getPlanCollaboratorByIDBatch(ctx context.Context, ke return output } collaborators, err := storage.PlanCollaboratorGetIDLOADER(loaders.DataReader.Store, jsonParams) - if err != nil { //TODO: EASI-(EASI-3945), this might make a problem, because the count doesn't match. Look at the user account loader for how a result is generated for each record. Maybe make a helper function? + if err != nil { setEachOutputToError(err, output) return output } From 85bb92fbe1daf01c54be48942f2ac995e6bdc8a9 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 12:46:48 -0700 Subject: [PATCH 14/19] feat: update seeder to use config context instead of background, so it has access to data loaders --- cmd/dbseed/resolver_wrappers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/dbseed/resolver_wrappers.go b/cmd/dbseed/resolver_wrappers.go index 3e925122c0..5a9f97adc1 100644 --- a/cmd/dbseed/resolver_wrappers.go +++ b/cmd/dbseed/resolver_wrappers.go @@ -28,7 +28,7 @@ func (s *Seeder) createModelPlan( princ := s.getTestPrincipalByUsername(euaID) plan, err := resolvers.ModelPlanCreate( - context.Background(), + s.Config.Context, s.Config.Logger, nil, nil, @@ -111,7 +111,7 @@ func (s *Seeder) addPlanCollaborator( princ := s.getTestPrincipalByUUID(mp.CreatedBy) collaborator, _, err := resolvers.PlanCollaboratorCreate( - context.Background(), + s.Config.Context, s.Config.Store, s.Config.Store, s.Config.Logger, @@ -275,7 +275,7 @@ func (s *Seeder) addPlanDocumentSolutionLinks( func (s *Seeder) getTestPrincipalByUsername(userName string) *authentication.ApplicationPrincipal { - userAccount, _ := userhelpers.GetOrCreateUserAccount(context.Background(), s.Config.Store, s.Config.Store, userName, true, false, userhelpers.GetOktaAccountInfoWrapperFunction(userhelpers.GetUserInfoFromOktaLocal)) + userAccount, _ := userhelpers.GetOrCreateUserAccount(s.Config.Context, s.Config.Store, s.Config.Store, userName, true, false, userhelpers.GetOktaAccountInfoWrapperFunction(userhelpers.GetUserInfoFromOktaLocal)) princ := &authentication.ApplicationPrincipal{ Username: userName, From fe6172638d5159951c058dfd17ac4416a8a87b69 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 12:48:27 -0700 Subject: [PATCH 15/19] feat: update postman collection with example of new notification type --- MINT.postman_collection.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MINT.postman_collection.json b/MINT.postman_collection.json index 17e52d8906..14d46f817f 100644 --- a/MINT.postman_collection.json +++ b/MINT.postman_collection.json @@ -2089,7 +2089,8 @@ "// pm.collectionVariables.set(\"operationalSolutionSubtaskID\"+i, subtaskID);", "// }" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2099,7 +2100,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "query currentUser {\n currentUser {\n notifications {\n numUnreadNotifications\n notifications {\n __typename\n id\n isRead\n inAppSent\n emailSent\n activity {\n activityType\n entityID\n actorID\n metaData {\n __typename\n ... on TaggedInPlanDiscussionActivityMeta{\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n content\n }\n ... on TaggedInDiscussionReplyActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n replyID\n content\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n }\n }\n}\n", + "query": "query currentUser {\n currentUser {\n account{\n username\n commonName\n }\n notifications {\n numUnreadNotifications\n notifications {\n __typename\n id\n isRead\n inAppSent\n emailSent\n activity {\n activityType\n entityID\n actorID\n metaData {\n __typename\n ... on AddedAsCollaboratorMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n }\n collaboratorID\n collaborator {\n teamRoles\n userAccount{\n commonName \n }\n } \n }\n ... on TaggedInPlanDiscussionActivityMeta{\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n content\n }\n ... on TaggedInDiscussionReplyActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n replyID\n content\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n }\n }\n}\n", "variables": "" } }, From 2bc17b6942f8afda6e277cdbb116af37bb4ba240 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 13:13:24 -0700 Subject: [PATCH 16/19] feat: move audit.change to be the last table truncated to address constraint issue in unit tests --- pkg/storage/truncate.go | 4 ++-- scripts/dev | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/storage/truncate.go b/pkg/storage/truncate.go index d243de55bd..9e2ed05012 100644 --- a/pkg/storage/truncate.go +++ b/pkg/storage/truncate.go @@ -29,10 +29,10 @@ func (s *Store) TruncateAllTablesDANGEROUS(logger *zap.Logger) error { analyzed_audit, existing_model_link, model_plan, - audit.change, user_notification, user_notification_preferences, - activity + activity, + audit.change ` _, err := s.db.Exec("TRUNCATE " + tables) diff --git a/scripts/dev b/scripts/dev index 2128439321..4db8a78b86 100755 --- a/scripts/dev +++ b/scripts/dev @@ -382,10 +382,10 @@ namespace :db do operational_need, existing_model_link, model_plan, - audit.change, user_notification, user_notification_preferences, - activity" + activity, + audit.change" puts "Cleaning database..." `echo "TRUNCATE #{tableList}" | psql` From cb66b97d86d653f91baf644eb4281a2b18bf9c5e Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 13:23:24 -0700 Subject: [PATCH 17/19] feat: incorporate truncate logic from the daily digest notification branch --- pkg/storage/truncate.go | 23 +++++++++++++++++++---- scripts/dev | 5 +++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/storage/truncate.go b/pkg/storage/truncate.go index 9e2ed05012..39905c376c 100644 --- a/pkg/storage/truncate.go +++ b/pkg/storage/truncate.go @@ -1,6 +1,8 @@ package storage import ( + "fmt" + "go.uber.org/zap" ) @@ -49,14 +51,27 @@ func (s *Store) TruncateAllTablesDANGEROUS(logger *zap.Logger) error { func removeNonSystemAccounts(s *Store) error { - script := `DELETE FROM user_account - WHERE username NOT IN - ` + scriptPreferences := `DELETE FROM user_notification_preferences + WHERE user_id IN ( + SELECT id + FROM user_account + WHERE username NOT IN %s + );` + + scriptUser := `DELETE FROM user_account + WHERE username NOT IN %s;` + systemAccounts := "( 'UNKNOWN_USER','MINT_SYSTEM')" - _, err := s.db.Exec(script + systemAccounts) + + _, err := s.db.Exec(fmt.Sprintf(scriptPreferences, systemAccounts)) if err != nil { return err } + _, err = s.db.Exec(fmt.Sprintf(scriptUser, systemAccounts)) + if err != nil { + return err + } + return nil } diff --git a/scripts/dev b/scripts/dev index 4db8a78b86..fda862168f 100755 --- a/scripts/dev +++ b/scripts/dev @@ -392,6 +392,11 @@ namespace :db do #remove all non system accounts systemAccounts = "( 'UNKNOWN_USER','MINT_SYSTEM')" + + # remove preferences + `echo "DELETE FROM user_notification_preferences WHERE user_id IN ( SELECT id FROM user_account WHERE username NOT IN #{systemAccounts} )" | psql` + + # remove accounts `echo "DELETE FROM user_account WHERE username NOT IN #{systemAccounts}" | psql` end From 273e9211f1c10729753a01819accc64927b5e4fa Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 13:31:41 -0700 Subject: [PATCH 18/19] feat: add comments to the embedded SQL files for plan collaborator --- pkg/sqlqueries/plan_collaborator.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/sqlqueries/plan_collaborator.go b/pkg/sqlqueries/plan_collaborator.go index d983b53cea..b6f652648b 100644 --- a/pkg/sqlqueries/plan_collaborator.go +++ b/pkg/sqlqueries/plan_collaborator.go @@ -2,21 +2,33 @@ package sqlqueries import _ "embed" +// planCollaboratorCreateSQL creates a Plan Collaborator entry in the database +// //go:embed SQL/plan_collaborator/create.sql var planCollaboratorCreateSQL string +// planCollaboratorUpdateSQL updates a Plan Collaborator entry in the database +// //go:embed SQL/plan_collaborator/update.sql var planCollaboratorUpdateSQL string +// planCollaboratorDeleteSQL deletes a Plan Collaborator entry in the database +// //go:embed SQL/plan_collaborator/delete.sql var planCollaboratorDeleteSQL string +// planCollaboratorGetByIDSQL returns a Plan Collaborator entry in the database. When possible, the data loader version should be preferred +// //go:embed SQL/plan_collaborator/get_by_id.sql var planCollaboratorGetByIDSQL string +// planCollaboratorGetByIDLoaderSQL returns a Collection of Plan Collaborator entries from the database. It expects a JSON array of IDs, which represent the id of the entry +// //go:embed SQL/plan_collaborator/get_by_id_loader.sql var planCollaboratorGetByIDLoaderSQL string +// planCollaboratorGetByModelPlanIDLoaderSQL returns a Collection of Plan Collaborator entries from the database. It expects a JSON array of model_plan_ids, the result can be processed to return all collaborators for various model plans +// //go:embed SQL/plan_collaborator/get_by_model_plan_id_LOADER.sql var planCollaboratorGetByModelPlanIDLoaderSQL string From ff9197ca23f6f4c7ffac6a76437513920172ddfa Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Tue, 12 Mar 2024 13:37:16 -0700 Subject: [PATCH 19/19] chore: correct embed statement capitalization --- pkg/sqlqueries/plan_collaborator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sqlqueries/plan_collaborator.go b/pkg/sqlqueries/plan_collaborator.go index b6f652648b..2800931772 100644 --- a/pkg/sqlqueries/plan_collaborator.go +++ b/pkg/sqlqueries/plan_collaborator.go @@ -24,7 +24,7 @@ var planCollaboratorGetByIDSQL string // planCollaboratorGetByIDLoaderSQL returns a Collection of Plan Collaborator entries from the database. It expects a JSON array of IDs, which represent the id of the entry // -//go:embed SQL/plan_collaborator/get_by_id_loader.sql +//go:embed SQL/plan_collaborator/get_by_id_LOADER.sql var planCollaboratorGetByIDLoaderSQL string // planCollaboratorGetByModelPlanIDLoaderSQL returns a Collection of Plan Collaborator entries from the database. It expects a JSON array of model_plan_ids, the result can be processed to return all collaborators for various model plans