rails_audit_log-graphql 0.6.0 → 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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +85 -2
- data/ROADMAP.md +1 -7
- data/lib/generators/rails_audit_log/graphql/install/install_generator.rb +22 -1
- data/lib/rails_audit_log/graphql/queries/audit_log_entries_query_mixin.rb +61 -12
- data/lib/rails_audit_log/graphql/schema_plugin.rb +22 -0
- data/lib/rails_audit_log/graphql/sources/record_by_id_source.rb +14 -0
- data/lib/rails_audit_log/graphql/subscriptions/audit_log_subscriptions_mixin.rb +19 -0
- data/lib/rails_audit_log/graphql/subscriptions/broadcaster.rb +31 -0
- data/lib/rails_audit_log/graphql/testing/minitest_assertions.rb +87 -0
- data/lib/rails_audit_log/graphql/testing/rspec_matchers.rb +121 -0
- data/lib/rails_audit_log/graphql/testing.rb +30 -0
- data/lib/rails_audit_log/graphql/version.rb +1 -1
- data/lib/rails_audit_log/graphql.rb +34 -1
- data/sig/rails_audit_log/graphql.rbs +41 -2
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 681be68e65f4b6f543de2ce47434d6b547db12299d15c2f136d94f1db5b15355
|
|
4
|
+
data.tar.gz: 184994ef369aea31293293d01e22663bb1e3dc48f3e958aec6f523997ec9ed7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d06f3473ed427c34208a6067d4e77a445a10068e8ffc269f4725a858092c1a55c9dd12b504632df53e260f5e5b3b0b6c3aa8eab3a4499c475bdb165165d9403
|
|
7
|
+
data.tar.gz: a9c2936cc464d8288a815f1345b12c23918aaf7a70a15241e77e50ccf90ce0daefd27e172f1f554e7861d7d0e3d6bb9a34557cde7c614f162652795d5288a287
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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
|
+
|
|
16
|
+
## [0.6.1] - 2026-06-04
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `actorType:` filter argument on `auditLogEntries`, `auditLogEntriesConnection`, and `auditLogEntriesCount` — filter by actor model class name (e.g. `"User"`)
|
|
21
|
+
- `forTenant:` argument on `auditLogReify` and `auditLogEntriesCount` — consistent tenant scoping across all query fields; both also respect `RailsAuditLog.current_tenant` auto-tenant
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `auditLogReify` return type changed from generic `JSON` to `AuditLogJson` for consistency with other entry fields
|
|
26
|
+
- `rails g rails_audit_log:graphql:install` now also injects `SchemaPlugin` into the host schema file (detected via `app/graphql/**/*schema*.rb` glob); `print_next_steps` updated to mention all available queries and the complexity config override
|
|
27
|
+
|
|
3
28
|
## [0.6.0] - 2026-06-04
|
|
4
29
|
|
|
5
30
|
### 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)
|
|
@@ -152,6 +155,7 @@ List entries with optional filters and offset pagination.
|
|
|
152
155
|
| `itemType` | `String` | — | Filter by audited model class name |
|
|
153
156
|
| `itemId` | `ID` | — | Filter by audited record ID |
|
|
154
157
|
| `actorId` | `ID` | — | Filter by actor ID |
|
|
158
|
+
| `actorType` | `String` | — | Filter by actor model class name (e.g. `"User"`) |
|
|
155
159
|
| `since` | `ISO8601DateTime` | — | Return entries created at or after this time |
|
|
156
160
|
| `until` | `ISO8601DateTime` | — | Return entries created at or before this time |
|
|
157
161
|
| `touching` | `String` | — | Filter to entries that changed a specific attribute |
|
|
@@ -172,6 +176,7 @@ Same filters as `auditLogEntries`, but returns a [Relay-style connection](https:
|
|
|
172
176
|
| `itemType` | `String` | Filter by audited model class name |
|
|
173
177
|
| `itemId` | `ID` | Filter by audited record ID |
|
|
174
178
|
| `actorId` | `ID` | Filter by actor ID |
|
|
179
|
+
| `actorType` | `String` | Filter by actor model class name (e.g. `"User"`) |
|
|
175
180
|
| `forTenant` | `String` | Scope to a specific tenant ID; overrides auto-tenant |
|
|
176
181
|
| `first` | `Int` | Return the first N edges after `after` |
|
|
177
182
|
| `after` | `String` | Cursor to paginate forward from |
|
|
@@ -221,7 +226,9 @@ Returns the count of matching audit log entries. Respects auto-tenant when `Rail
|
|
|
221
226
|
|---|---|---|
|
|
222
227
|
| `event` | `String` | Filter by event type (`create`, `update`, `destroy`) |
|
|
223
228
|
| `itemType` | `String` | Filter by audited model class name |
|
|
229
|
+
| `actorType` | `String` | Filter by actor model class name (e.g. `"User"`) |
|
|
224
230
|
| `since` | `ISO8601DateTime` | Count entries created at or after this time |
|
|
231
|
+
| `forTenant` | `String` | Scope to a specific tenant ID; overrides auto-tenant |
|
|
225
232
|
|
|
226
233
|
```graphql
|
|
227
234
|
{ auditLogEntriesCount(event: "update", itemType: "Post") }
|
|
@@ -291,9 +298,9 @@ The plugin also adds `AuditLogActor.record` and `AuditedResource.record` fields
|
|
|
291
298
|
|
|
292
299
|
[↑ Back to top](#table-of-contents)
|
|
293
300
|
|
|
294
|
-
### `auditLogReify(itemType:, itemId:, at:):
|
|
301
|
+
### `auditLogReify(itemType:, itemId:, at:): AuditLogJson`
|
|
295
302
|
|
|
296
|
-
Reconstructs the attribute state of a record at a given point in time. Returns the attributes as
|
|
303
|
+
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.
|
|
297
304
|
|
|
298
305
|
```graphql
|
|
299
306
|
{
|
|
@@ -384,6 +391,82 @@ For each entry, the broadcaster triggers:
|
|
|
384
391
|
|
|
385
392
|
[↑ Back to top](#table-of-contents)
|
|
386
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
|
+
|
|
387
470
|
## Development
|
|
388
471
|
|
|
389
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
|
-
|
|
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._
|
|
@@ -7,10 +7,11 @@ module RailsAuditLog
|
|
|
7
7
|
module Graphql
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
|
10
|
-
desc "Injects AuditLogEntriesQueryMixin into your GraphQL QueryType."
|
|
10
|
+
desc "Injects AuditLogEntriesQueryMixin into your GraphQL QueryType and SchemaPlugin into your schema."
|
|
11
11
|
|
|
12
12
|
QUERY_TYPE_PATH = "app/graphql/types/query_type.rb"
|
|
13
13
|
MIXIN = "RailsAuditLog::Graphql::Queries::AuditLogEntriesQueryMixin"
|
|
14
|
+
SCHEMA_PLUGIN = "RailsAuditLog::Graphql::SchemaPlugin"
|
|
14
15
|
|
|
15
16
|
def inject_mixin
|
|
16
17
|
if File.exist?(File.join(destination_root, QUERY_TYPE_PATH))
|
|
@@ -24,11 +25,31 @@ module RailsAuditLog
|
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
def inject_schema_plugin
|
|
29
|
+
schema_files = Dir.glob(File.join(destination_root, "app/graphql/**/*schema*.rb"))
|
|
30
|
+
if schema_files.any?
|
|
31
|
+
schema_path = schema_files.first.delete_prefix("#{destination_root}/")
|
|
32
|
+
inject_into_file schema_path,
|
|
33
|
+
" include #{SCHEMA_PLUGIN}\n",
|
|
34
|
+
after: /class\s+\S+\s*<\s*GraphQL::Schema\s*\n/
|
|
35
|
+
else
|
|
36
|
+
say ""
|
|
37
|
+
say "No schema file found. Add this line manually to your GraphQL::Schema subclass:", :yellow
|
|
38
|
+
say " include #{SCHEMA_PLUGIN}", :green
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
27
42
|
def print_next_steps
|
|
28
43
|
say ""
|
|
29
44
|
say "Done! Your GraphQL API now has:", :green
|
|
30
45
|
say " auditLogEntry(id: ID!): AuditLogEntry"
|
|
31
46
|
say " auditLogEntries(...): [AuditLogEntry!]!"
|
|
47
|
+
say " auditLogReify(itemType:, itemId:, at:): AuditLogJson"
|
|
48
|
+
say " auditLogEntriesCount(...): Int!"
|
|
49
|
+
say ""
|
|
50
|
+
say "SchemaPlugin applies complexity/depth limits and enables dataloader."
|
|
51
|
+
say "Override defaults in an initializer:"
|
|
52
|
+
say " RailsAuditLog::Graphql.max_complexity = 500"
|
|
32
53
|
say ""
|
|
33
54
|
say "See the README for full documentation."
|
|
34
55
|
end
|
|
@@ -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,
|
|
@@ -29,6 +46,7 @@ module RailsAuditLog
|
|
|
29
46
|
argument :item_type, String, required: false, description: "Filter by audited model class name."
|
|
30
47
|
argument :item_id, GraphQL::Types::ID, required: false, description: "Filter by audited record ID."
|
|
31
48
|
argument :actor_id, GraphQL::Types::ID, required: false, description: "Filter by actor ID."
|
|
49
|
+
argument :actor_type, String, required: false, description: "Filter by actor model class name (e.g. \"User\")."
|
|
32
50
|
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Return entries created at or after this time."
|
|
33
51
|
argument :until, GraphQL::Types::ISO8601DateTime, required: false, as: :until_time, description: "Return entries created at or before this time."
|
|
34
52
|
argument :touching, String, required: false, description: "Filter to entries that changed a specific attribute (matches object_changes keys)."
|
|
@@ -50,6 +68,7 @@ module RailsAuditLog
|
|
|
50
68
|
argument :item_type, String, required: false, description: "Filter by audited model class name."
|
|
51
69
|
argument :item_id, GraphQL::Types::ID, required: false, description: "Filter by audited record ID."
|
|
52
70
|
argument :actor_id, GraphQL::Types::ID, required: false, description: "Filter by actor ID."
|
|
71
|
+
argument :actor_type, String, required: false, description: "Filter by actor model class name (e.g. \"User\")."
|
|
53
72
|
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Return entries created at or after this time."
|
|
54
73
|
argument :until, GraphQL::Types::ISO8601DateTime, required: false, as: :until_time, description: "Return entries created at or before this time."
|
|
55
74
|
argument :touching, String, required: false, description: "Filter to entries that changed a specific attribute (matches object_changes keys)."
|
|
@@ -60,7 +79,7 @@ module RailsAuditLog
|
|
|
60
79
|
|
|
61
80
|
base.field(
|
|
62
81
|
:audit_log_reify,
|
|
63
|
-
|
|
82
|
+
Types::AuditLogJsonScalar,
|
|
64
83
|
null: true,
|
|
65
84
|
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
85
|
resolver_method: :resolve_audit_log_reify
|
|
@@ -68,6 +87,8 @@ module RailsAuditLog
|
|
|
68
87
|
argument :item_type, String, required: true, description: "The audited model class name."
|
|
69
88
|
argument :item_id, GraphQL::Types::ID, required: true, description: "The audited record ID."
|
|
70
89
|
argument :at, GraphQL::Types::ISO8601DateTime, required: true, description: "Reconstruct state as of this time."
|
|
90
|
+
argument :for_tenant, String, required: false,
|
|
91
|
+
description: "Scope to a specific tenant ID. Overrides auto-tenant when RailsAuditLog.current_tenant is configured."
|
|
71
92
|
end
|
|
72
93
|
|
|
73
94
|
base.field(
|
|
@@ -79,10 +100,18 @@ module RailsAuditLog
|
|
|
79
100
|
) do
|
|
80
101
|
argument :event, String, required: false, description: "Filter by event type (create, update, destroy)."
|
|
81
102
|
argument :item_type, String, required: false, description: "Filter by audited model class name."
|
|
103
|
+
argument :actor_type, String, required: false, description: "Filter by actor model class name (e.g. \"User\")."
|
|
82
104
|
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Count entries created at or after this time."
|
|
105
|
+
argument :for_tenant, String, required: false,
|
|
106
|
+
description: "Scope to a specific tenant ID. Overrides auto-tenant when RailsAuditLog.current_tenant is configured."
|
|
83
107
|
end
|
|
84
108
|
end
|
|
85
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]
|
|
86
115
|
def resolve_audit_log_entry(id:, for_tenant: nil)
|
|
87
116
|
check_authentication!
|
|
88
117
|
tenant_id = for_tenant || RailsAuditLog.current_tenant&.call
|
|
@@ -90,43 +119,62 @@ module RailsAuditLog
|
|
|
90
119
|
base.find_by(id: id)
|
|
91
120
|
end
|
|
92
121
|
|
|
93
|
-
|
|
122
|
+
# Resolves the +auditLogEntries+ field.
|
|
123
|
+
#
|
|
124
|
+
# @return [ActiveRecord::Relation]
|
|
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)
|
|
94
126
|
check_authentication!
|
|
95
|
-
scope = 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)
|
|
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)
|
|
96
128
|
scope.limit(per_page).offset((page - 1) * per_page)
|
|
97
129
|
end
|
|
98
130
|
|
|
99
|
-
|
|
131
|
+
# Resolves the +auditLogEntriesConnection+ field.
|
|
132
|
+
#
|
|
133
|
+
# @return [ActiveRecord::Relation]
|
|
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)
|
|
100
135
|
check_authentication!
|
|
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)
|
|
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)
|
|
102
137
|
end
|
|
103
138
|
|
|
104
|
-
|
|
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]
|
|
147
|
+
def resolve_audit_log_reify(item_type:, item_id:, at:, for_tenant: nil)
|
|
105
148
|
check_authentication!
|
|
106
|
-
|
|
149
|
+
tenant_id = for_tenant || RailsAuditLog.current_tenant&.call
|
|
150
|
+
scope = RailsAuditLog::AuditLogEntry
|
|
107
151
|
.where(item_type: item_type, item_id: item_id)
|
|
108
152
|
.where("created_at <= ?", at)
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
scope = scope.for_tenant(tenant_id) if tenant_id
|
|
154
|
+
entry = scope.order(created_at: :desc, id: :desc).first
|
|
111
155
|
return nil if entry.nil? || entry.event == "destroy"
|
|
112
156
|
to_attrs = (entry.object_changes || {}).transform_values { |v| v[1] }
|
|
113
157
|
entry.object.present? ? entry.object.merge(to_attrs) : to_attrs
|
|
114
158
|
end
|
|
115
159
|
|
|
116
|
-
|
|
160
|
+
# Resolves the +auditLogEntriesCount+ field.
|
|
161
|
+
#
|
|
162
|
+
# @return [Integer]
|
|
163
|
+
def resolve_audit_log_entries_count(event: nil, item_type: nil, actor_type: nil, since: nil, for_tenant: nil)
|
|
117
164
|
check_authentication!
|
|
118
165
|
scope = RailsAuditLog::AuditLogEntry.all
|
|
119
166
|
scope = scope.where(event: event) if event
|
|
120
167
|
scope = scope.where(item_type: item_type) if item_type
|
|
168
|
+
scope = scope.where(actor_type: actor_type) if actor_type
|
|
121
169
|
scope = scope.where("created_at >= ?", since) if since
|
|
122
|
-
tenant_id = RailsAuditLog.current_tenant&.call
|
|
170
|
+
tenant_id = for_tenant || RailsAuditLog.current_tenant&.call
|
|
123
171
|
scope = scope.for_tenant(tenant_id) if tenant_id
|
|
124
172
|
scope.count
|
|
125
173
|
end
|
|
126
174
|
|
|
127
175
|
private
|
|
128
176
|
|
|
129
|
-
def build_scope(event: nil, item_type: nil, item_id: nil, actor_id: nil, since: nil, until_time: nil, touching: nil, order_by: nil, for_tenant: nil)
|
|
177
|
+
def build_scope(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)
|
|
130
178
|
sort_field = order_by&.field || :created_at
|
|
131
179
|
sort_direction = order_by&.direction || :desc
|
|
132
180
|
scope = RailsAuditLog::AuditLogEntry.order(sort_field => sort_direction)
|
|
@@ -134,6 +182,7 @@ module RailsAuditLog
|
|
|
134
182
|
scope = scope.where(item_type: item_type) if item_type
|
|
135
183
|
scope = scope.where(item_id: item_id) if item_id
|
|
136
184
|
scope = scope.where(actor_id: actor_id) if actor_id
|
|
185
|
+
scope = scope.where(actor_type: actor_type) if actor_type
|
|
137
186
|
scope = scope.where("created_at >= ?", since) if since
|
|
138
187
|
scope = scope.where("created_at <= ?", until_time) if until_time
|
|
139
188
|
if touching
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
|
@@ -112,6 +112,7 @@ module RailsAuditLog
|
|
|
112
112
|
?item_type: String?,
|
|
113
113
|
?item_id: String?,
|
|
114
114
|
?actor_id: String?,
|
|
115
|
+
?actor_type: String?,
|
|
115
116
|
?since: Time?,
|
|
116
117
|
?until_time: Time?,
|
|
117
118
|
?touching: String?,
|
|
@@ -126,6 +127,7 @@ module RailsAuditLog
|
|
|
126
127
|
?item_type: String?,
|
|
127
128
|
?item_id: String?,
|
|
128
129
|
?actor_id: String?,
|
|
130
|
+
?actor_type: String?,
|
|
129
131
|
?since: Time?,
|
|
130
132
|
?until_time: Time?,
|
|
131
133
|
?touching: String?,
|
|
@@ -136,13 +138,16 @@ module RailsAuditLog
|
|
|
136
138
|
def resolve_audit_log_reify: (
|
|
137
139
|
item_type: String,
|
|
138
140
|
item_id: String,
|
|
139
|
-
at: Time
|
|
141
|
+
at: Time,
|
|
142
|
+
?for_tenant: String?
|
|
140
143
|
) -> Hash[String, untyped]?
|
|
141
144
|
|
|
142
145
|
def resolve_audit_log_entries_count: (
|
|
143
146
|
?event: String?,
|
|
144
147
|
?item_type: String?,
|
|
145
|
-
?
|
|
148
|
+
?actor_type: String?,
|
|
149
|
+
?since: Time?,
|
|
150
|
+
?for_tenant: String?
|
|
146
151
|
) -> Integer
|
|
147
152
|
|
|
148
153
|
private
|
|
@@ -152,6 +157,7 @@ module RailsAuditLog
|
|
|
152
157
|
?item_type: String?,
|
|
153
158
|
?item_id: String?,
|
|
154
159
|
?actor_id: String?,
|
|
160
|
+
?actor_type: String?,
|
|
155
161
|
?since: Time?,
|
|
156
162
|
?until_time: Time?,
|
|
157
163
|
?touching: String?,
|
|
@@ -162,5 +168,38 @@ module RailsAuditLog
|
|
|
162
168
|
def check_authentication!: () -> void
|
|
163
169
|
end
|
|
164
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
|
|
165
204
|
end
|
|
166
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.
|
|
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
|