diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d91e2655..77bc4f53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,9 @@ npm install -g yarn brew install go brew install docker + +# Go specific tools +go install github.com/gzuidhof/tygo@latest ``` # Running Tests diff --git a/Makefile b/Makefile index 55ec5f19..e8633c47 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,10 @@ serve-frontend-prod: dep-frontend serve-backend: go run backend/cmd/fasten/fasten.go start --config ./config.dev.yaml --debug +.PHONY: migrate +migrate: + go run backend/cmd/fasten/fasten.go migrate --config ./config.dev.yaml --debug + ######################################################################################################################## # Backend @@ -36,6 +40,7 @@ clean-backend: dep-backend: go mod vendor + .PHONY: test-backend test-backend: dep-backend go vet ./... @@ -45,6 +50,11 @@ test-backend: dep-backend test-backend-coverage: dep-backend go test -coverprofile=backend-coverage.txt -covermode=atomic -v ./... +.PHONY: generate-backend +generate-backend: + go generate ./... + tygo generate + ######################################################################################################################## # Frontend ######################################################################################################################## @@ -68,6 +78,10 @@ build-frontend-desktop-sandbox: dep-frontend build-frontend-desktop-prod: dep-frontend cd frontend && yarn build -- -c desktop_prod +.PHONY: build-frontend-offline-sandbox +build-frontend-offline-sandbox: dep-frontend + cd frontend && yarn build -- -c offline_sandbox + .PHONY: test-frontend # reduce logging, disable angular-cli analytics for ci environment diff --git a/backend/cmd/fasten/fasten.go b/backend/cmd/fasten/fasten.go index b2ff634e..be3d205c 100644 --- a/backend/cmd/fasten/fasten.go +++ b/backend/cmd/fasten/fasten.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/analogj/go-util/utils" "github.com/fastenhealth/fasten-onprem/backend/pkg/config" + "github.com/fastenhealth/fasten-onprem/backend/pkg/database" "github.com/fastenhealth/fasten-onprem/backend/pkg/errors" "github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus" "github.com/fastenhealth/fasten-onprem/backend/pkg/version" @@ -136,6 +137,55 @@ func main() { }, }, }, + { + Name: "version", + Usage: "Print the version", + Action: func(c *cli.Context) error { + fmt.Println(version.VERSION) + return nil + }, + }, + { + Name: "migrate", + Usage: "Run database migrations, without starting application", + Action: func(c *cli.Context) error { + + if c.IsSet("config") { + err = appconfig.ReadConfig(c.String("config")) // Find and read the config file + if err != nil { // Handle errors reading the config file + //ignore "could not find config file" + fmt.Printf("Could not find config file at specified path: %s", c.String("config")) + return err + } + } + + if c.Bool("debug") { + appconfig.Set("log.level", "DEBUG") + } + + appLogger, logFile, err := CreateLogger(appconfig) + if logFile != nil { + defer logFile.Close() + } + if err != nil { + return err + } + + _, err = database.NewRepository(appconfig, appLogger, event_bus.NewNoopEventBusServer()) + return err + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Usage: "Specify the path to the config file", + }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + EnvVars: []string{"DEBUG"}, + }, + }, + }, }, } diff --git a/backend/pkg/database/gorm_common.go b/backend/pkg/database/gorm_common.go index a183db3d..1b7ddcc3 100644 --- a/backend/pkg/database/gorm_common.go +++ b/backend/pkg/database/gorm_common.go @@ -58,8 +58,8 @@ func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) err //create Fasten source credential for this user. fastenUserCred := models.SourceCredential{ - UserID: user.ID, - SourceType: sourcePkg.SourceTypeFasten, + UserID: user.ID, + PlatformType: sourcePkg.PlatformTypeFasten, } fastenUserCredResp := gr.GormClient.Create(&fastenUserCred) if fastenUserCredResp.Error != nil { @@ -723,7 +723,7 @@ func (gr *GormRepository) AddResourceComposition(ctx context.Context, compositio } //generate placeholder source - placeholderSource := models.SourceCredential{UserID: currentUser.ID, SourceType: "manual", ModelBase: models.ModelBase{ID: uuid.Nil}} + placeholderSource := models.SourceCredential{UserID: currentUser.ID, PlatformType: "manual", ModelBase: models.ModelBase{ID: uuid.Nil}} existingCompositionResources := []*models.ResourceBase{} rawResourceLookupTable := map[string]*models.ResourceBase{} @@ -882,7 +882,7 @@ func (gr *GormRepository) CreateSource(ctx context.Context, sourceCreds *models. return gr.GormClient.WithContext(ctx). Where(models.SourceCredential{ UserID: sourceCreds.UserID, - SourceType: sourceCreds.SourceType, + EndpointID: sourceCreds.EndpointID, Patient: sourceCreds.Patient}). Assign(*sourceCreds).FirstOrCreate(sourceCreds).Error } @@ -899,15 +899,14 @@ func (gr *GormRepository) UpdateSource(ctx context.Context, sourceCreds *models. Where(models.SourceCredential{ ModelBase: models.ModelBase{ID: sourceCreds.ID}, UserID: sourceCreds.UserID, - SourceType: sourceCreds.SourceType, + EndpointID: sourceCreds.EndpointID, }).Updates(models.SourceCredential{ - AccessToken: sourceCreds.AccessToken, - RefreshToken: sourceCreds.RefreshToken, - ExpiresAt: sourceCreds.ExpiresAt, - DynamicClientId: sourceCreds.DynamicClientId, - DynamicClientRegistrationMode: sourceCreds.DynamicClientRegistrationMode, - DynamicClientJWKS: sourceCreds.DynamicClientJWKS, - LatestBackgroundJobID: sourceCreds.LatestBackgroundJobID, + AccessToken: sourceCreds.AccessToken, + RefreshToken: sourceCreds.RefreshToken, + ExpiresAt: sourceCreds.ExpiresAt, + DynamicClientId: sourceCreds.DynamicClientId, + DynamicClientJWKS: sourceCreds.DynamicClientJWKS, + LatestBackgroundJobID: sourceCreds.LatestBackgroundJobID, }).Error } diff --git a/backend/pkg/database/gorm_repository_graph_test.go b/backend/pkg/database/gorm_repository_graph_test.go index 49613ddc..37f9feca 100644 --- a/backend/pkg/database/gorm_repository_graph_test.go +++ b/backend/pkg/database/gorm_repository_graph_test.go @@ -80,7 +80,8 @@ func (suite *RepositoryGraphTestSuite) TestGetFlattenedResourceGraph() { ModelBase: models.ModelBase{ ID: uuid.New(), }, - UserID: userModel.ID, + UserID: userModel.ID, + PlatformType: sourcePkg.PlatformTypeManual, } err = dbRepo.CreateSource(authContext, &testSourceCredential) require.NoError(suite.T(), err) @@ -92,7 +93,7 @@ func (suite *RepositoryGraphTestSuite) TestGetFlattenedResourceGraph() { "type": "test", }) - manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, sourcePkg.SourceTypeManual, authContext, testLogger, &testSourceCredential) + manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, authContext, testLogger, &testSourceCredential) summary, err := manualClient.SyncAllBundle(dbRepo, bundleFile, sourcePkg.FhirVersion401) require.NoError(suite.T(), err) @@ -146,7 +147,8 @@ func (suite *RepositoryGraphTestSuite) TestGetFlattenedResourceGraph_NDJson() { ModelBase: models.ModelBase{ ID: uuid.New(), }, - UserID: userModel.ID, + UserID: userModel.ID, + PlatformType: sourcePkg.PlatformTypeManual, } err = dbRepo.CreateSource(authContext, &testSourceCredential) require.NoError(suite.T(), err) @@ -158,7 +160,7 @@ func (suite *RepositoryGraphTestSuite) TestGetFlattenedResourceGraph_NDJson() { "type": "test", }) - manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, sourcePkg.SourceTypeManual, authContext, testLogger, &testSourceCredential) + manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, authContext, testLogger, &testSourceCredential) summary, err := manualClient.SyncAllBundle(dbRepo, bundleFile, sourcePkg.FhirVersion401) require.NoError(suite.T(), err) diff --git a/backend/pkg/database/gorm_repository_migrations.go b/backend/pkg/database/gorm_repository_migrations.go index c957f283..630a3db5 100644 --- a/backend/pkg/database/gorm_repository_migrations.go +++ b/backend/pkg/database/gorm_repository_migrations.go @@ -3,10 +3,16 @@ package database import ( "context" "fmt" + _20231017112246 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231017112246" + _20231201122541 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231201122541" + _0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806" + _20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850" "github.com/fastenhealth/fasten-onprem/backend/pkg/models" databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database" + sourceCatalog "github.com/fastenhealth/fasten-sources/catalog" sourcePkg "github.com/fastenhealth/fasten-sources/pkg" "github.com/go-gormigrate/gormigrate/v2" + "github.com/google/uuid" "gorm.io/gorm" ) @@ -20,15 +26,15 @@ func (gr *GormRepository) Migrate() error { //use "echo $(date '+%Y%m%d%H%M%S')" to generate new ID's m := gormigrate.New(gr.GormClient, gormMigrateOptions, []*gormigrate.Migration{ { - ID: "20231017112246", // base database models //TODO: figure out how to version these correctly (SourceCredential is complicated) + ID: "20231017112246", Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate( - &models.BackgroundJob{}, - &models.Glossary{}, - &models.SourceCredential{}, - &models.UserSettingEntry{}, - &models.User{}, + &_20231017112246.BackgroundJob{}, + &_20231017112246.Glossary{}, + &_20231017112246.SourceCredential{}, + &_20231017112246.UserSettingEntry{}, + &_20231017112246.User{}, ) }, }, @@ -44,7 +50,7 @@ func (gr *GormRepository) Migrate() error { ID: "20231201122541", // Adding Fasten Source Credential for each user Migrate: func(tx *gorm.DB) error { - users := []models.User{} + users := []_20231201122541.User{} results := tx.Find(&users) if results.Error != nil { return results.Error @@ -52,9 +58,9 @@ func (gr *GormRepository) Migrate() error { for _, user := range users { tx.Logger.Info(context.Background(), fmt.Sprintf("Creating Fasten Source Credential for user: %s", user.ID)) - fastenUserCred := models.SourceCredential{ + fastenUserCred := _20231201122541.SourceCredential{ UserID: user.ID, - SourceType: sourcePkg.SourceTypeFasten, + SourceType: string(sourcePkg.PlatformTypeFasten), } fastenUserCredCreateResp := tx.Create(&fastenUserCred) if fastenUserCredCreateResp.Error != nil { @@ -65,6 +71,89 @@ func (gr *GormRepository) Migrate() error { return nil }, }, + { + ID: "20240114092806", // Adding additional fields to Source Credential + Migrate: func(tx *gorm.DB) error { + + err := tx.AutoMigrate( + &_0240114092806.SourceCredential{}, + ) + if err != nil { + return err + } + + //attempt to populate the endpoint id, portal id and brand id for each existing source credential + sourceCredentials := []_0240114092806.SourceCredential{} + results := tx.Find(&sourceCredentials) + if results.Error != nil { + return results.Error + } + + for ndx, _ := range sourceCredentials { + sourceCredential := &sourceCredentials[ndx] + + if sourceCredential.SourceType == string(sourcePkg.PlatformTypeFasten) || sourceCredential.SourceType == string(sourcePkg.PlatformTypeManual) { + tx.Logger.Info(context.Background(), fmt.Sprintf("Updating Legacy SourceType (%s) to PlatformType: %s", sourceCredential.SourceType, sourceCredential.ID)) + + sourceCredential.PlatformType = string(sourceCredential.SourceType) + + fastenUpdateSourceCredential := tx.Save(sourceCredential) + if fastenUpdateSourceCredential.Error != nil { + tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred update Fasten Source Credential: %s", sourceCredential.ID)) + return fastenUpdateSourceCredential.Error + } + + continue + } + + tx.Logger.Info(context.Background(), fmt.Sprintf("Mapping Legacy SourceType (%s) to Brand, Portal and Endpoint IDs: %s", sourceCredential.SourceType, sourceCredential.ID)) + + matchingBrand, matchingPortal, matchingEndpoint, err := sourceCatalog.GetPatientAccessInfoForLegacySourceType(sourceCredential.SourceType, sourceCredential.ApiEndpointBaseUrl) + if err != nil { + tx.Logger.Error(context.Background(), err.Error()) + return err + } + portalId := uuid.MustParse(matchingPortal.Id) + sourceCredential.PortalID = &portalId + brandId := uuid.MustParse(matchingBrand.Id) + sourceCredential.Display = matchingPortal.Name + sourceCredential.BrandID = &brandId + sourceCredential.EndpointID = uuid.MustParse(matchingEndpoint.Id) + sourceCredential.PlatformType = string(matchingEndpoint.GetPlatformType()) + + fastenUpdateSourceCredential := tx.Save(sourceCredential) + if fastenUpdateSourceCredential.Error != nil { + tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred update Fasten Source Credential: %s", sourceCredential.ID)) + return fastenUpdateSourceCredential.Error + } + } + return nil + }, + }, + { + ID: "20240114103850", // cleanup unnecessary fields, now that we're using Brands, Portals and Endpoints. + Migrate: func(tx *gorm.DB) error { + + return tx.AutoMigrate( + &_20240114103850.SourceCredential{}, + ) + }, + }, + }) + + // run when database is empty + m.InitSchema(func(tx *gorm.DB) error { + err := tx.AutoMigrate( + &models.BackgroundJob{}, + &models.Glossary{}, + &models.SourceCredential{}, + &models.UserSettingEntry{}, + &models.User{}, + ) + if err != nil { + return err + } + return nil }) if err := m.Migrate(); err != nil { diff --git a/backend/pkg/database/gorm_repository_test.go b/backend/pkg/database/gorm_repository_test.go index ec257f56..663a5e07 100644 --- a/backend/pkg/database/gorm_repository_test.go +++ b/backend/pkg/database/gorm_repository_test.go @@ -1293,7 +1293,7 @@ func (suite *RepositoryTestSuite) TestCreateBackgroundJob_Sync() { authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") //test - sourceCredential := models.SourceCredential{ModelBase: models.ModelBase{ID: uuid.New()}, SourceType: sourcePkg.SourceType("bluebutton")} + sourceCredential := models.SourceCredential{ModelBase: models.ModelBase{ID: uuid.New()}, PlatformType: sourcePkg.PlatformType("bluebutton")} backgroundJob := models.NewSyncBackgroundJob(sourceCredential) err = dbRepo.CreateBackgroundJob( context.WithValue(authContext, pkg.ContextKeyTypeAuthUsername, "test_username"), @@ -1408,7 +1408,7 @@ func (suite *RepositoryTestSuite) TestUpdateBackgroundJob() { require.NoError(suite.T(), err) authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") - sourceCredential := models.SourceCredential{ModelBase: models.ModelBase{ID: uuid.New()}, SourceType: sourcePkg.SourceType("bluebutton")} + sourceCredential := models.SourceCredential{ModelBase: models.ModelBase{ID: uuid.New()}, PlatformType: sourcePkg.PlatformType("bluebutton")} backgroundJob := models.NewSyncBackgroundJob(sourceCredential) err = dbRepo.CreateBackgroundJob( context.WithValue(authContext, pkg.ContextKeyTypeAuthUsername, "test_username"), diff --git a/backend/pkg/database/migrations/20231017112246/background_job.go b/backend/pkg/database/migrations/20231017112246/background_job.go new file mode 100644 index 00000000..a3802eee --- /dev/null +++ b/backend/pkg/database/migrations/20231017112246/background_job.go @@ -0,0 +1,23 @@ +package _20231017112246 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg" + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + "github.com/google/uuid" + "gorm.io/datatypes" + "time" +) + +type BackgroundJob struct { + models.ModelBase + User models.User `json:"user,omitempty"` //SECURITY: user and user.id will be set by the repository service + UserID uuid.UUID `json:"user_id"` + + JobType pkg.BackgroundJobType `json:"job_type"` + Data datatypes.JSON `gorm:"column:data;type:text;serializer:json" json:"data,omitempty"` + JobStatus pkg.BackgroundJobStatus `json:"job_status"` + LockedTime *time.Time `json:"locked_time"` + DoneTime *time.Time `json:"done_time"` + Retries int `json:"retries"` + Schedule *pkg.BackgroundJobSchedule `json:"schedule,omitempty"` +} diff --git a/backend/pkg/database/migrations/20231017112246/glossary.go b/backend/pkg/database/migrations/20231017112246/glossary.go new file mode 100644 index 00000000..c22b0eab --- /dev/null +++ b/backend/pkg/database/migrations/20231017112246/glossary.go @@ -0,0 +1,16 @@ +package _20231017112246 + +import "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + +// Glossary contains patient friendly terms for medical concepts +// Can be retrieved by Code and CodeSystem +// Structured similar to ValueSet https://hl7.org/fhir/valueset.html +type Glossary struct { + models.ModelBase + Code string `json:"code" gorm:"uniqueIndex:idx_glossary_term"` + CodeSystem string `json:"code_system" gorm:"uniqueIndex:idx_glossary_term"` + Publisher string `json:"publisher"` + Title string `json:"title"` + Url string `json:"url"` + Description string `json:"description"` +} diff --git a/backend/pkg/database/migrations/20231017112246/source_credential.go b/backend/pkg/database/migrations/20231017112246/source_credential.go new file mode 100644 index 00000000..b4523f40 --- /dev/null +++ b/backend/pkg/database/migrations/20231017112246/source_credential.go @@ -0,0 +1,56 @@ +package _20231017112246 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + "github.com/google/uuid" +) + +// SourceCredential Data/Medical Provider Credentials +// similar to LighthouseSourceDefinition from fasten-source +type SourceCredential struct { + models.ModelBase + User *User `json:"user,omitempty"` + UserID uuid.UUID `json:"user_id" gorm:"uniqueIndex:idx_user_source_patient"` + SourceType string `json:"source_type" gorm:"uniqueIndex:idx_user_source_patient"` + Patient string `json:"patient" gorm:"uniqueIndex:idx_user_source_patient"` + + LatestBackgroundJob *BackgroundJob `json:"latest_background_job,omitempty"` + LatestBackgroundJobID *uuid.UUID `json:"-"` + + //oauth endpoints + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + IntrospectionEndpoint string `json:"introspection_endpoint"` + RegistrationEndpoint string `json:"registration_endpoint"` //optional - required when Dynamic Client Registration mode is set + + Scopes []string `json:"scopes_supported" gorm:"type:text;serializer:json"` + Issuer string `json:"issuer"` + GrantTypesSupported []string `json:"grant_types_supported" gorm:"type:text;serializer:json"` + ResponseType []string `json:"response_types_supported" gorm:"type:text;serializer:json"` + ResponseModesSupported []string `json:"response_modes_supported" gorm:"type:text;serializer:json"` + Audience string `json:"aud"` //optional - required for some providers + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported" gorm:"type:text;serializer:json"` + + //Fasten custom configuration + UserInfoEndpoint string `json:"userinfo_endpoint"` //optional - supported by some providers, not others. + ApiEndpointBaseUrl string `json:"api_endpoint_base_url"` //api endpoint we'll communicate with after authentication + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` //lighthouse url the provider will redirect to (registered with App) + + Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE) + DynamicClientRegistrationMode string `json:"dynamic_client_registration_mode"` //if enabled, will dynamically register client with provider (https://oauth.net/2/dynamic-client-registration/) + CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight + //SecretKeyPrefix string `json:"-"` //the secret key prefix to use, if empty (default) will use the sourceType value + + // auth/credential data + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + ExpiresAt int64 `json:"expires_at"` + CodeChallenge string `json:"code_challenge"` + CodeVerifier string `json:"code_verifier"` + + //dynamic client auth/credential data + DynamicClientJWKS []map[string]string `json:"dynamic_client_jwks" gorm:"type:text;serializer:json"` + DynamicClientId string `json:"dynamic_client_id"` +} diff --git a/backend/pkg/database/migrations/20231017112246/user.go b/backend/pkg/database/migrations/20231017112246/user.go new file mode 100644 index 00000000..746dc8f6 --- /dev/null +++ b/backend/pkg/database/migrations/20231017112246/user.go @@ -0,0 +1,17 @@ +package _20231017112246 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" +) + +type User struct { + models.ModelBase + FullName string `json:"full_name"` + Username string `json:"username" gorm:"unique"` + Password string `json:"password"` + + //additional optional metadata that Fasten stores with users + Picture string `json:"picture"` + Email string `json:"email"` + //Roles datatypes.JSON `json:"roles"` +} diff --git a/backend/pkg/database/migrations/20231017112246/user_settings_entry.go b/backend/pkg/database/migrations/20231017112246/user_settings_entry.go new file mode 100644 index 00000000..84e32a3f --- /dev/null +++ b/backend/pkg/database/migrations/20231017112246/user_settings_entry.go @@ -0,0 +1,26 @@ +package _20231017112246 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + "github.com/google/uuid" +) + +type UserSettingEntry struct { + //GORM attributes, see: http://gorm.io/docs/conventions.html + models.ModelBase + User *User `json:"user,omitempty" gorm:"-"` + UserID uuid.UUID `json:"user_id" gorm:"not null;index:,unique,composite:user_setting_key_name"` + + SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:user_setting_key_name"` + SettingKeyDescription string `json:"setting_key_description"` + SettingDataType string `json:"setting_data_type"` + + SettingValueNumeric int `json:"setting_value_numeric"` + SettingValueString string `json:"setting_value_string"` + SettingValueBool bool `json:"setting_value_bool"` + SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"` +} + +func (s UserSettingEntry) TableName() string { + return "user_settings" +} diff --git a/backend/pkg/database/migrations/20231201122541/background_job.go b/backend/pkg/database/migrations/20231201122541/background_job.go new file mode 100644 index 00000000..500eec50 --- /dev/null +++ b/backend/pkg/database/migrations/20231201122541/background_job.go @@ -0,0 +1,23 @@ +package _20231201122541 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg" + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + "github.com/google/uuid" + "gorm.io/datatypes" + "time" +) + +type BackgroundJob struct { + models.ModelBase + User models.User `json:"user,omitempty"` //SECURITY: user and user.id will be set by the repository service + UserID uuid.UUID `json:"user_id"` + + JobType pkg.BackgroundJobType `json:"job_type"` + Data datatypes.JSON `gorm:"column:data;type:text;serializer:json" json:"data,omitempty"` + JobStatus pkg.BackgroundJobStatus `json:"job_status"` + LockedTime *time.Time `json:"locked_time"` + DoneTime *time.Time `json:"done_time"` + Retries int `json:"retries"` + Schedule *pkg.BackgroundJobSchedule `json:"schedule,omitempty"` +} diff --git a/backend/pkg/database/migrations/20231201122541/source_credential.go b/backend/pkg/database/migrations/20231201122541/source_credential.go new file mode 100644 index 00000000..125db8ba --- /dev/null +++ b/backend/pkg/database/migrations/20231201122541/source_credential.go @@ -0,0 +1,56 @@ +package _20231201122541 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + "github.com/google/uuid" +) + +// SourceCredential Data/Medical Provider Credentials +// similar to LighthouseSourceDefinition from fasten-source +type SourceCredential struct { + models.ModelBase + User *User `json:"user,omitempty"` + UserID uuid.UUID `json:"user_id" gorm:"uniqueIndex:idx_user_source_patient"` + SourceType string `json:"source_type" gorm:"uniqueIndex:idx_user_source_patient"` + Patient string `json:"patient" gorm:"uniqueIndex:idx_user_source_patient"` + + LatestBackgroundJob *BackgroundJob `json:"latest_background_job,omitempty"` + LatestBackgroundJobID *uuid.UUID `json:"-"` + + //oauth endpoints + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + IntrospectionEndpoint string `json:"introspection_endpoint"` + RegistrationEndpoint string `json:"registration_endpoint"` //optional - required when Dynamic Client Registration mode is set + + Scopes []string `json:"scopes_supported" gorm:"type:text;serializer:json"` + Issuer string `json:"issuer"` + GrantTypesSupported []string `json:"grant_types_supported" gorm:"type:text;serializer:json"` + ResponseType []string `json:"response_types_supported" gorm:"type:text;serializer:json"` + ResponseModesSupported []string `json:"response_modes_supported" gorm:"type:text;serializer:json"` + Audience string `json:"aud"` //optional - required for some providers + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported" gorm:"type:text;serializer:json"` + + //Fasten custom configuration + UserInfoEndpoint string `json:"userinfo_endpoint"` //optional - supported by some providers, not others. + ApiEndpointBaseUrl string `json:"api_endpoint_base_url"` //api endpoint we'll communicate with after authentication + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` //lighthouse url the provider will redirect to (registered with App) + + Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE) + DynamicClientRegistrationMode string `json:"dynamic_client_registration_mode"` //if enabled, will dynamically register client with provider (https://oauth.net/2/dynamic-client-registration/) + CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight + //SecretKeyPrefix string `json:"-"` //the secret key prefix to use, if empty (default) will use the sourceType value + + // auth/credential data + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + ExpiresAt int64 `json:"expires_at"` + CodeChallenge string `json:"code_challenge"` + CodeVerifier string `json:"code_verifier"` + + //dynamic client auth/credential data + DynamicClientJWKS []map[string]string `json:"dynamic_client_jwks" gorm:"type:text;serializer:json"` + DynamicClientId string `json:"dynamic_client_id"` +} diff --git a/backend/pkg/database/migrations/20231201122541/user.go b/backend/pkg/database/migrations/20231201122541/user.go new file mode 100644 index 00000000..f31cee50 --- /dev/null +++ b/backend/pkg/database/migrations/20231201122541/user.go @@ -0,0 +1,17 @@ +package _20231201122541 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" +) + +type User struct { + models.ModelBase + FullName string `json:"full_name"` + Username string `json:"username" gorm:"unique"` + Password string `json:"password"` + + //additional optional metadata that Fasten stores with users + Picture string `json:"picture"` + Email string `json:"email"` + //Roles datatypes.JSON `json:"roles"` +} diff --git a/backend/pkg/database/migrations/20240114092806/source_credential.go b/backend/pkg/database/migrations/20240114092806/source_credential.go new file mode 100644 index 00000000..5625e70a --- /dev/null +++ b/backend/pkg/database/migrations/20240114092806/source_credential.go @@ -0,0 +1,60 @@ +package _0240114092806 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + "github.com/google/uuid" +) + +type SourceCredential struct { + models.ModelBase + User *models.User `json:"user,omitempty"` + UserID uuid.UUID `json:"user_id" gorm:"uniqueIndex:idx_user_source_patient"` + SourceType string `json:"source_type" gorm:"uniqueIndex:idx_user_source_patient"` + Patient string `json:"patient" gorm:"uniqueIndex:idx_user_source_patient"` + + //New Fields + Display string `json:"display"` + BrandID *uuid.UUID `json:"brand_id"` + PortalID *uuid.UUID `json:"portal_id"` + EndpointID uuid.UUID `json:"endpoint_id"` + PlatformType string `json:"platform_type"` + + LatestBackgroundJob *models.BackgroundJob `json:"latest_background_job,omitempty"` + LatestBackgroundJobID *uuid.UUID `json:"-"` + + //oauth endpoints + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + IntrospectionEndpoint string `json:"introspection_endpoint"` + RegistrationEndpoint string `json:"registration_endpoint"` //optional - required when Dynamic Client Registration mode is set + + Scopes []string `json:"scopes_supported" gorm:"type:text;serializer:json"` + Issuer string `json:"issuer"` + GrantTypesSupported []string `json:"grant_types_supported" gorm:"type:text;serializer:json"` + ResponseType []string `json:"response_types_supported" gorm:"type:text;serializer:json"` + ResponseModesSupported []string `json:"response_modes_supported" gorm:"type:text;serializer:json"` + Audience string `json:"aud"` //optional - required for some providers + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported" gorm:"type:text;serializer:json"` + + //Fasten custom configuration + UserInfoEndpoint string `json:"userinfo_endpoint"` //optional - supported by some providers, not others. + ApiEndpointBaseUrl string `json:"api_endpoint_base_url"` //api endpoint we'll communicate with after authentication + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` //lighthouse url the provider will redirect to (registered with App) + + Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE) + CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight + //SecretKeyPrefix string `json:"-"` //the secret key prefix to use, if empty (default) will use the sourceType value + + // auth/credential data + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + ExpiresAt int64 `json:"expires_at"` + CodeChallenge string `json:"code_challenge"` + CodeVerifier string `json:"code_verifier"` + + //dynamic client auth/credential data + DynamicClientJWKS []map[string]string `json:"dynamic_client_jwks" gorm:"type:text;serializer:json"` + DynamicClientId string `json:"dynamic_client_id"` +} diff --git a/backend/pkg/database/migrations/20240114103850/source_credential.go b/backend/pkg/database/migrations/20240114103850/source_credential.go new file mode 100644 index 00000000..f90d34c9 --- /dev/null +++ b/backend/pkg/database/migrations/20240114103850/source_credential.go @@ -0,0 +1,39 @@ +package _20240114103850 + +import ( + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + sourcesPkg "github.com/fastenhealth/fasten-sources/pkg" + "github.com/google/uuid" +) + +// SourceCredential Data/Medical Provider Credentials +// similar to LighthouseSourceDefinition from fasten-source +type SourceCredential struct { + models.ModelBase + User *models.User `json:"user,omitempty"` + UserID uuid.UUID `json:"user_id" gorm:"uniqueIndex:idx_user_source_patient"` + Patient string `json:"patient" gorm:"uniqueIndex:idx_user_source_patient"` + EndpointID uuid.UUID `json:"endpoint_id" gorm:"uniqueIndex:idx_user_source_patient"` + + //New Fields + Display string `json:"display"` + BrandID uuid.UUID `json:"brand_id"` + PortalID uuid.UUID `json:"portal_id"` + PlatformType sourcesPkg.PlatformType `json:"platform_type"` + + LatestBackgroundJob *models.BackgroundJob `json:"latest_background_job,omitempty"` + LatestBackgroundJobID *uuid.UUID `json:"-"` + + // auth/credential data + ClientId string `json:"client_id"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + ExpiresAt int64 `json:"expires_at"` + CodeChallenge string `json:"code_challenge"` + CodeVerifier string `json:"code_verifier"` + + //dynamic client auth/credential data + DynamicClientJWKS []map[string]string `json:"dynamic_client_jwks" gorm:"type:text;serializer:json"` + DynamicClientId string `json:"dynamic_client_id"` +} diff --git a/backend/pkg/models/background_job_sync.go b/backend/pkg/models/background_job_sync.go index 61bb02e9..35d565e7 100644 --- a/backend/pkg/models/background_job_sync.go +++ b/backend/pkg/models/background_job_sync.go @@ -11,7 +11,7 @@ func NewSyncBackgroundJob(source SourceCredential) *BackgroundJob { now := time.Now() data := BackgroundJobSyncData{ SourceID: source.ID, - SourceType: string(source.SourceType), + BrandID: source.BrandID.String(), CheckpointData: nil, ErrorData: nil, } @@ -28,7 +28,7 @@ func NewSyncBackgroundJob(source SourceCredential) *BackgroundJob { type BackgroundJobSyncData struct { SourceID uuid.UUID `json:"source_id"` - SourceType string `json:"source_type"` + BrandID string `json:"brand_id"` CheckpointData map[string]interface{} `json:"checkpoint_data,omitempty"` ErrorData map[string]interface{} `json:"error_data,omitempty"` } diff --git a/backend/pkg/models/source_credential.go b/backend/pkg/models/source_credential.go index 888ea07e..7235b007 100644 --- a/backend/pkg/models/source_credential.go +++ b/backend/pkg/models/source_credential.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/fastenhealth/fasten-onprem/backend/pkg/jwk" + sourcesDefinitions "github.com/fastenhealth/fasten-sources/definitions" sourcesPkg "github.com/fastenhealth/fasten-sources/pkg" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jwa" @@ -20,40 +21,22 @@ import ( // similar to LighthouseSourceDefinition from fasten-source type SourceCredential struct { ModelBase - User *User `json:"user,omitempty"` - UserID uuid.UUID `json:"user_id" gorm:"uniqueIndex:idx_user_source_patient"` - SourceType sourcesPkg.SourceType `json:"source_type" gorm:"uniqueIndex:idx_user_source_patient"` - Patient string `json:"patient" gorm:"uniqueIndex:idx_user_source_patient"` + User *User `json:"user,omitempty"` + UserID uuid.UUID `json:"user_id" gorm:"uniqueIndex:idx_user_source_patient"` + Patient string `json:"patient" gorm:"uniqueIndex:idx_user_source_patient"` + EndpointID uuid.UUID `json:"endpoint_id" gorm:"uniqueIndex:idx_user_source_patient"` + + //New Fields + Display string `json:"display"` + BrandID uuid.UUID `json:"brand_id"` + PortalID uuid.UUID `json:"portal_id"` + PlatformType sourcesPkg.PlatformType `json:"platform_type"` LatestBackgroundJob *BackgroundJob `json:"latest_background_job,omitempty"` LatestBackgroundJobID *uuid.UUID `json:"-"` - //oauth endpoints - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - IntrospectionEndpoint string `json:"introspection_endpoint"` - RegistrationEndpoint string `json:"registration_endpoint"` //optional - required when Dynamic Client Registration mode is set - - Scopes []string `json:"scopes_supported" gorm:"type:text;serializer:json"` - Issuer string `json:"issuer"` - GrantTypesSupported []string `json:"grant_types_supported" gorm:"type:text;serializer:json"` - ResponseType []string `json:"response_types_supported" gorm:"type:text;serializer:json"` - ResponseModesSupported []string `json:"response_modes_supported" gorm:"type:text;serializer:json"` - Audience string `json:"aud"` //optional - required for some providers - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported" gorm:"type:text;serializer:json"` - - //Fasten custom configuration - UserInfoEndpoint string `json:"userinfo_endpoint"` //optional - supported by some providers, not others. - ApiEndpointBaseUrl string `json:"api_endpoint_base_url"` //api endpoint we'll communicate with after authentication - ClientId string `json:"client_id"` - RedirectUri string `json:"redirect_uri"` //lighthouse url the provider will redirect to (registered with App) - - Confidential bool `json:"confidential"` //if enabled, requires client_secret to authenticate with provider (PKCE) - DynamicClientRegistrationMode string `json:"dynamic_client_registration_mode"` //if enabled, will dynamically register client with provider (https://oauth.net/2/dynamic-client-registration/) - CORSRelayRequired bool `json:"cors_relay_required"` //if true, requires CORS proxy/relay, as provider does not return proper response to CORS preflight - //SecretKeyPrefix string `json:"-"` //the secret key prefix to use, if empty (default) will use the sourceType value - // auth/credential data + ClientId string `json:"client_id"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` IdToken string `json:"id_token"` @@ -66,13 +49,26 @@ type SourceCredential struct { DynamicClientId string `json:"dynamic_client_id"` } -func (s *SourceCredential) GetSourceType() sourcesPkg.SourceType { - return s.SourceType -} func (s *SourceCredential) GetSourceId() string { return s.ID.String() } +func (s *SourceCredential) GetEndpointId() string { + return s.EndpointID.String() +} + +func (s *SourceCredential) GetPortalId() string { + return s.PortalID.String() +} + +func (s *SourceCredential) GetBrandId() string { + return s.BrandID.String() +} + +func (s *SourceCredential) GetPlatformType() sourcesPkg.PlatformType { + return s.PlatformType +} + func (s *SourceCredential) GetClientId() string { return s.ClientId } @@ -81,18 +77,6 @@ func (s *SourceCredential) GetPatientId() string { return s.Patient } -func (s *SourceCredential) GetOauthAuthorizationEndpoint() string { - return s.AuthorizationEndpoint -} - -func (s *SourceCredential) GetOauthTokenEndpoint() string { - return s.TokenEndpoint -} - -func (s *SourceCredential) GetApiEndpointBaseUrl() string { - return s.ApiEndpointBaseUrl -} - func (s *SourceCredential) GetRefreshToken() string { return s.RefreshToken } @@ -135,7 +119,14 @@ codeVerifier: codeVerifier // IsDynamicClient this method is used to check if this source uses dynamic client registration (used to customize token refresh logic) func (s *SourceCredential) IsDynamicClient() bool { - return len(s.DynamicClientRegistrationMode) > 0 + + endpoint, err := sourcesDefinitions.GetSourceDefinition(sourcesDefinitions.GetSourceConfigOptions{ + EndpointId: s.EndpointID.String(), + }) + if err != nil || endpoint == nil { + return false + } + return len(endpoint.DynamicClientRegistrationMode) > 0 } // This method will generate a new keypair, register a new dynamic client with the provider @@ -144,6 +135,17 @@ func (s *SourceCredential) IsDynamicClient() bool { // - DynamicClientId func (s *SourceCredential) RegisterDynamicClient() error { + endpoint, err := sourcesDefinitions.GetSourceDefinition(sourcesDefinitions.GetSourceConfigOptions{ + EndpointId: s.EndpointID.String(), + }) + if err != nil { + return fmt.Errorf("an error occurred while retrieving source definition: %w", err) + } else if endpoint == nil { + return fmt.Errorf("endpoint definition not found") + } else if endpoint.RegistrationEndpoint == "" { + return fmt.Errorf("registration endpoint not found") + } + //this source requires dynamic client registration // see https://fhir.epic.com/Documentation?docId=Oauth2§ion=Standalone-Oauth2-OfflineAccess-0 @@ -183,7 +185,7 @@ func (s *SourceCredential) RegisterDynamicClient() error { } //http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair)))) - req, err := http.NewRequest(http.MethodPost, s.RegistrationEndpoint, bytes.NewBuffer(payloadBytes)) + req, err := http.NewRequest(http.MethodPost, endpoint.RegistrationEndpoint, bytes.NewBuffer(payloadBytes)) if err != nil { return fmt.Errorf("an error occurred while generating dynamic client registration request: %w", err) } @@ -223,7 +225,14 @@ func (s *SourceCredential) RegisterDynamicClient() error { // - AccessToken // - ExpiresAt func (s *SourceCredential) RefreshDynamicClientAccessToken() error { - if len(s.DynamicClientRegistrationMode) == 0 { + endpoint, err := sourcesDefinitions.GetSourceDefinition(sourcesDefinitions.GetSourceConfigOptions{ + EndpointId: s.EndpointID.String(), + }) + if err != nil { + return fmt.Errorf("an error occurred while retrieving source definition: %w", err) + } + + if len(endpoint.DynamicClientRegistrationMode) == 0 { return fmt.Errorf("dynamic client registration mode not set") } if len(s.DynamicClientJWKS) == 0 { @@ -243,7 +252,7 @@ func (s *SourceCredential) RefreshDynamicClientAccessToken() error { t := jwt.New() t.Set("kid", jwkeypair.KeyID()) t.Set(jwt.SubjectKey, s.DynamicClientId) - t.Set(jwt.AudienceKey, s.TokenEndpoint) + t.Set(jwt.AudienceKey, endpoint.TokenEndpoint) t.Set(jwt.JwtIDKey, uuid.New().String()) t.Set(jwt.ExpirationKey, time.Now().Add(time.Minute*2).Unix()) // must be less than 5 minutes from now. Time when this JWT expires t.Set(jwt.IssuedAtKey, time.Now().Unix()) @@ -265,7 +274,7 @@ func (s *SourceCredential) RefreshDynamicClientAccessToken() error { "client_id": {s.DynamicClientId}, } - tokenResp, err := http.PostForm(s.TokenEndpoint, postForm) + tokenResp, err := http.PostForm(endpoint.TokenEndpoint, postForm) if err != nil { return fmt.Errorf("an error occurred while sending dynamic client token request, %s", err) diff --git a/backend/pkg/web/handler/background_jobs.go b/backend/pkg/web/handler/background_jobs.go index 1e3a07fc..02814fb2 100644 --- a/backend/pkg/web/handler/background_jobs.go +++ b/backend/pkg/web/handler/background_jobs.go @@ -36,7 +36,7 @@ func BackgroundJobSyncResources( _sourceCred *models.SourceCredential, ) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) { // after creating the client, we should do a bulk import - sourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), _sourceCred.SourceType, _backgroundJobContext, _logger, _sourceCred) + sourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), _backgroundJobContext, _logger, _sourceCred) if err != nil { resultErr := fmt.Errorf("an error occurred while initializing hub client using source credential: %w", err) _logger.Errorln(resultErr) diff --git a/backend/pkg/web/handler/resource_related.go b/backend/pkg/web/handler/resource_related.go index cf3eaf41..1bc270ab 100644 --- a/backend/pkg/web/handler/resource_related.go +++ b/backend/pkg/web/handler/resource_related.go @@ -32,7 +32,7 @@ func CreateRelatedResources(c *gin.Context) { sourceCredentials, err := databaseRepo.GetSources(c) var fastenSourceCredential *models.SourceCredential for _, sourceCredential := range sourceCredentials { - if sourceCredential.SourceType == sourcePkg.SourceTypeFasten { + if sourceCredential.PlatformType == sourcePkg.PlatformTypeFasten { fastenSourceCredential = &sourceCredential break } @@ -55,7 +55,7 @@ func CreateRelatedResources(c *gin.Context) { ) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) { //step 3: create a "fasten" client, which we can use to parse resources to add to the database - fastenSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeFasten, _backgroundJobContext, _logger, _sourceCred) + fastenSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), _backgroundJobContext, _logger, _sourceCred) if err != nil { resultErr := fmt.Errorf("could not create Fasten source client") _logger.Errorln(resultErr) diff --git a/backend/pkg/web/handler/source.go b/backend/pkg/web/handler/source.go index 04d619ef..f91da706 100644 --- a/backend/pkg/web/handler/source.go +++ b/backend/pkg/web/handler/source.go @@ -9,6 +9,7 @@ import ( "github.com/fastenhealth/fasten-onprem/backend/pkg/models" "github.com/fastenhealth/fasten-sources/clients/factory" sourceModels "github.com/fastenhealth/fasten-sources/clients/models" + sourceDefinitions "github.com/fastenhealth/fasten-sources/definitions" sourcePkg "github.com/fastenhealth/fasten-sources/pkg" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -31,11 +32,16 @@ func CreateReconnectSource(c *gin.Context) { logger.Infof("Parsed Create SourceCredential Credentials Payload: %v", sourceCred) - if sourceCred.DynamicClientRegistrationMode == "user-authenticated" { + //get the endpoint definition + endpointDefinition, err := sourceDefinitions.GetSourceDefinition(sourceDefinitions.GetSourceConfigOptions{ + EndpointId: sourceCred.EndpointID.String(), + }) + + if endpointDefinition.DynamicClientRegistrationMode == "user-authenticated" { logger.Warnf("This client requires a dynamic client registration, starting registration process") - if len(sourceCred.RegistrationEndpoint) == 0 { - err := fmt.Errorf("this client requires dynamic registration, but does not provide a registration endpoint: %s", sourceCred.DynamicClientRegistrationMode) + if len(endpointDefinition.RegistrationEndpoint) == 0 { + err := fmt.Errorf("this client requires dynamic registration, but does not provide a registration endpoint: %s", endpointDefinition.DynamicClientRegistrationMode) logger.Errorln(err) c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()}) return @@ -143,9 +149,9 @@ func CreateManualSource(c *gin.Context) { // create a "manual" client, which we can use to parse the manualSourceCredential := models.SourceCredential{ - SourceType: sourcePkg.SourceTypeManual, + PlatformType: sourcePkg.PlatformTypeManual, } - tempSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeManual, c, logger, &manualSourceCredential) + tempSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), c, logger, &manualSourceCredential) if err != nil { logger.Errorln("An error occurred while initializing hub client using manual source without credentials", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) @@ -179,7 +185,7 @@ func CreateManualSource(c *gin.Context) { _databaseRepo database.DatabaseRepository, _sourceCred *models.SourceCredential, ) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) { - manualSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeManual, _backgroundJobContext, _logger, _sourceCred) + manualSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), _backgroundJobContext, _logger, _sourceCred) if err != nil { resultErr := fmt.Errorf("an error occurred while initializing hub client using manual source with credential: %w", err) logger.Errorln(resultErr) diff --git a/backend/pkg/web/handler/source_test.go b/backend/pkg/web/handler/source_test.go index 4b36b0a3..80ab555b 100644 --- a/backend/pkg/web/handler/source_test.go +++ b/backend/pkg/web/handler/source_test.go @@ -77,27 +77,24 @@ func (suite *SourceHandlerTestSuite) AfterTest(suiteName, testName string) { os.Remove(suite.TestDatabase.Name()) } - func CreateManualSourceHttpRequestFromFile(fileName string) (*http.Request, error) { - + file, err := os.Open(fileName) - if err!= nil { - log.Fatal("Could not open file ", err.Error()) + if err != nil { + log.Fatal("Could not open file ", err.Error()) return nil, err } defer file.Close() - body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, _ := writer.CreateFormFile("file", filepath.Base(file.Name())) io.Copy(part, file) writer.Close() - req, err := http.NewRequest("POST", "/source/manual", body) - if err!= nil { - log.Fatal("Could not make http request ", err.Error()) + if err != nil { + log.Fatal("Could not make http request ", err.Error()) return nil, err } req.Header.Add("Content-Type", writer.FormDataContentType()) @@ -123,7 +120,7 @@ func (suite *SourceHandlerTestSuite) TestCreateManualSourceHandler() { //test req, err := CreateManualSourceHttpRequestFromFile("testdata/Tania553_Harris789_545c2380-b77f-4919-ab5d-0f615f877250.json") - require.NoError(suite.T(),err) + require.NoError(suite.T(), err) ctx.Request = req CreateManualSource(ctx) @@ -144,7 +141,7 @@ func (suite *SourceHandlerTestSuite) TestCreateManualSourceHandler() { require.NoError(suite.T(), err) require.Equal(suite.T(), true, respWrapper.Success) - require.Equal(suite.T(), "manual", string(respWrapper.Source.SourceType)) + require.Equal(suite.T(), "manual", string(respWrapper.Source.PlatformType)) require.Equal(suite.T(), 196, respWrapper.Data.TotalResources) summary, err := suite.AppRepository.GetSourceSummary(ctx, respWrapper.Source.ID.String()) require.NoError(suite.T(), err) diff --git a/backend/pkg/web/handler/unsafe.go b/backend/pkg/web/handler/unsafe.go index 4cbaebe6..461315b8 100644 --- a/backend/pkg/web/handler/unsafe.go +++ b/backend/pkg/web/handler/unsafe.go @@ -46,7 +46,7 @@ func UnsafeRequestSource(c *gin.Context) { return } - client, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), foundSource.SourceType, c, logger, foundSource) + client, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), c, logger, foundSource) if err != nil { logger.Errorf("Could not initialize source client %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) @@ -140,7 +140,7 @@ func UnsafeSyncResourceNames(c *gin.Context) { return } - client, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), foundSource.SourceType, c, logger, foundSource) + client, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), c, logger, foundSource) if err != nil { logger.Errorf("Could not initialize source client %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) diff --git a/go.mod b/go.mod index 8afb6b1c..c09af358 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/dave/jennifer v1.6.1 github.com/dominikbraun/graph v0.15.0 github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 - github.com/fastenhealth/fasten-sources v0.4.21 + github.com/fastenhealth/fasten-sources v0.5.0 github.com/fastenhealth/gofhir-models v0.0.6 github.com/gin-gonic/gin v1.9.0 github.com/go-gormigrate/gormigrate/v2 v2.1.1 @@ -60,7 +60,7 @@ require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.13.0 // indirect + github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/goccy/go-json v0.10.2 // indirect diff --git a/go.sum b/go.sum index b9504862..ffcd3ff6 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fastenhealth/fasten-sources v0.4.21 h1:zs6un6SpK4zkzUzignHtsaWpdEx9MNbORjUFZ6DoFDg= -github.com/fastenhealth/fasten-sources v0.4.21/go.mod h1:tnvfgYG9utKCQ18+Jp26QBOO1ncwAkYx2l/XOBJqnyw= +github.com/fastenhealth/fasten-sources v0.5.0 h1:VQIayggGOdGlosnIrgcPMsDH/xqDjvz4jbRx4DQfZ3w= +github.com/fastenhealth/fasten-sources v0.5.0/go.mod h1:hUQATAu5KrxKbACJoVt4iEKIGnRtmiOmHz+5TLfyiCM= github.com/fastenhealth/gofhir-models v0.0.6 h1:yJYYaV1eJtHiGEfA1rXLsyOm/9hIi6s2cGoZzGfW1tM= github.com/fastenhealth/gofhir-models v0.0.6/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -129,8 +129,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= -github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= diff --git a/tygo.yaml b/tygo.yaml new file mode 100644 index 00000000..1a588537 --- /dev/null +++ b/tygo.yaml @@ -0,0 +1,6 @@ +packages: + - path: "github.com/fastenhealth/fasten-sources/pkg/models/catalog" + output_path: "frontend/src/app/models/patient-access-brands/" + exclude_files: + - "catalog_query_options.go" +