-
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