Function(Ctx ctx, P parent)`. The Stream of values will be returned in the `data` field of the `GraphQLResult` returned on execution.
If using a WebSocket server, the client should support either `graphql-transport-ws` or `graphql-ws` sub-protocols.
```dart
final apiSchema = GraphQLSchema(
queryType: objectType('Query'),
subscriptionType: objectType(
'Subscription',
fields: [
graphQLInt.nonNull().fields(
'secondsSinceSubscription',
subscribe: (Ctx ctx, Object rootValue) {
return Stream.periodic(const Duration(seconds: 1), (secs) {
return secs;
});
}
),
]
),
);
Future main() async {
final GraphQLResult result = await GraphQL(apiSchema).parseAndExecute(
'subscription { secondsSinceSubscription }',
);
assert(result.isSubscription);
final Stream stream = result.subscriptionStream!;
stream.listen((event) {
final data = event.data as Map;
assert(data['secondsSinceSubscription'] is int);
print(data['secondsSinceSubscription']);
});
}
```
The `resolve` callback in a subscription field will always receive a `SubscriptionEvent` as it's parent.
From that you can access the event value with `SubscriptionEvent.value` which will be the emitted by the Stream returned in the `subscribe` callback. The error handling in each callback is different, if an error is thrown in the `subscribe` callback, the Stream will end with an error. But if you throw an error in the `resolve` callback it will continue sending events, just the event resolved with a thrown Object will have `GraphQLError`s as a result of processing the thrown Object (More information in [Error Handling](#error-handling)).
For usage in a web server you can use any of the [web server integrations](#web-integrations) which support WebSocket subscriptions (For example, [leto_shelf](https://github.com/juancastillo0/leto/tree/main/leto_shelf)).
### Examples
For a complete subscriptions example with events from a database please see the [chat_example](https://github.com/juancastillo0/leto/tree/main/chat_example), in particular the [events](https://github.com/juancastillo0/leto/tree/main/chat_example/server/lib/events) directory.
## Request Contexts
All `Ctx`s implement `ScopedHolder`, so that then can be used to retrieve values from the scoped map, more in [`ScopedMap`](#scopedmap).
### Ctx
[Source Code](https://github.com/juancastillo0/leto/blob/main/leto_schema/lib/src/req_ctx.dart)
A unique context for each field resolver
- args: the arguments passed as inputs to this field
- object: the parent Object's value, same as the first parameter of `resolve`.
- objectCtx: the parent Object's execution context ([ObjectExecutionCtx](#objectexecutionctx))
- field: The `GraphQLObjectField` being resolved
- path: The path to this field
- executionCtx: The request's execution context ([ExecutionCtx](#executionctx))
- lookahead: A function for retrieving nested selected fields. More in the [LookAhead section](#lookahead-eager-loading)
### ObjectExecutionCtx
This is the context associated with an object execution, can be retrieved through `Ctx.objectCtx`. There will be as many instances as there are objects to execute in the request. Contains the value of the object, the field selections and the path in the GraphQL request to this object.
### ExecutionCtx
This is the context associated with the execution phase of the request, created after the validation phase.
Contains validated and coerced (parsed) input values and the specific validated operation within the request's document to execute. It has an `errors` list with the encountered errors during execution. Can be retrieved with `ObjectExecutionCtx.executionCtx`.
### RequestCtx
This is the base context associated with the request, contains the raw information about the GraphQL document, the raw (not validated nor parsed) input values, input extensions, the schema, root value and the scoped map for this request. Can be retrieved with `ExecutionCtx.requestCtx`.
# Validation
## Schema Validation
[GraphQL Specification](https://spec.graphql.org/draft/#sec-Type-System)
Implements the "Type Validation" sub-sections of the specification's "Type System" section.
Guaranties that the `GraphQLSchema` instance is valid, verifies the Type System validations in the specification. For example, an Object field's type can only be an Output Type or an Union should have at least one possible type and all of them have to be Object types.
This will be executed before stating a GraphQL server. Leto implements all of the Specification's schema validation. The code for all rules can be found in the [validate_schema.dart](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/validate/validate_schema.dart) file in `package:leto_schema`.
## Document Validation
[GraphQL Specification](https://spec.graphql.org/draft/#sec-Validation)
This will be executed before executing any request. Leto implements all of the Specification's document validation. The code for all rules can be found in the [validate](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/validate) folder in `package:leto_schema`.
You can add custom validation rules to a server with the `GraphQL.customValidationRules` parameter, they will be added on top of the `specifiedValidationRules`. One example of a custom validation rule is the [Query Complexity](#query-complexity) validation.
## Query Complexity
[Tests](https://github.com/juancastillo0/leto/tree/main/leto_schema/test/validation/query_complexity_test.dart)
This document validation rule allows you to restrict the complexity of a GraphQL request.
The provided `queryComplexityRuleBuilder` returns a `ValidationRule` that reports errors when the `maxComplexity` or `maxDepth` configuration parameters are reached.
- `maxComplexity`
Specifies the maximum complexity for a given operation. The complexity is measured based on the selected fields and should be. If this complexity is surpassed (is greater) a validation error will be reported.
- `maxDepth`
Specifies the maximum depth for a given operation. The depth is defined as the number of objects (including the root operation object) that have to be traversed to arrive to a given field. If this depth is surpassed (is greater) a validation error will be reported.
The complexity for each fieldNode is given by:
`complexity = fieldComplexity + (childrenComplexity + fieldTypeComplexity) * complexityMultiplier`
Where fieldComplexity is the `ElementComplexity` in
`GraphQLObjectField.attachments` or `defaultFieldComplexity`
if there aren't any.
childrenComplexity is:
- scalar or enum (leaf types): 0
- object or interface: sum(objectFieldsComplexities)
- union: max(possibleTypesComplexities)
fieldTypeComplexity will be taken as the `ElementComplexity`
from `GraphQLNamedType.attachments` or 0 if there aren't any.
If the fieldType is a `GraphQLListType`, complexityMultiplier
will be the provided `listComplexityMultiplier`, otherwise 1.
## Skip validation with Persisted Queries
TODO: 1A
Using the `PersistedQueriesExtensions` you can set the `skipValidation` parameter so that the validation is skipped for already cached (and validated) documents.
## Input Validation
Input validation refers to the verification of the values or structure of the payload sent as input in a request. It could, of coursed, be performed manually before the execution of each request. However, we provide a couple of tools to help with the process, in particular using code generation and the [`valida` package](https://github.com/juancastillo0/valida).
The following example shows an input object `ConnectionArguments` with the annotations `@Valida()` and its fields `first` and `last` with the annotation `@ValidaNum(min: 1)`. If the `ConnectionArguments` object is used as input in a resolver, the validation is performed over the input value on execution and an error will be thrown if either `first` or `last` are less than 1. For more information on the supported annotations and validations, view the [`valida` package](https://github.com/juancastillo0/valida).
```dart
@JsonSerializable()
@Valida()
@GraphQLInput()
class ConnectionArguments {
/// Returns the items in the list that come before the specified cursor.
final String? before;
/// Returns the items in the list that come after the specified cursor.
final String? after;
/// Returns the first n items from the list.
@ValidaNum(min: 1)
final int? first;
/// Returns the last n items from the list.
@ValidaNum(min: 1)
final int? last;
const ConnectionArguments({
this.before,
this.after,
this.first,
this.last,
});
factory ConnectionArguments.fromJson(Map json) =>
_$ConnectionArgumentsFromJson(json);
Map toJson() => _$ConnectionArgumentsToJson(this);
}
```
# Miscellaneous
## `GraphQLResult`
[GraphQL Specification](https://spec.graphql.org/draft/#sec-Response)
The returned `GraphQLResult` is the output of the execution of a GraphQL request it contains the encountered `GraphQLError`s, the output `extensions` and the `data` payload. The `GraphQLResult.toJson` Map is used by `package:leto_shelf` when constructing an HTTP response's body.
- The `data` is a `Map?` for Queries and Mutations or a `Stream` for subscriptions. It has the payload returned by the resolvers during execution. Will be null if there was an error in validation or in the execution of a non-nullable root field. If there was an error in validation, the `data` property will not be set in the `GraphQLResult.toJson` Map following the [spec](https://spec.graphql.org/draft/#sec-Response).
- The `errors` contain the `GraphQLError`s encountered during validation or execution. If a resolver throws an error, it will appear in this error list. If the field's return type is nullable, a null value will be set as the output for that field. If the type is non-nullable the resolver will continue to throw an exception until a nullable field is reached or the root resolver is reached (in this case the `GraphQLResult.data` property will be null).
- The `extensions` is a `Map?` with custom values that you may want to provide to the client. All values should be serializable since they may be returned as part of an HTTP response. Most `GraphQLExtensions` modify this values to provide additional functionalities. The keys for the `extensions` Map should be unique, you may want to prefix them with an identifier such as a package name.
## `ScopedMap`
[Source code](https://github.com/juancastillo0/leto/blob/main/leto_schema/lib/src/req_ctx.dart)
An `ScopedMap` allows you to pass and use dependencies or services within your resolvers or extensions. It consists of multiple maps, one for each scope, and a set of immutable references (or keys) with overridable defaults.
To retrieve a value from a reference, the map checks whether a value was already instantiated for the scope or in any of its parents. If it has not been instantiated, the default is returned and saved in the scope.
This forms a tree of scopes, where one node scope has access to its parent values.
To override the value of a reference for a given scope you instantiate a `ScopedMap` with the values to override, if it is a child, you can pass the parent as a parameter to the constructor.
### `ScopedHolder`
A `ScopedHolder` is simply an object that contains a `ScopedMap get globals;` getter. This map represents the scope associated with the object. As discussed in the [Request Contexts section](#request-contexts), all contexts are (implement) `ScopedHolder`s and therefore have access to the values in the scope.
### `ScopedRef`
You can specify the behavior and the default values of references using `ScopedRef`. As explained in the source code docs, a "global" ref will instantiate a value accessible to all scopes in a scope tree. A "local" ref will instantiate the value (and make it accessible) only to children scopes in which the value is instantiated.
### Example usage
Example usage with the `GraphQL` executor and different ways to override the values is shown in the following code snippet:
```dart
final ScopedRef ref = ScopedRef.global((ScopedMap scope) => 4);
final schema = GraphQLSchema(
queryObject: objectType(
'Query',
fields: [
graphQLint.field('fieldName', (ctx) => ref.get(ctx)),
],
),
);
final executorWithDefault = GraphQL(schema);
var result = await executorWithDefault.parseAndExecute('{fieldName}');
var data = result.data as Map;
assert(data['fieldName'] == 4);
result = await executorWithDefault.parseAndExecute(
'{fieldName}',
globalVariables: {
ref: 6,
},
);
data = result.data as Map;
assert(data['fieldName'] == 6);
final executorWithOverride = GraphQL(
schema,
globalVariables: ScopedMap(
{
ref: 5,
},
),
);
result = await executorWithOverride.parseAndExecute('{fieldName}');
data = result.data as Map;
assert(data['fieldName'] == 5);
```
## Error Handling
One typically has multiple options to represent and let the client know that there was an error in the request.
If using HTTP (or WebSockets) fatal errors such as a malformed query string are already handled and follow the spec in each case.
### Exceptions and `GraphQLError`
If an error does not require a different type to be expressed and a more implicit approach is preferable, perhaps for errors that happen in most endpoints (authentication, authorization, input validation), one can use exceptions and send the necessary information through custom extensions in the payload.
```dart
@Query()
Future userChats(Ctx ctx) async {
final user = await userFromCtx(ctx);
if (user == null) {
throw GraphQLError(
'This endpoint requires an authenticated user.', // message
extensions: {
'appError': {
'code': 'UNAUTHENTICATED',
},
},
// You can also pass a `sourceError` and `stackTrace` if the given error
// was generated from an exception
// sourceError,
// stackTrace,
);
}
// You could also throw a list of errors with GraphQLException
final errors = [
if (!user.emailVerified)
GraphQLError(
'This functionality requires that you verify your email.', // message
extensions: {
'appError': {
'code': 'UNVERIFIED_EMAIL',
},
},
),
if (!user.canReadUserChats)
GraphQLError(
'You do not have access to this functionality.', // message
extensions: {
'appError': {
'code': 'UNAUTHORIZED',
},
},
),
];
if (errors.isNotEmpty) throw GraphQLException(errors);
// AUTHORIZED, now get the userChats
}
```
Of course, this could be abstracted and structured in a better way. For example, the "UNAUTHENTICATED" error could be a constant or it could be thrown inside `userFromCtx(ctx)` call. The `appError` key could be anything you want, but is has to be unique to avoid overriding other extensions.
### Result types
- Result
```graphql
"""
SomethingT! when the operation was successful or SomethingE! when an error was encountered.
"""
type ResultSomethingTSomethingE {
ok: SomethingT
err: SomethingE
isOk: Boolean!
}
```
- ResultU
```graphql
"""
SomethingT when the operation was successful or SomethingE when an error was encountered.
"""
union ResultUSomethingTSomethingE = SomethingT | SomethingE
```
### Error lists and interfaces
The error in the result union or object could be a simple object specific to the resolver. However, it could also by an union, an object that implements an "ApplicationError" interface or a list of errors, where the errors could also be of union type or objects that implement interfaces, or both. For a more thorough discussion on the topic, this [guide to GraphQL errors](https://productionreadygraphql.com/2020-08-01-guide-to-graphql-errors) may help you.
## Hot Reload and Cycles
Since type and field schema definitions should probably be reused, this may pose a conflict to the beautifully hot reload capabilities of Dart. The cached instances will not change unless you execute the more expensive hot restart, which may also cause you to lose other state when developing.
Because of this, we provide an utility class `HotReloadableDefinition` that handles definition caching, helps with cycles in instantiation and controls the re-instantiation of values. It receives a `create` function that should return a new instance of the value. This value will be cached and reused throughout the schema's construction. To retrieve the current instance you can use the `HotReloadableDefinition.value` getter.
The provided `create` function receives a `setValue` callback that should be called right after the instance's creation (with the newly constructed instance as argument), this is only necessary if the instance definition may contain cycles.
To re-instantiate all values that use `HotReloadableDefinition` you can execute the static `HotReloadableDefinition.incrementCounter` which will invalidate previously created instances, if you call `HotReloadableDefinition.value` again, a new instance will be created with the, potentially new, hot reloaded code.
When using code generation all schema definitions use the `HotReloadableDefinition` class to create type and field instances, you only need to call the generated `recreateGraphQLApiSchema` function to instantiate the `GraphQLSchema` each time the application hot reloads.
You can use other packages to hot reload the dart virtual machine (vm), for example:
- If using shelf you may want to try https://pub.dev/packages/shelf_hotreload. Most shelf examples in this repository already use this package.
- You could also search in https://pub.dev or try https://pub.dev/packages/hotreloader, which is used by `package:shelf_hotreload`.
# Solving the N+1 problem
When fetching nested fields, a specific resolvers could be executed multiple times for each request since the parent object will execute it for all its children. This may pose a problem when the resolver has to do non-trivial work for each execution. For example, retrieving a row from a database. To solve this problem, Leto provides you with two tools: LookAhead and DataLoader.
## LookAhead (Eager loading)
You can mitigate the N+1 problem by fetching all the necessary information from the parent's resolver so that when the nested fields are executed they just return the previously fetch items. This would prevent all SQL queries for nested fields since the parent resolver has all the information about the selected nested fields and can use this to execute a request that fetches the necessary columns or joins.
```dart
@GraphQLObject()
class Model {
final String id;
final String name;
final NestedModel? nested;
const Model(this.id, this.name, this.nested);
}
@GraphQLObject()
class NestedModel {
final String id;
final String name;
const NestedModel(this.id, this.name);
}
final modelRepo = ScopedRef.global(
(ScopedHolder scope) => ModelRepo();
);
class ModelRepo {
List getModels({bool withNested = false}) {
// request the database
// if `withNested` = true, join with the `nestedModel` table
throw Unimplemented();
}
}
@Query()
FutureOr> getModels(Ctx ctx) {
final PossibleSelections lookahead = ctx.lookahead();
assert(!lookahead.isUnion);
final PossibleSelectionsObject lookaheadObj = lookahead.asObject;
final withNested = lookaheadObj.contains('nested');
final ModelRepo repo = modelRepo.get(ctx);
return repo.getModels(withNested: withNested);
}
```
With this implementation and given the following queries:
```graphql
query getModelsWithNested {
getModels {
id
name
nested {
id
name
}
}
}
query getModelsBase {
getModels {
id
name
}
}
```
`ModelRepo.getModels` will receive `true` in the `withNested` param for the `getModelsWithNested` query since `lookaheadObj.contains('nested')` will be `true`. On the other hand, the `withNested` param will be `false` for the `getModelsBase` query since the "nested" field was not selected.
In this way, `ModelRepo.getModels` knows what nested fields it should return. It could add additional joins in a SQL query, for example.
The `PossibleSelections` class has the information about all the nested selected fields when the type of the field is a Composite Type (Object, Interface or Union). When it's an Union, it will provide a map from the type name Object variants to the given variant selections. The @skip and @include directives are already taken into account. You can read more about the `PossibleSelections` class in the [source code](https://github.com/juancastillo0/leto/blob/main/leto_schema/lib/src/req_ctx.dart).
## DataLoader (Batching)
The code in Leto is a port of [graphql/dataloader](https://github.com/graphql/dataloader).
An easier to implement but probably less performant way of solving the N+1 problem is by using a `DataLoader`. It allows you to batch multiple requests and execute the complete batch in a single function call.
```dart
@GraphQLObject()
class Model {
final String id;
final String name;
final int nestedId;
const Model(this.id, this.name, this.nestedId);
NestedModel nested(Ctx ctx) {
return modelNestedRepo.get(ctx).getNestedModel(nestedId);
}
}
class NestedModelRepo {
late final dataLoader = DataLoader.unmapped(getNestedModelsFromIds);
Future> getNestedModel(String id) {
// Batch the id, eventually `dataLoader` will execute
// `getNestedModelsFromIds` with a list of batched ids
return dataLoader.load(id);
}
Future> getNestedModelsFromIds(List ids) {
// Multiple calls to `Model.nested` will be batched and
// all ids will be passed in the `ids` argument
// request the database
final List models = throw Unimplemented();
// Make a map from id to model instance
final Map modelsMap = models.fold(
{}, (map, model) => map..[model.id] = model
);
// Return the models in the same order as the `ids` argument
return List.of(ids.map((id) => modelsMap[id]!));
}
}
final modelNestedRepo = ScopedRef.local(
(scope) => NestedModelRepo()
);
@Query()
List getModels(Ctx ctx) {
return modelRepo.get(ctx).getModels();
}
```
The DataLoader has some options for configuring it. For example you can specify the maximum size of the batch (default: `2^53` or the maximum javascript integer), whether to batch requests or not (default: `true`) and provide a custom batch schedule function, by default it will use `Future.delayed(Duration.zero, executeBatch)`.
You can also configure caching by providing a custom cache implementation, a custom function that maps the key passed to `DataLoader.load` to the cache's key or disabling caching in the DataLoader.
## Combining LookAhead with DataLoader
You can use both, LookAhead and DataLoader at the same time. The keys provided to the `DataLoader.load` function can be anything, so you could send the `PossibleSelection` information, for example.
# Extensions
Extensions implement additional functionalities to the server's parsing, validation and execution. For example, extensions for tracing ([GraphQLTracingExtension](#apollo-tracing)), logging ([GraphQLLoggingExtension](#logging-extension)), error handling or caching ([GraphQLPersistedQueries](#persisted_queries) and [GraphQLCacheExtension](#response-cache)). All extension implementations can be found in the [extensions](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions) folder in `package:leto`. The main API with all the methods that can be overridden is found in [this file](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/extension.dart).
## Persisted Queries
Save network bandwidth by storing GraphQL documents on the server and not requiring the Client to send the full document String on each request.
More information: https://www.apollographql.com/docs/apollo-server/performance/apq/
[Source code](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/persisted_queries.dart)
## Apollo Tracing
Trace the parsing, validation and execution of your GraphQL server to monitor execution times of all GraphQL requests.
More information: https://github.com/apollographql/apollo-tracing
[Source code](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/tracing.dart)
## Response Cache
Utility for caching responses in your GraphQL server and client.
Client GQL Link implementation in:
// TODO: 2E
- Hash: Similar to HTTP If-None-Match and Etag headers. Computes a hash of the payload (sha1 by default) and returns it to the Client when requested. If the Client makes a request with a hash (computed locally or saved from a previous server response), the extension compares the hash and only returns the full body when the hash do not match. If the hash match, the client already has the last version of the payload.
- MaxAge: If passed a `Cache` object, it will save the responses and compare the saved date with the current date, if the maxAge para is greater than the difference, it returns the cached value without executing the field's resolver.
- UpdatedAt: Similar to HTTP If-Modified-Since and Last-Modified headers.
// TODO: 2E retrieve hash, updatedAt and maxAge in resolvers.
[Source code](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/cache_extension.dart)
## Logging Extension
The logging extension allows you monitor requests and responses executed by your server.
Provides some utilities for printing and retrieving information from execution, logging errors and provides a default `GraphQLLog` class that contains aggregated information about the request.
[Source code](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/logging_extension.dart)
## Map Error Extension
Simple extension for mapping an error catched on resolver execution.
With a function that receives the thrown error and some context as parameter and returns a `GraphQLException?`, this extension will override the error and pass it to the executor, which will eventually return it to the user as an error in the response's `errors` list.
[Source code](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/map_error_extension.dart)
## Custom Extensions
To create a custom extension you can extend [`GraphQLExtension`](https://github.com/juancastillo0/leto/blob/main/leto/lib/src/extensions/extension.dart) and override the necessary functions, all of which are executed throughout a request's parsing, validation and execution.
To save state scoped to a single request you can use the `ScopedMap.setScoped(key, value)` and retrieve the state in a different method with `final value = ScopedMap.get(key);`. Where the `ScopedMap` can be accessed with `ctx.globals`.
All extensions are implemented in this way, so you can look at the source code for some examples.
# Directives
For more information: [GraphQL specification](https://spec.graphql.org/draft/#sec-Type-System.Directives)
[`GraphQLDirective`](https://github.com/juancastillo0/leto/blob/main/leto_schema/lib/src/directive.dart) allows you to provide more information about different elements of your schema and queries.
The default skip, include, deprecated and specifiedBy directives are provided. Fields in the different type system definition classes allow you to include the deprecated reason for fields or enum values, and a url of the specification for scalar types. This information will be printed when using the `printSchema` utility, can be retrieved in Dart through GraphQL extension for modifying the behavior of request execution or, if introspection is enabled, will be exposed by the GraphQL server.
The skip and include directives are supported during document execution following the spec. Right now, custom directives on execution can be obtained by using the parsed `DocumentNode` from package:gql, in the future better support could be implemented.
Provide custom directives supported by your server through the
`GraphQLSchema.directives` field.
You can retrieve custom directives values in your GraphQL Schema definition when using the `buildSchema` utility, which will parse all directives and leave them accessible through the `astNode` Dart fields in the different GraphQL elements. Setting custom directives values through the GraphQL Schema Dart classes is a work in progress. Right now, you can add `DirectiveNode`s to the element's [attachments](#attachments) if you want to print it with `printSchema`, however the API will probably change. See https://github.com/graphql/graphql-js/issues/1343
## `KeyDirective`
Specifies that a given Object can be identified by the fields
passed as argument to the directive
It is repeatable, there can be multiple keys per Object.
The following example shows an Object that can be identified by two keys,
the "id" field and the combination "type" and "nested.value" fields.
```graphql
type Model @key(fields: "id") @key(fields: "type nested { value } ") {
id: String!
type: String!
nested {
value: int!
}
}
```
## `ValidaDirective`
Using `package:valida`, the valida directive represents the validation configuration. At the moment the `ValidaField` annotation over arguments and input fields is used to populated the valida directive. For example, the following annotated `GraphQLInput` that verifies that all lengths inside the `strs` field have at least 1 byte length:
```dart
@Valida()
@GraphQLInput()
class ValidaArgModel {
@ValidaList(each: ValidaString(minLength: 1))
final List strs;
final ValidaArgModel? inner;
ValidaArgModel({
required this.strs,
this.inner,
});
Map toJson() {
return {
'strs': strs,
'inner': inner?.toJson(),
};
}
factory ValidaArgModel.fromJson(Map map) {
return ValidaArgModel(
strs: List.from(map['strs']! as List),
inner: map['inner'] != null
? ValidaArgModel.fromJson((map['inner']! as Map).cast())
: null,
);
}
}
```
Will generate the following GraphQL definition with valida directive.
```graphql
input ValidaArgModel {
strs: [String!]! @valida(jsonSpec: """
{"variantType":"list","each":{"variantType":"string","minLength":1}}
""")
inner: ValidaArgModel
}
```
In this case the JSON '{"variantType":"list","each":{"variantType":"string","minLength":1}}' is the result of executing the annotation's (`ValidaList(each: ValidaString(minLength: 1))`) toJson method.
# Attachments
This API is experimental.
All GraphQL elements in the schema can have addition custom attachments. This can be used by other libraries or extensions to change the behavior of execution. For example, for supporting custom input validations or configuring the max age for some fields in an extension that caches responses.
## AttachmentWithValidation
An attachment can register validation logic by implementing `AttachmentWithValidation`. The required validation method `validateElement` will be executed when the GraphQLSchema is validated, as an argument it will receive the Schema validation context and the `GraphQLElement` associated with the attachment.
## ToDirectiveValue
Implementing this interface allows the GraphQLSchema's SDL String to contain the attachment's information by adding directives over the specific element associated with the attachment. Attachments that implement `ToDirectiveValue` require the following getters:
```dart
/// The directive value represented by this object
DirectiveNode get directiveValue;
/// The directive definition of the [directiveValue]
GraphQLDirective get directiveDefinition;
```
---
We provide two attachments, both of which implement `AttachmentWithValidation` and `ToDirectiveValue`.
### `KeyAttachment`
Implements the [key directive](#keydirective) over a given object. The `fields` String is required.
### `ValidaAttachment`
Implements the [valida directive](#validadirective) over a given input field or argument. The `annotation` argument should be the `ValidaField` specified for the element. You probably should use it manually, when using code generation the validation will be performed for any `@Valida()` annotated class or resolver and the attachment will be placed at the appropriate location.
## Usage
To use associate a GraphQLElement (type, field or directive) with attachments, you may pass them as arguments to each of the GraphQLElements constructor.
### AttachFn for code generation
To add attachments to types or fields when using code generation you can use the `AttachFn` decorator. An example of this is the following, using the [`KeyAttachment`](#keyattachment) for [`KeyDirective`](#keydirective) and the [`ValidaAttachment`](#validaattachment) for [`ValidaDirective`](#validadirective) (which is set up in the generated code).
```dart
@Valida()
@AttachFn(KeyedAttachment.attachments)
@GraphQLObject()
class KeyedAttachment {
final String id;
final String name;
@ValidaDate(max: 'now')
final DateTime createdAt;
final NestedAttachment nested;
KeyedAttachment({
required this.id,
required this.name,
required this.createdAt,
required this.nested,
});
static List attachments() {
return const [
KeyAttachment('id'),
KeyAttachment('name nested {id}'),
];
}
}
@AttachFn(NestedAttachment.attachments)
@GraphQLObject()
class NestedAttachment {
final int id;
NestedAttachment({
required this.id,
});
static List attachments() {
return const [
KeyAttachment('id'),
];
}
}
```
Will generate the following GraphQL schema:
```graphql
type KeyedAttachment @key(fields: "id") @key(fields: "name nested {id}") {
id: ID!
name: String!
createdAt: Date! @valida(jsonSpec: """
{"variantType":"date","max":"now"}
""")
nested: NestedAttachment!
}''',
'''
type NestedAttachment @key(fields: "id") {
id: Int!
```
# Utilities
Most GraphQL utilities can be found in the [`utilities`](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/utilities) folder in package:leto_schema.
### [`buildSchema`](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/utilities/build_schema.dart)
Create a `GraphQLSchema` from a GraphQL Schema Definition (SDL) document String.
### [`printSchema`](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/utilities/print_schema.dart)
Transform a `GraphQLSchema` into a String in the GraphQL Schema Definition Language (SDL).
### [`extendSchema`](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/utilities/extend_schema.dart)
Experimental. Extend a `GraphQLSchema` with an SDL document. This will return an extended `GraphQLSchema` with the additional types, fields, inputs and directives provided in the document.
### [`introspectionQuery`](https://github.com/juancastillo0/leto/tree/main/leto_schema/lib/src/utilities/introspection_query.dart)
Create an introspection document query for retrieving Schema information from a GraphQL server.
### [`mergeSchemas`](https://github.com/juancastillo0/leto/blob/main/leto_shelf/example/lib/schema/graphql_utils.dart)
Experimental. Merge multiple `GraphQLSchema`. The output `GraphQLSchema` contains all the query, mutations and subscription fields from the input schemas. Nested objects are also merged.
### [`schemaFromJson`](https://github.com/juancastillo0/leto/blob/main/leto_shelf/example/lib/schema/schema_from_json.dart)
Experimental. Build a GraphQLSchema from a JSON value, will add query, mutation, subscription and custom events on top of the provided JSON value. Will try to infer the types from the JSON structure.
# Contributing
Thanks for considering making a contribution! Every issue or question helps!
This package uses [melos](https://github.com/invertase/melos) to manage dependencies. To install it run:
```bash
pub global activate melos
```
Then, to link the local packages run:
```bash
melos bootstrap
```
If using fvm, you may need to run:
```bash
fvm flutter pub global run melos bootstrap
```
You can view most of the commands that you will need in the [`melos.yaml` file](./melos.yaml). However, melos can be used to do other stuff like executing any command you want using `melos exec`, for more information please view the [melos Github repo](https://github.com/invertase/melos).
## Scripts
The following scripts are used in CI and can be used thought development. At the moment, both of them are related to generating documentation.
### [`collect_examples.dart`](./scripts/collect_examples.dart)
This script allows you to include documentation in the README files (or any Markdown file) from comments in Dart code. In the future, we will probably support sharing documentation between Dart files.
For example, by placing this comment annotations in a section of a Dart file:
```dart
// @example-start{name-of-example,extension:graphql,start:1,end:-2}
const graphQLTypeSection = '''
type ObjectName {
objectField: String
}
''';
// @example-end{name-of-example}
```
And adding this comment in the Markdown (`.md`) file:
```markdown
```
You can copy the same Dart snippet by executing `dart run script/collect_examples.dart`. The Markdown file will be updated to:
~~~markdown
```graphql
type ObjectName {
objectField: String
}
```
~~~
You can find many examples in the [source code of this README](https://raw.githubusercontent.com/juancastillo0/leto/main/README.md) with Dart examples annotated through out the repository's Dart files.
The scripts has a couple of arguments, in particular `--check` is used in CI to verify the the code snippets are synchronized between the Dart files and Markdown files. `--generate-dart-file` will generate Dart code with the snippet Strings and `--generate-md-dir` will generate one `.md` file for each example within the directory passed as argument.
### [`generate_docusaurus.dart`](./scripts/generate_docusaurus.dart)
Generates the Docusaurus documentation page. Basically, it copies the README file sections into the `/docusaurus/docs/` directory in a format that Docusaurus understands. This is used in CI to build the [documentation page](https://juancastillo0.github.io/leto).
Small pieces of configuration can be included within the README files. For example, to assign tags to a section (which will be converted to a page, perhaps `/docs/leto_shelf/section-title` if it is in `leto_shelf`'s README) you can use.
```markdown
# Section Title
```
The content with the brackets should be a JSON string.