groovestack-base 0.1.5

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.
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ module Types
7
+ class BaseArgument < ::GraphQL::Schema::Argument
8
+ def initialize(*args, camelize: ::Groovestack::Base.config.graphql.camelize, **kwargs, &block)
9
+ # Then, call super _without_ any args, where Ruby will take
10
+ # _all_ the args originally passed to this method and pass it to the super method.
11
+ super
12
+ end
13
+ end
14
+
15
+ class BaseField < ::GraphQL::Schema::Field
16
+ argument_class ::Groovestack::Base::GraphQL::Types::BaseArgument
17
+
18
+ attr_accessor :authenticate, :visibility_permission
19
+
20
+ # rubocop:disable Metrics/ParameterLists
21
+ def initialize(
22
+ *args,
23
+ authenticate: nil,
24
+ visibility_permission: nil,
25
+ null: false,
26
+ camelize: ::Groovestack::Base.config.graphql.camelize,
27
+ **kwargs, &block
28
+ )
29
+ # Then, call super _without_ any args, where Ruby will take
30
+ # _all_ the args originally passed to this method and pass it to the super method.
31
+ @authenticate = authenticate
32
+ @visibility_permission = visibility_permission
33
+ super(*args, null: null, camelize: camelize, **kwargs, &block)
34
+ end
35
+ # rubocop:enable Metrics/ParameterLists
36
+ end
37
+
38
+ class BaseObject < ::GraphQL::Schema::Object
39
+ field_class ::Groovestack::Base::GraphQL::Types::BaseField
40
+ end
41
+
42
+ class SubscriptionPayload < ::Groovestack::Base::GraphQL::Types::BaseObject
43
+ field :event, ::GraphQL::Types::JSON, null: false
44
+ field :subscription, String, null: false
45
+ field :subscription_args, ::GraphQL::Types::JSON, null: false
46
+ end
47
+
48
+ class Currency < ::Groovestack::Base::GraphQL::Types::BaseObject
49
+ description 'A currency object'
50
+
51
+ field :code, String, null: false, description: 'currency code', method: :iso_code
52
+ field :symbol, String, null: false, description: 'currency symbol'
53
+ end
54
+
55
+ class Money < ::Groovestack::Base::GraphQL::Types::BaseObject
56
+ description 'A money object'
57
+
58
+ field :amount, String, null: false, description: 'amount in decimal'
59
+ field :currency, ::Groovestack::Base::GraphQL::Types::Currency, null: false, description: 'currency metadata'
60
+ field :formatted_amount, String, null: false,
61
+ description: 'amount in decimal formatted with currency symbol',
62
+ method: :format
63
+
64
+ def amount
65
+ object.amount.to_s
66
+ end
67
+ end
68
+
69
+ # AUTHORIZATION
70
+
71
+ class AuthorizedBaseField < BaseField
72
+ def authorized?(obj, args, ctx)
73
+ authorized_fields = begin
74
+ obj.authorized_fields_for_serialization(ctx[:current_user])
75
+ rescue StandardError
76
+ []
77
+ end
78
+
79
+ super && authorized_fields.include?(name.to_sym)
80
+ end
81
+ end
82
+
83
+ class AuthorizedBaseObject < BaseObject
84
+ field_class ::Groovestack::Base::GraphQL::Types::AuthorizedBaseField
85
+ end
86
+
87
+ class VisibleBaseField < BaseField
88
+ def visible?(context)
89
+ return super unless @visibility_permission
90
+
91
+ # visibility profile are the visibility levels the
92
+ # current user is authorized for
93
+ visibility_profile = context.schema.visibility_profile_for_context(context).map(&:to_sym)
94
+
95
+ return super unless visibility_profile
96
+
97
+ super && visibility_profile.include?(@visibility_permission.to_sym)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module Listener
6
+ extend ::ActiveSupport::Concern
7
+
8
+ CORE_LISTENER = true
9
+
10
+ module ClassMethods
11
+ def init(_throttle_seconds: 0)
12
+ raise 'listener must define class method init'
13
+ end
14
+
15
+ def throttle(seconds)
16
+ wait = false
17
+
18
+ proc do |&block|
19
+ unless wait
20
+ wait = true
21
+
22
+ Thread.new do
23
+ block.call
24
+
25
+ sleep(seconds)
26
+
27
+ wait = false
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ module Listeners
36
+ class InitAll
37
+ def self.run(throttle_seconds: 0)
38
+ core_listeners = ObjectSpace.each_object(Class).select do |klass|
39
+ klass.const_defined?(:CORE_LISTENER) && klass.const_get(:CORE_LISTENER)
40
+ end
41
+ core_listeners.map { |listener| listener.init(throttle_seconds: throttle_seconds) }.flatten
42
+ end
43
+ end
44
+
45
+ class Database
46
+ attr_accessor :db_channel, :connection, :connection_instance
47
+
48
+ def initialize(db_channel)
49
+ @db_channel = db_channel
50
+ @connection = nil
51
+ @connection_instance = nil
52
+ end
53
+
54
+ def listen
55
+ Rails.logger.info "Listening to #{db_channel}"
56
+ ::ActiveRecord::Base.connection_pool.with_connection do |connection|
57
+ @connection = connection
58
+
59
+ @connection_instance = connection.instance_variable_get(:@connection)
60
+
61
+ begin
62
+ connection_instance.async_exec "LISTEN #{db_channel}"
63
+
64
+ loop do
65
+ connection_instance.wait_for_notify do |_channel, _pid, payload|
66
+ yield payload if block_given?
67
+ end
68
+ end
69
+ ensure
70
+ unlisten
71
+ end
72
+ end
73
+ end
74
+
75
+ def unlisten
76
+ connection_instance.async_exec "UNLISTEN #{db_channel}"
77
+ end
78
+
79
+ def disconnect
80
+ ::ActiveRecord::Base.connection_pool.checkin(connection)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module PubSub
6
+ class GraphQLPublisher
7
+ include Wisper::Publisher
8
+
9
+ def trigger_event(subscription, args, event, **kwargs)
10
+ publish(:graphql_trigger_event, subscription, args, event, kwargs)
11
+ end
12
+ end
13
+
14
+ class GraphQLSubscriber
15
+ def graphql_trigger_event(subscription, args, event, kwargs)
16
+ ::Groovestack::Base::GraphQL::Subscriptions::EventHandler.trigger(subscription, args, event, kwargs)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ ActiveSupport.on_load(:after_initialize) do
24
+ ::Groovestack::Base::PubSub::GraphQLPublisher.subscribe(::Groovestack::Base::PubSub::GraphQLSubscriber.new)
25
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma/plugin'
4
+
5
+ # Need to pre-declare Puma::Plugin here - not sure why, but it's innocusous so leave it
6
+ module Puma
7
+ class Plugin # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+
11
+ Puma::Plugin.create do # rubocop:disable Metrics/BlockLength
12
+ def production?
13
+ ENV.fetch('RACK_ENV', 'development') == 'production'
14
+ end
15
+
16
+ def log(msg)
17
+ if production? && defined?(Rails)
18
+ Rails.logger.info msg
19
+ else
20
+ puts msg
21
+ end
22
+ end
23
+
24
+ def flush
25
+ Rails.logger.flush if production? && defined?(Rails)
26
+ end
27
+
28
+ def start(_launcher) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
29
+ trap('TERM') do
30
+ log '[core-cron] Graceful shutdown'
31
+ exit # rubocop:disable Rails/Exit
32
+ end
33
+
34
+ in_background do
35
+ log '[core-cron] Starting core-cron worker'
36
+ log '[core-cron] Waiting for CORE-CRON PGLock'
37
+
38
+ loop do
39
+ begin
40
+ core_listeners = []
41
+
42
+ # hold the command for 30 minutes
43
+ PgLock.new(name: 'core-cron-commander', ttl: 30.minutes.to_i, attempts: 1).lock do
44
+ log '[core-cron] Obtained CORE-CRON PGLock'
45
+
46
+ core_listeners = ::Groovestack::Base::Listeners::InitAll.run(throttle_seconds: 1)
47
+ end
48
+ rescue Timeout::Error
49
+ # normal behavior - don't do anything when this happens
50
+
51
+ # ensure channels closed and connections checked in for listeners
52
+ core_listeners.each(&:unlisten)
53
+ core_listeners.each(&:disconnect)
54
+ end
55
+
56
+ puts 'PGLock expired. Taking a breath before attempting to re-aquire.'
57
+ # Loop and keep checking for CORE-CRON command
58
+ sleep 14
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module CoreRailtie
6
+ extend ActiveSupport::Concern
7
+
8
+ CORE_ENGINE = true
9
+
10
+ included do # rubocop:disable Metrics/BlockLength
11
+ def dx_validations
12
+ []
13
+ end
14
+
15
+ def module_name
16
+ self.class.name.deconstantize.demodulize
17
+ end
18
+
19
+ def module_version
20
+ self.class.name.deconstantize.constantize::VERSION
21
+ end
22
+
23
+ def after_init
24
+ unless Rails.const_defined?('Console') && (ENV['RAILS_ENV'] || ENV.fetch('RACK_ENV', nil)) == 'development'
25
+ return
26
+ end
27
+
28
+ puts 'CORE Platform DX Mode'.bold if module_name == 'Base'
29
+ core_base_dx_validate
30
+ end
31
+
32
+ def append_migrations(app) # rubocop:disable Metrics/AbcSize
33
+ return if app.root.present? && root.present? && app.root.to_s.match?(root.to_s)
34
+
35
+ config.paths['db/migrate'].expanded.each do |expanded_path|
36
+ app.config.paths['db/migrate'] << expanded_path
37
+ end
38
+ end
39
+
40
+ def append_initializers(app) # rubocop:disable Metrics/AbcSize
41
+ return if app.root.present? && root.present? && app.root.to_s.match?(root.to_s)
42
+
43
+ config.paths['config/initializers'].expanded.each do |expanded_path|
44
+ app.config.paths['config/initializers'] << expanded_path
45
+ end
46
+ end
47
+
48
+ def core_base_dx_validate # rubocop:disable Metrics/AbcSize
49
+ errors = []
50
+ dx_validations.each do |v|
51
+ v[:eval].call
52
+ rescue StandardError
53
+ errors << v[:message]
54
+ break
55
+ end
56
+
57
+ if errors.length.positive?
58
+ print '⚠️'.brown
59
+ msg = " Groovestack::#{module_name}\t#{module_version}"
60
+ msg += module_description if respond_to?(:module_description)
61
+ msg += "\t#{errors[0]}"
62
+ else
63
+ print '✔'.green
64
+ msg = " Groovestack::#{module_name}\t#{module_version}"
65
+ msg += module_description if respond_to?(:module_description)
66
+ end
67
+
68
+ puts msg
69
+ end
70
+ end
71
+ end
72
+
73
+ class Railtie < Rails::Engine
74
+ include CoreRailtie
75
+
76
+ def dx_validations
77
+ [
78
+ {
79
+ eval: proc { require 'pg' },
80
+ message: "Error: 'pg' gem is required, add it your your gemfile"
81
+ },
82
+ {
83
+ eval: proc { require 'graphql' },
84
+ message: "Error: 'graphql' gem is required, add it your your gemfile"
85
+ }
86
+ ]
87
+ end
88
+
89
+ initializer :append_migrations do |app|
90
+ append_migrations app
91
+ end
92
+
93
+ initializer :append_initializers do |app|
94
+ append_initializers app
95
+ end
96
+
97
+ initializer :init_puma do |app|
98
+ return unless defined?(Puma)
99
+
100
+ unless app.root.present? && root.present? && app.root.to_s.match?(root.to_s)
101
+ config.paths.add('lib/groovestack/base/puma/plugin')
102
+ app.config.paths.add('app/lib/puma/plugin')
103
+ config.paths['lib/groovestack/base/puma/plugin'].expanded.each do |expanded_path|
104
+ app.config.paths['app/lib/puma/plugin'] << expanded_path
105
+ end
106
+ end
107
+ end
108
+
109
+ config.after_initialize do
110
+ after_init
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/SingleLineMethods
4
+ class String
5
+ def black; "\e[30m#{self}\e[0m" end
6
+ def red; "\e[31m#{self}\e[0m" end
7
+ def green; "\e[32m#{self}\e[0m" end
8
+ def brown; "\e[33m#{self}\e[0m" end
9
+ def blue; "\e[34m#{self}\e[0m" end
10
+ def magenta; "\e[35m#{self}\e[0m" end
11
+ def cyan; "\e[36m#{self}\e[0m" end
12
+ def gray; "\e[37m#{self}\e[0m" end
13
+
14
+ def bg_black; "\e[40m#{self}\e[0m" end
15
+ def bg_red; "\e[41m#{self}\e[0m" end
16
+ def bg_green; "\e[42m#{self}\e[0m" end
17
+ def bg_brown; "\e[43m#{self}\e[0m" end
18
+ def bg_blue; "\e[44m#{self}\e[0m" end
19
+ def bg_magenta; "\e[45m#{self}\e[0m" end
20
+ def bg_cyan; "\e[46m#{self}\e[0m" end
21
+ def bg_gray; "\e[47m#{self}\e[0m" end
22
+
23
+ def bold; "\e[1m#{self}\e[22m" end
24
+ def italic; "\e[3m#{self}\e[23m" end
25
+ def underline; "\e[4m#{self}\e[24m" end
26
+ def blink; "\e[5m#{self}\e[25m" end
27
+ def reverse_color; "\e[7m#{self}\e[27m" end
28
+ end
29
+ # rubocop:enable Style/SingleLineMethods
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ VERSION = '0.1.5'
6
+ end
7
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'dry-configurable'
5
+ require 'graphql'
6
+ require 'pg'
7
+ require 'pg_lock'
8
+
9
+ require 'groovestack/base/version'
10
+ require 'groovestack/base/utilities/string'
11
+
12
+ module Groovestack
13
+ module Base
14
+ module GraphQL
15
+ module Subscriptions
16
+ autoload :EventHandler, 'groovestack/base/graphql/subscriptions/event_handler'
17
+ end
18
+
19
+ autoload :Types, 'groovestack/base/graphql/types'
20
+ autoload :BaseInputObject, 'groovestack/base/graphql/base_input_object'
21
+ autoload :BaseMutation, 'groovestack/base/graphql/base_mutation'
22
+ autoload :Documentation, 'groovestack/base/graphql/documentation'
23
+ autoload :BaseSubscription, 'groovestack/base/graphql/base_subscription'
24
+ autoload :Helpers, 'groovestack/base/graphql/helpers'
25
+
26
+ module Tracers
27
+ autoload :AtomicMultiplexTransaction, 'groovestack/base/graphql/tracers/atomic_multiplex_transaction'
28
+ end
29
+
30
+ module Providers
31
+ module ReactAdmin
32
+ autoload :Resource, 'groovestack/base/graphql/providers/react_admin/resource'
33
+ autoload :Types, 'groovestack/base/graphql/providers/react_admin/types'
34
+ end
35
+ end
36
+ end
37
+
38
+ autoload :ActiveRecord, 'groovestack/base/active_record'
39
+ autoload :Listener, 'groovestack/base/listeners'
40
+ autoload :Listeners, 'groovestack/base/listeners'
41
+
42
+ autoload :PubSub, 'groovestack/base/pub_sub' if defined?(Wisper)
43
+ autoload :CoreRailtie, 'groovestack/base/railtie' if defined?(Rails::Railtie)
44
+
45
+ extend Dry::Configurable
46
+
47
+ DEFAULT_ERROR_MONITOR = ::Logger.new($stdout)
48
+
49
+ # ex:
50
+ # /config/initializers/core_base.rb
51
+
52
+ # Groovestack::Base.configure do |config|
53
+ # config.error_notifier.handler = Bugsnag
54
+ # end
55
+ setting :error_notifier, reader: true do
56
+ setting :handler, reader: true
57
+ setting :notify_method, reader: true, default: :notify
58
+ end
59
+
60
+ setting :graphql do
61
+ setting :camelize, default: false
62
+ end
63
+
64
+ def self.notify_error(prefix, err)
65
+ msg = "#{[prefix, 'error'].compact.join(' ')}: #{err}"
66
+
67
+ if error_notifier&.handler.present?
68
+ error_notifier.handler.send(error_notifier.notify_method, msg)
69
+ else
70
+ DEFAULT_ERROR_MONITOR.error(msg)
71
+ end
72
+ end
73
+
74
+ class Error < StandardError; end
75
+ class WrongSchemaFormat < Groovestack::Base::Error; end
76
+ end
77
+ end
78
+
79
+ require 'groovestack/base/puma/plugin/core_cron' if defined?(Puma)
80
+
81
+ # TODO: remove aliases after all core modules have been updated
82
+ # to reference Groovestack::Base
83
+ module Core
84
+ module Base
85
+ module GraphQL
86
+ module Subscriptions
87
+ EventHandler = ::Groovestack::Base::GraphQL::Subscriptions::EventHandler
88
+ end
89
+
90
+ Types = ::Groovestack::Base::GraphQL::Types
91
+ BaseInputObject = ::Groovestack::Base::GraphQL::BaseInputObject
92
+ BaseMutation = ::Groovestack::Base::GraphQL::BaseMutation
93
+ Documentation = ::Groovestack::Base::GraphQL::Documentation
94
+ BaseSubscription = ::Groovestack::Base::GraphQL::BaseSubscription
95
+ Helpers = ::Groovestack::Base::GraphQL::Helpers
96
+
97
+ module Tracers
98
+ AtomicMultiplexTransaction = ::Groovestack::Base::GraphQL::Tracers::AtomicMultiplexTransaction
99
+ end
100
+
101
+ module Providers
102
+ module ReactAdmin
103
+ Resource = ::Groovestack::Base::GraphQL::Providers::ReactAdmin::Resource
104
+ Types = ::Groovestack::Base::GraphQL::Providers::ReactAdmin::Types
105
+ end
106
+ end
107
+ end
108
+
109
+ ActiveRecord = ::Groovestack::Base::ActiveRecord
110
+ Listener = ::Groovestack::Base::Listener
111
+ Listeners = ::Groovestack::Base::Listeners
112
+
113
+ PubSub = ::Groovestack::Base::PubSub if defined?(Wisper)
114
+ CoreRailtie = ::Groovestack::Base::CoreRailtie if defined?(Rails::Railtie)
115
+
116
+ extend Dry::Configurable
117
+
118
+ DEFAULT_ERROR_MONITOR = ::Logger.new($stdout)
119
+
120
+ setting :error_notifier, reader: true do
121
+ setting :handler, reader: true
122
+ setting :notify_method, reader: true, default: :notify
123
+ end
124
+
125
+ setting :graphql do
126
+ setting :camelize, default: false
127
+ end
128
+
129
+ def self.notify_error(prefix, err)
130
+ msg = "#{[prefix, 'error'].compact.join(' ')}: #{err}"
131
+
132
+ if error_notifier&.handler.present?
133
+ error_notifier.handler.send(error_notifier.notify_method, msg)
134
+ else
135
+ DEFAULT_ERROR_MONITOR.error(msg)
136
+ end
137
+ end
138
+
139
+ Error = ::Groovestack::Base::Error
140
+ WrongSchemaFormat = ::Groovestack::Base::WrongSchemaFormat
141
+ end
142
+ end