fasten-onprem/backend/pkg/models/database/generate.go

705 lines
26 KiB
Go

//go:build exclude
//go:generate go run generate.go
package main
import (
"encoding/json"
"fmt"
"github.com/samber/lo"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"github.com/dave/jennifer/jen"
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/errors"
"github.com/iancoleman/strcase"
"golang.org/x/exp/slices"
)
type SearchParameter struct {
Id string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Experimental bool `json:"experimental"`
Description string `json:"description"`
Code string `json:"code"`
Base []string `json:"base"`
Type string `json:"type"`
XpathUsage string `json:"xpathUsage"`
Expression string `json:"expression"`
Target []string `json:"target"`
}
type SearchParameterBundle struct {
Entry []struct {
Resource SearchParameter `json:"resource"`
} `json:"entry"`
}
type DBField struct {
FieldType string
Description string
FHIRPathExpression string
}
var licenseComment = strings.Split(strings.Trim(`
THIS FILE IS GENERATED BY https://github.com/fastenhealth/fasten-onprem/blob/main/backend/pkg/models/database/generate.go
PLEASE DO NOT EDIT BY HAND
`, "\n"), "\n")
func main() {
// Read config file for database type
appconfig, err := config.Create()
if err != nil {
fmt.Printf("FATAL: %+v\n", err)
os.Exit(1)
}
// Find and read the config file
err = appconfig.ReadConfig("../../../../config.yaml")
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
//ignore "could not find config file"
} else if err != nil {
os.Exit(1)
}
databaseType := appconfig.GetString("database.type")
// Read the search-parameters.json file
searchParamsData, err := ioutil.ReadFile("search-parameters.json")
if err != nil {
log.Fatal(err)
}
// Parse the search-parameters.json file
var searchParamsBundle SearchParameterBundle
err = json.Unmarshal(searchParamsData, &searchParamsBundle)
if err != nil {
log.Fatal(err)
}
// Parse the choiceTypePaths.json file
choiceTypePathsData, err := ioutil.ReadFile("choiceTypePaths.json")
if err != nil {
log.Fatal(err)
}
var choiceTypePathsLookup map[string][]string
err = json.Unmarshal(choiceTypePathsData, &choiceTypePathsLookup)
if err != nil {
log.Fatal(err)
}
resourceFieldMap := map[string]map[string]DBField{}
// Generate Go structs for each resource type
for _, entry := range searchParamsBundle.Entry {
if entry.Resource.Status != "active" && entry.Resource.Status != "draft" {
continue
}
if entry.Resource.Type == "composite" || entry.Resource.Type == "special" {
continue
}
if entry.Resource.Name == "patient" {
//skip Patient, not needed for searching.
continue
}
camelCaseResourceName := strcase.ToCamel(entry.Resource.Name)
//log.Printf("processing %v", entry.Resource.Id)
for _, resourceName := range entry.Resource.Base {
if !slices.Contains(AllowedResources, resourceName) {
continue
}
fieldMap, ok := resourceFieldMap[resourceName]
if !ok {
fieldMap = map[string]DBField{}
}
fieldMap[camelCaseResourceName] = DBField{
FieldType: entry.Resource.Type,
Description: entry.Resource.Description,
FHIRPathExpression: entry.Resource.Expression,
}
resourceFieldMap[resourceName] = fieldMap
}
}
// make sure all "base" resources have a field map
for _, resourceName := range AllowedResources {
_, ok := resourceFieldMap[resourceName]
if !ok {
resourceFieldMap[resourceName] = map[string]DBField{}
}
}
//add default fields to all resources
for resourceName, fieldMap := range resourceFieldMap {
fieldMap["MetaLastUpdated"] = DBField{
FieldType: "date",
Description: "When the resource version last changed",
FHIRPathExpression: "Resource.meta.lastUpdated",
}
fieldMap["Language"] = DBField{
FieldType: "token",
Description: "Language of the resource content",
FHIRPathExpression: "Resource.language",
}
fieldMap["MetaProfile"] = DBField{
FieldType: "reference",
Description: "Profiles this resource claims to conform to",
FHIRPathExpression: "Resource.meta.profile",
}
fieldMap["MetaVersionId"] = DBField{
FieldType: "keyword",
Description: "Tags applied to this resource",
FHIRPathExpression: "Resource.meta.versionId",
}
fieldMap["MetaTag"] = DBField{
FieldType: "token",
Description: "Tags applied to this resource",
FHIRPathExpression: "Resource.meta.tag",
}
fieldMap["Text"] = DBField{
FieldType: "keyword",
Description: "Text search against the narrative",
FHIRPathExpression: "text",
}
fieldMap["Type"] = DBField{
FieldType: "special",
Description: "A resource type filter",
}
resourceFieldMap[resourceName] = fieldMap
}
// create files for each resource type
for resourceName, fieldMap := range resourceFieldMap {
file := jen.NewFile("database")
for _, line := range licenseComment {
file.HeaderComment(line)
}
// Generate fields for search parameters. Make sure they are in a sorted order, otherwise the generated code will be different each time.
keys := make([]string, 0, len(fieldMap))
for k, _ := range fieldMap {
keys = append(keys, k)
}
// Generate struct declaration
structName := "Fhir" + strings.Title(resourceName)
file.Type().Id(structName).StructFunc(func(g *jen.Group) {
//Add the OriginBase embedded struct
g.Qual("github.com/fastenhealth/fasten-onprem/backend/pkg/models", "ResourceBase")
sort.Strings(keys)
for _, fieldName := range keys {
fieldInfo := fieldMap[fieldName]
g.Comment(fieldInfo.Description)
if fieldInfo.FieldType == "keyword" {
g.Comment("This is a primitive string literal (`keyword` type). It is not a recognized SearchParameter type from https://hl7.org/fhir/r4/search.html, it's Fasten Health-specific")
} else {
g.Comment(fmt.Sprintf("https://hl7.org/fhir/r4/search.html#%s", fieldInfo.FieldType))
}
golangFieldType := mapFieldType(fieldInfo.FieldType)
var isPointer bool
if strings.HasPrefix(golangFieldType, "*") {
golangFieldType = strings.TrimPrefix(golangFieldType, "*")
isPointer = true
}
var golangFieldStatement *jen.Statement
if strings.Contains(golangFieldType, "#") {
golangFieldTypeParts := strings.Split(golangFieldType, "#")
if isPointer {
golangFieldStatement = g.Id(fieldName).Op("*").Add(jen.Qual(golangFieldTypeParts[0], golangFieldTypeParts[1]))
} else {
golangFieldStatement = g.Id(fieldName).Add(jen.Qual(golangFieldTypeParts[0], golangFieldTypeParts[1]))
}
} else {
if isPointer {
golangFieldStatement = g.Id(fieldName).Op("*").Id(golangFieldType)
} else {
golangFieldStatement = g.Id(fieldName).Id(golangFieldType)
}
}
if databaseType == "sqlite" {
golangFieldStatement.Tag(map[string]string{
"json": fmt.Sprintf("%s,omitempty", strcase.ToLowerCamel(fieldName)),
"gorm": fmt.Sprintf("column:%s;%s", strcase.ToLowerCamel(fieldName), mapGormTypeSqlite(fieldInfo.FieldType)),
})
} else {
golangFieldStatement.Tag(map[string]string{
"json": fmt.Sprintf("%s,omitempty", strcase.ToLowerCamel(fieldName)),
"gorm": fmt.Sprintf("column:%s;%s", strcase.ToLowerCamel(fieldName), mapGormTypePostgres(fieldInfo.FieldType)),
})
}
}
})
//create an instance function that returns a map of all fields and their types
file.Func().Call(jen.Id("s").Op("*").Id(structName)).Id("GetSearchParameters").Params().Params(jen.Map(jen.String()).String()).BlockFunc(func(g *jen.Group) {
g.Id("searchParameters").Op(":=").Map(jen.String()).String().Values(jen.DictFunc(func(d jen.Dict) {
for _, fieldName := range keys {
fieldInfo := fieldMap[fieldName]
fieldNameLowerCamel := strcase.ToLowerCamel(fieldName)
d[jen.Lit(fieldNameLowerCamel)] = jen.Lit(fieldInfo.FieldType)
}
d[jen.Lit("id")] = jen.Lit("keyword")
d[jen.Lit("source_id")] = jen.Lit("keyword")
d[jen.Lit("source_uri")] = jen.Lit("keyword")
d[jen.Lit("source_resource_id")] = jen.Lit("keyword")
d[jen.Lit("source_resource_type")] = jen.Lit("keyword")
d[jen.Lit("sort_date")] = jen.Lit("date")
}))
g.Return(jen.Id("searchParameters"))
})
//create an instance function that extracts all search parameters from the raw resource and populates the struct
file.Func().Call(jen.Id("s").Op("*").Id(structName)).Id("PopulateAndExtractSearchParameters").Params(jen.Id("resourceRaw").Qual("encoding/json", "RawMessage")).Params(jen.Error()).BlockFunc(func(g *jen.Group) {
//set resourceRaw to ResourceRaw field
g.Id("s.ResourceRaw").Op("=").Qual("gorm.io/datatypes", "JSON").Call(jen.Id("resourceRaw"))
g.Comment("unmarshal the raw resource (bytes) into a map")
g.Var().Id("resourceRawMap").Map(jen.String()).Interface()
g.Err().Op(":=").Qual("encoding/json", "Unmarshal").Call(jen.Id("resourceRaw"), jen.Op("&").Id("resourceRawMap"))
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
g.Return(jen.Err())
})
//check length of fhirPathJs script (may not have been embedded correctly)
g.If(jen.Len(jen.Id("fhirPathJs")).Op("==").Lit(0)).BlockFunc(func(f *jen.Group) {
f.Return(jen.Qual("fmt", "Errorf").Call(jen.Lit("fhirPathJs script is empty")))
})
//initialize goja vm
g.Id("vm").Op(":=").Qual("github.com/dop251/goja", "New").Call()
g.Comment("setup the global window object")
g.Id("vm").Dot("Set").Call(jen.Lit("window"), jen.Id("vm").Dot("NewObject").Call())
g.Comment("set the global FHIR Resource object")
g.Id("vm").Dot("Set").Call(jen.Lit("fhirResource"), jen.Id("resourceRawMap"))
g.Comment("compile the fhirpath library")
g.List(jen.Id("fhirPathJsProgram"), jen.Id("err")).Op(":=").Qual("github.com/dop251/goja", "Compile").Call(jen.Lit("fhirpath.min.js"), jen.Id("fhirPathJs"), jen.True())
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(e *jen.Group) {
e.Return(jen.Err())
})
g.Comment("compile the searchParametersExtractor library")
g.List(jen.Id("searchParametersExtractorJsProgram"), jen.Id("err")).Op(":=").Qual("github.com/dop251/goja", "Compile").Call(jen.Lit("searchParameterExtractor.js"), jen.Id("searchParameterExtractorJs"), jen.True())
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(e *jen.Group) {
e.Return(jen.Err())
})
g.Comment("add the fhirpath library in the goja vm")
g.List(jen.Id("_"), jen.Id("err")).Op("=").Id("vm").Dot("RunProgram").Call(jen.Id("fhirPathJsProgram"))
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(e *jen.Group) {
e.Return(jen.Err())
})
g.Comment("add the searchParametersExtractor library in the goja vm")
g.List(jen.Id("_"), jen.Id("err")).Op("=").Id("vm").Dot("RunProgram").Call(jen.Id("searchParametersExtractorJsProgram"))
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(e *jen.Group) {
e.Return(jen.Err())
})
g.Comment("execute the fhirpath expression for each search parameter")
for _, fieldName := range keys {
fieldInfo := fieldMap[fieldName]
//skip any empty fhirpath expressions, we cant extract anything
if len(fieldInfo.FHIRPathExpression) == 0 {
continue
} else {
//TODO: "Observation.value as CodeableConcept" and other similar expressions do not work with goja in Golang, but do work in Javascript
// we're unsure why, but we can work around this by removing the " as " part of the expression, and instead use the fully qualified field name:
// "Observation.valueCodeableConcept" instead of "Observation.value as CodeableConcept"
// however, we cannot just remove the " As "string, as there are primitive types that start with a lowercase letter, and we need to uppercase the first letter
// https://www.hl7.org/fhir/R4/datatypes.html#CodeableConcept
// https://hl7.org/fhir/r4/formats.html#choice
// this is a very naive implementation, but it works for now
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as string", " as String")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(string)", " as String")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as time", " as Time")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(time)", " as Time")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as date", " as Date")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(date)", " as Date")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as dateTime", " as DateTime")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(dateTime)", " as DateTime")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as boolean", " as Boolean")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(boolean)", " as Boolean")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as url", " as Url")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(url)", " as Url")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as code", " as Code")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(code)", " as Code")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as integer", " as Integer")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(integer)", " as Integer")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as uri", " as Uri")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(uri)", " as Uri")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as decimal", " as Decimal")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(decimal)", " as Decimal")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(Age)", " as Age")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(Period)", " as Period")
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, ".as(Range)", " as Range")
//remove all " as " from the fhirpath expression, this does not work correctly with goja or otto
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, " as ", "")
//remove `Resource.` prefix from resource expression
fieldInfo.FHIRPathExpression = strings.ReplaceAll(fieldInfo.FHIRPathExpression, "Resource.", "")
//next, lets see if this fhir path expression is missing choices, and if so, lets add them
newFHIRPathExpressionParts := []string{}
lo.ForEach(strings.Split(fieldInfo.FHIRPathExpression, " | "), func(expression string, i int) {
normalizedExpression := strings.Trim(strings.TrimSpace(expression), "()")
if choiceTypesPathSuffixes, ok := choiceTypePathsLookup[normalizedExpression]; ok {
//this is a choice type (Observation.value[x]), lets add all the choice types to the expression
lo.ForEach(choiceTypesPathSuffixes, func(choiceTypeSufix string, i int) {
newFHIRPathExpressionParts = append(newFHIRPathExpressionParts, fmt.Sprintf("%s%s", normalizedExpression, choiceTypeSufix))
})
} else {
//do nothing, this is not a choice type
newFHIRPathExpressionParts = append(newFHIRPathExpressionParts, expression)
}
})
fieldInfo.FHIRPathExpression = strings.Join(newFHIRPathExpressionParts, " | ")
}
g.Comment(fmt.Sprintf("extracting %s", fieldName))
fieldNameVar := fmt.Sprintf("%sResult", strcase.ToLowerCamel(fieldName))
g.List(jen.Id(fieldNameVar), jen.Id("err")).Op(":=").Id("vm").Dot("RunString").CallFunc(func(r *jen.Group) {
if fieldInfo.FieldType == "string" {
//strings are unusual in that they can contain HumanName and Address types, which are not actually simple types
//we need to do some additional processing,
r.Lit(fmt.Sprintf("extractStringSearchParameters(fhirResource, '%s')", fieldInfo.FHIRPathExpression))
} else if fieldInfo.FieldType == "date" {
//dates are unusual in that they can contain Period types, which are actually a range of Date/DateTimes
//we need to do some additional processing
//our naiive solution is to drop the "end" of the Period range.
r.Lit(fmt.Sprintf("extractDateSearchParameters(fhirResource, '%s')", fieldInfo.FHIRPathExpression))
} else if isSimpleFieldType(fieldInfo.FieldType) {
//"Don't JSON.stringfy simple types"
r.Lit(fmt.Sprintf("extractSimpleSearchParameters(fhirResource, '%s')", fieldInfo.FHIRPathExpression))
} else if fieldInfo.FieldType == "token" {
r.Lit(fmt.Sprintf("extractTokenSearchParameters(fhirResource, '%s')", fieldInfo.FHIRPathExpression))
} else if fieldInfo.FieldType == "reference" {
r.Lit(fmt.Sprintf("extractReferenceSearchParameters(fhirResource, '%s')", fieldInfo.FHIRPathExpression))
} else {
r.Lit(fmt.Sprintf("extractCatchallSearchParameters(fhirResource, '%s')", fieldInfo.FHIRPathExpression))
}
})
g.If(jen.Err().Op("==").Nil().Op("&&").Id(fieldNameVar).Dot("String").Call().Op("!=").Lit("undefined")).BlockFunc(func(i *jen.Group) {
switch fieldInfo.FieldType {
case "token", "special", "quantity", "string", "reference":
i.Id("s").Dot(fieldName).Op("=").Index().Byte().Parens(jen.Id(fieldNameVar).Dot("String").Call())
break
case "number":
i.Id("s").Dot(fieldName).Op("=").Id(fieldNameVar).Dot("ToFloat").Call()
break
case "date":
//parse RFC3339 date
i.List(jen.Id("t"), jen.Id("err")).Op(":=").Qual("time", "Parse").Call(jen.Qual("time", "RFC3339"), jen.Id(fieldNameVar).Dot("String").Call())
i.If(jen.Err().Op("==").Nil()).BlockFunc(func(e *jen.Group) {
e.Id("s").Dot(fieldName).Op("=").Op("&").Id("t")
}).Else().If(jen.Err().Op("!=").Nil()).BlockFunc(func(e *jen.Group) {
//parse date only
e.List(jen.Id("d"), jen.Id("err")).Op(":=").Qual("time", "Parse").Call(jen.Lit("2006-01-02"), jen.Id(fieldNameVar).Dot("String").Call())
e.If(jen.Err().Op("==").Nil()).BlockFunc(func(f *jen.Group) {
f.Id("s").Dot(fieldName).Op("=").Op("&").Id("d")
})
})
case "uri":
i.Id("s").Dot(fieldName).Op("=").Id(fieldNameVar).Dot("String").Call()
break
default:
i.Id("s").Dot(fieldName).Op("=").Id(fieldNameVar).Dot("String").Call()
break
}
//if fieldName == "Text" {
// //convert html to markdown
// i.Id("converter").Op(":=").Qual("github.com/JohannesKaufmann/html-to-markdown", "NewConverter").Call(jen.Lit(""), jen.True(), jen.Nil())
// i.List(jen.Id("markdown"), jen.Id("err")).Op(":=").Id("converter").Dot("ConvertString").Call(jen.Id("s").Dot(fieldName))
// i.If(jen.Err().Op("==").Nil()).BlockFunc(func(q *jen.Group) {
// q.Id("s").Dot(fieldName).Op("=").Id("markdown")
// })
//}
})
}
g.Return(jen.Nil())
})
file.Comment("TableName overrides the table name from fhir_observations (pluralized) to `fhir_observation`. https://gorm.io/docs/conventions.html#TableName")
file.Func().Call(jen.Id("s").Op("*").Id(structName)).Id("TableName").Params().Params(jen.String()).BlockFunc(func(g *jen.Group) {
g.Return(jen.Lit(strcase.ToSnake(structName)))
})
// Save the generated Go code to a file
filename := fmt.Sprintf("%s.go", strcase.ToSnake(structName))
fmt.Printf("Generated Go struct for %s: %s\n", structName, filename)
err = file.Save(filename)
if err != nil {
log.Fatal(err)
}
}
bytes, err := json.MarshalIndent(resourceFieldMap["Observation"], "", " ")
log.Printf("%s, %v", string(bytes), err)
utilsFile := jen.NewFile("database")
// Generate go embed code for the fhirpath.js file
//utilsFile.ImportName("embed", "")
utilsFile.Anon("embed")
utilsFile.Comment("//go:embed fhirpath.min.js")
utilsFile.Var().Id("fhirPathJs").String()
utilsFile.Anon("embed")
utilsFile.Comment("//go:embed searchParameterExtractor.js")
utilsFile.Var().Id("searchParameterExtractorJs").String()
utilsFile.Comment("Generates all tables in the database associated with these models")
utilsFile.Func().Id("Migrate").Params(
jen.Id("gormClient").Op("*").Qual("gorm.io/gorm", "DB"),
).Params(jen.Error()).BlockFunc(func(g *jen.Group) {
/*
err := sr.GormClient.AutoMigrate(
&models.User{},
&models.SourceCredential{},
&models.ResourceFhir{},
&models.Glossary{},
)
if err != nil {
return fmt.Errorf("Failed to automigrate! - %v", err)
}
return nil
*/
g.Id("err").Op(":=").Id("gormClient").Dot("AutoMigrate").CallFunc(func(g *jen.Group) {
for _, resourceName := range AllowedResources {
g.Op("&").Id("Fhir" + resourceName).Values()
}
})
g.If(jen.Id("err").Op("!=").Nil()).Values(jen.Return(jen.Id("err")))
g.Return(jen.Nil())
})
//A function which returns a the corresponding FhirResource when provided the FhirResource type string
//uses a switch statement to return the correct type
utilsFile.Comment("Returns a map of all the resource names to their corresponding go struct")
utilsFile.Func().Id("NewFhirResourceModelByType").Params(jen.Id("resourceType").String()).Params(jen.Id("IFhirResourceModel"), jen.Error()).BlockFunc(func(g *jen.Group) {
g.Switch(jen.Id("resourceType")).BlockFunc(func(s *jen.Group) {
for _, resourceName := range AllowedResources {
s.Case(jen.Lit(resourceName)).BlockFunc(func(c *jen.Group) {
c.Return(jen.Op("&").Id("Fhir"+resourceName).Values(), jen.Nil())
})
}
s.Default().BlockFunc(func(d *jen.Group) {
d.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call(jen.Lit("Invalid resource type for model: %s"), jen.Id("resourceType")))
})
})
})
//A function which returns the GORM table name for a FHIRResource when provided the FhirResource type string
//uses a switch statement to return the correct type
utilsFile.Comment("Returns the GORM table name for a FHIRResource when provided the FhirResource type string")
utilsFile.Func().Id("GetTableNameByResourceType").Params(jen.Id("resourceType").String()).Params(jen.String(), jen.Error()).BlockFunc(func(g *jen.Group) {
g.Switch(jen.Id("resourceType")).BlockFunc(func(s *jen.Group) {
for _, resourceName := range AllowedResources {
s.Case(jen.Lit(resourceName)).BlockFunc(func(c *jen.Group) {
c.Return(jen.Lit(strcase.ToSnake("Fhir"+resourceName)), jen.Nil())
})
}
s.Default().BlockFunc(func(d *jen.Group) {
d.Return(jen.Lit(""), jen.Qual("fmt", "Errorf").Call(jen.Lit("Invalid resource type for table name: %s"), jen.Id("resourceType")))
})
})
})
//A function which returns all allowed resource types
utilsFile.Comment("Returns a slice of all allowed resource types")
utilsFile.Func().Id("GetAllowedResourceTypes").Params().Params(jen.Index().String()).BlockFunc(func(g *jen.Group) {
g.Return(jen.Index().String().ValuesFunc(func(g *jen.Group) {
for _, resourceName := range AllowedResources {
g.Lit(resourceName)
}
}))
})
// Save the generated Go code to a file
err = utilsFile.Save("utils.go")
if err != nil {
log.Fatal(err)
}
}
// TODO: should we do this, or allow all resources instead of just USCore?
// The dataabase would be full of empty data, but we'd be more flexible & future-proof.. supporting other countries, etc.
var AllowedResources = []string{
"Account",
"AdverseEvent",
"AllergyIntolerance",
"Appointment",
"Binary",
"CarePlan",
"CareTeam",
"Claim",
"ClaimResponse",
"Composition",
"Condition",
"Consent",
"Coverage",
"CoverageEligibilityRequest",
"CoverageEligibilityResponse",
"Device",
"DeviceRequest",
"DiagnosticReport",
"DocumentManifest",
"DocumentReference",
"Encounter",
"Endpoint",
"EnrollmentRequest",
"EnrollmentResponse",
"ExplanationOfBenefit",
"FamilyMemberHistory",
"Goal",
"ImagingStudy",
"Immunization",
"InsurancePlan",
"Location",
"Media",
"Medication",
"MedicationAdministration",
"MedicationDispense",
"MedicationRequest",
"MedicationStatement",
"NutritionOrder",
"Observation",
"Organization",
"OrganizationAffiliation",
"Patient",
"Person",
"Practitioner",
"PractitionerRole",
"Procedure",
"Provenance",
"Questionnaire",
"QuestionnaireResponse",
"RelatedPerson",
"Schedule",
"ServiceRequest",
"Slot",
"Specimen",
"VisionPrescription",
}
// simple field types are not json encoded in the DB and are always single values (not arrays)
func isSimpleFieldType(fieldType string) bool {
switch fieldType {
case "number", "uri", "date", "keyword":
return true
case "token", "reference", "special", "quantity", "string":
return false
default:
return true
}
return true
}
// https://hl7.org/fhir/search.html#token
// https://hl7.org/fhir/r4/valueset-search-param-type.html
func mapFieldType(fieldType string) string {
switch fieldType {
case "number":
return "float64"
case "token":
return "gorm.io/datatypes#JSON"
case "reference":
return "gorm.io/datatypes#JSON"
case "date":
return "*time#Time"
case "string":
return "gorm.io/datatypes#JSON"
case "uri":
return "string"
case "special":
return "gorm.io/datatypes#JSON"
case "quantity":
return "gorm.io/datatypes#JSON"
case "keyword":
return "string"
default:
return "string"
}
}
// https://www.sqlite.org/datatype3.html
func mapGormTypeSqlite(fieldType string) string {
// gorm:"type:text;serializer:json"
switch fieldType {
case "number":
return "type:real"
case "token":
return "type:text;serializer:json"
case "reference":
return "type:text;serializer:json"
case "date":
return "type:datetime"
case "string":
return "type:text;serializer:json"
case "uri":
return "type:text"
case "special":
return "type:text;serializer:json"
case "quantity":
return "type:text;serializer:json"
case "keyword":
return "type:text"
default:
return "type:text"
}
}
func mapGormTypePostgres(fieldType string) string {
switch fieldType {
case "number":
return "type:real"
case "token":
return "type:text;serializer:json"
case "reference":
return "type:text;serializer:json"
case "date":
return "type:timestamptz"
case "string":
return "type:text;serializer:json"
case "uri":
return "type:text"
case "special":
return "type:text;serializer:json"
case "quantity":
return "type:text;serializer:json"
case "keyword":
return "type:text"
default:
return "type:text"
}
}