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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +145 -0
- data/Gemfile +2 -0
- data/graphiti.gemspec +1 -1
- data/lib/generators/graphiti/install_generator.rb +12 -31
- data/lib/generators/graphiti/locale_generator.rb +56 -0
- data/lib/generators/graphiti/resource_generator.rb +2 -1
- data/lib/generators/graphiti/templates/application_resource.rb.erb +6 -9
- data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
- data/lib/generators/graphiti/templates/locale.yml.erb +17 -9
- data/lib/generators/graphiti/templates/resource_writes_spec.rb.erb +1 -1
- data/lib/graphiti/adapters/active_record.rb +1 -15
- data/lib/graphiti/context_hash.rb +21 -0
- data/lib/graphiti/debugger.rb +3 -3
- data/lib/graphiti/error_serializers/invalid_request.rb +2 -6
- data/lib/graphiti/error_serializers/translated_title.rb +13 -0
- data/lib/graphiti/error_serializers/validation.rb +14 -4
- data/lib/graphiti/query.rb +8 -1
- data/lib/graphiti/rails/railtie.rb +1 -1
- data/lib/graphiti/request_validators/update_validator.rb +1 -1
- data/lib/graphiti/resource.rb +3 -3
- data/lib/graphiti/resource_proxy.rb +14 -10
- data/lib/graphiti/scope.rb +4 -5
- data/lib/graphiti/sideload.rb +47 -9
- data/lib/graphiti/spec_helpers/matchers.rb +2 -1
- data/lib/graphiti/util/serializer_relationships.rb +1 -10
- data/lib/graphiti/util/transaction_hooks_recorder.rb +2 -2
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +8 -4
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac45688980e3b5a1b21c320161e5b8e67610dbcc2e0b4379f072cc86d8ff453f
|
|
4
|
+
data.tar.gz: f1d4398684f5b44872251db6cbe8155a80b5c96388ebb42b4af45fa17373035c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47b24e17dafad601b9ac9cf0898a7a838f9a4cedef37e88fd579e0a3b30cd4e14ac322e0b71bc0d0d2a168f04881f9cb2cbcc4d90984a0944440a04f533ca189
|
|
7
|
+
data.tar.gz: 582d454ccaf706f46308dec57bbfe5db9b44663f84383c482fde1d23bc7e02dfe28045009d03e3607e19d9070f8e206dda99c79d42a0a78ebab88c8d9bd17b2b
|
data/.github/workflows/ci.yml
CHANGED
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
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.
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 %>.
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
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[:
|
|
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
|
data/lib/graphiti/debugger.rb
CHANGED
|
@@ -11,13 +11,13 @@ module Graphiti
|
|
|
11
11
|
private_constant :CHUNKS
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
# Pool threads inherit this through the
|
|
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
|
-
|
|
16
|
+
Fiber[CHUNKS] ||= Concurrent::Array.new
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def chunks=(value)
|
|
20
|
-
|
|
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
|
|
@@ -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,
|
|
18
|
+
each_error do |attribute, message, validation_code|
|
|
17
19
|
errors << {
|
|
18
|
-
code:
|
|
20
|
+
code: code,
|
|
19
21
|
status: "422",
|
|
20
|
-
title:
|
|
22
|
+
title: title,
|
|
21
23
|
detail: detail_for(attribute, message),
|
|
22
24
|
source: {pointer: pointer_for(object, attribute)},
|
|
23
|
-
meta: meta_for(attribute, message,
|
|
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]
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -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[:
|
|
431
|
+
action ||= @params.fetch(:action, Graphiti.context[:action]).try(:to_sym)
|
|
425
432
|
case action
|
|
426
433
|
when :index
|
|
427
434
|
:all
|
data/lib/graphiti/resource.rb
CHANGED
|
@@ -37,8 +37,8 @@ module Graphiti
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def with_context(object,
|
|
41
|
-
Graphiti.with_context(object,
|
|
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[:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
50
|
-
|
|
48
|
+
previous = Fiber[POOL_THREAD]
|
|
49
|
+
Fiber[POOL_THREAD] = true
|
|
51
50
|
yield
|
|
52
51
|
ensure
|
|
53
|
-
|
|
52
|
+
Fiber[POOL_THREAD] = previous
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def initialize(object, resource, query, opts = {})
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
Fiber[:_graphiti_hooks]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def reset_hooks
|
|
65
|
-
|
|
65
|
+
Fiber[:_graphiti_hooks] = {
|
|
66
66
|
after_graph_persist: [],
|
|
67
67
|
before_commit: [],
|
|
68
68
|
after_commit: []
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
|
@@ -51,18 +51,20 @@ module Graphiti
|
|
|
51
51
|
|
|
52
52
|
# @api private
|
|
53
53
|
def self.context
|
|
54
|
-
|
|
54
|
+
Fiber[:context] ||= ContextHash.new
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# @api private
|
|
58
58
|
def self.context=(val)
|
|
59
|
-
|
|
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,
|
|
65
|
+
def self.with_context(obj, action = nil)
|
|
64
66
|
prior = context
|
|
65
|
-
self.context = {object: obj,
|
|
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
|
|
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.
|
|
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.
|
|
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
|