rails_audit_log-graphql 0.5.0 → 0.6.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: f64cce54638f8999c6489e0881a1fedfcb447b0b5b8f52270a0d8247719c8f19
4
- data.tar.gz: 4b1225952a5a6adce3a5a03b804360765b0c70b9ad9a9688db96ba34139430c5
3
+ metadata.gz: 6ca2df96cd0a69c6449fc8585832293c9a06d487a7525f40c1d15762c61710b5
4
+ data.tar.gz: 8262257281bef88c4ab07700ec88b5755ca6b2a095fd879bf5462d403ec1ac00
5
5
  SHA512:
6
- metadata.gz: 0b54b242a022b49d1917dd3c0370f96bde5ec210dad8959a34bc424132c7efd97aa0e1f781cebd0eaf21056b3b29c2c0c9632c08e94a4b0e2da851afe57044e5
7
- data.tar.gz: fb448e67961e641eb6ce89081e832082cfa886856ccbd5f4d5de767d1bd21835d38034e6dd55a4d00b1f633f58da6a094e5d8e4e4886c24a6aece095386023f2
6
+ metadata.gz: 4598525fe46095cbd1209e9bc3af297950526eb9c7ecfab9e382ee3554f376e73b0db3073986d49cdd05190e42f90e1d09e3f1a00c3ecb948a0f02d04f5ba9f0
7
+ data.tar.gz: 2069e44509afdccf33c443bcf443da4480e83e60034f73bf24b0a7139ab012910d60e17dd85d06897b0642a6e4cb0d0d548a08c6bfbd5ac8318f6bd787d998b3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-06-04
4
+
5
+ ### Added
6
+
7
+ - `AuditLogJsonScalar` (`AuditLogJson`) — custom scalar type for `objectChanges`, `object`, and `metadata` fields; replaces the generic `JSON` scalar with a self-documenting type
8
+ - `RecordByIdSource` — `GraphQL::Dataloader::Source` that batch-loads AR records by class name and ID; eliminates N+1 queries when resolving `actor.record` and `auditedResource.record` on list responses
9
+ - `record` field on `AuditLogActor` and `AuditedResource` — returns the database record as JSON, batch-loaded via dataloader
10
+ - `auditLogReify(itemType:, itemId:, at:)` — reconstructs the attribute state of a record at a given point in time; returns `JSON` or `nil` when the record was destroyed or no entry exists
11
+ - `SchemaPlugin` — include into host schema to apply `max_complexity` (200), `max_depth` (10), `default_max_page_size` (25), and `use GraphQL::Dataloader`; all limits configurable via `RailsAuditLog::Graphql.max_complexity=` etc.
12
+
3
13
  ## [0.5.0] - 2026-06-04
4
14
 
5
15
  ### Added
data/README.md CHANGED
@@ -23,6 +23,8 @@ A [graphql-ruby](https://graphql-ruby.org) API layer for the [`rails_audit_log`]
23
23
  - [auditLogEntriesCount](#auditlogentriescount-int)
24
24
  - [Tenant scoping](#tenant-scoping)
25
25
  - [Authentication](#authentication)
26
+ - [SchemaPlugin](#schemaplugin)
27
+ - [auditLogReify](#auditlogreify)
26
28
  - [Subscriptions](#subscriptions)
27
29
  - [AuditLogSubscriptionsMixin](#auditlogsubscriptionsmixin)
28
30
  - [auditLogEntryCreated](#auditlogentrycreated)
@@ -259,6 +261,51 @@ If no authenticate block is set, all queries are permitted.
259
261
 
260
262
  [↑ Back to top](#table-of-contents)
261
263
 
264
+ ### SchemaPlugin
265
+
266
+ Include `RailsAuditLog::Graphql::SchemaPlugin` into your schema to enable query protection and dataloader batching in one step:
267
+
268
+ ```ruby
269
+ class MySchema < GraphQL::Schema
270
+ include RailsAuditLog::Graphql::SchemaPlugin
271
+ query Types::QueryType
272
+ end
273
+ ```
274
+
275
+ This applies the following defaults (all overridable via `RailsAuditLog::Graphql.*=`):
276
+
277
+ | Setting | Default | Description |
278
+ |---|---|---|
279
+ | `max_complexity` | `200` | Reject queries whose field-complexity sum exceeds this |
280
+ | `max_depth` | `10` | Reject queries nested deeper than this |
281
+ | `default_max_page_size` | `25` | Assumed page size for connection complexity calculation |
282
+
283
+ Override in an initializer:
284
+
285
+ ```ruby
286
+ RailsAuditLog::Graphql.max_complexity = 500
287
+ RailsAuditLog::Graphql.max_depth = 15
288
+ ```
289
+
290
+ The plugin also adds `AuditLogActor.record` and `AuditedResource.record` fields — nullable JSON fields that load the actual database record via `RecordByIdSource`, a `GraphQL::Dataloader::Source` that batches loads by class name to eliminate N+1 queries on list responses.
291
+
292
+ [↑ Back to top](#table-of-contents)
293
+
294
+ ### `auditLogReify(itemType:, itemId:, at:): JSON`
295
+
296
+ Reconstructs the attribute state of a record at a given point in time. Returns the attributes as JSON, or `nil` when no entry exists at or before `at` or the record was destroyed at that time.
297
+
298
+ ```graphql
299
+ {
300
+ auditLogReify(itemType: "Post", itemId: "42", at: "2026-01-15T12:00:00Z") {
301
+ title
302
+ publishedAt
303
+ }
304
+ }
305
+ ```
306
+
307
+ [↑ Back to top](#table-of-contents)
308
+
262
309
  ### Subscriptions
263
310
 
264
311
  Requires Action Cable in the host application.
data/ROADMAP.md CHANGED
@@ -4,15 +4,6 @@ This gem adds a GraphQL API layer on top of [`rails_audit_log`](https://github.c
4
4
 
5
5
  ---
6
6
 
7
- ## 0.6.0 — Performance & Safety
8
-
9
- - **Dataloader batch loading** — batch-resolve polymorphic `actor` and `item` associations using graphql-ruby's `dataloader` to eliminate N+1 queries on list responses
10
- - **`auditLogReify` query** — `auditLogReify(itemType:, itemId:, at:)` returns the reconstructed object state as JSON at a given point in time, backed by `RailsAuditLog.version_at`
11
- - **Query complexity & depth limits** — built-in defaults (`max_complexity`, `max_depth`) with a config override (`RailsAuditLogGraphql.max_complexity = 200`) to protect against expensive queries before the API is declared stable
12
- - **`AuditLogJsonScalar`** — proper JSON scalar type for `objectChanges` and `metadata` fields, replacing opaque String serialization and making the schema self-documenting
13
-
14
- ---
15
-
16
7
  ## 1.0.0 — Stable API
17
8
 
18
9
  - Full YARD documentation
@@ -58,6 +58,18 @@ module RailsAuditLog
58
58
  description: "Scope to a specific tenant ID. Overrides auto-tenant when RailsAuditLog.current_tenant is configured."
59
59
  end
60
60
 
61
+ base.field(
62
+ :audit_log_reify,
63
+ GraphQL::Types::JSON,
64
+ null: true,
65
+ description: "Reconstruct the attribute state of a record at a given point in time. Returns nil when no entry exists at or before `at`, or when the record was destroyed.",
66
+ resolver_method: :resolve_audit_log_reify
67
+ ) do
68
+ argument :item_type, String, required: true, description: "The audited model class name."
69
+ argument :item_id, GraphQL::Types::ID, required: true, description: "The audited record ID."
70
+ argument :at, GraphQL::Types::ISO8601DateTime, required: true, description: "Reconstruct state as of this time."
71
+ end
72
+
61
73
  base.field(
62
74
  :audit_log_entries_count,
63
75
  GraphQL::Types::Int,
@@ -89,6 +101,18 @@ module RailsAuditLog
89
101
  build_scope(event: event, item_type: item_type, item_id: item_id, actor_id: actor_id, since: since, until_time: until_time, touching: touching, order_by: order_by, for_tenant: for_tenant)
90
102
  end
91
103
 
104
+ def resolve_audit_log_reify(item_type:, item_id:, at:)
105
+ check_authentication!
106
+ entry = RailsAuditLog::AuditLogEntry
107
+ .where(item_type: item_type, item_id: item_id)
108
+ .where("created_at <= ?", at)
109
+ .order(created_at: :desc, id: :desc)
110
+ .first
111
+ return nil if entry.nil? || entry.event == "destroy"
112
+ to_attrs = (entry.object_changes || {}).transform_values { |v| v[1] }
113
+ entry.object.present? ? entry.object.merge(to_attrs) : to_attrs
114
+ end
115
+
92
116
  def resolve_audit_log_entries_count(event: nil, item_type: nil, since: nil)
93
117
  check_authentication!
94
118
  scope = RailsAuditLog::AuditLogEntry.all
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAuditLog
4
+ module Graphql
5
+ module SchemaPlugin
6
+ def self.included(base)
7
+ base.max_complexity(RailsAuditLog::Graphql.max_complexity)
8
+ base.max_depth(RailsAuditLog::Graphql.max_depth)
9
+ base.default_max_page_size(RailsAuditLog::Graphql.default_max_page_size)
10
+ base.use(GraphQL::Dataloader)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAuditLog
4
+ module Graphql
5
+ module Sources
6
+ class RecordByIdSource < GraphQL::Dataloader::Source
7
+ def initialize(class_name)
8
+ @class_name = class_name
9
+ end
10
+
11
+ def fetch(ids)
12
+ klass = @class_name.safe_constantize
13
+ return ids.map { nil } unless klass
14
+
15
+ records = klass.where(id: ids).index_by { |r| r.id.to_s }
16
+ ids.map { |id| records[id]&.attributes }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -9,6 +9,8 @@ module RailsAuditLog
9
9
 
10
10
  field :id, GraphQL::Types::ID, null: false, description: "The actor's ID."
11
11
  field :type_name, String, null: false, description: "The actor's model class name (e.g. \"User\")."
12
+ field :record, GraphQL::Types::JSON, null: true,
13
+ description: "The actor record loaded from the database, serialized as JSON. Batch-loaded via dataloader."
12
14
 
13
15
  def id
14
16
  object[:id]
@@ -17,6 +19,11 @@ module RailsAuditLog
17
19
  def type_name
18
20
  object[:type_name]
19
21
  end
22
+
23
+ def record
24
+ return nil unless object[:id] && object[:type_name]
25
+ dataloader.with(Sources::RecordByIdSource, object[:type_name]).load(object[:id].to_s)
26
+ end
20
27
  end
21
28
  end
22
29
  end
@@ -11,9 +11,9 @@ module RailsAuditLog
11
11
  field :event, String, null: false
12
12
  field :item_type, String, null: false
13
13
  field :item_id, GraphQL::Types::ID, null: false
14
- field :object_changes, GraphQL::Types::JSON, null: true
15
- field :object, GraphQL::Types::JSON, null: true, method_conflict_warning: false
16
- field :metadata, GraphQL::Types::JSON, null: true
14
+ field :object_changes, Types::AuditLogJsonScalar, null: true
15
+ field :object, Types::AuditLogJsonScalar, null: true, method_conflict_warning: false
16
+ field :metadata, Types::AuditLogJsonScalar, null: true
17
17
  field :reason, String, null: true
18
18
  field :whodunnit_snapshot, String, null: true
19
19
  field :actor_type, String, null: true
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAuditLog
4
+ module Graphql
5
+ module Types
6
+ class AuditLogJsonScalar < GraphQL::Schema::Scalar
7
+ graphql_name "AuditLogJson"
8
+ description "A JSON blob stored on an audit log entry (objectChanges, object, or metadata)."
9
+
10
+ def self.coerce_input(value, _ctx)
11
+ value.is_a?(Hash) ? value : JSON.parse(value)
12
+ rescue JSON::ParserError
13
+ raise GraphQL::CoercionError, "#{value.inspect} is not valid JSON"
14
+ end
15
+
16
+ def self.coerce_result(value, _ctx)
17
+ value
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -9,6 +9,8 @@ module RailsAuditLog
9
9
 
10
10
  field :id, GraphQL::Types::ID, null: false, description: "The audited record's ID."
11
11
  field :type_name, String, null: false, description: "The audited model class name (e.g. \"Post\")."
12
+ field :record, GraphQL::Types::JSON, null: true,
13
+ description: "The audited record loaded from the database, serialized as JSON. Batch-loaded via dataloader."
12
14
 
13
15
  def id
14
16
  object[:id]
@@ -17,6 +19,10 @@ module RailsAuditLog
17
19
  def type_name
18
20
  object[:type_name]
19
21
  end
22
+
23
+ def record
24
+ dataloader.with(Sources::RecordByIdSource, object[:type_name]).load(object[:id].to_s)
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RailsAuditLog
4
4
  module Graphql
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -4,6 +4,7 @@ require "graphql"
4
4
  require_relative "graphql/version"
5
5
  require_relative "graphql/types/base_object"
6
6
  require_relative "graphql/types/base_subscription"
7
+ require_relative "graphql/types/audit_log_json_scalar"
7
8
  require_relative "graphql/types/diff_type"
8
9
  require_relative "graphql/types/actor_type"
9
10
  require_relative "graphql/types/audited_resource_type"
@@ -11,13 +12,23 @@ require_relative "graphql/types/audit_log_entry_type"
11
12
  require_relative "graphql/types/sort_direction_enum"
12
13
  require_relative "graphql/types/audit_log_entry_sort_field_enum"
13
14
  require_relative "graphql/input_objects/audit_log_entry_sort_input"
15
+ require_relative "graphql/sources/record_by_id_source"
14
16
  require_relative "graphql/queries/audit_log_entries_query_mixin"
15
17
  require_relative "graphql/subscriptions/audit_log_entry_created"
16
18
  require_relative "graphql/subscriptions/audit_log_subscriptions_mixin"
17
19
  require_relative "graphql/subscriptions/broadcaster"
20
+ require_relative "graphql/schema_plugin"
18
21
 
19
22
  module RailsAuditLog
20
23
  module Graphql
21
24
  class Error < StandardError; end
25
+
26
+ @max_complexity = 200
27
+ @max_depth = 10
28
+ @default_max_page_size = 25
29
+
30
+ class << self
31
+ attr_accessor :max_complexity, :max_depth, :default_max_page_size
32
+ end
22
33
  end
23
34
  end
@@ -2,6 +2,17 @@ module RailsAuditLog
2
2
  module Graphql
3
3
  VERSION: String
4
4
 
5
+ @max_complexity: Integer
6
+ @max_depth: Integer
7
+ @default_max_page_size: Integer
8
+
9
+ def self.max_complexity: () -> Integer
10
+ def self.max_complexity=: (Integer) -> Integer
11
+ def self.max_depth: () -> Integer
12
+ def self.max_depth=: (Integer) -> Integer
13
+ def self.default_max_page_size: () -> Integer
14
+ def self.default_max_page_size=: (Integer) -> Integer
15
+
5
16
  module Types
6
17
  class BaseObject < GraphQL::Schema::Object
7
18
  end
@@ -9,6 +20,11 @@ module RailsAuditLog
9
20
  class BaseSubscription < GraphQL::Schema::Subscription
10
21
  end
11
22
 
23
+ class AuditLogJsonScalar < GraphQL::Schema::Scalar
24
+ def self.coerce_input: (untyped value, untyped ctx) -> Hash[String, untyped]
25
+ def self.coerce_result: (untyped value, untyped ctx) -> untyped
26
+ end
27
+
12
28
  class DiffType < BaseObject
13
29
  def attribute: () -> String
14
30
  def from: () -> untyped
@@ -18,11 +34,13 @@ module RailsAuditLog
18
34
  class ActorType < BaseObject
19
35
  def id: () -> String
20
36
  def type_name: () -> String
37
+ def record: () -> Hash[String, untyped]?
21
38
  end
22
39
 
23
40
  class AuditedResourceType < BaseObject
24
41
  def id: () -> String
25
42
  def type_name: () -> String
43
+ def record: () -> Hash[String, untyped]?
26
44
  end
27
45
 
28
46
  class AuditLogEntryType < BaseObject
@@ -54,6 +72,17 @@ module RailsAuditLog
54
72
  end
55
73
  end
56
74
 
75
+ module Sources
76
+ class RecordByIdSource < GraphQL::Dataloader::Source
77
+ def initialize: (String class_name) -> void
78
+ def fetch: (Array[String] ids) -> Array[Hash[String, untyped]?]
79
+ end
80
+ end
81
+
82
+ module SchemaPlugin
83
+ def self.included: (untyped base) -> void
84
+ end
85
+
57
86
  module Subscriptions
58
87
  class AuditLogEntryCreated < Types::BaseSubscription
59
88
  def subscribe: (?item_type: String?, ?item_id: String?, ?actor_id: String?) -> :no_response
@@ -104,6 +133,12 @@ module RailsAuditLog
104
133
  ?for_tenant: String?
105
134
  ) -> untyped
106
135
 
136
+ def resolve_audit_log_reify: (
137
+ item_type: String,
138
+ item_id: String,
139
+ at: Time
140
+ ) -> Hash[String, untyped]?
141
+
107
142
  def resolve_audit_log_entries_count: (
108
143
  ?event: String?,
109
144
  ?item_type: String?,
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -86,12 +86,15 @@ files:
86
86
  - lib/rails_audit_log/graphql/input_objects/audit_log_entry_sort_input.rb
87
87
  - lib/rails_audit_log/graphql/queries/audit_log_entries_query_mixin.rb
88
88
  - lib/rails_audit_log/graphql/release_tooling.rb
89
+ - lib/rails_audit_log/graphql/schema_plugin.rb
90
+ - lib/rails_audit_log/graphql/sources/record_by_id_source.rb
89
91
  - lib/rails_audit_log/graphql/subscriptions/audit_log_entry_created.rb
90
92
  - lib/rails_audit_log/graphql/subscriptions/audit_log_subscriptions_mixin.rb
91
93
  - lib/rails_audit_log/graphql/subscriptions/broadcaster.rb
92
94
  - lib/rails_audit_log/graphql/types/actor_type.rb
93
95
  - lib/rails_audit_log/graphql/types/audit_log_entry_sort_field_enum.rb
94
96
  - lib/rails_audit_log/graphql/types/audit_log_entry_type.rb
97
+ - lib/rails_audit_log/graphql/types/audit_log_json_scalar.rb
95
98
  - lib/rails_audit_log/graphql/types/audited_resource_type.rb
96
99
  - lib/rails_audit_log/graphql/types/base_object.rb
97
100
  - lib/rails_audit_log/graphql/types/base_subscription.rb