-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Server code architecture
The Hasura GraphQL Engine’s backend server is written in Haskell. For more details on building the server code, see server/CONTRIBUTING.md
.
Table of Contents
The server has a command line interface with 5 commands. They are
- serve - Start HTTP and WebSocket servers
- export - Export metadata in JSON
- clean - Clean metadata
- execute - Execute a JSON Query
- version - Print server version
optparse-applicative the package is used to generate and parse CLI commands and options.
In file src-exec/Init.hs
:
-
RawHGECommand
-> parsed command from CLI -
parseHGECommand
-> raw command parser -
HGECommand
-> resolved command with environment variables -
mkHGEOptions
-> make command with environment variables
This command starts GraphQL engine backend server.
It accepts ServeOptions
through the command line. Example
graphql-engine --database-url postgresql://postgres@localhost:5432/db serve --server-port 8181 --enable-console --admin-secret mySecret
This command does the following actions.
- Resolve Auth Mode using admin secret or auth webhook or JWT config.
- Make Postgres connection info.
- Create Postgres connection pool using connection info generated in
2
- Use pool created in
3
to initialise state - Create server application and cache IO ref
- Start schema sync
- Start event trigger threads
- Start update checker
- Start telemetry reporter
- Start the application created in
5
The server operates in any one of four authorization modes. They are
- No Auth
- Only Admin Secret
- Admin Secret and Webhook
- Admin Secret and JWT
In file src-lib/Hasura/Server/Auth.hs
:
-
AuthMode
-> type represents authorization mode -
mkAuthMode
-> make authorization mode
The server uses an in-house custom library pg-client-hs to establish a connection with Postgres. The library provides an interface to create Postgres transactions and run them. Check the library for more details.
The server accepts Postgres connection parameters through CLI.
Or user can also provide database URI string via --database-url
option.
GraphQL Engine server reserves hdb_catalog
and hdb_views
schema to store
application state and define custom triggers and views. Whenever the server starts
it checks for these schemas to be present. If not present, define schemas,
tables, views and functions. Refer src-rsr/initialize.sql
file.
The hdb_catalog
Postgres schema is versioned. You can modify the schema
by bumping up the version. The migration SQL scripts are found in src-rsr
folder.
On startup server queries version from hdb_catalog.hdb_version
table.
If it is not the current version, then server migrates the hdb_catalog
schema.
If it is greater than the current version, the server exits with an error.
In file src-exec/Migrate.hs
:
-
migrateCatalog
-> migrates catalog if required
When multiple instances of GraphQL engine are running on the same database, we need to sync the metadata changes across all instances. This will facilitate horizontal scaling of GraphQL engine server.
The server uses Postgres' notify
and listen
feature.
For any metadata request which modifies the state, the server makes an entry in
hdb_catalog.hdb_schema_update_event
table with its instance uuid.
The server listens on that table for events. Server checks uuid present in
an event payload with its uuid. If the event from another instance, then
server re-builds metadata and replaces in reference.
The server performs schema syncing in separate threads using forkIO
In file src-lib/Hasura/Server/SchemaUpdate.hs
:
-
startSchemaSync
-> start schema syncing threads
The Hasura GraphQL Engine uses Spock-core framework to build HTTP server and wai-websockets to build Websocket server. Learn more about cache IO Ref here
In src-lib/Hasura/Server/App.hs
:
-
mkWaiApp
-> Builds WAI application, Cache Reference
The HTTP API has following routes
- GET
/console
-> serves console if enabled - GET
/healthz
-> reports server health status - GET
/v1/version
-> fetch server version - POST
/v1/query
-> JSON Query API - POST
/v1/graphql
-> GraphQL API - POST
/v1/graphql/explain
-> Explain a GraphQL query - POST
/v1alpha1/pg_dump
-> PG Dump of database
Developer APIs:
- GET
/dev/plan_cache
-> fetch cached query plans - GET
/dev/subscriptions
-> fetch active subscriptions - GET
/dev/subscriptions/extended
-> fetch extended details of active subscriptions
The WebSocket server is used to perform GraphQL queries and subscriptions.
See file src-lib/Hasura/GraphQL/Transport/WebSocket.hs
.
The server creates two separate threads to fetch events from Postgres and process (consume) those events. An STM queue is used to propagate events between these two threads
See file src-lib/Hasura/Events/Lib.hs
for more details.
The Server creates a thread to check for updates and log if available. The checker frequency is a day.
See file src-lib/Server/CheckUpdates.hs
for more details.
If telemetry is enabled, the Server creates a thread to report anonymized metrics to the telemetry server regarding the usage of various features of Hasura.
See file src-lib/Hasura/Server/Telemetry.hs
for more details.
The Hasura GraphQL Engine metadata is application state or cache which stores essential information of
- Tables/Views
- SQL Functions
- Relationships
- Permissions
- Event Triggers
- Remote GraphQL Schemas
- GraphQL Query Collections
- Query Templates
Each one of above also referred to as metadata object
. The Metadata APIs
are used to add, drop or manage metadata. Only tables, views and SQL functions
present in Metadata are via GraphQL and JSON Query API.
Role-based GraphQL schema is auto-generated from metadata.
The type SchemaCache
(see src-lib/Hasura/RQL/Types/SchemaCache.hs
file)
is internal representation of cached metadata.
The server stores raw metadata in hdb_catalog
schema of Postgres.
On start-up, RQReloadMetadata
API call and in case of renames via RQRunSql
the server fetches metadata stored in hdb_catalog
schema and builds the
SchemaCache
. Cache, thus generated is replaced in IO reference.
In file src-lib/Hasura/RQL/DDL/Schema/Table.hs
:
-
buildSchemaCache
-> build metadata from postgres and remote servers
A metadata object is said to be inconsistent if there is an exception in making it using the raw data from Postgres and stitching schema from remote servers. In a few cases, the server should build metadata by ignoring inconsistent objects.
The server should ignore inconsistencies in the following cases:
- On startup
-
RQReloadMetadata
query type
The server should consider inconsistencies in the following cases:
- Migrating catalog on startup
- Renaming tables, columns and relationships
-
RQReplaceMetadata
query types
In file src-lib/Hasura/RQL/DDL/Schema/Table.hs
:
-
buildSchemaCacheStrict
-> build metadata and throw exception if any inconsistency
Server stores metadata in an IO reference with its version. Modification to metadata in reference is guarded with lock and this ensures each request has the latest metadata.
In file src-lib/Hasura/Server/App.hs
:
-
SchemaCacheRef
-> type represents cache reference -
withSCUpdate
-> function used to update cache reference
In Hasura metadata, one object is dependant on others. Adding, removing or
modifying an object may result in inconsistency of other objects.
Say, if a column is used to define a relationship and user is trying
to drop the column using RQRunSql
then there'll be inconsistency
in the relationship. We should track metadata dependencies and check for
them when necessary. This is represented through a dependency map which is a
hash map from a dependent
to a set of its dependencies
.
type DepMap = M.HashMap SchemaObjId (HS.HashSet SchemaDependency)
On dropping/modifying a dependency, the server obtains all dependents and reports to the client with an API exception.
In file src-lib/Hasura/Server/RQL/Types/SchemaCache.hs
:
-
DepMap
-> type that represents dependency map -
getDependentObjs
-> get dependents -
addToDepMap
-> add dependencies
See file src-lib/Hasura/RQL/DDL/Deps.hs
for functions related to dependencies
There are two kinds of JSON query APIs available via /v1/query
route.
They are
See type RQLQuery
in src-lib/Hasura/Server/Query.hs
file.
Query types RQInsert
, RQSelect
, RQUpdate
, RQDelete
and RQCount
are JSON queries
and rest all are Metadata queries. The RQBulk
query type essentially
accepts queries in bulk as an array.
These APIs are used to modify the metadata in both cache and
Postgres hdb_catalog
.
Users can manage metadata via
-
RQReplaceMetadata
-> Import and apply metadata -
RQExportMetadata
-> Export metadata -
RQClearMetadata
-> Clear user defined metadata -
RQReloadMetadata
-> Rebuild metadata from Postgres and add it to cache
Each metadata query occurs two phases:-
1. Validation:
In this phase, the incoming request if validated. For example, if a request is
made to track a table via RQTrackTable
, the server checks whether it is
being added already by looking up in schema cache.
See trackExistingTableOrViewP1
in src-lib/Hasura/RQL/DDL/Schema/Table.hs
file for tracking a table.
2. Execution:
In this phase, the server fetches required information from Postgres to make
the internal representation of metadata object.
For table it is TableInfo
(see src-lib/Hasura/RQL/Types/SchemaCache.hs
file)
Add the metadata object to cache along with it's dependencies. Learn more
about dependencies here.
If execution is successful, we get a success message JSON and modified schema cache. The modified schema cache is replaced in cache IO reference and success message is sent to the client.
[Metadata Request] -> validate with cache -> add/drop/modify catalog & cache -> [Success | Error]
Metadata objects and related modules:-
tables -> src-lib/Hasura/RQL/DDL/Schema/Table.hs
functions -> src-lib/Hasura/RQL/DDL/Schema/Function.hs
relationships -> src-lib/Hasura/RQL/DDL/Relationship.hs
permissions -> src-lib/Hasura/RQL/DDL/Permission.hs
remote schemas -> src-lib/Hasura/RQL/DDL/RemoteSchema.hs
event triggers -> src-lib/Hasura/RQL/DDL/EventTrigger.hs
query collections -> src-lib/Hasura/RQL/DDL/QueryCollection.hs
query templates -> src-lib/Hasura/RQL/DDL/QueryTemplate.hs
metadata management -> src-lib/Hasura/RQL/DDL/Metadata.hs
These are custom syntax in JSON to perform CRUD operations on table data. The console uses these APIs to fetch data from Postgres such as tables or functions to be tracked, foreign keys, Postgres type information etc. The incoming JSON payload is validated and resolved to an internal AST by enforcing permission rules for the role. The internal AST is compiled to the SQL AST which is convertible to SQL String.
[JSON Request] -> validation with permission -> [Internal AST] -> [SQL AST] -> [SQL String]
Query types and related module:
- select ->
src-lib/Hasura/RQL/DML/Select.hs
- delete ->
src-lib/Hasura/RQL/DML/Delete.hs
- update ->
src-lib/Hasura/RQL/DML/Update.hs
- insert ->
src-lib/Hasura/RQL/DML/Insert.hs
- count ->
src-lib/Hasura/RQL/DML/Count.hs
The GraphQL Engine, in a nutshell, is GraphQL/JSON to SQL compiler. All
queries and mutations are eventually converted to a custom Abstract Syntax
Tree (AST) which is encodable to a SQL String via ToSQL
type class.
In file src-lib/Hasura/SQL/DML.hs
:
-
SQLExp
-> the AST for Postgres SQL. -
Select
-> represents select SQL statement.
Any type which is convertible to SQL String.
In file src-lib/Hasura/SQL/Types.hs
:
class ToSQL a where
toSQL :: a -> TB.Builder
Where TB.Builder is a performant Text builder.
The Hasura GraphQL Engine server generates instant GraphQL APIs on top of Postgres table, views and SQL functions. The server also stitches remote schemas added to the metadata.
We use custom library graphql-parser-hs which has GraphQL AST and parser.
Our entire schema is auto-generated from metadata.
We generate role based schema in which permissions are enforced. For example,
if a role doesn't have permission to select a table then the schema related to
that table is not present in generated GraphQL schema. For each role, a GraphQL
context is generated and stored in SchemaCache
value.
In file src-lib/Hasura/GraphQL/Context.hs
:
-
GCtx
-> The GraphQL Context -
GCtxMap
-> Role to context map -
OpCtxMap
-> Operation context map
In file src-lib/Hasura/GraphQL/Schema.hs
:
-
mkGCtxMap
-> Builds GraphQL context
The incoming GraphQL request is validated along with variables and converted to annotated AST.
In file src-lib/Hasura/GraphQL/Validate.hs
:
-
validateGQ
-> validates the GraphQL request
In file src-lib/Hasura/GraphQL/Validate/Field.hs
:
-
SelSet
-> The annotated selection set
After validation, the annotated selection set (SelSet
) is executed per each
field. Operation context (OpCtxMap
) which is in GraphQL context
(GCtx
) has required information to resolve a query/mutation.
In file src-lib/Hasura/GraphQL/Execute/Query.hs
:
-
convertQuerySelSet
-> Resolve query selection set -
queryOpFromPlan
-> Resolve cached query
In file src-lib/Hasura/GraphQL/Execute.hs
:
-
resolveMutSelSet
-> Resolve mutation selection set
All incoming queries and subscriptions can be cached only if they satisfy:
- All variables types should be primitive scalars
- Each top-level field should use all available variables
In file src-lib/Hasura/GraphQL/Execute/Query.hs
:
-
QueryPlan
-> GraphQL query compiled to SQL query -
ReusableQueryPlan
-> Cachable query -
getReusablePlan
-> Make a cachable query if possible
The server supports native GraphQL introspection system.
In file src-lib/Hasura/GraphQL/Resolve/Introspect.hs
:
-
schemaR
-> Resolve__schema
field -
typeR
-> Resolve__type
field
TODO
The server supports fetching Postgres SQL query plan for a GraphQL query
via /v1/graphql/explain
endpoint. SQL, thus generated, is executed with
EXPLAIN ANALYZE.
[GraphQL Query Request] -> validate -> resolve to SQL -> execute SQL with EXPLAIN ANALYZE
In file src-lib/Hasura/GraphQL/Explain.hs
:
GQLExplain
-> Explain request
explainGQLQuery
-> Execute explain request
Server executes pg_dump
and returns the output. See src-rsr/run_pg_dump.sh
script.
In file src-lib/Hasura/Server/PGDump.hs
:
-
PGDumpReqBody
-> PG Dump API request -
execPGDump
-> Handler for PG Dump API
Executable
src-exec
├── Main.hs # the main module
├── Migrate.hs # catalog and metadata migrations
└── Ops.hs # operations
Library
src-lib
├── Data
│ ├── Aeson
│ │ └── Extended.hs
│ ├── HashMap
│ │ └── Strict
│ │ └── InsOrd
│ │ └── Extended.hs
│ ├── Parser
│ │ └── JSONPath.hs # JSON path parser
│ ├── Sequence
│ │ └── NonEmpty.hs # Non empty sequence
│ ├── TByteString.hs
│ └── Text
│ └── Extended.hs
├── Hasura
│ ├── Cache.hs # Abstractions for Unbounded cache
│ ├── Db.hs # Database operations
│ ├── EncJSON.hs # JSON text builder
│ ├── Events # Event triggers related
│ │ ├── HTTP.hs
│ │ └── Lib.hs
│ ├── GraphQL # All GraphQL related
│ │ ├── Context.hs
│ │ ├── Execute
│ │ │ ├── LiveQuery # Subscriptions
│ │ │ │ ├── Fallback.hs
│ │ │ │ ├── Multiplexed.hs
│ │ │ │ └── Types.hs
│ │ │ ├── LiveQuery.hs
│ │ │ ├── Plan.hs
│ │ │ └── Query.hs # HTTP query
│ │ ├── Execute.hs
│ │ ├── Explain.hs # Analyze GraphQL query
│ │ ├── RemoteServer.hs # Remote schemas
│ │ ├── Resolve # GraphQL query resolving
│ │ │ ├── BoolExp.hs
│ │ │ ├── Context.hs
│ │ │ ├── ContextTypes.hs
│ │ │ ├── InputValue.hs
│ │ │ ├── Insert.hs
│ │ │ ├── Introspect.hs
│ │ │ ├── Mutation.hs
│ │ │ └── Select.hs
│ │ ├── Resolve.hs
│ │ ├── Schema.hs # GraphQL schema generation
│ │ ├── Transport
│ │ │ ├── HTTP
│ │ │ │ └── Protocol.hs
│ │ │ ├── HTTP.hs
│ │ │ ├── WebSocket
│ │ │ │ ├── Protocol.hs
│ │ │ │ └── Server.hs
│ │ │ └── WebSocket.hs
│ │ ├── Utils.hs
│ │ ├── Validate # GraphQL query validation
│ │ │ ├── Context.hs
│ │ │ ├── Field.hs
│ │ │ ├── InputValue.hs
│ │ │ └── Types.hs
│ │ └── Validate.hs
│ ├── HTTP.hs
│ ├── Logging.hs # Global logging related
│ ├── Prelude.hs # Custom prelude
│ ├── RQL # JSON queries
│ │ ├── DDL # Metadata related
│ │ │ ├── Deps.hs
│ │ │ ├── EventTrigger.hs
│ │ │ ├── Headers.hs
│ │ │ ├── Metadata.hs
│ │ │ ├── Permission
│ │ │ │ ├── Internal.hs
│ │ │ │ └── Triggers.hs
│ │ │ ├── Permission.hs
│ │ │ ├── QueryCollection.hs
│ │ │ ├── QueryTemplate.hs
│ │ │ ├── Relationship
│ │ │ │ ├── Rename.hs
│ │ │ │ └── Types.hs
│ │ │ ├── Relationship.hs
│ │ │ ├── RemoteSchema.hs
│ │ │ ├── Schema
│ │ │ │ ├── Diff.hs
│ │ │ │ ├── Function.hs
│ │ │ │ ├── Rename.hs
│ │ │ │ └── Table.hs
│ │ │ └── Utils.hs
│ │ ├── DML # JSON CRUD queries
│ │ │ ├── Count.hs
│ │ │ ├── Delete.hs
│ │ │ ├── Insert.hs
│ │ │ ├── Internal.hs
│ │ │ ├── Mutation.hs
│ │ │ ├── QueryTemplate.hs
│ │ │ ├── Returning.hs
│ │ │ ├── Select
│ │ │ │ ├── Internal.hs
│ │ │ │ └── Types.hs
│ │ │ ├── Select.hs
│ │ │ └── Update.hs
│ │ ├── GBoolExp.hs # SQL boolean expressions
│ │ ├── Instances.hs
│ │ ├── Types
│ │ │ ├── BoolExp.hs
│ │ │ ├── Catalog.hs
│ │ │ ├── Common.hs
│ │ │ ├── DML.hs
│ │ │ ├── Error.hs
│ │ │ ├── EventTrigger.hs
│ │ │ ├── Metadata.hs
│ │ │ ├── Permission.hs
│ │ │ ├── QueryCollection.hs
│ │ │ ├── RemoteSchema.hs
│ │ │ ├── SchemaCache.hs
│ │ │ └── SchemaCacheTypes.hs
│ │ └── Types.hs
│ ├── Server # API server
│ │ ├── App.hs # Creates Http and Websocket app
│ │ ├── Auth # Authorization related
│ │ │ ├── JWT # JSON Web Token
│ │ │ │ ├── Internal.hs
│ │ │ │ └── Logging.hs
│ │ │ └── JWT.hs
│ │ ├── Auth.hs
│ │ ├── CheckUpdates.hs # Checks if any new version
│ │ ├── Config.hs # Get server configuration
│ │ ├── Context.hs
│ │ ├── Cors.hs
│ │ ├── Init.hs # Initialise server and CLI parser
│ │ ├── Logging.hs # Server logging
│ │ ├── Middleware.hs
│ │ ├── PGDump.hs # Dump postgres
│ │ ├── Query.hs # JSON query handler
│ │ ├── SchemaUpdate.hs # Horizontal scaling
│ │ ├── Telemetry.hs # Reports telemetry
│ │ ├── Utils.hs
│ │ └── Version.hs # Server version
│ └── SQL # SQL related
│ ├── DML.hs # AST
│ ├── GeoJSON.hs # JSON representation of geography/geometry types
│ ├── Rewrite.hs # Rewrite SQL Select AST with modified identifiers
│ ├── Time.hs # Postgres time
│ ├── Types.hs # Haskell types for Postgres
│ └── Value.hs # Postgres SQL value parser
└── Network
└── URI
└── Extended.hs # instances for URI
Resources
src-rsr
├── catalog_metadata.sql # SQL to fetch server metadata from Postgres
├── console.html # console HTML template
├── hdb_metadata.yaml # System defined Metadata
├── initialise.sql # Init SQL script
├── insert_trigger.sql.j2 # Insert permission trigger template
├── introspection.json # The GraphQL introspection query payload
├── migrate_from_10_to_11.sql # Migration scripts
├── migrate_from_11_to_12.sql
├── migrate_from_12_to_13.sql
├── migrate_from_13_to_14.sql
├── migrate_from_14_to_15.sql
├── migrate_from_15_to_16.sql
├── migrate_from_1.sql
├── migrate_from_4_to_5.sql
├── migrate_from_5_to_6.sql
├── migrate_from_6_to_7.sql
├── migrate_from_7_to_8.sql
├── migrate_from_8_to_9.sql
├── migrate_from_9_to_10.sql
├── migrate_metadata_from_15_to_16.yaml
├── migrate_metadata_from_1.yaml
├── migrate_metadata_from_4_to_5.yaml
├── migrate_metadata_from_7_to_8.yaml
├── migrate_metadata_from_8_to_9.yaml
├── run_pg_dump.sh # Script to dump postgres schema
├── schema.graphql # Default GraphQL schema
├── table_meta.sql # Fetch a table's metadata
└── trigger.sql.j2 # Event trigger template
- All files with
migrate_from_*
are catalog schema migrations. - All files with
migrate_metadata_from_*
are metadata migrations. - All files with
*.j2
are jinja templates