rails_audit_log-graphql 0.6.1 → 1.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: a1df5ae47e9bae5cc635eb04372e8a84f3c484350f50284d98ab4602d9f2175c
4
- data.tar.gz: 5bd2df5463caab8646bfab17973cd85343819b94fb189d6c3d52bbe65cbf11ce
3
+ metadata.gz: 681be68e65f4b6f543de2ce47434d6b547db12299d15c2f136d94f1db5b15355
4
+ data.tar.gz: 184994ef369aea31293293d01e22663bb1e3dc48f3e958aec6f523997ec9ed7b
5
5
  SHA512:
6
- metadata.gz: 3899c333a5af6c7f9c1a05cfe1bc882f8464ad9da9d21ed677c75ce7ec6546a6181893d8a27f563822d46ed4bfb7d6f91e8349285d1bc83e3cc6aaae2a8d5147
7
- data.tar.gz: cdc13b1d159a794f67fa1c46a3410b3196529dba59e234eba32147338d15e85a2478bcb676821738256792e4358100a4c07bc96cfb855c00d68db828807a1d48
6
+ metadata.gz: 5d06f3473ed427c34208a6067d4e77a445a10068e8ffc269f4725a858092c1a55c9dd12b504632df53e260f5e5b3b0b6c3aa8eab3a4499c475bdb165165d9403
7
+ data.tar.gz: a9c2936cc464d8288a815f1345b12c23918aaf7a70a15241e77e50ccf90ce0daefd27e172f1f554e7861d7d0e3d6bb9a34557cde7c614f162652795d5288a287
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2026-06-04
4
+
5
+ ### Added
6
+
7
+ - **RSpec matcher** — `have_graphql_audit_entry(:update).touching(:title).for_type("Post")` for asserting audit entries in `Schema.execute` responses; include `RailsAuditLog::Graphql::Testing::RSpecMatchers` in your RSpec config
8
+ - **Minitest assertions** — `assert_graphql_audit_entry` and `refute_graphql_audit_entry` with the same filter interface; include `RailsAuditLog::Graphql::Testing::MinitestAssertions` in your test class
9
+ - Full YARD documentation on all public modules, classes, and methods
10
+ - RBS type signatures for `Testing::RSpecMatchers` and `Testing::MinitestAssertions`
11
+
12
+ ### Changed
13
+
14
+ - API stability guarantee — no breaking changes to public interfaces without a major version bump
15
+
3
16
  ## [0.6.1] - 2026-06-04
4
17
 
5
18
  ### Added
data/README.md CHANGED
@@ -29,6 +29,9 @@ A [graphql-ruby](https://graphql-ruby.org) API layer for the [`rails_audit_log`]
29
29
  - [AuditLogSubscriptionsMixin](#auditlogsubscriptionsmixin)
30
30
  - [auditLogEntryCreated](#auditlogentrycreated)
31
31
  - [Broadcaster](#broadcaster)
32
+ - [Testing](#testing)
33
+ - [RSpec matchers](#rspec-matchers)
34
+ - [Minitest assertions](#minitest-assertions)
32
35
  - [Development](#development)
33
36
  - [Contributing](#contributing)
34
37
  - [License](#license)
@@ -388,6 +391,82 @@ For each entry, the broadcaster triggers:
388
391
 
389
392
  [↑ Back to top](#table-of-contents)
390
393
 
394
+ ## Testing
395
+
396
+ `rails_audit_log-graphql` ships test helpers for both RSpec and Minitest so you can
397
+ assert that your mutations actually produce the expected audit log entries.
398
+
399
+ Require the testing helpers from your test helper:
400
+
401
+ ```ruby
402
+ # spec/spec_helper.rb or test/test_helper.rb
403
+ require "rails_audit_log/graphql/testing"
404
+ ```
405
+
406
+ ### RSpec matchers
407
+
408
+ Include `RailsAuditLog::Graphql::Testing::RSpecMatchers` in your configuration:
409
+
410
+ ```ruby
411
+ RSpec.configure do |config|
412
+ config.include RailsAuditLog::Graphql::Testing::RSpecMatchers
413
+ end
414
+ ```
415
+
416
+ Then use `have_graphql_audit_entry` against any `GraphQL::Query::Result` (or plain hash) returned by `Schema.execute`:
417
+
418
+ ```ruby
419
+ result = MySchema.execute('{ auditLogEntries { event diff { attribute } } }')
420
+
421
+ # Assert an entry with a specific event exists
422
+ expect(result).to have_graphql_audit_entry(:update)
423
+
424
+ # Assert the update entry touched the :title attribute
425
+ # (requires diff { attribute } in the query)
426
+ expect(result).to have_graphql_audit_entry(:update).touching(:title)
427
+
428
+ # Scope to a specific model
429
+ expect(result).to have_graphql_audit_entry(:create).for_type("Post")
430
+
431
+ # Combine chains
432
+ expect(result).to have_graphql_audit_entry(:update).for_type("Post").touching(:title)
433
+ ```
434
+
435
+ The matcher searches `auditLogEntries` and `auditLogEntriesConnection.nodes` in the
436
+ response data.
437
+
438
+ [↑ Back to top](#table-of-contents)
439
+
440
+ ### Minitest assertions
441
+
442
+ Include `RailsAuditLog::Graphql::Testing::MinitestAssertions` in your test class:
443
+
444
+ ```ruby
445
+ class ActiveSupport::TestCase
446
+ include RailsAuditLog::Graphql::Testing::MinitestAssertions
447
+ end
448
+ ```
449
+
450
+ Use `assert_graphql_audit_entry` and `refute_graphql_audit_entry`:
451
+
452
+ ```ruby
453
+ result = MySchema.execute('{ auditLogEntries { event diff { attribute } } }')
454
+
455
+ # Assert an update entry exists
456
+ assert_graphql_audit_entry result, event: :update
457
+
458
+ # Assert an update entry that touched :title
459
+ assert_graphql_audit_entry result, event: :update, touching: :title
460
+
461
+ # Assert no destroy entry exists
462
+ refute_graphql_audit_entry result, event: :destroy
463
+
464
+ # Custom failure message
465
+ assert_graphql_audit_entry result, event: :update, message: "mutation should have created an update entry"
466
+ ```
467
+
468
+ [↑ Back to top](#table-of-contents)
469
+
391
470
  ## Development
392
471
 
393
472
  ```bash
data/ROADMAP.md CHANGED
@@ -4,10 +4,4 @@ This gem adds a GraphQL API layer on top of [`rails_audit_log`](https://github.c
4
4
 
5
5
  ---
6
6
 
7
- ## 1.0.0 Stable API
8
-
9
- - Full YARD documentation
10
- - **RSpec matchers** — `expect(response).to have_graphql_audit_entry(:update).touching(:title)`
11
- - **Minitest assertions** — `assert_graphql_audit_entry`
12
- - API stability guarantee — no breaking changes without a major version bump
13
- - Complete README with setup guide, examples, and schema reference
7
+ _No planned milestones at this time._
@@ -3,7 +3,24 @@
3
3
  module RailsAuditLog
4
4
  module Graphql
5
5
  module Queries
6
+ # Mixin that adds all audit log query fields to a host application's +QueryType+.
7
+ #
8
+ # Include in your +QueryType+:
9
+ #
10
+ # class Types::QueryType < Types::BaseObject
11
+ # include RailsAuditLog::Graphql::Queries::AuditLogEntriesQueryMixin
12
+ # end
13
+ #
14
+ # This adds the following fields to the schema:
15
+ # - +auditLogEntry(id:)+ — fetch a single entry by ID
16
+ # - +auditLogEntries(...)+ — offset-paginated list with filters
17
+ # - +auditLogEntriesConnection(...)+ — cursor-paginated Relay connection
18
+ # - +auditLogEntriesCount(...)+ — count matching entries
19
+ # - +auditLogReify(itemType:, itemId:, at:)+ — reconstruct record state at a point in time
20
+ #
21
+ # @see https://github.com/eclectic-coding/rails_audit_log-graphql README
6
22
  module AuditLogEntriesQueryMixin
23
+ # @api private
7
24
  def self.included(base)
8
25
  base.field(
9
26
  :audit_log_entry,
@@ -90,6 +107,11 @@ module RailsAuditLog
90
107
  end
91
108
  end
92
109
 
110
+ # Resolves the +auditLogEntry+ field.
111
+ #
112
+ # @param id [String] the entry ID
113
+ # @param for_tenant [String, nil] optional tenant scope
114
+ # @return [RailsAuditLog::AuditLogEntry, nil]
93
115
  def resolve_audit_log_entry(id:, for_tenant: nil)
94
116
  check_authentication!
95
117
  tenant_id = for_tenant || RailsAuditLog.current_tenant&.call
@@ -97,17 +119,31 @@ module RailsAuditLog
97
119
  base.find_by(id: id)
98
120
  end
99
121
 
122
+ # Resolves the +auditLogEntries+ field.
123
+ #
124
+ # @return [ActiveRecord::Relation]
100
125
  def resolve_audit_log_entries(event: nil, item_type: nil, item_id: nil, actor_id: nil, actor_type: nil, since: nil, until_time: nil, touching: nil, order_by: nil, for_tenant: nil, page: 1, per_page: 25)
101
126
  check_authentication!
102
127
  scope = build_scope(event: event, item_type: item_type, item_id: item_id, actor_id: actor_id, actor_type: actor_type, since: since, until_time: until_time, touching: touching, order_by: order_by, for_tenant: for_tenant)
103
128
  scope.limit(per_page).offset((page - 1) * per_page)
104
129
  end
105
130
 
131
+ # Resolves the +auditLogEntriesConnection+ field.
132
+ #
133
+ # @return [ActiveRecord::Relation]
106
134
  def resolve_audit_log_entries_connection(event: nil, item_type: nil, item_id: nil, actor_id: nil, actor_type: nil, since: nil, until_time: nil, touching: nil, order_by: nil, for_tenant: nil)
107
135
  check_authentication!
108
136
  build_scope(event: event, item_type: item_type, item_id: item_id, actor_id: actor_id, actor_type: actor_type, since: since, until_time: until_time, touching: touching, order_by: order_by, for_tenant: for_tenant)
109
137
  end
110
138
 
139
+ # Resolves the +auditLogReify+ field. Returns the reconstructed attribute
140
+ # hash for +item_type+/+item_id+ as of +at+, or +nil+ when no entry exists.
141
+ #
142
+ # @param item_type [String]
143
+ # @param item_id [String]
144
+ # @param at [Time]
145
+ # @param for_tenant [String, nil]
146
+ # @return [Hash, nil]
111
147
  def resolve_audit_log_reify(item_type:, item_id:, at:, for_tenant: nil)
112
148
  check_authentication!
113
149
  tenant_id = for_tenant || RailsAuditLog.current_tenant&.call
@@ -121,6 +157,9 @@ module RailsAuditLog
121
157
  entry.object.present? ? entry.object.merge(to_attrs) : to_attrs
122
158
  end
123
159
 
160
+ # Resolves the +auditLogEntriesCount+ field.
161
+ #
162
+ # @return [Integer]
124
163
  def resolve_audit_log_entries_count(event: nil, item_type: nil, actor_type: nil, since: nil, for_tenant: nil)
125
164
  check_authentication!
126
165
  scope = RailsAuditLog::AuditLogEntry.all
@@ -2,7 +2,29 @@
2
2
 
3
3
  module RailsAuditLog
4
4
  module Graphql
5
+ # Schema-level plugin that applies query-protection limits and enables
6
+ # dataloader batching in one +include+.
7
+ #
8
+ # Include in your GraphQL schema class:
9
+ #
10
+ # class MySchema < GraphQL::Schema
11
+ # include RailsAuditLog::Graphql::SchemaPlugin
12
+ # query Types::QueryType
13
+ # end
14
+ #
15
+ # This applies the following defaults (all overridable via
16
+ # {RailsAuditLog::Graphql} class-level accessors):
17
+ #
18
+ # | Setting | Default | Description |
19
+ # |------------------------|---------|--------------------------------------------------|
20
+ # | +max_complexity+ | 200 | Reject queries whose complexity exceeds this |
21
+ # | +max_depth+ | 10 | Reject queries nested deeper than this |
22
+ # | +default_max_page_size+| 25 | Page-size assumption for connection complexity |
23
+ #
24
+ # Also enables +GraphQL::Dataloader+ for N+1-free batch loading of
25
+ # +actor.record+ and +auditedResource.record+ fields.
5
26
  module SchemaPlugin
27
+ # @api private
6
28
  def self.included(base)
7
29
  base.max_complexity(RailsAuditLog::Graphql.max_complexity)
8
30
  base.max_depth(RailsAuditLog::Graphql.max_depth)
@@ -3,11 +3,25 @@
3
3
  module RailsAuditLog
4
4
  module Graphql
5
5
  module Sources
6
+ # Dataloader source that batch-loads ActiveRecord records by class name and ID,
7
+ # returning their +attributes+ hash.
8
+ #
9
+ # Used internally by {Types::ActorType} and {Types::AuditedResourceType} to
10
+ # resolve the +record+ field without N+1 queries.
11
+ #
12
+ # @example Manual use in a custom resolver
13
+ # dataloader.with(RecordByIdSource, "User").load("42")
6
14
  class RecordByIdSource < GraphQL::Dataloader::Source
15
+ # @param class_name [String] the ActiveRecord model class name to load from
7
16
  def initialize(class_name)
8
17
  @class_name = class_name
9
18
  end
10
19
 
20
+ # Batch-loads records for the given IDs.
21
+ #
22
+ # @param ids [Array<String>] record IDs to load
23
+ # @return [Array<Hash, nil>] +attributes+ hash for each ID, or +nil+ when
24
+ # the record does not exist or the class cannot be constantized
11
25
  def fetch(ids)
12
26
  klass = @class_name.safe_constantize
13
27
  return ids.map { nil } unless klass
@@ -3,7 +3,26 @@
3
3
  module RailsAuditLog
4
4
  module Graphql
5
5
  module Subscriptions
6
+ # Mixin that adds the +auditLogEntryCreated+ subscription field to a host
7
+ # application's +SubscriptionType+.
8
+ #
9
+ # Include in your +SubscriptionType+:
10
+ #
11
+ # class Types::SubscriptionType < Types::BaseObject
12
+ # include RailsAuditLog::Graphql::Subscriptions::AuditLogSubscriptionsMixin
13
+ # end
14
+ #
15
+ # The schema must also use +GraphQL::Subscriptions::ActionCableSubscriptions+:
16
+ #
17
+ # class MySchema < GraphQL::Schema
18
+ # subscription Types::SubscriptionType
19
+ # use GraphQL::Subscriptions::ActionCableSubscriptions
20
+ # end
21
+ #
22
+ # @see AuditLogEntryCreated
23
+ # @see Broadcaster
6
24
  module AuditLogSubscriptionsMixin
25
+ # @api private
7
26
  def self.included(base)
8
27
  base.field(
9
28
  :audit_log_entry_created,
@@ -3,25 +3,56 @@
3
3
  module RailsAuditLog
4
4
  module Graphql
5
5
  module Subscriptions
6
+ # Bridges +ActiveSupport::Notifications+ events emitted by
7
+ # +rails_audit_log+ to GraphQL subscription triggers.
8
+ #
9
+ # Start it once in an initializer after the schema is defined:
10
+ #
11
+ # Rails.application.config.after_initialize do
12
+ # RailsAuditLog::Graphql::Subscriptions::Broadcaster.new(schema: MySchema).start
13
+ # end
14
+ #
15
+ # For each +rails_audit_log.entry_created+ notification the broadcaster
16
+ # fires two subscription triggers:
17
+ # - +auditLogEntryCreated(itemType:, itemId:)+ for record-specific subscribers
18
+ # - +auditLogEntryCreated(actorId:)+ for actor-specific subscribers (when an
19
+ # actor is present on the entry)
6
20
  class Broadcaster
21
+ # @api private
7
22
  EVENT = "rails_audit_log.entry_created"
8
23
 
24
+ # @param schema [Class] the GraphQL schema class (must use
25
+ # +GraphQL::Subscriptions::ActionCableSubscriptions+)
9
26
  def initialize(schema:)
10
27
  @schema = schema
11
28
  @subscriber = nil
12
29
  end
13
30
 
31
+ # Subscribe to +rails_audit_log.entry_created+ notifications and begin
32
+ # broadcasting to GraphQL subscribers. Idempotent — calling +start+ a
33
+ # second time replaces the previous subscriber.
34
+ #
35
+ # @return [void]
14
36
  def start
15
37
  @subscriber = ActiveSupport::Notifications.subscribe(EVENT) do |*, payload|
16
38
  broadcast(payload[:entry])
17
39
  end
18
40
  end
19
41
 
42
+ # Unsubscribe from +ActiveSupport::Notifications+. After calling +stop+,
43
+ # no further subscription triggers will fire until {#start} is called again.
44
+ #
45
+ # @return [void]
20
46
  def stop
21
47
  ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
22
48
  @subscriber = nil
23
49
  end
24
50
 
51
+ # Trigger GraphQL subscriptions for +entry+. Fires both the
52
+ # record-scoped and actor-scoped variants.
53
+ #
54
+ # @param entry [RailsAuditLog::AuditLogEntry] the newly created entry
55
+ # @return [void]
25
56
  def broadcast(entry)
26
57
  @schema.subscriptions.trigger(
27
58
  "audit_log_entry_created",
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAuditLog
4
+ module Graphql
5
+ module Testing
6
+ # Minitest assertions for GraphQL audit log entries.
7
+ #
8
+ # Include this module in a test class or in a shared support file:
9
+ #
10
+ # class ActiveSupport::TestCase
11
+ # include RailsAuditLog::Graphql::Testing::MinitestAssertions
12
+ # end
13
+ #
14
+ # The assertions inspect the +auditLogEntries+ and +auditLogEntriesConnection.nodes+
15
+ # keys in the response data. To use the +touching:+ option, include +diff { attribute }+
16
+ # in your GraphQL query.
17
+ module MinitestAssertions
18
+ # Asserts that the GraphQL response contains at least one audit log entry
19
+ # matching the given criteria.
20
+ #
21
+ # @param response [Hash, GraphQL::Query::Result] the result of +Schema.execute+
22
+ # @param event [Symbol, String] expected event type (:create, :update, :destroy)
23
+ # @param touching [Symbol, String, nil] when given, requires the entry's +diff+ to
24
+ # include an attribute with this name (query must include +diff { attribute }+)
25
+ # @param item_type [String, nil] when given, requires the entry's +itemType+ to match
26
+ # @param message [String, nil] custom failure message
27
+ #
28
+ # @example Assert an update entry exists
29
+ # result = MySchema.execute('{ auditLogEntries { event } }')
30
+ # assert_graphql_audit_entry result, event: :update
31
+ #
32
+ # @example Assert an update entry that touched :title
33
+ # result = MySchema.execute('{ auditLogEntries { event diff { attribute } } }')
34
+ # assert_graphql_audit_entry result, event: :update, touching: :title
35
+ def assert_graphql_audit_entry(response, event:, touching: nil, item_type: nil, message: nil)
36
+ matched = filter_entries(response, event: event, touching: touching, item_type: item_type)
37
+ default_msg = build_message("Expected", event, touching, item_type)
38
+ assert matched.any?, message || default_msg
39
+ end
40
+
41
+ # Asserts that the GraphQL response does NOT contain an audit log entry
42
+ # matching the given criteria.
43
+ #
44
+ # @param response [Hash, GraphQL::Query::Result] the result of +Schema.execute+
45
+ # @param event [Symbol, String] the event type to check
46
+ # @param touching [Symbol, String, nil] attribute name to match against +diff+
47
+ # @param item_type [String, nil] model class name to match against +itemType+
48
+ # @param message [String, nil] custom failure message
49
+ def refute_graphql_audit_entry(response, event:, touching: nil, item_type: nil, message: nil)
50
+ matched = filter_entries(response, event: event, touching: touching, item_type: item_type)
51
+ default_msg = build_message("Expected no", event, touching, item_type)
52
+ refute matched.any?, message || default_msg
53
+ end
54
+
55
+ private
56
+
57
+ def filter_entries(response, event:, touching:, item_type:)
58
+ entries = extract_graphql_entries(response)
59
+ matched = entries.select { |e| e["event"] == event.to_s }
60
+ matched = matched.select { |e| e["itemType"] == item_type.to_s } if item_type
61
+ if touching
62
+ matched = matched.select { |e| e["diff"]&.any? { |d| d["attribute"] == touching.to_s } }
63
+ end
64
+ matched
65
+ end
66
+
67
+ def build_message(prefix, event, touching, item_type)
68
+ msg = "#{prefix} GraphQL audit entry with event #{event.inspect}"
69
+ msg += " touching #{touching.inspect}" if touching
70
+ msg += " for type #{item_type.inspect}" if item_type
71
+ msg
72
+ end
73
+
74
+ def extract_graphql_entries(response)
75
+ data = response.respond_to?(:[]) ? (response["data"] || response[:data]) : nil
76
+ return [] unless data
77
+
78
+ entries = []
79
+ entries += Array(data["auditLogEntries"])
80
+ entries += Array(data.dig("auditLogEntriesConnection", "nodes"))
81
+ entries += Array(data.dig("auditLogEntriesConnection", "edges")).filter_map { |e| e["node"] }
82
+ entries
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAuditLog
4
+ module Graphql
5
+ module Testing
6
+ # RSpec matchers for asserting GraphQL audit log entries in a query response.
7
+ #
8
+ # Include this module in your RSpec configuration to use the {#have_graphql_audit_entry}
9
+ # matcher:
10
+ #
11
+ # RSpec.configure do |config|
12
+ # config.include RailsAuditLog::Graphql::Testing::RSpecMatchers
13
+ # end
14
+ #
15
+ # Or include it in a single example group:
16
+ #
17
+ # RSpec.describe "audit logging" do
18
+ # include RailsAuditLog::Graphql::Testing::RSpecMatchers
19
+ # end
20
+ #
21
+ # The matcher inspects the +auditLogEntries+ and +auditLogEntriesConnection.nodes+
22
+ # keys in the response data. To use the +touching+ chain, include +diff { attribute }+
23
+ # in your GraphQL query.
24
+ module RSpecMatchers
25
+ # Returns a matcher that asserts a GraphQL response contains an audit log
26
+ # entry with the given event type.
27
+ #
28
+ # @param event [Symbol, String] expected event type (:create, :update, :destroy)
29
+ # @return [HaveGraphqlAuditEntry]
30
+ #
31
+ # @example Assert an update entry exists
32
+ # result = MySchema.execute('{ auditLogEntries { event } }')
33
+ # expect(result).to have_graphql_audit_entry(:update)
34
+ #
35
+ # @example Assert an update entry that touched :title
36
+ # result = MySchema.execute('{ auditLogEntries { event diff { attribute } } }')
37
+ # expect(result).to have_graphql_audit_entry(:update).touching(:title)
38
+ #
39
+ # @example Assert a create entry for a specific model
40
+ # expect(result).to have_graphql_audit_entry(:create).for_type("Post")
41
+ def have_graphql_audit_entry(event)
42
+ HaveGraphqlAuditEntry.new(event.to_s)
43
+ end
44
+
45
+ # @api private
46
+ class HaveGraphqlAuditEntry
47
+ # @param event [String] the required event value
48
+ def initialize(event)
49
+ @event = event
50
+ @touching = nil
51
+ @item_type = nil
52
+ end
53
+
54
+ # Requires at least one matching entry's +diff+ to contain +attribute+.
55
+ # The query must include +diff { attribute }+ for this chain to work.
56
+ #
57
+ # @param attribute [Symbol, String] the changed attribute name
58
+ # @return [self]
59
+ def touching(attribute)
60
+ @touching = attribute.to_s
61
+ self
62
+ end
63
+
64
+ # Requires all matching entries to have +itemType+ equal to +item_type+.
65
+ #
66
+ # @param item_type [String] the ActiveRecord model class name
67
+ # @return [self]
68
+ def for_type(item_type)
69
+ @item_type = item_type.to_s
70
+ self
71
+ end
72
+
73
+ # @api private
74
+ def matches?(response)
75
+ @response = response
76
+ matched = extract_entries(response).select { |e| e["event"] == @event }
77
+ matched = matched.select { |e| e["itemType"] == @item_type } if @item_type
78
+ if @touching
79
+ matched = matched.select { |e| e["diff"]&.any? { |d| d["attribute"] == @touching } }
80
+ end
81
+ matched.any?
82
+ end
83
+
84
+ # @api private
85
+ def failure_message
86
+ "expected GraphQL response to include an audit entry with event #{@event.inspect}" \
87
+ "#{touching_clause}#{type_clause}, but it did not.\n" \
88
+ "Entries found: #{extract_entries(@response).map { |e| e["event"] }.inspect}"
89
+ end
90
+
91
+ # @api private
92
+ def failure_message_when_negated
93
+ "expected GraphQL response not to include an audit entry with event #{@event.inspect}" \
94
+ "#{touching_clause}#{type_clause}, but one was found."
95
+ end
96
+
97
+ private
98
+
99
+ def touching_clause
100
+ @touching ? " touching #{@touching.inspect}" : ""
101
+ end
102
+
103
+ def type_clause
104
+ @item_type ? " for type #{@item_type.inspect}" : ""
105
+ end
106
+
107
+ def extract_entries(response)
108
+ data = response.respond_to?(:[]) ? (response["data"] || response[:data]) : nil
109
+ return [] unless data
110
+
111
+ entries = []
112
+ entries += Array(data["auditLogEntries"])
113
+ entries += Array(data.dig("auditLogEntriesConnection", "nodes"))
114
+ entries += Array(data.dig("auditLogEntriesConnection", "edges")).filter_map { |e| e["node"] }
115
+ entries
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "testing/rspec_matchers"
4
+ require_relative "testing/minitest_assertions"
5
+
6
+ module RailsAuditLog
7
+ module Graphql
8
+ # Test helpers for asserting GraphQL audit log entries in query responses.
9
+ #
10
+ # Require this file from your test helper to load both RSpec matchers and
11
+ # Minitest assertions:
12
+ #
13
+ # # spec/spec_helper.rb or test/test_helper.rb
14
+ # require "rails_audit_log/graphql/testing"
15
+ #
16
+ # Then include the appropriate module for your test framework:
17
+ #
18
+ # # RSpec
19
+ # RSpec.configure do |config|
20
+ # config.include RailsAuditLog::Graphql::Testing::RSpecMatchers
21
+ # end
22
+ #
23
+ # # Minitest
24
+ # class ActiveSupport::TestCase
25
+ # include RailsAuditLog::Graphql::Testing::MinitestAssertions
26
+ # end
27
+ module Testing
28
+ end
29
+ end
30
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RailsAuditLog
4
4
  module Graphql
5
- VERSION = "0.6.1"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
@@ -19,7 +19,24 @@ require_relative "graphql/subscriptions/audit_log_subscriptions_mixin"
19
19
  require_relative "graphql/subscriptions/broadcaster"
20
20
  require_relative "graphql/schema_plugin"
21
21
 
22
+ # The top-level namespace for the rails_audit_log gem.
22
23
  module RailsAuditLog
24
+ # GraphQL API layer for rails_audit_log.
25
+ #
26
+ # Provides ready-made types, queries, subscriptions, and test helpers for
27
+ # exposing {https://github.com/eclectic-coding/rails_audit_log rails_audit_log}
28
+ # audit log entries through a graphql-ruby schema.
29
+ #
30
+ # == Configuration
31
+ #
32
+ # Override query-protection defaults in an initializer:
33
+ #
34
+ # RailsAuditLog::Graphql.max_complexity = 500
35
+ # RailsAuditLog::Graphql.max_depth = 15
36
+ # RailsAuditLog::Graphql.default_max_page_size = 50
37
+ #
38
+ # These values are picked up by {SchemaPlugin} when it is included in the
39
+ # host schema.
23
40
  module Graphql
24
41
  class Error < StandardError; end
25
42
 
@@ -28,7 +45,23 @@ module RailsAuditLog
28
45
  @default_max_page_size = 25
29
46
 
30
47
  class << self
31
- attr_accessor :max_complexity, :max_depth, :default_max_page_size
48
+ # Maximum allowed query complexity score.
49
+ # Queries whose field-complexity sum exceeds this value are rejected.
50
+ # Default: +200+.
51
+ # @return [Integer]
52
+ attr_accessor :max_complexity
53
+
54
+ # Maximum allowed query depth.
55
+ # Queries nested deeper than this value are rejected.
56
+ # Default: +10+.
57
+ # @return [Integer]
58
+ attr_accessor :max_depth
59
+
60
+ # Default maximum page size for Relay connections.
61
+ # Used by graphql-ruby when calculating connection complexity.
62
+ # Default: +25+.
63
+ # @return [Integer]
64
+ attr_accessor :default_max_page_size
32
65
  end
33
66
  end
34
67
  end
@@ -168,5 +168,38 @@ module RailsAuditLog
168
168
  def check_authentication!: () -> void
169
169
  end
170
170
  end
171
+
172
+ module Testing
173
+ module RSpecMatchers
174
+ def have_graphql_audit_entry: (Symbol | String event) -> HaveGraphqlAuditEntry
175
+
176
+ class HaveGraphqlAuditEntry
177
+ def initialize: (String event) -> void
178
+ def touching: (Symbol | String attribute) -> self
179
+ def for_type: (String item_type) -> self
180
+ def matches?: (untyped response) -> bool
181
+ def failure_message: () -> String
182
+ def failure_message_when_negated: () -> String
183
+ end
184
+ end
185
+
186
+ module MinitestAssertions
187
+ def assert_graphql_audit_entry: (
188
+ untyped response,
189
+ event: Symbol | String,
190
+ ?touching: (Symbol | String)?,
191
+ ?item_type: String?,
192
+ ?message: String?
193
+ ) -> void
194
+
195
+ def refute_graphql_audit_entry: (
196
+ untyped response,
197
+ event: Symbol | String,
198
+ ?touching: (Symbol | String)?,
199
+ ?item_type: String?,
200
+ ?message: String?
201
+ ) -> void
202
+ end
203
+ end
171
204
  end
172
205
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_audit_log-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -91,6 +91,9 @@ files:
91
91
  - lib/rails_audit_log/graphql/subscriptions/audit_log_entry_created.rb
92
92
  - lib/rails_audit_log/graphql/subscriptions/audit_log_subscriptions_mixin.rb
93
93
  - lib/rails_audit_log/graphql/subscriptions/broadcaster.rb
94
+ - lib/rails_audit_log/graphql/testing.rb
95
+ - lib/rails_audit_log/graphql/testing/minitest_assertions.rb
96
+ - lib/rails_audit_log/graphql/testing/rspec_matchers.rb
94
97
  - lib/rails_audit_log/graphql/types/actor_type.rb
95
98
  - lib/rails_audit_log/graphql/types/audit_log_entry_sort_field_enum.rb
96
99
  - lib/rails_audit_log/graphql/types/audit_log_entry_type.rb