active_graphql 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ ```