Skip to main content

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 GraphQLObjectTypes 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 GraphQLTypes. 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};
}