graphiti 1.13.4 → 2.0.0.beta.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -37
- data/.standard.yml +1 -1
- data/Appraisals +0 -15
- data/CHANGELOG.md +7 -32
- data/UPGRADING.md +71 -0
- data/graphiti.gemspec +19 -1
- data/lib/graphiti/debugger.rb +6 -18
- data/lib/graphiti/errors.rb +19 -13
- data/lib/graphiti/query.rb +1 -0
- data/lib/graphiti/request_validators/validator.rb +1 -11
- data/lib/graphiti/resource/configuration.rb +0 -4
- data/lib/graphiti/resource/interface.rb +1 -21
- data/lib/graphiti/resource/persistence.rb +36 -56
- data/lib/graphiti/resource/polymorphism.rb +0 -10
- data/lib/graphiti/resource.rb +11 -2
- data/lib/graphiti/resource_proxy.rb +86 -11
- data/lib/graphiti/runner.rb +2 -1
- data/lib/graphiti/scope.rb +45 -73
- data/lib/graphiti/scoping/filter.rb +1 -3
- data/lib/graphiti/serializer.rb +1 -0
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +23 -25
- data/lib/graphiti/sideload.rb +37 -38
- data/lib/graphiti/types.rb +1 -5
- data/lib/graphiti/util/hash.rb +9 -2
- data/lib/graphiti/util/persistence.rb +39 -3
- data/lib/graphiti/version.rb +1 -1
- metadata +14 -8
- data/gemfiles/rails_5_2.gemfile +0 -18
- data/gemfiles/rails_5_2_graphiti_rails.gemfile +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a741f7953d1f54e730d2e801715f3b1d718044a4e0bd59cfd8bb3b364373e715
|
|
4
|
+
data.tar.gz: 99391699d407dfb92178cf851cce313d691ea930eb0ada81abe764b7a8310087
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77d02ef7c1a525c286d56677ef103f56c7ea80873f565c6db72d2208d887fac967c8f9cddc6a06f940c4a62c6d7faf58390f5331f18fd2a562aa855fff002afd
|
|
7
|
+
data.tar.gz: e4e83880da4609da20727c8e8bf7b21cddc6e7436d571218da2f874e682dfdbe03c6960799e12614716b936881a8af95ee0459d6458afcaf5f3818dae198543e
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -2,7 +2,7 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
branches: [main]
|
|
5
|
+
branches: [main, beta]
|
|
6
6
|
pull_request: {}
|
|
7
7
|
workflow_dispatch: {}
|
|
8
8
|
|
|
@@ -29,7 +29,6 @@ jobs:
|
|
|
29
29
|
fail-fast: false
|
|
30
30
|
matrix:
|
|
31
31
|
ruby:
|
|
32
|
-
- "2.7"
|
|
33
32
|
- "3.0"
|
|
34
33
|
- "3.1"
|
|
35
34
|
- "3.2"
|
|
@@ -37,10 +36,8 @@ jobs:
|
|
|
37
36
|
- "3.4"
|
|
38
37
|
gemfile:
|
|
39
38
|
- Gemfile
|
|
40
|
-
- gemfiles/rails_5_2.gemfile
|
|
41
39
|
- gemfiles/rails_6.gemfile
|
|
42
40
|
- gemfiles/rails_7.gemfile
|
|
43
|
-
- gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
44
41
|
- gemfiles/rails_6_graphiti_rails.gemfile
|
|
45
42
|
- gemfiles/rails_7_graphiti_rails.gemfile
|
|
46
43
|
- gemfiles/rails_7_1_graphiti_rails.gemfile
|
|
@@ -70,14 +67,10 @@ jobs:
|
|
|
70
67
|
# Skip some extra variants
|
|
71
68
|
- gemfile: Gemfile
|
|
72
69
|
appraisal: true
|
|
73
|
-
- gemfile: gemfiles/rails_5_2.gemfile
|
|
74
|
-
appraisal: false
|
|
75
70
|
- gemfile: gemfiles/rails_6.gemfile
|
|
76
71
|
appraisal: false
|
|
77
72
|
- gemfile: gemfiles/rails_7.gemfile
|
|
78
73
|
appraisal: false
|
|
79
|
-
- gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
80
|
-
appraisal: false
|
|
81
74
|
- gemfile: gemfiles/rails_6_graphiti_rails.gemfile
|
|
82
75
|
appraisal: false
|
|
83
76
|
- gemfile: gemfiles/rails_7_graphiti_rails.gemfile
|
|
@@ -92,30 +85,22 @@ jobs:
|
|
|
92
85
|
appraisal: false
|
|
93
86
|
|
|
94
87
|
# Rails 8.1 needs ruby 3.2 +
|
|
95
|
-
- gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile
|
|
96
|
-
ruby: 2.7
|
|
97
88
|
- gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile
|
|
98
89
|
ruby: 3.0
|
|
99
90
|
- gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile
|
|
100
91
|
ruby: 3.1
|
|
101
92
|
|
|
102
93
|
# Rails 8 needs ruby 3.2 +
|
|
103
|
-
- gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile
|
|
104
|
-
ruby: 2.7
|
|
105
94
|
- gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile
|
|
106
95
|
ruby: 3.0
|
|
107
96
|
- gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile
|
|
108
97
|
ruby: 3.1
|
|
109
98
|
|
|
110
99
|
# Rails 7.2 needs ruby 3.1 +
|
|
111
|
-
- gemfile: gemfiles/rails_7_2_graphiti_rails.gemfile
|
|
112
|
-
ruby: 2.7
|
|
113
100
|
- gemfile: gemfiles/rails_7_2_graphiti_rails.gemfile
|
|
114
101
|
ruby: 3.0
|
|
115
102
|
|
|
116
103
|
# Rails 7.1 needs ruby 3.0 +
|
|
117
|
-
- gemfile: gemfiles/rails_7_1_graphiti_rails.gemfile
|
|
118
|
-
ruby: 2.7
|
|
119
104
|
- gemfile: gemfiles/rails_7_1_graphiti_rails.gemfile
|
|
120
105
|
ruby: 3.0
|
|
121
106
|
|
|
@@ -133,27 +118,6 @@ jobs:
|
|
|
133
118
|
- gemfile: gemfiles/rails_6_graphiti_rails.gemfile
|
|
134
119
|
ruby: 3.4
|
|
135
120
|
|
|
136
|
-
# Rails 5 can't run on Ruby 3
|
|
137
|
-
- gemfile: gemfiles/rails_5_2.gemfile
|
|
138
|
-
ruby: 3.0
|
|
139
|
-
- gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
140
|
-
ruby: 3.0
|
|
141
|
-
- gemfile: gemfiles/rails_5_2.gemfile
|
|
142
|
-
ruby: 3.1
|
|
143
|
-
- gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
144
|
-
ruby: 3.1
|
|
145
|
-
- gemfile: gemfiles/rails_5_2.gemfile
|
|
146
|
-
ruby: 3.2
|
|
147
|
-
- gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
148
|
-
ruby: 3.2
|
|
149
|
-
- gemfile: gemfiles/rails_5_2.gemfile
|
|
150
|
-
ruby: 3.3
|
|
151
|
-
- gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
152
|
-
ruby: 3.3
|
|
153
|
-
- gemfile: gemfiles/rails_5_2.gemfile
|
|
154
|
-
ruby: 3.4
|
|
155
|
-
- gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile
|
|
156
|
-
ruby: 3.4
|
|
157
121
|
continue-on-error: ${{ matrix.ruby == 'ruby-head' }}
|
|
158
122
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
|
159
123
|
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
|
|
@@ -171,6 +135,8 @@ jobs:
|
|
|
171
135
|
publish:
|
|
172
136
|
name: Release
|
|
173
137
|
runs-on: ubuntu-latest
|
|
138
|
+
# Only main auto-releases. Beta releases are cut by manually running
|
|
139
|
+
# "Generate New Release" with the beta branch selected.
|
|
174
140
|
if: github.ref == 'refs/heads/main'
|
|
175
141
|
needs: [test]
|
|
176
142
|
steps:
|
|
@@ -178,4 +144,7 @@ jobs:
|
|
|
178
144
|
uses: benc-uk/workflow-dispatch@v1
|
|
179
145
|
with:
|
|
180
146
|
workflow: Generate New Release
|
|
147
|
+
# Without an explicit ref the dispatch runs against the default
|
|
148
|
+
# branch rather than the branch that triggered it.
|
|
149
|
+
ref: ${{ github.ref }}
|
|
181
150
|
token: ${{ secrets.GITHUB_TOKEN }}
|
data/.standard.yml
CHANGED
data/Appraisals
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
appraise "rails-5_2" do
|
|
2
|
-
gem "rails", "~> 5.2.0"
|
|
3
|
-
gem "rspec-rails"
|
|
4
|
-
gem "sqlite3", "~> 1.4.0"
|
|
5
|
-
gem "database_cleaner"
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
appraise "rails-5_2-graphiti-rails" do
|
|
9
|
-
gem "rails", "~> 5.2.0"
|
|
10
|
-
gem "rspec-rails"
|
|
11
|
-
gem "sqlite3", "~> 1.4.0"
|
|
12
|
-
gem "database_cleaner"
|
|
13
|
-
gem "graphiti-rails", "~> 0.4.0"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
1
|
appraise "rails-6" do
|
|
17
2
|
gem "rails", "~> 6.0"
|
|
18
3
|
gem "rspec-rails"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,43 +1,18 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# [2.0.0-beta.1](https://github.com/graphiti-api/graphiti/compare/v1.12.2...v2.0.0-beta.1) (2026-07-30)
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
###
|
|
7
|
-
|
|
8
|
-
* resolve inline when already on a sideload pool thread ([2757dde](https://github.com/graphiti-api/graphiti/commit/2757ddee5728224540c7a3bfa33583fe71c64703))
|
|
9
|
-
* scope debugger chunks to the request rather than the class ([be1d833](https://github.com/graphiti-api/graphiti/commit/be1d83346e4303eaa700c0cbda6b4c60ddf587e7))
|
|
10
|
-
|
|
11
|
-
## [1.13.3](https://github.com/graphiti-api/graphiti/compare/v1.13.2...v1.13.3) (2026-08-09)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
### Bug Fixes
|
|
15
|
-
|
|
16
|
-
* reject invalid page parameters ([#537](https://github.com/graphiti-api/graphiti/issues/537)) ([fbb5683](https://github.com/graphiti-api/graphiti/commit/fbb5683393301e17ffcea84990ac5a95bdd3e15f)), closes [#347](https://github.com/graphiti-api/graphiti/issues/347)
|
|
17
|
-
* treat empty polymorphic configuration as unset ([#538](https://github.com/graphiti-api/graphiti/issues/538)) ([a27eeea](https://github.com/graphiti-api/graphiti/commit/a27eeea6eeb72749376c1f7959cadc89012142b9)), closes [#199](https://github.com/graphiti-api/graphiti/issues/199)
|
|
18
|
-
|
|
19
|
-
## [1.13.2](https://github.com/graphiti-api/graphiti/compare/v1.13.1...v1.13.2) (2026-08-07)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Bug Fixes
|
|
23
|
-
|
|
24
|
-
* drop the relationship guard install notice ([65b8be6](https://github.com/graphiti-api/graphiti/commit/65b8be65455a7d034c9dca082434c1dbfe610a7a))
|
|
25
|
-
|
|
26
|
-
## [1.13.1](https://github.com/graphiti-api/graphiti/compare/v1.13.0...v1.13.1) (2026-08-02)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
### Bug Fixes
|
|
30
|
-
|
|
31
|
-
* accept a single value for array filters ([#517](https://github.com/graphiti-api/graphiti/issues/517)) ([23f3e80](https://github.com/graphiti-api/graphiti/commit/23f3e80b583b0bf0843d8bb9334970ae56d83299))
|
|
32
|
-
* typecast null values in filter lists ([#515](https://github.com/graphiti-api/graphiti/issues/515)) ([3dcf4c8](https://github.com/graphiti-api/graphiti/commit/3dcf4c868abb12eb6b6d59cd0814b227d4838ece))
|
|
6
|
+
### Features
|
|
33
7
|
|
|
34
|
-
|
|
8
|
+
* drop Ruby 2.7 and Rails 5.2 support ([e905ddb](https://github.com/graphiti-api/graphiti/commit/e905ddb5c842299ffbfe5ec3c4ff98ec9b603a51))
|
|
9
|
+
* the model you inspect is the model that saves ([#465](https://github.com/graphiti-api/graphiti/issues/465)) ([a905fff](https://github.com/graphiti-api/graphiti/commit/a905fffc3a2aa5e663ff4cadcd632e68f053c81a))
|
|
35
10
|
|
|
36
11
|
|
|
37
|
-
###
|
|
12
|
+
### BREAKING CHANGES
|
|
38
13
|
|
|
39
|
-
*
|
|
40
|
-
*
|
|
14
|
+
* around_persistence hooks receive the assigned model instead of the attributes hash. Move attribute-hash modifications to before_attributes, or set values on the model. Custom create/update overrides that should receive a pre-assigned model must accept an assigned_model: keyword. See UPGRADING.md
|
|
15
|
+
* Ruby >= 3.0 / Rails >= 6 are now required.
|
|
41
16
|
|
|
42
17
|
## [1.12.2](https://github.com/graphiti-api/graphiti/compare/v1.12.1...v1.12.2) (2026-07-29)
|
|
43
18
|
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Upgrading Graphiti
|
|
2
|
+
|
|
3
|
+
## 1.x to 2.0
|
|
4
|
+
|
|
5
|
+
### Requirements
|
|
6
|
+
|
|
7
|
+
Graphiti 2.0 requires Ruby 3.0+ and Rails 6.0+ (when using Rails).
|
|
8
|
+
|
|
9
|
+
### The model you inspect is the model that saves
|
|
10
|
+
|
|
11
|
+
Proxies returned by `build` and `find` now apply the request payload lazily, and expose the resulting model before anything is written:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# Creates
|
|
15
|
+
resource = MyResource.build(params)
|
|
16
|
+
resource.data # unsaved model, attributes applied
|
|
17
|
+
resource.data.valid? # inspect before committing to anything
|
|
18
|
+
resource.save # persists that same instance
|
|
19
|
+
|
|
20
|
+
# Updates
|
|
21
|
+
resource = MyResource.find(params)
|
|
22
|
+
resource.assign_attributes(params)
|
|
23
|
+
resource.data.changed # dirty tracking works
|
|
24
|
+
resource.update
|
|
25
|
+
|
|
26
|
+
# or assign and save in one call, Rails-style
|
|
27
|
+
resource = MyResource.find(params)
|
|
28
|
+
resource.update(params)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`assign_attributes` is idempotent per payload, validation still runs before assignment, and the instance you inspect is the instance that saves.
|
|
32
|
+
|
|
33
|
+
### Breaking: around_persistence receives the model, not the attributes hash
|
|
34
|
+
|
|
35
|
+
To make the above hold, attributes are assigned to the model once, before the persistence hooks fire. `around_persistence` now wraps the save of an already-assigned model and receives that model:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# 1.x
|
|
39
|
+
def do_around_persistence(attributes)
|
|
40
|
+
attributes[:tenant_id] = current_tenant.id
|
|
41
|
+
model = yield
|
|
42
|
+
model.log_saved!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 2.0
|
|
46
|
+
def do_around_persistence(model)
|
|
47
|
+
model.tenant_id = current_tenant.id # last chance to touch the model before save, inside the transaction
|
|
48
|
+
saved = yield
|
|
49
|
+
saved.log_saved!
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To migrate, move attribute-hash modifications to `before_attributes` (which still receives the mutable hash, before assignment), or set the value on the model as above. Hooks that only wrap their yield - transactions, timing, post-save side effects - need no changes. Graphiti 1.x releases warn at runtime when a hook would be affected.
|
|
54
|
+
|
|
55
|
+
`before/around/after_attributes` and `before/around/after_save` are unchanged.
|
|
56
|
+
|
|
57
|
+
### Breaking: custom create/update overrides and pre-assigned models
|
|
58
|
+
|
|
59
|
+
If a resource overrides `create` or `update` and callers inspect the model before saving, the override must accept the already-assigned model:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
def create(attributes, meta = nil, assigned_model: nil)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Without the keyword, saving a proxy whose model was inspected raises `Graphiti::Errors::AssignedModelNotSupported` rather than silently discarding changes made to the inspected instance. Overridden resources used only via plain `save` are unaffected.
|
|
66
|
+
|
|
67
|
+
### Fine print
|
|
68
|
+
|
|
69
|
+
- If you inspect the model before saving, the attributes callbacks run at inspection time (in your controller, outside the save transaction). On the plain `save` path they run inside the transaction, at the same point as 1.x.
|
|
70
|
+
- Writable guards judge persisted state: a guard asking for the model gets a fresh build/find, never the current request's unsaved changes. A payload cannot influence its own authorization.
|
|
71
|
+
- Sideposted child models are still built and assigned during save; `data` exposes the pre-assigned root model only.
|
data/graphiti.gemspec
CHANGED
|
@@ -16,7 +16,25 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.bindir = "exe"
|
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
18
18
|
spec.require_paths = ["lib"]
|
|
19
|
-
spec.required_ruby_version = ">=
|
|
19
|
+
spec.required_ruby_version = ">= 3.0"
|
|
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
|
|
20
38
|
|
|
21
39
|
spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
|
|
22
40
|
spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
|
data/lib/graphiti/debugger.rb
CHANGED
|
@@ -4,22 +4,11 @@
|
|
|
4
4
|
module Graphiti
|
|
5
5
|
class Debugger
|
|
6
6
|
class << self
|
|
7
|
-
attr_accessor :enabled, :debug_models, :preserve, :pry
|
|
7
|
+
attr_accessor :enabled, :chunks, :debug_models, :preserve, :pry
|
|
8
8
|
end
|
|
9
|
-
|
|
10
|
-
CHUNKS = :__graphiti_debugger_chunks
|
|
11
|
-
private_constant :CHUNKS
|
|
9
|
+
self.chunks = []
|
|
12
10
|
|
|
13
11
|
class << self
|
|
14
|
-
# Pool threads inherit this through the thread-local copy Scope#future_with_context makes, so their chunks reach the right request.
|
|
15
|
-
def chunks
|
|
16
|
-
Thread.current[CHUNKS] ||= Concurrent::Array.new
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def chunks=(value)
|
|
20
|
-
Thread.current[CHUNKS] = value
|
|
21
|
-
end
|
|
22
|
-
|
|
23
12
|
def on_data(name, start, stop, id, payload)
|
|
24
13
|
return [] unless enabled
|
|
25
14
|
|
|
@@ -190,15 +179,14 @@ module Graphiti
|
|
|
190
179
|
end
|
|
191
180
|
|
|
192
181
|
def graph_statements
|
|
193
|
-
|
|
194
|
-
statements.each do |chunk|
|
|
182
|
+
@chunks.each do |chunk|
|
|
195
183
|
if (parent = chunk[:parent])
|
|
196
|
-
relevant =
|
|
184
|
+
relevant = chunks.find { |c| c[:resource] == parent }
|
|
197
185
|
relevant[:children].unshift(chunk) if relevant
|
|
198
186
|
end
|
|
199
187
|
end
|
|
200
|
-
|
|
201
|
-
|
|
188
|
+
@chunks.reject! { |c| !!c[:parent] }
|
|
189
|
+
@chunks
|
|
202
190
|
end
|
|
203
191
|
|
|
204
192
|
def chunk_to_hash(chunk)
|
data/lib/graphiti/errors.rb
CHANGED
|
@@ -84,6 +84,25 @@ module Graphiti
|
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
class AssignedModelNotSupported < Base
|
|
88
|
+
def initialize(resource_class, method_name)
|
|
89
|
+
@resource_class = resource_class
|
|
90
|
+
@method_name = method_name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def message
|
|
94
|
+
<<~MSG
|
|
95
|
+
#{@resource_class}: the model was already assigned via #assign_attributes (or #data before save), but ##{@method_name} is overridden without a parameter to receive it, so changes made to the unsaved model would be silently lost.
|
|
96
|
+
|
|
97
|
+
Accept and use the assigned model in your override:
|
|
98
|
+
|
|
99
|
+
def #{@method_name}(attributes, meta = nil, assigned_model: nil)
|
|
100
|
+
|
|
101
|
+
or persist with #save alone, without inspecting the model first. See UPGRADING.md.
|
|
102
|
+
MSG
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
87
106
|
class RemoteWrite < Base
|
|
88
107
|
def initialize(resource_class)
|
|
89
108
|
@resource_class = resource_class
|
|
@@ -467,19 +486,6 @@ module Graphiti
|
|
|
467
486
|
end
|
|
468
487
|
end
|
|
469
488
|
|
|
470
|
-
class InvalidWrapModel < Base
|
|
471
|
-
def initialize(resource_class, model)
|
|
472
|
-
@resource_class = resource_class
|
|
473
|
-
@model = model
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def message
|
|
477
|
-
<<~MSG
|
|
478
|
-
#{@resource_class.name}.wrap was given a #{@model.class}, but this resource is for #{@resource_class.model} (subclasses are fine).
|
|
479
|
-
MSG
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
|
|
483
489
|
class ResourceEndpointConflict < Base
|
|
484
490
|
def initialize(path, action, resource_a, resource_b)
|
|
485
491
|
@path = path
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Graphiti
|
|
|
4
4
|
class Query
|
|
5
5
|
attr_reader :resource, :association_name, :params, :action
|
|
6
6
|
|
|
7
|
+
# TODO: make the trailing optionals keywords once the satellite gems are rolled in - see Runner#query for the nil-padding this forces
|
|
7
8
|
def initialize(resource, params, association_name = nil, nested_include = nil, parents = [], action = nil)
|
|
8
9
|
@resource = resource
|
|
9
10
|
@association_name = association_name
|
|
@@ -11,15 +11,12 @@ module Graphiti
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def validate
|
|
14
|
-
validate_page
|
|
15
|
-
|
|
16
14
|
# Right now, all requests - even reads - go through the validator
|
|
17
15
|
# In the future these should have their own validation logic, but
|
|
18
16
|
# for now we can just bypass
|
|
19
|
-
return
|
|
17
|
+
return true unless @params.has_key?(:data)
|
|
20
18
|
|
|
21
19
|
resource = @root_resource
|
|
22
|
-
|
|
23
20
|
if @params[:data].has_key?(:type)
|
|
24
21
|
if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
|
|
25
22
|
if @root_resource.type != meta_type && @root_resource.polymorphic?
|
|
@@ -50,13 +47,6 @@ module Graphiti
|
|
|
50
47
|
|
|
51
48
|
private
|
|
52
49
|
|
|
53
|
-
def validate_page
|
|
54
|
-
page = @params[:page]
|
|
55
|
-
return if page.nil? || page.respond_to?(:each_pair)
|
|
56
|
-
|
|
57
|
-
@errors.add(:page, :invalid, message: "must be an object")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
50
|
def process_relationships(resource, relationships, payload_path)
|
|
61
51
|
relationships.each_key do |name|
|
|
62
52
|
unless resource.class.sideload(name.to_sym)
|
|
@@ -50,31 +50,11 @@ module Graphiti
|
|
|
50
50
|
def build(params, base_scope = nil)
|
|
51
51
|
validate_request!(params)
|
|
52
52
|
runner = Runner.new(self, params)
|
|
53
|
-
runner.proxy(base_scope, single: true, raise_on_missing: true)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Wrap models fetched outside graphiti so they render like any other proxy
|
|
57
|
-
def wrap(models, base_scope = nil)
|
|
58
|
-
validate_wrap_models!(models)
|
|
59
|
-
runner = Runner.new(self, {}, nil, :find)
|
|
60
|
-
runner.proxy(base_scope, bypass_required_filters: true).tap do |proxy|
|
|
61
|
-
proxy.data = models
|
|
62
|
-
end
|
|
53
|
+
runner.proxy(base_scope, single: true, raise_on_missing: true, assign_action: :create)
|
|
63
54
|
end
|
|
64
55
|
|
|
65
56
|
private
|
|
66
57
|
|
|
67
|
-
# Skip polymorphic parents - resource_for_model raises a better error for unknown children
|
|
68
|
-
def validate_wrap_models!(models)
|
|
69
|
-
return if abstract_class? || (polymorphic? && !polymorphic_child?)
|
|
70
|
-
|
|
71
|
-
[models].flatten.compact.each do |model|
|
|
72
|
-
unless model.is_a?(self.model)
|
|
73
|
-
raise Errors::InvalidWrapModel.new(self, model)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
58
|
def caching_options
|
|
79
59
|
{cache: @cache_resource, cache_expires_in: @cache_expires_in, cache_tag: @cache_tag}
|
|
80
60
|
end
|
|
@@ -69,46 +69,57 @@ module Graphiti
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
# +model_instance+ is the already-built model from a previous #assign
|
|
73
|
+
# (see ResourceProxy#assign_attributes). When given, attributes are
|
|
74
|
+
# applied to it rather than to a freshly built/found model.
|
|
75
|
+
def assign(assign_params, meta = nil, action_name = nil, model_instance: nil)
|
|
76
|
+
# Only update strips :id (it identifies the record to find) - a create
|
|
77
|
+
# payload may legitimately carry a client-supplied id to assign.
|
|
78
|
+
if action_name == :update
|
|
79
|
+
id = assign_params[:id]
|
|
80
|
+
assign_params = assign_params.except(:id)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
run_callbacks :attributes, action_name, assign_params, meta do |params|
|
|
84
|
+
model_instance ||= if action_name == :update
|
|
85
|
+
self.class._find(id: id).data
|
|
86
|
+
else
|
|
87
|
+
call_with_meta(:build, model, meta)
|
|
88
|
+
end
|
|
89
|
+
call_with_meta(:assign_attributes, model_instance, params, meta)
|
|
90
|
+
model_instance
|
|
91
|
+
end
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
model_instance
|
|
94
|
+
end
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
# Attributes are assigned before the persistence callbacks fire, so
|
|
97
|
+
# around_persistence receives the assigned model - its pre-yield
|
|
98
|
+
# position is the last chance to touch the model before save, inside
|
|
99
|
+
# the transaction. Modify attributes in before_attributes instead.
|
|
100
|
+
def create(create_params, meta = nil, assigned_model: nil)
|
|
101
|
+
model_instance = assigned_model || assign(create_params, meta, :create)
|
|
84
102
|
|
|
103
|
+
run_callbacks :persistence, :create, model_instance, meta do
|
|
85
104
|
run_callbacks :save, :create, model_instance, meta do
|
|
86
105
|
model_instance = call_with_meta(:save, model_instance, meta)
|
|
87
106
|
end
|
|
88
107
|
|
|
89
108
|
model_instance
|
|
90
109
|
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def update(update_params, meta = nil)
|
|
94
|
-
model_instance = nil
|
|
95
|
-
id = update_params[:id]
|
|
96
|
-
update_params = update_params.except(:id)
|
|
97
|
-
|
|
98
|
-
snapshot = attributes_snapshot(:update, update_params)
|
|
99
110
|
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
model_instance
|
|
112
|
+
end
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
call_with_meta(:assign_attributes, model_instance, params, meta)
|
|
106
|
-
model_instance
|
|
107
|
-
end
|
|
114
|
+
def update(update_params, meta = nil, assigned_model: nil)
|
|
115
|
+
model_instance = assigned_model || assign(update_params, meta, :update)
|
|
108
116
|
|
|
117
|
+
run_callbacks :persistence, :update, model_instance, meta do
|
|
109
118
|
run_callbacks :save, :update, model_instance, meta do
|
|
110
119
|
model_instance = call_with_meta(:save, model_instance, meta)
|
|
111
120
|
end
|
|
121
|
+
|
|
122
|
+
model_instance
|
|
112
123
|
end
|
|
113
124
|
|
|
114
125
|
model_instance
|
|
@@ -140,37 +151,6 @@ module Graphiti
|
|
|
140
151
|
|
|
141
152
|
private
|
|
142
153
|
|
|
143
|
-
# In Graphiti 2.0, attributes are assigned to the model before the
|
|
144
|
-
# persistence hooks fire, so hash modifications made by an
|
|
145
|
-
# around_persistence hook before its yield will no longer be applied.
|
|
146
|
-
# Snapshot-and-compare detects exactly those hooks: the comparison
|
|
147
|
-
# happens before any attributes-phase callback has run, so hooks that
|
|
148
|
-
# only wrap their yield never trigger the warning.
|
|
149
|
-
def attributes_snapshot(action, params)
|
|
150
|
-
return unless around_persistence_hooks?(action)
|
|
151
|
-
Util::Hash.deep_dup(params)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def around_persistence_hooks?(action)
|
|
155
|
-
hooks = self.class.config[:callbacks][:persistence].try(:[], :around) || []
|
|
156
|
-
hooks.any? { |hook| hook[:only].include?(action) }
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def warn_attributes_mutated_in_around_persistence(action, snapshot, params)
|
|
160
|
-
return if snapshot.nil? || snapshot == params
|
|
161
|
-
|
|
162
|
-
changed_keys = (snapshot.keys | params.keys).reject { |key| snapshot[key] == params[key] }
|
|
163
|
-
Graphiti::DEPRECATOR.warn(<<~MSG)
|
|
164
|
-
#{self.class}'s around_persistence hook modified the attributes hash before yield (changed keys: #{changed_keys.map(&:inspect).join(", ")}). In Graphiti 2.0, attributes are assigned to the model before persistence hooks run, and these modifications will be silently ignored. Move the modification to a hook that runs before assignment:
|
|
165
|
-
|
|
166
|
-
before_attributes do |attributes|
|
|
167
|
-
attributes[#{changed_keys.first.inspect}] = ...
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
or set the value on the model itself in a before_save hook. See UPGRADING.md in the Graphiti 2.0 release.
|
|
171
|
-
MSG
|
|
172
|
-
end
|
|
173
|
-
|
|
174
154
|
def run_callbacks(kind, action, *args)
|
|
175
155
|
fire_around_callbacks(kind, action, *args) do |*yieldargs|
|
|
176
156
|
fire_callbacks(kind, :before, action, *yieldargs)
|
|
@@ -8,8 +8,6 @@ module Graphiti
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def serializer_for(model)
|
|
11
|
-
return super unless self.class.polymorphic?
|
|
12
|
-
|
|
13
11
|
if polymorphic_child?
|
|
14
12
|
serializer
|
|
15
13
|
else
|
|
@@ -19,14 +17,10 @@ module Graphiti
|
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
def associate_all(*args)
|
|
22
|
-
return super unless self.class.polymorphic?
|
|
23
|
-
|
|
24
20
|
_associate(:associate_all, *args)
|
|
25
21
|
end
|
|
26
22
|
|
|
27
23
|
def associate(*args)
|
|
28
|
-
return super unless self.class.polymorphic?
|
|
29
|
-
|
|
30
24
|
_associate(:associate, *args)
|
|
31
25
|
end
|
|
32
26
|
|
|
@@ -40,8 +34,6 @@ module Graphiti
|
|
|
40
34
|
|
|
41
35
|
module ClassMethods
|
|
42
36
|
def inherited(klass)
|
|
43
|
-
return super unless polymorphic?
|
|
44
|
-
|
|
45
37
|
klass.type = nil
|
|
46
38
|
klass.model = klass.infer_model
|
|
47
39
|
klass.endpoint = klass.infer_endpoint
|
|
@@ -50,8 +42,6 @@ module Graphiti
|
|
|
50
42
|
end
|
|
51
43
|
|
|
52
44
|
def sideload(name)
|
|
53
|
-
return super unless polymorphic?
|
|
54
|
-
|
|
55
45
|
if (split_on = name.to_s.split(/^on__/)).length > 1
|
|
56
46
|
on_type, name = split_on[1].split("--").map(&:to_sym)
|
|
57
47
|
end
|