command_tower 0.3.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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/command_tower/admin_controller.rb +104 -0
  6. data/app/controllers/command_tower/application_controller.rb +81 -0
  7. data/app/controllers/command_tower/auth/plain_text_controller.rb +132 -0
  8. data/app/controllers/command_tower/inbox/message_blast_controller.rb +89 -0
  9. data/app/controllers/command_tower/inbox/message_controller.rb +79 -0
  10. data/app/controllers/command_tower/user_controller.rb +49 -0
  11. data/app/controllers/command_tower/username_controller.rb +26 -0
  12. data/app/helpers/command_tower/application_helper.rb +4 -0
  13. data/app/helpers/command_tower/schema_helper.rb +29 -0
  14. data/app/jobs/command_tower/application_job.rb +4 -0
  15. data/app/mailers/command_tower/application_mailer.rb +8 -0
  16. data/app/mailers/command_tower/email_verification_mailer.rb +12 -0
  17. data/app/models/command_tower/application_record.rb +45 -0
  18. data/app/models/message.rb +30 -0
  19. data/app/models/message_blast.rb +27 -0
  20. data/app/models/user.rb +61 -0
  21. data/app/models/user_secret.rb +72 -0
  22. data/app/services/command_tower/README.md +49 -0
  23. data/app/services/command_tower/argument_validation/README.md +192 -0
  24. data/app/services/command_tower/argument_validation/class_methods.rb +178 -0
  25. data/app/services/command_tower/argument_validation/instance_methods.rb +148 -0
  26. data/app/services/command_tower/argument_validation.rb +11 -0
  27. data/app/services/command_tower/authorize/validate.rb +49 -0
  28. data/app/services/command_tower/inbox_service/blast/delete.rb +23 -0
  29. data/app/services/command_tower/inbox_service/blast/metadata.rb +26 -0
  30. data/app/services/command_tower/inbox_service/blast/new_user_blaster.rb +24 -0
  31. data/app/services/command_tower/inbox_service/blast/retrieve.rb +30 -0
  32. data/app/services/command_tower/inbox_service/blast/upsert.rb +67 -0
  33. data/app/services/command_tower/inbox_service/message/metadata.rb +35 -0
  34. data/app/services/command_tower/inbox_service/message/modify.rb +44 -0
  35. data/app/services/command_tower/inbox_service/message/retrieve.rb +36 -0
  36. data/app/services/command_tower/inbox_service/message/send.rb +33 -0
  37. data/app/services/command_tower/jwt/authenticate_user.rb +86 -0
  38. data/app/services/command_tower/jwt/decode.rb +21 -0
  39. data/app/services/command_tower/jwt/encode.rb +15 -0
  40. data/app/services/command_tower/jwt/login_create.rb +21 -0
  41. data/app/services/command_tower/jwt/time_delay_token.rb +17 -0
  42. data/app/services/command_tower/login_strategy/plain_text/create.rb +43 -0
  43. data/app/services/command_tower/login_strategy/plain_text/email_verification/generate.rb +29 -0
  44. data/app/services/command_tower/login_strategy/plain_text/email_verification/required.rb +20 -0
  45. data/app/services/command_tower/login_strategy/plain_text/email_verification/send.rb +23 -0
  46. data/app/services/command_tower/login_strategy/plain_text/email_verification/verify.rb +24 -0
  47. data/app/services/command_tower/login_strategy/plain_text/login.rb +50 -0
  48. data/app/services/command_tower/secrets/cleanse.rb +14 -0
  49. data/app/services/command_tower/secrets/generate.rb +62 -0
  50. data/app/services/command_tower/secrets/verify.rb +27 -0
  51. data/app/services/command_tower/secrets.rb +15 -0
  52. data/app/services/command_tower/service_base.rb +89 -0
  53. data/app/services/command_tower/service_logging.rb +41 -0
  54. data/app/services/command_tower/user_attributes/modify.rb +68 -0
  55. data/app/services/command_tower/user_attributes/roles.rb +27 -0
  56. data/app/services/command_tower/username/available.rb +64 -0
  57. data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +26 -0
  58. data/config/routes.rb +55 -0
  59. data/db/migrate/20241117043720_create_command_tower_users.rb +42 -0
  60. data/db/migrate/20241204065708_create_command_tower_user_secrets.rb +16 -0
  61. data/db/migrate/20250223023306_create_command_tower_messages.rb +12 -0
  62. data/db/migrate/20250223023313_create_command_tower_message_blasts.rb +14 -0
  63. data/lib/command_tower/authorization/default.yml +42 -0
  64. data/lib/command_tower/authorization/entity.rb +101 -0
  65. data/lib/command_tower/authorization/role.rb +101 -0
  66. data/lib/command_tower/authorization.rb +85 -0
  67. data/lib/command_tower/configuration/admin/config.rb +18 -0
  68. data/lib/command_tower/configuration/application/config.rb +40 -0
  69. data/lib/command_tower/configuration/authorization/config.rb +24 -0
  70. data/lib/command_tower/configuration/base.rb +11 -0
  71. data/lib/command_tower/configuration/config.rb +77 -0
  72. data/lib/command_tower/configuration/email/config.rb +87 -0
  73. data/lib/command_tower/configuration/jwt/config.rb +22 -0
  74. data/lib/command_tower/configuration/login/config.rb +18 -0
  75. data/lib/command_tower/configuration/login/strategy/plain_text/config.rb +57 -0
  76. data/lib/command_tower/configuration/login/strategy/plain_text/email_verify.rb +50 -0
  77. data/lib/command_tower/configuration/login/strategy/plain_text/lockable.rb +27 -0
  78. data/lib/command_tower/configuration/otp/config.rb +54 -0
  79. data/lib/command_tower/configuration/user/config.rb +56 -0
  80. data/lib/command_tower/configuration/username/check.rb +31 -0
  81. data/lib/command_tower/configuration/username/config.rb +41 -0
  82. data/lib/command_tower/engine.rb +53 -0
  83. data/lib/command_tower/error.rb +5 -0
  84. data/lib/command_tower/schema/admin/users.rb +15 -0
  85. data/lib/command_tower/schema/error/base.rb +15 -0
  86. data/lib/command_tower/schema/error/invalid_argument.rb +15 -0
  87. data/lib/command_tower/schema/error/invalid_argument_response.rb +17 -0
  88. data/lib/command_tower/schema/inbox/blast_request.rb +15 -0
  89. data/lib/command_tower/schema/inbox/blast_response.rb +16 -0
  90. data/lib/command_tower/schema/inbox/message_blast_entity.rb +16 -0
  91. data/lib/command_tower/schema/inbox/message_blast_metadata.rb +16 -0
  92. data/lib/command_tower/schema/inbox/message_entity.rb +14 -0
  93. data/lib/command_tower/schema/inbox/metadata.rb +18 -0
  94. data/lib/command_tower/schema/inbox/modified.rb +13 -0
  95. data/lib/command_tower/schema/page.rb +14 -0
  96. data/lib/command_tower/schema/plain_text/create_user_request.rb +18 -0
  97. data/lib/command_tower/schema/plain_text/create_user_response.rb +17 -0
  98. data/lib/command_tower/schema/plain_text/email_verify_request.rb +11 -0
  99. data/lib/command_tower/schema/plain_text/email_verify_response.rb +11 -0
  100. data/lib/command_tower/schema/plain_text/email_verify_send_request.rb +9 -0
  101. data/lib/command_tower/schema/plain_text/email_verify_send_response.rb +11 -0
  102. data/lib/command_tower/schema/plain_text/login_request.rb +15 -0
  103. data/lib/command_tower/schema/plain_text/login_response.rb +13 -0
  104. data/lib/command_tower/schema/user.rb +28 -0
  105. data/lib/command_tower/schema.rb +38 -0
  106. data/lib/command_tower/spec_helper.rb +19 -0
  107. data/lib/command_tower/version.rb +5 -0
  108. data/lib/command_tower.rb +33 -0
  109. data/lib/generators/api_engine_base/configure/USAGE +8 -0
  110. data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
  111. data/lib/tasks/auto_annotate_models.rake +60 -0
  112. metadata +255 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::ArgumentValidation
4
+ module ClassMethods
5
+ ON_ARGUMENT_VALIDATION = [
6
+ DEFAULT_VALIDATION = :raise,
7
+ :fail_early,
8
+ :log,
9
+ ]
10
+
11
+ def on_argument_validation_assigned
12
+ @on_argument_validation ||= DEFAULT_VALIDATION
13
+ end
14
+
15
+ def on_argument_validation(set)
16
+ raise "Must be one of #{ON_ARGUMENT_VALIDATION}" unless ON_ARGUMENT_VALIDATION.include?(set)
17
+
18
+ @on_argument_validation = set
19
+ end
20
+
21
+ def compose_exact(count, name, required:, delegation: false, &block)
22
+ raise CommandTower::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
23
+
24
+ validation_proc = Proc.new do |input_count, keys|
25
+ language = (input_count > 0) ? "But #{input_count} keys were assigned" : "But no key was assigned"
26
+ {
27
+ message: "Expected [#{count}] of the keys to have a value assigned. #{language}",
28
+ is_valid: (input_count == count),
29
+ requirement: "Exactly #{count} key(s) of #{keys} must be provided.",
30
+ }
31
+ end
32
+
33
+ composition_validation_proc = Proc.new do
34
+ keys = compositions[__stacked_type.last[:name]][:keys]
35
+ if keys.length < count
36
+ raise CommandTower::ServiceBase::CompositionValidationError, "Composition configuration error. Key [#{name}] expects EXACTLY #{count} keys to create an instance. Only #{keys.length} is provided for the composition. Please add more keys or reduce the expectation"
37
+ end
38
+ end
39
+ composition(name:, type: :compose_exact, required:, delegation:, composition_validation_proc:, validation_proc:, &block)
40
+ end
41
+
42
+ def at_most(count, name, required:, &block)
43
+ raise CommandTower::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
44
+
45
+ validation_proc = Proc.new do |input_count, keys|
46
+ {
47
+ message: "Expected at most #{count} keys assigned",
48
+ is_valid: (input_count <= count),
49
+ requirement: "Validation Error: At most #{count} key(s) of #{keys} can be provided.",
50
+ }
51
+ end
52
+
53
+ composition(name:, type: :at_most, required:, delegation: false, validation_proc:, &block)
54
+ end
55
+
56
+ def at_least(count, name, required:, &block)
57
+ raise CommandTower::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
58
+
59
+ validation_proc = Proc.new do |input_count, keys|
60
+ {
61
+ message: "Expected at least #{count} keys assigned. Available keys",
62
+ is_valid: (input_count >= count),
63
+ requirement: "Validation Error: At least #{count} key(s) of #{keys} must be provided.",
64
+ }
65
+ end
66
+
67
+ composition_validation_proc = Proc.new do
68
+ keys = compositions[__stacked_type.last[:name]][:keys]
69
+ if keys.length < count
70
+ raise CommandTower::ServiceBase::CompositionValidationError, "Composition configuration error. Key [#{name}] expects AT LEAST #{count} keys to create an instance. Only #{keys.length} is provided for the composition. Please add more keys or reduce the expectation"
71
+ end
72
+ end
73
+ composition(name:, type: :at_least, required:, delegation: false, composition_validation_proc:, validation_proc:, &block)
74
+ end
75
+
76
+ def at_least_one(name, required:, &block)
77
+ at_least(1, name, required:, &block)
78
+ end
79
+
80
+ def one_of(name, required:, delegation: true, &block)
81
+ compose_exact(1, name, required:, delegation:, &block)
82
+ end
83
+
84
+ def composition(name:, type:, required:, delegation:, validation_proc:, composition_validation_proc: nil, &block)
85
+ compositions[name] ||= { type:, name:, keys: [], required:, delegation:, validation_proc: }
86
+ if __stacked_type.map { _1[:type] }.include?(type)
87
+ raise CommandTower::ServiceBase::NestedDuplicateTypeError, "Duplicate Nested type's are not allowed. #{type} composition was included more than once"
88
+ end
89
+
90
+ __stacked_type << { type:, name: }
91
+
92
+ yield
93
+
94
+ if composition_validation_proc
95
+ composition_validation_proc.()
96
+ end
97
+
98
+ if __existing_names.include?(name)
99
+ raise CommandTower::ServiceBase::NameConflictError, "Name conflict for #{name}. Duplicated as a key"
100
+ end
101
+
102
+ __existing_names << name
103
+ # returning from the yield...pop it from the stack.
104
+ # This allows us to know the current depth of where we are in the nested stack
105
+ __stacked_type.pop
106
+
107
+ if delegation
108
+ delegate name, to: :context
109
+ delegate :"#{name}_key", to: :context
110
+ end
111
+ end
112
+
113
+ def validate(name, default: nil, length: false, is_a: nil, is_one: nil, lt: nil, lte: nil, eq: nil, gt: nil, gte: nil, delegation: true, sensitive: false, required: false)
114
+ if __existing_names.include?(name)
115
+ raise CommandTower::ServiceBase::NameConflictError, "Duplicate key name found. [#{name}] can only be defined once"
116
+ end
117
+
118
+ __existing_names << name
119
+
120
+ if default
121
+ if is_a
122
+ if Array(is_a).none? { _1 === default }
123
+ raise CommandTower::ServiceBase::DefaultValueError, "Default value provided [#{default}] does not match any `is_a` value(s) of #{is_a}."
124
+ end
125
+ end
126
+
127
+ if is_one
128
+ if Array(is_one).none? { _1 == default }
129
+ raise CommandTower::ServiceBase::DefaultValueError, "Default value provided [#{default}] does not match any `is_one` value(s) of #{is_one}."
130
+ end
131
+ end
132
+ end
133
+
134
+ if __stacked_type.length > 0
135
+ compositions[__stacked_type.last[:name]][:keys] << name
136
+ end
137
+
138
+ validate_params << {
139
+ name:,
140
+ is_a:,
141
+ lt:,
142
+ lte:,
143
+ eq:,
144
+ gt:,
145
+ gte:,
146
+ length:,
147
+ required:,
148
+ is_one:,
149
+ default:,
150
+ }
151
+ sensitive_params << name if sensitive
152
+
153
+ if delegation
154
+ delegate name, to: :context
155
+ end
156
+ end
157
+
158
+ def sensitive_params
159
+ @sensitive_params ||= []
160
+ end
161
+
162
+ def validate_params
163
+ @validate_params ||= []
164
+ end
165
+
166
+ def compositions
167
+ @compositions ||= {}
168
+ end
169
+
170
+ def __stacked_type
171
+ @__stacked_type ||= []
172
+ end
173
+
174
+ def __existing_names
175
+ @__existing_names ||= []
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::ArgumentValidation
4
+ module InstanceMethods
5
+ def run_validations!
6
+ context.valid_arguments = true
7
+
8
+ validate_param!
9
+ validate_compositions!
10
+ continue_with_logical_code!
11
+ end
12
+
13
+ def continue_with_logical_code!
14
+ return if @context_validation_failures.nil?
15
+
16
+ invalid_argument_keys = @context_validation_failures.keys
17
+ msg = @context_validation_failures.map { |_k, obj| obj[:msg] }.join(", ")
18
+ context.fail!(msg:, invalid_argument_hash: @context_validation_failures, invalid_argument_keys:, invalid_arguments: true)
19
+ end
20
+
21
+ def inline_argument_failure!(errors:)
22
+ errors = errors.to_hash
23
+ invalid_argument_keys = errors.keys
24
+ invalid_argument_hash = {}
25
+ human_readable = []
26
+
27
+ errors.each do |k, v|
28
+ error_message = Array(v).join(", ")
29
+ invalid_argument_hash[k] = { msg: error_message }
30
+ human_readable << "#{k}: #{error_message}"
31
+ end
32
+
33
+ msg = "Invalid arguments: #{human_readable.join(", ")}"
34
+ context.fail!(msg:, invalid_argument_hash:, invalid_argument_keys:, invalid_arguments: true)
35
+ end
36
+
37
+ def validate_param!
38
+ self.class.validate_params.each do |metadata|
39
+ value = context.public_send(metadata[:name])
40
+ use_length = metadata[:length]
41
+ if metadata[:required] && value.nil?
42
+ __failed_argument_validation(msg: "Parameter [#{metadata[:name]}] is required but not present", argument: metadata[:name], metadata:)
43
+ end
44
+
45
+ if value.nil? && metadata[:default]
46
+ context.public_send(:"#{metadata[:name]}=", metadata[:default])
47
+ next
48
+ elsif value.nil?
49
+ next
50
+ end
51
+
52
+ if is_a = metadata[:is_a]
53
+ direct_type = false
54
+ ancestor_type = false
55
+
56
+ # Check if direct type of `is_a` Integer === 5 => true
57
+ direct_type = Array(is_a).none? { _1 === value }
58
+
59
+ # If it is a direct type, we dont need to do any other type of checking
60
+ if direct_type == true
61
+ lineage = value.ancestors rescue []
62
+ # Check inclusion in ancestor list
63
+ ancestor_type = Array(is_a).none? { lineage.include?(_1) }
64
+ end
65
+ if direct_type && ancestor_type
66
+ __failed_argument_validation(msg: "Parameter [#{metadata[:name]}] must be of type #{is_a}. Given #{value.class} [#{value}]", argument: metadata[:name], metadata:)
67
+ end
68
+ end
69
+
70
+ if metadata[:is_one]
71
+ if Array(metadata[:is_one]).none? { _1 == value }
72
+ __failed_argument_validation(msg: "Parameter [#{metadata[:name]}] must be one of #{Array(metadata[:is_one])}. Given #{value}", argument: metadata[:name], metadata:)
73
+ end
74
+ end
75
+
76
+ validate_sign!(name: metadata[:name], value:, sign: "lt", validation: metadata[:lte], use_length:, metadata:) { (use_length ? value.length : value) <= _1 }
77
+ validate_sign!(name: metadata[:name], value:, sign: "lte", validation: metadata[:lt], use_length:, metadata:) { (use_length ? value.length : value) < _1 }
78
+ validate_sign!(name: metadata[:name], value:, sign: "eq", validation: metadata[:eq], use_length:, metadata:) { (use_length ? value.length : value) == _1 }
79
+ validate_sign!(name: metadata[:name], value:, sign: "gte", validation: metadata[:gte], use_length:, metadata:) { (use_length ? value.length : value) >= _1 }
80
+ validate_sign!(name: metadata[:name], value:, sign: "gt", validation: metadata[:gt], use_length:, metadata:) { (use_length ? value.length : value) > _1 }
81
+ end
82
+ end
83
+
84
+ def validate_compositions!
85
+ self.class.compositions.each do |type, metadata|
86
+ value_list = {}
87
+
88
+ metadata[:keys].each do |argument|
89
+ value = context.public_send(argument)
90
+ next if value.nil?
91
+
92
+ value_list[argument] = value
93
+ end
94
+
95
+ composition_result = metadata[:validation_proc].(value_list.count, value_list.keys)
96
+
97
+ next if value_list.count == 0 && !metadata[:required]
98
+
99
+ if !composition_result[:is_valid]
100
+ composition_result[:message]
101
+ context.client_composite_error = composition_result[:requirement]
102
+ msg = "Composite Key failure for #{type} [#{metadata[:name]}]. #{composition_result[:message]}. Provided values for the following keys: #{value_list.keys}. Available keys #{metadata[:keys]}"
103
+ __failed_argument_validation(msg:, argument: metadata[:name], metadata: ,error: CommandTower::ServiceBase::CompositionValidationError)
104
+ next
105
+ end
106
+
107
+ if metadata[:delegation]
108
+ context.public_send("#{metadata[:name]}=", value_list.first[1])
109
+ context.public_send("#{metadata[:name]}_key=", value_list.first[0])
110
+ end
111
+ end
112
+ end
113
+
114
+ def validate_sign!(name:, value:, sign:, validation:, use_length:, metadata:)
115
+ return if validation.nil?
116
+ return if yield(validation)
117
+
118
+ __failed_argument_validation(metadata:, msg: "Parameter [#{name}]#{ " lengths" if use_length} must be #{sign} to #{validation}. Given #{value}", argument: name)
119
+ end
120
+
121
+ def __failed_argument_validation(msg:, argument:, metadata:, error: CommandTower::ServiceBase::ArgumentValidationError)
122
+ case self.class.on_argument_validation_assigned
123
+ when :raise
124
+ raise error, msg
125
+ when :fail_early
126
+ @context_validation_failures ||= {}
127
+ @context_validation_failures[argument] = {
128
+ msg: msg,
129
+ required: metadata[:requirement],
130
+ is_a: metadata[:is_a],
131
+ }
132
+ # When gracefully failing, it will find all failures first before setting appropriate
133
+ # context variables -- Check out continue_with_logical_code!
134
+ else
135
+ context.invalid_arguments = true
136
+ log_warn(msg)
137
+ end
138
+ end
139
+
140
+ def sanitize_params
141
+ self.class.sensitive_params.each do |param|
142
+ next if context.send(param).nil?
143
+
144
+ context.send("#{param}=","[FILTERED]")
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::ArgumentValidation
4
+ class NameConflictError < CommandTower::ServiceBase::ConfigurationError; end
5
+ class NestedDuplicateTypeError < CommandTower::ServiceBase::ConfigurationError; end
6
+
7
+ def self.included(base)
8
+ base.extend(CommandTower::ArgumentValidation::ClassMethods)
9
+ base.include(CommandTower::ArgumentValidation::InstanceMethods)
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower::Authorize
4
+ class Validate < CommandTower::ServiceBase
5
+ on_argument_validation :fail_early
6
+
7
+ validate :user, is_a: User, required: true
8
+ validate :controller, is_a: [ActionController::Base, ActionController::API], required: true
9
+ validate :method, is_a: String, required: true
10
+
11
+ def call
12
+ context.authorization_required = authorization_required?
13
+ unless context.authorization_required
14
+ log_info("controller:#{controller}; method:#{method} -- No Authorization required")
15
+ context.msg = "Authorization not required at this time"
16
+ return
17
+ end
18
+
19
+ # At this point we know authorization on the route is required
20
+ # Iterate through the users roles to find a matching role that allows authorization
21
+ # If at least 1 of the users roles passes validation, we can allow access to the path
22
+ log_info("User Roles: #{user.roles}")
23
+ auhorization_result = user_role_objects.any? do |_role_name, role_object|
24
+ result = role_object.authorized?(controller:, method:, user:)
25
+ log_info("Role:#{result[:role]};Authorized:[#{result[:authorized]}];Reason:[#{result[:reason]}]")
26
+ result[:authorized] == true
27
+ end
28
+
29
+ if auhorization_result
30
+ context.msg = "User is Authorized for action"
31
+ else
32
+ context.fail!(msg: "Unauthorized Access. Incorrect User Privileges")
33
+ end
34
+ end
35
+
36
+ def authorization_required?
37
+ controller_mapping = CommandTower::Authorization.mapped_controllers[controller]
38
+ return false if controller_mapping.nil?
39
+
40
+ controller_mapping.include?(method.to_sym)
41
+ end
42
+
43
+ def user_role_objects
44
+ CommandTower::Authorization::Role.roles.select do |role_name, _|
45
+ user.roles.include?(role_name.to_s)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Blast
6
+ class Delete < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ validate :id, is_a: Integer, required: true
10
+
11
+ def call
12
+ message_blast = ::MessageBlast.where(id:).first
13
+
14
+ if message_blast.nil?
15
+ inline_argument_failure!(errors: { id: "MessageBlast ID not found" })
16
+ end
17
+
18
+ message_blast.destroy
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Blast
6
+ class Metadata < CommandTower::ServiceBase
7
+ def call
8
+ entities = ::MessageBlast.all.select(:id, :title, :existing_users, :new_users).map do |mb|
9
+ CommandTower::Schema::Inbox::MessageBlastEntity.new(
10
+ title: mb.title,
11
+ id: mb.id,
12
+ existing_users: mb.existing_users,
13
+ new_users: mb.new_users,
14
+ )
15
+ end
16
+
17
+
18
+ context.metadata = CommandTower::Schema::Inbox::MessageBlastMetadata.new(
19
+ entities: entities,
20
+ count: entities.length,
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Blast
6
+ class NewUserBlaster < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ validate :user, is_a: User, required: true
10
+
11
+ def call
12
+ ::MessageBlast.where(new_users: true).each do |message_blast|
13
+ InboxService::Message::Send.(
14
+ user:,
15
+ text: message_blast.text,
16
+ title: message_blast.title,
17
+ message_blast:,
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Blast
6
+ class Retrieve < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ validate :id, is_a: Integer, required: true
10
+
11
+ def call
12
+ message_blast = ::MessageBlast.where(id:).first
13
+
14
+ if message_blast.nil?
15
+ inline_argument_failure!(errors: { id: "MessageBlast ID not found" })
16
+ end
17
+
18
+ context.message_blast = CommandTower::Schema::Inbox::MessageBlastEntity.new(
19
+ created_by: CommandTower::Schema::User.convert_user_object(user: message_blast.user),
20
+ title: message_blast.title,
21
+ text: message_blast.text,
22
+ id: message_blast.id,
23
+ existing_users: message_blast.existing_users,
24
+ new_users: message_blast.new_users,
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Blast
6
+ class Upsert < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ validate :user, is_a: User, required: true
10
+ validate :existing_users, is_one: [true, false], required: true
11
+ validate :new_users, is_one: [true, false], required: true
12
+ validate :text, is_a: String, required: true
13
+ validate :title, is_a: String, required: true
14
+ validate :id, is_a: Integer, required: false
15
+
16
+ def call
17
+ ar = record
18
+
19
+ ar.existing_users = existing_users
20
+ ar.new_users = new_users
21
+ ar.text = text
22
+ ar.title = title
23
+ ar.user = user
24
+
25
+ # Probably should wrap this in a transaction block
26
+ ar.save!
27
+ blast_messages!(message_blast: ar)
28
+
29
+ context.message_blast = ar
30
+ context.blast = CommandTower::Schema::Inbox::BlastResponse.new(
31
+ existing_users:,
32
+ new_users:,
33
+ text:,
34
+ title:,
35
+ id: ar.id,
36
+ created_by: CommandTower::Schema::User.convert_user_object(user:)
37
+ )
38
+ end
39
+
40
+ def blast_messages!(message_blast:)
41
+ # Only blast messages to existing users that are brand new
42
+ return if @existing_record
43
+ return unless existing_users
44
+
45
+ User.all.each do |u|
46
+ InboxService::Message::Send.(
47
+ user: u,
48
+ text: text,
49
+ title: title,
50
+ message_blast: message_blast,
51
+ )
52
+ end
53
+ end
54
+
55
+ def record
56
+ return MessageBlast.new if id.nil?
57
+
58
+ record = MessageBlast.where(id:).first
59
+ inline_argument_failure!(errors: { id: "Message Blast ID does not exist" }) if record.nil?
60
+
61
+ @existing_record = true
62
+ record
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Message
6
+ class Metadata < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ validate :user, is_a: User, required: true
10
+
11
+ def call
12
+ entities = ::Message.where(user:).select(:id, :title, :viewed).map do |message|
13
+ CommandTower::Schema::Inbox::MessageEntity.new(
14
+ title: message.title,
15
+ id: message.id,
16
+ viewed: message.viewed,
17
+ )
18
+ end
19
+
20
+ params = {
21
+ entities: entities.nil? ? nil : entities,
22
+ count: entities.length,
23
+ }
24
+
25
+ context.metadata = CommandTower::Schema::Inbox::Metadata.new(**params.compact)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+
34
+
35
+
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Message
6
+ class Modify < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ IS_ONE = [
10
+ VIEWED = :viewed,
11
+ DELETE = :delete,
12
+ ]
13
+ validate :user, is_a: User, required: true
14
+ validate :ids, is_a: Array, required: true
15
+ validate :type, is_one: IS_ONE, required: true
16
+
17
+ def call
18
+ messages = ::Message.where(user:, id: ids)
19
+ if messages.empty?
20
+ inline_argument_failure!(errors: { ids: "No ID's found for user" })
21
+ end
22
+
23
+ case type
24
+ when VIEWED
25
+ modified_ids = messages.update(viewed: true).pluck(:id)
26
+ when DELETE
27
+ modified_ids = messages.destroy_all.pluck(:id)
28
+ end
29
+
30
+ context.modified = CommandTower::Schema::Inbox::Modified.new(
31
+ ids: modified_ids,
32
+ type:,
33
+ count: modified_ids.length,
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+
42
+
43
+
44
+
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module InboxService
5
+ module Message
6
+ class Retrieve < CommandTower::ServiceBase
7
+ on_argument_validation :fail_early
8
+
9
+ validate :user, is_a: User, required: true
10
+ validate :id, is_a: Integer, required: true
11
+
12
+ def call
13
+ message = ::Message.where(user:, id:).first
14
+
15
+ if message.nil?
16
+ inline_argument_failure!(errors: { id: "Message ID not found for user" })
17
+ end
18
+
19
+ message.update!(viewed: true)
20
+
21
+ context.message = CommandTower::Schema::Inbox::MessageEntity.new(
22
+ title: message.title,
23
+ id: message.id,
24
+ text: message.text,
25
+ viewed: message.viewed,
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+
34
+
35
+
36
+