Annotations (Decorators)
All annotations with documentation and the supported configuration parameters can be found in package:leto_schema
's decorators file.
Outputs
Annotations for GraphQL Output Types.
GraphQLObject
Generate GraphQLObjectType
s and Interfaces with this annotation. The constructor provides a couple of parameters to configure the generated fields.
GraphQLField
Configures the generation of a GraphQLObjectField
in a GraphQLObjectType
.
In this example, the omitFields
parameter is used to omit all fields by default from the generation. Also an usage of the interfaces
parameter is shown, a better approach for specifying interfaces is shown in the Interfaces section.
final customInterface = objectType<Object>(
'ClassConfig2Interface',
fields: [
graphQLString.nonNull().field('value'),
],
isInterface: true,
);
(omitFields: true, interfaces: ['customInterface'])
class ClassConfig2 {
()
final String value;
(nullable: true)
final String valueOverridden;
final String notFound;
(name: 'renamedValue2')
final String value2;
const ClassConfig2({
required this.value,
required this.valueOverridden,
required this.notFound,
required this.value2,
});
}
Fields annotated with @GraphQLField()
will appear in the type definition, but notFound
will not since omitFields: true
and notFound
is not annotated with @GraphQLField()
. The type implements the interface specified in the annotation.
type ClassConfig2 implements ClassConfig2Interface {
value: String!
valueOverridden: String
renamedValue2: String!
}
The following class uses the nullableFields
parameter to override the default nullability type inference. When true, all fields will be nullable by default.
(nullableFields: true, name: 'RenamedClassConfig')
class ClassConfig {
(deprecationReason: 'value deprecated')
()
final String value;
final String valueOverridden;
final String? valueNull;
(nullable: true)
final String value2;
ClassConfig({
required this.value2,
required this.value,
required this.valueOverridden,
this.valueNull,
});
}
The previous Dart code will generate a GraphQL Object Type with the following definition:
type RenamedClassConfig {
value: String! @deprecated(reason: "value deprecated")
valueOverridden: String
valueNull: String
value2: String
}
Interfaces
You may use abstract classes to specify that a given class annotated with @GraphQLObject()
should generate a GraphQLInterface. Implemented classes that generate GraphQLInterfaces will appear as an interface of a the generated Object or Interface.
The following annotated Dart classes show the behavior.
()
abstract class NestedInterface {
Decimal get dec;
}
()
abstract class NamedInterface {
String? get name;
}
()
class NestedInterfaceImpl implements NestedInterface {
final Decimal dec;
final String? name;
NestedInterfaceImpl(this.name, this.dec);
}
()
class NestedInterfaceImpl2 implements NestedInterfaceImpl {
final Decimal dec;
final String? name;
final String name2;
NestedInterfaceImpl2({
required this.dec,
required this.name,
required this.name2,
});
}
()
class NestedInterfaceImpl3 extends NestedInterfaceImpl
implements NamedInterface {
final String name3;
NestedInterfaceImpl3({
required Decimal dec,
required String? name,
required this.name3,
}) : super(name, dec);
}
Will generate the following GraphQL definitions:
interface NestedInterface {
dec: Decimal!
}
interface NamedInterface {
name: String
}
type NestedInterfaceImpl implements NestedInterface {
dec: Decimal!
name: String
}
type NestedInterfaceImpl2 implements NestedInterface {
dec: Decimal!
name: String
name2: String!
}
type NestedInterfaceImpl3 implements NamedInterface & NestedInterface {
name3: String!
dec: Decimal!
name: String
}
GraphQLUnion
Unions allow you to specify that a given value can be one of multiple possible objects. For code generation we use freezed-like unions where factory constructors specify the different properties for the different objects. Other annotations such as @GraphQLField()
, @GraphQLDocumentation()
and freezed's @Default
will also work as shown in the example.
()
class UnionA with _$UnionA {
const factory UnionA.a1({
// five with default
(5) int one,
}) = _UnionA1;
const factory UnionA.a2({
(fromJson: decimalFromJson, toJson: decimalToJson)
('custom deprecated msg')
Decimal? dec,
}) = _UnionA2;
const factory UnionA.a3({
(description: 'description for one') List<int>? one,
}) = UnionA3;
const factory UnionA.a4({
(name: 'oneRenamed') required List<int> one,
}) = _UnionA4;
factory UnionA.fromJson(Map<String, Object?> json) => _$UnionAFromJson(json);
}
union UnionA = UnionA1 | UnionA2 | UnionA3 | UnionA4
type UnionA1 {
"""five with default"""
one: Int!
}
type UnionA2 {
dec: Decimal @deprecated(reason: "custom deprecated msg")
}
type UnionA3 {
"""description for one"""
one: [Int!]
}
type UnionA4 {
oneRenamed: [Int!]!
}
If you don't use package:freezed
, your can still generate unions with the same Dart definition, but actually defining the constructors for each possible object in the union:
GraphQLAttachments unionNoFreezedAttachments() => const [ElementComplexity(50)];
(unionNoFreezedAttachments)
(
description: '''
Description from annotation.
Union generated from raw Dart classes''',
)
(name: 'UnionNoFreezedRenamed')
class UnionNoFreezed {
const factory UnionNoFreezed.a(String value) = UnionNoFreezedA.named;
const factory UnionNoFreezed.b(int value) = UnionNoFreezedB;
}
()
class UnionNoFreezedA implements UnionNoFreezed {
final String value;
const UnionNoFreezedA.named(this.value);
}
()
class UnionNoFreezedB implements UnionNoFreezed {
final int value;
const UnionNoFreezedB(this.value);
}
()
List<UnionNoFreezed> getUnionNoFrezzed() {
return const [UnionNoFreezed.a('value'), UnionNoFreezed.b(12)];
}
Which generates code for the following GraphQL definitions:
"""
Description from annotation.
Union generated from raw Dart classes
"""
union UnionNoFreezedRenamed @cost(complexity: 50) = UnionNoFreezedA | UnionNoFreezedB
type UnionNoFreezedA {
value: String!
}
type UnionNoFreezedB {
value: Int!
}
Inputs
Annotations for GraphQL Input Types.
GraphQLInput
Specifies that a given class should generate a GraphQLInputObject
. All classes annotated with @GraphQLInput()
should provide a fromJson
factory or static method as shown in the following examples. You can use packages such us json_serializable
to generate the serialization code.
GraphQLArg
This annotation allows you to specify a default value for Input types in the schema. The type with a default value should support de-serializing the provided default value or should be able to be serialized with a toJson
method. This will also work for arguments in resolvers as shown in the Resolver Inputs section.
(name: 'InputMNRenamed')
class InputMN {
final String name;
final InputM? parent;
final Json json;
final List<Json> jsonListArgDef;
final List<List<InputM>?>? parentNullDef;
static List<List<InputM>?> parentNullDefDefault() => [
null,
[
const InputM(
name: 'defaultName',
nested: [],
nestedNullItem: [],
ints: [0, 0],
doubles: [0, 0.1],
)
]
];
const InputMN({
required this.name,
this.parent,
this.json = const JsonList([JsonNumber(1)]),
(defaultCode: 'const [JsonMap({})]')
required this.jsonListArgDef,
(defaultFunc: parentNullDefDefault) this.parentNullDef,
});
factory InputMN.fromJson(Map<String, Object?> json) {
return InputMN(
name: json['name']! as String,
parent: json['parent'] != null
? InputM.fromJson(json['parent']! as Map<String, Object?>)
: null,
json: Json.fromJson(json['json']),
jsonListArgDef: List.of(
(json['jsonListArgDef'] as List).map(
(Object? e) => Json.fromJson(e),
),
),
parentNullDef: json['parentNullDef'] != null
? List.of(
(json['parentNullDef']! as List<Object?>).map(
(e) => e == null
? null
: List.of(
(e as List<Object?>).map(
(e) => InputM.fromJson(e as Map<String, Object?>),
),
),
),
)
: null,
);
}
Map<String, Object?> toJson() => {
'name': name,
'parent': parent,
'json': json,
'jsonListArgDef': jsonListArgDef,
if (parentNullDef != null) 'parentNullDef': parentNullDef,
};
}
Generic input types are supported. However the API may change in the future. Your fromJson
method should have generic argument factories as parameters, functions that return the generic instance from a serialized value. You can use the @JsonSerializable(genericArgumentFactories: true)
if using json_serializable
as shown in the example.
()
(genericArgumentFactories: true)
class InputGen<T> {
final String name;
final T generic;
const InputGen({
required this.name,
required this.generic,
});
factory InputGen.fromJson(
Map<String, Object?> json,
T Function(Object?) fromJsonT,
) =>
_$InputGenFromJson(json, fromJsonT);
Map<String, Object?> toJson() => {'name': name, 'generic': generic};
}
Resolver Inputs
// TODO: 1G @FromCtx()
Type.fromCtx;
Authentication (admin|role);
For resolvers, you just specify the type that you want as input and the input GraphQL type will be included in the generated field definition. You can use the @GraphQLArg()
annotation to specify a default value or specify the default value directly in the dart code if it can be a const
Dart definition.
List<Decimal?> _defaultListDecimalNull() => [null, Decimal.parse('2')];
GraphQLType<dynamic, dynamic> _timestampsType() =>
graphQLTimestamp.list().nonNull();
()
enum EnumValue { v1, v2, v3 }
final enumCustomGraphQLType = enumType<int>(
'EnumCustom',
{
'TWO': 2,
'THREE': 3,
},
);
const testManyDefaultsGraphQLStr =
'testManyDefaults(str: String! = "def", intInput: Int! = 2,'
' doubleInput: Float! = 3.0, doubleInputNull: Float = 4.2,'
' boolean: Boolean! = true, listStr: [String!]! = ["dw", "dd2"],'
' listDecimalNull: [Decimal] = [null, "2"],'
' listUri: [Uri!]! = ["http://localhost:8060/"],'
' date: Date! = "2021-03-24T00:00:00.000",'
' gen: InputGenIntReq = {name: "gen", generic: 2},'
' enumValue: EnumValue! = v1, enumCustom: EnumCustom = THREE,'
' enumCustomList: [EnumCustom!]! = [TWO],'
' timestamps: [Timestamp]! = [1611446400000, null],'
' json: Json! = {d: [2]}): String!';
()
String testManyDefaults({
String str = 'def',
int intInput = 2,
double doubleInput = 3,
double? doubleInputNull = 4.2,
bool boolean = true,
List<String> listStr = const ['dw', 'dd2'],
(defaultFunc: _defaultListDecimalNull)
List<Decimal?>? listDecimalNull,
(defaultCode: "[Uri.parse('http://localhost:8060/')]")
required List<Uri> listUri,
(defaultCode: 'DateTime.parse("2021-03-24")')
required DateTime date,
(defaultCode: "InputGen(name: 'gen', generic: 2)")
InputGen<int>? gen,
EnumValue enumValue = EnumValue.v1,
(typeName: 'enumCustomGraphQLType') int enumCustom = 3,
(
typeName: 'enumCustomGraphQLType.nonNull().list().nonNull()',
)
List<int> enumCustomList = const [2],
(
defaultCode: '[DateTime.fromMillisecondsSinceEpoch(1611446400000), null]',
)
(type: _timestampsType)
required List<DateTime?> timestamps,
Json json = const Json.map({
'd': Json.list([Json.number(2)])
}),
}) {
Other
Other miscellaneous decorators include the general GraphQLDocumentation
, GraphQLEnum
to generate a GraphQLEnumType
and AttachFn
to specify attachments.
GraphQLDocumentation
Dart comments for all elements will be taken as the description in the generated GraphQLType or Field. Also, Dart's @Deprecated()
annotation can be used for setting the deprecationReason
for fields, input fields, arguments and enum values. Another way, which will override the previous two, is by using the @GraphQLDocumentation()
with the description
and deprecationReason
params.
The GraphQLType of a field, input field, argument or class can be configured using the type
or typeName
params. More information in the GraphQLDocumentation type parameters section.
GraphQLEnum
Enums work as expected using the @GraphQLEnum()
annotation. The valuesCase
parameter can be used to specify the case of the generated GraphQL enum definition. Some example of simple enums:
/// comments for docs
(name: 'SimpleEnumRenamed')
enum SimpleEnum {
(simpleVariantAttachments)
simpleVariantOne,
SIMPLE_VARIANT_TWO,
}
GraphQLAttachments simpleVariantAttachments() => const [CustomAttachment()];
(valuesCase: EnumNameCase.snake_case)
enum SnakeCaseEnum {
(description: 'description from annotation')
('custom deprecated')
variantOne,
/// Documentation for variant two
variantTwo,
}
The SimpleEnum
Dart enum will generate the following GraphQL definition:
enum ClassEnum @cost(complexity: 2) {
VARIANT_ONE
"""The second variant docs"""
VARIANT_TWO
errorRenamed
}
And the SnakeCaseEnum
Dart enum will generate the following GraphQL definition:
enum SnakeCaseEnum {
"""description from annotation"""
variant_one @deprecated(reason: "custom deprecated")
"""Documentation for variant two"""
variant_two
}
You can also provide a custom enum class, you will need to annotate each static variant with the @GraphQLEnumVariant()
decorator and have a Class.values
static getter. All variants should be of the same type as the Enum class. An example of this is shown in the following code snippet.
GraphQLAttachments classEnumAttachments() => const [ElementComplexity(2)];
(classEnumAttachments)
(valuesCase: EnumNameCase.CONSTANT_CASE)
class ClassEnum {
final int code;
final bool isError;
const ClassEnum._(this.code, this.isError);
()
static const variantOne = ClassEnum._(100, false);
/// The second variant docs
()
static const variantTwo = ClassEnum._(201, false);
(name: 'errorRenamed')
(variantErrorAttachments)
static const variantErrorThree = ClassEnum._(300, true);
static GraphQLAttachments variantErrorAttachments() =>
const [CustomAttachment()];
static const List<ClassEnum> values = [
variantOne,
variantTwo,
variantErrorThree,
];
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ClassEnum && other.code == code && other.isError == isError;
}
int get hashCode => code.hashCode ^ isError.hashCode;
}
AttachFn
You can use the AttachFn
decorator over a class, field or function or argument to specify Attachments for a GraphQLElement. You pass a function that returns a list of attachments. More documentation on the usage can be found in usage for code generation.
Generics
You can use generics with code generation. For example a simple wrapper around an error like this:
()
class ErrCodeInterface<T extends Object> {
final String? message;
final T code;
const ErrCodeInterface(this.code, [this.message]);
}
Will generate something a function that returns a GraphQLType
with the generic as a GraphQLType
passed as argument:
final _types =
HotReloadableDefinition<Map<String, GraphQLObjectType<ErrCodeInterface>>>(
(_) => {});
/// Auto-generated from [ErrCodeInterface].
GraphQLObjectType<ErrCodeInterface<T>>
errCodeInterfaceGraphQLType<T extends Object>(
GraphQLType<T, Object> tGraphQLType, {
String? name,
}) {
final __name = name ?? 'ErrCodeInterface${tGraphQLType.printableName}';
if (_types.value[__name] != null) {
return _types.value[__name]! as GraphQLObjectType<ErrCodeInterface<T>>;
}
final __types = objectType<ErrCodeInterface<T>>(
__name,
isInterface: false,
interfaces: [],
);
_types.value[__name] = __types;
__types.fields.addAll(
[
graphQLString.field('message', resolve: (obj, ctx) => obj.message),
tGraphQLType.nonNull().field('code', resolve: (obj, ctx) => obj.code),
],
);
return __types;
}
It also has an optional name property to override the default generic name that is constructed
from the base name and the generic GraphQLType
s. This name can be used with the genericTypeName
.
//TODO: 2G genericTypeName should be usable in other situations. Maybe fields or in GraphQLDocumentation
for all
Generics Input
You can also use generics for inputs like the following example. In this case, the fromJson
factory should receive a
function that parses the generic values (as many function as there are generic type parameters).
()
(genericArgumentFactories: true)
class InputGen<T> {
final String name;
final T generic;
const InputGen({
required this.name,
required this.generic,
});
factory InputGen.fromJson(
Map<String, Object?> json,
T Function(Object?) fromJsonT,
) =>
_$InputGenFromJson(json, fromJsonT);
Map<String, Object?> toJson() => {'name': name, 'generic': generic};
}