Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Typed options for `package:graphql`: every operation file has
`<operation>QueryOptions` / `<operation>WatchQueryOptions` (queries),
`<operation>MutationOptions` and `<operation>SubscriptionOptions`. They take
typed variables and the full set of options (fetch and error policies,
polling, optimistic results, mutation `update`, ...), and use the generated
parser as `parserFn`, so `parsedData` and `ObservableQuery` results are typed
- `read<Operation>` / `write<Operation>` cache helpers on the client extension
of queries
- `scalar_mapping` entries can be `{type, import, converter}`: the import is
added to the generated files, and the converter is used for fields, list
elements, variables and results. Mapped types without a converter are read
with `fromJson` and written with `toJson`
- `strict_scalars`: fail with `SCH401` for custom scalars without a mapping
- `dart run flutter_graphql_codegen:fetch_schema` downloads the schema from
`schema_endpoint` (new config key) into `schema_url`, so the schema can live
in the repository and builds don't need the network

### Fixed
- Variables of a scalar mapped to `DateTime` (`scalar_mapping: {Date:
DateTime}`) were put into the request as `DateTime` objects, which can't be
sent as JSON. They are now serialized like `DateTime` scalars

## [2.2.0] - 2026-10-17

### Added
Expand Down
105 changes: 94 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,29 @@ schema_headers:
Authorization: "Bearer ${GRAPHQL_TOKEN}"
```

#### Keeping the schema in the repository

Downloading during every build makes builds depend on the network and on the
server's current schema. The alternative: keep the schema as a file in the
project and refresh it with a command when the server changes.

```yaml
schema_url: lib/graphql/schema.graphql # the build reads this file
schema_endpoint: https://api.example.com/graphql # fetch_schema downloads from here
schema_headers:
Authorization: "Bearer ${GRAPHQL_TOKEN}"
```

```bash
dart run flutter_graphql_codegen:fetch_schema
```

It downloads the schema the same way the builder does (SDL, `?sdl`,
introspection) with `schema_headers`, and writes it to `schema_url`. Options:
`--config <file>`, `--endpoint <url>`, `--output <file>` and
`--header "Name: value"` (repeatable). It exits with 1 when the download fails
and 64 on wrong usage, so it works in scripts and CI.

### All options

| Option | Default | Description |
Expand All @@ -103,6 +126,8 @@ schema_headers:
| `unknown_enum_fallback` | `false` | Add a `$unknown` value to every enum instead of throwing on values the server added later (see below) |
| `deprecated_annotations` | `false` | Turn schema `@deprecated` into Dart `@Deprecated` (see below) |
| `validate_documents` | `true` | Check operations against the schema before generating (see [Errors](#errors)) |
| `strict_scalars` | `false` | Fail when a custom scalar has no `scalar_mapping` entry (see [Custom scalars](#custom-scalars)) |
| `schema_endpoint` | none | Where `fetch_schema` downloads the schema from (see above) |

A misspelled key is ignored with a warning that suggests the closest option.

Expand Down Expand Up @@ -169,9 +194,15 @@ class GetUserVariables { ... Map<String, dynamic> toJson() }

User? parseGetUserData(Map<String, dynamic>? data); // response -> model

// package:graphql options with typed variables and parsedData
QueryOptions<User?> getUserQueryOptions(GetUserVariables variables, {fetchPolicy, ...});
WatchQueryOptions<User?> getUserWatchQueryOptions(GetUserVariables variables, {...});

extension GetUserExtension on graphql.GraphQLClient {
Future<QueryResult<Map<String, dynamic>>> getUser([Map<String, dynamic>? variables]);
Future<User?> getUserData([Map<String, dynamic>? variables]);
User? readGetUser(GetUserVariables variables); // from the cache
void writeGetUser(GetUserVariables variables, Map<String, dynamic> data);
}
```

Expand All @@ -192,19 +223,39 @@ print(dashboard?.me?.id);
print(dashboard?.notifications?.length);
```

The extension methods only cover the common case. For `watchQuery`, a specific
`fetchPolicy`, or anything else `package:graphql` offers, use the document and
the parser directly:
### Options for `query`, `watchQuery`, `mutate` and `subscribe`

The extension methods cover the common case. For `watchQuery`, a fetch or
error policy, polling, optimistic results or a mutation `update` callback, use
the generated options functions. They take typed variables, and their
`parserFn` is the generated parser, so `parsedData` is typed:

```dart
final query = client.watchQuery(WatchQueryOptions(
document: getUserDocument,
variables: GetUserVariables(id: '42').toJson(),
fetchPolicy: FetchPolicy.cacheAndNetwork,
final result = await client.query(getUserQueryOptions(
const GetUserVariables(id: '42'),
fetchPolicy: FetchPolicy.networkOnly,
));
final users = query.stream.map((result) => parseGetUserData(result.data));
final User? user = result.parsedData;

final ObservableQuery<User?> query = client.watchQuery(
getUserWatchQueryOptions(const GetUserVariables(id: '42'),
fetchPolicy: FetchPolicy.cacheAndNetwork),
);
query.stream.listen((result) => print(result.parsedData?.name));
```

There are `<operation>QueryOptions` and `<operation>WatchQueryOptions` for
queries, `<operation>MutationOptions` for mutations and
`<operation>SubscriptionOptions` for subscriptions. Operations without
variables take no variables argument; operations whose variables are all
optional take `variables:` as a named argument. Unlike the extension methods,
`client.query(...)` doesn't throw on errors: check `result.hasException`.

For queries, `read<Operation>` returns the cached result, typed, or null when
it isn't cached, and `write<Operation>` puts data into the cache. Write data in
the shape the server returns, with `__typename` on every object including the
root.

### Variables

`GetUserVariables` turns typed values into the map that `package:graphql` expects.
Expand Down Expand Up @@ -270,9 +321,40 @@ becomes `class Map_`, `scalar Object` becomes `Object_`. `__typename` and
| `Byte`, `Short` | `Byte` wrapper / `int` |
| anything else | a generated wrapper class with `raw` (the JSON value as received) and `value` (its string form) |

To use your own type instead, add it to `scalar_mapping`. The generator doesn't
add imports, so stick to types that are always in scope (`String`, `int`,
`DateTime`, `Map<String, dynamic>`, ...).
To use your own type instead, add it to `scalar_mapping`. The short form works
for types that are always in scope (`String`, `int`, `DateTime`,
`Map<String, dynamic>`, ...):

```yaml
scalar_mapping:
Date: DateTime
JSON: Map<String, dynamic>
```

For a type from another library, use the long form with `import`. The import
is added to `types.dart` and to the operation files:

```yaml
scalar_mapping:
Decimal:
type: Decimal
import: package:decimal/decimal.dart
Money:
type: Money
import: package:my_app/money.dart
converter: MoneyConverter
```

Without `converter`, the type needs `fromJson` and `toJson`, like `Decimal` from
`package:decimal`. With `converter`, give the name of a `JsonConverter` class
with a `const` constructor, exported by the same `import`. It's used for fields,
list elements, variables and results. Give it a non-nullable JSON type
(`JsonConverter<Money, int>`, not `<Money, Object?>`), so null values are
handled for you instead of being passed to `fromJson`.

With `strict_scalars: true`, a custom scalar that isn't mapped (and isn't one of
the built-in ones above) fails the build with `SCH401`, instead of silently
becoming a wrapper class.

Non-nullable `int`, `double` and `bool` fields have lenient converters: a missing
or malformed value becomes `0`, `0.0` or `false` instead of throwing.
Expand All @@ -299,6 +381,7 @@ Problems found during generation are reported with a code:
| `CFG001` / `CFG002` | `schema_url` / `document_paths` missing from the config |
| `CFG999` | Other config problem (bad value, unset env variable in `schema_headers`) |
| `SCH400` | The schema doesn't parse |
| `SCH401` | A custom scalar has no `scalar_mapping` entry and `strict_scalars` is on |
| `SCH404` | Schema file not found |
| `SCH500` | Remote schema download failed; the message lists every attempt |
| `BLD100` | An operation file doesn't parse |
Expand Down
8 changes: 8 additions & 0 deletions bin/fetch_schema.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'dart:io';

import 'package:flutter_graphql_codegen/src/fetch_schema_command.dart';

/// `dart run flutter_graphql_codegen:fetch_schema --help`
Future<void> main(List<String> args) async {
exitCode = await runFetchSchema(args);
}
3 changes: 3 additions & 0 deletions lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,9 @@ Builder graphqlCodegenBuilder(BuilderOptions options) {
CodegenConfigContext.unknownEnumFallback = cfg.unknownEnumFallback;
CodegenConfigContext.deprecatedAnnotations = cfg.deprecatedAnnotations;
CodegenConfigContext.validateDocuments = cfg.validateDocuments;
CodegenConfigContext.scalarImports = Map.of(cfg.scalarImports);
CodegenConfigContext.scalarConverters = Map.of(cfg.scalarConverters);
CodegenConfigContext.strictScalars = cfg.strictScalars;

return GraphQLCodegenBuilder(cfg);
}
Loading
Loading