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": "" } }, diff --git a/cmd/dbseed/resolver_wrappers.go b/cmd/dbseed/resolver_wrappers.go index c9c652807a..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, @@ -110,8 +110,8 @@ func (s *Seeder) addPlanCollaborator( ) *models.PlanCollaborator { princ := s.getTestPrincipalByUUID(mp.CreatedBy) - collaborator, _, err := resolvers.CreatePlanCollaborator( - context.Background(), + collaborator, _, err := resolvers.PlanCollaboratorCreate( + s.Config.Context, s.Config.Store, s.Config.Store, s.Config.Logger, @@ -122,6 +122,7 @@ func (s *Seeder) addPlanCollaborator( princ, true, userhelpers.GetUserInfoAccountInfoWrapperFunc(stubFetchUserInfo), + true, ) if err != nil { panic(err) @@ -274,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, 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..eee757e437 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -18,6 +18,16 @@ 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) { + return PlanCollaboratorGetByID(ctx, obj.CollaboratorID) +} + // 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 +59,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 +75,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/added_as_collaborator_email_test.go b/pkg/graph/resolvers/added_as_collaborator_email_test.go index 770e2b090e..5a8f5ec47a 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" @@ -60,8 +58,8 @@ func (s *ResolverSuite) TestAddedAsCollaboratorEmail() { Return(emailServiceConfig). AnyTimes() - _, _, err := CreatePlanCollaborator( - context.Background(), + _, _, err := PlanCollaboratorCreate( + 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/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 57fe54ebaf..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, @@ -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 ef44d1c873..015c9f8de0 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" @@ -25,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, @@ -36,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 @@ -65,8 +69,23 @@ 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 + } + // 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 + } + + 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 { + if emailService != nil && emailTemplateService != nil && pref.AddedAsCollaborator.SendEmail() { err = sendCollaboratorAddedEmail(emailService, emailTemplateService, addressBook, collabAccount.Email, modelPlan) if err != nil { return retCollaborator, planFavorite, err @@ -115,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 } @@ -132,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 } @@ -149,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 @@ -163,13 +182,13 @@ 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) { - collaborator, err := store.PlanCollaboratorFetchByID(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. 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 9bcd613634..8f8ce3fb39 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" @@ -19,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 := PlanCollaboratorCreate( + 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) @@ -68,8 +154,8 @@ func (suite *ResolverSuite) TestCreatePlanCollaborator() { ). AnyTimes() - collaborator, _, err := CreatePlanCollaborator( - context.Background(), + collaborator, _, err := PlanCollaboratorCreate( + suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Store, suite.testConfigs.Logger, @@ -80,8 +166,10 @@ func (suite *ResolverSuite) TestCreatePlanCollaborator() { suite.testConfigs.Principal, false, userhelpers.GetUserInfoAccountInfoWrapperFunc(suite.stubFetchUserInfo), + 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) @@ -92,6 +180,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() } @@ -101,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}, @@ -128,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}, @@ -148,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}, @@ -165,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}, @@ -191,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 := FetchCollaboratorByID(suite.testConfigs.Logger, collaborator.ID, suite.testConfigs.Store) + collaboratorByID, err := PlanCollaboratorGetByID(suite.testConfigs.Context, collaborator.ID) suite.NoError(err) suite.EqualValues(collaboratorByID, collaborator) } @@ -205,24 +300,24 @@ 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.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) 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) @@ -244,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}) @@ -257,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 { @@ -278,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) + +} diff --git a/pkg/graph/resolvers/resolver_test.go b/pkg/graph/resolvers/resolver_test.go index 395f0056cb..51ae0dc87f 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, @@ -155,8 +155,8 @@ func (suite *ResolverSuite) createPlanCollaborator(mp *models.ModelPlan, userNam ). AnyTimes() - collaborator, _, err := CreatePlanCollaborator( - context.Background(), + collaborator, _, err := PlanCollaboratorCreate( + 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..a4455d3dd5 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, @@ -55,6 +55,7 @@ func (r *mutationResolver) CreatePlanCollaborator(ctx context.Context, input mod principal, true, userhelpers.GetUserInfoAccountInfoWrapperFunc(r.service.FetchUserInfo), + true, ) return planCollaborator, err } @@ -64,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. @@ -72,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. @@ -514,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 FetchCollaboratorByID(logger, id, r.store) + return PlanCollaboratorGetByID(ctx, id) } // TaskListSectionLocks is the resolver for the taskListSectionLocks field. 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 new file mode 100644 index 0000000000..0185113363 --- /dev/null +++ b/pkg/models/added_as_collaborator_meta.go @@ -0,0 +1,65 @@ +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 + CollaboratorID uuid.UUID `json:"collaboratorID"` +} + +// newNewPlanDiscussionActivityMeta creates a New NewPlanDiscussionActivityMeta +func newAddedAsCollaboratorMeta(modelPlanID uuid.UUID, collaboratorID uuid.UUID) *AddedAsCollaboratorMeta { + version := 0 //iterate this if this type ever updates + return &AddedAsCollaboratorMeta{ + ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityAddedAsCollaborator, version), + modelPlanRelation: NewModelPlanRelation(modelPlanID), + CollaboratorID: collaboratorID, + } + +} + +// 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, collaboratorID), + } +} + +// 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 (cm AddedAsCollaboratorMeta) Value() (driver.Value, error) { + + 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 (cm *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, cm) + if err != nil { + return err + } + + return nil +} 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/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 } } 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()) + +} 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/get_by_id.sql similarity index 100% rename from pkg/storage/SQL/plan_collaborator/fetch_by_id.sql rename to pkg/sqlqueries/SQL/plan_collaborator/get_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..2800931772 --- /dev/null +++ b/pkg/sqlqueries/plan_collaborator.go @@ -0,0 +1,58 @@ +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 + +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: planCollaboratorGetByIDSQL, + CollectionGetByIDLoader: planCollaboratorGetByIDLoaderSQL, + CollectionGetByModelPlanIDLoader: planCollaboratorGetByModelPlanIDLoaderSQL, +} 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/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..e6513e0cb7 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 @@ -51,10 +59,65 @@ 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} } } 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) + output := make([]*dataloader.Result, len(keys)) + if err != nil { + 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 { + setEachOutputToError(err, output) + return output + } + + 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 +} + +// 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() + + 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..88673b87a4 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 } @@ -137,10 +139,11 @@ 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(planCollaboratorFetchByIDSQL) + stmt, err := s.db.PrepareNamed(sqlqueries.PlanCollaborator.GetByID) if err != nil { return nil, err } diff --git a/pkg/storage/truncate.go b/pkg/storage/truncate.go index d243de55bd..39905c376c 100644 --- a/pkg/storage/truncate.go +++ b/pkg/storage/truncate.go @@ -1,6 +1,8 @@ package storage import ( + "fmt" + "go.uber.org/zap" ) @@ -29,10 +31,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) @@ -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/pkg/worker/worker_test.go b/pkg/worker/worker_test.go index 23ef21a7ff..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, @@ -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 diff --git a/scripts/dev b/scripts/dev index 2128439321..fda862168f 100755 --- a/scripts/dev +++ b/scripts/dev @@ -382,16 +382,21 @@ 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` #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 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 {