diff --git a/api/api.go b/api/api.go index 4c32ce6c..1b042e5b 100644 --- a/api/api.go +++ b/api/api.go @@ -44,7 +44,7 @@ var _ ServerInterface = (*Wrapper)(nil) type Wrapper struct { APIAuth *Auth ACL *acl.Repository - NutsClient nutsClient.HTTPClient + NutsClient *nutsClient.HTTPClient CustomerRepository customers.Repository PatientRepository patients.Repository ReportRepository reports.Repository diff --git a/api/api.yaml b/api/api.yaml index d0bd745f..a26ec05a 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -593,32 +593,42 @@ paths: $ref: "#/components/schemas/Patient" /private/network/discovery: - get: + # Search has become a POST instead of GET, since it uses an object (query) as input, + # which OpenAPI Codegenerator does not handle properly for GET operations. + post: description: Searches for other care organizations on Nuts Network. operationId: searchOrganizations - parameters: - - name: query - in: query - description: Keyword for finding care organizations. - required: true - schema: - type: string - - name: didServiceType - in: query - description: > - Filters other care organizations on the Nuts Network on service, only returning care organizations have a service in their DID Document which' type matches the given didServiceType and not including your own. - If not supplied, care organizations aren't filtered on service. - required: false - schema: - type: string - - name: discoveryServiceType - in: query - description: > - Filters other care organizations on the Nuts Network on service, only returning care organizations that registered for the given service at a discovery server. - It false, it will search on all Discovery Services. - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - query + properties: + query: + type: object + additionalProperties: + type: string + description: > + The search query, key-value string properties. E.g.: + 'credentialSubject.organization.name: Ziekenhuis' + discoveryServiceID: + description: > + Filters other care organizations on the Nuts Network, only returning care organizations that registered for the given Discovery Service. + It false, it will search on all Discovery Services. + type: string + didServiceType: + description: > + Filters other care organizations on the Nuts Network on service, only returning care organizations have a service in their DID Document which' type matches the given didServiceType and not including your own. + If not supplied, care organizations aren't filtered on service. + type: string + excludeOwn: + description: > + If true, the current care organization is excluded from the search results. + Defaults to false. + type: boolean responses: 200: description: Search successful. @@ -1023,6 +1033,7 @@ components: - name - city - did + - discoveryServices properties: name: description: Name of the care organization. @@ -1033,6 +1044,12 @@ components: did: description: Decentralized Identifier which uniquely identifies the care organization on the Nuts Network. type: string + discoveryServices: + description: > + List of Discovery Services the care organization is registered with. + type: array + items: + type: string CreateTransferNegotiationRequest: description: An request object to create a new transfer negotiation. type: object diff --git a/api/discovery.go b/api/discovery.go deleted file mode 100644 index 12248770..00000000 --- a/api/discovery.go +++ /dev/null @@ -1,30 +0,0 @@ -package api - -import ( - "net/http" - "strings" - - "github.com/labstack/echo/v4" - "github.com/nuts-foundation/nuts-demo-ehr/domain/types" -) - -type SearchDiscoveryServiceParams = types.SearchDiscoveryServiceParams - -func (w Wrapper) SearchDiscoveryService(ctx echo.Context, params types.SearchDiscoveryServiceParams) error { - request := types.SearchDiscoveryServiceJSONBody{} - if err := ctx.Bind(&request); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err) - } - // Make sure we search with wild cards - for key, value := range request { - if len(value) > 0 && !strings.HasSuffix(value, "*") { - request[key] = value + "*" - } - } - - result, err := w.NutsClient.SearchDiscoveryService(ctx.Request().Context(), request, params.Service) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - return ctx.JSON(http.StatusOK, result) -} diff --git a/api/generated.go b/api/generated.go index 2b7da4e6..32445d24 100644 --- a/api/generated.go +++ b/api/generated.go @@ -80,8 +80,8 @@ type ServerInterface interface { // (POST /private/episode/{episodeID}/collaboration) CreateCollaboration(ctx echo.Context, episodeID string) error - // (GET /private/network/discovery) - SearchOrganizations(ctx echo.Context, params SearchOrganizationsParams) error + // (POST /private/network/discovery) + SearchOrganizations(ctx echo.Context) error // (GET /private/network/inbox) GetInbox(ctx echo.Context) error @@ -484,31 +484,8 @@ func (w *ServerInterfaceWrapper) SearchOrganizations(ctx echo.Context) error { ctx.Set(BearerAuthScopes, []string{}) - // Parameter object where we will unmarshal all parameters from the context - var params SearchOrganizationsParams - // ------------- Required query parameter "query" ------------- - - err = runtime.BindQueryParameter("form", true, true, "query", ctx.QueryParams(), ¶ms.Query) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter query: %s", err)) - } - - // ------------- Optional query parameter "didServiceType" ------------- - - err = runtime.BindQueryParameter("form", true, false, "didServiceType", ctx.QueryParams(), ¶ms.DidServiceType) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter didServiceType: %s", err)) - } - - // ------------- Optional query parameter "discoveryServiceType" ------------- - - err = runtime.BindQueryParameter("form", true, false, "discoveryServiceType", ctx.QueryParams(), ¶ms.DiscoveryServiceType) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter discoveryServiceType: %s", err)) - } - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.SearchOrganizations(ctx, params) + err = w.Handler.SearchOrganizations(ctx) return err } @@ -947,7 +924,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/private/episode/:episodeID", wrapper.GetEpisode) router.GET(baseURL+"/private/episode/:episodeID/collaboration", wrapper.GetCollaboration) router.POST(baseURL+"/private/episode/:episodeID/collaboration", wrapper.CreateCollaboration) - router.GET(baseURL+"/private/network/discovery", wrapper.SearchOrganizations) + router.POST(baseURL+"/private/network/discovery", wrapper.SearchOrganizations) router.GET(baseURL+"/private/network/inbox", wrapper.GetInbox) router.GET(baseURL+"/private/network/inbox/info", wrapper.GetInboxInfo) router.GET(baseURL+"/private/network/patient", wrapper.GetRemotePatient) diff --git a/api/registry.go b/api/registry.go index 3e17202c..067049cd 100644 --- a/api/registry.go +++ b/api/registry.go @@ -7,28 +7,33 @@ import ( "github.com/nuts-foundation/nuts-demo-ehr/domain/types" ) -type SearchOrganizationsParams = types.SearchOrganizationsParams - -func (w Wrapper) SearchOrganizations(ctx echo.Context, params SearchOrganizationsParams) error { +func (w Wrapper) SearchOrganizations(ctx echo.Context) error { customer, err := w.getCustomer(ctx) if err != nil { return err } - organizations, err := w.NutsClient.SearchService(ctx.Request().Context(), params.Query, params.DiscoveryServiceType, params.DidServiceType) + var request types.SearchOrganizationsJSONRequestBody + if err := ctx.Bind(&request); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + organizations, err := w.NutsClient.SearchDiscoveryService(ctx.Request().Context(), request.Query, request.DiscoveryServiceID, request.DidServiceType) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } - var results = make([]types.Organization, 0) - + var results = make(map[string]types.Organization, 0) for _, organization := range organizations { // Hide our own organization - if organization.ID == *customer.Did { + if request.ExcludeOwn != nil && *request.ExcludeOwn && organization.ID == *customer.Did { continue } - - results = append(results, types.FromNutsOrganization(organization)) + current, exists := results[organization.ID] + if !exists { + current = types.FromNutsOrganization(organization.NutsOrganization) + } + current.DiscoveryServices = append(current.DiscoveryServices, organization.ServiceID) + results[organization.ID] = current } return ctx.JSON(http.StatusOK, results) diff --git a/domain/fhir/eoverdracht/converters.go b/domain/fhir/eoverdracht/converters.go index bcb7de35..335b32e9 100644 --- a/domain/fhir/eoverdracht/converters.go +++ b/domain/fhir/eoverdracht/converters.go @@ -10,7 +10,6 @@ import ( "github.com/monarko/fhirgo/STU3/resources" "github.com/nuts-foundation/nuts-demo-ehr/domain/fhir" "github.com/nuts-foundation/nuts-demo-ehr/domain/types" - types2 "github.com/oapi-codegen/runtime/types" "github.com/tidwall/gjson" ) diff --git a/domain/fhir/zorginzage/convertors.go b/domain/fhir/zorginzage/convertors.go index da5d7940..8274757f 100644 --- a/domain/fhir/zorginzage/convertors.go +++ b/domain/fhir/zorginzage/convertors.go @@ -6,7 +6,6 @@ import ( "github.com/nuts-foundation/nuts-demo-ehr/domain/fhir" "github.com/nuts-foundation/nuts-demo-ehr/domain/types" - types2 "github.com/oapi-codegen/runtime/types" ) func ToEpisode(episode *fhir.EpisodeOfCare) *types.Episode { diff --git a/domain/transfer/sender/sql_repository.go b/domain/transfer/sender/sql_repository.go index 4fbb3d96..c4840c20 100644 --- a/domain/transfer/sender/sql_repository.go +++ b/domain/transfer/sender/sql_repository.go @@ -15,7 +15,6 @@ import ( sqlUtil "github.com/nuts-foundation/nuts-demo-ehr/sql" "github.com/jmoiron/sqlx" - openapi_types "github.com/oapi-codegen/runtime/types" ) type sqlTransfer struct { diff --git a/domain/types/generated_types.go b/domain/types/generated_types.go index ec951315..a8f837f5 100644 --- a/domain/types/generated_types.go +++ b/domain/types/generated_types.go @@ -235,6 +235,9 @@ type Organization struct { // Did Decentralized Identifier which uniquely identifies the care organization on the Nuts Network. Did string `json:"did"` + // DiscoveryServices List of Discovery Services the care organization is registered with. + DiscoveryServices []string `json:"discoveryServices"` + // Name Name of the care organization. Name string `json:"name"` } @@ -451,16 +454,19 @@ type TransferRequest struct { TransferDate *openapi_types.Date `json:"transferDate,omitempty"` } -// SearchOrganizationsParams defines parameters for SearchOrganizations. -type SearchOrganizationsParams struct { - // Query Keyword for finding care organizations. - Query string `form:"query" json:"query"` - +// SearchOrganizationsJSONBody defines parameters for SearchOrganizations. +type SearchOrganizationsJSONBody struct { // DidServiceType Filters other care organizations on the Nuts Network on service, only returning care organizations have a service in their DID Document which' type matches the given didServiceType and not including your own. If not supplied, care organizations aren't filtered on service. - DidServiceType *string `form:"didServiceType,omitempty" json:"didServiceType,omitempty"` + DidServiceType *string `json:"didServiceType,omitempty"` + + // DiscoveryServiceID Filters other care organizations on the Nuts Network, only returning care organizations that registered for the given Discovery Service. It false, it will search on all Discovery Services. + DiscoveryServiceID *string `json:"discoveryServiceID,omitempty"` - // DiscoveryServiceType Filters other care organizations on the Nuts Network on service, only returning care organizations that registered for the given service at a discovery server. It false, it will search on all Discovery Services. - DiscoveryServiceType *string `form:"discoveryServiceType,omitempty" json:"discoveryServiceType,omitempty"` + // ExcludeOwn If true, the current care organization is excluded from the search results. Defaults to false. + ExcludeOwn *bool `json:"excludeOwn,omitempty"` + + // Query The search query, key-value string properties. E.g.: 'credentialSubject.organization.name: Ziekenhuis' + Query map[string]string `json:"query"` } // GetRemotePatientParams defines parameters for GetRemotePatient. @@ -508,6 +514,9 @@ type CreateEpisodeJSONRequestBody = CreateEpisodeRequest // CreateCollaborationJSONRequestBody defines body for CreateCollaboration for application/json ContentType. type CreateCollaborationJSONRequestBody = CreateCollaborationRequest +// SearchOrganizationsJSONRequestBody defines body for SearchOrganizations for application/json ContentType. +type SearchOrganizationsJSONRequestBody SearchOrganizationsJSONBody + // UpdatePatientJSONRequestBody defines body for UpdatePatient for application/json ContentType. type UpdatePatientJSONRequestBody = PatientProperties diff --git a/nuts/client/discovery.go b/nuts/client/discovery.go index 103d9767..8bce9bc0 100644 --- a/nuts/client/discovery.go +++ b/nuts/client/discovery.go @@ -20,32 +20,27 @@ type DiscoverySearchResult struct { } type Discovery interface { - SearchService(ctx context.Context, organizationSearchParam string, discoveryServiceID *string, didServiceType *string) ([]DiscoverySearchResult, error) + SearchDiscoveryService(ctx context.Context, query map[string]string, discoveryServiceID *string, didServiceType *string) ([]DiscoverySearchResult, error) } -func (c HTTPClient) SearchService(ctx context.Context, organizationSearchParam string, discoveryServiceID *string, didServiceType *string) ([]DiscoverySearchResult, error) { +func (c HTTPClient) SearchDiscoveryService(ctx context.Context, query map[string]string, discoveryServiceID *string, didServiceType *string) ([]DiscoverySearchResult, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - searchParams := map[string]interface{}{ - "credentialSubject.organization.name": organizationSearchParam + "*", - } - + var serviceIDs []string if discoveryServiceID != nil { - results, err := c.searchDiscoveryService(ctx, searchParams, *discoveryServiceID, didServiceType) + serviceIDs = append(serviceIDs, *discoveryServiceID) + } else { + // service ID not specified, search all discovery services + var err error + serviceIDs, err = c.getDiscoveryServices(ctx) if err != nil { return nil, err } - return results, nil - } - // service ID not specified, search all discovery services - discoveryServices, err := c.getDiscoveryServices(ctx) - if err != nil { - return nil, err } searchResults := make([]DiscoverySearchResult, 0) - for _, serviceID := range discoveryServices { - currResults, err := c.searchDiscoveryService(ctx, searchParams, serviceID, didServiceType) + for _, serviceID := range serviceIDs { + currResults, err := c.searchDiscoveryService(ctx, query, serviceID, didServiceType) if err != nil { return nil, err } @@ -54,8 +49,12 @@ func (c HTTPClient) SearchService(ctx context.Context, organizationSearchParam s return searchResults, nil } -func (c HTTPClient) searchDiscoveryService(ctx context.Context, query map[string]interface{}, discoveryServiceID string, didServiceType *string) ([]DiscoverySearchResult, error) { - params := nutsDiscoveryClient.SearchPresentationsParams{Query: &query} +func (c HTTPClient) searchDiscoveryService(ctx context.Context, query map[string]string, discoveryServiceID string, didServiceType *string) ([]DiscoverySearchResult, error) { + queryAsMap := make(map[string]interface{}, 0) + for key, value := range query { + queryAsMap[key] = value + } + params := nutsDiscoveryClient.SearchPresentationsParams{Query: &queryAsMap} resp, err := c.discovery().SearchPresentations(ctx, discoveryServiceID, ¶ms) if err != nil { diff --git a/nuts/client/iam.go b/nuts/client/iam.go index e2a84036..98faa07e 100644 --- a/nuts/client/iam.go +++ b/nuts/client/iam.go @@ -3,6 +3,7 @@ package client import ( "context" "encoding/json" + "errors" nutsIamClient "github.com/nuts-foundation/nuts-demo-ehr/nuts/client/iam" "net/http" "time" @@ -64,14 +65,14 @@ func (c HTTPClient) GetAuthenticationResult(token string) (*nutsIamClient.TokenR } func (c HTTPClient) RequestServiceAccessToken(ctx context.Context, relyingPartyDID, authorizationServerDID string, scope string) (string, error) { - response, err := c.iam().RequestServiceAccessToken(ctx, relyingPartyDID, iam.RequestServiceAccessTokenJSONRequestBody{ + response, err := c.iam().RequestServiceAccessToken(ctx, relyingPartyDID, nutsIamClient.RequestServiceAccessTokenJSONRequestBody{ Scope: scope, Verifier: authorizationServerDID, }) if err != nil { return "", err } - tokenResponse, err := iam.ParseRequestServiceAccessTokenResponse(response) + tokenResponse, err := nutsIamClient.ParseRequestServiceAccessTokenResponse(response) if err != nil { return "", err } diff --git a/nuts/client/vdr.go b/nuts/client/vdr.go index 04589b67..dda62694 100644 --- a/nuts/client/vdr.go +++ b/nuts/client/vdr.go @@ -9,7 +9,7 @@ import ( func (c HTTPClient) ResolveServiceEndpoint(ctx context.Context, did string, serviceType string, endpointType string) (interface{}, error) { ep := vdr_v2.FilterServicesParamsEndpointType(endpointType) - response, err := c.vdr().FilterServices(ctx, did, "whatever", &vdr_v2.FilterServicesParams{EndpointType: &ep, Type: &serviceType}) + response, err := c.vdr().FilterServices(ctx, did, &vdr_v2.FilterServicesParams{EndpointType: &ep, Type: &serviceType}) if err != nil { return "", err } diff --git a/nuts/registry/organizations.go b/nuts/registry/organizations.go index 5df1c775..e5642995 100644 --- a/nuts/registry/organizations.go +++ b/nuts/registry/organizations.go @@ -3,7 +3,6 @@ package registry import ( "context" "errors" - "fmt" "github.com/nuts-foundation/nuts-demo-ehr/nuts" "sync" "time" @@ -43,44 +42,31 @@ func (r remoteOrganizationRegistry) Get(ctx context.Context, organizationDID str return cached, nil } - services, err := r.client.SearchDiscoveryService(ctx, map[string]string{ + searchResults, err := r.client.SearchDiscoveryService(ctx, map[string]string{ "credentialSubject.id": organizationDID, - }, nil) + }, nil, nil) if err != nil { return nil, err - } - var result *NutsOrganization + // find a search result that yields organization_name and organization_city - for _, searchResults := range services { - for _, searchResult := range searchResults { - organizationName := searchResult.Fields["organization_name"].(string) - organizationCity := searchResult.Fields["organization_city"].(string) - if organizationName == "" { - continue - } - } - if found { - credentials[j] = cred - j++ + var results []nuts.NutsOrganization + for _, searchResult := range searchResults { + if searchResult.Details.Name == "" { + continue } + results = append(results, searchResult.NutsOrganization) } - credentials = credentials[:j] - if len(credentials) > 1 { + if len(results) > 1 { // TODO: Get latest issued VC, or maybe all of them? return nil, errors.New("multiple organizations found (not supported yet)") } - var results []nuts.NutsOrganization - err = credentials[0].UnmarshalCredentialSubject(&results) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal NutsOrganizationCredential subject: %w", err) - } - if result == nil { + if len(results) == 0 { return nil, errors.New("organization not found") } - r.toCache(*result) - return result, nil + r.toCache(results[0]) + return &results[0], nil } func (r remoteOrganizationRegistry) GetCompoundServiceEndpoint(ctx context.Context, organizationDID, serviceType string, field string) (string, error) { diff --git a/web/src/ehr/patient/Patients.vue b/web/src/ehr/patient/Patients.vue index f7c13643..cb7881c6 100644 --- a/web/src/ehr/patient/Patients.vue +++ b/web/src/ehr/patient/Patients.vue @@ -17,23 +17,26 @@

Patients

- - +
+ + +
diff --git a/web/src/ehr/patient/ViewRemotePatient.vue b/web/src/ehr/patient/ViewRemotePatient.vue index 9f18bfe0..eeb23131 100644 --- a/web/src/ehr/patient/ViewRemotePatient.vue +++ b/web/src/ehr/patient/ViewRemotePatient.vue @@ -1,16 +1,35 @@