active_graphql 0.2.1

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.hound.yml +4 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +48 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/CHANGELOG.md +25 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +15 -0
  11. data/Gemfile.lock +134 -0
  12. data/LICENSE.txt +21 -0
  13. data/Rakefile +6 -0
  14. data/active_graphql.gemspec +49 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docs/.nojekyll +0 -0
  18. data/docs/README.md +95 -0
  19. data/docs/_sidebar.md +4 -0
  20. data/docs/client.md +69 -0
  21. data/docs/index.html +70 -0
  22. data/docs/model.md +464 -0
  23. data/lib/active_graphql.rb +10 -0
  24. data/lib/active_graphql/client.rb +38 -0
  25. data/lib/active_graphql/client/actions.rb +15 -0
  26. data/lib/active_graphql/client/actions/action.rb +116 -0
  27. data/lib/active_graphql/client/actions/action/format_inputs.rb +80 -0
  28. data/lib/active_graphql/client/actions/action/format_outputs.rb +40 -0
  29. data/lib/active_graphql/client/actions/mutation_action.rb +29 -0
  30. data/lib/active_graphql/client/actions/query_action.rb +23 -0
  31. data/lib/active_graphql/client/adapters.rb +10 -0
  32. data/lib/active_graphql/client/adapters/graphlient_adapter.rb +32 -0
  33. data/lib/active_graphql/client/response.rb +47 -0
  34. data/lib/active_graphql/errors.rb +11 -0
  35. data/lib/active_graphql/model.rb +174 -0
  36. data/lib/active_graphql/model/action_formatter.rb +96 -0
  37. data/lib/active_graphql/model/build_or_relation.rb +66 -0
  38. data/lib/active_graphql/model/configuration.rb +83 -0
  39. data/lib/active_graphql/model/find_in_batches.rb +54 -0
  40. data/lib/active_graphql/model/relation_proxy.rb +321 -0
  41. data/lib/active_graphql/version.rb +5 -0
  42. metadata +254 -0
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/docs/.nojekyll ADDED
File without changes
data/docs/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # ActiveGraphql
2
+ [![Build Status](https://travis-ci.com/samesystem/active_graphql.svg?branch=master)](https://travis-ci.com/samesystem/active_graphql)
3
+ [![codecov](https://codecov.io/gh/samesystem/active_graphql/branch/master/graph/badge.svg)](https://codecov.io/gh/samesystem/active_graphql)
4
+ [![Documentation](https://readthedocs.org/projects/ansicolortags/badge/?version=latest)](https://samesystem.github.io/active_graphql)
5
+
6
+ GraphQL client which allows to interact with graphql using ActiveRecord-like API
7
+
8
+ Detailed documentation can be found at https://samesystem.github.io/active_graphql
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'active_graphql'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install active_graphql
25
+
26
+ ## Usage
27
+
28
+ You can fetch data from GraphQL in two different ways: using `ActiveGraphql::Client` or using `ActiveGraphql::Model`
29
+
30
+ ### [ActiveGraphql::Client](client.md)
31
+
32
+ `ActiveGraphql::Client` is a client which allows you to make requests using ruby-friendly code:
33
+
34
+ ```ruby
35
+ client = ActiveGraphql::Client.new(url: 'https://example.com/graphql')
36
+
37
+ client.query(:findUser).inputs(id: 1).outputs(:name, :avatar_url).result
38
+ # or same request with AR-style syntax
39
+ client.query(:findUser).select(:name, :avatar_url).where(id: 1).result
40
+ ```
41
+
42
+ Find out more how to use Client in [Client documentation](client.md)
43
+
44
+ ### [ActiveGraphql::Model](model.md)
45
+
46
+ If you have well structured GraphQL endpoint, which has CRUD actions for each entity then you can interact with GraphQL endpoints using `ActiveGraphql::Model`.
47
+ It allows you to have separate class for separate GraphQL entity, Here is an example:
48
+
49
+ Suppose you have following endpoints in graphql:
50
+
51
+ * `users(filter: UsersFilter!`) - index action with filtering possibilities
52
+ * `user(id: ID!)` - show action
53
+
54
+ In this case you can create ruby class like this:
55
+
56
+ ```ruby
57
+ class User
58
+ include ActiveGraphql::Model
59
+
60
+ active_graphql do |c|
61
+ c.url('http://example.com/graphql')
62
+ c.attributes :id, :first_name, :last_name, :created_at
63
+ end
64
+ end
65
+ ```
66
+
67
+ with this small setup you are able to do following:
68
+
69
+ ```ruby
70
+ User.where(first_name: 'John').to_a # list all users with name "John"
71
+ User.limit(5).to_a # list first 5 users
72
+ User.find(1) # find user with ID: 1
73
+ User.first(2) # find first 2 users
74
+ User.last(3) # find last 3 users
75
+ ```
76
+
77
+ Find out more how to use Model in [Model documentation](client.md)
78
+
79
+ ## Development
80
+
81
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
82
+
83
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
84
+
85
+ ## Contributing
86
+
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/samesystem/active_graphql. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
88
+
89
+ ## License
90
+
91
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
92
+
93
+ ## Code of Conduct
94
+
95
+ Everyone interacting in the ActiveGraphql project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/samesystem/active_graphql/blob/master/CODE_OF_CONDUCT.md).
data/docs/_sidebar.md ADDED
@@ -0,0 +1,4 @@
1
+ * [Home](README)
2
+ * [Client](client.md)
3
+ * [Model](model.md)
4
+
data/docs/client.md ADDED
@@ -0,0 +1,69 @@
1
+ # Client
2
+
3
+ ## Initialization
4
+
5
+ to initialize graphql client, simply create new client instance with url:
6
+
7
+ ```ruby
8
+ client = ActiveGraphql::Client.new(url: 'http://example.com/graphql')
9
+ ```
10
+
11
+ you can also provide extra options which will be accepted by addapter, like this:
12
+
13
+ ```ruby
14
+ client = ActiveGraphql::Client.new(url: 'http://example.com/graphql', headers: {}, schema_path: '...')
15
+ ```
16
+
17
+ ## query and mutation actions
18
+
19
+ ```ruby
20
+ mutation = client.mutation(:create_user)
21
+ query = client.query(:find_user)
22
+ ```
23
+
24
+ ### where (alias: input)
25
+
26
+ In order to filter values you can query with `where` method:
27
+
28
+ ```ruby
29
+ query = query.where(name: 'John', date: { from: '2000-01-01' })
30
+ ```
31
+
32
+ this will produce following GraphQL:
33
+
34
+ ```graphql
35
+ query {
36
+ find_user(name: "John", date: { from: "2000-01-01" }) {
37
+ ...
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### select (alias: output)
43
+
44
+ In order to select which attributes you want to receive from query then you need to use `select` method:
45
+
46
+ ```ruby
47
+ query = query.select(:name, date: [:year])
48
+ ```
49
+
50
+ this will produce following GraphQL:
51
+
52
+ ```graphql
53
+ query {
54
+ find_user {
55
+ name
56
+ date { year }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### meta
62
+
63
+ You can assign meta attributes in order to use them later
64
+
65
+ ```ruby
66
+ query = query.meta(custom: true)
67
+ query = query.meta(also_custom: 'yes')
68
+ query.meta_attributes # => { :custom => true, :also_custom => "yes" }
69
+ ```
data/docs/index.html ADDED
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Document</title>
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
7
+ <meta name="description" content="Description">
8
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
9
+ <link rel="stylesheet" href="https://unpkg.com/docsify/lib/themes/vue.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script>
14
+ function parseQueryString (queryString) {
15
+ var params = {};
16
+ var temp;
17
+ // Split into key/value pairs
18
+ queries = queryString.split("&");
19
+ // Convert the array of strings into an object
20
+ for (var i = 0, l = queries.length; i < l; i++ ) {
21
+ temp = queries[i].split('=');
22
+ params[temp[0]] = temp[1];
23
+ }
24
+ return params;
25
+ };
26
+
27
+ function getJsonFromUrl() {
28
+ return parseQueryString(location.search.substr(1));
29
+ }
30
+
31
+ window.$docsify = {
32
+ auto2top: true,
33
+ name: 'ActiveGraphql',
34
+ repo: 'https://github.com/samesystem/active_graphql',
35
+ subMaxLevel: 3,
36
+ loadSidebar: true,
37
+ formatUpdated: '{MM}/{DD} {HH}:{mm}',
38
+ branchBasePath: 'https://raw.githubusercontent.com/samesystem/active_graphql/',
39
+ plugins: [
40
+ function (hook, vm) { // reasign any config value by param attribute
41
+ Object.assign(window.$docsify, getJsonFromUrl());
42
+ },
43
+
44
+ function (hook, vm) { // allow to change branch
45
+ if (!window.$docsify.branchBasePath || !window.$docsify.branch) {
46
+ return;
47
+ }
48
+
49
+ var branch = window.$docsify.branch;
50
+ var basePath = window.$docsify.branchBasePath + branch;
51
+ window.$docsify.basePath = basePath;
52
+ },
53
+
54
+ function (hook, vm) { // add edit page link
55
+ hook.beforeEach(function (html) {
56
+ var branch = window.$docsify.branch || 'master'
57
+ var url = 'https://github.com/samesystem/active_graphql/edit/' + branch + '/docs/' + vm.route.file
58
+ var editHtml = '[:memo: Edit Document](' + url + ')\n'
59
+ return html
60
+ + '\n\n----\n\n'
61
+ + editHtml
62
+ })
63
+ }
64
+ ]
65
+ }
66
+ </script>
67
+ <script src="https://unpkg.com/docsify/lib/docsify.js"></script>
68
+ <script src="https://unpkg.com/docsify/lib/plugins/search.min.js"></script>
69
+ </body>
70
+ </html>
data/docs/model.md ADDED
@@ -0,0 +1,464 @@
1
+ # Model
2
+
3
+ ## Setup
4
+
5
+ To create graphql model, you need to include `ActiveGraphql::Model` module in your ruby class like this:
6
+
7
+ ```ruby
8
+ class User
9
+ include ActiveGraphql::Model
10
+
11
+ active_graphql do |c|
12
+ c.url 'http://localhost:3000'
13
+ c.attributes :id, :first_name, :last_name
14
+ end
15
+ end
16
+ ```
17
+
18
+ Attributes also can be nested, like this:
19
+
20
+ ```ruby
21
+ class User
22
+ include ActiveGraphql::Model
23
+
24
+ active_graphql do |c|
25
+ c.attributes location: [:city, :country, :street]
26
+ end
27
+ end
28
+
29
+ User.find(3).location # { city: 'London', country: ... }
30
+ ```
31
+
32
+ ### active_graphql.url
33
+
34
+ Sets url where all GraphQL queries should go
35
+
36
+ ```ruby
37
+ class User
38
+ include ActiveGraphql::Model
39
+
40
+ active_graphql do |c|
41
+ c.url 'http://localhost:3000'
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### active_graphql.attributes
47
+
48
+ Sets attributes which can be fetched from graphql
49
+
50
+ ```ruby
51
+ class User
52
+ include ActiveGraphql::Model
53
+
54
+ active_graphql do |c|
55
+ c.attributes :id, :first_name, :last_name
56
+ end
57
+ end
58
+
59
+ User.find(3).first_name # => some name returned from graphql
60
+ ```
61
+
62
+ ### active_graphql.attribute
63
+
64
+ Sets attribute which can be fetched from graphql
65
+
66
+ ```ruby
67
+ class User
68
+ include ActiveGraphql::Model
69
+
70
+ active_graphql do |c|
71
+ c.attribute :name
72
+ end
73
+ end
74
+
75
+ User.find(3).name # => "John"
76
+ ```
77
+
78
+ #### nested attributes
79
+
80
+ You can have nested attributes. Nested values will be returned as hash:
81
+
82
+ ```ruby
83
+ class User
84
+ include ActiveGraphql::Model
85
+
86
+ active_graphql do |c|
87
+ c.attribute :id
88
+ c.attribute :location, [:lat, :long]
89
+ end
90
+ end
91
+
92
+ User.find(3).location #=> { lat: 25.0, long: 26.0 }
93
+ ```
94
+
95
+ #### decorated attributes
96
+
97
+ You can use decorator methods in order to modify model attribute values. It's very in combination with nested values
98
+
99
+ ```ruby
100
+ class User
101
+ include ActiveGraphql::Model
102
+
103
+ active_graphql do |c|
104
+ c.attribute :name, decorate_with: :make_fancy_name
105
+ end
106
+
107
+ def make_fancy_name(original_name)
108
+ "Mr. #{original_name}"
109
+ end
110
+ end
111
+
112
+ User.find(3).name #=> "Mr. John"
113
+ ```
114
+
115
+ ### active_graphql.resource_name
116
+
117
+ Sets attributes which can be fetched from graphql
118
+
119
+ ```ruby
120
+ class User
121
+ include ActiveGraphql::Model
122
+
123
+ active_graphql do |c|
124
+ c.resource_name :admin_user
125
+ c.attributes :id
126
+ end
127
+ end
128
+
129
+ User.where(name: 'John').to_graphql # => "query { adminUsers(name: "John") { id } }"
130
+ ```
131
+
132
+ ### active_graphql.primary_key
133
+
134
+ By default primary key is `id`, but you can change it like this:
135
+
136
+ ```ruby
137
+ class User
138
+ include ActiveGraphql::Model
139
+
140
+ active_graphql do |c|
141
+ c.primary_key :email
142
+ end
143
+ end
144
+
145
+ User.find('john@example.com') # will execute in GraphQL: 'query { user(email: "john@example.com") }'
146
+ ```
147
+
148
+ ## Methods
149
+
150
+ ### find
151
+
152
+ Use `find` method in order to find record:
153
+
154
+ ```ruby
155
+ user = User.find(5)
156
+ ```
157
+
158
+ ### update
159
+
160
+ Use `update` to update record on graphql side:
161
+
162
+ ```ruby
163
+ User.find(5).update(first_name: 'John') # => true or false
164
+ ```
165
+
166
+ ### update!
167
+
168
+ Use `update!` to update record on graphql side:
169
+
170
+ ```ruby
171
+ User.find(5).update!(first_name: 'John') # => true or exception
172
+ ```
173
+
174
+ ### destroy
175
+
176
+ Use `destroy` in order to delete record on graphql side:
177
+
178
+ ```ruby
179
+ User.find(5).destroy # => true or false
180
+ ```
181
+
182
+ ### create
183
+
184
+ to create model on graphql side simply use `create` method, like this:
185
+
186
+ ```ruby
187
+ user = User.create(first_name: 'John', last_name: 'Doe')
188
+ ```
189
+
190
+ ### create!
191
+
192
+ as in ActiveRecord, there is `create!` method which will raise error when create fails:
193
+
194
+ ```ruby
195
+ user = User.create!(first_name: 'John', last_name: 'Doe')
196
+ ```
197
+
198
+ ### where
199
+
200
+ Use `where` method in order to find multiple record:
201
+
202
+ ```ruby
203
+ users = User.where(name: 'John')
204
+ ```
205
+
206
+ ### merge
207
+
208
+ Use `merge` method in order to merge multiple queries:
209
+
210
+ ```ruby
211
+ # same as User.where(name: 'John', surname: 'Doe') :
212
+ users = User.where(name: 'John').merge(User.where(surname: 'Doe'))
213
+ ```
214
+
215
+ ### or
216
+
217
+ Use `or` method in order to query using "or" predicate:
218
+
219
+ ```ruby
220
+ # same as User.where(or: { name: 'John', surname: 'Doe' }) :
221
+ users = User.where(name: 'John').or(User.where(surname: 'Doe'))
222
+ ```
223
+
224
+ Keep in mind that your endpoint must support filtering by "or" key like this:
225
+
226
+ ```graphql
227
+ query {
228
+ users(filter: { or: { name: 'John', surname: 'Doe' } }) {
229
+ ...
230
+ }
231
+ }
232
+ ```
233
+
234
+ ### order
235
+
236
+ Use `order` when you need to sort results:
237
+
238
+ ```ruby
239
+ users.order(created_at: :desc)
240
+ ```
241
+
242
+ ### find_each
243
+
244
+ In order to iterate through multiple pages, you need to use `find_each` method
245
+
246
+ ```ruby
247
+ User.all.find_each do |user|
248
+ do_something(user)
249
+ end
250
+ ```
251
+
252
+ ### paginate
253
+
254
+ you can also paginate records:
255
+
256
+ ```ruby
257
+ User.paginate(page: 1, per_page: 3)
258
+ ```
259
+
260
+ ### page
261
+
262
+ you can also paginate records:
263
+
264
+ ```ruby
265
+ User.page(1)
266
+ ```
267
+
268
+ ### Selecting certain fields
269
+
270
+ You can select only attributes which you want to be selected from model, like this:
271
+
272
+ ```ruby
273
+ class User
274
+ include ActiveGraphql::Model
275
+
276
+ active_graphql do |c|
277
+ c.url 'http://example.com/graphql'
278
+ c.attributes :id, :first_name, location: %i[street city], name: :full_name
279
+ end
280
+
281
+ def self.main_data
282
+ select(:first_name, location: :city, name: :full_name)
283
+ end
284
+ end
285
+
286
+ User.main_data
287
+ ```
288
+
289
+ This will produce GraphQL:
290
+ ```graphql
291
+ query {
292
+ users {
293
+ firstName
294
+ location {
295
+ city
296
+ }
297
+ name {
298
+ fullName
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ ### defining custom queries
305
+
306
+ You can define your custom queries by adding class method, like this:
307
+
308
+ ```ruby
309
+ class User
310
+ include ActiveGraphql::Model
311
+
312
+ active_graphql do |c|
313
+ c.attributes :id
314
+ end
315
+
316
+ def self.with_custom
317
+ where(custom: true)
318
+ end
319
+ end
320
+
321
+ User.where(id: 1).with_custom
322
+ ```
323
+
324
+ this will produce GraphQL:
325
+
326
+ ```graphql
327
+ query {
328
+ users(filter: { id: 1, custom: true } ) {
329
+ id
330
+ }
331
+ }
332
+ ```
333
+
334
+ ### mutate
335
+
336
+ You can define your custom mutations by adding instance method, like this:
337
+
338
+ ```ruby
339
+ class User
340
+ include ActiveGraphql::Model
341
+
342
+ active_graphql do |c|
343
+ c.attributes :id, :first_name, :last_name
344
+ end
345
+
346
+ def update_name(first_name, last_name)
347
+ mutate(:update_name, input: { first_name: 'Fancy', last_name: 'Pants' })
348
+ end
349
+ end
350
+
351
+ User.last.update_name('Fancy', 'Pants')
352
+ ```
353
+
354
+ This will produce GraphQL:
355
+ ```graphql
356
+ mutation {
357
+ updateName(id: 99, input: { firstName: 'Fancy', lastName: 'Pants' }) {
358
+ id
359
+ firstName
360
+ lastName
361
+ ...
362
+ }
363
+ }
364
+ ```
365
+
366
+ ## Requirements for GraphQL server side
367
+
368
+ In order to make active_graphql work, server must met some conditions.
369
+
370
+ ### Naming requirements
371
+
372
+ Resource, attribute and field names must be in camelcase
373
+
374
+ #### Resource name requirements for CRUD actions
375
+
376
+ Let's say we have `BlogPost` resource, so CRUD actions should be named like this:
377
+ - `blogPost(id: ID!)` (aka, `show` action)
378
+ - `blogPosts(filter: FilterInput)` (aka, `index` action)
379
+ - `createBlogPost(input: SomeCreateInput!)` (aka, `create` action)
380
+ - `updateBlogPost(id: ID!, input: SomeUpdateInput!)` (aka, `update` action)
381
+ - `destroyBlogPost(id: ID!)` (aka, `destroy` action)
382
+
383
+ ### Requirements for Model#find methods
384
+
385
+ In order to make Model#find work, server must have resource in singular form with single `id: ID!` argument.
386
+
387
+ Example: `user(id: ID!)`
388
+
389
+ ### Requirements for Model#all, Model#find_each methods
390
+
391
+ In order to make Model#all and Model#find_each work, server must have resource in plural form and also response should be paginated.
392
+
393
+ Example:
394
+ ```
395
+ users(first: Integer, last: Integer, before: String, after: String) {
396
+ edges {
397
+ node {
398
+ ...
399
+ }
400
+ }
401
+ }
402
+ ```
403
+
404
+ ### Requirements for Model#where, Model#find_by methods
405
+
406
+ In order to make Model#where and Model#find_by work, server must have resource in plural form with `filter: SomeFilterInput` argument. Also resource must match requirements for Model#all too (see previous section)
407
+
408
+ Example:
409
+ ```
410
+ type UsersFilterInput {
411
+ firstName: String!
412
+ lastName: String!
413
+ }
414
+
415
+ users(filter: UserFilterInput) {
416
+ edges {
417
+ node {
418
+ ...
419
+ }
420
+ }
421
+ }
422
+ ```
423
+
424
+ ### Requirements for Model#or method
425
+
426
+ In order to make Model#or resouce must match requirements for `Model#where` method. Also `filter` input must have `or` argument
427
+
428
+ Example:
429
+ ```
430
+ type UsersFilterInput {
431
+ or: UsersOrFilterInput
432
+ groupId: [ID!],
433
+ name: String!
434
+ }
435
+
436
+ type UsersOrFilterInput {
437
+ groupId: [ID!],
438
+ name: String!
439
+ }
440
+
441
+ users(filter: UserFilterInput) {
442
+ edges {
443
+ node {
444
+ ...
445
+ }
446
+ }
447
+ }
448
+ ```
449
+
450
+ ### Requirements for Model#count
451
+
452
+ In order to make Model#where and Model#find_by work, server must have resource in plural form. This resource must have `total:Integer` **output** field:
453
+
454
+ Example:
455
+ ```
456
+ users() {
457
+ total
458
+ edges {
459
+ node {
460
+ ...
461
+ }
462
+ }
463
+ }
464
+ ```