graphiti 2.0.0.beta.12 → 2.0.0

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: 6e73d0272cfabe57d3eb0cbc82ad7199dea624afead6856ca72c1107857fa144
4
- data.tar.gz: 795ddb1e77c8f61d2e74c66c333ccc4acd5c37ccb6aa3b4c7cb816f52dfe1b71
3
+ metadata.gz: ac45688980e3b5a1b21c320161e5b8e67610dbcc2e0b4379f072cc86d8ff453f
4
+ data.tar.gz: f1d4398684f5b44872251db6cbe8155a80b5c96388ebb42b4af45fa17373035c
5
5
  SHA512:
6
- metadata.gz: f5ff1c0630702209372d0512afd16d29c87d6f561c551e08a5080bd25efa8b3bae465d33729129289a3ade41db781b779bffd753b55f6cf7cf02389662fa835e
7
- data.tar.gz: '0785ff7bd32570ef7bf588c7287b4704d7f9daf50bafb47f0739f02b054086a297cef4427364f818b5ab540a72935447bead5898957f597a400a40b435f4a81f'
6
+ metadata.gz: 47b24e17dafad601b9ac9cf0898a7a838f9a4cedef37e88fd579e0a3b30cd4e14ac322e0b71bc0d0d2a168f04881f9cb2cbcc4d90984a0944440a04f533ca189
7
+ data.tar.gz: 582d454ccaf706f46308dec57bbfe5db9b44663f84383c482fde1d23bc7e02dfe28045009d03e3607e19d9070f8e206dda99c79d42a0a78ebab88c8d9bd17b2b
@@ -2,7 +2,7 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main, beta]
5
+ branches: [main, 1.x, beta]
6
6
  pull_request: {}
7
7
  workflow_dispatch: {}
8
8
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,142 @@
1
1
  graphiti changelog
2
2
 
3
+ # [2.0.0](https://github.com/graphiti-api/graphiti/compare/v1.13.3...v2.0.0) (2026-09-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * a subclass redeclaring a relationship reaches its serializer ([976dbc4](https://github.com/graphiti-api/graphiti/commit/976dbc47e7bbf951da925a13a34f1bc9679470f8))
9
+ * accept a single value for array filters ([#517](https://github.com/graphiti-api/graphiti/issues/517)) ([ee685b6](https://github.com/graphiti-api/graphiti/commit/ee685b625e967a8225d48b486bd093e3a6970176))
10
+ * always render included when the client asked to include ([8691b3f](https://github.com/graphiti-api/graphiti/commit/8691b3fd08f3cae0481ba72839911fdc71ef5634))
11
+ * apply sideloads at definition once setup! has run ([39ef393](https://github.com/graphiti-api/graphiti/commit/39ef39383520816319282854c0ae99677bcd5127)), closes [#524](https://github.com/graphiti-api/graphiti/issues/524)
12
+ * autolink = true no longer clobbers an inherited :on_demand ([2d26f8c](https://github.com/graphiti-api/graphiti/commit/2d26f8cb567e82a16918345de9114b8ceef64523))
13
+ * bridge the last 1.x names that died with a bare NameError ([34e1dbd](https://github.com/graphiti-api/graphiti/commit/34e1dbd3e34bb8e6be56292c51fcb32674f75995))
14
+ * build the entity map when the root query is, not on first use ([134d18b](https://github.com/graphiti-api/graphiti/commit/134d18b12b9ebb6903cca813209f60e5626b25a6))
15
+ * coerce debug flags from env strings ([46be6aa](https://github.com/graphiti-api/graphiti/commit/46be6aa05d4e2a9e5d6b2562f223b4b05582bb39))
16
+ * compare each concurrency path against itself in the table to better illustrate the performance wins ([d0f0e92](https://github.com/graphiti-api/graphiti/commit/d0f0e92860b3b4affe3fca6dbb9b871fe8a1374f))
17
+ * compare the endpoint id to the payload id as strings ([62051d7](https://github.com/graphiti-api/graphiti/commit/62051d7531432357db7bc464cce77ccc55168d07))
18
+ * drop the relationship guard install notice ([c204312](https://github.com/graphiti-api/graphiti/commit/c20431214e126cb8e72c82022ee4c852b10a024b))
19
+ * evaluate writable guards under the action being performed ([d56b86a](https://github.com/graphiti-api/graphiti/commit/d56b86a5ffaac4106aaea1e2ee5400069a4205fe))
20
+ * **generators:** stop injecting a routes host into config/application.rb ([db92d06](https://github.com/graphiti-api/graphiti/commit/db92d0649ca0246662a86b1172229eaa2a817d2b))
21
+ * give client-caused errors real detail and stop 500ing on bad query params ([35acc3e](https://github.com/graphiti-api/graphiti/commit/35acc3e6c78f68eeeb05728ea16264b136455c30)), closes [#531](https://github.com/graphiti-api/graphiti/issues/531)
22
+ * keep a belongs_to linkage when another include shares its name ([2c9c7c7](https://github.com/graphiti-api/graphiti/commit/2c9c7c7ac06cd319ac95e1445eb55a0322841de2))
23
+ * keep a customized sideload's instances out of entity dedup ([b1beee3](https://github.com/graphiti-api/graphiti/commit/b1beee3288fae2b20b009983aaa2f6c71189e2ca))
24
+ * keep deep filters narrow when an include path repeats ([833a10b](https://github.com/graphiti-api/graphiti/commit/833a10be120a4f2cd615ad05f750c7f246d62801))
25
+ * 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)
26
+ * keep request state in fiber storage so it survives into child fibers ([6569935](https://github.com/graphiti-api/graphiti/commit/6569935c2140d4dd91cd6d4a7f9ec7c1bd05c888))
27
+ * let an abstract resource declare relationships ([6036392](https://github.com/graphiti-api/graphiti/commit/6036392ebd611c7ed492beb3fe5754433eac9ffd)), closes [#453](https://github.com/graphiti-api/graphiti/issues/453)
28
+ * make each appraisal test the Rails version it is named for ([9e00652](https://github.com/graphiti-api/graphiti/commit/9e00652e124a411a2916d0fef52dc4fd2dc59020))
29
+ * only register the jsonapi mime type when it is missing ([88401ab](https://github.com/graphiti-api/graphiti/commit/88401abc8c77f1e859b4b8da3f9574c75834eb43))
30
+ * raise a more helpful named error when belongs_to linkage reads an unselected foreign key ([f59ea0b](https://github.com/graphiti-api/graphiti/commit/f59ea0bda94b388b1c2c0ae0e50b258f8ce0eb11))
31
+ * read the links param from the query, not the context object ([3b934cb](https://github.com/graphiti-api/graphiti/commit/3b934cb25be049e1e80ba400123da944cfd23d04))
32
+ * reject a non-object data payload instead of raising TypeError ([51603b8](https://github.com/graphiti-api/graphiti/commit/51603b854270715b80a75d9aa05a4020e6bbdce5))
33
+ * reject invalid page parameters ([#537](https://github.com/graphiti-api/graphiti/issues/537)) ([bb3698b](https://github.com/graphiti-api/graphiti/commit/bb3698b52911c658c5d023ad8fd55b5106195001)), closes [#347](https://github.com/graphiti-api/graphiti/issues/347)
34
+ * remove the unused ActiveRecord adapter create and update ([b66fb8e](https://github.com/graphiti-api/graphiti/commit/b66fb8e5033b2ed7c35b5784cb8bcac67ceaa87f))
35
+ * require active_support so graphiti boots without Rails ([d431a03](https://github.com/graphiti-api/graphiti/commit/d431a0328fb4071429f39f4c69cda9c6cee308f2))
36
+ * require rescue_registry 1.1 for the fiber storage fix ([ac63996](https://github.com/graphiti-api/graphiti/commit/ac63996767d26adccf7ef63d63ac15b591a4a45a))
37
+ * resolve inline when already on a sideload pool thread ([11c9de7](https://github.com/graphiti-api/graphiti/commit/11c9de7fb4bfc2d83b02aaba3e58c1f94c84cec4))
38
+ * scope debugger chunks to the request rather than the class ([9a77896](https://github.com/graphiti-api/graphiti/commit/9a77896c6e56d23b89d0a2aef358d8e8e0c28c54))
39
+ * stop assigning the same child twice when a resource repeats in the include path ([588503b](https://github.com/graphiti-api/graphiti/commit/588503b109508495d8170373d80019c6971f7d9c))
40
+ * surface the first child error from concurrent polymorphic sideloads ([5a08c0e](https://github.com/graphiti-api/graphiti/commit/5a08c0edab2bc1ee0c87c6abbe1067297569411e))
41
+ * treat empty polymorphic configuration as unset ([#538](https://github.com/graphiti-api/graphiti/issues/538)) ([5c17899](https://github.com/graphiti-api/graphiti/commit/5c1789993fbb4638d4443cf014b8bef358addb80)), closes [#199](https://github.com/graphiti-api/graphiti/issues/199)
42
+ * typecast null values in filter lists ([#515](https://github.com/graphiti-api/graphiti/issues/515)) ([3756fd1](https://github.com/graphiti-api/graphiti/commit/3756fd12c48d2fb3cf5e2d40c38aab92f12218b0))
43
+ * Update some missing sections and terms after renames, register unsupported pagination error ([39cff19](https://github.com/graphiti-api/graphiti/commit/39cff19f6f1f73126514c8b18234a18ad5eb7379))
44
+ * with_options accepts the predicate backed relationship options ([71ab441](https://github.com/graphiti-api/graphiti/commit/71ab4418bccc4cff7e6cc10151c1728641d25d34))
45
+
46
+
47
+ ### Code Refactoring
48
+
49
+ * bridge the remaining 1.x names ([3d80ef9](https://github.com/graphiti-api/graphiti/commit/3d80ef98cf765a152032b7fed89b3cdc10be23dc))
50
+
51
+
52
+ ### Features
53
+
54
+ * add full set of commented-out Resource defaults in ApplicationResource when using the generator ([367678e](https://github.com/graphiti-api/graphiti/commit/367678eb3c2c737331f8edb00e00e70483a778b4))
55
+ * Add graphiti:audit task to audit resources for issues ([84436ea](https://github.com/graphiti-api/graphiti/commit/84436eab7c7d0713ec6f355b62ba8b06913ee31a))
56
+ * 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)
57
+ * add schema rake tasks, so checking a schema no longer means running the suite ([6f4783d](https://github.com/graphiti-api/graphiti/commit/6f4783d5654659125b7eb358bf1175b6feaacb7c))
58
+ * belongs_to renders resource linkage by default ([024824d](https://github.com/graphiti-api/graphiti/commit/024824d319619811ade886c72ce345cfeb584dbc)), closes [#168](https://github.com/graphiti-api/graphiti/issues/168) [#185](https://github.com/graphiti-api/graphiti/issues/185) [#167](https://github.com/graphiti-api/graphiti/issues/167) [#167](https://github.com/graphiti-api/graphiti/issues/167)
59
+ * carry CurrentAttributes into concurrent sideloads ([7092096](https://github.com/graphiti-api/graphiti/commit/7092096f6d99ad277cc56817c616d946f655d55b))
60
+ * carry the assigned model on the resource, not through override signatures ([8ad848d](https://github.com/graphiti-api/graphiti/commit/8ad848d9bcd24cd63efb67e8ce32d5c8f3cfe147))
61
+ * declare Graphiti's client errors in rescue_responses ([e369824](https://github.com/graphiti-api/graphiti/commit/e369824d6e11b671ffd212caad3d0a43a7ceefcf))
62
+ * dedupe under concurrency and across distinct resources ([800b1a2](https://github.com/graphiti-api/graphiti/commit/800b1a2bf9bb59702efd00afe8e4f54ae06b500e))
63
+ * deduplicate sideloaded entities across include paths ([0c2829a](https://github.com/graphiti-api/graphiti/commit/0c2829a634e9b96e2403ff168be9a48c6a663c78))
64
+ * deprecate SpecHelpers::Sugar in favor of the full helper names ([b155849](https://github.com/graphiti-api/graphiti/commit/b155849f55fe84b2d0911fa4681d82682dfa39ae))
65
+ * drop Ruby 2.7 and Rails 5.2 support ([e905ddb](https://github.com/graphiti-api/graphiti/commit/e905ddb5c842299ffbfe5ec3c4ff98ec9b603a51))
66
+ * fold allow_nil and deny_empty into one blanks: filter option ([a108403](https://github.com/graphiti-api/graphiti/commit/a108403e5cd87fd2fdf177da30c4c4f97b9fe750))
67
+ * fold graphiti_spec_helpers into graphiti ([da955a1](https://github.com/graphiti-api/graphiti/commit/da955a1e090ba44be31bf043ed90baa6661a4772))
68
+ * fold graphiti-rails into graphiti ([7740f8a](https://github.com/graphiti-api/graphiti/commit/7740f8a5803f1fbf02ef4e27d8baa6e0809d2b43)), closes [graphiti-rails#52](https://github.com/graphiti-rails/issues/52)
69
+ * **generators:** explicit rendering by default, respond_with only when Responders is included ([70741fe](https://github.com/graphiti-api/graphiti/commit/70741fef2a3a585bbef32c3e535027ff610fda77))
70
+ * handle exceptions with rescue_registry, fold in graphiti_errors ([e48171c](https://github.com/graphiti-api/graphiti/commit/e48171c117e7b8141fb6f966a972b0c1cd50c5ab))
71
+ * in-tree plain ruby and sinatra examples ([b92c6d9](https://github.com/graphiti-api/graphiti/commit/b92c6d9af22f759cc1f1eaccc250ecd04901e2aa))
72
+ * 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)
73
+ * mark deprecated settings in the generated ApplicationResource ([52b0642](https://github.com/graphiti-api/graphiti/commit/52b0642e689a16d14dacaf4e28a2641f10774cf0))
74
+ * move link rendering onto the resource as page_links and relationship_links ([57878d1](https://github.com/graphiti-api/graphiti/commit/57878d14fdfc4741e8d418c8edbc4a8687335843))
75
+ * move typecast_reads onto the resource and let the railtie own Rails config ([236e970](https://github.com/graphiti-api/graphiti/commit/236e9706d7d8f08e9640c5c22ac596e2f7466849))
76
+ * omit relationships that render no ids and no link ([916aa8c](https://github.com/graphiti-api/graphiti/commit/916aa8c9c596c5d9b6c802c5ab13b766a13d1e70))
77
+ * one link mode per relationship, cascading from relationship_links ([f0e1da1](https://github.com/graphiti-api/graphiti/commit/f0e1da1e13a8d12b47458495b378779f5687ffe2))
78
+ * point connection pool timeouts at the sizing formula and add an audit task to check ([6e017c6](https://github.com/graphiti-api/graphiti/commit/6e017c60fd45338e4e6c8527adebfd46be4bfd8d))
79
+ * raise MissingRelationshipMethod when rendering reads an association the model does not define ([1c41cee](https://github.com/graphiti-api/graphiti/commit/1c41cee787b4f747d2906b2230f8fb926e3d887d))
80
+ * rename always_include_resource_ids to resource_ids, with a belongs_to_resource_ids_by_default setting ([8b11151](https://github.com/graphiti-api/graphiti/commit/8b111516822f6fc6e3675b59941688791cb34e74))
81
+ * require Ruby 3.2 and Rails 7.1 ([635b249](https://github.com/graphiti-api/graphiti/commit/635b249a99f7d1ef726ae2328707ee6210cc0608))
82
+ * ship the Rails application template in this repo ([76d5329](https://github.com/graphiti-api/graphiti/commit/76d53291f94c576792e3649044784f88fbaa47a7))
83
+ * split validate_endpoints into validate_requests and validate_links ([100a370](https://github.com/graphiti-api/graphiti/commit/100a370deec6a0e6ae165bdf77e50b6b14d068d3))
84
+ * take error text from locale keys ([d01fd06](https://github.com/graphiti-api/graphiti/commit/d01fd06fc1ede8c3dd798ec48de4d5b01645d87e)), closes [#216](https://github.com/graphiti-api/graphiti/issues/216)
85
+ * 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))
86
+ * write every error code Graphiti renders into the generated locale file ([15f19c1](https://github.com/graphiti-api/graphiti/commit/15f19c1dbe56559d9d041fa50e2cc5ea7b2dcf4b))
87
+
88
+
89
+ ### Performance Improvements
90
+
91
+ * fingerprint the checkout being measured, not the working tree ([99656e1](https://github.com/graphiti-api/graphiti/commit/99656e17ca63c4057013fa9727b72d1e6c157086))
92
+ * memoize whether a belongs_to renders ids from its foreign key ([dcb60b2](https://github.com/graphiti-api/graphiti/commit/dcb60b2096c414756ccff534773d2e45594f0144))
93
+ * nest the entity map instead of keying it by an array ([20bf625](https://github.com/graphiti-api/graphiti/commit/20bf6259d0347b02568b7d47054bb799e69629c3))
94
+ * only deduplicate resources that two include paths can both reach ([744f014](https://github.com/graphiti-api/graphiti/commit/744f014249382bc25d5ea43f692778ad7adc25c3))
95
+ * record latest performance stats ([7380ef9](https://github.com/graphiti-api/graphiti/commit/7380ef912e9f3b99618bf47ed04e06d1efad4e6c))
96
+ * resolve a lone sideload inline rather than through the pool ([3415592](https://github.com/graphiti-api/graphiti/commit/3415592d75900389a8f11f53b65ab399e297e4a9))
97
+ * reuse the resolved data and proxies when building cache keys ([8a17350](https://github.com/graphiti-api/graphiti/commit/8a17350542ba0062fdcddd115afe8041b62198c7))
98
+ * skip the on-demand link strip when no relationship renders links on demand ([cf86a8c](https://github.com/graphiti-api/graphiti/commit/cf86a8c249077d9f9d5a13de49989d102bbaed04))
99
+ * skip the promise machinery when nothing is sideloaded ([d879923](https://github.com/graphiti-api/graphiti/commit/d879923070a35d1a0e908b2d04746aa81effb262))
100
+ * skip zipping a lone sideload promise ([7ef2cea](https://github.com/graphiti-api/graphiti/commit/7ef2cea654c48175504105f907b5b651c9fcdaf3))
101
+ * stop building a key for every record an association is checked against ([ccb5f71](https://github.com/graphiti-api/graphiti/commit/ccb5f71dab092988998a5f0c286ba65206e4fa35))
102
+
103
+
104
+ ### BREAKING CHANGES
105
+
106
+ * nothing removed, everything warns and goes away in 3.0. Except `include GraphitiErrors`, which now raises, as rescue_registry replaced it, so there's nothing to point it at.
107
+ * graphiti_errors is no longer a dependency and must be removed from the Gemfile, along with any `include GraphitiErrors`.
108
+
109
+ GraphitiErrors::Validation::Serializer is now Graphiti::ErrorSerializers::Validation, and GraphitiErrors.enable!/disable! becomes handle_request_exceptions. 409 responses now report code "conflict" and title "Conflict Error".
110
+ * Ruby >= 3.2 and Rails >= 7.1 are now required.
111
+ * remove graphiti-rails from your Gemfile. Controllers serving Graphiti resources must `include Graphiti::Rails::Controller` — previously every controller received it whether it wanted it or not. Graphiti::Responders is now Graphiti::Rails::Responders.
112
+ * remove graphiti_spec_helpers from your Gemfile. Prefer Graphiti::SpecHelpers and "graphiti/spec_helpers/rspec"; the old namespace and require paths still resolve, warn, and are removed in 3.0.
113
+ * 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
114
+ * Ruby >= 3.0 / Rails >= 6 are now required.
115
+
116
+ # [2.0.0-beta.13](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.12...v2.0.0-beta.13) (2026-08-31)
117
+
118
+
119
+ ### Bug Fixes
120
+
121
+ * compare the endpoint id to the payload id as strings ([62051d7](https://github.com/graphiti-api/graphiti/commit/62051d7531432357db7bc464cce77ccc55168d07))
122
+ * evaluate writable guards under the action being performed ([d56b86a](https://github.com/graphiti-api/graphiti/commit/d56b86a5ffaac4106aaea1e2ee5400069a4205fe))
123
+ * **generators:** stop injecting a routes host into config/application.rb ([db92d06](https://github.com/graphiti-api/graphiti/commit/db92d0649ca0246662a86b1172229eaa2a817d2b))
124
+ * keep a belongs_to linkage when another include shares its name ([2c9c7c7](https://github.com/graphiti-api/graphiti/commit/2c9c7c7ac06cd319ac95e1445eb55a0322841de2))
125
+ * keep deep filters narrow when an include path repeats ([833a10b](https://github.com/graphiti-api/graphiti/commit/833a10be120a4f2cd615ad05f750c7f246d62801))
126
+ * keep request state in fiber storage so it survives into child fibers ([6569935](https://github.com/graphiti-api/graphiti/commit/6569935c2140d4dd91cd6d4a7f9ec7c1bd05c888))
127
+ * only register the jsonapi mime type when it is missing ([88401ab](https://github.com/graphiti-api/graphiti/commit/88401abc8c77f1e859b4b8da3f9574c75834eb43))
128
+ * with_options accepts the predicate backed relationship options ([71ab441](https://github.com/graphiti-api/graphiti/commit/71ab4418bccc4cff7e6cc10151c1728641d25d34))
129
+
130
+
131
+ ### Features
132
+
133
+ * write every error code Graphiti renders into the generated locale file ([15f19c1](https://github.com/graphiti-api/graphiti/commit/15f19c1dbe56559d9d041fa50e2cc5ea7b2dcf4b))
134
+
135
+
136
+ ### Performance Improvements
137
+
138
+ * record latest performance stats ([7380ef9](https://github.com/graphiti-api/graphiti/commit/7380ef912e9f3b99618bf47ed04e06d1efad4e6c))
139
+
3
140
  # [2.0.0-beta.12](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.11...v2.0.0-beta.12) (2026-08-30)
4
141
 
5
142
 
@@ -204,6 +341,14 @@ GraphitiErrors::Validation::Serializer is now Graphiti::ErrorSerializers::Valida
204
341
  * 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
205
342
  * Ruby >= 3.0 / Rails >= 6 are now required.
206
343
 
344
+ ## [1.13.4](https://github.com/graphiti-api/graphiti/compare/v1.13.3...v1.13.4) (2026-08-20)
345
+
346
+
347
+ ### Bug Fixes
348
+
349
+ * resolve inline when already on a sideload pool thread ([11c9de7](https://github.com/graphiti-api/graphiti/commit/11c9de7fb4bfc2d83b02aaba3e58c1f94c84cec4))
350
+ * scope debugger chunks to the request rather than the class ([9a77896](https://github.com/graphiti-api/graphiti/commit/9a77896c6e56d23b89d0a2aef358d8e8e0c28c54))
351
+
207
352
  ## [1.13.3](https://github.com/graphiti-api/graphiti/compare/v1.13.2...v1.13.3) (2026-08-09)
208
353
 
209
354
 
data/Gemfile CHANGED
@@ -3,6 +3,8 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in graphiti.gemspec
4
4
  gemspec
5
5
 
6
+ gem "rescue_registry", "~> 1.1"
7
+
6
8
  group :test do
7
9
  gem "database_cleaner"
8
10
  gem "pry"
data/graphiti.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
22
22
  spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
23
23
  spec.add_dependency "dry-types", ">= 0.15.0", "< 2.0"
24
- spec.add_dependency "rescue_registry", "~> 1.0"
24
+ spec.add_dependency "rescue_registry", "~> 1.1"
25
25
  spec.add_dependency "concurrent-ruby", ">= 1.2", "< 2.0"
26
26
  spec.add_dependency "activesupport", ">= 7.1"
27
27
  # Bundled (no longer default) as of Ruby 4.0; graphiti uses OpenStruct in lib/
@@ -28,23 +28,8 @@ module Graphiti
28
28
  app_controller_code
29
29
  end
30
30
 
31
- inject_into_file "config/application.rb", after: "Rails::Application\n" do
32
- <<-'RUBY'
33
- # In order for Graphiti to generate links, you need to set the routes host.
34
- # When not explicitly set, via the HOST env var, this will fall back to
35
- # the rails server settings.
36
- # Rails::Server is not defined in console or rake tasks, so this will only
37
- # use those defaults when they are available.
38
- routes.default_url_options[:host] = ENV.fetch('HOST') do
39
- if defined?(Rails::Server)
40
- argv_options = Rails::Server::Options.new.parse!(ARGV)
41
- "http://#{argv_options[:Host]}:#{argv_options[:Port]}"
42
- end
43
- end
44
- RUBY
45
- end
46
-
47
- if defined?(RSpec)
31
+ # Thor aborts the whole generator when this file is missing.
32
+ if defined?(RSpec) && File.exist?(File.join(destination_root, "spec/rails_helper.rb"))
48
33
  inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
49
34
  <<~RUBY
50
35
 
@@ -60,20 +45,7 @@ module Graphiti
60
45
  end
61
46
 
62
47
  insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do
63
- if defined?(VandalUi)
64
- <<-STR
65
- scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
66
- mount VandalUi::Engine, at: '/vandal'
67
- # your routes go here
68
- end
69
- STR
70
- else
71
- <<-STR
72
- scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
73
- # your routes go here
74
- end
75
- STR
76
- end
48
+ namespace_scope
77
49
  end
78
50
  end
79
51
 
@@ -83,6 +55,15 @@ module Graphiti
83
55
  @options["omit-comments"]
84
56
  end
85
57
 
58
+ # The resource generator re-finds this scope by its format default.
59
+ def namespace_scope
60
+ lines = [" scope path: \"#{api_namespace}\", defaults: {format: :jsonapi} do\n"]
61
+ lines << " mount VandalUi::Engine, at: '/vandal'\n" if defined?(VandalUi)
62
+ lines << " # your routes go here\n"
63
+ lines << " end\n"
64
+ lines.join
65
+ end
66
+
86
67
  def app_controller_code
87
68
  str = +" include Graphiti::Rails::Controller\n"
88
69
  if defined?(::Responders)
@@ -8,6 +8,28 @@ module Graphiti
8
8
  aliases: ["-c"],
9
9
  desc: "Generate without documentation comments"
10
10
 
11
+ # Every code Graphiti renders a title for, and the title it renders today.
12
+ ERROR_TITLES = {
13
+ bad_request: "Request Error",
14
+ not_found: "Not Found",
15
+ conflict: "Conflict Error",
16
+ unprocessable_entity: "Validation Error",
17
+ internal_server_error: "Internal Server Error"
18
+ }.freeze
19
+
20
+ # The two codes no registration produces.
21
+ ERROR_SOURCES = {
22
+ unprocessable_entity: ["a write that failed model validations"],
23
+ internal_server_error: ["any exception Graphiti did not register"]
24
+ }.freeze
25
+
26
+ # Written into the app's locale file, not defaulted in Graphiti.
27
+ ERROR_DETAILS = {
28
+ internal_server_error: "We've probably received an error report already, but please contact us if the issue persists."
29
+ }.freeze
30
+
31
+ COMMENT_WIDTH = 68
32
+
11
33
  desc "Writes the error text Graphiti renders, ready to edit"
12
34
  def locale
13
35
  template("locale.yml.erb", File.join("config/locales", "graphiti.en.yml"))
@@ -19,6 +41,40 @@ module Graphiti
19
41
  @options["omit-comments"]
20
42
  end
21
43
 
44
+ def error_titles
45
+ ERROR_TITLES
46
+ end
47
+
48
+ def error_detail(code)
49
+ ERROR_DETAILS[code]
50
+ end
51
+
52
+ # What raises this code, so the file says where its text is used.
53
+ def error_source_lines(code)
54
+ ERROR_SOURCES[code] || wrap(exception_names(code))
55
+ end
56
+
57
+ def exception_names(code)
58
+ Graphiti::Rails::CLIENT_ERROR_STATUSES
59
+ .select { |_exception, status| status == code }
60
+ .keys.map { |exception| exception.delete_prefix("Graphiti::Errors::") }
61
+ end
62
+
63
+ def wrap(names)
64
+ items = names.map.with_index do |name, index|
65
+ (index == names.length - 1) ? name : "#{name},"
66
+ end
67
+
68
+ items.each_with_object([]) do |item, lines|
69
+ joined = "#{lines.last} #{item}"
70
+ if lines.empty? || joined.length > COMMENT_WIDTH
71
+ lines << item
72
+ else
73
+ lines[-1] = joined
74
+ end
75
+ end
76
+ end
77
+
22
78
  def default_messages
23
79
  Graphiti::Util::SimpleErrors::DEFAULT_MESSAGES
24
80
  end
@@ -171,7 +171,8 @@ module Graphiti
171
171
  code << %(, controller: "#{controller_path}") if options[:controller]
172
172
  code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
173
173
  code << "\n"
174
- inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
174
+ # Matches the install generator's scope, including its pre-2.0 form.
175
+ inject_into_file "config/routes.rb", after: /format: :jsonapi.*$\n/ do
175
176
  indent(code, 4)
176
177
  end
177
178
  end
@@ -1,21 +1,18 @@
1
1
  <%- unless omit_comments? -%>
2
- # ApplicationResource is similar to ApplicationRecord - a base class that
3
- # holds configuration/methods for subclasses.
4
2
  # All Resources should inherit from ApplicationResource.
5
3
  <%- end -%>
6
4
  class ApplicationResource < Graphiti::Resource
7
- <%- unless omit_comments? -%>
8
- # Use the ActiveRecord Adapter for all subclasses.
9
- # Subclasses can still override this default.
10
- <%- end -%>
11
5
  self.abstract_class = true
12
6
  self.adapter = Graphiti::Adapters::ActiveRecord
13
- self.base_url = Rails.application.routes.default_url_options[:host]
7
+ <%- unless omit_comments? -%>
8
+ # Or follow config/environments:
9
+ # self.base_url = ActionDispatch::Http::URL.url_for(Rails.application.routes.default_url_options)
10
+ <%- end -%>
11
+ self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000')
14
12
  self.endpoint_namespace = '<%= api_namespace %>'
15
13
  <%- unless omit_comments? -%>
16
14
 
17
- # Defaults every Resource inherits. Uncomment to change them API-wide, or set
18
- # them on individual Resources. See https://graphiti.dev/concepts/resources
15
+ # Defaults every Resource inherits. See https://graphiti.dev/concepts/resources
19
16
  <%- resource_setting_groups.each_pair do |group, lines| -%>
20
17
 
21
18
  # <%= group.to_s.humanize %>
@@ -56,7 +56,7 @@ class <%= controller_class_name %> < ApplicationController
56
56
  def update
57
57
  <%= file_name %> = <%= resource_klass %>.find(params)
58
58
 
59
- if <%= file_name %>.update_attributes
59
+ if <%= file_name %>.update
60
60
  render jsonapi: <%= file_name %>
61
61
  else
62
62
  render jsonapi_errors: <%= file_name %>
@@ -1,19 +1,27 @@
1
1
  <%- unless omit_comments? -%>
2
- # Everything Graphiti puts in an errors payload.
3
2
  # For another language, add a block below or a graphiti.<locale>.yml file.
4
3
  <%- end -%>
5
4
  en:
6
5
  graphiti:
7
6
  errors:
8
7
  <%- unless omit_comments? -%>
9
- # Titles and details are keyed by error code, and Graphiti ships none.
10
- # Without a key you get the HTTP status name and no detail.
11
- #
12
- # internal_server_error:
13
- # title: "Something went wrong"
14
- # detail: "We've probably received an error report already, but please contact us if the issue persists."
15
- #
16
- # format joins them: "data.attributes.title is an unknown attribute".
8
+ # Keyed by error code. Edit any value to reword it. Comments name what
9
+ # raises each code, relative to Graphiti::Errors.
10
+ <%- end -%>
11
+ <%- error_titles.each_pair do |code, title| -%>
12
+ <%= code %>:
13
+ <%- unless omit_comments? -%>
14
+ <%- error_source_lines(code).each do |line| -%>
15
+ # <%= line %>
16
+ <%- end -%>
17
+ <%- end -%>
18
+ title: <%= title.inspect %>
19
+ <%- if error_detail(code) -%>
20
+ detail: <%= error_detail(code).inspect %>
21
+ <%- end -%>
22
+ <%- end -%>
23
+ <%- unless omit_comments? -%>
24
+ # format joins the two below: "data.attributes.title is an unknown attribute".
17
25
  <%- end -%>
18
26
  format: <%= default_format.inspect %>
19
27
  messages:
@@ -45,7 +45,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
45
45
 
46
46
  xit 'works (add some attributes and enable this spec)' do
47
47
  expect {
48
- expect(instance.update_attributes).to eq(true)
48
+ expect(instance.update).to eq(true)
49
49
  }.to change { <%= var %>.reload.updated_at }
50
50
  # .and change { <%= var %>.foo }.to('bar') <- example
51
51
  end
@@ -246,7 +246,7 @@ module Graphiti
246
246
 
247
247
  children.each do |child|
248
248
  if association_type == :many_to_many &&
249
- [:create, :update].include?(Graphiti.context[:namespace]) &&
249
+ [:create, :update].include?(Graphiti.context[:action]) &&
250
250
  !parent.send(association_name).exists?(child.id) &&
251
251
  child.errors.blank?
252
252
  parent.send(association_name) << child
@@ -281,20 +281,6 @@ module Graphiti
281
281
  # Nothing to do in the else case, happened when we merged foreign key
282
282
  end
283
283
 
284
- # (see Adapters::Abstract#create)
285
- def create(model_class, create_params)
286
- instance = model_class.new(create_params)
287
- instance.save
288
- instance
289
- end
290
-
291
- # (see Adapters::Abstract#update)
292
- def update(model_class, update_params)
293
- instance = model_class.find(update_params.only(:id))
294
- instance.update_attributes(update_params.except(:id))
295
- instance
296
- end
297
-
298
284
  def save(model_instance)
299
285
  model_instance.save
300
286
  model_instance
@@ -0,0 +1,21 @@
1
+ module Graphiti
2
+ # graphiti-api/graphiti#134 recommended reading Graphiti.context[:namespace] from base_scope before current_action existed, so the old key keeps working for one major.
3
+ class ContextHash < Hash
4
+ def [](key)
5
+ super(deprecate_namespace(key))
6
+ end
7
+
8
+ def []=(key, value)
9
+ super(deprecate_namespace(key), value)
10
+ end
11
+
12
+ private
13
+
14
+ def deprecate_namespace(key)
15
+ return key unless key == :namespace
16
+
17
+ Graphiti::DEPRECATOR.deprecation_warning(:"context[:namespace]", "Use #current_action instead", caller_locations(2))
18
+ :action
19
+ end
20
+ end
21
+ end
@@ -11,13 +11,13 @@ module Graphiti
11
11
  private_constant :CHUNKS
12
12
 
13
13
  class << self
14
- # Pool threads inherit this through the thread-local copy Scope#future_with_context makes, so their chunks reach the right request.
14
+ # Pool threads inherit this through the fiber storage copy Scope#future_with_context makes, so their chunks reach the right request.
15
15
  def chunks
16
- Thread.current[CHUNKS] ||= Concurrent::Array.new
16
+ Fiber[CHUNKS] ||= Concurrent::Array.new
17
17
  end
18
18
 
19
19
  def chunks=(value)
20
- Thread.current[CHUNKS] = value
20
+ Fiber[CHUNKS] = value
21
21
  end
22
22
 
23
23
  def on_data(name, start, stop, id, payload)
@@ -1,6 +1,8 @@
1
1
  module Graphiti
2
2
  module ErrorSerializers
3
3
  class InvalidRequest
4
+ include TranslatedTitle
5
+
4
6
  STATUS = 400
5
7
 
6
8
  attr_reader :source
@@ -48,12 +50,6 @@ module Graphiti
48
50
  "bad_request"
49
51
  end
50
52
 
51
- def title
52
- return default_title unless defined?(::I18n)
53
-
54
- ::I18n.t :title, scope: [:graphiti, :errors, code], default: default_title
55
- end
56
-
57
53
  def default_title
58
54
  "Request Error"
59
55
  end
@@ -0,0 +1,13 @@
1
+ module Graphiti
2
+ module ErrorSerializers
3
+ module TranslatedTitle
4
+ private
5
+
6
+ def title
7
+ return default_title unless defined?(::I18n)
8
+
9
+ ::I18n.t :title, scope: [:graphiti, :errors, code], default: default_title
10
+ end
11
+ end
12
+ end
13
+ end
@@ -3,6 +3,8 @@ module Graphiti
3
3
  # Walks into related objects so nested writes report against the record
4
4
  # that actually failed.
5
5
  class Validation
6
+ include TranslatedTitle
7
+
6
8
  attr_reader :object
7
9
 
8
10
  def initialize(object, relationship_payloads = {}, relationship_meta = {})
@@ -13,14 +15,14 @@ module Graphiti
13
15
 
14
16
  def attribute_errors
15
17
  [].tap do |errors|
16
- each_error do |attribute, message, code|
18
+ each_error do |attribute, message, validation_code|
17
19
  errors << {
18
- code: "unprocessable_entity",
20
+ code: code,
19
21
  status: "422",
20
- title: "Validation Error",
22
+ title: title,
21
23
  detail: detail_for(attribute, message),
22
24
  source: {pointer: pointer_for(object, attribute)},
23
- meta: meta_for(attribute, message, code, @relationship_meta)
25
+ meta: meta_for(attribute, message, validation_code, @relationship_meta)
24
26
  }
25
27
  end
26
28
  end
@@ -34,6 +36,14 @@ module Graphiti
34
36
 
35
37
  private
36
38
 
39
+ def code
40
+ "unprocessable_entity"
41
+ end
42
+
43
+ def default_title
44
+ "Validation Error"
45
+ end
46
+
37
47
  def each_error
38
48
  object.errors.messages.each_pair do |attribute, messages|
39
49
  details = object.errors.details.find { |k, _| k == attribute }[1]
@@ -26,6 +26,7 @@ module Graphiti
26
26
  @parents = parents
27
27
  @action = parse_action(action)
28
28
  @entity_map = Concurrent::Map.new if parents.empty?
29
+ @association_owners = Concurrent::Map.new if parents.empty?
29
30
  end
30
31
 
31
32
  def association?
@@ -39,6 +40,12 @@ module Graphiti
39
40
  @entity_map
40
41
  end
41
42
 
43
+ def association_owners
44
+ return root.association_owners unless root == self
45
+
46
+ @association_owners
47
+ end
48
+
42
49
  # Deduplication exists for a row two include paths both resolve, so a
43
50
  # resource class sitting at one node has nothing to collide with.
44
51
  def repeated_resource_classes
@@ -421,7 +428,7 @@ module Graphiti
421
428
  end
422
429
 
423
430
  def parse_action(action)
424
- action ||= @params.fetch(:action, Graphiti.context[:namespace]).try(:to_sym)
431
+ action ||= @params.fetch(:action, Graphiti.context[:action]).try(:to_sym)
425
432
  case action
426
433
  when :index
427
434
  :all
@@ -77,7 +77,7 @@ module Graphiti
77
77
  end
78
78
 
79
79
  def register_mime_type
80
- Mime::Type.register(MEDIA_TYPE, :jsonapi)
80
+ Mime::Type.register(MEDIA_TYPE, :jsonapi) if Mime[:jsonapi].nil?
81
81
  end
82
82
 
83
83
  def register_parameter_parser
@@ -28,7 +28,7 @@ module Graphiti
28
28
  end
29
29
 
30
30
  def payload_matches_endpoint?
31
- unless @params.dig(:data, :id) == @params.dig(:filter, :id)
31
+ unless @params.dig(:data, :id).to_s == @params.dig(:filter, :id).to_s
32
32
  attribute_mismatch([:data, :id])
33
33
  end
34
34
 
@@ -37,8 +37,8 @@ module Graphiti
37
37
  end
38
38
  end
39
39
 
40
- def with_context(object, namespace = nil)
41
- Graphiti.with_context(object, namespace) do
40
+ def with_context(object, action = nil)
41
+ Graphiti.with_context(object, action) do
42
42
  yield
43
43
  end
44
44
  end
@@ -55,7 +55,7 @@ module Graphiti
55
55
  # is called, not a fixed list. Persistence overrides it with :create/:update
56
56
  # while saving, and :show while resolving sideloads afterwards.
57
57
  def self.current_action
58
- Graphiti.context[:namespace]
58
+ Graphiti.context[:action]
59
59
  end
60
60
 
61
61
  def current_action
@@ -159,7 +159,7 @@ module Graphiti
159
159
  params = normalized_params_copy(params)
160
160
  add_endpoint_filter(params, action)
161
161
  validator = ::Graphiti::RequestValidator.new(@resource, params, action)
162
- validator.validate!
162
+ with_context_action(action) { validator.validate! }
163
163
 
164
164
  if @assigned_model && same_write_payload?(validator.deserialized_payload)
165
165
  return @assigned_model
@@ -174,32 +174,36 @@ module Graphiti
174
174
  )
175
175
  end
176
176
 
177
+ # TODO: remove this. Only used for persisting many-to-many with AR
178
+ # (see activerecord adapter)
179
+ def with_context_action(action)
180
+ original = Graphiti.context[:action]
181
+ Graphiti.context[:action] = action
182
+ yield
183
+ ensure
184
+ Graphiti.context[:action] = original
185
+ end
186
+
177
187
  def save(action: :create)
178
- # TODO: remove this. Only used for persisting many-to-many with AR
179
- # (see activerecord adapter)
180
- original = Graphiti.context[:namespace]
181
- begin
182
- Graphiti.context[:namespace] = action
188
+ validator = with_context_action(action) do
183
189
  # An assigned model can only come from #assign_attributes, which
184
190
  # validated the payload it stored - re-validating here would run the
185
191
  # writable guards (and their guard_model lookups) a redundant time.
186
192
  unless @assigned_model
187
193
  ::Graphiti::RequestValidator.new(@resource, @payload.params, action).validate!
188
194
  end
189
- validator = persist {
195
+ persist {
190
196
  @resource.persist_with_relationships \
191
197
  @payload.meta(action: action),
192
198
  @payload.attributes,
193
199
  @payload.relationships,
194
200
  assigned_model: @assigned_model
195
201
  }
196
- ensure
197
- Graphiti.context[:namespace] = original
198
202
  end
199
203
  @data, success = validator.to_a
200
204
 
201
205
  if success
202
- # If the context namespace is `update` or `create`, certain
206
+ # If the context action is `update` or `create`, certain
203
207
  # adapters will cause N+1 validation calls, so lets explicitly
204
208
  # switch to a lookup context.
205
209
  Graphiti.with_context(Graphiti.context[:object], :show) do
@@ -39,18 +39,17 @@ module Graphiti
39
39
  !Graphiti.config.concurrency || on_pool_thread?
40
40
  end
41
41
 
42
- # TODO: move to Fiber[] once the floor is Ruby 3.2
43
42
  def self.on_pool_thread?
44
- Thread.current[POOL_THREAD] == true
43
+ Fiber[POOL_THREAD] == true
45
44
  end
46
45
 
47
46
  # Restores rather than clears because :caller_runs may have run the task on a request thread.
48
47
  def self.marking_pool_thread
49
- previous = Thread.current[POOL_THREAD]
50
- Thread.current[POOL_THREAD] = true
48
+ previous = Fiber[POOL_THREAD]
49
+ Fiber[POOL_THREAD] = true
51
50
  yield
52
51
  ensure
53
- Thread.current[POOL_THREAD] = previous
52
+ Fiber[POOL_THREAD] = previous
54
53
  end
55
54
 
56
55
  def initialize(object, resource, query, opts = {})
@@ -61,6 +61,28 @@ module Graphiti
61
61
  self.scope_proc = blk
62
62
  end
63
63
 
64
+ ASSIGNING_QUERY = :__graphiti_assigning_query
65
+ private_constant :ASSIGNING_QUERY
66
+
67
+ def self.assigning_node(query)
68
+ return yield if query.nil?
69
+
70
+ previous = Fiber[ASSIGNING_QUERY]
71
+ Fiber[ASSIGNING_QUERY] = query
72
+ yield
73
+ ensure
74
+ Fiber[ASSIGNING_QUERY] = previous unless query.nil?
75
+ end
76
+
77
+ def self.current_assigning_query
78
+ Fiber[ASSIGNING_QUERY]
79
+ end
80
+
81
+ def self.assigned?(query, record, association_name)
82
+ owners = query.association_owners[association_name]
83
+ !owners.nil? && owners.key?(record.object_id)
84
+ end
85
+
64
86
  def self.assign(&blk)
65
87
  self.assign_proc = blk
66
88
  end
@@ -365,7 +387,7 @@ module Graphiti
365
387
 
366
388
  if self.class.scope_proc
367
389
  build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
368
- fire_assign(parents, sideload_results)
390
+ fire_assign(parents, sideload_results, query)
369
391
  end
370
392
  else
371
393
  sync_load(parents, query, graph_parent, &proxy_block)
@@ -378,7 +400,7 @@ module Graphiti
378
400
 
379
401
  if self.class.scope_proc
380
402
  build_sideload_scope(parents, query, graph_parent).future_resolve do |sideload_results|
381
- fire_assign(parents, sideload_results)
403
+ fire_assign(parents, sideload_results, query)
382
404
  end
383
405
  else
384
406
  future_load(parents, query, graph_parent, &proxy_block)
@@ -413,10 +435,14 @@ module Graphiti
413
435
  end
414
436
 
415
437
  def associate_all(parent, children)
438
+ return unless claim_association(parent)
439
+
416
440
  parent_resource.associate_all(parent, children, association_name, type)
417
441
  end
418
442
 
419
443
  def associate(parent, child)
444
+ return unless claim_association(parent)
445
+
420
446
  parent_resource.associate(parent, child, association_name, type)
421
447
  end
422
448
 
@@ -506,7 +532,7 @@ module Graphiti
506
532
  opts[:sideload_parent_length] = parents.length
507
533
  opts[:query] = query
508
534
  opts[:after_resolve] = ->(results) {
509
- fire_assign(parents, results)
535
+ fire_assign(parents, results, query)
510
536
  }
511
537
  end
512
538
  end
@@ -519,16 +545,28 @@ module Graphiti
519
545
  end
520
546
  end
521
547
 
522
- def fire_assign(parents, children)
523
- with_error_handling Errors::SideloadAssignError do
524
- if self.class.assign_proc
525
- instance_exec(parents, children, &self.class.assign_proc)
526
- else
527
- assign(parents, children)
548
+ def fire_assign(parents, children, query = nil)
549
+ self.class.assigning_node(query) do
550
+ with_error_handling Errors::SideloadAssignError do
551
+ if self.class.assign_proc
552
+ instance_exec(parents, children, &self.class.assign_proc)
553
+ else
554
+ assign(parents, children)
555
+ end
528
556
  end
529
557
  end
530
558
  end
531
559
 
560
+ # A record can be shared by two nodes of the include tree, and a node that narrows
561
+ # differently returns different rows, so the first node to populate an association owns it.
562
+ def claim_association(parent)
563
+ query = self.class.current_assigning_query
564
+ return true if query.nil?
565
+
566
+ owners = query.association_owners.compute_if_absent(association_name) { Concurrent::Map.new }
567
+ owners.compute_if_absent(parent.object_id) { query.hash } == query.hash
568
+ end
569
+
532
570
  def with_error_handling(error_class)
533
571
  begin
534
572
  result = yield
@@ -59,6 +59,7 @@ module Graphiti
59
59
 
60
60
  class RelationMatcher < BaseMatcher
61
61
  GRAPHITI_OPTS = %i[primary_key foreign_key resource readable writable link single].freeze
62
+ SIDELOAD_METHODS = {resource: :resource_class, readable: :readable?, writable: :writable?, single: :single?}.freeze
62
63
  GRAPHITI_CONFIG_KEY = :sideloads
63
64
  EXPECTED_ACTION = ""
64
65
 
@@ -73,7 +74,7 @@ module Graphiti
73
74
  attr_reader :target, :opts, :resource
74
75
 
75
76
  def assert_opt(opt)
76
- asserted_opt = (opt == :resource) ? :resource_class : opt
77
+ asserted_opt = SIDELOAD_METHODS.fetch(opt, opt)
77
78
  return true if config.send(asserted_opt) == opts[opt]
78
79
 
79
80
  @opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
@@ -71,7 +71,7 @@ module Graphiti
71
71
  # not predict, so the loaded records win. Only the un-included case
72
72
  # is worth short-circuiting.
73
73
  if sideload_ref.resource_ids_from_foreign_key? &&
74
- !self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
74
+ !::Graphiti::Sideload.assigned?(@proxy.query, @object, sideload_ref.association_name)
75
75
  linkage always: sideload_ref.render_resource_ids? do
76
76
  foreign_key = begin
77
77
  @object.public_send(sideload_ref.foreign_key)
@@ -104,15 +104,6 @@ module Graphiti
104
104
  end
105
105
  end
106
106
 
107
- # A relationship nested under another one is still loaded and can still
108
- # be narrowed by a deep filter, so the foreign key is not a safe
109
- # stand-in for what the request actually returns.
110
- def included_anywhere?(include_hash, name)
111
- include_hash.any? do |key, nested|
112
- key == name || included_anywhere?(nested, name)
113
- end
114
- end
115
-
116
107
  def data_proc
117
108
  sideload_ref = @sideload
118
109
  resource_class_ref = @resource_class
@@ -58,11 +58,11 @@ module Graphiti
58
58
  private
59
59
 
60
60
  def _hooks
61
- Thread.current[:_graphiti_hooks]
61
+ Fiber[:_graphiti_hooks]
62
62
  end
63
63
 
64
64
  def reset_hooks
65
- Thread.current[:_graphiti_hooks] = {
65
+ Fiber[:_graphiti_hooks] = {
66
66
  after_graph_persist: [],
67
67
  before_commit: [],
68
68
  after_commit: []
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.12"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -51,18 +51,20 @@ module Graphiti
51
51
 
52
52
  # @api private
53
53
  def self.context
54
- Thread.current[:context] ||= {}
54
+ Fiber[:context] ||= ContextHash.new
55
55
  end
56
56
 
57
57
  # @api private
58
58
  def self.context=(val)
59
- Thread.current[:context] = val
59
+ Fiber[:context] = ContextHash.new.tap do |hash|
60
+ val.each { |key, value| hash[key] = value }
61
+ end
60
62
  end
61
63
 
62
64
  # @api private
63
- def self.with_context(obj, namespace = nil)
65
+ def self.with_context(obj, action = nil)
64
66
  prior = context
65
- self.context = {object: obj, namespace: namespace}
67
+ self.context = {object: obj, action: action}
66
68
  yield
67
69
  ensure
68
70
  self.context = prior
@@ -169,6 +171,7 @@ require "graphiti/version"
169
171
  require "graphiti/jsonapi_serializable_ext"
170
172
  require "graphiti/configuration"
171
173
  require "graphiti/context"
174
+ require "graphiti/context_hash"
172
175
  require "graphiti/errors"
173
176
  require "graphiti/types"
174
177
  require "graphiti/audit"
@@ -239,6 +242,7 @@ require "graphiti/query"
239
242
  require "graphiti/debugger"
240
243
  require "graphiti/util/cache_debug"
241
244
  require "graphiti/util/uri_decoder"
245
+ require "graphiti/error_serializers/translated_title"
242
246
  require "graphiti/error_serializers/validation"
243
247
  require "graphiti/error_serializers/invalid_request"
244
248
  require "graphiti/error_serializers/conflict_request"
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.12
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
@@ -69,14 +69,14 @@ dependencies:
69
69
  requirements:
70
70
  - - "~>"
71
71
  - !ruby/object:Gem::Version
72
- version: '1.0'
72
+ version: '1.1'
73
73
  type: :runtime
74
74
  prerelease: false
75
75
  version_requirements: !ruby/object:Gem::Requirement
76
76
  requirements:
77
77
  - - "~>"
78
78
  - !ruby/object:Gem::Version
79
- version: '1.0'
79
+ version: '1.1'
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: concurrent-ruby
82
82
  requirement: !ruby/object:Gem::Requirement
@@ -318,12 +318,14 @@ files:
318
318
  - lib/graphiti/cli.rb
319
319
  - lib/graphiti/configuration.rb
320
320
  - lib/graphiti/context.rb
321
+ - lib/graphiti/context_hash.rb
321
322
  - lib/graphiti/debugger.rb
322
323
  - lib/graphiti/delegates/pagination.rb
323
324
  - lib/graphiti/deserializer.rb
324
325
  - lib/graphiti/error_serializers/conflict_request.rb
325
326
  - lib/graphiti/error_serializers/deprecated_constants.rb
326
327
  - lib/graphiti/error_serializers/invalid_request.rb
328
+ - lib/graphiti/error_serializers/translated_title.rb
327
329
  - lib/graphiti/error_serializers/validation.rb
328
330
  - lib/graphiti/errors.rb
329
331
  - lib/graphiti/extensions/boolean_attribute.rb