monday_ruby 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.env +1 -1
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +104 -0
- data/README.md +146 -142
- data/docs/.vitepress/config.mjs +255 -0
- data/docs/.vitepress/theme/index.js +4 -0
- data/docs/.vitepress/theme/style.css +43 -0
- data/docs/README.md +80 -0
- data/docs/explanation/architecture.md +507 -0
- data/docs/explanation/best-practices/errors.md +478 -0
- data/docs/explanation/best-practices/performance.md +1084 -0
- data/docs/explanation/best-practices/rate-limiting.md +630 -0
- data/docs/explanation/best-practices/testing.md +820 -0
- data/docs/explanation/column-values.md +857 -0
- data/docs/explanation/design.md +795 -0
- data/docs/explanation/graphql.md +356 -0
- data/docs/explanation/migration/v1.md +808 -0
- data/docs/explanation/pagination.md +447 -0
- data/docs/guides/advanced/batch.md +1274 -0
- data/docs/guides/advanced/complex-queries.md +1114 -0
- data/docs/guides/advanced/errors.md +818 -0
- data/docs/guides/advanced/pagination.md +934 -0
- data/docs/guides/advanced/rate-limiting.md +981 -0
- data/docs/guides/authentication.md +286 -0
- data/docs/guides/boards/create.md +386 -0
- data/docs/guides/boards/delete.md +405 -0
- data/docs/guides/boards/duplicate.md +511 -0
- data/docs/guides/boards/query.md +530 -0
- data/docs/guides/boards/update.md +453 -0
- data/docs/guides/columns/create.md +452 -0
- data/docs/guides/columns/metadata.md +492 -0
- data/docs/guides/columns/query.md +455 -0
- data/docs/guides/columns/update-multiple.md +459 -0
- data/docs/guides/columns/update-values.md +509 -0
- data/docs/guides/files/add-to-column.md +40 -0
- data/docs/guides/files/add-to-update.md +37 -0
- data/docs/guides/files/clear-column.md +33 -0
- data/docs/guides/first-request.md +285 -0
- data/docs/guides/folders/manage.md +750 -0
- data/docs/guides/groups/items.md +626 -0
- data/docs/guides/groups/manage.md +501 -0
- data/docs/guides/installation.md +169 -0
- data/docs/guides/items/create.md +493 -0
- data/docs/guides/items/delete.md +514 -0
- data/docs/guides/items/query.md +605 -0
- data/docs/guides/items/subitems.md +483 -0
- data/docs/guides/items/update.md +699 -0
- data/docs/guides/updates/manage.md +619 -0
- data/docs/guides/use-cases/dashboard.md +1421 -0
- data/docs/guides/use-cases/import.md +1962 -0
- data/docs/guides/use-cases/task-management.md +1381 -0
- data/docs/guides/workspaces/manage.md +502 -0
- data/docs/index.md +69 -0
- data/docs/package-lock.json +2468 -0
- data/docs/package.json +13 -0
- data/docs/reference/client.md +540 -0
- data/docs/reference/configuration.md +586 -0
- data/docs/reference/errors.md +693 -0
- data/docs/reference/resources/account.md +208 -0
- data/docs/reference/resources/activity-log.md +369 -0
- data/docs/reference/resources/board-view.md +359 -0
- data/docs/reference/resources/board.md +393 -0
- data/docs/reference/resources/column.md +543 -0
- data/docs/reference/resources/file.md +236 -0
- data/docs/reference/resources/folder.md +386 -0
- data/docs/reference/resources/group.md +507 -0
- data/docs/reference/resources/item.md +348 -0
- data/docs/reference/resources/subitem.md +267 -0
- data/docs/reference/resources/update.md +259 -0
- data/docs/reference/resources/workspace.md +213 -0
- data/docs/reference/response.md +560 -0
- data/docs/tutorial/first-integration.md +713 -0
- data/lib/monday/client.rb +24 -0
- data/lib/monday/configuration.rb +5 -0
- data/lib/monday/request.rb +15 -0
- data/lib/monday/resources/base.rb +4 -0
- data/lib/monday/resources/file.rb +56 -0
- data/lib/monday/util.rb +1 -0
- data/lib/monday/version.rb +1 -1
- metadata +87 -4
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# Design Decisions
|
|
2
|
+
|
|
3
|
+
This document explores the key design decisions behind the monday_ruby gem, explaining the rationale, trade-offs, and principles that guide its implementation.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Design Philosophy](#design-philosophy)
|
|
8
|
+
- [Client-Resource Pattern Choice](#client-resource-pattern-choice)
|
|
9
|
+
- [Configuration Design](#configuration-design)
|
|
10
|
+
- [Error Exception Hierarchy](#error-exception-hierarchy)
|
|
11
|
+
- [GraphQL Abstraction Level](#graphql-abstraction-level)
|
|
12
|
+
- [Response Object Design](#response-object-design)
|
|
13
|
+
- [The base64 Dependency](#the-base64-dependency)
|
|
14
|
+
- [Future Design Considerations](#future-design-considerations)
|
|
15
|
+
|
|
16
|
+
## Design Philosophy
|
|
17
|
+
|
|
18
|
+
The monday_ruby gem is built on several core principles that inform all design decisions:
|
|
19
|
+
|
|
20
|
+
### 1. Ruby-Idiomatic Interface
|
|
21
|
+
|
|
22
|
+
The gem should feel natural to Ruby developers, even if they've never used GraphQL. This means:
|
|
23
|
+
- Using snake_case method names (`board.query` not `board.Query`)
|
|
24
|
+
- Accepting Ruby data structures (hashes, arrays, symbols)
|
|
25
|
+
- Following Ruby conventions for error handling (exceptions, not error codes)
|
|
26
|
+
- Providing sensible defaults that work for common cases
|
|
27
|
+
|
|
28
|
+
### 2. Explicit Over Implicit
|
|
29
|
+
|
|
30
|
+
While the gem hides GraphQL complexity, it remains explicit about what it's doing:
|
|
31
|
+
- Method names clearly indicate the operation (`create`, `query`, `update`, `delete`)
|
|
32
|
+
- Users explicitly specify what data they want via the `select` parameter
|
|
33
|
+
- No hidden network calls or lazy loading
|
|
34
|
+
- Query building is transparent (the query string could be logged/inspected)
|
|
35
|
+
|
|
36
|
+
### 3. Flexibility Without Complexity
|
|
37
|
+
|
|
38
|
+
The gem provides simple defaults for common cases while allowing customization:
|
|
39
|
+
- Default field selections for quick usage
|
|
40
|
+
- Override options for specific needs
|
|
41
|
+
- Extensible architecture for adding resources
|
|
42
|
+
- No forced opinions about how to structure application code
|
|
43
|
+
|
|
44
|
+
### 4. Fail Fast and Clearly
|
|
45
|
+
|
|
46
|
+
When things go wrong, the gem should make it obvious:
|
|
47
|
+
- Specific exception types for different error categories
|
|
48
|
+
- Response objects attached to exceptions for debugging
|
|
49
|
+
- No silent failures or generic error messages
|
|
50
|
+
- API errors are surfaced, not swallowed
|
|
51
|
+
|
|
52
|
+
These principles create a gem that's approachable for beginners but powerful for advanced users.
|
|
53
|
+
|
|
54
|
+
## Client-Resource Pattern Choice
|
|
55
|
+
|
|
56
|
+
The decision to use a client-resource pattern rather than alternative approaches deserves deeper exploration.
|
|
57
|
+
|
|
58
|
+
### The Decision
|
|
59
|
+
|
|
60
|
+
Resources are instantiated through the client and hold a reference back to it:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
client = Monday::Client.new(token: "...")
|
|
64
|
+
client.board.query(...) # client.board is a Board instance
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Rather than:
|
|
68
|
+
- Static methods: `Monday::Board.query(client, ...)`
|
|
69
|
+
- Standalone instances: `board = Monday::Board.new(token: "...")`
|
|
70
|
+
- Global configuration: `Monday.configure(...); Monday::Board.query(...)`
|
|
71
|
+
|
|
72
|
+
### Why This Design?
|
|
73
|
+
|
|
74
|
+
**1. Configuration Scoping**
|
|
75
|
+
|
|
76
|
+
By tying resources to a client instance, configuration becomes scoped:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
client_a = Monday::Client.new(token: "token_a")
|
|
80
|
+
client_b = Monday::Client.new(token: "token_b")
|
|
81
|
+
|
|
82
|
+
client_a.board.query(...) # Uses token_a
|
|
83
|
+
client_b.board.query(...) # Uses token_b
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is critical for applications that interact with multiple monday.com accounts or need different configurations (like different timeouts) for different request types.
|
|
87
|
+
|
|
88
|
+
**2. Dependency Injection**
|
|
89
|
+
|
|
90
|
+
Resources receive their dependencies (the client) through their constructor. This makes testing easier:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# In tests
|
|
94
|
+
mock_client = double("Client")
|
|
95
|
+
board = Monday::Resources::Board.new(mock_client)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Resources don't need global state or singleton instances to function.
|
|
99
|
+
|
|
100
|
+
**3. Discoverability**
|
|
101
|
+
|
|
102
|
+
The client acts as a namespace for all available resources. You can discover what's available by exploring the client:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
client.methods.grep(/^[a-z]/) # Shows all resource accessors
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This is harder with static methods spread across multiple classes.
|
|
109
|
+
|
|
110
|
+
**4. Shared State**
|
|
111
|
+
|
|
112
|
+
All resources on a client share the same configuration, connection, and error handling. This ensures consistency:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
client = Monday::Client.new(
|
|
116
|
+
token: "...",
|
|
117
|
+
open_timeout: 5,
|
|
118
|
+
read_timeout: 30
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# All resources use the same timeouts
|
|
122
|
+
client.board.query(...)
|
|
123
|
+
client.item.create(...)
|
|
124
|
+
client.group.delete(...)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Trade-offs
|
|
128
|
+
|
|
129
|
+
**Advantages:**
|
|
130
|
+
- Configuration scoping (multiple clients possible)
|
|
131
|
+
- Clear dependency relationships
|
|
132
|
+
- Consistent behavior across resources
|
|
133
|
+
- Easy to test with mocks
|
|
134
|
+
|
|
135
|
+
**Disadvantages:**
|
|
136
|
+
- More verbose than static methods: `client.board.query(...)` vs `Board.query(...)`
|
|
137
|
+
- Resources can't be used independently (always need a client)
|
|
138
|
+
- Extra initialization step (creating the client)
|
|
139
|
+
|
|
140
|
+
For a library wrapping an authenticated API, these trade-offs favor the client-resource pattern. The scoping and dependency injection benefits outweigh the verbosity.
|
|
141
|
+
|
|
142
|
+
## Configuration Design
|
|
143
|
+
|
|
144
|
+
The gem supports both global and instance-level configuration, which is unusual. Most libraries choose one approach.
|
|
145
|
+
|
|
146
|
+
### The Dual System
|
|
147
|
+
|
|
148
|
+
**Global Configuration:**
|
|
149
|
+
```ruby
|
|
150
|
+
Monday.configure do |config|
|
|
151
|
+
config.token = "..."
|
|
152
|
+
config.version = "2023-07"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
client = Monday::Client.new # Uses global config
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Instance Configuration:**
|
|
159
|
+
```ruby
|
|
160
|
+
client = Monday::Client.new(
|
|
161
|
+
token: "...",
|
|
162
|
+
version: "2023-07"
|
|
163
|
+
) # Creates its own config
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Why Both?
|
|
167
|
+
|
|
168
|
+
This design serves different use cases:
|
|
169
|
+
|
|
170
|
+
**Global configuration** is ideal for:
|
|
171
|
+
- Simple applications with one monday.com account
|
|
172
|
+
- Setting defaults for all clients
|
|
173
|
+
- Quick prototyping and scripts
|
|
174
|
+
- Rails applications (config in an initializer)
|
|
175
|
+
|
|
176
|
+
**Instance configuration** is ideal for:
|
|
177
|
+
- Multi-tenant applications
|
|
178
|
+
- Testing (different configs for different test scenarios)
|
|
179
|
+
- Applications integrating with multiple monday.com accounts
|
|
180
|
+
- Overriding specific settings for specific requests
|
|
181
|
+
|
|
182
|
+
### Implementation Details
|
|
183
|
+
|
|
184
|
+
The implementation is elegantly simple:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
def configure(config_args)
|
|
188
|
+
return Monday.config if config_args.empty?
|
|
189
|
+
Configuration.new(**config_args)
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If no arguments are provided, use the global singleton. Otherwise, create a new `Configuration` instance. This means:
|
|
194
|
+
|
|
195
|
+
- No global client instance (would prevent multiple configurations)
|
|
196
|
+
- Global config is lazy-loaded (created on first access)
|
|
197
|
+
- Instance configs are independent (don't affect global or each other)
|
|
198
|
+
|
|
199
|
+
### Alternative Approaches
|
|
200
|
+
|
|
201
|
+
**Only Global Configuration:**
|
|
202
|
+
Many Ruby libraries (like Octokit, Faraday) use only global configuration. This is simpler but makes multi-tenant scenarios difficult:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# Hard to manage multiple accounts
|
|
206
|
+
Monday.configure(token: "account_a_token")
|
|
207
|
+
result_a = Monday::Board.query(...)
|
|
208
|
+
|
|
209
|
+
Monday.configure(token: "account_b_token") # Overwrites!
|
|
210
|
+
result_b = Monday::Board.query(...)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Only Instance Configuration:**
|
|
214
|
+
Some libraries require explicit configuration for every client. This is flexible but verbose for single-account applications:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# Repetitive in simple cases
|
|
218
|
+
client1 = Monday::Client.new(token: "...", version: "2023-07", host: "...")
|
|
219
|
+
client2 = Monday::Client.new(token: "...", version: "2023-07", host: "...")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Inheritance Pattern:**
|
|
223
|
+
Some libraries allow instance configs to inherit from global and override selectively:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
Monday.configure do |config|
|
|
227
|
+
config.version = "2023-07"
|
|
228
|
+
config.host = "https://api.monday.com/v2"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
client = Monday::Client.new(token: "...") # Inherits version and host, sets token
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The current design doesn't support inheritance. Instance configs are completely independent from global config. This is simpler to understand (no merge logic) but less flexible.
|
|
235
|
+
|
|
236
|
+
### Trade-offs
|
|
237
|
+
|
|
238
|
+
The dual system is more complex than a single approach, but it serves real use cases. The simplicity of the implementation (just return global or create instance) keeps maintenance burden low.
|
|
239
|
+
|
|
240
|
+
Future versions could add inheritance if user demand exists, but the current design satisfies the common cases.
|
|
241
|
+
|
|
242
|
+
## Error Exception Hierarchy
|
|
243
|
+
|
|
244
|
+
The gem defines a hierarchy of exception classes that mirror monday.com's error types.
|
|
245
|
+
|
|
246
|
+
### The Hierarchy
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
Monday::Error (base)
|
|
250
|
+
├── Monday::InternalServerError (500)
|
|
251
|
+
├── Monday::AuthorizationError (401, 403)
|
|
252
|
+
├── Monday::RateLimitError (429)
|
|
253
|
+
├── Monday::ResourceNotFoundError (404)
|
|
254
|
+
├── Monday::InvalidRequestError (400)
|
|
255
|
+
└── Monday::ComplexityError (GraphQL complexity)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Design Rationale
|
|
259
|
+
|
|
260
|
+
**1. Catchall with Specificity**
|
|
261
|
+
|
|
262
|
+
Users can rescue all API errors:
|
|
263
|
+
```ruby
|
|
264
|
+
rescue Monday::Error => e
|
|
265
|
+
# Handle any monday.com error
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Or specific types:
|
|
270
|
+
```ruby
|
|
271
|
+
rescue Monday::RateLimitError => e
|
|
272
|
+
sleep 60
|
|
273
|
+
retry
|
|
274
|
+
rescue Monday::AuthorizationError => e
|
|
275
|
+
refresh_token
|
|
276
|
+
retry
|
|
277
|
+
rescue Monday::Error => e
|
|
278
|
+
log_error(e)
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**2. HTTP Semantics**
|
|
283
|
+
|
|
284
|
+
The exception hierarchy follows HTTP status code semantics. This makes the gem's behavior predictable to developers familiar with REST APIs, even though monday.com uses GraphQL.
|
|
285
|
+
|
|
286
|
+
**3. Rich Error Objects**
|
|
287
|
+
|
|
288
|
+
All exceptions include:
|
|
289
|
+
- `message`: Human-readable error description
|
|
290
|
+
- `response`: The full Response object for debugging
|
|
291
|
+
- `code`: HTTP status code or custom error code
|
|
292
|
+
|
|
293
|
+
This allows detailed error handling:
|
|
294
|
+
```ruby
|
|
295
|
+
rescue Monday::Error => e
|
|
296
|
+
puts "Error: #{e.message}"
|
|
297
|
+
puts "Status: #{e.code}"
|
|
298
|
+
puts "Body: #{e.response.body.inspect}"
|
|
299
|
+
puts "Error data: #{e.error_data.inspect}"
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### The Mapping Problem
|
|
304
|
+
|
|
305
|
+
monday.com returns errors in inconsistent formats:
|
|
306
|
+
- HTTP status codes (401, 404, 429, 500)
|
|
307
|
+
- GraphQL error codes (`ComplexityException`, `USER_UNAUTHORIZED`)
|
|
308
|
+
- Different key names (`code` vs `error_code`)
|
|
309
|
+
- Errors in arrays vs. top-level objects
|
|
310
|
+
|
|
311
|
+
The gem handles this with two mapping methods:
|
|
312
|
+
|
|
313
|
+
**`Util.status_code_exceptions_mapping`** - Maps HTTP codes to exceptions:
|
|
314
|
+
```ruby
|
|
315
|
+
{
|
|
316
|
+
"500" => InternalServerError,
|
|
317
|
+
"429" => RateLimitError,
|
|
318
|
+
"404" => ResourceNotFoundError,
|
|
319
|
+
# ...
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**`Util.response_error_exceptions_mapping`** - Maps API error codes to exceptions:
|
|
324
|
+
```ruby
|
|
325
|
+
{
|
|
326
|
+
"ComplexityException" => [ComplexityError, 429],
|
|
327
|
+
"USER_UNAUTHORIZED" => [AuthorizationError, 403],
|
|
328
|
+
"InvalidBoardIdException" => [InvalidRequestError, 400],
|
|
329
|
+
# ...
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The client tries both approaches:
|
|
334
|
+
1. Check HTTP status code → raise default exception if not 2xx
|
|
335
|
+
2. Check response body error codes → raise specific exception
|
|
336
|
+
|
|
337
|
+
This handles both HTTP-level errors (network issues, auth failures) and GraphQL-level errors (invalid queries, business logic failures).
|
|
338
|
+
|
|
339
|
+
### Why Not One Generic Exception?
|
|
340
|
+
|
|
341
|
+
A single `Monday::Error` would be simpler:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
# Hypothetical simpler design
|
|
345
|
+
raise Monday::Error.new(message: error_message, code: error_code)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
But this loses semantic information. Users would have to check error codes or messages to determine the error type:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
rescue Monday::Error => e
|
|
352
|
+
if e.code == 429
|
|
353
|
+
# Rate limit
|
|
354
|
+
elsif e.code == 401
|
|
355
|
+
# Auth error
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Specific exception types make error handling clearer and more robust (error messages can change, but exception types are part of the API contract).
|
|
361
|
+
|
|
362
|
+
### Trade-offs
|
|
363
|
+
|
|
364
|
+
**Advantages:**
|
|
365
|
+
- Semantic error handling (rescue specific types)
|
|
366
|
+
- Follows HTTP conventions
|
|
367
|
+
- Rich error information
|
|
368
|
+
- Extensible (new exception types can be added)
|
|
369
|
+
|
|
370
|
+
**Disadvantages:**
|
|
371
|
+
- More classes to maintain
|
|
372
|
+
- Mapping tables need updates when monday.com adds error codes
|
|
373
|
+
- Users must learn the exception hierarchy
|
|
374
|
+
|
|
375
|
+
The benefits of semantic error handling outweigh the maintenance cost, especially as the gem matures and error types stabilize.
|
|
376
|
+
|
|
377
|
+
## GraphQL Abstraction Level
|
|
378
|
+
|
|
379
|
+
A key design question is: How much GraphQL should the gem expose?
|
|
380
|
+
|
|
381
|
+
### The Chosen Abstraction
|
|
382
|
+
|
|
383
|
+
The gem provides a **high-level abstraction** that hides GraphQL entirely:
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
client.board.query(
|
|
387
|
+
args: {ids: [123]},
|
|
388
|
+
select: ["id", "name", {"items" => ["id"]}]
|
|
389
|
+
)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Users don't write GraphQL queries. They call Ruby methods with Ruby data structures.
|
|
393
|
+
|
|
394
|
+
### Alternative Abstraction Levels
|
|
395
|
+
|
|
396
|
+
**Low-Level (Expose GraphQL):**
|
|
397
|
+
```ruby
|
|
398
|
+
client.execute(<<~GRAPHQL)
|
|
399
|
+
query {
|
|
400
|
+
boards(ids: [123]) {
|
|
401
|
+
id
|
|
402
|
+
name
|
|
403
|
+
items { id }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
GRAPHQL
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Medium-Level (Query builders):**
|
|
410
|
+
```ruby
|
|
411
|
+
client.query do |q|
|
|
412
|
+
q.boards(ids: [123]) do |b|
|
|
413
|
+
b.field :id
|
|
414
|
+
b.field :name
|
|
415
|
+
b.field :items do |i|
|
|
416
|
+
i.field :id
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**High-Level (Hide GraphQL):**
|
|
423
|
+
```ruby
|
|
424
|
+
client.board.query(args: {ids: [123]}, select: ["id", "name", {"items" => ["id"]}])
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Why High-Level?
|
|
428
|
+
|
|
429
|
+
**1. Accessibility**
|
|
430
|
+
|
|
431
|
+
Most Ruby developers haven't used GraphQL. By hiding it, the gem is accessible to a broader audience. Users can be productive without learning GraphQL syntax, schema introspection, or query optimization.
|
|
432
|
+
|
|
433
|
+
**2. Consistency**
|
|
434
|
+
|
|
435
|
+
All methods follow the same pattern: `args` for parameters, `select` for fields. This consistency makes the API predictable. Once you understand `board.query`, you understand `item.query`.
|
|
436
|
+
|
|
437
|
+
**3. Simplicity**
|
|
438
|
+
|
|
439
|
+
No query builder DSL to learn. No GraphQL client library to understand. Just method calls with hashes and arrays.
|
|
440
|
+
|
|
441
|
+
**4. Monday.com Specifics**
|
|
442
|
+
|
|
443
|
+
The abstraction can encode monday.com-specific knowledge:
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
# Default field selections that make sense for monday.com
|
|
447
|
+
def query(args: {}, select: DEFAULT_SELECT)
|
|
448
|
+
# DEFAULT_SELECT = ["id", "name", "description"]
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Users get sensible defaults without knowing what fields exist.
|
|
453
|
+
|
|
454
|
+
### Trade-offs
|
|
455
|
+
|
|
456
|
+
**Advantages:**
|
|
457
|
+
- No GraphQL knowledge required
|
|
458
|
+
- Consistent API across resources
|
|
459
|
+
- Defaults encode monday.com best practices
|
|
460
|
+
- Simple to use for common cases
|
|
461
|
+
|
|
462
|
+
**Disadvantages:**
|
|
463
|
+
- Can't use all GraphQL features (aliases, fragments, directives)
|
|
464
|
+
- Abstraction can leak (some monday.com concepts don't map cleanly)
|
|
465
|
+
- Less flexible than raw GraphQL
|
|
466
|
+
- Users must learn the gem's API instead of standard GraphQL
|
|
467
|
+
|
|
468
|
+
### When the Abstraction Leaks
|
|
469
|
+
|
|
470
|
+
The high-level abstraction sometimes reveals its GraphQL underpinnings:
|
|
471
|
+
|
|
472
|
+
**Field selection syntax** mirrors GraphQL structure:
|
|
473
|
+
```ruby
|
|
474
|
+
select: ["id", {"items" => ["id", "name"]}]
|
|
475
|
+
# Generates: id items { id name }
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Arguments** use GraphQL types:
|
|
479
|
+
```ruby
|
|
480
|
+
args: {operator: :and} # Symbol becomes GraphQL enum
|
|
481
|
+
args: {rules: [...]} # Array becomes GraphQL list
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
These aren't pure Ruby APIs - they're GraphQL concepts exposed through Ruby syntax.
|
|
485
|
+
|
|
486
|
+
### Future Direction
|
|
487
|
+
|
|
488
|
+
The abstraction could evolve in two directions:
|
|
489
|
+
|
|
490
|
+
**More abstraction**: Hide even the field selection:
|
|
491
|
+
```ruby
|
|
492
|
+
client.board.find(123) # Returns a board object with default fields
|
|
493
|
+
client.board.find(123, include: [:items]) # Include related items
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Less abstraction**: Expose an escape hatch for raw GraphQL:
|
|
497
|
+
```ruby
|
|
498
|
+
client.execute(graphql_query_string)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
The current design balances these extremes. It's high-level enough for ease of use but low-level enough to expose GraphQL's power (explicit field selection, complex queries).
|
|
502
|
+
|
|
503
|
+
## Response Object Design
|
|
504
|
+
|
|
505
|
+
The gem wraps `Net::HTTP::Response` in a custom `Monday::Response` class rather than returning the raw response.
|
|
506
|
+
|
|
507
|
+
### The Design
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
class Response
|
|
511
|
+
attr_reader :status, :body, :headers
|
|
512
|
+
|
|
513
|
+
def initialize(response)
|
|
514
|
+
@status = response.code.to_i
|
|
515
|
+
@body = parse_body # Parses JSON
|
|
516
|
+
@headers = parse_headers
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def success?
|
|
520
|
+
(200..299).cover?(status) && !errors?
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Why Wrap?
|
|
526
|
+
|
|
527
|
+
**1. Consistent Interface**
|
|
528
|
+
|
|
529
|
+
`Net::HTTP::Response` has quirks:
|
|
530
|
+
- `response.code` is a string ("200"), not an integer
|
|
531
|
+
- `response.body` is raw JSON, not parsed
|
|
532
|
+
- Headers are accessed with `response.each_header`
|
|
533
|
+
|
|
534
|
+
The wrapper provides a cleaner, more predictable interface:
|
|
535
|
+
- `response.status` is always an integer
|
|
536
|
+
- `response.body` is always a parsed hash
|
|
537
|
+
- `response.headers` is a simple hash
|
|
538
|
+
|
|
539
|
+
**2. monday.com Specifics**
|
|
540
|
+
|
|
541
|
+
The `success?` method encodes monday.com-specific knowledge:
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
def success?
|
|
545
|
+
(200..299).cover?(status) && !errors?
|
|
546
|
+
end
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
monday.com returns HTTP 200 for GraphQL errors, so HTTP status alone doesn't indicate success. The wrapper checks both HTTP status and response body.
|
|
550
|
+
|
|
551
|
+
**3. Future Evolution**
|
|
552
|
+
|
|
553
|
+
The wrapper provides a stable API even if the underlying HTTP library changes. If the gem switches from `Net::HTTP` to `httparty` or `faraday`, the Response interface can remain the same.
|
|
554
|
+
|
|
555
|
+
**4. Exception Context**
|
|
556
|
+
|
|
557
|
+
All exceptions include the Response object:
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
exception.response.body
|
|
561
|
+
exception.response.status
|
|
562
|
+
exception.response.headers
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
This wouldn't work cleanly with raw `Net::HTTP::Response` because it doesn't guarantee a parsed body or integer status.
|
|
566
|
+
|
|
567
|
+
### Alternative: Return Raw Response
|
|
568
|
+
|
|
569
|
+
The gem could return `Net::HTTP::Response` directly:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
http_response = client.board.query(...)
|
|
573
|
+
body = JSON.parse(http_response.body)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Advantages:**
|
|
577
|
+
- Users can access all `Net::HTTP::Response` methods
|
|
578
|
+
- No abstraction layer
|
|
579
|
+
- Familiar to Ruby developers
|
|
580
|
+
|
|
581
|
+
**Disadvantages:**
|
|
582
|
+
- Users must parse JSON themselves
|
|
583
|
+
- No monday.com-specific success detection
|
|
584
|
+
- Less consistent (status is string vs integer confusion)
|
|
585
|
+
- Tied to Net::HTTP (harder to change HTTP library)
|
|
586
|
+
|
|
587
|
+
### Trade-offs
|
|
588
|
+
|
|
589
|
+
The wrapper adds a thin abstraction layer, but it significantly improves usability:
|
|
590
|
+
|
|
591
|
+
```ruby
|
|
592
|
+
# With wrapper
|
|
593
|
+
response = client.board.query(...)
|
|
594
|
+
if response.success?
|
|
595
|
+
boards = response.body["data"]["boards"]
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Without wrapper (hypothetical)
|
|
599
|
+
response = client.board.query(...)
|
|
600
|
+
if response.code.to_i.between?(200, 299)
|
|
601
|
+
parsed = JSON.parse(response.body)
|
|
602
|
+
unless parsed["errors"] || parsed["error_code"]
|
|
603
|
+
boards = parsed["data"]["boards"]
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
The wrapper encapsulates complexity that users would otherwise repeat in every integration.
|
|
609
|
+
|
|
610
|
+
## The base64 Dependency
|
|
611
|
+
|
|
612
|
+
The gem explicitly depends on the `base64` gem, even though the code never directly requires or uses Base64 encoding. This decision requires explanation.
|
|
613
|
+
|
|
614
|
+
### The Issue
|
|
615
|
+
|
|
616
|
+
Starting with Ruby 3.4, `base64` was removed from Ruby's default gems. It must be explicitly added as a dependency to Gemfile.
|
|
617
|
+
|
|
618
|
+
The monday_ruby gem uses `Net::HTTP` for HTTP requests. `Net::HTTP` internally requires `base64` for HTTP Basic Authentication, even if the gem doesn't use Basic Auth.
|
|
619
|
+
|
|
620
|
+
Without the explicit dependency, the gem would fail on Ruby 3.4+ with:
|
|
621
|
+
```
|
|
622
|
+
LoadError: cannot load such file -- base64
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### The Decision
|
|
626
|
+
|
|
627
|
+
Rather than letting users discover this error in production, the gem explicitly declares the dependency:
|
|
628
|
+
|
|
629
|
+
```ruby
|
|
630
|
+
# In gemspec
|
|
631
|
+
spec.add_dependency "base64", "~> 0.2.0"
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Why Not Remove Net::HTTP?
|
|
635
|
+
|
|
636
|
+
The gem could switch to an HTTP library that doesn't require `base64`:
|
|
637
|
+
- `httparty`
|
|
638
|
+
- `faraday`
|
|
639
|
+
- `rest-client`
|
|
640
|
+
|
|
641
|
+
However:
|
|
642
|
+
- `Net::HTTP` is in Ruby's standard library (no external dependencies until Ruby 3.4)
|
|
643
|
+
- It's simple and well-understood
|
|
644
|
+
- The gem's HTTP needs are basic (POST requests with JSON)
|
|
645
|
+
- Switching would add dependencies for Ruby < 3.4 users
|
|
646
|
+
|
|
647
|
+
Adding `base64` as a dependency is simpler than changing HTTP libraries.
|
|
648
|
+
|
|
649
|
+
### Future Considerations
|
|
650
|
+
|
|
651
|
+
As Ruby 3.4+ adoption grows, this decision may be revisited. Options include:
|
|
652
|
+
- Keep the `base64` dependency (current approach)
|
|
653
|
+
- Switch to a different HTTP library
|
|
654
|
+
- Conditionally require `base64` only on Ruby 3.4+
|
|
655
|
+
|
|
656
|
+
For now, explicit dependency on `base64` is the simplest solution that works across all Ruby versions.
|
|
657
|
+
|
|
658
|
+
## Future Design Considerations
|
|
659
|
+
|
|
660
|
+
Design decisions aren't permanent. As the gem evolves, several areas merit reconsideration.
|
|
661
|
+
|
|
662
|
+
### 1. Query Caching
|
|
663
|
+
|
|
664
|
+
Currently, every request hits the monday.com API. Future versions could cache responses:
|
|
665
|
+
|
|
666
|
+
```ruby
|
|
667
|
+
client = Monday::Client.new(token: "...", cache: Redis.new)
|
|
668
|
+
client.board.query(args: {ids: [123]}) # Hits API
|
|
669
|
+
client.board.query(args: {ids: [123]}) # Returns cached response
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Considerations:**
|
|
673
|
+
- Cache invalidation is hard (when does cached data become stale?)
|
|
674
|
+
- monday.com data changes frequently (boards, items updated constantly)
|
|
675
|
+
- Would complicate the simple request-response model
|
|
676
|
+
- Adds dependency on cache backend
|
|
677
|
+
|
|
678
|
+
Caching might be better left to application code using the gem.
|
|
679
|
+
|
|
680
|
+
### 2. Async/Batch Requests
|
|
681
|
+
|
|
682
|
+
The gem could support batching multiple queries:
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
client.batch do |batch|
|
|
686
|
+
batch.board.query(args: {ids: [123]})
|
|
687
|
+
batch.item.query(args: {ids: [456]})
|
|
688
|
+
batch.group.query(args: {ids: [789]})
|
|
689
|
+
end # Executes all queries in one HTTP request
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
GraphQL supports this natively. The gem could expose it.
|
|
693
|
+
|
|
694
|
+
**Considerations:**
|
|
695
|
+
- Batch requests are more complex (partial failures, ordering)
|
|
696
|
+
- monday.com's API may have batch size limits
|
|
697
|
+
- The simple one-method-one-request model would break
|
|
698
|
+
- Testing becomes harder (mocking batch responses)
|
|
699
|
+
|
|
700
|
+
This would be a significant design change requiring careful thought.
|
|
701
|
+
|
|
702
|
+
### 3. Pagination Helpers
|
|
703
|
+
|
|
704
|
+
The gem exposes monday.com's cursor-based pagination but doesn't provide helpers:
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
# Current approach
|
|
708
|
+
response = client.board.items_page(board_id: 123, limit: 100)
|
|
709
|
+
items = response.body.dig("data", "boards", 0, "items_page", "items")
|
|
710
|
+
cursor = response.body.dig("data", "boards", 0, "items_page", "cursor")
|
|
711
|
+
|
|
712
|
+
response = client.board.items_page(board_id: 123, cursor: cursor)
|
|
713
|
+
# Repeat...
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
A pagination helper could simplify this:
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
# Hypothetical helper
|
|
720
|
+
client.board.items_page(board_id: 123).each_page do |items, cursor|
|
|
721
|
+
process(items)
|
|
722
|
+
break if cursor.nil?
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Or automatic pagination
|
|
726
|
+
all_items = client.board.all_items(board_id: 123) # Fetches all pages
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Considerations:**
|
|
730
|
+
- Auto-pagination could make many API calls without users realizing
|
|
731
|
+
- Rate limiting becomes more complex
|
|
732
|
+
- Adds stateful behavior (tracking cursors)
|
|
733
|
+
- Different monday.com resources paginate differently
|
|
734
|
+
|
|
735
|
+
Pagination helpers would need careful design to avoid surprising behavior.
|
|
736
|
+
|
|
737
|
+
### 4. Response Object Enhancement
|
|
738
|
+
|
|
739
|
+
The Response object could provide convenience methods:
|
|
740
|
+
|
|
741
|
+
```ruby
|
|
742
|
+
response = client.board.query(args: {ids: [123]})
|
|
743
|
+
|
|
744
|
+
# Current approach
|
|
745
|
+
boards = response.body.dig("data", "boards")
|
|
746
|
+
|
|
747
|
+
# Enhanced approach
|
|
748
|
+
boards = response.data.boards # Method chaining
|
|
749
|
+
boards = response.boards # Even simpler
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Considerations:**
|
|
753
|
+
- Requires understanding monday.com's response structure
|
|
754
|
+
- Different queries return different structures
|
|
755
|
+
- Could hide response complexity (good or bad?)
|
|
756
|
+
- Adds magic (method_missing or dynamic method definition)
|
|
757
|
+
|
|
758
|
+
This would make common cases simpler but could confuse debugging.
|
|
759
|
+
|
|
760
|
+
### 5. Validation
|
|
761
|
+
|
|
762
|
+
The gem could validate arguments before making requests:
|
|
763
|
+
|
|
764
|
+
```ruby
|
|
765
|
+
client.board.query(args: {ids: "not an array"})
|
|
766
|
+
# Currently: monday.com API returns error
|
|
767
|
+
# Could: Gem raises ArgumentError immediately
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
**Considerations:**
|
|
771
|
+
- Requires duplicating monday.com's validation logic
|
|
772
|
+
- monday.com's API evolves (validation rules change)
|
|
773
|
+
- Validation errors vs. API errors (different exception types?)
|
|
774
|
+
- Adds maintenance burden
|
|
775
|
+
|
|
776
|
+
Early validation helps users but couples the gem to monday.com's current API.
|
|
777
|
+
|
|
778
|
+
## Conclusion
|
|
779
|
+
|
|
780
|
+
The monday_ruby gem's design emerged from specific goals and constraints:
|
|
781
|
+
|
|
782
|
+
- **Client-resource pattern**: Balances organization, discoverability, and flexibility
|
|
783
|
+
- **Dual configuration**: Serves both simple and complex use cases
|
|
784
|
+
- **Exception hierarchy**: Enables semantic error handling
|
|
785
|
+
- **High-level abstraction**: Prioritizes accessibility over GraphQL power
|
|
786
|
+
- **Response wrapper**: Provides consistency and monday.com-specific logic
|
|
787
|
+
- **Explicit dependencies**: Ensures compatibility across Ruby versions
|
|
788
|
+
|
|
789
|
+
These decisions involve trade-offs. The design optimizes for:
|
|
790
|
+
1. Ease of use for Ruby developers new to monday.com
|
|
791
|
+
2. Explicit behavior over hidden magic
|
|
792
|
+
3. Flexibility for advanced users
|
|
793
|
+
4. Maintainability as monday.com's API evolves
|
|
794
|
+
|
|
795
|
+
Future evolution will balance these goals against emerging use cases and community feedback. Good design isn't about perfect decisions - it's about thoughtful trade-offs that serve the majority of users while remaining open to change.
|