{- | Module : GraphQLdbi Description : Here are methods to interpret your GraphQL-to-SQL query and process the Persistent style 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 style 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, and the 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 GraphQLdbi ( -- * Functions -- | Here is three methods to the package interpretation, validation, and formatting features. module GraphQLdbi, -- * Server exceptions -- | These are server exceptions to make error handling. module Model.ServerExceptions ) where import Data.Text (Text) import Control.Monad.IO.Class (liftIO,MonadIO()) import Control.Exception (throw) import Components.Parsers.QueryParser (validateQuery,parseStringToObjects,processString) import Components.ObjectHandlers.ServerObjectValidator (checkObjectsAttributes,replaceObjectsVariables) import Components.QueryComposers.SQLQueryComposer (makeSqlQueries) import Components.Parsers.ServerSchemaJsonParser (fetchArguments) import Components.ObjectHandlers.ServerObjectTrimmer (mergeDuplicatedRootObjects) import Components.Parsers.VariablesParser (parseVariables) import Components.DataProcessors.ListDataProcessor (processReturnedValues) import Model.ServerExceptions import Model.ServerObjectTypes (RootObject,SchemaSpecs,QueryData) {- | This function is to parse your schema. The return values are passed to the other two functions to interpret your schema. You can declare and define your schema with tuples and lists without reading a json file as defined in below function, but you will not receive type and duplicate checking from this function. __Schema:__ The schema json file is formated as one json object to describe the GraphQL objects and the object heirarchy. Only "PrimitiveObjects" are valid descendants to "ParentalObjects". Only the shared object fields and shared scalar fields are parental object fields. If a parental object is a declared field from a primitive object, all descendants are supposed to be declared in database relationships. When declaring any primitive or parental object, the pseudonym is supposed to list all possible root object references. As a nested object, you should declare the names in the nested object field definitions in corresponding object definitions. Primitive objects are expected to not have all NULL unique id values, and they are otherwise removed from the result. Database relationships are defined as an ordered sequence from the identity table to the target association table. The order is identity table, identity table join field(s), target table, target table join field(s), then (multiple) triplets (of intermediate table, intermediate table to-join field, then intermediate table from-join field in order of nearness from identity table) if relevant. If multiple fields are defining the join, one declares every field with a space " " separation. For example, the declaration is [A,"a b", B, "c d"] if the join condition is A.a=B.c and A.b = B.d. You can add schema associations as declaring more object-fields in the primitive objects. __Scalar fields:__ Scalar fields are declared with a type, and they are cast to corresponding JSON format when making the GraphQL result. Valid scalar field types are Text, ByteString, Int, Double, Rational, Bool, Day, TimeOfDay, or UTCTime. __Function exceptions:__ Exceptions are returned when error is faced. This method returns errors of: * ImportSchemaException (if there is a problem with reading your schema) * ImportSchemaServerNameException (if there is a problem with reading your servername argument) * ImportSchemaPseudonymsException (if there is a problem with reading your pseudonyms list argument) * ImportSchemaScalarFieldsException (if there is a problem with reading your scalarfields list argument) * ImportSchemaObjectFieldsException (if there is a problem with reading your objectfields list argument) * ImportSchemaDatabaseTablesException (if there is a problem with reading your databasetables list argument) * ImportSchemaDatabaseRelationshipsException (if there is a problem with reading your databaserelationships list argument) * ImportSchemaDuplicateException (if two or more Server objects are sharing the same name) __Schema format example:__ Here is an example schema to look at formatting: @ { "ParentalObjects":[ { "ServerName":"Taxonomy", "Pseudonyms":[ "taxonomy", "Taxonomy" ], "ServerChildren":[ "Breed", "Species", "Family", "Genus" ] } ], "PrimitiveObjects":[ { "ServerName": "Person", "Pseudonyms": [ "person", "Person" ], "ScalarFields": [ { "Name": "id", "Type": "Int", "Arguments": [] }, { "Name": "name", "Type": "Text", "Arguments": [] }, { "Name": "gender", "Type": "Int", "Arguments": [ { "Name": "as", "Options": [ { "Name": "MALEFEMALE", "Type": "Text", "Prefix": "CASE WHEN ", "Suffix": "==1 THEN 'MALE' ELSE 'FEMALE' END" } ] } ] } ], "ObjectFields": [ { "Names": [ "pet" ], "ServerName": "Pet" }, { "Names": [ "breed" ], "ServerName": "Breed" }, { "Names": [ "species" ], "ServerName": "Species" }, { "Names": [ "genus" ], "ServerName": "Genus" }, { "Names": [ "family" ], "ServerName": "Family" }, { "Names": [ "taxonomy" ], "ServerName": "Taxonomy" } ], "DatabaseTable": "person", "UniqueIds": [ "id" ], "DatabaseRelationships": [ ["person","id","pet","id","pet_ownership","owner_id","animal_id"], ["person","id","breed","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id"], ["person","id","species","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id"], ["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","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"] ] }, { "ServerName": "Family", "Pseudonyms": [ "Family", "family" ], "ScalarFields": [ { "Name": "id", "Type": "Int", "Arguments": [] }, { "Name": "name", "Type": "Text", "Arguments": [] } ], "ObjectFields": [ { "Names": [ "genus" ], "ServerName": "Genus" }, { "Names": [ "species" ], "ServerName": "Species" }, { "Names": [ "breed" ], "ServerName": "Breed" }, { "Names": [ "pet" ], "ServerName": "Pet" }, { "Names": [ "person" ], "ServerName": "Person" } ], "DatabaseTable": "family", "UniqueIds": [ "id" ], "DatabaseRelationships": [ ["family","id","person","id","genus","family_id","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"], ["family","id","pet","id","genus","family_id","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id"], ["family","id","genus","family_id"], ["family","id","species","genus_id","genus","family_id","id"], ["family","id","breed","species_id","genus","family_id","id","species","genus_id","id"] ] }, { "ServerName": "Genus", "Pseudonyms": [ "Genus", "genus" ], "ScalarFields": [ { "Name": "id", "Type": "Int", "Arguments": [] }, { "Name": "name", "Type": "Text", "Arguments": [] } ], "ObjectFields": [ { "Names": [ "family" ], "ServerName": "Family" }, { "Names": [ "species" ], "ServerName": "Species" }, { "Names": [ "breed" ], "ServerName": "Breed" }, { "Names": [ "pet" ], "ServerName": "Pet" }, { "Names": [ "person" ], "ServerName": "Person" } ], "DatabaseTable": "genus", "UniqueIds": [ "id" ], "DatabaseRelationships": [ ["genus","id","person","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"], ["genus","id","pet","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id"], ["genus","family_id","family","id"], ["genus","id","species","genus_id"], ["genus","id","breed","species_id","species","genus_id","id"] ] }, { "ServerName": "Species", "Pseudonyms": [ "Species", "species" ], "ScalarFields": [ { "Name": "id", "Type": "Int", "Arguments": [] }, { "Name": "name", "Type": "Text", "Arguments": [] } ], "ObjectFields": [ { "Names": [ "family" ], "ServerName": "Family" }, { "Names": [ "genus" ], "ServerName": "Genus" }, { "Names": [ "breed" ], "ServerName": "Breed" }, { "Names": [ "pet" ], "ServerName": "Pet" }, { "Names": [ "person" ], "ServerName": "Person" } ], "DatabaseTable": "species", "UniqueIds": [ "id" ], "DatabaseRelationships": [ ["species","id","person","id","breed","species_id","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"], ["species","id","pet","id","breed","species_id","id","pet_type","breed_id","pet_id"], ["species","id","breed","species_id"], ["species","genus_id","genus","id"], ["species","id","family","genus_id","genus","species_id","id"] ] }, { "ServerName": "Breed", "Pseudonyms": [ "Breed", "breed" ], "ScalarFields": [ { "Name": "id", "Type": "Int", "Arguments": [] }, { "Name": "name", "Type": "Text", "Arguments": [] } ], "ObjectFields": [ { "Names": [ "family" ], "ServerName": "Family" }, { "Names": [ "genus" ], "ServerName": "Genus" }, { "Names": [ "species" ], "ServerName": "Species" }, { "Names": [ "pet" ], "ServerName": "Pet" }, { "Names": [ "person" ], "ServerName": "Person" } ], "DatabaseTable": "breed", "UniqueIds": [ "id" ], "DatabaseRelationships": [ ["breed","id","person","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"], ["breed","id","pet","id","pet_type","breed_id","pet_id"], ["breed","species_id","species","id"], ["breed","species_id","genus","id","species","id","genus_id"], ["breed","species_id","family","id","species","id","genus_id","genus","id","family_id"] ] }, { "ServerName": "Pet", "Pseudonyms": [ "pet", "Pet" ], "ScalarFields": [ { "Name": "id", "Type": "Int", "Arguments": [] }, { "Name": "name", "Type": "Text", "Arguments": [] }, { "Name": "gender", "Type": "Int", "Arguments": [ { "Name": "as", "Options": [ { "Name": "MALEFEMALE", "Type": "Text", "Prefix": "CASE WHEN ", "Suffix": "==1 THEN 'MALE' ELSE 'FEMALE' END" } ] } ] } ], "ObjectFields": [ { "Names": [ "owner" ], "ServerName": "Person" }, { "Names": [ "breed" ], "ServerName": "Breed" }, { "Names": [ "species" ], "ServerName": "Species" }, { "Names": [ "genus" ], "ServerName": "Genus" }, { "Names": [ "family" ], "ServerName": "Family" }, { "Names": [ "taxonomy" ], "ServerName": "Taxonomy" } ], "DatabaseTable": "pet", "UniqueIds": [ "id" ], "DatabaseRelationships": [ ["pet","id","person","id","pet_ownership","animal_id","owner_id"], ["pet","id","breed","id","pet_type","pet_id","breed_id"], ["pet","id","species","id","pet_type","pet_id","breed_id","breed","id","species_id"], ["pet","id","genus","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id"], ["pet","id","family","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id","genus","id","family_id"] ] } ] } @ __Schema recommendation:__ When you add every possible chain from every PrimitiveObject to every other PrimitiveObject, the schema coverage is extended. __Closing remark:__ As the last remark, I'll turn attention to a quote from specifications. @"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 elaborate on your schema representation. You should give all planned details, and you should make adjustments when you feel to evolve your server by making associations that are like simple arcs in a graph. -} processSchema :: (MonadIO m) => FilePath -- ^ This is the path to schema json file -> m SchemaSpecs -- ^ The return value is a monad for your schema data. processSchema fp = do schema <- liftIO $ fetchArguments fp return schema {- | This function is to parse, validate, and interpret your query with variables. If you don't have variables, you may pass an empty string. The function is expecting your schema as SchemaSpec type from the above function. You can alternatively define you schema with lists and tuples. You also can use the above function to print and paste your schema data into your script file. __Function return value:__ The returned values are one tuple to contain information to pass into your database interface and to later reuse in below data processing function. The tuple is (PackagedObjects,SQLQueries). The second tuple member is a collection of queries. Examples are found in this <https://github.com/jasonsychau/graphql-w-persistent repository page> to pass queries (and data) to your database. __Function exceptions:__ Exceptions are returned when error is faced. This method returns errors of: * SyntaxException (if there is a problem with the given GraphQL query syntax) * ParseFragmentException (if there is a problem with a Fragment syntax) * EmptyQueryException (if the query is left blank) * InvalidObjectException (if there is problem in nested object syntax, an object is not recognized, or server data is misinterpreted) * InvalidObjectSubFieldException (if a subfield is requested from nested object that is not having ownership of the subfield) * InvalidScalarException (if there is syntax error in listing scalar fields, or server data is misinterpreted) * NullArgumentException (if a transformation is set but no argument is given though we do not yet support transformations) * CreatingSqlQueryObjectFieldsException (if there are too many nested object subfields than nested objects to hold them or if there is a problem with the aligning of server object relationship argument - that's the fifth schema argument) * RelationshipConfigurationException (if the relationship schema fifth arugment is incorrect number of String values, if an unrecognized pairing is found, or if two table field cardinalities are not same) * FailedObjectEqualityException (if we could not match identical queries) * DuplicateRootObjectsException (if there is an overlapping query) * MissingVariableValueException (if we are missing variables in the variables-value string to the query) * InvalidVariableNameException (if we cannot find a variable in the query with given variables-value string) * MismatchedVariableTypeException (if query variable type is not matching object scalar type) * InvalidVariableTypeException (if variable type is invalid or missing) * ReadVariablesException (if the variables json string syntax is incorrect or was not correctly read) * VariablesSyntaxException (if the query variables declaration is invalid syntax) -} processQueryString :: SchemaSpecs -- ^ This is the data about your schema -> String -- ^ This is the GraphQL query -> String -- ^ This is the variables string -> (QueryData,[[String]]) -- ^ The return value is a tuple with package objects and list with grouped sql query strings. processQueryString (svrobjs,sss,sos,sodn,sor,soa) qry vars = let dvars = parseVariables vars qry str = processString qry objs = if (validateQuery str)==True then (parseStringToObjects str svrobjs sos soa dvars) else throw SyntaxException robjs = mergeDuplicatedRootObjects $ replaceObjectsVariables sss soa objs dvars (tbls,qrys) = if (checkObjectsAttributes robjs sss soa)==True then (makeSqlQueries robjs sss sodn sor soa) else (throw InvalidObjectSubfieldException) in (zip robjs tbls,qrys) {- | After casting all database results to Text (examples are given in this repository <https://github.com/jasonsychau/graphql-w-persistent site>), you may use this function to make GraphQL data. The second argument is the package objects given from function processQueryString. The return result is a string to resemble a GraphQL return value. __Formatting notes (IMPORTANT):__ Nulls are expected as "Unexpected null". This is the Persistent default and important when processing ids to separate different results. One can translate nulls to "Unexpected null" when making Text types of query results. This is in the repository examples. __Function exceptions:__ Exceptions are returned when error is faced. This method returns errors 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) -} processQueryData :: SchemaSpecs -- ^ This is the schema data from the first method -> QueryData -- ^ This is the package objects that is from processQueryString function. -> [[[[Text]]]] -- ^ This is the database query results value in the same order and after casting to Text values. -> String -- ^ The return value is a string type to describe the GraphQL-organized return values. processQueryData (_,sss,_,sodn,_,soa) cc dt = let (ro,tb) = unzip cc in processReturnedValues sss sodn soa ro tb dt