From 05550c9b1d2b5cea48c1cf0f2be0eed8b8909a6b Mon Sep 17 00:00:00 2001 From: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:47:38 -0600 Subject: [PATCH] feat: admin search and search-ability of JSON fields (#11) * feat: search with admin params Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> * missing annotation Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> * more ports Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> * fix admin search Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> * test and cleanup Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> * add type, allow JSON fields to be searched Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> * fix imports Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> --------- Signed-off-by: Sarah Funkhouser <147884153+golanglemonade@users.noreply.github.com> --- annotation.go | 15 +- genhooks/gen.go | 10 - genhooks/gen_test.go | 35 --- genhooks/{gen_query.go => genquery.go} | 6 +- genhooks/{gen_schema.go => genschema.go} | 4 +- genhooks/{gen_search.go => gensearch.go} | 165 ++++++++--- genhooks/gensearch_test.go | 339 +++++++++++++++++++++++ genhooks/templates/search/graph.tpl | 44 ++- genhooks/templates/search/query.tpl | 12 +- mixin/audit_mixin.go | 6 + 10 files changed, 534 insertions(+), 102 deletions(-) delete mode 100644 genhooks/gen_test.go rename genhooks/{gen_query.go => genquery.go} (96%) rename genhooks/{gen_schema.go => genschema.go} (95%) rename genhooks/{gen_search.go => gensearch.go} (56%) create mode 100644 genhooks/gensearch_test.go diff --git a/annotation.go b/annotation.go index 337ee29..73955a8 100644 --- a/annotation.go +++ b/annotation.go @@ -34,12 +34,12 @@ type ThroughCleanup struct { } // SchemaGenAnnotation is an annotation used to indicate that schema generation should be skipped for this type -// Searchable will be ignored if Skip is set to true -// Searchable and SkipSearch allow for schemas to be be opt in or opt out +// When Skip is true, the search schema generation is always skipped +// SkipSearch allow for schemas to be be opt out of search schema generation type SchemaGenAnnotation struct { // Skip indicates that the schema generation should be skipped for this type Skip bool - // SkipSearch indicates that the schema should not be searchable, this is ignored if Skip is set to true + // SkipSearch indicates that the schema should not be searchable // Schemas are also not searchable if not fields are marked as searchable SkipSearch bool } @@ -53,6 +53,8 @@ type QueryGenAnnotation struct { type SearchFieldAnnotation struct { // Searchable indicates that the field should be searchable Searchable bool + // ExcludeAdmin indicates that the field will be excluded from the admin search which includes all fields by default + ExcludeAdmin bool } // Name returns the name of the CascadeAnnotation @@ -122,6 +124,13 @@ func FieldSearchable() *SearchFieldAnnotation { } } +// FieldAdminSearchable returns a new SearchFieldAnnotation with the exclude admin searchable flag set +func FieldAdminSearchable(s bool) *SearchFieldAnnotation { + return &SearchFieldAnnotation{ + ExcludeAdmin: !s, + } +} + // Decode unmarshalls the CascadeAnnotation func (a *CascadeAnnotation) Decode(annotation interface{}) error { buf, err := json.Marshal(annotation) diff --git a/genhooks/gen.go b/genhooks/gen.go index b015362..4f52445 100644 --- a/genhooks/gen.go +++ b/genhooks/gen.go @@ -27,13 +27,3 @@ func isSoftDeleteField(f *gen.Field) bool { func getFileName(dir, name string) string { return filepath.Clean(dir + strings.ToLower(name) + ".graphql") } - -// toFirstLower converts the first character of a string to lowercase -// except if the entire string is uppercase, e.g TTL, should remain as TTL -func toFirstLower(s string) string { - if strings.ToUpper(s) == s { - return s - } - - return strings.ToLower(s[:1]) + s[1:] -} diff --git a/genhooks/gen_test.go b/genhooks/gen_test.go deleted file mode 100644 index a5882fd..0000000 --- a/genhooks/gen_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package genhooks - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestToFirstLower(t *testing.T) { - testCases := []struct { - name string - input string - expected string - }{ - - { - name: "upper first", - input: "HelloThere", - expected: "helloThere", - }, - { - name: "all Upper", - input: "HELLO", - expected: "HELLO", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := toFirstLower(tc.input) - - assert.Equal(t, tc.expected, result) - }) - } -} diff --git a/genhooks/gen_query.go b/genhooks/genquery.go similarity index 96% rename from genhooks/gen_query.go rename to genhooks/genquery.go index 0cc665d..d352fd2 100644 --- a/genhooks/gen_query.go +++ b/genhooks/genquery.go @@ -9,8 +9,8 @@ import ( "entgo.io/contrib/entgql" "entgo.io/ent/entc/gen" + "github.com/99designs/gqlgen/codegen/templates" "github.com/gertd/go-pluralize" - "github.com/stoewer/go-strcase" "github.com/theopenlane/entx" ) @@ -97,7 +97,7 @@ func getFieldNames(fields []*gen.Field) []string { continue } - fieldNames = append(fieldNames, toFirstLower(f.StructField())) + fieldNames = append(fieldNames, templates.ToGoPrivate(f.StructField())) } // sort field names @@ -167,7 +167,7 @@ func isHistorySchema(node *gen.Type) bool { func createQuery() *template.Template { // function map for template fm := template.FuncMap{ - "ToLowerCamel": strcase.LowerCamelCase, + "ToLowerCamel": templates.ToGoPrivate, "ToPlural": pluralize.NewClient().Plural, } diff --git a/genhooks/gen_schema.go b/genhooks/genschema.go similarity index 95% rename from genhooks/gen_schema.go rename to genhooks/genschema.go index adb146b..698a8c7 100644 --- a/genhooks/gen_schema.go +++ b/genhooks/genschema.go @@ -6,8 +6,8 @@ import ( "os" "entgo.io/ent/entc/gen" + "github.com/99designs/gqlgen/codegen/templates" "github.com/gertd/go-pluralize" - "github.com/stoewer/go-strcase" "github.com/theopenlane/entx" ) @@ -77,7 +77,7 @@ func checkSchemaGenSkip(node *gen.Type) bool { func createTemplate() *template.Template { // function map for template fm := template.FuncMap{ - "ToLowerCamel": strcase.LowerCamelCase, + "ToLowerCamel": templates.ToGoPrivate, "ToPlural": pluralize.NewClient().Plural, } diff --git a/genhooks/gen_search.go b/genhooks/gensearch.go similarity index 56% rename from genhooks/gen_search.go rename to genhooks/gensearch.go index 80d58b0..28a286e 100644 --- a/genhooks/gen_search.go +++ b/genhooks/gensearch.go @@ -6,32 +6,42 @@ import ( "log" "os" "slices" - "strings" + "entgo.io/contrib/entgql" "entgo.io/ent/entc/gen" "entgo.io/ent/entc/load" + entfield "entgo.io/ent/schema/field" + "github.com/99designs/gqlgen/codegen/templates" "github.com/gertd/go-pluralize" "github.com/stoewer/go-strcase" "github.com/theopenlane/entx" ) -const ( - searchFilename = "search" -) - // schema data for template type search struct { - // Objects is a list of objects to generate bulk resolvers for + // Name of the search (e.g. Global, Admin) + Name string + // Objects is a list of objects to generate search resolvers for Objects []Object } -// Object is a struct to hold the object name for the bulk resolver +// Object is a struct to hold the object name for the search resolver type Object struct { // Name of the object Name string // Fields that are searchable for object - Fields []string + Fields []Field + // AdminFields are fields that are only searchable by admin + AdminFields []Field +} + +// Field is a struct to hold the field name and type +type Field struct { + // Name of the field + Name string + // Type of the field (string, json, etc) + Type string } // GenSchema generates graphql schemas when specified to be searchable @@ -39,7 +49,7 @@ func GenSearchSchema(graphSchemaDir, graphQueryDir string) gen.Hook { return func(next gen.Generator) gen.Generator { return gen.GenerateFunc(func(g *gen.Graph) error { // create schema template - tmpl := createSearchTemplate() + schemaTmpl := createSearchTemplate() // create query template queryTmpl := createSearchQueryTemplate() @@ -52,11 +62,13 @@ func GenSearchSchema(graphSchemaDir, graphQueryDir string) gen.Hook { return cmp.Compare(a.Name, b.Name) }) - // create search schema file - genSearchSchemaTemplate(graphSchemaDir, tmpl, inputData) + // create search schema file for global and admin + genSearchSchemaTemplate(graphSchemaDir, schemaTmpl, inputData, false) + genSearchSchemaTemplate(graphSchemaDir, schemaTmpl, inputData, true) - // create search query file - genSearchQueryTemplate(graphQueryDir, queryTmpl, inputData) + // create search query file for global and admin + genSearchQueryTemplate(graphQueryDir, queryTmpl, inputData, false) + genSearchQueryTemplate(graphQueryDir, queryTmpl, inputData, true) return next.Generate(g) }) @@ -79,13 +91,14 @@ func getInputData(g *gen.Graph) search { continue } - fields := GetSearchableFields(f.Name, g) + fields, adminFields := GetSearchableFields(f.Name, g) // only add object if there are searchable fields other than the ID field (ID is always searchable) if len(fields) > 1 { inputData.Objects = append(inputData.Objects, Object{ - Name: f.Name, - Fields: fields, + Name: f.Name, + Fields: fields, + AdminFields: adminFields, }) } } @@ -94,9 +107,16 @@ func getInputData(g *gen.Graph) search { } // genSearchSchemaTemplate generates the search schema file -func genSearchSchemaTemplate(graphSchemaDir string, tmpl *template.Template, inputData search) { +func genSearchSchemaTemplate(graphSchemaDir string, tmpl *template.Template, inputData search, isAdmin bool) { + fileName := getSearchFileName(isAdmin) + + inputData.Name = "Global" + if isAdmin { + inputData.Name = "Admin" + } + // create search schema file - filePath := getFileName(graphSchemaDir, searchFilename) + filePath := getFileName(graphSchemaDir, fileName) file, err := os.Create(filePath) if err != nil { @@ -110,9 +130,16 @@ func genSearchSchemaTemplate(graphSchemaDir string, tmpl *template.Template, inp } // genSearchQueryTemplate generates the search query file -func genSearchQueryTemplate(graphQueryDir string, tmpl *template.Template, inputData search) { +func genSearchQueryTemplate(graphQueryDir string, tmpl *template.Template, inputData search, isAdmin bool) { + fileName := getSearchFileName(isAdmin) + + inputData.Name = "Global" + if isAdmin { + inputData.Name = "Admin" + } + // create search query file - filePath := getFileName(graphQueryDir, searchFilename) + filePath := getFileName(graphQueryDir, fileName) file, err := os.Create(filePath) if err != nil { @@ -130,7 +157,7 @@ func createSearchTemplate() *template.Template { // function map for template fm := template.FuncMap{ "toPlural": pluralize.NewClient().Plural, - "toLowerCamel": strcase.LowerCamelCase, + "toLower": templates.ToGoPrivate, "toUpperCamel": strcase.UpperCamelCase, } @@ -148,7 +175,7 @@ func createSearchQueryTemplate() *template.Template { // function map for template fm := template.FuncMap{ "toPlural": pluralize.NewClient().Plural, - "toLowerCamel": strcase.LowerCamelCase, + "toLower": templates.ToGoPrivate, "toUpperCamel": strcase.UpperCamelCase, } @@ -177,24 +204,40 @@ func includeSchemaForSearch(node *gen.Type) bool { } // GetSearchableFields returns a list of searchable fields for a schema based on the search annotation -func GetSearchableFields(schemaName string, graph *gen.Graph) (fields []string) { +// all fields will be included in the admin search +// whereas only fields with the search annotation will be included in the global search +func GetSearchableFields(schemaName string, graph *gen.Graph) (fields []Field, adminFields []Field) { // add the object name that is being searched schema := getEntSchema(graph, schemaName) + if schema == nil { + return + } + for _, field := range schema.Fields { - if isFieldSearchable(field) { - fieldName := strcase.UpperCamelCase(field.Name) - // capitalize ID field - if strings.EqualFold(field.Name, "id") { - fieldName = "ID" - } + if isFieldTypeExcluded(field) { + continue + } + + fieldName := templates.ToGo(field.Name) - fields = append(fields, fieldName) + f := Field{ + Name: fieldName, + Type: field.Info.Type.String(), + } + + if isFieldSearchable(field) { + fields = append(fields, f) + adminFields = append(adminFields, f) + } else if isAdminFieldSearchable(field) { + adminFields = append(adminFields, f) } } // sort fields so we have consistent output - slices.Sort(fields) + slices.SortFunc(fields, func(a, b Field) int { + return cmp.Compare(a.Name, b.Name) + }) return } @@ -202,7 +245,6 @@ func GetSearchableFields(schemaName string, graph *gen.Graph) (fields []string) // isFieldSearchable checks if the field has the SearchField annotation func isFieldSearchable(field *load.Field) bool { searchAnt := &entx.SearchFieldAnnotation{} - if ant, ok := field.Annotations[searchAnt.Name()]; ok { if err := searchAnt.Decode(ant); err != nil { return false @@ -214,6 +256,26 @@ func isFieldSearchable(field *load.Field) bool { return false } +// isAdminFieldSearchable checks if the field has the admin SearchField annotation +// it also checks for the SkipWhereInput annotation to exclude the field from the search schema +func isAdminFieldSearchable(field *load.Field) bool { + searchAnt := &entx.SearchFieldAnnotation{} + + if entSkip(field) { + return false + } + + if ant, ok := field.Annotations[searchAnt.Name()]; ok { + if err := searchAnt.Decode(ant); err != nil { + return false + } + + return !searchAnt.ExcludeAdmin + } + + return true +} + // getEntSchema returns the schema for a given name func getEntSchema(graph *gen.Graph, name string) *load.Schema { for _, s := range graph.Schemas { @@ -224,3 +286,42 @@ func getEntSchema(graph *gen.Graph, name string) *load.Schema { return nil } + +// getFileName returns the file path for the search schema file +func getSearchFileName(isAdmin bool) string { + fileName := "search" + if isAdmin { + fileName = "admin" + fileName + } + + return fileName +} + +// entSkip checks if the field has the entgql.Skip annotation that would exclude the field from being searchable +func entSkip(field *load.Field) bool { + entAnt := &entgql.Annotation{} + if ant, ok := field.Annotations[entAnt.Name()]; ok { + if err := entAnt.Decode(ant); err == nil { + switch { + case entAnt.Skip.Is(entgql.SkipType): + return true + case entAnt.Skip.Is(entgql.SkipWhereInput): + return true + } + } + } + + return false +} + +// isFieldTypeExcluded checks if the field type should be excluded from being searchable +func isFieldTypeExcluded(field *load.Field) bool { + // exclude the following field types from being searchable + excludedTypes := []entfield.Type{ + entfield.TypeBool, + entfield.TypeEnum, + entfield.TypeTime, + } + + return slices.Contains(excludedTypes, field.Info.Type) +} diff --git a/genhooks/gensearch_test.go b/genhooks/gensearch_test.go new file mode 100644 index 0000000..5fd4f34 --- /dev/null +++ b/genhooks/gensearch_test.go @@ -0,0 +1,339 @@ +package genhooks + +import ( + "testing" + + "entgo.io/contrib/entgql" + "entgo.io/ent/entc/gen" + "entgo.io/ent/entc/load" + "entgo.io/ent/schema/field" + "github.com/stretchr/testify/assert" + "github.com/theopenlane/entx" +) + +func TestIsFieldTypeExcluded(t *testing.T) { + testCases := []struct { + name string + input *load.Field + expected bool + }{ + { + name: "bool, excluded", + input: &load.Field{ + Info: &field.TypeInfo{ + Type: field.TypeBool, + }, + }, + expected: true, + }, + { + name: "string, included", + input: &load.Field{ + Info: &field.TypeInfo{ + Type: field.TypeString, + }, + }, + expected: false, + }, + { + name: "time, excluded", + input: &load.Field{ + Info: &field.TypeInfo{ + Type: field.TypeTime, + }, + }, + expected: true, + }, + { + name: "enum, excluded", + input: &load.Field{ + Info: &field.TypeInfo{ + Type: field.TypeEnum, + }, + }, + expected: true, + }, + { + name: "json, excluded", + input: &load.Field{ + Info: &field.TypeInfo{ + Type: field.TypeJSON, + }, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := isFieldTypeExcluded(tc.input) + + assert.Equal(t, tc.expected, result) + }) + } +} +func TestEntSkip(t *testing.T) { + entAnt := &entgql.Annotation{} + + testCases := []struct { + name string + input *load.Field + expected bool + }{ + { + name: "no entql annotation, not skipped", + input: &load.Field{}, + expected: false, + }, + { + name: "skip update, not skipped", + input: &load.Field{ + Annotations: map[string]interface{}{ + entAnt.Name(): &entgql.Annotation{ + Skip: entgql.SkipMutationUpdateInput, + }, + }, + }, + expected: false, + }, + { + name: "skip where input, skipped", + input: &load.Field{ + Annotations: map[string]interface{}{ + entAnt.Name(): &entgql.Annotation{ + Skip: entgql.SkipWhereInput, + }, + }, + }, + expected: true, + }, + { + name: "skip all, skipped", + input: &load.Field{ + Annotations: map[string]interface{}{ + entAnt.Name(): &entgql.Annotation{ + Skip: entgql.SkipAll, + }, + }, + }, + expected: true, + }, + { + name: "skip type, skipped", + input: &load.Field{ + Annotations: map[string]interface{}{ + entAnt.Name(): &entgql.Annotation{ + Skip: entgql.SkipType, + }, + }, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := entSkip(tc.input) + + assert.Equal(t, tc.expected, result) + }) + } +} +func TestGetSearchFileName(t *testing.T) { + testCases := []struct { + name string + isAdmin bool + expected string + }{ + { + name: "not admin", + isAdmin: false, + expected: "search", + }, + { + name: "admin", + isAdmin: true, + expected: "adminsearch", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := getSearchFileName(tc.isAdmin) + + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestIncludeSchemaForSearch(t *testing.T) { + schemaGenAnt := &entx.SchemaGenAnnotation{} + + testCases := []struct { + name string + input *gen.Type + expected bool + }{ + { + name: "do not skip search, has annotation", + input: &gen.Type{ + Annotations: map[string]interface{}{ + schemaGenAnt.Name(): &entx.SchemaGenAnnotation{ + SkipSearch: false, + }, + }, + }, + expected: true, + }, + { + name: "skip search, has annotation", + input: &gen.Type{ + Annotations: map[string]interface{}{ + schemaGenAnt.Name(): &entx.SchemaGenAnnotation{ + SkipSearch: true, + }, + }, + }, + expected: false, + }, + { + name: "skip not set on annotation", + input: &gen.Type{ + Annotations: map[string]interface{}{ + schemaGenAnt.Name(): &entx.SchemaGenAnnotation{}, + }, + }, + expected: true, + }, + { + name: "no annotation", + input: &gen.Type{ + Annotations: map[string]interface{}{}, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := includeSchemaForSearch(tc.input) + + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestIsFieldSearchable(t *testing.T) { + searchAnt := &entx.SearchFieldAnnotation{} + + testCases := []struct { + name string + input *load.Field + expected bool + }{ + { + name: "searchable, has annotation", + input: &load.Field{ + Annotations: map[string]interface{}{ + searchAnt.Name(): &entx.SearchFieldAnnotation{ + Searchable: true, + }, + }, + }, + expected: true, + }, + { + name: "searchable false, has annotation", + input: &load.Field{ + Annotations: map[string]interface{}{ + searchAnt.Name(): &entx.SearchFieldAnnotation{ + Searchable: false, + }, + }, + }, + expected: false, + }, + { + name: "no searchable annotation, default false", + input: &load.Field{ + Annotations: map[string]interface{}{ + searchAnt.Name(): &entx.SearchFieldAnnotation{}, + }, + }, + expected: false, + }, + { + name: "no annotation, default false", + input: &load.Field{ + Annotations: map[string]interface{}{}, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := isFieldSearchable(tc.input) + + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestIsAdminFieldSearchable(t *testing.T) { + searchAnt := &entx.SearchFieldAnnotation{} + + testCases := []struct { + name string + input *load.Field + expected bool + }{ + { + name: "not excluded, has annotation", + input: &load.Field{ + Annotations: map[string]interface{}{ + searchAnt.Name(): &entx.SearchFieldAnnotation{ + Searchable: true, + ExcludeAdmin: false, + }, + }, + }, + expected: true, + }, + { + name: "excluded, has annotation", + input: &load.Field{ + Annotations: map[string]interface{}{ + searchAnt.Name(): &entx.SearchFieldAnnotation{ + ExcludeAdmin: true, + }, + }, + }, + expected: false, + }, + { + name: "no excluded annotation", + input: &load.Field{ + Annotations: map[string]interface{}{ + searchAnt.Name(): &entx.SearchFieldAnnotation{}, + }, + }, + expected: true, + }, + { + name: "no annotation", + input: &load.Field{ + Annotations: map[string]interface{}{}, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := isAdminFieldSearchable(tc.input) + + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/genhooks/templates/search/graph.tpl b/genhooks/templates/search/graph.tpl index 459808e..463cec1 100644 --- a/genhooks/templates/search/graph.tpl +++ b/genhooks/templates/search/graph.tpl @@ -1,31 +1,47 @@ -union GlobalSearchResult = +extend type Query{ + {{- range $object := $.Objects }} + """ + Search across {{ $object.Name }} objects + """ + {{- if eq $.Name "Global" }} + {{ $object.Name | toLower }}Search( + {{- else }} + {{ $.Name | toLower }}{{ $object.Name | toUpperCamel }}Search( + {{- end }} + """ + Search query + """ + query: String! + ): {{ $object.Name }}SearchResult + {{- end }} +} + +{{- if eq $.Name "Global" }} +union SearchResult = {{- range $object := $.Objects }} | {{ $object.Name | toUpperCamel }}SearchResult {{- end }} - -{{- range $object := $.Objects }} -type {{ $object.Name }}SearchResult { - {{ $object.Name | toLowerCamel | toPlural }}: [ {{ $object.Name | toUpperCamel}}!] -} - -{{- end }} - -type GlobalSearchResultConnection { +type SearchResultConnection { page: PageInfo! - nodes: [GlobalSearchResult!]! + nodes: [SearchResult!]! } - extend type Query{ """ - Search across objects + Search across all objects """ search( """ Search query """ query: String! - ): GlobalSearchResultConnection + ): SearchResultConnection +} +{{ range $object := $.Objects }} +type {{ $object.Name }}SearchResult { + {{ $object.Name | toLower | toPlural }}: [ {{ $object.Name | toUpperCamel}}!] } +{{ end }} +{{- end }} \ No newline at end of file diff --git a/genhooks/templates/search/query.tpl b/genhooks/templates/search/query.tpl index 2446c3a..9da4bfa 100644 --- a/genhooks/templates/search/query.tpl +++ b/genhooks/templates/search/query.tpl @@ -1,11 +1,17 @@ -query Search($query: String!) { +query {{ $.Name }}Search($query: String!) { search(query: $query) { nodes { {{- range $object := $.Objects }} ... on {{ $object.Name | toUpperCamel }}SearchResult { - {{ $object.Name| toLowerCamel | toPlural }} { + {{ $object.Name| toLower | toPlural }} { + {{- if eq $.Name "Admin" }} + {{- range $field := $object.AdminFields }} + {{ $field.Name | toLower }} + {{- end }} + {{- else }} {{- range $field := $object.Fields }} - {{ $field | toLowerCamel }} + {{ $field.Name | toLower }} + {{- end }} {{- end }} } } diff --git a/mixin/audit_mixin.go b/mixin/audit_mixin.go index 650d861..71644f1 100644 --- a/mixin/audit_mixin.go +++ b/mixin/audit_mixin.go @@ -10,6 +10,8 @@ import ( "entgo.io/ent/schema/mixin" "github.com/theopenlane/iam/auth" + + "github.com/theopenlane/entx" ) // AuditMixin provides auditing for all records where enabled. The created_at, created_by, updated_at, and updated_by records are automatically populated when this mixin is enabled. @@ -28,6 +30,7 @@ func (AuditMixin) Fields() []ent.Field { entgql.Skip( entgql.SkipMutationCreateInput, entgql.SkipMutationUpdateInput, ), + entx.FieldAdminSearchable(false), ), field.Time("updated_at"). Default(time.Now). @@ -37,6 +40,7 @@ func (AuditMixin) Fields() []ent.Field { entgql.Skip( entgql.SkipMutationCreateInput, entgql.SkipMutationUpdateInput, ), + entx.FieldAdminSearchable(false), ), field.String("created_by"). Immutable(). @@ -45,6 +49,7 @@ func (AuditMixin) Fields() []ent.Field { entgql.Skip( entgql.SkipMutationCreateInput, entgql.SkipMutationUpdateInput, ), + entx.FieldAdminSearchable(false), ), field.String("updated_by"). Optional(). @@ -52,6 +57,7 @@ func (AuditMixin) Fields() []ent.Field { entgql.Skip( entgql.SkipMutationCreateInput, entgql.SkipMutationUpdateInput, ), + entx.FieldAdminSearchable(false), ), } }