+50
-37
README.md
+50
-37
README.md
···
4
4
5
5
Squall parses `.gql` files in your project, introspects your GraphQL endpoint, and generates fully type-safe Gleam code with decoders, eliminating runtime errors and keeping your GraphQL queries in sync with your schema.
6
6
7
+
> **⚠️ Warning**: This project is in early development and may contain bugs. Use at your own risk.
8
+
7
9
## Features
8
10
9
-
- ✅ **Type-Safe**: Generated code is fully typed based on your GraphQL schema
10
-
- ✅ **Convention-based**: Drop `.gql` files in `src/**/graphql/` directories
11
-
- ✅ **Zero Configuration**: No config files needed
12
-
- ✅ **Schema Introspection**: Automatically fetches types from your GraphQL endpoint
13
-
- ✅ **Full GraphQL Support**: Queries, Mutations, and Subscriptions
14
-
- ✅ **Test-Driven**: Built with TDD, 38+ passing tests
15
-
- ✅ **Gleam-First**: Generates idiomatic Gleam code
11
+
- Type-safe code generation from GraphQL schema
12
+
- Convention over configuration - `.gql` files in `src/**/graphql/` directories
13
+
- Schema introspection from GraphQL endpoints
14
+
- Supports queries, mutations, and subscriptions
15
+
- Client abstraction with authentication and custom headers
16
+
- Multiple endpoint support
16
17
17
18
## Installation
18
19
···
20
21
21
22
```toml
22
23
[dependencies]
23
-
squall = "1.0.0"
24
+
squall = "0.1.0"
24
25
```
25
26
26
27
## Quick Start
···
49
50
### 3. Use the generated code
50
51
51
52
```gleam
53
+
import squall
52
54
import my_app/graphql/get_character
53
55
54
56
pub fn main() {
55
-
let endpoint = "https://rickandmortyapi.com/graphql"
57
+
// Create a client
58
+
let client = squall.new_client(
59
+
endpoint: "https://rickandmortyapi.com/graphql",
60
+
headers: []
61
+
)
56
62
57
-
case get_character.get_character(endpoint, "1") {
63
+
// Use the generated function
64
+
case get_character.get_character(client: client, id: "1") {
58
65
Ok(response) -> {
59
-
io.println("Character: " <> response.name)
66
+
io.println("Character: " <> response.character.name)
60
67
}
61
68
Error(err) -> {
62
69
io.println("Error: " <> err)
···
65
72
}
66
73
```
67
74
75
+
## Creating a Client
76
+
77
+
Squall uses a client abstraction to manage your GraphQL endpoint and headers (including authentication):
78
+
79
+
```gleam
80
+
import squall
81
+
82
+
// Create a client with custom headers
83
+
let client = squall.new_client(
84
+
endpoint: "https://api.example.com/graphql",
85
+
headers: [
86
+
#("Authorization", "Bearer your-api-token-here"),
87
+
#("X-Custom-Header", "value")
88
+
]
89
+
)
90
+
91
+
// Or use the convenience function for bearer token auth
92
+
let client = squall.new_client_with_auth(
93
+
endpoint: "https://api.example.com/graphql",
94
+
token: "your-api-token-here"
95
+
)
96
+
97
+
// For public APIs with no authentication
98
+
let client = squall.new_client(
99
+
endpoint: "https://rickandmortyapi.com/graphql",
100
+
headers: []
101
+
)
102
+
```
103
+
68
104
## How It Works
69
105
70
106
Squall follows a simple workflow:
···
116
152
}
117
153
118
154
pub fn get_user(
119
-
endpoint: String,
155
+
client: squall.Client,
120
156
id: String,
121
157
) -> Result(GetUserResponse, String) {
122
158
// HTTP request + JSON decoding implementation
···
161
197
- **`codegen`**: Generates Gleam code
162
198
- **`error`**: Comprehensive error handling
163
199
164
-
## Comparison with Squirrel
165
-
166
-
Squall is inspired by [squirrel](https://github.com/giacomocavalieri/squirrel) but for GraphQL instead of SQL:
167
-
168
-
| Feature | Squirrel (SQL) | Squall (GraphQL) |
169
-
|---------|----------------|------------------|
170
-
| Query Language | SQL | GraphQL |
171
-
| Type Source | PostgreSQL | GraphQL Schema |
172
-
| File Pattern | `sql/*.sql` | `graphql/*.gql` |
173
-
| Type Safety | ✅ | ✅ |
174
-
| Zero Config | ✅ | ✅ |
175
-
| Convention-based | ✅ | ✅ |
176
-
177
200
## Development
178
201
179
202
### Running Tests
···
212
235
- [x] Subscription support
213
236
- [x] Type-safe code generation
214
237
- [x] Schema introspection
215
-
- [ ] HTTP client implementation
238
+
- [x] Client abstraction with headers/authentication
239
+
- [x] Multiple endpoint support
216
240
- [ ] Fragment support
217
241
- [ ] Directive handling
218
242
- [ ] Custom scalar mapping
219
-
- [ ] Multiple endpoint support
220
243
221
244
## Contributing
222
245
···
230
253
## License
231
254
232
255
Apache-2.0
233
-
234
-
## Acknowledgments
235
-
236
-
- Inspired by [squirrel](https://github.com/giacomocavalieri/squirrel) by Giacomo Cavalieri
237
-
- Built with [Gleam](https://gleam.run/)
238
-
- Tested with the [Rick and Morty API](https://rickandmortyapi.com/graphql)
239
-
240
-
---
241
-
242
-
Made with 🌊 and Gleam
+8
-4
birdie_snapshots/mutation_function_generation.accepted
+8
-4
birdie_snapshots/mutation_function_generation.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Mutation function generation
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_mutation_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type User {
16
16
User(id: String, name: String)
···
41
41
}
42
42
}
43
43
44
-
pub fn create_user(endpoint: String, name: String) -> Result(CreateUserResponse, String) {
44
+
pub fn create_user(client: squall.Client, name: String) -> Result(CreateUserResponse, String) {
45
45
let query =
46
46
"mutation CreateUser($name: String!) { createUser(name: $name) { id name } }"
47
47
let variables =
···
49
49
let body =
50
50
json.object([#("query", json.string(query)), #("variables", variables)])
51
51
use req <- result.try(
52
-
request.to(endpoint)
52
+
request.to(client.endpoint)
53
53
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
54
54
)
55
55
let req =
···
57
57
|> request.set_method(http.Post)
58
58
|> request.set_body(json.to_string(body))
59
59
|> request.set_header("content-type", "application/json")
60
+
let req =
61
+
list.fold(client.headers, req, fn(r, header) {
62
+
request.set_header(r, header.0, header.1)
63
+
})
60
64
use resp <- result.try(
61
65
httpc.send(req)
62
66
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/query_with_inline_array_arguments.accepted
+8
-4
birdie_snapshots/query_with_inline_array_arguments.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Query with inline array arguments
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_inline_array_arguments_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type Episode {
16
16
Episode(id: String, name: String)
···
41
41
}
42
42
}
43
43
44
-
pub fn get_episodes(endpoint: String) -> Result(GetEpisodesResponse, String) {
44
+
pub fn get_episodes(client: squall.Client) -> Result(GetEpisodesResponse, String) {
45
45
let query =
46
46
"query GetEpisodes { episodesByIds(ids: [1, 2, 3]) { id name } }"
47
47
let variables =
···
49
49
let body =
50
50
json.object([#("query", json.string(query)), #("variables", variables)])
51
51
use req <- result.try(
52
-
request.to(endpoint)
52
+
request.to(client.endpoint)
53
53
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
54
54
)
55
55
let req =
···
57
57
|> request.set_method(http.Post)
58
58
|> request.set_body(json.to_string(body))
59
59
|> request.set_header("content-type", "application/json")
60
+
let req =
61
+
list.fold(client.headers, req, fn(r, header) {
62
+
request.set_header(r, header.0, header.1)
63
+
})
60
64
use resp <- result.try(
61
65
httpc.send(req)
62
66
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/query_with_inline_object_arguments.accepted
+8
-4
birdie_snapshots/query_with_inline_object_arguments.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Query with inline object arguments
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_inline_object_arguments_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type CharactersResult {
16
16
CharactersResult(results: Option(List(Character)))
···
58
58
}
59
59
}
60
60
61
-
pub fn get_characters(endpoint: String) -> Result(GetCharactersResponse, String) {
61
+
pub fn get_characters(client: squall.Client) -> Result(GetCharactersResponse, String) {
62
62
let query =
63
63
"query GetCharacters { characters(filter: { name: \"rick\", status: \"alive\" }) { results { id name } } }"
64
64
let variables =
···
66
66
let body =
67
67
json.object([#("query", json.string(query)), #("variables", variables)])
68
68
use req <- result.try(
69
-
request.to(endpoint)
69
+
request.to(client.endpoint)
70
70
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
71
71
)
72
72
let req =
···
74
74
|> request.set_method(http.Post)
75
75
|> request.set_body(json.to_string(body))
76
76
|> request.set_header("content-type", "application/json")
77
+
let req =
78
+
list.fold(client.headers, req, fn(r, header) {
79
+
request.set_header(r, header.0, header.1)
80
+
})
77
81
use resp <- result.try(
78
82
httpc.send(req)
79
83
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/query_with_inline_scalar_arguments.accepted
+8
-4
birdie_snapshots/query_with_inline_scalar_arguments.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Query with inline scalar arguments
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_inline_scalar_arguments_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type Character {
16
16
Character(id: String, name: String)
···
41
41
}
42
42
}
43
43
44
-
pub fn get_character(endpoint: String) -> Result(GetCharacterResponse, String) {
44
+
pub fn get_character(client: squall.Client) -> Result(GetCharacterResponse, String) {
45
45
let query =
46
46
"query GetCharacter { character(id: 1) { id name } }"
47
47
let variables =
···
49
49
let body =
50
50
json.object([#("query", json.string(query)), #("variables", variables)])
51
51
use req <- result.try(
52
-
request.to(endpoint)
52
+
request.to(client.endpoint)
53
53
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
54
54
)
55
55
let req =
···
57
57
|> request.set_method(http.Post)
58
58
|> request.set_body(json.to_string(body))
59
59
|> request.set_header("content-type", "application/json")
60
+
let req =
61
+
list.fold(client.headers, req, fn(r, header) {
62
+
request.set_header(r, header.0, header.1)
63
+
})
60
64
use resp <- result.try(
61
65
httpc.send(req)
62
66
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted
+8
-4
birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Query with multiple root fields and mixed arguments
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_multiple_root_fields_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type CharactersResult {
16
16
CharactersResult(info: Option(Info), results: Option(List(Character)))
···
116
116
}
117
117
}
118
118
119
-
pub fn multi_query(endpoint: String) -> Result(MultiQueryResponse, String) {
119
+
pub fn multi_query(client: squall.Client) -> Result(MultiQueryResponse, String) {
120
120
let query =
121
121
"query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }"
122
122
let variables =
···
124
124
let body =
125
125
json.object([#("query", json.string(query)), #("variables", variables)])
126
126
use req <- result.try(
127
-
request.to(endpoint)
127
+
request.to(client.endpoint)
128
128
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
129
129
)
130
130
let req =
···
132
132
|> request.set_method(http.Post)
133
133
|> request.set_body(json.to_string(body))
134
134
|> request.set_header("content-type", "application/json")
135
+
let req =
136
+
list.fold(client.headers, req, fn(r, header) {
137
+
request.set_header(r, header.0, header.1)
138
+
})
135
139
use resp <- result.try(
136
140
httpc.send(req)
137
141
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/query_with_nested_types_generation.accepted
+8
-4
birdie_snapshots/query_with_nested_types_generation.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Query with nested types generation
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_query_with_nested_types_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type Character {
16
16
Character(id: String, name: String, status: String)
···
42
42
}
43
43
}
44
44
45
-
pub fn get_character(endpoint: String, id: String) -> Result(GetCharacterResponse, String) {
45
+
pub fn get_character(client: squall.Client, id: String) -> Result(GetCharacterResponse, String) {
46
46
let query =
47
47
"query GetCharacter($id: ID!) { character(id: $id) { id name status } }"
48
48
let variables =
···
50
50
let body =
51
51
json.object([#("query", json.string(query)), #("variables", variables)])
52
52
use req <- result.try(
53
-
request.to(endpoint)
53
+
request.to(client.endpoint)
54
54
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
55
55
)
56
56
let req =
···
58
58
|> request.set_method(http.Post)
59
59
|> request.set_body(json.to_string(body))
60
60
|> request.set_header("content-type", "application/json")
61
+
let req =
62
+
list.fold(client.headers, req, fn(r, header) {
63
+
request.set_header(r, header.0, header.1)
64
+
})
61
65
use resp <- result.try(
62
66
httpc.send(req)
63
67
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/query_with_variables_function_generation.accepted
+8
-4
birdie_snapshots/query_with_variables_function_generation.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Query with variables function generation
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_query_with_variables_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type User {
16
16
User(id: String, name: Option(String))
···
44
44
}
45
45
}
46
46
47
-
pub fn get_user(endpoint: String, id: String) -> Result(GetUserResponse, String) {
47
+
pub fn get_user(client: squall.Client, id: String) -> Result(GetUserResponse, String) {
48
48
let query =
49
49
"query GetUser($id: ID!) { user(id: $id) { id name } }"
50
50
let variables =
···
52
52
let body =
53
53
json.object([#("query", json.string(query)), #("variables", variables)])
54
54
use req <- result.try(
55
-
request.to(endpoint)
55
+
request.to(client.endpoint)
56
56
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
57
57
)
58
58
let req =
···
60
60
|> request.set_method(http.Post)
61
61
|> request.set_body(json.to_string(body))
62
62
|> request.set_header("content-type", "application/json")
63
+
let req =
64
+
list.fold(client.headers, req, fn(r, header) {
65
+
request.set_header(r, header.0, header.1)
66
+
})
63
67
use resp <- result.try(
64
68
httpc.send(req)
65
69
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/simple_query_function_generation.accepted
+8
-4
birdie_snapshots/simple_query_function_generation.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Simple query function generation
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_simple_query_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type User {
16
16
User(id: String, name: Option(String))
···
44
44
}
45
45
}
46
46
47
-
pub fn get_user(endpoint: String) -> Result(GetUserResponse, String) {
47
+
pub fn get_user(client: squall.Client) -> Result(GetUserResponse, String) {
48
48
let query =
49
49
"query GetUser { user { id name } }"
50
50
let variables =
···
52
52
let body =
53
53
json.object([#("query", json.string(query)), #("variables", variables)])
54
54
use req <- result.try(
55
-
request.to(endpoint)
55
+
request.to(client.endpoint)
56
56
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
57
57
)
58
58
let req =
···
60
60
|> request.set_method(http.Post)
61
61
|> request.set_body(json.to_string(body))
62
62
|> request.set_header("content-type", "application/json")
63
+
let req =
64
+
list.fold(client.headers, req, fn(r, header) {
65
+
request.set_header(r, header.0, header.1)
66
+
})
63
67
use resp <- result.try(
64
68
httpc.send(req)
65
69
|> result.map_error(fn(_) { "HTTP request failed" }),
+8
-4
birdie_snapshots/type_with_reserved_keywords.accepted
+8
-4
birdie_snapshots/type_with_reserved_keywords.accepted
···
1
1
---
2
2
version: 1.2.0
3
3
title: Type with reserved keywords
4
-
file: ./test/codegen_test.gleam
5
-
test_name: generate_with_reserved_keywords_test
6
4
---
7
5
import gleam/dynamic
8
6
import gleam/http
9
7
import gleam/http/request
10
8
import gleam/httpc
11
9
import gleam/json
10
+
import gleam/list
12
11
import gleam/option.{type Option}
13
12
import gleam/result
13
+
import squall
14
14
15
15
pub type Item {
16
16
Item(
···
57
57
}
58
58
}
59
59
60
-
pub fn get_item(endpoint: String) -> Result(GetItemResponse, String) {
60
+
pub fn get_item(client: squall.Client) -> Result(GetItemResponse, String) {
61
61
let query =
62
62
"query GetItem { item { id type case let } }"
63
63
let variables =
···
65
65
let body =
66
66
json.object([#("query", json.string(query)), #("variables", variables)])
67
67
use req <- result.try(
68
-
request.to(endpoint)
68
+
request.to(client.endpoint)
69
69
|> result.map_error(fn(_) { "Invalid endpoint URL" }),
70
70
)
71
71
let req =
···
73
73
|> request.set_method(http.Post)
74
74
|> request.set_body(json.to_string(body))
75
75
|> request.set_header("content-type", "application/json")
76
+
let req =
77
+
list.fold(client.headers, req, fn(r, header) {
78
+
request.set_header(r, header.0, header.1)
79
+
})
76
80
use resp <- result.try(
77
81
httpc.send(req)
78
82
|> result.map_error(fn(_) { "HTTP request failed" }),
+15
src/squall.gleam
+15
src/squall.gleam
···
14
14
import squall/internal/parser
15
15
import squall/internal/schema
16
16
17
+
/// A GraphQL client with endpoint and headers configuration
18
+
pub type Client {
19
+
Client(endpoint: String, headers: List(#(String, String)))
20
+
}
21
+
22
+
/// Create a new GraphQL client with custom headers
23
+
pub fn new_client(endpoint: String, headers: List(#(String, String))) -> Client {
24
+
Client(endpoint: endpoint, headers: headers)
25
+
}
26
+
27
+
/// Create a new GraphQL client with bearer token authentication
28
+
pub fn new_client_with_auth(endpoint: String, token: String) -> Client {
29
+
Client(endpoint: endpoint, headers: [#("Authorization", "Bearer " <> token)])
30
+
}
31
+
17
32
pub fn main() {
18
33
case argv.load().arguments {
19
34
["generate", endpoint] -> generate(endpoint)
+15
-3
src/squall/internal/codegen.gleam
+15
-3
src/squall/internal/codegen.gleam
···
119
119
"import gleam/http/request",
120
120
"import gleam/httpc",
121
121
"import gleam/json",
122
+
"import gleam/list",
122
123
"import gleam/option.{type Option}",
123
124
"import gleam/result",
125
+
"import squall",
124
126
]
125
127
126
128
import_lines
···
517
519
518
520
// Build parameter list as documents
519
521
let param_docs = case variables {
520
-
[] -> [doc.from_string("endpoint: String")]
522
+
[] -> [doc.from_string("client: squall.Client")]
521
523
vars -> {
522
524
let var_param_docs =
523
525
vars
···
535
537
})
536
538
|> list.filter_map(fn(r) { r })
537
539
538
-
[doc.from_string("endpoint: String"), ..var_param_docs]
540
+
[doc.from_string("client: squall.Client"), ..var_param_docs]
539
541
}
540
542
}
541
543
···
585
587
doc.from_string("use req <- result.try("),
586
588
doc.concat([
587
589
doc.line,
588
-
call_doc("request.to", [doc.from_string("endpoint")]),
590
+
call_doc("request.to", [doc.from_string("client.endpoint")]),
589
591
doc.line,
590
592
doc.from_string("|> result.map_error(fn(_) { \"Invalid endpoint URL\" }),"),
591
593
])
···
603
605
doc.from_string("|> request.set_body(json.to_string(body))"),
604
606
doc.line,
605
607
doc.from_string("|> request.set_header(\"content-type\", \"application/json\")"),
608
+
]),
609
+
),
610
+
let_var(
611
+
"req",
612
+
doc.concat([
613
+
doc.from_string("list.fold(client.headers, req, fn(r, header) {"),
614
+
doc.line,
615
+
doc.from_string(" request.set_header(r, header.0, header.1)"),
616
+
doc.line,
617
+
doc.from_string("})"),
606
618
]),
607
619
),
608
620
doc.concat([