{- |
Module      : GraphQL
Description : Here is methods to interpret your GraphQL-SQL query and process the Persistent results
License     : IPS
Maintainer  : jasonsychau@live.ca
Stability   : provisional

<https://graphql.github.io/ Here> is a link to the official documents. You can learn and get a feel of how can you implement this package which is a GraphQL-to-SQL translator with Persistent package return value processing to make a GraphQL format return object string.

This module is made to enable interpreting your GraphQL queries. The expected query type is a single string to comprise all your GraphQL queries and fragments. Expected variable type is a single string to your variable-to-value definitions.

When errors are encountered, this module is simply going to throw an uncaught Exception. The Exception name is some hint to where was the error encountered. More information is provided in the below function description.

Not all GraphQL features are currently supported. For a list to updates and current state, you should check the GitHub updates and example <https://github.com/jasonsychau/graphql-w-persistent page>.

-}
module GraphQL (
    -- * Functions
    -- | These are available methods to give access to the package interpretation, validation, and formatting features.
    -- | NOTE: It is is assumed that all nested objects are corresponding to a database table with a primary key that's named "id".
    module GraphQL,
    -- * Server data types
    -- | These are server data types that are return from processQueryString. You do not need to know these to know how is this package used, but you'll get some insight to what is used to store and organize your GraphQL data.
    module Model.ServerObjectTypes,
    -- * Server exceptions
    -- | These are server exceptions to make error handling.
    module Model.ServerExceptions
    ) where

import Data.Text (Text)
import Control.Monad.IO.Class as IO
import qualified Components.Parsers.QueryParser as QP
import qualified Components.Parsers.ServerSchemaJsonParser as JP
import qualified Components.Parsers.VariablesParser as VP
import qualified Components.DataProcessors.PersistentDataProcessor as DP
import Model.ServerObjectTypes
import Model.ServerExceptions
import GraphQLHelper


{- | 
   This is the simplest function to call to get queries from a GraphQL query. If you prefer to define schema in a json file, you may skip to the below processQueryStringWithJson function.

   __Instructions to give server schema as arguments:__

   (1) make a list to all server objects in the heirarchy in separate tuples with a list to all pseudonyms (that's including the ones that are referred in relationships as nested objects)
   (2) make a list to all above mentioned server objects in separate tuples with a list to all valid scalar subfields as you read them from your database in a tuple with the variable type (valid types are Text, ByteString, Int, Double, Rational, Bool, Day, TimeOfDay, or UTCTime)
   (3) make a list to all above mentioned server objects in separate tuples with a list to all valid nested relationship object subfields as you want them to be named (also include these in the first list)
   (4) make a list to all above mentioned server objects in separate tuples with a list to all reference database table name(s) as you read them
   (5) make a list to database table names in separate tuples with another database table name and a list to the SQL database tables that are between the two first tables; the list order is...
   identity table name (the first String in this tuple), all identity leaving-field names (with a space to separate fields), referring table name (the table that's corresponding to the second String in this tuple), referring table entering-field name(s), and (if present) 
   three String sequences that is intermediary-table name, intermediary-table entering-column name(s), and intermediary-table leaving-column name(s). The intermediary tables are ordered from closest to indentity table to closest to referencing table.

   __To implement server type heirarchy:__
   
   * add new tuple to first schema argument,
   * add new tuple to second schema argument (where list is the intersection fields on all children),
   * add new tuple to third schema argument (where list is the intersection fields on all children),
   * and add new tuple to fourth schema argument (where list is all children database tables).
  
   You should not need more cases within fifth schema argument, but you may find a child database table and third schema argument list elements pair that is not included.
   
   You then will need to include this, or error is thrown.

   
   Here is an example of the GraphQL schema arguments that are expected from the last five arguments. We'll use the GitHub <https://github.com/jasonsychau/graphql-w-persistent example> database schema 
   to illustrate the expected arguments.


   * The first schema argument (second argument) is a mapping to names to which a server object is possibly referred in any GraphQL query:

   @
       [("Person",["Person","person","owner"]),("Family",["Family","family"]),("Genus",["Genus","genus"]),("Species",["Species","species"]),("Breed",["Breed","breed"]),("Pet",["Pet","pet"]),("Taxonomy",["Taxonomy","taxonomy"])]
   @

   * The second schema argument is a mapping to valid scalar subfields and type from every server object that is listed in the first schema argument:

   @
       [("Person",[("id","Int"),("name","Text"),("gender","Int")]),("Family",[("id","Int"),("name","Text")]),("Genus",[("name","Text")]),("Species",[("id","Int"),("name","Text")]),("Breed",[("id","Int"),("name","Text")]),("Pet",[("id","Int"),("name","Text"),("gender","Int")]),("Taxonomy",[("name","Text")])]
   @

   NOTE: You should give only the intersection subfields set to parent server objects with all the children server objects.

   NOTE: Valid scalar types are Text, ByteString, Int, Double, Rational, Bool, Day, TimeOfDay, or UTCTime.
  
   * The third schema argument is a mapping to valid nested object (or entity) subfields from every server object that is listed in the first schema argument:

   @
       [("Person",["pet"]),("Family",["genus","species","breed","pet"]),("Genus",["family","species","breed","pet"]),("Species",["family","genus","breed","pet"]),("Breed",["family","genus","species","pet"]),("Pet",["owner","breed","species","genus","family"]),("Taxonomy",["pet"])]
   @

   NOTE: You should give only the intersection subfields set to parent server objects with all the children server objects.

   NOTE: You should also put the list names in the lists in the first argument to map the name to ServerObject.

   * The fourth schema argument is a mapping to exact database names (it is maybe helpful to first have the database schema and copy names to this list mapping) for every listed server object in the first schema argument:

   @
       [("Person",["person"]),("Family",["family"]),("Genus",["genus"]),("Species",["species"]),("Breed",["breed"]),("Pet",["pet"]),("Taxonomy",["family","genus","species","breed"])]
   @

   NOTE: This is where can you introduce type heirarchies and generalizations. The above Taxonomy server object is an example. You can see that it is an encompassing term for all the biological classifications in our database schema. 

   * The fifth schema argument is a mapping to exact database table names and fields names to link the from-object to the to-object.

   Within every tuple,

   the first String is the identity database table name (with type heirarchies, you make a separate tuple on every referenced database table if the to-from pair is not already mentioned as explicit pairing).

   The second String is the referencing database name.

   The third tuple value is a list that is showing the link between our identity table and referring table. The order is first identity database table name, second identity database leaving field name(s) (that are separated with a space), 
   third referencing database table name, fourth referencing database table arrival field name(s), and the remainder (if identity table and referencing table are not directly linked) is a triplet of Strings that 
   are ordered from nearest to identity table to closer to referencing table. The triplets are intermediate database table name, intermediate database arrival field(s), and intermediate database exiting 
   field(s). Here is an illustration:

   @
       -- Person
       [("person","pet",["person","id","pet","id","pet_ownership","owner_id","animal_id"]),
       ("person","breed",["person","id","breed","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id"]),
       ("person","species",["person","id","species","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id"]),
       ("person","genus",["person","id","genus","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id"]),
       ("person","family",["person","id","family","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id","genus","id","family_id"]),
       -- Family
       ("family","pet",["family","id","pet","id","genus","family_id","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id"]),
       ("family","genus",["family","id","genus","family_id"]),
       ("family","species",["family","id","species","genus_id","genus","family_id","id"]),
       ("family","breed",["family","id","breed","species_id","genus","family_id","id","species","genus_id","id"]),
       -- Genus
       ("genus","pet",["genus","id","pet","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id"]),
       ("genus","family",["genus","family_id","family","id"]),
       ("genus","species",["genus","id","species","genus_id"]),
       ("genus","breed",["genus","id","breed","species_id","species","genus_id","id"]),
       -- Species
       ("species","pet",["species","id","pet","id","breed","species_id","id","pet_type","breed_id","pet_id"]),
       ("species","breed",["species","id","breed","species_id"]),
       ("species","genus",["species","genus_id","genus","id"]),
       ("species","family",["species","id","family","genus_id","genus","species_id","id"]),
       -- Breed
       ("breed","pet",["breed","id","pet","id","pet_type","breed_id","pet_id"]),
       ("breed","species",["breed","species_id","species","id"]),
       ("breed","genus",["breed","species_id","genus","id","species","id","genus_id"]),
       ("breed","family",["breed","species_id","family","id","species","id","genus_id","genus","id","family_id"]),
       -- Pet
       ("pet","person",["pet","id","person","id","pet_ownership","animal_id","owner_id"]),
       ("pet","breed",["pet","id","breed","id","pet_type","pet_id","breed_id"]),
       ("pet","species",["pet","id","species","id","pet_type","pet_id","breed_id","breed","id","species_id"]),
       ("pet","genus",["pet","id","genus","id","pet_type","pet_id","breed_id","breed","id","genus_id"]),
       ("pet","family",["pet","id","family","id","pet_type","pet_id","breed_id","breed","id","genus_id","genus","id","family_id"])]
   @

   When an error is encountered, an uncaught exception is thrown. Exceptions thrown by this function are:

   * SyntaxException (when there is a problem with the given GraphQL query syntax)
   * ParseFragmentException (when there is a problem with a Fragment syntax)
   * EmptyQueryException (when the query is left blank)
   * InvalidObjectException (when there is problem in nested object syntax, an object is not recognized, or server data is misinterpreted)
   * InvalidObjectSubFieldException (when a subfield is requested from nested object that is not having ownership of the subfield)
   * InvalidScalarException (when there is syntax error in listing scalar fields, or server data is misinterpreted)
   * NullArgumentException (when a transformation is set but no argument is given though we do not yet support transformations) 
   * CreatingSqlQueryObjectFieldsException (when there are too many nested object subfields than nested objects to hold them or when there is a problem with the aligning of server object relationship argument - that's the fifth schema argument)
   * RelationshipConfigurationException (when the relationship schema fifth arugment is incorrect number of String values, when an unrecognized pairing is found, or when two table field cardinalities are not same)
   * FailedObjectEqualityException (when we could not match identical queries)
   * DuplicateRootObjectsException (when there is an overlapping query)
   
   As a final note, we turn our attention to what is posted on the official GraphQL docs:
  
   @"In contrast, GraphQL only returns the data that's explicitly requested, so new capabilities can be added via new types and new fields on those types without creating a breaking change. This has lead to a common practice of always avoiding breaking changes and serving a versionless API."@ - <https://graphql.github.io/learn/best-practices/>
  
   With respect to that, you should be sure to be explicit with your here representation on your data schema. You should give all planned details, and you should make adjustments when you feel to evolve your server with your desired schema.   
-}
processQueryString :: String                        -- ^ GraphQL query argument as String.
                   -> [(String,[String])]           -- ^ unique server object name to list of query reference names.
                   -> [(String,[(String,String)])]  -- ^ unique server object name to list of valid scalar subfields (which are exactly named after database column names) and the subfield type.
                   -> [(String,[String])]           -- ^ unique server object name to list of valid nested object subfields (which are named to your preference, though you must include these in the tuple list in the first argument list)
                   -> [(String,[String])]           -- ^ unique server object name to list of database table names (which are exact references to table names).
                   -> [(String,String,[String])]    -- ^ two database table names to list of from-to-and intermediate triplet strings as described above to identify all GraphQL relationships with database sequences.
                   -> ([RootObject],[[String]])     -- ^ The return value is one tuple with server objects and list with grouped sql query strings.
processQueryString str svrobjs sss sos sodn sor = checkObjectsToSql sss sos sodn sor $ checkStringToObjects svrobjs $ QP.processString str


{- |
   This method is same as above, but another second argument is a string to give variables in your query.

   The expected variables string is json syntax.

   Valid types are Text, ByteString, Int, Double, Rational, Bool, Day, TimeOfDay, or UTCTime when declaring variable types at the beginning of the query.

   There are additional thrown exceptions:

   * MissingVariableValueException (when we are missing variables in the variables-value string to the query)
   * InvalidVariableNameException (when we cannot find a variable in the query with given variables-value string)
   * MismatchedVariableTypeException (when query variable type is not matching object scalar type)
   * InvalidVariableTypeException (when variable type is invalid or missing)
   * ReadVariablesException (when the variables json string syntax is incorrect or was not correctly read)
   * VariablesSyntaxException (when the query variables declaration is invalid syntax)

   NOTE: the variable-value json string is variable names without $ at the beginning while all other calls are with preceding $.
-}
processQueryStringWithVariables :: String                        -- ^ This is the GraphQL query
                                -> String                        -- ^ This is the variables string
                                -> [(String,[String])]           -- ^ unique server object name to list of query reference names.
                                -> [(String,[(String,String)])]  -- ^ unique server object name to list of valid scalar subfields (which are exactly named after database column names) and the subfield type.
                                -> [(String,[String])]           -- ^ unique server object name to list of valid nested object subfields (which are named to your preference, though you must include these in the tuple list in the first argument list)
                                -> [(String,[String])]           -- ^ unique server object name to list of database table names (which are exact references to table names).
                                -> [(String,String,[String])]    -- ^ two database table names to list of from-to-and intermediate triplet strings as described above to identify all GraphQL relationships with database sequences.
                                -> ([RootObject],[[String]])     -- ^ The return value is one tuple with server objects and list with grouped sql query strings.
processQueryStringWithVariables qry vars svrobjs sss sos sodn sor = checkObjectsToSqlWithVariables sss sos sodn sor dvars $ checkStringToObjectsWithVariables svrobjs dvars $ QP.processString qry
                                                                      where
                                                                        dvars = VP.parseVariables vars qry

{- |
     Except being nested in a monad, this funcion is same as above.

     It is allowing you to use a json file to declare your server data schema.

     The json format is a list of objects. You detail your schema with a list of objects to refer to server objects...

     __Instructions to give server schema:__

     The every object is...

     (1) "servername" is a string to name the server object
     (2) "pseudonyms" is a list of string to give all query reference
     (3) "scalarfields" is a list of objects that are scalar name under "name" and scalar type under "type"
     (4) "objectfields" is a list of nested object fields
     (5) "databasetables" is a list of tables in your database
     (6) "databaserelationships" is a list of database relationships from this refering table to another table

     NOTE: You do not need to repeat databaserelationships in any objects. A relationship is given once, and more is redundant... 

     NOTE: You should put every objectfields value in some pseudonym list value.

     NOTE: multiple fields are separated by a space in defining the relationships. For example, the relationship is [A,"a b", B, "c d"] if A.a=B.c and A.b = B.d.

     Here is an example:

     @
      [
        {
          "servername": "Person",
          "pseudonyms": [
            "person",
            "Person",
            "owner"
          ],
          "scalarfields": [
            {
              "name": "id",
              "type": "Int"
            },
            {
              "name": "name",
              "type": "Text"
            },
            {
              "name": "gender",
              "type": "Int"
            }
          ],
          "objectfields": [
            "pet"
          ],
          "databasetables": [
            "person"
          ],
          "databaserelationships": [
            ["person","id","pet","id","pet_ownership","owner_id","animal_id"]
          ]
        },
        {
          "servername": "Pet",
          "pseudonyms": [
            "pet",
            "Pet"
          ],
          "scalarfields": [
            {
              "name": "id",
              "type": "Int"
            },
            {
              "name": "name",
              "type": "Text"
            },
            {
              "name": "gender",
              "type": "Int"
            }
          ],
          "objectfields": [
            "owner"
          ],
          "databasetables": [
            "pet"
          ],
          "databaserelationships": [
            ["pet","id","person","id","pet_ownership","animal_id","owner_id"]
          ]
        }
      ]
     @

     __To implement server type heirarchy:__
   
     * add new object to list,
     * give the children intersection scalar fields in the scalarfields argument,
     * give the children intersection object fields in the objectfields argument,
     * and add all referring children database table names to the databasetables argument.

     You should not need more cases in the databaserelationships argument, but you may find a child database table and object field database table pair that is not included. If you don't include these relationships, an error is thrown.

     The exceptions thrown with this function are same as above function processQueryString, but there are a few more...

     * ImportSchemaException (when there is a problem with reading your schema)
     * ImportSchemaServerNameException (when there is a problem with reading your servername argument)
     * ImportSchemaPseudonymsException (when there is a problem with reading your pseudonyms list argument)
     * ImportSchemaScalarFieldsException (when there is a problem with reading your scalarfields list argument)
     * ImportSchemaObjectFieldsException (when there is a problem with reading your objectfields list argument)
     * ImportSchemaDatabaseTablesException (when there is a problem with reading your databasetables list argument)
     * ImportSchemaDatabaseRelationshipsException (when there is a problem with reading your databaserelationships list argument)

   As a final note, we turn our attention to what is posted on the official GraphQL docs:
  
   @"In contrast, GraphQL only returns the data that's explicitly requested, so new capabilities can be added via new types and new fields on those types without creating a breaking change. This has lead to a common practice of always avoiding breaking changes and serving a versionless API."@ - <https://graphql.github.io/learn/best-practices/>
  
   With respect to that, you should be sure to be explicit with your here representation on your data schema. You should give all planned details, and you should make adjustments when you feel to evolve your server with your desired schema.   
-}
processQueryStringWithJson :: (MonadIO m)
                           => String                       -- ^ This is the GraphQL query
                           -> FilePath                     -- ^ This is the filepath to your server schema json file
                           -> m ([RootObject],[[String]])  -- ^ The return value is a monad of one tuple with server objects and list with grouped sql query strings.
processQueryStringWithJson qry fp = do
                                  (svrobjs,sss,sos,sodn,sor) <- IO.liftIO $ JP.fetchArguments fp
                                  return $ checkObjectsToSql sss sos sodn sor $ checkStringToObjects svrobjs $ QP.processString qry


{- |
   This method is same as above, but another third argument is a string to give variables in your query.

   The expected variables string is json syntax.

   Valid types are Text, ByteString, Int, Double, Rational, Bool, Day, TimeOfDay, or UTCTime when declaring variable types at the beginning of the query.

   There are additional thrown exceptions:

   * MissingVariableValueException (when we are missing variables in the variables-value string to the query)
   * InvalidVariableNameException (when we cannot find a variable in the query with given variables-value string)
   * MismatchedVariableTypeException (when query variable type is not matching object scalar type)
   * InvalidVariableTypeException (when variable type is invalid or missing)
   * ReadVariablesException (when the variables json string syntax is incorrect or was not correctly read)
   * VariablesSyntaxException (when the query variables declaration is invalid syntax)

   NOTE: the variable-value json string is variable names without $ at the beginning while all other calls are with preceding $.
-}
processQueryStringWithJsonAndVariables :: (MonadIO m)
                                       => String                       -- ^ This is the GraphQL query
                                       -> String                       -- ^ This is the variables string
                                       -> FilePath                     -- ^ This is the filepath to your server schema json file
                                       -> m ([RootObject],[[String]])  -- ^ The return value is a monad of one tuple with server objects and list with grouped sql query strings.
processQueryStringWithJsonAndVariables qry vars fp = do
                                                  (svrobjs,sss,sos,sodn,sor) <- IO.liftIO $ JP.fetchArguments fp
                                                  let dvars = VP.parseVariables vars qry
                                                  return $ checkObjectsToSqlWithVariables sss sos sodn sor dvars $ checkStringToObjectsWithVariables svrobjs dvars $ QP.processString qry

{- |
    This is the function to call after casting PersistValues to Text from processQueryString of which you may find on my examples <https://github.com/jasonsychau/graphql-w-persistent page>.
    
    The ordered arguments are:
    
     * all the data that is cast to Text type
     * an unmodified copy of the RootObject list that is returned from processing the query string.
    
    The return result is a string to resemble the GraphQL return value.

    Current implementation is to cast all values to Text since arbitrary columns data types are not inferred, though next update is changing this limitation.


    When an error is encountered, an uncaught exception is thrown. The exceptions thrown are of:

    * InvalidObjectException (when server data is misinterpreted or an unrecognized server object is met)
    * InvalidScalarException (when server data is misinterpreted)
    * EOFDataProcessingException (when the given data is shorter than expected in reference to the given serve objects)
    * InvalidArgumentException (when there is an internal argument error - you should not observe this)
    * InvalidVariableTypeException (when an unrecognized base data type is met)
    * InvalidObjectScalarFieldException (when an unrecognized object-scalar pair is met)
-}
processPersistentData :: [(String,[(String,String)])]  -- ^ unique server object name to list of valid scalar subfields (which are exactly named after database column names) and the subfield type.
                      -> [[[[Text]]]]                  -- ^ database query return value as casted to only Text data types and with no other alterations (a list of GraphQL-grouped results list of SQL query results list of data row lists)
                      -> [RootObject]                  -- ^ unmodified server objects that was given by the previous processQueryString function.
                      -> String                        -- ^ The return value is a string type to describe the GraphQL-organized return values.
processPersistentData sss dt ro = DP.processReturnedValues sss ro dt

{- | 
    This is a json version from the above function. It is also wrapped in a monad

    The exceptions thrown with this function are same as above function, but there are a few more...

     * ImportSchemaException (when there is a problem with reading your schema)
     * ImportSchemaServerNameException (when there is a problem with reading your servername argument)
     * ImportSchemaPseudonymsException (when there is a problem with reading your pseudonyms list argument)
     * ImportSchemaScalarFieldsException (when there is a problem with reading your scalarfields list argument)
     * ImportSchemaObjectFieldsException (when there is a problem with reading your objectfields list argument)
     * ImportSchemaDatabaseTablesException (when there is a problem with reading your databasetables list argument)
     * ImportSchemaDatabaseRelationshipsException (when there is a problem with reading your databaserelationships list argument)
-}
processPersistentDataWithJson :: (MonadIO m)
                              => FilePath      -- ^ This is the file path to schema json
                              -> [[[[Text]]]]  -- ^ This is the unmodified Persistent database query return value (a list of GraphQL-grouped results list of SQL query results list of data row lists).
                              -> [RootObject]  -- ^ This is the unmodified server objects that was composed from previous processQueryString function.
                              -> m String      -- ^ The return value is a monad of string type to describe the GraphQL-organized return values.
processPersistentDataWithJson fp dt ro = do
                                      (_,sss,_,_,_) <- IO.liftIO $ JP.fetchArguments fp
                                      return $ DP.processReturnedValues sss ro dt