diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 3e10cdc505..f15acca872 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -69,7 +69,7 @@ func (r *newModelPlanActivityMetaResolver) ModelPlan(ctx context.Context, obj *m // DataExchangeApproach is the resolver for the dataExchangeApproach field. func (r *planDataExchangeApproachCompletedActivityMetaResolver) DataExchangeApproach(ctx context.Context, obj *models.PlanDataExchangeApproachCompletedActivityMeta) (*models.PlanDataExchangeApproach, error) { logger := appcontext.ZLogger(ctx) - + //TODO: this can be replaced with a a call to // PlanDataExchangeApproachGetByModelPlanIDLoader() when model plan relation is added to the meta type return PlanDataExchangeApproachGetByID(logger, r.store, obj.DataExchangeApproachID) } diff --git a/pkg/graph/resolvers/model_plan.resolvers.go b/pkg/graph/resolvers/model_plan.resolvers.go index dbeed4f385..850d4c1231 100644 --- a/pkg/graph/resolvers/model_plan.resolvers.go +++ b/pkg/graph/resolvers/model_plan.resolvers.go @@ -69,9 +69,7 @@ func (r *modelPlanResolver) Payments(ctx context.Context, obj *models.ModelPlan) // DataExchangeApproach is the resolver for the dataExchangeApproach field. func (r *modelPlanResolver) DataExchangeApproach(ctx context.Context, obj *models.ModelPlan) (*models.PlanDataExchangeApproach, error) { - logger := appcontext.ZLogger(ctx) - - return PlanDataExchangeApproachGetByModelPlanID(logger, r.store, obj.ID) + return PlanDataExchangeApproachGetByModelPlanIDLoader(ctx, obj.ID) } // TaskListStatus is the resolver for the taskListStatus field. diff --git a/pkg/graph/resolvers/plan_data_exchange_approach.go b/pkg/graph/resolvers/plan_data_exchange_approach.go index 606e54824c..fcbcf2b9b3 100644 --- a/pkg/graph/resolvers/plan_data_exchange_approach.go +++ b/pkg/graph/resolvers/plan_data_exchange_approach.go @@ -1,6 +1,7 @@ package resolvers import ( + "context" "fmt" "time" @@ -8,6 +9,7 @@ import ( "go.uber.org/zap" "github.com/cms-enterprise/mint-app/pkg/authentication" + "github.com/cms-enterprise/mint-app/pkg/storage/loaders" "github.com/cms-enterprise/mint-app/pkg/models" "github.com/cms-enterprise/mint-app/pkg/storage" @@ -77,3 +79,8 @@ func PlanDataExchangeApproachUpdate( retDataExchangeApproach, err := storage.PlanDataExchangeApproachUpdate(store, logger, existing) return retDataExchangeApproach, err } + +// PlanDataExchangeApproachGetByModelPlanIDLoader calls a data loader to batch fetching a a plan data exchange object that corresponds to a model plan +func PlanDataExchangeApproachGetByModelPlanIDLoader(ctx context.Context, modelPlanID uuid.UUID) (*models.PlanDataExchangeApproach, error) { + return loaders.PlanDataExchangeApproach.ByModelPlanID.Load(ctx, modelPlanID) +} diff --git a/pkg/graph/resolvers/plan_data_exchange_approach_test.go b/pkg/graph/resolvers/plan_data_exchange_approach_test.go index d9e533802e..11431fbc29 100644 --- a/pkg/graph/resolvers/plan_data_exchange_approach_test.go +++ b/pkg/graph/resolvers/plan_data_exchange_approach_test.go @@ -1,6 +1,45 @@ package resolvers -import "github.com/cms-enterprise/mint-app/pkg/models" +import ( + "github.com/google/uuid" + + "github.com/cms-enterprise/mint-app/pkg/models" + "github.com/cms-enterprise/mint-app/pkg/storage/loaders" +) + +func (suite *ResolverSuite) TestPlanDataExchangeApproachGetByModelPlanIDLoader() { + + plan1 := suite.createModelPlan("model plan 1") + approach1, err := PlanDataExchangeApproachGetByModelPlanIDLoader(suite.testConfigs.Context, plan1.ID) + suite.NoError(err) + suite.EqualValues(plan1.ID, approach1.ModelPlanID) + plan2 := suite.createModelPlan("model plan 2") + + approach2, err := PlanDataExchangeApproachGetByModelPlanIDLoader(suite.testConfigs.Context, plan2.ID) + suite.NoError(err) + suite.EqualValues(plan2.ID, approach2.ModelPlanID) + plan3 := suite.createModelPlan("model plan 3") + approach3, err := PlanDataExchangeApproachGetByModelPlanIDLoader(suite.testConfigs.Context, plan3.ID) + suite.NoError(err) + suite.EqualValues(plan3.ID, approach3.ModelPlanID) + expectedResults := []loaders.KeyAndExpected[uuid.UUID, uuid.UUID]{ + {Key: plan1.ID, Expected: approach1.ID}, + {Key: plan2.ID, Expected: approach2.ID}, + {Key: plan3.ID, Expected: approach3.ID}, + } + + verifyFunc := func(data *models.PlanDataExchangeApproach, expected uuid.UUID) bool { + if suite.NotNil(data) { + return suite.EqualValues(expected, data.ID) + } + return false + + } + + loaders.VerifyLoaders[uuid.UUID, *models.PlanDataExchangeApproach, uuid.UUID](suite.testConfigs.Context, &suite.Suite, loaders.PlanDataExchangeApproach.ByModelPlanID, + expectedResults, verifyFunc) + +} func (suite *ResolverSuite) TestPlanDataExchangeApproachGetByID() { plan1 := suite.createModelPlan("model plan 1") diff --git a/pkg/sqlqueries/SQL/plan_data_exchange_approach/get_by_model_plan_id_LOADER.sql b/pkg/sqlqueries/SQL/plan_data_exchange_approach/get_by_model_plan_id_LOADER.sql new file mode 100644 index 0000000000..3c2adc769d --- /dev/null +++ b/pkg/sqlqueries/SQL/plan_data_exchange_approach/get_by_model_plan_id_LOADER.sql @@ -0,0 +1,35 @@ +WITH QUERIED_IDS AS ( + /*Translate the input to a table */ + SELECT UNNEST(CAST(:model_plan_ids AS UUID[])) AS model_plan_id +) + +SELECT + approach.id, + approach.model_plan_id, + approach.data_to_collect_from_participants, + approach.data_to_collect_from_participants_reports_details, + approach.data_to_collect_from_participants_other, + approach.data_will_not_be_collected_from_participants, + approach.data_to_collect_from_participants_note, + approach.data_to_send_to_participants, + approach.data_to_send_to_participants_note, + approach.does_need_to_make_multi_payer_data_available, + approach.anticipated_multi_payer_data_availability_use_case, + approach.does_need_to_make_multi_payer_data_available_note, + approach.does_need_to_collect_and_aggregate_multi_source_data, + approach.multi_source_data_to_collect, + approach.multi_source_data_to_collect_other, + approach.does_need_to_collect_and_aggregate_multi_source_data_note, + approach.will_implement_new_data_exchange_methods, + approach.new_data_exchange_methods_description, + approach.new_data_exchange_methods_note, + approach.additional_data_exchange_considerations_description, + approach.created_by, + approach.created_dts, + approach.modified_by, + approach.modified_dts, + approach.marked_complete_by, + approach.marked_complete_dts, + approach.status +FROM plan_data_exchange_approach AS approach +INNER JOIN QUERIED_IDS ON approach.model_plan_id = QUERIED_IDS.model_plan_id diff --git a/pkg/sqlqueries/plan_data_exchange_approach.go b/pkg/sqlqueries/plan_data_exchange_approach.go index 780856aea9..63096c9929 100644 --- a/pkg/sqlqueries/plan_data_exchange_approach.go +++ b/pkg/sqlqueries/plan_data_exchange_approach.go @@ -14,17 +14,23 @@ var planDataExchangeApproachGetByIDSQL string //go:embed SQL/plan_data_exchange_approach/get_by_model_plan_id.sql var planDataExchangeApproachGetByModelPlanIDSQL string +//go:embed SQL/plan_data_exchange_approach/get_by_model_plan_id_LOADER.sql +var planDataExchangeApproachGetByModelPlanIDLoaderSQL string + type planDataExchangeApproachScripts struct { Create string Update string GetByID string GetByModelPlanID string + //Uses a list of model_plan_ids to return a corresponding list of data exchange approach objects + GetByModelPlanIDLoader string } // PlanDataExchangeApproach houses all the SQL scripts for the plan_data_exchange_approach table var PlanDataExchangeApproach = planDataExchangeApproachScripts{ - Create: planDataExchangeApproachCreateSQL, - Update: planDataExchangeApproachUpdateSQL, - GetByID: planDataExchangeApproachGetByIDSQL, - GetByModelPlanID: planDataExchangeApproachGetByModelPlanIDSQL, + Create: planDataExchangeApproachCreateSQL, + Update: planDataExchangeApproachUpdateSQL, + GetByID: planDataExchangeApproachGetByIDSQL, + GetByModelPlanID: planDataExchangeApproachGetByModelPlanIDSQL, + GetByModelPlanIDLoader: planDataExchangeApproachGetByModelPlanIDLoaderSQL, } diff --git a/pkg/storage/loaders/loader_wrapper_test_utilities.go b/pkg/storage/loaders/loader_wrapper_test_utilities.go new file mode 100644 index 0000000000..1fcb7507eb --- /dev/null +++ b/pkg/storage/loaders/loader_wrapper_test_utilities.go @@ -0,0 +1,60 @@ +package loaders + +import ( + "context" + "fmt" + + "github.com/stretchr/testify/suite" + "golang.org/x/sync/errgroup" +) + +type KeyAndExpected[K comparable, Expected any] struct { + Key K + Expected Expected +} + +// verifyLoader is meant to be called by VerifyLoaders to validate a result set of data, and the returned results +func verifyLoader[K comparable, V any, Expected any](ctx context.Context, suite *suite.Suite, loaderWrapper LoaderWrapper[K, V], key K, expected Expected, validateResult func(V, Expected) bool) bool { + res, err := loaderWrapper.Load(ctx, key) + suite.NoError(err) + return validateResult(res, expected) +} + +// VerifyLoaders validates a data loader. It takes a loaderWrapper and a list of expected results and loads the data loader asynchronously. This ensures that the dataloader is returning the results as expected +/* +example + expectedResults := []loaders.KeyAndExpected[uuid.UUID, uuid.UUID]{ + {Key: model_plan1.ID, Expected: approach1.ID}, + {Key: model_plan2.ID, Expected: approach2.ID}, + {Key: model_plan3.ID, Expected: approach3.ID}, + } + + verifyFunc := func(data *models.PlanDataExchangeApproach, expected uuid.UUID) bool { + if suite.NotNil(data) { + return suite.EqualValues(expected, data.ID) + } + return false + + } + + loaders.VerifyLoaders[uuid.UUID, *models.PlanDataExchangeApproach, uuid.UUID](suite.testConfigs.Context, &suite.Suite, loaders.PlanDataExchangeApproach.ByModelPlanID, + expectedResults, verifyFunc) + + This example takes the model_plan_id keys, and specifies the expected values. + + +*/ +func VerifyLoaders[K comparable, V any, Expected any](ctx context.Context, suite *suite.Suite, loaderWrapper LoaderWrapper[K, V], expectedResults []KeyAndExpected[K, Expected], validateResult func(V, Expected) bool) { + g, ctx := errgroup.WithContext(ctx) + for _, expected := range expectedResults { + g.Go(func() error { + passed := verifyLoader[K, V, Expected](ctx, suite, loaderWrapper, expected.Key, expected.Expected, validateResult) + if !passed { + return fmt.Errorf("dataloader verification function failed") + } + return nil + }) + } + err := g.Wait() + suite.NoError(err) +} diff --git a/pkg/storage/loaders/plan_data_exchange_approach_loader.go b/pkg/storage/loaders/plan_data_exchange_approach_loader.go new file mode 100644 index 0000000000..b3a7281003 --- /dev/null +++ b/pkg/storage/loaders/plan_data_exchange_approach_loader.go @@ -0,0 +1,41 @@ +package loaders + +import ( + "context" + + "github.com/google/uuid" + + "github.com/cms-enterprise/mint-app/pkg/appcontext" + "github.com/cms-enterprise/mint-app/pkg/models" + "github.com/cms-enterprise/mint-app/pkg/storage" + + "github.com/graph-gophers/dataloader/v7" +) + +// planDataExchangeApproachLoaders is a struct that holds LoaderWrappers related to Plan Data Exchange Approach +type planDataExchangeApproachLoaders struct { + // ByModelPlanID Gets a plan data exchange approach record associated with a model plan by the supplied model plan id + ByModelPlanID LoaderWrapper[uuid.UUID, *models.PlanDataExchangeApproach] +} + +var PlanDataExchangeApproach = &planDataExchangeApproachLoaders{ + ByModelPlanID: NewLoaderWrapper(batchPlanDataExchangeApproachByModelPlanID), +} + +func batchPlanDataExchangeApproachByModelPlanID(ctx context.Context, modelPlanIDs []uuid.UUID) []*dataloader.Result[*models.PlanDataExchangeApproach] { + logger := appcontext.ZLogger(ctx) + loaders, err := Loaders(ctx) + if err != nil { + return errorPerEachKey[uuid.UUID, *models.PlanDataExchangeApproach](modelPlanIDs, err) + } + + data, err := storage.PlanDataExchangeApproachGetByModelPlanIDLoader(loaders.DataReader.Store, logger, modelPlanIDs) + if err != nil { + return errorPerEachKey[uuid.UUID, *models.PlanDataExchangeApproach](modelPlanIDs, err) + } + getKeyFunc := func(data *models.PlanDataExchangeApproach) uuid.UUID { + return data.ModelPlanID + } + return oneToOneDataLoaderFunc(modelPlanIDs, data, getKeyFunc) + +} diff --git a/pkg/storage/plan_data_exchange_approachStore.go b/pkg/storage/plan_data_exchange_approachStore.go index 843c74c5c7..625df7727d 100644 --- a/pkg/storage/plan_data_exchange_approachStore.go +++ b/pkg/storage/plan_data_exchange_approachStore.go @@ -4,6 +4,8 @@ import ( _ "embed" "fmt" + "github.com/lib/pq" + "github.com/cms-enterprise/mint-app/pkg/sqlqueries" "github.com/google/uuid" @@ -56,3 +58,18 @@ func PlanDataExchangeApproachGetByModelPlanID(np sqlutils.NamedPreparer, _ *zap. return sqlutils.GetProcedure[models.PlanDataExchangeApproach](np, sqlqueries.PlanDataExchangeApproach.GetByModelPlanID, utilitysql.CreateModelPlanIDQueryMap(modelPlanID)) } + +// PlanDataExchangeApproachGetByModelPlanIDLoader returns the plan basics for a slice of model plan ids +func PlanDataExchangeApproachGetByModelPlanIDLoader(np sqlutils.NamedPreparer, _ *zap.Logger, modelPlanIDs []uuid.UUID) ([]*models.PlanDataExchangeApproach, error) { + + args := map[string]interface{}{ + "model_plan_ids": pq.Array(modelPlanIDs), + } + + res, err := sqlutils.SelectProcedure[models.PlanDataExchangeApproach](np, sqlqueries.PlanDataExchangeApproach.GetByModelPlanIDLoader, args) + if err != nil { + return nil, err + } + return res, nil + +}