graphiti 2.0.0.beta.4 → 2.0.0.beta.5
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/.github/workflows/docs.yml +4 -7
- data/CHANGELOG.md +14 -0
- data/docs/concepts/links.md +3 -3
- data/docs/upgrading.md +4 -9
- data/graphiti.gemspec +0 -18
- data/lib/generators/graphiti/resource_generator.rb +25 -1
- data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
- data/lib/graphiti/rails/rake_helpers.rb +42 -0
- data/lib/graphiti/spec_helpers/matchers.rb +196 -0
- data/lib/graphiti/spec_helpers/rspec.rb +1 -0
- data/lib/graphiti/spec_helpers.rb +1 -0
- data/lib/graphiti/version.rb +1 -1
- data/lib/tasks/graphiti.rake +7 -32
- data/website/docusaurus.config.js +67 -11
- data/website/src/css/custom.css +29 -2
- data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +0 -0
- data/website/static/1.13/assets/img/sunrise.png +0 -0
- data/website/static/1.13/assets/main.css +4 -4
- data/website/static/CNAME +1 -0
- metadata +7 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdd882c646fa88d0ac6522b7725ad6132b7ad890043822f5e2a2ce8af3f0fc4c
|
|
4
|
+
data.tar.gz: 687c64e9c5e6d2931a09bde9f5595b965e36f1a090d863fa17b9edf11559533f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1907226aa19ad065b0affe66f5ea22bb7149fd11aef92d225d7b433a5c67111bde2d7ef5911e633c5ea9059559ff903654c21f835099b5e15e72fdf7ab59cc9b
|
|
7
|
+
data.tar.gz: 91fd8e83a1452d29ffb45076b94a9eeb316c4e508a2f6565c5e079e6c54a184eede79da45ecb9d25f340f395551d907e10eb152e05666387792c434ed5fb4361
|
data/.github/workflows/docs.yml
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
name: Docs
|
|
2
2
|
|
|
3
|
-
# Publishes the documentation site from beta
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# baseUrl returns to "/" and add website/static/CNAME.
|
|
3
|
+
# Publishes the documentation site from beta. graphiti.dev now points at this
|
|
4
|
+
# repo via website/static/CNAME, so this deploy is the live site: the 1.x
|
|
5
|
+
# Jekyll site is frozen under website/static/1.13 and ships with every build,
|
|
6
|
+
# and graphiti-api.github.io no longer serves the domain.
|
|
8
7
|
on:
|
|
9
8
|
push:
|
|
10
9
|
branches: [beta]
|
|
@@ -39,8 +38,6 @@ jobs:
|
|
|
39
38
|
|
|
40
39
|
- name: Build
|
|
41
40
|
working-directory: website
|
|
42
|
-
env:
|
|
43
|
-
DOCS_BASE_URL: /graphiti/
|
|
44
41
|
run: npm run build
|
|
45
42
|
|
|
46
43
|
- name: Publish to gh-pages
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
+
# [2.0.0-beta.5](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2026-08-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* drop the relationship guard install notice ([c204312](https://github.com/graphiti-api/graphiti/commit/c20431214e126cb8e72c82022ee4c852b10a024b))
|
|
9
|
+
* keep rake task helpers out of the global namespace ([0a41a60](https://github.com/graphiti-api/graphiti/commit/0a41a6022fa4c9151948714c31db367aee330e22)), closes [graphiti-api/graphiti-rails#91](https://github.com/graphiti-api/graphiti-rails/issues/91)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* add rspec matchers for resource relationships and attributes ([70525f7](https://github.com/graphiti-api/graphiti/commit/70525f7e10c511aa0c867ecc873f63f1790b8247)), closes [graphiti-api/graphiti_spec_helpers#14](https://github.com/graphiti-api/graphiti_spec_helpers/issues/14)
|
|
15
|
+
* let the resource generator name the controller ([6ea7141](https://github.com/graphiti-api/graphiti/commit/6ea714181f3da011cae9f42477b4c3681a7a90ff)), closes [graphiti-api/graphiti-rails#53](https://github.com/graphiti-api/graphiti-rails/issues/53)
|
|
16
|
+
|
|
3
17
|
# [2.0.0-beta.4](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2026-08-07)
|
|
4
18
|
|
|
5
19
|
|
data/docs/concepts/links.md
CHANGED
|
@@ -170,11 +170,11 @@ end
|
|
|
170
170
|
|
|
171
171
|
### Pagination Links {#pagination-links}
|
|
172
172
|
|
|
173
|
-
Requesting
|
|
173
|
+
Requesting large collections can make for slow responses. [Pagination](https://jsonapi.org/format/#fetching-pagination) breaks the response into smaller pieces, and pagination links tell the client how to walk them. They can appear in a response two ways.
|
|
174
174
|
|
|
175
175
|
#### Showing by default {#pagination-links-showing-by-default}
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
Every collection response returns pagination links:
|
|
178
178
|
|
|
179
179
|
```ruby
|
|
180
180
|
Graphiti.configure do |c|
|
|
@@ -184,7 +184,7 @@ end
|
|
|
184
184
|
|
|
185
185
|
#### When requested {#pagination-links-when-requested}
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Links are rendered only when the request asks for them with `?pagination_links=true`. Worth doing when the collection is large: the `last` link needs a total count, so rendering links costs a `stat(:total, :count)` on every request that gets them.
|
|
188
188
|
|
|
189
189
|
```ruby
|
|
190
190
|
Graphiti.configure do |c|
|
data/docs/upgrading.md
CHANGED
|
@@ -13,9 +13,8 @@ Four things, and three of them fail loudly if you skip them.
|
|
|
13
13
|
|
|
14
14
|
**1. Drop three gems.** `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` are now part of `graphiti` itself.
|
|
15
15
|
|
|
16
|
-
```diff
|
|
17
|
-
#
|
|
18
|
-
+ gem "graphiti"
|
|
16
|
+
```diff title="Gemfile"
|
|
17
|
+
+ gem "graphiti", "~> 2.0.0.beta" # follows the betas and picks up 2.0 final when it ships
|
|
19
18
|
- gem "graphiti-rails"
|
|
20
19
|
- gem "graphiti_spec_helpers"
|
|
21
20
|
- gem "graphiti_errors"
|
|
@@ -31,18 +30,14 @@ class ApplicationController < ActionController::Base
|
|
|
31
30
|
end
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
If the controller already has `include Graphiti::Rails`, replace that line rather than adding to it. The old spelling still sets the controller up, so nothing breaks mid-upgrade, but it warns and goes away in the next major version.
|
|
33
|
+
If the controller already has `include Graphiti::Rails`, replace it with `include Graphiti::Rails::Controller`.
|
|
37
34
|
|
|
38
35
|
<details>
|
|
39
36
|
<summary>What the include actually brings, and what a controller without it loses</summary>
|
|
40
37
|
|
|
41
38
|
Until 2.0, Graphiti added itself to **every** controller in the application: an `around_action` wrapping each request in a Graphiti context, another wrapping it in the debugger, and a catch-all exception handler, on Devise controllers, admin controllers, HTML pages, everything.
|
|
42
39
|
|
|
43
|
-
`Graphiti::Rails::Controller` now bundles all of it, and
|
|
44
|
-
|
|
45
|
-
A controller without it gets no Graphiti context, no debugger, and none of Graphiti's exception handlers, so if a resource action sees an empty `Graphiti.context`, this include is what is missing.
|
|
40
|
+
`Graphiti::Rails::Controller` now bundles all of it, and including it is required. Including it in `ApplicationController` matches 1.x behavior. Including it in an API base class scopes it and leaves the rest of the app alone. A controller without it gets no Graphiti context, no debugger, and none of Graphiti's exception handlers, so if a resource action sees an empty `Graphiti.context`, this include is what is missing.
|
|
46
41
|
|
|
47
42
|
The class-level DSL travels with it, which is the one failure you see before a request is ever served:
|
|
48
43
|
|
data/graphiti.gemspec
CHANGED
|
@@ -18,24 +18,6 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.require_paths = ["lib"]
|
|
19
19
|
spec.required_ruby_version = ">= 3.2"
|
|
20
20
|
|
|
21
|
-
# TODO: remove once the 1.12 upgrade window has passed
|
|
22
|
-
spec.post_install_message = <<~MSG
|
|
23
|
-
Graphiti: relationship readable:/writable: guards are now enforced.
|
|
24
|
-
|
|
25
|
-
Symbols, strings, and procs passed to a relationship's readable/writable
|
|
26
|
-
guards were always interpreted as true and never actually called before this
|
|
27
|
-
conditional relationship change. They are now evaluated per-request, where
|
|
28
|
-
unreadable relationships are omitted from responses and includes, and unwritable
|
|
29
|
-
relationships reject writes.
|
|
30
|
-
|
|
31
|
-
To list every affected relationship in your app:
|
|
32
|
-
|
|
33
|
-
bin/rails runner 'puts Graphiti.guarded_relationships'
|
|
34
|
-
|
|
35
|
-
Audit these before deploying. Apps using schema.json will also see affected
|
|
36
|
-
relationships flagged as "became guarded" by the schema check.
|
|
37
|
-
MSG
|
|
38
|
-
|
|
39
21
|
spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
|
|
40
22
|
spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
|
|
41
23
|
spec.add_dependency "dry-types", ">= 0.15.0", "< 2.0"
|
|
@@ -32,6 +32,12 @@ module Graphiti
|
|
|
32
32
|
aliases: ["--model", "-m"],
|
|
33
33
|
desc: "Specify to use attributes from a particular model"
|
|
34
34
|
|
|
35
|
+
# No short alias: -c is already --omit-comments.
|
|
36
|
+
class_option :controller,
|
|
37
|
+
banner: "Name",
|
|
38
|
+
type: :string,
|
|
39
|
+
desc: "Generate the controller under this name instead of the resource's, e.g. Api::V1::Posts"
|
|
40
|
+
|
|
35
41
|
desc "This generator creates a resource file at app/resources, as well as corresponding controller/specs/route/etc"
|
|
36
42
|
def generate_all
|
|
37
43
|
generate_model
|
|
@@ -125,8 +131,23 @@ module Graphiti
|
|
|
125
131
|
defined?(::Responders)
|
|
126
132
|
end
|
|
127
133
|
|
|
134
|
+
# Api::V1::Posts, Api::V1::PostsController and api/v1/posts all name the
|
|
135
|
+
# same controller. Normalizing here means the class name, the file path and
|
|
136
|
+
# the route all come from one place.
|
|
137
|
+
def controller_class_name
|
|
138
|
+
@controller_class_name ||= begin
|
|
139
|
+
given = options[:controller]
|
|
140
|
+
base = given ? given.sub(/Controller\z/, "").camelize : model_klass.name
|
|
141
|
+
"#{base.pluralize}Controller"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def controller_path
|
|
146
|
+
controller_class_name.sub(/Controller\z/, "").underscore
|
|
147
|
+
end
|
|
148
|
+
|
|
128
149
|
def generate_controller
|
|
129
|
-
to = File.join("app/controllers",
|
|
150
|
+
to = File.join("app/controllers", "#{controller_path}_controller.rb")
|
|
130
151
|
template("controller.rb.erb", to)
|
|
131
152
|
end
|
|
132
153
|
|
|
@@ -142,6 +163,9 @@ module Graphiti
|
|
|
142
163
|
|
|
143
164
|
def generate_route
|
|
144
165
|
code = "resources :#{file_name.pluralize}"
|
|
166
|
+
# Rails would otherwise infer the controller from the route name, which
|
|
167
|
+
# is only right when the controller was named after the resource.
|
|
168
|
+
code << %(, controller: "#{controller_path}") if options[:controller]
|
|
145
169
|
code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
|
|
146
170
|
code << "\n"
|
|
147
171
|
inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% module_namespacing do -%>
|
|
2
|
-
class <%=
|
|
2
|
+
class <%= controller_class_name %> < ApplicationController
|
|
3
3
|
<%- if actions?('index') -%>
|
|
4
4
|
def index
|
|
5
5
|
<%= file_name.pluralize %> = <%= resource_klass %>.all(params)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "graphiti/rails/test_helpers"
|
|
2
|
+
|
|
3
|
+
module Graphiti
|
|
4
|
+
module Rails
|
|
5
|
+
# Rake's `namespace` takes a block, and a block does not open a new definee,
|
|
6
|
+
# so helpers defined inside one are defined on Object and turn up in every
|
|
7
|
+
# request spec in the host app. They live here to stay off that namespace.
|
|
8
|
+
module RakeHelpers
|
|
9
|
+
extend TestHelpers
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# ::Rails throughout, because bare Rails resolves to Graphiti::Rails here.
|
|
14
|
+
def session
|
|
15
|
+
@session ||= ActionDispatch::Integration::Session.new(::Rails.application)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setup_rails!
|
|
19
|
+
::Rails.application.eager_load!
|
|
20
|
+
::Rails.application.config.cache_classes = true
|
|
21
|
+
::Rails.application.config.action_controller.perform_caching = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def make_request(path, debug = false)
|
|
25
|
+
if path.split("/").length == 2
|
|
26
|
+
path = "#{ApplicationResource.endpoint_namespace}#{path}"
|
|
27
|
+
end
|
|
28
|
+
path << if path.include?("?")
|
|
29
|
+
"&cache=bust"
|
|
30
|
+
else
|
|
31
|
+
"?cache=bust"
|
|
32
|
+
end
|
|
33
|
+
path = "#{path}&debug=true" if debug
|
|
34
|
+
handle_request_exceptions do
|
|
35
|
+
headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
|
|
36
|
+
session.get(path.to_s, headers: headers)
|
|
37
|
+
end
|
|
38
|
+
JSON.parse(session.response.body)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module SpecHelpers
|
|
3
|
+
# Assertions against a resource's DSL. The schema specs cannot cover
|
|
4
|
+
# adapter-dependent options (primary key, foreign key, and so on) because
|
|
5
|
+
# the schema does not carry them; these read the resource config directly.
|
|
6
|
+
module Matchers
|
|
7
|
+
class BaseMatcher
|
|
8
|
+
GRAPHITI_OPTS = [].freeze
|
|
9
|
+
GRAPHITI_CONFIG_KEY = ""
|
|
10
|
+
EXPECTED_ACTION = ""
|
|
11
|
+
|
|
12
|
+
def description
|
|
13
|
+
"#{self.class::EXPECTED_ACTION} #{target}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def failure_message
|
|
17
|
+
"expected that #{resource.class} would #{self.class::EXPECTED_ACTION} #{target}\n#{@opt_failures.join("\n")}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def failure_message_when_negated
|
|
21
|
+
"expected that #{resource.class} would not #{self.class::EXPECTED_ACTION} #{target}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def opt_failure_message(opt, expected, actual)
|
|
25
|
+
"expected that #{opt} would be #{expected}, was #{actual}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def does_not_match?(resource)
|
|
29
|
+
!matches?(resource)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def matches?(resource)
|
|
33
|
+
@resource = resource
|
|
34
|
+
|
|
35
|
+
expected? && expected_opts?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def with_options(opts)
|
|
39
|
+
@opts = opts
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def actual_opts
|
|
46
|
+
self.class::GRAPHITI_OPTS & opts.keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def config
|
|
50
|
+
@config ||= resource.class.config[self.class::GRAPHITI_CONFIG_KEY][target]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def expected_opts?
|
|
54
|
+
return false unless config
|
|
55
|
+
|
|
56
|
+
actual_opts.map { |opt| assert_opt(opt) }.all?(true)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class RelationMatcher < BaseMatcher
|
|
61
|
+
GRAPHITI_OPTS = %i[primary_key foreign_key resource readable writable link single].freeze
|
|
62
|
+
GRAPHITI_CONFIG_KEY = :sideloads
|
|
63
|
+
EXPECTED_ACTION = ""
|
|
64
|
+
|
|
65
|
+
def initialize(target)
|
|
66
|
+
@target = target
|
|
67
|
+
@opts = {}
|
|
68
|
+
@opt_failures = []
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
attr_reader :target, :opts, :resource
|
|
74
|
+
|
|
75
|
+
def assert_opt(opt)
|
|
76
|
+
asserted_opt = opt == :resource ? :resource_class : opt
|
|
77
|
+
return true if config.send(asserted_opt) == opts[opt]
|
|
78
|
+
|
|
79
|
+
@opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def expected?
|
|
84
|
+
config && config.type == relation_name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# BelongsToMatcher -> :belongs_to, matching Sideload#type.
|
|
88
|
+
def relation_name
|
|
89
|
+
self.class.name.demodulize.gsub("Matcher", "").underscore.to_sym
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class BelongsToMatcher < RelationMatcher
|
|
94
|
+
EXPECTED_ACTION = "belong to"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class HasManyMatcher < RelationMatcher
|
|
98
|
+
EXPECTED_ACTION = "have many"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class HasOneMatcher < RelationMatcher
|
|
102
|
+
EXPECTED_ACTION = "have one"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class ResourceDSLMatcher < BaseMatcher
|
|
106
|
+
def initialize(target, type)
|
|
107
|
+
@target = target
|
|
108
|
+
@type = type
|
|
109
|
+
@opts = {}
|
|
110
|
+
@opt_failures = []
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
attr_reader :target, :type, :opts, :resource
|
|
116
|
+
|
|
117
|
+
def expected?
|
|
118
|
+
config && assert_type
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def assert_type
|
|
122
|
+
return true if config[:type] == type
|
|
123
|
+
|
|
124
|
+
@opt_failures << opt_failure_message("type", type, config[:type])
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def assert_opt(opt)
|
|
129
|
+
return true if config[opt] == opts[opt]
|
|
130
|
+
|
|
131
|
+
@opt_failures << opt_failure_message(opt, opts[opt], config[opt])
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class ExposeAttributeMatcher < ResourceDSLMatcher
|
|
137
|
+
GRAPHITI_OPTS = %i[writable readable sortable filterable].freeze
|
|
138
|
+
GRAPHITI_CONFIG_KEY = :attributes
|
|
139
|
+
EXPECTED_ACTION = "expose"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class FilterAttributeMatcher < ResourceDSLMatcher
|
|
143
|
+
GRAPHITI_OPTS = %i[allow deny single required allow_nil deny_empty].freeze
|
|
144
|
+
GRAPHITI_CONFIG_KEY = :filters
|
|
145
|
+
EXPECTED_ACTION = "filter"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @param [Symbol] relation
|
|
149
|
+
#
|
|
150
|
+
# @example expect(subject).to belong_to_resource(:user)
|
|
151
|
+
# @example expect(subject).to belong_to_resource(:user).with_options(foreign_key: :user_id, resource: UserResource)
|
|
152
|
+
# @example expect(subject).not_to belong_to_resource(:user)
|
|
153
|
+
def belong_to_resource(relation)
|
|
154
|
+
BelongsToMatcher.new(relation)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @param [Symbol] relation
|
|
158
|
+
#
|
|
159
|
+
# @example expect(subject).to have_many_resources(:posts)
|
|
160
|
+
# @example expect(subject).to have_many_resources(:posts).with_options(foreign_key: :post_id, resource: PostResource)
|
|
161
|
+
# @example expect(subject).not_to have_many_resources(:posts)
|
|
162
|
+
def have_many_resources(relation)
|
|
163
|
+
HasManyMatcher.new(relation)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param [Symbol] relation
|
|
167
|
+
#
|
|
168
|
+
# @example expect(subject).to have_one_resource(:post)
|
|
169
|
+
# @example expect(subject).to have_one_resource(:post).with_options(foreign_key: :post_id, resource: PostResource)
|
|
170
|
+
# @example expect(subject).not_to have_one_resource(:post)
|
|
171
|
+
def have_one_resource(relation)
|
|
172
|
+
HasOneMatcher.new(relation)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @param [Symbol] attribute
|
|
176
|
+
# @param [Symbol] type
|
|
177
|
+
#
|
|
178
|
+
# @example expect(subject).to expose_attribute(:name, :string)
|
|
179
|
+
# @example expect(subject).to expose_attribute(:name, :string).with_options(writable: false)
|
|
180
|
+
# @example expect(subject).not_to expose_attribute(:name, :string)
|
|
181
|
+
def expose_attribute(attribute, type)
|
|
182
|
+
ExposeAttributeMatcher.new(attribute, type)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @param [Symbol] attribute
|
|
186
|
+
# @param [Symbol] type
|
|
187
|
+
#
|
|
188
|
+
# @example expect(subject).to filter_attribute(:name, :string)
|
|
189
|
+
# @example expect(subject).to filter_attribute(:name, :string).with_options(allow_nil: false)
|
|
190
|
+
# @example expect(subject).not_to filter_attribute(:name, :string)
|
|
191
|
+
def filter_attribute(attribute, type)
|
|
192
|
+
FilterAttributeMatcher.new(attribute, type)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
data/lib/graphiti/version.rb
CHANGED
data/lib/tasks/graphiti.rake
CHANGED
|
@@ -1,52 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
include Graphiti::Rails::TestHelpers
|
|
3
|
-
|
|
4
|
-
def session
|
|
5
|
-
@session ||= ActionDispatch::Integration::Session.new(Rails.application)
|
|
6
|
-
end
|
|
1
|
+
require "graphiti/rails/rake_helpers"
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Rails.application.config.cache_classes = true
|
|
11
|
-
Rails.application.config.action_controller.perform_caching = false
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def make_request(path, debug = false)
|
|
15
|
-
if path.split("/").length == 2
|
|
16
|
-
path = "#{ApplicationResource.endpoint_namespace}#{path}"
|
|
17
|
-
end
|
|
18
|
-
path << if path.include?("?")
|
|
19
|
-
"&cache=bust"
|
|
20
|
-
else
|
|
21
|
-
"?cache=bust"
|
|
22
|
-
end
|
|
23
|
-
path = "#{path}&debug=true" if debug
|
|
24
|
-
handle_request_exceptions do
|
|
25
|
-
headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
|
|
26
|
-
session.get(path.to_s, headers: headers)
|
|
27
|
-
end
|
|
28
|
-
JSON.parse(session.response.body)
|
|
29
|
-
end
|
|
3
|
+
namespace :graphiti do
|
|
4
|
+
helpers = Graphiti::Rails::RakeHelpers
|
|
30
5
|
|
|
31
6
|
desc "Execute request without web server."
|
|
32
7
|
task :request, [:path, :debug] => [:environment] do |_, args|
|
|
33
|
-
setup_rails!
|
|
8
|
+
helpers.setup_rails!
|
|
34
9
|
Graphiti.logger = Graphiti.stdout_logger
|
|
35
10
|
Graphiti::Debugger.preserve = true
|
|
36
11
|
require "pp"
|
|
37
12
|
path, debug = args[:path], args[:debug]
|
|
38
13
|
puts "Graphiti Request: #{path}"
|
|
39
|
-
json = make_request(path, debug)
|
|
14
|
+
json = helpers.make_request(path, debug)
|
|
40
15
|
pp json
|
|
41
16
|
Graphiti::Debugger.flush if debug
|
|
42
17
|
end
|
|
43
18
|
|
|
44
19
|
desc "Execute benchmark without web server."
|
|
45
20
|
task :benchmark, [:path, :requests] => [:environment] do |_, args|
|
|
46
|
-
setup_rails!
|
|
21
|
+
helpers.setup_rails!
|
|
47
22
|
took = Benchmark.ms {
|
|
48
23
|
args[:requests].to_i.times do
|
|
49
|
-
make_request(args[:path])
|
|
24
|
+
helpers.make_request(args[:path])
|
|
50
25
|
end
|
|
51
26
|
}
|
|
52
27
|
puts "Took: #{(took / args[:requests].to_f).round(2)}ms"
|
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
const {themes} = require('prism-react-renderer');
|
|
3
3
|
|
|
4
|
+
const codeBlockTheme = (theme, backgroundColor) => ({
|
|
5
|
+
...theme,
|
|
6
|
+
plain: {...theme.plain, backgroundColor},
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Docusaurus prepends baseUrl to navbar and Link hrefs, but not to raw HTML
|
|
10
|
+
// like announcementBar content, so that markup has to build its own absolute
|
|
11
|
+
// paths. Always has a trailing slash.
|
|
12
|
+
const baseUrl = process.env.DOCS_BASE_URL || '/';
|
|
13
|
+
|
|
14
|
+
// The theme's strikethrough and underline on diff lines are inline styles, so
|
|
15
|
+
// CSS cannot undo them. Colour and the +/- signs already carry it.
|
|
16
|
+
const withoutDiffDecoration = (theme) => ({
|
|
17
|
+
...theme,
|
|
18
|
+
styles: theme.styles.map(({types, style}) =>
|
|
19
|
+
types.some((type) => type === 'inserted' || type === 'deleted')
|
|
20
|
+
? {types, style: {...style, textDecorationLine: undefined}}
|
|
21
|
+
: {types, style}
|
|
22
|
+
),
|
|
23
|
+
});
|
|
24
|
+
|
|
4
25
|
/** @type {import('@docusaurus/types').Config} */
|
|
5
26
|
const config = {
|
|
6
27
|
title: 'Graphiti',
|
|
7
28
|
tagline: 'Stylish Graph APIs',
|
|
8
29
|
favicon: 'img/favicon.ico',
|
|
9
30
|
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
31
|
+
// Apex, matching the CNAME. This feeds canonical tags and sitemap.xml, so it
|
|
32
|
+
// has to agree with the host actually serving the site or the two split
|
|
33
|
+
// search ranking between them.
|
|
34
|
+
url: 'https://graphiti.dev',
|
|
35
|
+
// graphiti.dev resolves to this repo, so the site serves from the root.
|
|
36
|
+
// DOCS_BASE_URL still overrides it for a build served from the project-pages
|
|
37
|
+
// path, which is what /graphiti/ was during the beta.
|
|
38
|
+
baseUrl,
|
|
14
39
|
organizationName: 'graphiti-api',
|
|
15
40
|
projectName: 'graphiti',
|
|
16
41
|
|
|
@@ -75,7 +100,7 @@ const config = {
|
|
|
75
100
|
{from: ['/cookbooks/customizing-sideloads'], to: '/topics/customizing-sideloads'},
|
|
76
101
|
{from: ['/cookbooks/hopping-relationships'], to: '/topics/hopping-relationships'},
|
|
77
102
|
|
|
78
|
-
{from: ['/js/introduction'], to: '/js'},
|
|
103
|
+
{from: ['/js/introduction'], to: '/js/'},
|
|
79
104
|
{
|
|
80
105
|
from: [
|
|
81
106
|
'/js/reads/index',
|
|
@@ -100,11 +125,24 @@ const config = {
|
|
|
100
125
|
to: '/js/writes',
|
|
101
126
|
},
|
|
102
127
|
],
|
|
128
|
+
|
|
129
|
+
// The 2.0 docs were staged under /graphiti/ for the whole beta, so
|
|
130
|
+
// those URLs are in the wild. createRedirects sees every generated
|
|
131
|
+
// route, which the static /1.13/ tree is not: that one is by hand.
|
|
132
|
+
createRedirects(existingPath) {
|
|
133
|
+
return [`/graphiti${existingPath}`];
|
|
134
|
+
},
|
|
103
135
|
},
|
|
104
136
|
],
|
|
105
137
|
],
|
|
106
138
|
|
|
107
139
|
themeConfig: {
|
|
140
|
+
announcementBar: {
|
|
141
|
+
// Changing the id un-dismisses the bar for everyone who has closed it.
|
|
142
|
+
id: 'graphiti-2-0-beta',
|
|
143
|
+
content: `Graphiti 2.0 is in beta, and the docs have been reorganized to describe it. <a href="${baseUrl}1.13/">Docs for 1.x</a>.`,
|
|
144
|
+
isCloseable: true,
|
|
145
|
+
},
|
|
108
146
|
colorMode: {
|
|
109
147
|
// Dark for a first-time visitor. Flip respectPrefersColorScheme to true
|
|
110
148
|
// to follow the OS setting instead, in which case defaultMode only
|
|
@@ -122,19 +160,37 @@ const config = {
|
|
|
122
160
|
position: 'right',
|
|
123
161
|
// 1.x stays as the original Jekyll site, frozen under /1.13.
|
|
124
162
|
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
163
|
+
// The pathname:// prefix is required. It is Docusaurus' escape hatch
|
|
164
|
+
// for a path that is served statically but is not an app route:
|
|
165
|
+
// without it the link renders as a react-router push, which finds no
|
|
166
|
+
// match and shows the 404 page even though the file is served fine.
|
|
167
|
+
// The prefix is stripped before baseUrl is applied, and it also makes
|
|
168
|
+
// the link count as external, hence target '_self' to keep it from
|
|
169
|
+
// opening in a new tab.
|
|
128
170
|
dropdownItemsAfter: [
|
|
129
|
-
{href: '
|
|
171
|
+
{href: 'pathname:///1.13/', label: '1.x', target: '_self'},
|
|
130
172
|
],
|
|
131
173
|
},
|
|
132
174
|
{href: 'https://github.com/graphiti-api/graphiti', label: 'GitHub', position: 'right'},
|
|
133
175
|
{href: 'https://discord.gg/wgqkMBsSRV', label: 'Discord', position: 'right'},
|
|
134
176
|
],
|
|
135
177
|
},
|
|
136
|
-
footer: {
|
|
137
|
-
|
|
178
|
+
footer: {
|
|
179
|
+
style: 'dark',
|
|
180
|
+
copyright: [
|
|
181
|
+
'Originally created by <a href="https://github.com/richmolj">Lee Richmond</a>,',
|
|
182
|
+
'<a href="https://github.com/wadetandy">Wade Tandy</a>, and',
|
|
183
|
+
'<a href="https://github.com/wagenet">Peter Wagenet</a>.',
|
|
184
|
+
'Maintained by <a href="https://github.com/jkeen">Jeff Keen</a>',
|
|
185
|
+
'with <a href="https://github.com/graphiti-api/graphiti/graphs/contributors">many contributors</a>.',
|
|
186
|
+
'<br />Graphiti is released under the MIT license.',
|
|
187
|
+
].join(' '),
|
|
188
|
+
},
|
|
189
|
+
prism: {
|
|
190
|
+
theme: withoutDiffDecoration(themes.oneLight),
|
|
191
|
+
darkTheme: withoutDiffDecoration(codeBlockTheme(themes.oneDark, '#21252b')),
|
|
192
|
+
additionalLanguages: ['ruby', 'bash', 'json', 'http', 'diff'],
|
|
193
|
+
},
|
|
138
194
|
},
|
|
139
195
|
};
|
|
140
196
|
|
data/website/src/css/custom.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
--docusaurus-highlighted-code-line-bg: rgba(255, 102, 217, 0.12);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
[data-theme='dark'] {
|
|
24
|
+
html[data-theme='dark'] {
|
|
25
25
|
--ifm-color-primary: #ff66d9;
|
|
26
26
|
--ifm-color-primary-dark: #ff45d1;
|
|
27
27
|
--ifm-color-primary-darker: #ff34cd;
|
|
@@ -32,10 +32,13 @@
|
|
|
32
32
|
|
|
33
33
|
--ifm-background-color: #292e33;
|
|
34
34
|
--ifm-navbar-background-color: #292e33;
|
|
35
|
-
--ifm-footer-background-color: #21252a;
|
|
36
35
|
--docusaurus-highlighted-code-line-bg: rgba(255, 102, 217, 0.2);
|
|
37
36
|
}
|
|
38
37
|
|
|
38
|
+
html[data-theme='dark'] .footer--dark {
|
|
39
|
+
--ifm-footer-background-color: #21252a;
|
|
40
|
+
}
|
|
41
|
+
|
|
39
42
|
.navbar__title { font-family: 'Bungee', cursive; font-weight: 400; font-size: 1.35rem; }
|
|
40
43
|
|
|
41
44
|
/* Black sets tight at display sizes and needs more room above it than a
|
|
@@ -56,3 +59,27 @@
|
|
|
56
59
|
vertical-align: baseline;
|
|
57
60
|
padding: 0.15em 0.3em;
|
|
58
61
|
}
|
|
62
|
+
|
|
63
|
+
/* The bar is full-bleed on every page, so it takes a tint of the brand pink
|
|
64
|
+
rather than the pink itself, which at that width reads as a promo banner.
|
|
65
|
+
Docusaurus hashes the class name, hence the prefix selector. */
|
|
66
|
+
.theme-announcement-bar {
|
|
67
|
+
background: #fdeef7;
|
|
68
|
+
color: #7a1758;
|
|
69
|
+
font-size: 1.1rem;
|
|
70
|
+
border-bottom: 1px solid rgba(224, 40, 157, 0.25);
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
[data-theme='dark'] .theme-announcement-bar {
|
|
75
|
+
background: #33272f;
|
|
76
|
+
color: #ffcbf1;
|
|
77
|
+
border-bottom-color: rgba(255, 102, 217, 0.25);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The link and the close button both carry their own colour, so they drift
|
|
81
|
+
from the bar's text unless told to inherit it. */
|
|
82
|
+
.theme-announcement-bar a,
|
|
83
|
+
.theme-announcement-bar button {
|
|
84
|
+
color: inherit;
|
|
85
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -14890,7 +14890,7 @@ a.header:hover {
|
|
|
14890
14890
|
position: fixed;
|
|
14891
14891
|
width: 100%;
|
|
14892
14892
|
/*background: url("https://www.toptal.com/designers/subtlepatterns/patterns/connectwork.png");*/
|
|
14893
|
-
background: url("/assets/img/fancy-cushion.png");
|
|
14893
|
+
background: url("/1.13/assets/img/fancy-cushion.png");
|
|
14894
14894
|
/*background: url("https://www.toptal.com/designers/subtlepatterns/patterns/dark-honeycomb.png");*/
|
|
14895
14895
|
border-bottom: 1px solid black;
|
|
14896
14896
|
box-shadow: 1px 1px 15px -2px black;
|
|
@@ -14976,7 +14976,7 @@ body {
|
|
|
14976
14976
|
color: rgb(76.5, 76.5, 76.5);
|
|
14977
14977
|
}
|
|
14978
14978
|
body.layout-home {
|
|
14979
|
-
background-image: url("/assets/img/home-bg.jpg");
|
|
14979
|
+
background-image: url("/1.13/assets/img/home-bg.jpg");
|
|
14980
14980
|
background-attachment: fixed;
|
|
14981
14981
|
background-position: center;
|
|
14982
14982
|
background-repeat: no-repeat;
|
|
@@ -15124,10 +15124,10 @@ h4 {
|
|
|
15124
15124
|
}
|
|
15125
15125
|
|
|
15126
15126
|
#cover-image.prod-bg {
|
|
15127
|
-
background-image: url("/
|
|
15127
|
+
background-image: url("/1.13/assets/img/sunrise.png") !important;
|
|
15128
15128
|
}
|
|
15129
15129
|
#cover-image.dev-bg {
|
|
15130
|
-
background-image: url("/assets/img/sunrise.png") !important;
|
|
15130
|
+
background-image: url("/1.13/assets/img/sunrise.png") !important;
|
|
15131
15131
|
}
|
|
15132
15132
|
|
|
15133
15133
|
.solution img {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
graphiti.dev
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphiti
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.beta.
|
|
4
|
+
version: 2.0.0.beta.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lee Richmond
|
|
@@ -359,6 +359,7 @@ files:
|
|
|
359
359
|
- lib/graphiti/rails/debugging.rb
|
|
360
360
|
- lib/graphiti/rails/exception_handlers.rb
|
|
361
361
|
- lib/graphiti/rails/railtie.rb
|
|
362
|
+
- lib/graphiti/rails/rake_helpers.rb
|
|
362
363
|
- lib/graphiti/rails/responders.rb
|
|
363
364
|
- lib/graphiti/rails/test_helpers.rb
|
|
364
365
|
- lib/graphiti/renderer.rb
|
|
@@ -400,6 +401,7 @@ files:
|
|
|
400
401
|
- lib/graphiti/spec_helpers/errors.rb
|
|
401
402
|
- lib/graphiti/spec_helpers/errors_proxy.rb
|
|
402
403
|
- lib/graphiti/spec_helpers/helpers.rb
|
|
404
|
+
- lib/graphiti/spec_helpers/matchers.rb
|
|
403
405
|
- lib/graphiti/spec_helpers/node.rb
|
|
404
406
|
- lib/graphiti/spec_helpers/rspec.rb
|
|
405
407
|
- lib/graphiti/stats/dsl.rb
|
|
@@ -459,7 +461,9 @@ files:
|
|
|
459
461
|
- website/static/1.13/assets/img/backend.gif
|
|
460
462
|
- website/static/1.13/assets/img/conformity.png
|
|
461
463
|
- website/static/1.13/assets/img/error_payload.png
|
|
464
|
+
- website/static/1.13/assets/img/fancy-cushion.png
|
|
462
465
|
- website/static/1.13/assets/img/gh.png
|
|
466
|
+
- website/static/1.13/assets/img/home-bg.jpg
|
|
463
467
|
- website/static/1.13/assets/img/lifecycle.gif
|
|
464
468
|
- website/static/1.13/assets/img/logo-500.png
|
|
465
469
|
- website/static/1.13/assets/img/logo.png
|
|
@@ -472,6 +476,7 @@ files:
|
|
|
472
476
|
- website/static/1.13/assets/img/rest2.gif
|
|
473
477
|
- website/static/1.13/assets/img/rest3.gif
|
|
474
478
|
- website/static/1.13/assets/img/rethink-rest-graffiti.jpg
|
|
479
|
+
- website/static/1.13/assets/img/sunrise.png
|
|
475
480
|
- website/static/1.13/assets/img/why.png
|
|
476
481
|
- website/static/1.13/assets/js/highlight.pack.js
|
|
477
482
|
- website/static/1.13/assets/main.css
|
|
@@ -550,6 +555,7 @@ files:
|
|
|
550
555
|
- website/static/1.13/tutorial/step_7.html
|
|
551
556
|
- website/static/1.13/tutorial/step_8.html
|
|
552
557
|
- website/static/1.13/tutorial/step_9.html
|
|
558
|
+
- website/static/CNAME
|
|
553
559
|
- website/static/assets/img/error_payload.png
|
|
554
560
|
- website/static/assets/img/legacy/legacy-0378a3bb39.png
|
|
555
561
|
- website/static/assets/img/legacy/legacy-05bbd3e5fd.png
|
|
@@ -572,14 +578,6 @@ homepage: https://github.com/graphiti-api/graphiti
|
|
|
572
578
|
licenses:
|
|
573
579
|
- MIT
|
|
574
580
|
metadata: {}
|
|
575
|
-
post_install_message: "Graphiti: relationship readable:/writable: guards are now enforced.\n\nSymbols,
|
|
576
|
-
strings, and procs passed to a relationship's readable/writable\nguards were always
|
|
577
|
-
interpreted as true and never actually called before this\nconditional relationship
|
|
578
|
-
change. They are now evaluated per-request, where\nunreadable relationships are
|
|
579
|
-
omitted from responses and includes, and unwritable\nrelationships reject writes.\n\nTo
|
|
580
|
-
list every affected relationship in your app:\n\n bin/rails runner 'puts Graphiti.guarded_relationships'\n\nAudit
|
|
581
|
-
these before deploying. Apps using schema.json will also see affected \nrelationships
|
|
582
|
-
flagged as \"became guarded\" by the schema check.\n"
|
|
583
581
|
rdoc_options: []
|
|
584
582
|
require_paths:
|
|
585
583
|
- lib
|