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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f23c59b6750dc9a8dcaefaba6d6250a9deac2676c33b01930b0c715767b85a1
4
- data.tar.gz: 2252b0e870042723a30af9844d6006d8587f14228d82bc3eafd36c4b72b2fc8c
3
+ metadata.gz: a741f7953d1f54e730d2e801715f3b1d718044a4e0bd59cfd8bb3b364373e715
4
+ data.tar.gz: 99391699d407dfb92178cf851cce313d691ea930eb0ada81abe764b7a8310087
5
5
  SHA512:
6
- metadata.gz: '0579733a14742e30623cb686c0ba0c925f34458501ee6a7b322da2a6aa0fe7a85a4ccc422bf969858ed5eae306e65f3c2fb4e554995db51b7437910bd46237b6'
7
- data.tar.gz: aa3fcd0b1e671594e23bb006d126e55366633038b48c53332940f1ba5cfbf4782995cd4556867ad707bb9b13440d045208cc51b66d24b2bc070019d9eb9e108b
6
+ metadata.gz: 77d02ef7c1a525c286d56677ef103f56c7ea80873f565c6db72d2208d887fac967c8f9cddc6a06f940c4a62c6d7faf58390f5331f18fd2a562aa855fff002afd
7
+ data.tar.gz: e4e83880da4609da20727c8e8bf7b21cddc6e7436d571218da2f874e682dfdbe03c6960799e12614716b936881a8af95ee0459d6458afcaf5f3818dae198543e
@@ -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
@@ -1,6 +1,6 @@
1
1
  fix: true
2
2
  parallel: true
3
- ruby_version: 2.7
3
+ ruby_version: 3.0
4
4
 
5
5
  ignore:
6
6
  - 'vendor/**/*'
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
- ## [1.13.4](https://github.com/graphiti-api/graphiti/compare/v1.13.3...v1.13.4) (2026-08-20)
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
- ### Bug Fixes
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
- # [1.13.0](https://github.com/graphiti-api/graphiti/compare/v1.12.2...v1.13.0) (2026-07-30)
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
- ### Features
12
+ ### BREAKING CHANGES
38
13
 
39
- * add Resource.wrap as an easy way to use graphiti serialization on models fetched via other means (and not via graphiti's finders) ([#513](https://github.com/graphiti-api/graphiti/issues/513)) ([fcd19e2](https://github.com/graphiti-api/graphiti/commit/fcd19e2d023091d85947dbdd89259b587e0f582b))
40
- * deprecate mutating attributes in around_persistence hooks ([#514](https://github.com/graphiti-api/graphiti/issues/514)) [skip ci] ([8410ab0](https://github.com/graphiti-api/graphiti/commit/8410ab0a916077601cf85d8260a05ef9098149eb))
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 = ">= 2.7"
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"
@@ -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
- statements = chunks
194
- statements.each do |chunk|
182
+ @chunks.each do |chunk|
195
183
  if (parent = chunk[:parent])
196
- relevant = statements.find { |c| c[:resource] == parent }
184
+ relevant = chunks.find { |c| c[:resource] == parent }
197
185
  relevant[:children].unshift(chunk) if relevant
198
186
  end
199
187
  end
200
- statements.reject! { |c| !!c[:parent] }
201
- statements
188
+ @chunks.reject! { |c| !!c[:parent] }
189
+ @chunks
202
190
  end
203
191
 
204
192
  def chunk_to_hash(chunk)
@@ -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
@@ -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 errors.blank? unless @params.has_key?(:data)
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)
@@ -21,10 +21,6 @@ module Graphiti
21
21
  send(:prepend, Polymorphism)
22
22
  end
23
23
 
24
- def polymorphic?
25
- polymorphic.present?
26
- end
27
-
28
24
  def type=(val)
29
25
  val = val&.to_sym
30
26
  if (val = super)
@@ -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
- def create(create_params, meta = nil)
73
- model_instance = nil
74
- snapshot = attributes_snapshot(:create, create_params)
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
- run_callbacks :persistence, :create, create_params, meta do
77
- warn_attributes_mutated_in_around_persistence(:create, snapshot, create_params)
93
+ model_instance
94
+ end
78
95
 
79
- run_callbacks :attributes, :create, create_params, meta do |params|
80
- model_instance = call_with_meta(:build, model, meta)
81
- call_with_meta(:assign_attributes, model_instance, params, meta)
82
- model_instance
83
- end
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
- run_callbacks :persistence, :update, update_params, meta do
101
- warn_attributes_mutated_in_around_persistence(:update, snapshot, update_params)
111
+ model_instance
112
+ end
102
113
 
103
- run_callbacks :attributes, :update, update_params, meta do |params|
104
- model_instance = self.class._find(id: id).data
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