nulogy_message_bus_producer 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -17
  3. data/Rakefile +2 -4
  4. data/db/migrate/20200611150212_create_public_subscriptions_and_events_tables.rb +2 -2
  5. data/lib/nulogy_message_bus_producer.rb +8 -4
  6. data/lib/nulogy_message_bus_producer/base_subscription.rb +1 -1
  7. data/lib/nulogy_message_bus_producer/config.rb +25 -1
  8. data/lib/nulogy_message_bus_producer/configuration/query_parser.rb +71 -0
  9. data/lib/nulogy_message_bus_producer/subscription.rb +3 -1
  10. data/lib/nulogy_message_bus_producer/subscriptions/finder.rb +40 -0
  11. data/lib/nulogy_message_bus_producer/subscriptions/postgres_transport.rb +9 -2
  12. data/lib/nulogy_message_bus_producer/subscriptions/query_validator.rb +47 -0
  13. data/lib/nulogy_message_bus_producer/version.rb +1 -1
  14. data/spec/dummy/Rakefile +1 -1
  15. data/spec/dummy/app/mailers/application_mailer.rb +2 -2
  16. data/spec/dummy/bin/bundle +2 -2
  17. data/spec/dummy/bin/rails +3 -3
  18. data/spec/dummy/bin/rake +2 -2
  19. data/spec/dummy/bin/setup +10 -11
  20. data/spec/dummy/bin/update +10 -10
  21. data/spec/dummy/bin/yarn +6 -8
  22. data/spec/dummy/config.ru +1 -1
  23. data/spec/dummy/config/boot.rb +3 -3
  24. data/spec/dummy/config/environment.rb +1 -1
  25. data/spec/dummy/config/environments/development.rb +2 -2
  26. data/spec/dummy/config/environments/production.rb +5 -5
  27. data/spec/dummy/config/environments/test.rb +2 -2
  28. data/spec/dummy/config/initializers/assets.rb +2 -2
  29. data/spec/dummy/config/puma.rb +3 -3
  30. data/spec/dummy/config/spring.rb +2 -2
  31. data/spec/dummy/db/schema.rb +0 -2
  32. data/spec/dummy/log/development.log +2333 -0
  33. data/spec/dummy/log/test.log +44636 -0
  34. data/spec/integration/lib/nulogy_message_bus_producer/config_spec.rb +37 -0
  35. data/spec/integration/lib/nulogy_message_bus_producer/repopulate_replication_slots_spec.rb +2 -2
  36. data/spec/integration/lib/nulogy_message_bus_producer/subscription_spec.rb +20 -2
  37. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/finder_spec.rb +54 -0
  38. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/no_variables_spec.rb +1 -1
  39. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/postgres_transport_spec.rb +100 -54
  40. data/spec/integration/lib/nulogy_message_bus_producer/{subscriber_graphql_schema_validator_spec.rb → subscriptions/query_validator_spec.rb} +3 -3
  41. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/risky_subscription_blocker_spec.rb +0 -16
  42. data/spec/nulogy_message_bus_producer/configuration/query_parser_spec.rb +58 -0
  43. data/spec/spec_helper.rb +21 -1
  44. data/spec/support/kafka_connect.rb +1 -1
  45. data/spec/support/subscription_helpers.rb +21 -3
  46. data/spec/support/test_graphql_schema.rb +6 -0
  47. metadata +23 -25
  48. data/lib/nulogy_message_bus_producer/subscriber_graphql_schema_validator.rb +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f25e6c8908723688660d57c0959e9a90148c3546a7f8015348fd610f05743711
4
- data.tar.gz: 67440649f40183608436a823695d309dd7917c754aced8ac2c2e9e8d6279431f
3
+ metadata.gz: 53fdb223452262c8d9780b2e78fee69eb2a73d6e11d72a4041ef343813e17921
4
+ data.tar.gz: 25c21ecc26cf8843dfa4705a4051d653858a2ffae7ee934243bca4e7799c7713
5
5
  SHA512:
6
- metadata.gz: 59e33bfcea9890adbb3d9814a7d64b22bb73c85273115a35c3f8915859e0cc382e8d3ad2f55a29e9e5ad46176a537ca7ecf187dedc483a006ab812c8103334bb
7
- data.tar.gz: 3fc07ac8fc834bce6900cacbd1247ca069f9f83f678ec6865fc722e7d86d32e63718dc6306c55f046109803d9ab040fd9bf092782ac86d1a79c05d820d79f070
6
+ metadata.gz: c55aec7fbd31c27d479e9ae6f0f963ac074f76d159d9d7f54d07fc389092f4211a60415bdfeaa6f05114fa891f5c43b64893c732ba937255dcfb2bdfe32a70f4
7
+ data.tar.gz: 227c9de8ca1742b9fa9306d7e544b7df1964c09e98fffd889981ba5925742b128ba04e3b9c487d57dfced24625d9d575e866db9eec47d7d79b6bc6412302d37f
data/README.md CHANGED
@@ -1,39 +1,89 @@
1
1
  # Nulogy Message Bus Producer
2
2
 
3
- Nulogy's code for producing to to the Message Bus
3
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
4
+
5
+ This engine contains the classes and validations for producing messages to the Message Bus.
4
6
 
5
7
  ## Installation
6
8
 
7
- 1. Add the gem to your Gemfile `gem "nulogy_message_bus_producer"`
8
- 2. Install the migration `rake railties:install:migrations`
9
+ 1. Add the gem to your Gemfile: `gem "nulogy_message_bus_producer"`
10
+ 2. Install the migrations: `rake railties:install:migrations`
9
11
 
10
12
  ## Usage
11
13
 
12
- This engine contains the classes and validations for producing messages to the Message Bus.
14
+ Subscriptions should inherit from `NulogyMessageBusProducer::BaseSubscription`.
15
+
16
+ ### Subscriptions
17
+
18
+ Subscriptions are created by consumers to subscribe to certain events that the producer exposes.
19
+
20
+ When an event is fired/triggered, subscriptions are used to generate events into the
21
+ `NulogyMessageBusProducer.subscription_events_table_name` table.
22
+
23
+ Debezium then reads this table from the PG replication log and pushes them into Kafka.
24
+
25
+ There are two ways to create subscriptions:
26
+
27
+ #### Self-serve (deprecated)
28
+
29
+ You create these subscriptions via the public GraphQL API. e.g.
13
30
 
14
- In general, as long as a subscription inherits from `NulogyMessageBusProducer::BaseSubscription`, everything
15
- should be set up.
31
+ ```graphql
32
+ subscription {
33
+ workOrderUpdated(subscriptionId: "uuid", subscriptionGroupId: "uuid", topicName: "some-topic") {
34
+ workOrder {
35
+ id
36
+ code
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ These subscriptions are written to the `NulogyMessageBusProducer.subscriptions_table_name` table.
43
+
44
+ This style of subscriptions may be removed in a future release.
45
+
46
+ #### Configured (new)
16
47
 
17
- Subscriptions will be written to the `NulogyMessageBusProducer.subscriptions_table_name` while events generated from the subscriptions will be written to
18
- `NulogyMessageBusProducer.subscription_events_table_name`. Debezium then reads this table from the PG replication log and pushes them into Kafka.
48
+ Subscriptions provided by configuration in the producer app:
49
+
50
+ ```ruby
51
+ NulogyMessageBusProducer.configure do |c|
52
+ c.register_schema(schema: "ExampleDomain::Schema", key: "exampleDomain")
53
+
54
+ c.add_subscription!(
55
+ schema: "ExampleDomain::Schema",
56
+ query: <<~QUERY
57
+ subscription {
58
+ domainEvent(subscriptionId: "uuid", subscriptionGroupId: "uuid2", topicName: "consumer-inbox") {
59
+ domainObject {
60
+ field
61
+ }
62
+ }
63
+ }
64
+ QUERY
65
+ )
66
+ end
67
+ ```
19
68
 
20
69
  ## Configuration
21
70
 
22
- | Configuration | Default | Description |
23
- |--------------------------------|---------------------------------|---------------------------------------------------------------------------------------------|
24
- | context_for_subscription | `->(_subscription) { {} }` | A lambda used to inject any GraphQL Context to be used when producing subscription events |
25
- | producing_events_fails_with | :raise | How the producer should handle errors when producing subscription events |
26
- | subscriptions_table_name | "message_bus_subscriptions" | The name of the table for the Subscription model |
27
- | subscription_events_table_name | "message_bus_subscription_events" | The name of the table for the SubscriptionEvents model |
71
+ | Configuration | Default | Description |
72
+ |--------------------------------|-----------------------------------|----------------------------------------------------------------------------------------------------|
73
+ | context_for_subscription | `->(_subscription) { {} }` | A lambda used to inject any GraphQL Context to be used when producing subscription events |
74
+ | producing_events_fails_with | :raise | How the producer should handle errors when producing subscription events |
75
+ | subscriptions_table_name | "message_bus_subscriptions" | The name of the table for the Subscription model |
76
+ | subscription_events_table_name | "message_bus_subscription_events" | The name of the table for the SubscriptionEvents model |
77
+ | add_subscription! | [] (i.e. empty) | Add a subscription via configuration. It will supercede any configured via GraphQL for a given id. |
28
78
 
29
79
  ## Setup
30
80
 
31
81
  You should configure the gem with an initializer. A recommended setup:
32
82
 
33
83
  ```ruby
34
- # Table names are configurable (optional)
35
- NulogyMessageBusProducer.subscriptions_table_name = :message_bus_subscriptions
36
- NulogyMessageBusProducer.subscription_events_table_name = :message_bus_subscription_events
84
+ # (optional) Table names are configurable
85
+ # NulogyMessageBusProducer.subscriptions_table_name = :message_bus_subscriptions
86
+ # NulogyMessageBusProducer.subscription_events_table_name = :message_bus_subscription_events
37
87
 
38
88
  # Register known schemas
39
89
  NulogyMessageBusProducer.configure do |config|
data/Rakefile CHANGED
@@ -13,11 +13,9 @@ require "rspec/core/rake_task"
13
13
 
14
14
  RSpec::Core::RakeTask.new(:spec)
15
15
 
16
- require "rubocop/rake_task"
16
+ require "standard/rake"
17
17
 
18
- RuboCop::RakeTask.new
19
-
20
- task default: [:spec, :rubocop]
18
+ task default: %i[spec standard]
21
19
 
22
20
  require "rake/release"
23
21
 
@@ -5,7 +5,7 @@ class CreatePublicSubscriptionsAndEventsTables < ActiveRecord::Migration[5.2]
5
5
 
6
6
  create_table NulogyMessageBusProducer.subscriptions_table_name, id: :uuid, default: nil do |t|
7
7
  t.uuid :subscription_group_id, null: false
8
- t.string :event_type, null: false, index: { name: "index_nulogy_mb_producer_subscriptions_on_event_type" }
8
+ t.string :event_type, null: false, index: {name: "index_nulogy_mb_producer_subscriptions_on_event_type"}
9
9
  t.string :topic_name, null: false
10
10
  t.string :query, null: false
11
11
  t.text :schema_key, null: false
@@ -19,7 +19,7 @@ class CreatePublicSubscriptionsAndEventsTables < ActiveRecord::Migration[5.2]
19
19
  t.string :topic_name, null: false
20
20
  t.uuid :tenant_id, null: false
21
21
  t.json :event_json, null: false
22
- t.column :created_at, :datetime, index: { name: "index_nulogy_mb_producer_subscription_events_on_created_at" }
22
+ t.column :created_at, :datetime, index: {name: "index_nulogy_mb_producer_subscription_events_on_created_at"}
23
23
  end
24
24
  end
25
25
  end
@@ -1,6 +1,7 @@
1
1
  require "graphql"
2
2
 
3
3
  require "nulogy_message_bus_producer/config"
4
+ require "nulogy_message_bus_producer/configuration/query_parser"
4
5
  require "nulogy_message_bus_producer/version"
5
6
  require "nulogy_message_bus_producer/engine"
6
7
 
@@ -35,7 +36,7 @@ module NulogyMessageBusProducer
35
36
 
36
37
  def self.trigger_event(schema, event_type, root_object)
37
38
  schema_key = NulogyMessageBusProducer.resolve_schema_key(schema)
38
- subscriptions = Subscription.where(event_type: event_type, schema_key: schema_key)
39
+ subscriptions = Subscriptions::Finder.new(config).for_schema_event(schema_key, event_type)
39
40
 
40
41
  subscriptions.each do |subscription|
41
42
  args = {
@@ -65,7 +66,9 @@ module NulogyMessageBusProducer
65
66
  end
66
67
 
67
68
  def self.resolve_schema_key(schema)
68
- config.registered_schemas.invert.fetch(schema.name) do
69
+ schema_name = schema.respond_to?(:name) ? schema.name : schema
70
+
71
+ config.registered_schemas.invert.fetch(schema_name) do
69
72
  raise KeyError, <<~MESSAGE
70
73
  The schema registry did not contain an entry for the schema '#{schema.name}'.
71
74
 
@@ -80,7 +83,7 @@ module NulogyMessageBusProducer
80
83
  def self.validate_existing!
81
84
  return unless Db.exists? && Db.subscriptions_exist?
82
85
 
83
- validator = NulogyMessageBusProducer::SubscriberGraphqlSchemaValidator.new
86
+ validator = NulogyMessageBusProducer::Subscriptions::QueryValidator.new
84
87
 
85
88
  return if validator.validate
86
89
 
@@ -139,7 +142,8 @@ require "nulogy_message_bus_producer/base_subscription"
139
142
  require "nulogy_message_bus_producer/subscription"
140
143
  require "nulogy_message_bus_producer/subscription_event"
141
144
  require "nulogy_message_bus_producer/repopulate_replication_slots"
142
- require "nulogy_message_bus_producer/subscriber_graphql_schema_validator"
145
+ require "nulogy_message_bus_producer/subscriptions/finder"
143
146
  require "nulogy_message_bus_producer/subscriptions/postgres_transport"
147
+ require "nulogy_message_bus_producer/subscriptions/query_validator"
144
148
  require "nulogy_message_bus_producer/subscriptions/no_variables"
145
149
  require "nulogy_message_bus_producer/subscriptions/risky_subscription_blocker"
@@ -2,7 +2,7 @@ module NulogyMessageBusProducer
2
2
  # This base class contains the fields required to create a subscription.
3
3
  # For example, for a subscription to a model called AggregateRoot:
4
4
  #
5
- # class AggregateRootCreated < NulogyMessageBusProducer::BasePublicSubscription
5
+ # class AggregateRootCreated < NulogyMessageBusProducer::BaseSubscription
6
6
  # field :aggregate_root, Domain::Public::Types::AggregateRootType, null: true
7
7
  # end
8
8
  class BaseSubscription < GraphQL::Schema::Subscription
@@ -1,14 +1,16 @@
1
1
  module NulogyMessageBusProducer
2
2
  # Configuration for the gem
3
- # This is a private class, so do not use it directly. Use the methods on NulogyMessageBusProducer.
4
3
  class Config
5
4
  FAILURE_MODES = [:raise, :soft_fail].freeze
6
5
 
7
6
  attr_reader :registered_schemas
7
+ attr_reader :configured_subscriptions
8
8
  attr_writer :context_for_subscription
9
9
 
10
10
  def initialize(options = {})
11
11
  @registered_schemas = {}
12
+ @configured_subscriptions = []
13
+
12
14
  producing_events_fails_with(:raise)
13
15
 
14
16
  update(options)
@@ -48,6 +50,28 @@ module NulogyMessageBusProducer
48
50
  options.each { |key, value| public_send("#{key}=", value) }
49
51
  end
50
52
 
53
+ def add_subscription!(schema:, query:)
54
+ @configured_subscriptions ||= []
55
+
56
+ schema_key = NulogyMessageBusProducer.resolve_schema_key(schema)
57
+ query_parser = Configuration::QueryParser.new(query)
58
+ subscription = NulogyMessageBusProducer::Subscription.new(
59
+ id: query_parser.subscription_id,
60
+ subscription_group_id: query_parser.subscription_group_id,
61
+ schema_key: schema_key,
62
+ event_type: query_parser.event_type,
63
+ topic_name: query_parser.topic,
64
+ query: query
65
+ )
66
+
67
+ if subscription.valid?
68
+ @configured_subscriptions << subscription
69
+ subscription
70
+ else
71
+ raise ArgumentError, subscription.errors.full_messages
72
+ end
73
+ end
74
+
51
75
  private
52
76
 
53
77
  def raise_handler(subscription_id:, context:, variables:, result:)
@@ -0,0 +1,71 @@
1
+ module NulogyMessageBusProducer
2
+ module Configuration
3
+ class QueryParser
4
+ class ParseError < StandardError
5
+ attr_reader :source_error
6
+
7
+ def intialize(source_error)
8
+ @source_error = source_error
9
+ end
10
+ end
11
+
12
+ def initialize(query)
13
+ @query = query
14
+ end
15
+
16
+ def subscription_id
17
+ parse
18
+
19
+ argument("subscriptionId")
20
+ rescue => e
21
+ error("subscriptionId", e)
22
+ end
23
+
24
+ def subscription_group_id
25
+ parse
26
+
27
+ argument("subscriptionGroupId")
28
+ rescue => e
29
+ error("subscriptionGroupId", e)
30
+ end
31
+
32
+ def event_type
33
+ parse
34
+
35
+ first_selection.name
36
+ rescue => e
37
+ error("event type", e)
38
+ end
39
+
40
+ def topic
41
+ parse
42
+
43
+ argument("topicName")
44
+ rescue => e
45
+ error("topic", e)
46
+ end
47
+
48
+ private
49
+
50
+ def parse
51
+ @gql ||= GraphQL::Language::Parser.parse(@query)
52
+ end
53
+
54
+ def argument(field)
55
+ first_selection
56
+ .arguments.detect { |e| e.name == field }
57
+ .value
58
+ end
59
+
60
+ def first_selection
61
+ @gql
62
+ .definitions.first
63
+ .selections.first
64
+ end
65
+
66
+ def error(field, err)
67
+ raise ParseError.new(err), "Error extracting #{field} from the subscription"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -9,13 +9,15 @@ module NulogyMessageBusProducer
9
9
  def validate_each(record, attribute, _value)
10
10
  return if record.schema_key.blank? || record.query.blank?
11
11
 
12
- validator = NulogyMessageBusProducer::SubscriberGraphqlSchemaValidator.new
12
+ validator = NulogyMessageBusProducer::Subscriptions::QueryValidator.new
13
13
 
14
14
  validator.validate(record)
15
15
  validator.errors.each { |e| record.errors.add(attribute, e) }
16
16
  end
17
17
  end
18
18
 
19
+ validates :id, presence: true
20
+ validates :subscription_group_id, presence: true
19
21
  validates :schema_key, :event_type, presence: true
20
22
  validates :query, presence: true, valid_for_schema: true
21
23
 
@@ -0,0 +1,40 @@
1
+ module NulogyMessageBusProducer
2
+ module Subscriptions
3
+ # A facade to find subscriptions.
4
+ #
5
+ # It will retrieve subscriptions from these sources:
6
+ # - configured subscriptions via #add_subscription!
7
+ # - self-serve subscriptions via GraphQL API
8
+ #
9
+ # If a subscription exists with the same id in both sources, then
10
+ # the configured subscription will be returned.
11
+ class Finder
12
+ def initialize(config)
13
+ @config = config
14
+ end
15
+
16
+ # Note: raises like ActiveRecord#find
17
+ def find(id)
18
+ @config.configured_subscriptions.detect { |s| s.id == id } || Subscription.find(id)
19
+ end
20
+
21
+ def for_schema_event(schema_key, event_type)
22
+ subscriptions = configured(schema_key, event_type) + self_serve(schema_key, event_type)
23
+
24
+ subscriptions.uniq(&:id)
25
+ end
26
+
27
+ private
28
+
29
+ def self_serve(schema_key, event_type)
30
+ Subscription.where(event_type: event_type, schema_key: schema_key)
31
+ end
32
+
33
+ def configured(schema_key, event_type)
34
+ @config
35
+ .configured_subscriptions
36
+ .filter { |s| s.event_type == event_type && s.schema_key == schema_key }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -24,7 +24,7 @@ module NulogyMessageBusProducer
24
24
  end
25
25
 
26
26
  def read_subscription(subscription_id)
27
- subscription = Subscription.find(subscription_id)
27
+ subscription = find_subscription(subscription_id)
28
28
  context = NulogyMessageBusProducer.context_for_subscription(subscription)
29
29
 
30
30
  {
@@ -61,6 +61,7 @@ module NulogyMessageBusProducer
61
61
  end
62
62
  end
63
63
 
64
+ # TODO: how is this invoked?
64
65
  def delete_subscription(subscription_id)
65
66
  Subscription.find_by(id: subscription_id).destroy
66
67
  end
@@ -69,7 +70,7 @@ module NulogyMessageBusProducer
69
70
 
70
71
  def create_event(subscription_id, result)
71
72
  company_uuid = result.query.context.object[:company_uuid]
72
- subscription = Subscription.find_by(id: subscription_id)
73
+ subscription = find_subscription(subscription_id)
73
74
 
74
75
  SubscriptionEvent.create_or_update(
75
76
  id: SecureRandom.uuid,
@@ -80,6 +81,12 @@ module NulogyMessageBusProducer
80
81
  topic_name: subscription.topic_name
81
82
  )
82
83
  end
84
+
85
+ def find_subscription(id)
86
+ Subscriptions::Finder
87
+ .new(NulogyMessageBusProducer.config)
88
+ .find(id)
89
+ end
83
90
  end
84
91
  end
85
92
  end
@@ -0,0 +1,47 @@
1
+ module NulogyMessageBusProducer
2
+ module Subscriptions
3
+ # A custom validator that checks that the provided (or all) subscriptions have a query that is valid for its
4
+ # configured schema. Schemas must be registered with a `schema_key`, that is persisted in the database.
5
+ # It ties the subscription to a particular schema.
6
+ #
7
+ # This validator is run as part of an initializer as a last ditch effort to verify that the stored queries in the
8
+ # database are valid against the deployed schema, so that when events are generated in the system, they are always
9
+ # sucessfully created.
10
+ class QueryValidator
11
+ attr_reader :errors
12
+
13
+ def initialize
14
+ @errors = []
15
+ end
16
+
17
+ def validate(subscription_or_subscriptions = NulogyMessageBusProducer::Subscription.all)
18
+ Array(subscription_or_subscriptions).each do |subscription|
19
+ schema = find_schema(subscription)
20
+ next unless schema
21
+
22
+ gql_errors = schema.validate(subscription.query)
23
+ errors = gql_errors.map { |e| "#{e.message} #{display_id(subscription.id)}" }
24
+
25
+ @errors.concat(errors)
26
+ end
27
+
28
+ @errors.empty?
29
+ end
30
+
31
+ private
32
+
33
+ def find_schema(subscription)
34
+ NulogyMessageBusProducer.resolve_schema(subscription.schema_key) do
35
+ @errors << "Could not find a schema for schema_key '#{subscription.schema_key}' #{display_id(subscription.id)}"
36
+ nil
37
+ end
38
+ end
39
+
40
+ def display_id(id)
41
+ normalized = id.presence || "<new_record>"
42
+
43
+ "(id: #{normalized})"
44
+ end
45
+ end
46
+ end
47
+ end