SimpleGQL
What is SimpleGQL?
The zef.gql.server_simplegql
provides a way to host a GraphQL server using
only a GraphQL schema file. No setup of the Zef graph is required.
The purpose of SimpleGQL is to provide frontend developers a backend that "just
works" but can later be extended/interacted with the full suite of Zef capability.
This page assumes you understand GraphQL schema files.
Prepare a SimpleGQL-compatible GraphQL schema file
A SimpleGQL-compatible schema file contains only type
and enum
definitions,
and can look like:
# Zef.SchemaVersion: v1
type Employee {
firstName: String!
lastName: String!
dob: DateTime
reportsTo: Employee
responsibleFor: [Employee] @relation(rt: ReportsTo) @incoming
department: Department
status: EmploymentStatus
}
enum EmploymentStatus {
EMPLOYED
RETIRED
HIRING
}
type Department {
name: String!
employees: [Employee] @relation(rt: Department) @incoming
}
To host this as a GraphQL server, we save this file to sample.graphql
and then
run:
python3 -m zef.gql.simplegql --schema-file=sample.graphql --data-tag=data --create --port 5001 --bind 0.0.0.0
Note that this command uses (or creates if it doesn't already exist) a graph
with the tag data
. You may then point your browser at the GraphQL
Playground and enter http://localhost:5001/gql
to
explore the GraphQL API. Try creating a department and employee and then
querying them.
For help on the command line arguments, run:
python3 -m zef.gql.simplegql --help
SimpleGQL directives
The SimpleGQL schema follows standard GraphQL schema rules for type
and
enum
. In addition it supports the following directives on fields:
@relation(source, rt, target)
: indicates the underlying Zef graph relation
that should be traversed from this type of entity to obtain the field.@relation(rt)
as above, but where the source and target are guessed.@incoming
the underlying Zef graph relation is incoming to this entity.@unique
the field should be kept unique. Attempts to create entities with
the same field will result in an error.@search
: indicates this field is searchable and sortable in queries.
A special directive on a type of @upfetch
can also be used. This should be
used sparingly, and only when an atomic "check or create" operation is needed.
For example, a user account which is uniquely identified by its email could
have:
type User
@upfetch(field: "email") {
email: String! @unique @search
givenName: String
...
}
Only one field can be marked as the upfetch field of a type, and it creates a
special mutation for the GraphQL API. As an upfetch field is similar to a UID,
it must be unique and required. Note that each entity also has a UID, which
can be used instead.
Authentication
The types also support the @auth
directive. This is best described with an
example:
# Zef.SchemaVersion: v1
# Zef.Authentication: {"JWKURL": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com", "Audience": "firebase-audience-here", "Header": "X-Auth-Token"}
type User @auth(
query: "z | Out[RT.FirebaseID] | value | equals[info.context | get_in[('auth', 'USER_ID')][None] | collect]",
add: "info.context | get_in[('auth', 'admin')][False]"
) {
firebaseID: String! @unique
email: String @unique
givenName: String
familyName: String
phone: String
birthday: DateTime
}
The query
, add
, update
, updatePost
and delete
arguments are each user-defined
functions which act as predicates that should return whether this operation is
permitted. Each function has access to the variables:
z
: the current entity of the GraphQL type.type_node
: the entity corresponding to the GraphQL type schema node.info
: the ariadneinfo
struct
Each function is run at a different time and has some default fallback behaviour:
query
: runs whenever an entity is traversed. If empty will returnTrue
.add
: runs whenever a new entity is created, using the graph state as if
that entity has already been created. If not present, will fallback toquery
.update
: runs whenever an entity would be modified, using the graph state
before the modification. Falls back toquery
.updatePost
: runs whenever an entity would be modified, using the graph state
as if the modification has already happened. Falls back toupdate
.delete
: runs whenever an entity would be deleted, using the graph state
before the deletion. Falls back toupdate
.
Auth variables
The auth configuration must be present in a line with a comment beginning:
# Zef.Authentication:
and the remainder of the line will be parsed as a JSON object. The configuration
must be on one line only, without line breaks.
The auth configuration can be either for an asymmetric or symmetric key sharing.
In either case, JWTs are used to validate user sessions. The allowed keys in the
configuration JSON object are:
- Algo: either 'RS256' for asymmetric key sharing with a JWT or 'HS256' for
symmetric key sharing. - JWKURL: only allowed when
Algo
is 'RS256'. The URL where the JWK key can be obtained. - VerificationKey: only allowed when
Algo
is 'HS256'. The preshared key for
symmetric authentication as a string. Any\n
present in the string will be
replaced with line breaks. - VerificationKeyEnv: only allowed when
Algo
is 'HS256'. The name of an
environment variable where the preshared key is located. - Audience: to validate the audience field of the JWT
- Header: what HTTP header is used to find the JWT in client connections.
- Namespace: if not given, the entire JWT object is made available as
info.context['auth']
, otherwise only the key in the JWT given byNamespace
is made available atinfo.context['auth']
. - Public: if True (the default) then HTTP requests without a JWT are allowed
through withinfo.context['auth']
set toNone
.
If auth has been verified, the query progresses and the entire JWT is made
available at info.context['auth']
. If only a part of the JWT should be made
available, the configuration key Namespace
should be used.