packages feed

ihp-graphql-1.5.0: IHP/GraphQL/SchemaCompiler.hs

module IHP.GraphQL.SchemaCompiler where

import IHP.Prelude hiding (Type)
import IHP.GraphQL.Types

import IHP.Postgres.Types


type SqlSchema = [Statement]
type GraphQLSchema = [Definition]

sqlSchemaToGraphQLSchema :: SqlSchema -> GraphQLSchema
sqlSchemaToGraphQLSchema statements =
            [ schemaDefinition
            , queryDefinition statements
            , mutationDefinition statements
            ]
            <> customScalars
            <> recordTypes statements
            <> newRecordTypes statements
            <> patchTypes statements

schemaDefinition :: Definition
schemaDefinition =
    TypeSystemDefinition { typeSystemDefinition = SchemaDefinition
        { queryType = NamedType "Query"
        , mutationType = NamedType "Mutation"
        }
    }

customScalars :: [Definition]
customScalars =
    ["UUID", "Timestamp"]
    |> map (TypeSystemDefinition . TypeDefinition . ScalarTypeDefinition)

queryDefinition :: SqlSchema -> Definition
queryDefinition statements = TypeSystemDefinition { typeSystemDefinition = TypeDefinition typeDefinition }
    where
        typeDefinition =
            ObjectTypeDefinition
                { name = "Query"
                , implementsInterfaces = []
                , fieldDefinitions = mconcat $ map statementToQueryField statements
                }


mutationDefinition :: SqlSchema -> Definition
mutationDefinition statements = TypeSystemDefinition { typeSystemDefinition = TypeDefinition typeDefinition }
    where
        typeDefinition =
            ObjectTypeDefinition
                { name = "Mutation"
                , implementsInterfaces = []
                , fieldDefinitions = mconcat $ map statementToMutationFields statements
                }

statementToQueryField :: Statement -> [FieldDefinition]
statementToQueryField (StatementCreateTable CreateTable { name }) = 
        [ manyRecordsField ]
    where
        manyRecordsField = FieldDefinition
            { description = Just ("Returns all records from the `" <> name <> "` table")
            , name = lcfirst (tableNameToControllerName name)
            , argumentsDefinition = []
            , type_
            }
        type_ = NonNullType (ListType (NonNullType (NamedType (tableNameToModelName name))))
statementToQueryField _ = []

statementToMutationFields :: Statement -> [FieldDefinition]
statementToMutationFields (StatementCreateTable CreateTable { name }) = 
        [ createRecord, updateRecord, deleteRecord ]
    where
        createRecord = FieldDefinition
            { description = Nothing
            , name = "create" <> tableNameToModelName name
            , argumentsDefinition =
                [ ArgumentDefinition { name = lcfirst (tableNameToModelName name), argumentType = NonNullType (NamedType ("New" <> tableNameToModelName name)), defaultValue = Nothing }
                ]
            , type_ = NonNullType (NamedType (tableNameToModelName name))
            }
        updateRecord = FieldDefinition
            { description = Nothing
            , name = "update" <> tableNameToModelName name
            , argumentsDefinition =
                [ ArgumentDefinition { name = "id", argumentType = NonNullType (NamedType "ID"), defaultValue = Nothing }
                , ArgumentDefinition { name = "patch", argumentType = NonNullType (NamedType ((tableNameToModelName name) <> "Patch")), defaultValue = Nothing }
                ]
            , type_ = NonNullType (NamedType (tableNameToModelName name))
            }
        deleteRecord = FieldDefinition
            { description = Nothing
            , name = "delete" <> tableNameToModelName name
            , argumentsDefinition =
                [ ArgumentDefinition { name = "id", argumentType = NonNullType (NamedType "ID"), defaultValue = Nothing }
                ]
            , type_ = NonNullType (NamedType (tableNameToModelName name))
            }

statementToMutationFields _ = []

recordTypes :: [Statement] -> [Definition]
recordTypes statements = mapMaybe (recordType statements) statements

recordType :: SqlSchema -> Statement -> Maybe Definition
recordType schema (StatementCreateTable table@(CreateTable { name, columns })) = 
        Just TypeSystemDefinition { typeSystemDefinition = TypeDefinition typeDefinition }
    where
        typeDefinition =
            ObjectTypeDefinition
                { name = tableNameToModelName name
                , implementsInterfaces = []
                , fieldDefinitions = (map (columnToRecordField table) columns) <> foreignKeyFields
                }
        foreignKeyFields =
            schema
            |> mapMaybe foreignKeyToHasManyField

        foreignKeyToHasManyField (AddConstraint { tableName = fkTable, constraint = ForeignKeyConstraint { columnName = localColumn, referenceTable }}) | referenceTable == name = 
            Just FieldDefinition
                { description = Nothing
                , name = lcfirst (tableNameToControllerName fkTable)
                , argumentsDefinition = []
                , type_ = NonNullType (ListType (NonNullType (NamedType (tableNameToModelName fkTable))))
                }
        foreignKeyToHasManyField _ = Nothing
recordType _ _ = Nothing

newRecordTypes :: [Statement] -> [Definition]
newRecordTypes statements = mapMaybe (newRecordType statements) statements

newRecordType :: SqlSchema -> Statement -> Maybe Definition
newRecordType schema (StatementCreateTable table@(CreateTable { name, columns })) = 
        Just TypeSystemDefinition { typeSystemDefinition = TypeDefinition typeDefinition }
    where
        typeDefinition =
            InputObjectTypeDefinition
                { name = "New" <> tableNameToModelName name
                , fieldDefinitions = map (columnToNewRecordField table) columns
                }
newRecordType _ _ = Nothing

patchTypes :: [Statement] -> [Definition]
patchTypes statements = mapMaybe (patchType statements) statements

patchType :: SqlSchema -> Statement -> Maybe Definition
patchType schema (StatementCreateTable table@(CreateTable { name, columns })) = 
        Just TypeSystemDefinition { typeSystemDefinition = TypeDefinition typeDefinition }
    where
        typeDefinition =
            InputObjectTypeDefinition
                { name = tableNameToModelName name <> "Patch"
                , fieldDefinitions = map (columnToPatchRecordField table) columns
                }
patchType _ _ = Nothing

columnToRecordField :: CreateTable -> Column -> FieldDefinition
columnToRecordField table Column { name, columnType, notNull } = 
        FieldDefinition
            { description = Nothing
            , name = columnNameToFieldName name
            , argumentsDefinition = []
            , type_ = 
                if isPrimaryKey
                    then NonNullType (NamedType "ID")
                    else (if notNull
                        then NonNullType
                        else \v -> v) $ postgresTypeToGraphQLType columnType
            }
    where
        primaryKeyColumns = table.primaryKeyConstraint.primaryKeyColumnNames
        isPrimaryKey = name `elem` primaryKeyColumns

columnToNewRecordField :: CreateTable -> Column -> FieldDefinition
columnToNewRecordField table column@(Column { defaultValue = Just _ }) = removeNonNullFromFieldDefinition (columnToRecordField table column)
    where
columnToNewRecordField table column = columnToRecordField table column

columnToPatchRecordField :: CreateTable -> Column -> FieldDefinition
columnToPatchRecordField table column = removeNonNullFromFieldDefinition (columnToRecordField table column)

removeNonNull :: Type -> Type
removeNonNull (NonNullType type_) = type_
removeNonNull type_ = type_

removeNonNullFromFieldDefinition :: FieldDefinition -> FieldDefinition
removeNonNullFromFieldDefinition fieldDefinition = fieldDefinition { type_ = removeNonNull fieldDefinition.type_ }

postgresTypeToGraphQLType :: PostgresType -> Type
postgresTypeToGraphQLType PText = NamedType "String"
postgresTypeToGraphQLType PUUID = NamedType "UUID"
postgresTypeToGraphQLType PInt = NamedType "Int"
postgresTypeToGraphQLType PSmallInt = NamedType "Int"
postgresTypeToGraphQLType PBigInt = NamedType "BigInt"
postgresTypeToGraphQLType PBoolean = NamedType "Boolean"
postgresTypeToGraphQLType PTimestamp = NamedType "Timestamp"
postgresTypeToGraphQLType PTimestampWithTimezone = NamedType "Timestamp"
postgresTypeToGraphQLType PReal = NamedType "Float"
postgresTypeToGraphQLType PDouble = NamedType "Float"
postgresTypeToGraphQLType PPoint = NamedType "Point"
postgresTypeToGraphQLType PPolygon = error "todo"
postgresTypeToGraphQLType PDate = NamedType "Date"
postgresTypeToGraphQLType PBinary = NamedType "String"
postgresTypeToGraphQLType PTime = NamedType "Time"
postgresTypeToGraphQLType (PNumeric _ _) = NamedType "Float"
postgresTypeToGraphQLType (PVaryingN _) = NamedType "String"
postgresTypeToGraphQLType (PCharacterN _) = NamedType "String"
postgresTypeToGraphQLType PSingleChar = NamedType "String"
postgresTypeToGraphQLType PSerial = NamedType "Int"
postgresTypeToGraphQLType PBigserial = NamedType "BigInt"
postgresTypeToGraphQLType PJSONB = NamedType "JSON"
postgresTypeToGraphQLType PInet = NamedType "IPv4"
postgresTypeToGraphQLType PTSVector = NamedType "String"
postgresTypeToGraphQLType (PArray type_) = ListType (postgresTypeToGraphQLType type_)
postgresTypeToGraphQLType PTrigger = error "Trigger cannot be converted to a GraphQL type"
postgresTypeToGraphQLType PEventTrigger = error "Trigger cannot be converted to a GraphQL type"
postgresTypeToGraphQLType (PInterval _) = NamedType "String"
postgresTypeToGraphQLType (PCustomType theType) = NamedType "String"