2023-01-07 22:25:56 +02:00
package resolvers
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/inflector"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/security"
)
// parseAndRun starts a new one-off RecordFieldResolver.Resolve execution.
func parseAndRun ( fieldName string , resolver * RecordFieldResolver ) ( * search . ResolverResult , error ) {
r := & runner {
fieldName : fieldName ,
resolver : resolver ,
}
return r . run ( )
}
type runner struct {
used bool // indicates whether the runner was already executed
resolver * RecordFieldResolver // resolver is the shared expression fields resolver
fieldName string // the name of the single field expression the runner is responsible for
// shared processing state
// ---------------------------------------------------------------
activeProps [ ] string // holds the active props that remains to be processed
activeCollectionName string // the last used collection name
activeTableAlias string // the last used table alias
allowHiddenFields bool // indicates whether hidden fields (eg. email) should be allowed without extra conditions
nullifyMisingField bool // indicating whether to return null on missing field or return an error
withMultiMatch bool // indicates whether to attach a multiMatchSubquery condition to the ResolverResult
multiMatchActiveTableAlias string // the last used multi-match table alias
multiMatch * multiMatchSubquery // the multi-match subquery expression generated from the fieldName
}
func ( r * runner ) run ( ) ( * search . ResolverResult , error ) {
if r . used {
return nil , fmt . Errorf ( "the runner was already used" )
}
if len ( r . resolver . allowedFields ) > 0 && ! list . ExistInSliceWithRegex ( r . fieldName , r . resolver . allowedFields ) {
return nil , fmt . Errorf ( "failed to resolve field %q" , r . fieldName )
}
defer func ( ) {
r . used = true
} ( )
r . prepare ( )
// check for @collection field (aka. non-relational join)
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
if r . activeProps [ 0 ] == "@collection" {
return r . processCollectionField ( )
}
if r . activeProps [ 0 ] == "@request" {
2023-07-17 23:13:39 +03:00
if r . resolver . requestInfo == nil {
2023-01-07 22:25:56 +02:00
return & search . ResolverResult { Identifier : "NULL" } , nil
}
if strings . HasPrefix ( r . fieldName , "@request.auth." ) {
return r . processRequestAuthField ( )
}
if strings . HasPrefix ( r . fieldName , "@request.data." ) && len ( r . activeProps ) > 2 {
name , modifier , err := splitModifier ( r . activeProps [ 2 ] )
if err != nil {
return nil , err
}
dataField := r . resolver . baseCollection . Schema . GetFieldByName ( name )
if dataField == nil {
return r . resolver . resolveStaticRequestField ( r . activeProps [ 1 : ] ... )
}
dataField . InitOptions ( )
// check for data relation field
if dataField . Type == schema . FieldTypeRelation && len ( r . activeProps ) > 3 {
2023-07-17 23:13:39 +03:00
return r . processRequestInfoRelationField ( dataField )
2023-01-07 22:25:56 +02:00
}
// check for select:each field
if modifier == eachModifier && dataField . Type == schema . FieldTypeSelect && len ( r . activeProps ) == 3 {
2023-07-17 23:13:39 +03:00
return r . processRequestInfoSelectEachModifier ( dataField )
2023-01-07 22:25:56 +02:00
}
// check for data arrayble fields ":length" modifier
if modifier == lengthModifier && list . ExistInSlice ( dataField . Type , schema . ArraybleFieldTypes ( ) ) && len ( r . activeProps ) == 3 {
2023-07-17 23:13:39 +03:00
return r . processRequestInfoLengthModifier ( dataField )
2023-01-07 22:25:56 +02:00
}
}
// some other @request.* static field
return r . resolver . resolveStaticRequestField ( r . activeProps [ 1 : ] ... )
}
// regular field
return r . processActiveProps ( )
}
func ( r * runner ) prepare ( ) {
r . activeProps = strings . Split ( r . fieldName , "." )
r . activeCollectionName = r . resolver . baseCollection . Name
r . activeTableAlias = inflector . Columnify ( r . activeCollectionName )
r . allowHiddenFields = r . resolver . allowHiddenFields
// always allow hidden fields since the @.* filter is a system one
if r . activeProps [ 0 ] == "@collection" || r . activeProps [ 0 ] == "@request" {
r . allowHiddenFields = true
}
// enable the ignore flag for missing @request.* fields for backward
// compatibility and consistency with all @request.* filter fields and types
r . nullifyMisingField = r . activeProps [ 0 ] == "@request"
// prepare a multi-match subquery
r . multiMatch = & multiMatchSubquery {
baseTableAlias : r . activeTableAlias ,
params : dbx . Params { } ,
}
r . multiMatch . fromTableName = inflector . Columnify ( r . activeCollectionName )
r . multiMatch . fromTableAlias = "__mm_" + r . activeTableAlias
r . multiMatchActiveTableAlias = r . multiMatch . fromTableAlias
r . withMultiMatch = false
}
func ( r * runner ) processCollectionField ( ) ( * search . ResolverResult , error ) {
if len ( r . activeProps ) < 3 {
return nil , fmt . Errorf ( "invalid @collection field path in %q" , r . fieldName )
}
2023-12-03 10:57:49 +02:00
// nameOrId or nameOrId:alias
collectionParts := strings . SplitN ( r . activeProps [ 1 ] , ":" , 2 )
collection , err := r . resolver . loadCollection ( collectionParts [ 0 ] )
2023-01-07 22:25:56 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to load collection %q from field path %q" , r . activeProps [ 1 ] , r . fieldName )
}
r . activeCollectionName = collection . Name
2023-12-03 10:57:49 +02:00
if len ( collectionParts ) == 2 && collectionParts [ 1 ] != "" {
r . activeTableAlias = inflector . Columnify ( "__collection_alias_" + collectionParts [ 1 ] )
} else {
r . activeTableAlias = inflector . Columnify ( "__collection_" + r . activeCollectionName )
}
2023-01-07 22:25:56 +02:00
r . withMultiMatch = true
// join the collection to the main query
r . resolver . registerJoin ( inflector . Columnify ( collection . Name ) , r . activeTableAlias , nil )
// join the collection to the multi-match subquery
r . multiMatchActiveTableAlias = "__mm" + r . activeTableAlias
r . multiMatch . joins = append ( r . multiMatch . joins , & join {
tableName : inflector . Columnify ( collection . Name ) ,
tableAlias : r . multiMatchActiveTableAlias ,
} )
// leave only the collection fields
// aka. @collection.someCollection.fieldA.fieldB -> fieldA.fieldB
r . activeProps = r . activeProps [ 2 : ]
return r . processActiveProps ( )
}
func ( r * runner ) processRequestAuthField ( ) ( * search . ResolverResult , error ) {
// plain auth field
// ---
if list . ExistInSlice ( r . fieldName , plainRequestAuthFields ) {
return r . resolver . resolveStaticRequestField ( r . activeProps [ 1 : ] ... )
}
// resolve the auth collection field
// ---
2023-07-17 23:13:39 +03:00
if r . resolver . requestInfo == nil || r . resolver . requestInfo . AuthRecord == nil || r . resolver . requestInfo . AuthRecord . Collection ( ) == nil {
2023-01-07 22:25:56 +02:00
return & search . ResolverResult { Identifier : "NULL" } , nil
}
2023-07-17 23:13:39 +03:00
collection := r . resolver . requestInfo . AuthRecord . Collection ( )
2023-01-07 22:25:56 +02:00
r . resolver . loadedCollections = append ( r . resolver . loadedCollections , collection )
r . activeCollectionName = collection . Name
r . activeTableAlias = "__auth_" + inflector . Columnify ( r . activeCollectionName )
// join the auth collection to the main query
r . resolver . registerJoin (
inflector . Columnify ( r . activeCollectionName ) ,
r . activeTableAlias ,
dbx . HashExp {
// aka. __auth_users.id = :userId
2023-07-17 23:13:39 +03:00
( r . activeTableAlias + ".id" ) : r . resolver . requestInfo . AuthRecord . Id ,
2023-01-07 22:25:56 +02:00
} ,
)
// join the auth collection to the multi-match subquery
r . multiMatchActiveTableAlias = "__mm_" + r . activeTableAlias
r . multiMatch . joins = append (
r . multiMatch . joins ,
& join {
tableName : inflector . Columnify ( r . activeCollectionName ) ,
tableAlias : r . multiMatchActiveTableAlias ,
on : dbx . HashExp {
2023-07-17 23:13:39 +03:00
( r . multiMatchActiveTableAlias + ".id" ) : r . resolver . requestInfo . AuthRecord . Id ,
2023-01-07 22:25:56 +02:00
} ,
} ,
)
// leave only the auth relation fields
// aka. @request.auth.fieldA.fieldB -> fieldA.fieldB
r . activeProps = r . activeProps [ 2 : ]
return r . processActiveProps ( )
}
2023-07-17 23:13:39 +03:00
func ( r * runner ) processRequestInfoLengthModifier ( dataField * schema . SchemaField ) ( * search . ResolverResult , error ) {
dataItems := list . ToUniqueStringSlice ( r . resolver . requestInfo . Data [ dataField . Name ] )
2023-01-07 22:25:56 +02:00
result := & search . ResolverResult {
Identifier : fmt . Sprintf ( "%d" , len ( dataItems ) ) ,
}
return result , nil
}
2023-07-17 23:13:39 +03:00
func ( r * runner ) processRequestInfoSelectEachModifier ( dataField * schema . SchemaField ) ( * search . ResolverResult , error ) {
2023-01-07 22:25:56 +02:00
options , ok := dataField . Options . ( * schema . SelectOptions )
if ! ok {
return nil , fmt . Errorf ( "failed to initialize field %q options" , dataField . Name )
}
2023-07-17 23:13:39 +03:00
dataItems := list . ToUniqueStringSlice ( r . resolver . requestInfo . Data [ dataField . Name ] )
2023-01-07 22:25:56 +02:00
rawJson , err := json . Marshal ( dataItems )
if err != nil {
return nil , fmt . Errorf ( "cannot marshalize the data select item for field %q" , r . activeProps [ 2 ] )
}
placeholder := "dataSelect" + security . PseudorandomString ( 4 )
cleanFieldName := inflector . Columnify ( dataField . Name )
jeTable := fmt . Sprintf ( "json_each({:%s})" , placeholder )
jeAlias := "__dataSelect_" + cleanFieldName + "_je"
r . resolver . registerJoin ( jeTable , jeAlias , nil )
result := & search . ResolverResult {
Identifier : fmt . Sprintf ( "[[%s.value]]" , jeAlias ) ,
Params : dbx . Params { placeholder : rawJson } ,
}
if options . MaxSelect != 1 {
r . withMultiMatch = true
}
if r . withMultiMatch {
placeholder2 := "mm" + placeholder
jeTable2 := fmt . Sprintf ( "json_each({:%s})" , placeholder2 )
jeAlias2 := "__mm" + jeAlias
r . multiMatch . joins = append ( r . multiMatch . joins , & join {
tableName : jeTable2 ,
tableAlias : jeAlias2 ,
} )
r . multiMatch . params [ placeholder2 ] = rawJson
r . multiMatch . valueIdentifier = fmt . Sprintf ( "[[%s.value]]" , jeAlias2 )
result . MultiMatchSubQuery = r . multiMatch
}
return result , nil
}
2023-07-17 23:13:39 +03:00
func ( r * runner ) processRequestInfoRelationField ( dataField * schema . SchemaField ) ( * search . ResolverResult , error ) {
2023-01-07 22:25:56 +02:00
options , ok := dataField . Options . ( * schema . RelationOptions )
if ! ok {
return nil , fmt . Errorf ( "failed to initialize data field %q options" , dataField . Name )
}
dataRelCollection , err := r . resolver . loadCollection ( options . CollectionId )
if err != nil {
return nil , fmt . Errorf ( "failed to load collection %q from data field %q" , options . CollectionId , dataField . Name )
}
var dataRelIds [ ] string
2023-07-17 23:13:39 +03:00
if r . resolver . requestInfo != nil && len ( r . resolver . requestInfo . Data ) != 0 {
dataRelIds = list . ToUniqueStringSlice ( r . resolver . requestInfo . Data [ dataField . Name ] )
2023-01-07 22:25:56 +02:00
}
if len ( dataRelIds ) == 0 {
return & search . ResolverResult { Identifier : "NULL" } , nil
}
r . activeCollectionName = dataRelCollection . Name
r . activeTableAlias = inflector . Columnify ( "__data_" + dataRelCollection . Name )
// join the data rel collection to the main collection
r . resolver . registerJoin (
inflector . Columnify ( r . activeCollectionName ) ,
r . activeTableAlias ,
dbx . In (
fmt . Sprintf ( "[[%s.id]]" , inflector . Columnify ( r . activeTableAlias ) ) ,
list . ToInterfaceSlice ( dataRelIds ) ... ,
) ,
)
if options . MaxSelect == nil || * options . MaxSelect != 1 {
r . withMultiMatch = true
}
// join the data rel collection to the multi-match subquery
r . multiMatchActiveTableAlias = inflector . Columnify ( "__data_mm_" + dataRelCollection . Name )
r . multiMatch . joins = append (
r . multiMatch . joins ,
& join {
tableName : inflector . Columnify ( r . activeCollectionName ) ,
tableAlias : r . multiMatchActiveTableAlias ,
on : dbx . In ( r . multiMatchActiveTableAlias + ".id" , list . ToInterfaceSlice ( dataRelIds ) ... ) ,
} ,
)
// leave only the data relation fields
// aka. @request.data.someRel.fieldA.fieldB -> fieldA.fieldB
r . activeProps = r . activeProps [ 3 : ]
return r . processActiveProps ( )
}
func ( r * runner ) processActiveProps ( ) ( * search . ResolverResult , error ) {
totalProps := len ( r . activeProps )
for i , prop := range r . activeProps {
collection , err := r . resolver . loadCollection ( r . activeCollectionName )
if err != nil {
return nil , fmt . Errorf ( "failed to resolve field %q" , prop )
}
// last prop
if i == totalProps - 1 {
// system field, aka. internal model prop
// (always available but not part of the collection schema)
// -------------------------------------------------------
if list . ExistInSlice ( prop , resolvableSystemFieldNames ( collection ) ) {
result := & search . ResolverResult {
Identifier : fmt . Sprintf ( "[[%s.%s]]" , r . activeTableAlias , inflector . Columnify ( prop ) ) ,
}
// allow querying only auth records with emails marked as public
if prop == schema . FieldNameEmail && ! r . allowHiddenFields {
result . AfterBuild = func ( expr dbx . Expression ) dbx . Expression {
2023-01-26 22:24:39 +02:00
return dbx . Enclose ( dbx . And ( expr , dbx . NewExp ( fmt . Sprintf (
2023-01-07 22:25:56 +02:00
"[[%s.%s]] = TRUE" ,
r . activeTableAlias ,
schema . FieldNameEmailVisibility ,
2023-01-26 22:24:39 +02:00
) ) ) )
2023-01-07 22:25:56 +02:00
}
}
if r . withMultiMatch {
r . multiMatch . valueIdentifier = fmt . Sprintf ( "[[%s.%s]]" , r . multiMatchActiveTableAlias , inflector . Columnify ( prop ) )
result . MultiMatchSubQuery = r . multiMatch
}
return result , nil
}
name , modifier , err := splitModifier ( prop )
if err != nil {
return nil , err
}
field := collection . Schema . GetFieldByName ( name )
if field == nil {
if r . nullifyMisingField {
return & search . ResolverResult { Identifier : "NULL" } , nil
}
return nil , fmt . Errorf ( "unknown field %q" , name )
}
cleanFieldName := inflector . Columnify ( field . Name )
// arrayble fields ":length" modifier
// -------------------------------------------------------
if modifier == lengthModifier && list . ExistInSlice ( field . Type , schema . ArraybleFieldTypes ( ) ) {
jePair := r . activeTableAlias + "." + cleanFieldName
result := & search . ResolverResult {
Identifier : jsonArrayLength ( jePair ) ,
}
if r . withMultiMatch {
jePair2 := r . multiMatchActiveTableAlias + "." + cleanFieldName
r . multiMatch . valueIdentifier = jsonArrayLength ( jePair2 )
result . MultiMatchSubQuery = r . multiMatch
}
return result , nil
}
// select field with ":each" modifier
// -------------------------------------------------------
if field . Type == schema . FieldTypeSelect && modifier == eachModifier {
jePair := r . activeTableAlias + "." + cleanFieldName
jeAlias := r . activeTableAlias + "_" + cleanFieldName + "_je"
r . resolver . registerJoin ( jsonEach ( jePair ) , jeAlias , nil )
result := & search . ResolverResult {
Identifier : fmt . Sprintf ( "[[%s.value]]" , jeAlias ) ,
}
field . InitOptions ( )
options , ok := field . Options . ( * schema . SelectOptions )
if ! ok {
return nil , fmt . Errorf ( "failed to initialize field %q options" , prop )
}
if options . MaxSelect != 1 {
r . withMultiMatch = true
}
if r . withMultiMatch {
jePair2 := r . multiMatchActiveTableAlias + "." + cleanFieldName
jeAlias2 := r . multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
r . multiMatch . joins = append ( r . multiMatch . joins , & join {
tableName : jsonEach ( jePair2 ) ,
tableAlias : jeAlias2 ,
} )
r . multiMatch . valueIdentifier = fmt . Sprintf ( "[[%s.value]]" , jeAlias2 )
result . MultiMatchSubQuery = r . multiMatch
}
return result , nil
}
// default
// -------------------------------------------------------
result := & search . ResolverResult {
Identifier : fmt . Sprintf ( "[[%s.%s]]" , r . activeTableAlias , cleanFieldName ) ,
}
if r . withMultiMatch {
r . multiMatch . valueIdentifier = fmt . Sprintf ( "[[%s.%s]]" , r . multiMatchActiveTableAlias , cleanFieldName )
result . MultiMatchSubQuery = r . multiMatch
}
2024-01-03 10:43:46 +02:00
// wrap in json_extract to ensure that top-level primitives
// stored as json work correctly when compared to their SQL equivalent
// (https://github.com/pocketbase/pocketbase/issues/4068)
if field . Type == schema . FieldTypeJson {
result . NoCoalesce = true
result . Identifier = "JSON_EXTRACT(" + result . Identifier + ", '$')"
if r . withMultiMatch {
r . multiMatch . valueIdentifier = "JSON_EXTRACT(" + r . multiMatch . valueIdentifier + ", '$')"
}
}
2023-01-07 22:25:56 +02:00
return result , nil
}
field := collection . Schema . GetFieldByName ( prop )
if field == nil {
if r . nullifyMisingField {
return & search . ResolverResult { Identifier : "NULL" } , nil
}
return nil , fmt . Errorf ( "unknown field %q" , prop )
}
// check if it is a json field
if field . Type == schema . FieldTypeJson {
var jsonPath strings . Builder
jsonPath . WriteString ( "$" )
for _ , p := range r . activeProps [ i + 1 : ] {
if _ , err := strconv . Atoi ( p ) ; err == nil {
jsonPath . WriteString ( "[" )
jsonPath . WriteString ( inflector . Columnify ( p ) )
jsonPath . WriteString ( "]" )
} else {
jsonPath . WriteString ( "." )
jsonPath . WriteString ( inflector . Columnify ( p ) )
}
}
result := & search . ResolverResult {
2024-01-03 10:43:46 +02:00
NoCoalesce : true ,
2023-01-07 22:25:56 +02:00
Identifier : fmt . Sprintf (
"JSON_EXTRACT([[%s.%s]], '%s')" ,
r . activeTableAlias ,
inflector . Columnify ( prop ) ,
jsonPath . String ( ) ,
) ,
}
if r . withMultiMatch {
r . multiMatch . valueIdentifier = fmt . Sprintf (
"JSON_EXTRACT([[%s.%s]], '%s')" ,
r . multiMatchActiveTableAlias ,
inflector . Columnify ( prop ) ,
jsonPath . String ( ) ,
)
result . MultiMatchSubQuery = r . multiMatch
}
return result , nil
}
// check if it is a relation field
if field . Type != schema . FieldTypeRelation {
return nil , fmt . Errorf ( "field %q is not a valid relation" , prop )
}
// join the relation to the main query
// ---
field . InitOptions ( )
options , ok := field . Options . ( * schema . RelationOptions )
if ! ok {
return nil , fmt . Errorf ( "failed to initialize field %q options" , prop )
}
relCollection , relErr := r . resolver . loadCollection ( options . CollectionId )
if relErr != nil {
return nil , fmt . Errorf ( "failed to find field %q collection" , prop )
}
cleanFieldName := inflector . Columnify ( field . Name )
2023-03-07 23:28:35 +02:00
prefixedFieldName := r . activeTableAlias + "." + cleanFieldName
2023-01-07 22:25:56 +02:00
newTableAlias := r . activeTableAlias + "_" + cleanFieldName
2023-03-07 23:28:35 +02:00
newCollectionName := relCollection . Name
if ! options . IsMultiple ( ) {
r . resolver . registerJoin (
inflector . Columnify ( newCollectionName ) ,
newTableAlias ,
dbx . NewExp ( fmt . Sprintf ( "[[%s.id]] = [[%s]]" , newTableAlias , prefixedFieldName ) ) ,
)
} else {
jeAlias := r . activeTableAlias + "_" + cleanFieldName + "_je"
r . resolver . registerJoin ( jsonEach ( prefixedFieldName ) , jeAlias , nil )
r . resolver . registerJoin (
inflector . Columnify ( newCollectionName ) ,
newTableAlias ,
dbx . NewExp ( fmt . Sprintf ( "[[%s.id]] = [[%s.value]]" , newTableAlias , jeAlias ) ) ,
)
}
2023-01-07 22:25:56 +02:00
r . activeCollectionName = newCollectionName
r . activeTableAlias = newTableAlias
// ---
// join the relation to the multi-match subquery
// ---
2023-03-07 23:28:35 +02:00
if options . IsMultiple ( ) {
2023-01-07 22:25:56 +02:00
r . withMultiMatch = true
}
newTableAlias2 := r . multiMatchActiveTableAlias + "_" + cleanFieldName
2023-03-07 23:28:35 +02:00
prefixedFieldName2 := r . multiMatchActiveTableAlias + "." + cleanFieldName
if ! options . IsMultiple ( ) {
r . multiMatch . joins = append (
r . multiMatch . joins ,
& join {
tableName : inflector . Columnify ( newCollectionName ) ,
tableAlias : newTableAlias2 ,
on : dbx . NewExp ( fmt . Sprintf ( "[[%s.id]] = [[%s]]" , newTableAlias2 , prefixedFieldName2 ) ) ,
} ,
)
} else {
jeAlias2 := r . multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
r . multiMatch . joins = append (
r . multiMatch . joins ,
& join {
tableName : jsonEach ( prefixedFieldName2 ) ,
tableAlias : jeAlias2 ,
} ,
& join {
tableName : inflector . Columnify ( newCollectionName ) ,
tableAlias : newTableAlias2 ,
on : dbx . NewExp ( fmt . Sprintf ( "[[%s.id]] = [[%s.value]]" , newTableAlias2 , jeAlias2 ) ) ,
} ,
)
}
2023-01-07 22:25:56 +02:00
2023-03-07 23:28:35 +02:00
r . multiMatchActiveTableAlias = newTableAlias2
2023-01-07 22:25:56 +02:00
// ---
}
return nil , fmt . Errorf ( "failed to resolve field %q" , r . fieldName )
}
func jsonArrayLength ( tableColumnPair string ) string {
return fmt . Sprintf (
// note: the case is used to normalize value access for single and multiple relations.
2023-07-17 11:38:19 +03:00
` json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END) ` ,
tableColumnPair , tableColumnPair , tableColumnPair , tableColumnPair , tableColumnPair ,
2023-01-07 22:25:56 +02:00
)
}
func jsonEach ( tableColumnPair string ) string {
return fmt . Sprintf (
// note: the case is used to normalize value access for single and multiple relations.
` json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) ` ,
tableColumnPair , tableColumnPair , tableColumnPair ,
)
}
func resolvableSystemFieldNames ( collection * models . Collection ) [ ] string {
result := schema . BaseModelFieldNames ( )
if collection . IsAuth ( ) {
result = append (
result ,
schema . FieldNameUsername ,
schema . FieldNameVerified ,
schema . FieldNameEmailVisibility ,
schema . FieldNameEmail ,
)
}
return result
}