RailsAuditLog::Graphql
A graphql-ruby API layer for the rails_audit_log gem. Provides ready-made GraphQL types, queries, and subscriptions for querying audit log entries — without coupling graphql-ruby to the base gem.
Table of Contents
Installation
Add to your application's Gemfile:
gem "rails_audit_log"
gem "rails_audit_log-graphql"
Then run the install generator to wire up your schema:
rails g rails_audit_log:graphql:install
This injects include RailsAuditLog::Graphql::Queries::AuditLogEntriesQueryMixin into app/graphql/types/query_type.rb. If that file doesn't exist, the generator prints the line for you to add manually.
Usage
AuditLogEntryType
RailsAuditLog::Graphql::Types::AuditLogEntryType is a graphql-ruby object type that maps directly to RailsAuditLog::AuditLogEntry.
| GraphQL field | Type | Nullable |
|---|---|---|
id |
ID |
no |
event |
String |
no |
itemType |
String |
no |
itemId |
ID |
no |
createdAt |
ISO8601DateTime |
no |
objectChanges |
JSON |
yes |
object |
JSON |
yes |
metadata |
JSON |
yes |
reason |
String |
yes |
whodunnitSnapshot |
String |
yes |
actorType |
String |
yes |
actorId |
ID |
yes |
tenantId |
String |
yes |
actor |
AuditLogActor |
yes |
auditedResource |
AuditedResource |
no |
diff |
[AuditLogDiff!] |
yes |
AuditLogActor
RailsAuditLog::Graphql::Types::ActorType — a polymorphic reference to the actor who performed the audited action. Returned by the actor field on AuditLogEntry. null when no actor was recorded.
| GraphQL field | Type | Nullable |
|---|---|---|
id |
ID |
no |
typeName |
String |
no |
AuditedResource
RailsAuditLog::Graphql::Types::AuditedResourceType — a reference to the model record that was changed. Returned by the auditedResource field on AuditLogEntry. Always present.
| GraphQL field | Type | Nullable |
|---|---|---|
id |
ID |
no |
typeName |
String |
no |
AuditLogDiff
RailsAuditLog::Graphql::Types::DiffType — a single attribute change parsed from objectChanges. Returned as a list by the diff field on AuditLogEntry. null when objectChanges is not recorded (e.g. destroy events).
| GraphQL field | Type | Nullable | Description |
|---|---|---|---|
attribute |
String |
no | Name of the changed attribute |
from |
JSON |
yes | Value before the change |
to |
JSON |
yes | Value after the change |
Example:
{
auditLogEntries(event: "update") {
diff {
attribute
from
to
}
}
}
AuditLogEntriesQueryMixin
Include RailsAuditLog::Graphql::Queries::AuditLogEntriesQueryMixin into your app's QueryType to add two fields:
# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
include RailsAuditLog::Graphql::Queries::AuditLogEntriesQueryMixin
end
auditLogEntry(id: ID!): AuditLogEntry
Fetch a single entry by ID. Returns nil if not found.
auditLogEntries(...): [AuditLogEntry!]!
List entries with optional filters and offset pagination.
| Argument | Type | Default | Description |
|---|---|---|---|
event |
String |
— | Filter by event type (create, update, destroy) |
itemType |
String |
— | Filter by audited model class name |
itemId |
ID |
— | Filter by audited record ID |
actorId |
ID |
— | Filter by actor ID |
actorType |
String |
— | Filter by actor model class name (e.g. "User") |
since |
ISO8601DateTime |
— | Return entries created at or after this time |
until |
ISO8601DateTime |
— | Return entries created at or before this time |
touching |
String |
— | Filter to entries that changed a specific attribute |
orderBy |
AuditLogEntrySortInput |
CREATED_AT DESC |
Sort field and direction |
forTenant |
String |
— | Scope to a specific tenant ID; overrides auto-tenant |
page |
Int |
1 |
Page number (1-based) |
perPage |
Int |
25 |
Results per page |
Results default to created_at DESC ordering.
auditLogEntriesConnection(...): AuditLogEntryConnection!
Same filters as auditLogEntries, but returns a Relay-style connection for cursor-based pagination.
| Argument | Type | Description |
|---|---|---|
event |
String |
Filter by event type (create, update, destroy) |
itemType |
String |
Filter by audited model class name |
itemId |
ID |
Filter by audited record ID |
actorId |
ID |
Filter by actor ID |
actorType |
String |
Filter by actor model class name (e.g. "User") |
forTenant |
String |
Scope to a specific tenant ID; overrides auto-tenant |
first |
Int |
Return the first N edges after after |
after |
String |
Cursor to paginate forward from |
last |
Int |
Return the last N edges before before |
before |
String |
Cursor to paginate backward from |
Results are ordered by created_at DESC.
Example — first page:
{
auditLogEntriesConnection(first: 25) {
nodes {
id
event
itemType
itemId
createdAt
}
pageInfo {
hasNextPage
endCursor
}
}
}
Example — next page using a cursor:
{
auditLogEntriesConnection(first: 25, after: "eyJpZCI6NDJ9") {
nodes { id event }
pageInfo { hasNextPage endCursor }
}
}
auditLogEntriesCount(...): Int!
Returns the count of matching audit log entries. Respects auto-tenant when RailsAuditLog.current_tenant is configured.
| Argument | Type | Description |
|---|---|---|
event |
String |
Filter by event type (create, update, destroy) |
itemType |
String |
Filter by audited model class name |
actorType |
String |
Filter by actor model class name (e.g. "User") |
since |
ISO8601DateTime |
Count entries created at or after this time |
forTenant |
String |
Scope to a specific tenant ID; overrides auto-tenant |
{ auditLogEntriesCount(event: "update", itemType: "Post") }
Tenant scoping
When RailsAuditLog.current_tenant is configured, all queries automatically filter to the current tenant:
RailsAuditLog.configure do |c|
c.current_tenant { Current.tenant_id }
end
To override or explicitly specify a tenant per query, use the forTenant: argument (available on auditLogEntry, auditLogEntries, and auditLogEntriesConnection):
{ auditLogEntries(forTenant: "acme") { id event } }
Authentication
If RailsAuditLog.authenticate is configured, the block is called with the GraphQL context before every query. Return a truthy value to allow access; return falsy to raise GraphQL::ExecutionError with "Unauthorized".
RailsAuditLog.configure do |config|
config.authenticate { |ctx| ctx[:current_user]&.admin? }
end
If no authenticate block is set, all queries are permitted.
SchemaPlugin
Include RailsAuditLog::Graphql::SchemaPlugin into your schema to enable query protection and dataloader batching in one step:
class MySchema < GraphQL::Schema
include RailsAuditLog::Graphql::SchemaPlugin
query Types::QueryType
end
This applies the following defaults (all overridable via RailsAuditLog::Graphql.*=):
| Setting | Default | Description |
|---|---|---|
max_complexity |
200 |
Reject queries whose field-complexity sum exceeds this |
max_depth |
10 |
Reject queries nested deeper than this |
default_max_page_size |
25 |
Assumed page size for connection complexity calculation |
Override in an initializer:
RailsAuditLog::Graphql.max_complexity = 500
RailsAuditLog::Graphql.max_depth = 15
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.
auditLogReify(itemType:, itemId:, at:): AuditLogJson
Reconstructs the attribute state of a record at a given point in time. Returns the attributes as AuditLogJson, or nil when no entry exists at or before at or the record was destroyed at that time. Accepts forTenant: and respects auto-tenant.
{
auditLogReify(itemType: "Post", itemId: "42", at: "2026-01-15T12:00:00Z") {
title
publishedAt
}
}
Subscriptions
Requires Action Cable in the host application.
AuditLogSubscriptionsMixin
Include RailsAuditLog::Graphql::Subscriptions::AuditLogSubscriptionsMixin into your app's SubscriptionType to add the auditLogEntryCreated field. Your schema must also use GraphQL::Subscriptions::ActionCableSubscriptions.
# app/graphql/types/subscription_type.rb
class Types::SubscriptionType < Types::BaseObject
include RailsAuditLog::Graphql::Subscriptions::AuditLogSubscriptionsMixin
end
# app/graphql/my_schema.rb
class MySchema < GraphQL::Schema
query Types::QueryType
subscription Types::SubscriptionType
use GraphQL::Subscriptions::ActionCableSubscriptions
end
auditLogEntryCreated
Fires when a new RailsAuditLog::AuditLogEntry is created. Accepts one of two argument combinations:
| Argument | Type | Description |
|---|---|---|
itemType |
String |
Model class name to scope the subscription to |
itemId |
ID |
Record ID to scope the subscription to |
actorId |
ID |
Actor ID — subscribe to all entries by a specific actor |
Subscribe to all changes on a specific record:
subscription {
auditLogEntryCreated(itemType: "Post", itemId: "42") {
id
event
diff { attribute from to }
}
}
Subscribe to all entries by a specific actor:
subscription {
auditLogEntryCreated(actorId: "7") {
id
event
itemType
itemId
}
}
Broadcaster
RailsAuditLog::Graphql::Subscriptions::Broadcaster bridges ActiveSupport::Notifications (fired by RailsAuditLog::Streaming::NotificationsAdapter or ActiveJobAdapter) to GraphQL subscription triggers. Start it in an initializer:
# config/initializers/rails_audit_log_graphql.rb
RailsAuditLog.configure do |c|
c.streaming_adapter = RailsAuditLog::Streaming::NotificationsAdapter.new
end
Rails.application.config.after_initialize do
RailsAuditLog::Graphql::Subscriptions::Broadcaster.new(schema: MySchema).start
end
For each entry, the broadcaster triggers:
auditLogEntryCreated(itemType:, itemId:)— notifies record-specific subscribersauditLogEntryCreated(actorId:)— notifies actor-specific subscribers (when an actor is present)
Testing
rails_audit_log-graphql ships test helpers for both RSpec and Minitest so you can
assert that your mutations actually produce the expected audit log entries.
Require the testing helpers from your test helper:
# spec/spec_helper.rb or test/test_helper.rb
require "rails_audit_log/graphql/testing"
RSpec matchers
Include RailsAuditLog::Graphql::Testing::RSpecMatchers in your configuration:
RSpec.configure do |config|
config.include RailsAuditLog::Graphql::Testing::RSpecMatchers
end
Then use have_graphql_audit_entry against any GraphQL::Query::Result (or plain hash) returned by Schema.execute:
result = MySchema.execute('{ auditLogEntries { event diff { attribute } } }')
# Assert an entry with a specific event exists
expect(result).to have_graphql_audit_entry(:update)
# Assert the update entry touched the :title attribute
# (requires diff { attribute } in the query)
expect(result).to have_graphql_audit_entry(:update).touching(:title)
# Scope to a specific model
expect(result).to have_graphql_audit_entry(:create).for_type("Post")
# Combine chains
expect(result).to have_graphql_audit_entry(:update).for_type("Post").touching(:title)
The matcher searches auditLogEntries and auditLogEntriesConnection.nodes in the
response data.
Minitest assertions
Include RailsAuditLog::Graphql::Testing::MinitestAssertions in your test class:
class ActiveSupport::TestCase
include RailsAuditLog::Graphql::Testing::MinitestAssertions
end
Use assert_graphql_audit_entry and refute_graphql_audit_entry:
result = MySchema.execute('{ auditLogEntries { event diff { attribute } } }')
# Assert an update entry exists
assert_graphql_audit_entry result, event: :update
# Assert an update entry that touched :title
assert_graphql_audit_entry result, event: :update, touching: :title
# Assert no destroy entry exists
refute_graphql_audit_entry result, event: :destroy
# Custom failure message
assert_graphql_audit_entry result, event: :update, message: "mutation should have created an update entry"
Development
bin/setup # install dependencies
bundle exec rake # lint + tests
Contributing
See CONTRIBUTING.md.
License
The gem is available as open source under the terms of the MIT License.