Skip to content

Commit

Permalink
chore: add ocl concept pagination (#233)
Browse files Browse the repository at this point in the history
Signed-off-by: Kathurima Kimathi <kathurimakimathi415@gmail.com>
  • Loading branch information
KathurimaKimathi committed Apr 27, 2023
1 parent 5d7d46b commit 431fab0
Show file tree
Hide file tree
Showing 16 changed files with 570 additions and 74 deletions.
13 changes: 13 additions & 0 deletions pkg/clinical/application/dto/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,16 @@ type Terminology struct {
System TerminologySource `json:"system"`
Name string `json:"name"`
}

// TerminologyEdge is a terminology edge
type TerminologyEdge struct {
Node Terminology
Cursor string
}

// TerminologyConnection is the terminology connection
type TerminologyConnection struct {
TotalCount int `json:"totalCount,omitempty"`
Edges []TerminologyEdge `json:"edges,omitempty"`
PageInfo PageInfo `json:"pageInfo,omitempty"`
}
2 changes: 1 addition & 1 deletion pkg/clinical/application/dto/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Pagination struct {
Before string `json:"before"`

// A flag to indicate whether to ignore the pagination parameters
// i.e the implementer will not be done with pagination
// i.e the implementer will not perform/do pagination if this flag is true
Skip bool `json:"skip"`
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/clinical/domain/concept.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ type Concept struct {
VersionURL string `mapstructure:"version_url" json:"version_url"`
VersionsURL string `mapstructure:"versions_url" json:"versions_url"`
}

// ConceptPage models the output of ocl concepts with pagination
type ConceptPage struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []*Concept `json:"results"`
}
2 changes: 1 addition & 1 deletion pkg/clinical/infrastructure/infrastructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type ServiceOCL interface {
ctx context.Context, org string, source string, verbose bool, q *string,
sortAsc *string, sortDesc *string, conceptClass *string, dataType *string,
locale *string, includeRetired *bool,
includeMappings *bool, includeInverseMappings *bool) ([]*domain.Concept, error)
includeMappings *bool, includeInverseMappings *bool, paginationInput *dto.Pagination) (*domain.ConceptPage, error)
GetConcept(
ctx context.Context, org string, source string, concept string,
includeMappings bool, includeInverseMappings bool) (*domain.Concept, error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/url"

"github.com/savannahghi/clinical/pkg/clinical/application/dto"
"github.com/savannahghi/clinical/pkg/clinical/domain"
)

Expand All @@ -16,7 +17,7 @@ type FakeOCL struct {
ctx context.Context, org string, source string, verbose bool, q *string,
sortAsc *string, sortDesc *string, conceptClass *string, dataType *string,
locale *string, includeRetired *bool,
includeMappings *bool, includeInverseMappings *bool) ([]*domain.Concept, error)
includeMappings *bool, includeInverseMappings *bool, paginationInput *dto.Pagination) (*domain.ConceptPage, error)
MockGetConceptFn func(
ctx context.Context, org string, source string, concept string,
includeMappings bool, includeInverseMappings bool) (*domain.Concept, error)
Expand All @@ -30,15 +31,22 @@ func NewFakeOCLMock() *FakeOCL {
StatusCode: 200,
}, nil
},
MockListConceptsFn: func(ctx context.Context, org, source string, verbose bool, q, sortAsc, sortDesc, conceptClass, dataType, locale *string, includeRetired, includeMappings, includeInverseMappings *bool) ([]*domain.Concept, error) {
return []*domain.Concept{
{
ConceptClass: "CIEL",
DataType: "N/A",
DisplayLocale: "en/us",
DisplayName: "test",
ExternalID: "1234",
ID: "1234",
MockListConceptsFn: func(ctx context.Context, org, source string, verbose bool, q, sortAsc, sortDesc, conceptClass, dataType, locale *string, includeRetired, includeMappings, includeInverseMappings *bool, paginationInput *dto.Pagination) (*domain.ConceptPage, error) {
next := "https://api.openconceptlab.org/orgs/CIEL/sources/CIEL/concepts/?limit=2&page=2&q=Eggs&verbose=true"
previous := "https://api.openconceptlab.org/orgs/CIEL/sources/CIEL/concepts/?limit=2&page=1&q=Eggs&verbose=true"
return &domain.ConceptPage{
Count: 1,
Next: &next,
Previous: &previous,
Results: []*domain.Concept{
{
ConceptClass: "CIEL",
DataType: "N/A",
DisplayLocale: "en/us",
DisplayName: "test",
ExternalID: "1234",
ID: "1234",
},
},
}, nil
},
Expand All @@ -62,8 +70,8 @@ func (o *FakeOCL) ListConcepts(
ctx context.Context, org string, source string, verbose bool, q *string,
sortAsc *string, sortDesc *string, conceptClass *string, dataType *string,
locale *string, includeRetired *bool,
includeMappings *bool, includeInverseMappings *bool) ([]*domain.Concept, error) {
return o.MockListConceptsFn(ctx, org, source, verbose, q, sortAsc, sortDesc, conceptClass, dataType, locale, includeRetired, includeMappings, includeInverseMappings)
includeMappings *bool, includeInverseMappings *bool, paginationInput *dto.Pagination) (*domain.ConceptPage, error) {
return o.MockListConceptsFn(ctx, org, source, verbose, q, sortAsc, sortDesc, conceptClass, dataType, locale, includeRetired, includeMappings, includeInverseMappings, paginationInput)
}

// GetConcept is a mock implementation of getting a concept
Expand Down
48 changes: 44 additions & 4 deletions pkg/clinical/infrastructure/services/openconceptlab/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/mitchellh/mapstructure"
"github.com/savannahghi/clinical/pkg/clinical/application/dto"
"github.com/savannahghi/clinical/pkg/clinical/domain"
"github.com/savannahghi/serverutils"
)
Expand Down Expand Up @@ -136,14 +137,22 @@ func (s Service) ListConcepts(
_ context.Context, org string, source string, verbose bool, q *string,
sortAsc *string, sortDesc *string, conceptClass *string, dataType *string,
locale *string, includeRetired *bool,
includeMappings *bool, includeInverseMappings *bool) ([]*domain.Concept, error) {
includeMappings *bool, includeInverseMappings *bool, paginationInput *dto.Pagination) (*domain.ConceptPage, error) {
s.enforcePreconditions()

path := fmt.Sprintf("orgs/%s/sources/%s/concepts", org, source)

params := url.Values{}
params.Add("verbose", strconv.FormatBool(verbose))

if paginationInput.After != "" {
params.Add("page", paginationInput.After)
} else {
params.Add("page", "1")
}

params.Add("limit", strconv.Itoa(*paginationInput.First))

if q != nil {
params.Add("q", *q)
}
Expand Down Expand Up @@ -204,7 +213,38 @@ func (s Service) ListConcepts(
"unable to marshal OCL get concept response %s to JSON: %w", string(data), err)
}

var concepts []*domain.Concept
totalCount, err := strconv.Atoi(resp.Header.Get("num_found"))
if err != nil {
return nil, err
}

conceptsPage := &domain.ConceptPage{
Count: totalCount,
}

nextConcept := resp.Header.Get("next")

if nextConcept != "" {
params, err := url.ParseQuery(nextConcept)
if err != nil {
return nil, fmt.Errorf("server unable to parse url params: %w", err)
}

cursor := params["page"][0]
conceptsPage.Next = &cursor
}

previousConcept := resp.Header.Get("previous")

if previousConcept != "" {
params, err := url.ParseQuery(previousConcept)
if err != nil {
return nil, fmt.Errorf("server unable to parse url params: %w", err)
}

cursor := params["page"][0]
conceptsPage.Previous = &cursor
}

for _, terminologyConcept := range terminologyConcepts {
var concept *domain.Concept
Expand All @@ -214,8 +254,8 @@ func (s Service) ListConcepts(
return nil, err
}

concepts = append(concepts, concept)
conceptsPage.Results = append(conceptsPage.Results, concept)
}

return concepts, nil
return conceptsPage, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/jarcoal/httpmock"
"github.com/savannahghi/clinical/pkg/clinical/application/dto"
"github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/openconceptlab"
)

Expand Down Expand Up @@ -101,7 +102,9 @@ func toPointer[S any](s S) *S {
}

func TestService_ListConcepts(t *testing.T) {

limit := 10
next := "https://api.openconceptlab.org/orgs/CIEL/sources/CIEL/concepts/?limit=2&page=2&q=Eggs&verbose=true"
previous := "https://api.openconceptlab.org/orgs/CIEL/sources/CIEL/concepts/?limit=2&page=1&q=Eggs&verbose=true"
type args struct {
ctx context.Context
org string
Expand All @@ -116,6 +119,7 @@ func TestService_ListConcepts(t *testing.T) {
includeRetired *bool
includeMappings *bool
includeInverseMappings *bool
paginationInput *dto.Pagination
}
tests := []struct {
name string
Expand All @@ -138,6 +142,9 @@ func TestService_ListConcepts(t *testing.T) {
includeRetired: toPointer(true),
includeMappings: toPointer(true),
includeInverseMappings: toPointer(true),
paginationInput: &dto.Pagination{
First: &limit,
},
},
wantErr: false,
},
Expand All @@ -150,7 +157,7 @@ func TestService_ListConcepts(t *testing.T) {
if tt.name == "happy case: list concepts" {
httpmock.RegisterResponder(http.MethodGet, "/orgs/CIEL/sources/CIEL/concepts/",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, []map[string]interface{}{
response, err := httpmock.NewJsonResponse(200, []map[string]interface{}{
{
"concept_class": "Diagnosis",
"datatype": "N/A",
Expand Down Expand Up @@ -178,13 +185,18 @@ func TestService_ListConcepts(t *testing.T) {
},
})

response.Header.Set("num_found", "1")
response.Header.Set("next", next)
response.Header.Set("previous", previous)

return response, err
},
)
}

s := openconceptlab.NewServiceOCL()

got, err := s.ListConcepts(tt.args.ctx, tt.args.org, tt.args.source, tt.args.verbose, tt.args.q, tt.args.sortAsc, tt.args.sortDesc, tt.args.conceptClass, tt.args.dataType, tt.args.locale, tt.args.includeRetired, tt.args.includeMappings, tt.args.includeInverseMappings)
got, err := s.ListConcepts(tt.args.ctx, tt.args.org, tt.args.source, tt.args.verbose, tt.args.q, tt.args.sortAsc, tt.args.sortDesc, tt.args.conceptClass, tt.args.dataType, tt.args.locale, tt.args.includeRetired, tt.args.includeMappings, tt.args.includeInverseMappings, tt.args.paginationInput)
if (err != nil) != tt.wantErr {
t.Errorf("Service.ListConcepts() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down
2 changes: 1 addition & 1 deletion pkg/clinical/presentation/graph/clinical.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extend type Query {
getPatientWeightEntries(patientID: String!): [Observation!]

# Allergy
searchAllergy(name: String!): [Terminology]
searchAllergy(name: String!, pagination: Pagination!): TerminologyConnection
getAllergy(id: ID!): Allergy!
listPatientAllergies(patientID: ID!, pagination:Pagination!): AllergyConnection
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/clinical/presentation/graph/clinical.resolvers.go

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

Loading

0 comments on commit 431fab0

Please sign in to comment.