foobara 0.0.115 → 0.0.116

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94baacc7205e4b750c59df65dede92b2c95f15053144ba8d8324c81eb80da770
4
- data.tar.gz: 696db0d2d875c8f2c40ed9d916019518c3f2ffcc0829015a0333109c83a1e8ef
3
+ metadata.gz: fe51e6ab90aafb99246fd2872f0faa452004f71b0d37a7e68a2fc7e8027feff7
4
+ data.tar.gz: d9027059c2a75b268950b661e5c3bb3fd1d633f52afbbe6d302c8ba3351908bc
5
5
  SHA512:
6
- metadata.gz: 5b5cd5e50eee5c8f652d24107027eaf9115128742b57f8964010667d0e12da2a5bc1087d87870c507c9bd0cc6016eaa0248e9707d567c96c4193794863bebb6a
7
- data.tar.gz: 356150abc278d32b253ddc08ed1f4932ac4432a25f9ed4797518191b9a86fc01c06986df8e314aa5a6fc64b44441adc89946a6b6f49daac42d424a73d41e2589
6
+ metadata.gz: 3beecfb43803f74a01d9f1d6e36c6b6f10c5abbae6dc3eaf9bc9a62afe25d3413313ed6f65262ba4e4c165e8545bbb506f045cafbd3d7673a37d4b87e9202661
7
+ data.tar.gz: 34554e412fc605f453392d2432f59311abe65498ec0db28e1c617c61b4c7fb1ac4e1bd7140f1ee8064482355a6a90e800cefea7b2ea8f35e23ad00eed59cf674
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [0.0.116] - 2025-05-03
2
+
3
+ - Add automatic transaction support to requests, cover in/out mutators/transformers
4
+ - This makes it so that authenticators and allowed rules can more cleanly be
5
+ expressed when they have the same entity bases needed by the transformed command
6
+ - Move #authenticated_user from TransformedCommand to Request
7
+ - Add a Request#authenticated_credential
8
+ - Provide a way to skip validations for models
9
+ - A type without sensitive types derived from an entity type will now be registered as a
10
+ detached entity
11
+ - Fix model type re-registering bug
12
+
1
13
  # [0.0.115] - 2025-05-01
2
14
 
3
15
  - Make sure Authenticator#authenticate hits #applicable?
@@ -3,81 +3,22 @@ module Foobara
3
3
  module Concerns
4
4
  module Transactions
5
5
  include Concern
6
-
7
- def transactions
8
- @transactions ||= []
9
- end
10
-
11
- def opened_transactions
12
- @opened_transactions ||= []
13
- end
14
-
15
- def auto_detect_current_transactions
16
- bases = relevant_entity_classes.map(&:entity_base).uniq
17
-
18
- bases.each do |base|
19
- tx = base.current_transaction
20
- transactions << tx if tx
21
- end
22
- end
6
+ include NestedTransactionable
23
7
 
24
8
  def relevant_entity_classes
25
- @relevant_entity_classes ||= begin
26
- entity_classes = if inputs_type
27
- Entity.construct_associations(
28
- inputs_type
29
- ).values.uniq.map(&:target_class)
30
- else
31
- []
32
- end
33
-
34
- if result_type
35
- entity_classes += Entity.construct_associations(
36
- result_type
37
- ).values.uniq.map(&:target_class)
38
-
39
- if result_type.extends?(BuiltinTypes[:entity])
40
- entity_classes << result_type.target_class
41
- end
42
- end
9
+ return @relevant_entity_classes if defined?(@relevant_entity_classes)
43
10
 
44
- entity_classes += entity_classes.uniq.map do |entity_class|
45
- entity_class.deep_associations.values
46
- end.flatten.uniq.map(&:target_class)
11
+ entity_classes = if inputs_type
12
+ relevant_entity_classes_for_type(inputs_type)
13
+ else
14
+ []
15
+ end
47
16
 
48
- [*entity_classes, *self.class.depends_on_entities].uniq
17
+ if result_type
18
+ entity_classes += relevant_entity_classes_for_type(result_type)
49
19
  end
50
- end
51
-
52
- def open_transaction
53
- auto_detect_current_transactions
54
-
55
- bases_not_needing_transaction = transactions.map(&:entity_base)
56
-
57
- bases_needing_transaction = relevant_entity_classes.map(&:entity_base).uniq - bases_not_needing_transaction
58
-
59
- bases_needing_transaction.each do |entity_base|
60
- transaction = entity_base.transaction
61
- transaction.open!
62
- opened_transactions << transaction
63
- transactions << transaction
64
- end
65
- end
66
-
67
- def rollback_transaction
68
- opened_transactions.reverse.each do |transaction|
69
- if transaction.currently_open?
70
- # Hard to test this because halting and other exceptions rollback the transactions via
71
- # block form but to be safe keeping this
72
- # :nocov:
73
- transaction.rollback!
74
- # :nocov:
75
- end
76
- end
77
- end
78
20
 
79
- def commit_transaction
80
- opened_transactions.reverse.each(&:commit!)
21
+ @relevant_entity_classes = [*entity_classes, *self.class.depends_on_entities].uniq
81
22
  end
82
23
  end
83
24
  end
@@ -13,6 +13,10 @@ module Foobara
13
13
  @block = block
14
14
  end
15
15
 
16
+ def relevant_entity_classes(_request)
17
+ nil
18
+ end
19
+
16
20
  def symbol
17
21
  declaration_data[:symbol]
18
22
  end
@@ -24,6 +24,14 @@ module Foobara
24
24
  def authenticate(request)
25
25
  selector.process_value!(request)
26
26
  end
27
+
28
+ def relevant_entity_classes(request)
29
+ outcome = selector.processor_for(request)
30
+
31
+ if outcome.success?
32
+ outcome.result&.relevant_entity_classes(request)
33
+ end
34
+ end
27
35
  end
28
36
  end
29
37
  end
@@ -2,9 +2,11 @@ module Foobara
2
2
  class CommandConnector
3
3
  class Request
4
4
  include TruncatedInspect
5
+ include NestedTransactionable
5
6
 
6
7
  # TODO: this feels like a smell of some sort...
7
8
  attr_accessor :command,
9
+ :command_class,
8
10
  :error,
9
11
  :command_connector,
10
12
  # Why aren't there serializers on the response?
@@ -12,9 +14,9 @@ module Foobara
12
14
  :inputs,
13
15
  :full_command_name,
14
16
  :action,
15
- :response
16
-
17
- attr_reader :command_class
17
+ :response,
18
+ :authenticated_user,
19
+ :authenticated_credential
18
20
 
19
21
  def initialize(**opts)
20
22
  valid_keys = %i[inputs full_command_name action serializers]
@@ -33,8 +35,8 @@ module Foobara
33
35
  self.serializers = Util.array(opts[:serializers]) if opts.key?(:serializers)
34
36
  end
35
37
 
36
- def command_class=(klass)
37
- @command_class = klass
38
+ def mutate_request
39
+ return if error
38
40
 
39
41
  # TODO: we really need to revisit these interfaces. Something is wrong.
40
42
  if command_class.respond_to?(:mutate_request)
@@ -42,6 +44,25 @@ module Foobara
42
44
  end
43
45
  end
44
46
 
47
+ def authenticate
48
+ return if error
49
+ return unless command_class.respond_to?(:requires_authentication) && command_class.requires_authentication
50
+
51
+ authenticated_user, authenticated_credential = authenticator.authenticate(self)
52
+
53
+ # TODO: why are these on the command instead of the request??
54
+ self.authenticated_user = authenticated_user
55
+ self.authenticated_credential = authenticated_credential
56
+
57
+ unless authenticated_user
58
+ self.error = CommandConnector::UnauthenticatedError.new
59
+ end
60
+ end
61
+
62
+ def authenticator
63
+ command_class.authenticator
64
+ end
65
+
45
66
  def serializer
46
67
  return @serializer if defined?(@serializer)
47
68
 
@@ -77,12 +98,10 @@ module Foobara
77
98
  end
78
99
 
79
100
  def outcome
80
- outcome = command&.outcome
81
-
82
- if outcome
83
- outcome
84
- elsif error
101
+ if error
85
102
  Outcome.error(error)
103
+ else
104
+ command&.outcome
86
105
  end
87
106
  end
88
107
 
@@ -94,8 +113,34 @@ module Foobara
94
113
  outcome.error_collection
95
114
  end
96
115
 
116
+ def relevant_entity_classes
117
+ if command_class.is_a?(::Class) && command_class < TransformedCommand
118
+ entity_classes = authenticator&.relevant_entity_classes(self)
119
+ [*entity_classes, *relevant_entity_classes_from_inputs_transformer]
120
+ end || []
121
+ end
122
+
97
123
  private
98
124
 
125
+ def relevant_entity_classes_from_inputs_transformer(
126
+ object = [*command_class.inputs_transformer, *command_class.result_transformer]
127
+ )
128
+ case object
129
+ when TypeDeclarations::TypedTransformer
130
+ relevant_entity_classes_from_inputs_transformer([*object.from_type, *object.to_type])
131
+ when Types::Type
132
+ relevant_entity_classes_for_type(object)
133
+ when ::Array
134
+ object.map do |o|
135
+ relevant_entity_classes_from_inputs_transformer(o)
136
+ end.flatten
137
+ when Value::Processor::Pipeline
138
+ relevant_entity_classes_from_inputs_transformer(object.processors)
139
+ else
140
+ []
141
+ end
142
+ end
143
+
99
144
  def objects_to_serializers(objects)
100
145
  objects.map do |object|
101
146
  case object
@@ -22,6 +22,10 @@ module Foobara
22
22
  def success?
23
23
  request.success?
24
24
  end
25
+
26
+ def outcome
27
+ request.outcome
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -154,15 +154,11 @@ module Foobara
154
154
  command_registry.foobara_lookup_command(name)
155
155
  end
156
156
 
157
- # TODO: maybe instead connect commands with a shortcut_only: "describe" option
158
- # in order to make this easier to extend and manage.
159
- def request_to_command(request)
157
+ def request_to_command_class(request)
160
158
  action = request.action
161
- inputs = nil
162
159
  full_command_name = request.full_command_name
163
160
 
164
- case action
165
- when "run"
161
+ if action == "run"
166
162
  transformed_command_class = transformed_command_from_name(full_command_name)
167
163
 
168
164
  unless transformed_command_class
@@ -171,9 +167,36 @@ module Foobara
171
167
  # :nocov:
172
168
  end
173
169
 
174
- request.command_class = transformed_command_class
170
+ transformed_command_class
171
+ else
172
+ action = case action
173
+ when "describe_type", "manifest", "describe_command"
174
+ "describe"
175
+ when "describe", "ping", "query_git_commit_info", "help"
176
+ action
177
+ when "list"
178
+ "list_commands"
179
+ else
180
+ # :nocov:
181
+ raise InvalidContextError.new(message: "Not sure what to do with #{action}")
182
+ # :nocov:
183
+ end
184
+
185
+ command_name = Util.classify(action)
186
+ command_class = find_builtin_command_class(command_name)
187
+ full_command_name = command_class.full_command_name
188
+
189
+ transformed_command_from_name(full_command_name) || transform_command_class(command_class)
190
+ end
191
+ end
192
+
193
+ def request_to_command_inputs(request)
194
+ action = request.action
195
+ full_command_name = request.full_command_name
175
196
 
176
- inputs = request.inputs
197
+ case action
198
+ when "run"
199
+ request.inputs
177
200
  when "describe"
178
201
  manifestable = transformed_command_from_name(full_command_name) || type_from_name(full_command_name)
179
202
 
@@ -185,13 +208,7 @@ module Foobara
185
208
  # :nocov:
186
209
  end
187
210
 
188
- command_class = find_builtin_command_class("Describe")
189
- full_command_name = command_class.full_command_name
190
-
191
- inputs = request.inputs.merge(manifestable:, request:)
192
-
193
- transformed_command_class = transformed_command_from_name(full_command_name) ||
194
- transform_command_class(command_class)
211
+ request.inputs.merge(manifestable:, request:)
195
212
  when "describe_command"
196
213
  transformed_command_class = transformed_command_from_name(full_command_name)
197
214
 
@@ -201,12 +218,7 @@ module Foobara
201
218
  # :nocov:
202
219
  end
203
220
 
204
- command_class = find_builtin_command_class("Describe")
205
- full_command_name = command_class.full_command_name
206
-
207
- inputs = request.inputs.merge(manifestable: transformed_command_class, request:)
208
- transformed_command_class = transformed_command_from_name(full_command_name) ||
209
- transform_command_class(command_class)
221
+ request.inputs.merge(manifestable: transformed_command_class, request:)
210
222
  when "describe_type"
211
223
  type = type_from_name(full_command_name)
212
224
 
@@ -216,59 +228,30 @@ module Foobara
216
228
  # :nocov:
217
229
  end
218
230
 
219
- command_class = find_builtin_command_class("Describe")
220
- full_command_name = command_class.full_command_name
221
-
222
- inputs = request.inputs.merge(manifestable: type, request:)
223
- transformed_command_class = transformed_command_from_name(full_command_name) ||
224
- transform_command_class(command_class)
231
+ request.inputs.merge(manifestable: type, request:)
225
232
  when "manifest"
226
- command_class = find_builtin_command_class("Describe")
227
- full_command_name = command_class.full_command_name
228
-
229
- inputs = request.inputs.merge(manifestable: self, request:)
230
- transformed_command_class = transformed_command_from_name(full_command_name) ||
231
- transform_command_class(command_class)
232
- when "ping"
233
- command_class = find_builtin_command_class("Ping")
234
- full_command_name = command_class.full_command_name
235
-
236
- transformed_command_class = transformed_command_from_name(full_command_name) ||
237
- transform_command_class(command_class)
238
- when "query_git_commit_info"
239
- # TODO: this feels out of control... should just accomplish this through run I think instead. Same with ping.
240
- command_class = find_builtin_command_class("QueryGitCommitInfo")
241
- full_command_name = command_class.full_command_name
242
-
243
- transformed_command_class = transformed_command_from_name(full_command_name) ||
244
- transform_command_class(command_class)
233
+ request.inputs.merge(manifestable: self, request:)
234
+ when "ping", "query_git_commit_info"
235
+ nil
245
236
  when "help"
246
- command_class = find_builtin_command_class("Help")
247
- full_command_name = command_class.full_command_name
248
-
249
- inputs = { request: }
250
- transformed_command_class = transformed_command_from_name(full_command_name) ||
251
- transform_command_class(command_class)
237
+ { request: }
252
238
  when "list"
253
- command_class = find_builtin_command_class("ListCommands")
254
-
255
- full_command_name = command_class.full_command_name
256
-
257
- request.command_class = command_class
258
- inputs = request.inputs.merge(request:)
259
-
260
- transformed_command_class = transformed_command_from_name(full_command_name) ||
261
- transform_command_class(command_class)
239
+ request.inputs.merge(request:)
262
240
  else
263
241
  # :nocov:
264
242
  raise InvalidContextError.new(message: "Not sure what to do with #{action}")
265
243
  # :nocov:
266
244
  end
245
+ end
246
+
247
+ def request_to_command_instance(request)
248
+ command_class = request.command_class
249
+ inputs = request.inputs
267
250
 
268
251
  if inputs && !inputs.empty?
269
- transformed_command_class.new(inputs)
252
+ command_class.new(inputs)
270
253
  else
271
- transformed_command_class.new
254
+ command_class.new
272
255
  end
273
256
  end
274
257
 
@@ -305,6 +288,7 @@ module Foobara
305
288
  command = response.command
306
289
 
307
290
  if command.respond_to?(:serialize_result)
291
+ # TODO: Get serialization off of the command instance!!
308
292
  response.body = command.serialize_result(response.body)
309
293
  end
310
294
  end
@@ -383,18 +367,40 @@ module Foobara
383
367
  end
384
368
 
385
369
  def run_request(request)
386
- command = build_command(request)
387
-
388
- if command.respond_to?(:requires_authentication?) && command.requires_authentication?
389
- authenticate(request)
390
- end
391
-
392
- if command
393
- run_command(request)
394
- # :nocov:
395
- elsif !request.error
396
- raise "No command returned from #request_to_command"
397
- # :nocov:
370
+ command_class = determine_command_class(request)
371
+ request.command_class = command_class
372
+
373
+ return build_response(request) unless command_class
374
+
375
+ begin
376
+ request.open_transaction
377
+ request.use_transaction do
378
+ request.authenticate
379
+ request.mutate_request
380
+
381
+ inputs = request_to_command_inputs(request)
382
+ request.inputs = inputs
383
+ command = build_command_instance(request)
384
+ request.command = command
385
+
386
+ unless request.error
387
+ if command
388
+ run_command(request)
389
+ # :nocov:
390
+ else
391
+ raise "No command returned from #request_to_command"
392
+ # :nocov:
393
+ end
394
+ end
395
+ end
396
+ ensure
397
+ request.use_transaction do
398
+ if (request.response || request).outcome&.success?
399
+ request.commit_transaction_if_open
400
+ else
401
+ request.rollback_transaction
402
+ end
403
+ end
398
404
  end
399
405
 
400
406
  build_response(request)
@@ -414,33 +420,22 @@ module Foobara
414
420
  end
415
421
  end
416
422
 
417
- def authenticate(request)
418
- request_command = request.command
419
- authenticator = request_command.authenticator || self.authenticator
420
-
421
- request_command.after_load_records do |command:, **|
422
- authenticated_user, authenticated_credential = authenticator.authenticate(request)
423
-
424
- # TODO: why are these on the command instead of the request??
425
- request_command.authenticated_user = authenticated_user
426
- request_command.authenticated_credential = authenticated_credential
427
-
428
- unless authenticated_user
429
- request_command.outcome = Outcome.error(CommandConnector::UnauthenticatedError.new)
430
-
431
- command.state_machine.error!
432
- command.halt!
433
- end
423
+ def build_command_instance(request)
424
+ command = request_to_command_instance(request)
425
+ request.command = command
426
+ if command.is_a?(TransformedCommand)
427
+ # This allows the command to access the authenticated_user
428
+ command.request = request
434
429
  end
430
+
431
+ command
435
432
  end
436
433
 
437
- def build_command(request)
434
+ def determine_command_class(request)
438
435
  unless request.error
439
- command = request_to_command(request)
440
- request.command = command
436
+ command_class = request_to_command_class(request)
437
+ request.command_class = command_class
441
438
  end
442
-
443
- command
444
439
  end
445
440
 
446
441
  def build_response(request)
@@ -11,7 +11,7 @@ module Foobara
11
11
  # Is there maybe prior art for this in the associations stuff?
12
12
  object.primary_key
13
13
  when DetachedEntity
14
- if declaration_data[:detached_to_primary_key]
14
+ if detached_to_primary_key?
15
15
  object.primary_key
16
16
  else
17
17
  object.attributes
@@ -28,6 +28,13 @@ module Foobara
28
28
  object
29
29
  end
30
30
  end
31
+
32
+ def detached_to_primary_key?
33
+ return true unless declaration_data.is_a?(::Hash)
34
+ return true unless declaration_data.key?(:detached_to_primary_key)
35
+
36
+ declaration_data[:detached_to_primary_key]
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -95,7 +95,9 @@ module Foobara
95
95
 
96
96
  command_class.inputs_type
97
97
  else
98
- inputs_transformer.from_type || command_class.inputs_type
98
+ if inputs_transformer.is_a?(TypeDeclarations::TypedTransformer)
99
+ inputs_transformer.from_type
100
+ end || command_class.inputs_type
99
101
  end
100
102
  else
101
103
  command_class.inputs_type
@@ -479,8 +481,7 @@ module Foobara
479
481
  end
480
482
  end
481
483
 
482
- attr_accessor :command, :untransformed_inputs, :transformed_inputs, :outcome, :authenticated_user,
483
- :authenticated_credential
484
+ attr_accessor :command, :untransformed_inputs, :transformed_inputs, :outcome, :request
484
485
 
485
486
  def initialize(untransformed_inputs = {})
486
487
  self.untransformed_inputs = untransformed_inputs || {}
@@ -499,7 +500,6 @@ module Foobara
499
500
  :errors_transformers,
500
501
  :pre_commit_transformers,
501
502
  :serializers,
502
- :requires_authentication,
503
503
  :allowed_rule,
504
504
  :authenticator,
505
505
  to: :class
@@ -508,14 +508,19 @@ module Foobara
508
508
  apply_allowed_rule
509
509
  apply_pre_commit_transformers
510
510
  run_command
511
- # TODO: do this within the transaction!!!
511
+ # this gives us primary keys
512
+ flush_transactions
512
513
  transform_outcome
513
514
 
514
515
  outcome
515
516
  end
516
517
 
517
- def requires_authentication?
518
- !!requires_authentication
518
+ def authenticated_user
519
+ request.authenticated_user
520
+ end
521
+
522
+ def authenticated_credential
523
+ request.authenticated_credential
519
524
  end
520
525
 
521
526
  def transform_inputs
@@ -669,6 +674,10 @@ module Foobara
669
674
  outcome.errors
670
675
  end
671
676
 
677
+ def flush_transactions
678
+ request.opened_transactions&.reverse&.each(&:flush!)
679
+ end
680
+
672
681
  def transform_outcome
673
682
  if outcome.success?
674
683
  # can we do this while still in the transaction of the command???
@@ -2,6 +2,21 @@ module Foobara
2
2
  class Entity < DetachedEntity
3
3
  module SensitiveTypeRemovers
4
4
  class Entity < DetachedEntity::SensitiveTypeRemovers::DetachedEntity
5
+ def transform(strict_type_declaration)
6
+ new_type_declaration = super
7
+
8
+ if strict_type_declaration != new_type_declaration
9
+ if new_type_declaration[:type] == :entity
10
+ new_type_declaration[:type] = :detached_entity
11
+
12
+ if new_type_declaration[:model_base_class] == "Foobara::Entity"
13
+ new_type_declaration[:model_base_class] = "Foobara::DetachedEntity"
14
+ end
15
+ end
16
+ end
17
+
18
+ new_type_declaration
19
+ end
5
20
  end
6
21
  end
7
22
  end
@@ -4,22 +4,15 @@ module Foobara
4
4
  class Entity < DetachedEntity::SensitiveValueRemovers::DetachedEntity
5
5
  def transform(record)
6
6
  if record.loaded? || record.created?
7
- sanitized_record = super
8
-
9
- sanitized_record.is_loaded = record.loaded?
10
- sanitized_record.is_persisted = record.persisted?
11
-
12
- sanitized_record
7
+ super
13
8
  elsif record.persisted?
14
9
  # We will assume that we do not need to clean up the primary key itself as
15
10
  # we will assume we don't allow sensitive primary keys for now.
16
- sanitized_record = to_type.target_class.build(record.class.primary_key_attribute => record.primary_key)
17
-
18
- sanitized_record.is_persisted = true
19
- sanitized_record.is_loaded = false
20
- sanitized_record.is_built = false
21
-
22
- sanitized_record
11
+ # We use .new because the target_class should be a detached entity
12
+ to_type.target_class.new(
13
+ { record.class.primary_key_attribute => record.primary_key },
14
+ { mutable: false, skip_validations: true }
15
+ )
23
16
  else
24
17
  # :nocov:
25
18
  raise "Not sure what to do with a record that isn't loaded, created, or persisted"
@@ -28,7 +21,14 @@ module Foobara
28
21
  end
29
22
 
30
23
  def build_method
31
- :build
24
+ if to_type.extends?(:entity)
25
+ # TODO: test this code path
26
+ # :nocov:
27
+ :build
28
+ # :nocov:
29
+ else
30
+ :new
31
+ end
32
32
  end
33
33
  end
34
34
  end
@@ -30,9 +30,10 @@ module Foobara
30
30
  "detached_entity",
31
31
  "entity",
32
32
  "model_attribute_helpers",
33
+ "nested_transactionable",
33
34
  "command",
34
35
  "domain_mapper",
35
- "persistence",
36
+ "persistence", # Feels like this would be loaded before command?
36
37
  "in_memory_crud_driver_minimal",
37
38
  "in_memory_crud_driver",
38
39
  "manifest"
@@ -64,8 +64,9 @@ module Foobara
64
64
 
65
65
  if model_type
66
66
  unless Foobara::TypeDeclarations.declarations_equal?(declaration, model_type.declaration_data)
67
+ type_domain = domain
67
68
  self.model_type = nil
68
- domain.foobara_type_from_declaration(declaration)
69
+ type_domain.foobara_type_from_declaration(declaration)
69
70
  end
70
71
  else
71
72
  domain.foobara_type_from_declaration(declaration)
@@ -13,8 +13,8 @@ module Foobara
13
13
  end
14
14
  end
15
15
 
16
- def always_applicable?
17
- true
16
+ def applicable?(model_instance)
17
+ !model_instance.skip_validations
18
18
  end
19
19
 
20
20
  def process_value(model_instance)
@@ -17,6 +17,8 @@ module Foobara
17
17
 
18
18
  # TODO: consider splitting this up into multiple desugarizers
19
19
  def desugarize(strictish_type_declaration)
20
+ strictish_type_declaration = strictish_type_declaration.dup
21
+
20
22
  if strictish_type_declaration.key?(:model_module)
21
23
  model_module = strictish_type_declaration[:model_module]
22
24
 
@@ -47,7 +49,7 @@ module Foobara
47
49
  end
48
50
  end
49
51
 
50
- strictish_type_declaration[:model_base_class] = model_class.superclass.name
52
+ strictish_type_declaration[:model_base_class] ||= model_class.superclass.name
51
53
 
52
54
  model_class.name
53
55
  elsif klass.is_a?(::String)
@@ -169,10 +169,10 @@ module Foobara
169
169
 
170
170
  abstract
171
171
 
172
- attr_accessor :mutable
172
+ attr_accessor :mutable, :skip_validations
173
173
 
174
174
  def initialize(attributes = nil, options = {})
175
- allowed_options = %i[validate mutable ignore_unexpected_attributes]
175
+ allowed_options = %i[validate mutable ignore_unexpected_attributes skip_validations]
176
176
  invalid_options = options.keys - allowed_options
177
177
 
178
178
  unless invalid_options.empty?
@@ -181,6 +181,8 @@ module Foobara
181
181
  # :nocov:
182
182
  end
183
183
 
184
+ self.skip_validations = options[:skip_validations]
185
+
184
186
  if options[:ignore_unexpected_attributes]
185
187
  Thread.with_inheritable_thread_local_var(:foobara_ignore_unexpected_attributes, true) do
186
188
  initialize(attributes, options.except(:ignore_unexpected_attributes))
@@ -226,7 +228,7 @@ module Foobara
226
228
  mutable
227
229
  end
228
230
 
229
- validate! if validate
231
+ validate! if validate # TODO: test this code path
230
232
  end
231
233
 
232
234
  foobara_delegate :model_name, :valid_attribute_name?, :validate_attribute_name!, to: :class
@@ -26,7 +26,10 @@ module Foobara
26
26
 
27
27
  def load
28
28
  require require_path
29
- Util.require_directory("#{project_path}/src")
29
+ src_dir = "#{project_path}/src"
30
+ if Dir.exist?(src_dir)
31
+ Util.require_directory(src_dir)
32
+ end
30
33
  end
31
34
 
32
35
  def install!
@@ -0,0 +1,95 @@
1
+ module Foobara
2
+ module NestedTransactionable
3
+ include Concern
4
+
5
+ class << self
6
+ def relevant_entity_classes_for_type(type)
7
+ entity_classes = []
8
+ entity_classes += Entity.construct_associations(type).values.map(&:target_class)
9
+
10
+ if type.extends?(BuiltinTypes[:entity])
11
+ entity_classes << type.target_class
12
+ end
13
+
14
+ entity_classes.uniq.each do |entity_class|
15
+ entity_classes += entity_class.deep_associations.values.map(&:target_class)
16
+ end
17
+
18
+ entity_classes.uniq
19
+ end
20
+ end
21
+
22
+ def relevant_entity_classes_for_type(type)
23
+ NestedTransactionable.relevant_entity_classes_for_type(type)
24
+ end
25
+
26
+ def relevant_entity_classes
27
+ # :nocov:
28
+ raise "subclass responsibility"
29
+ # :nocov:
30
+ end
31
+
32
+ def transactions
33
+ @transactions ||= []
34
+ end
35
+
36
+ def opened_transactions
37
+ @opened_transactions ||= []
38
+ end
39
+
40
+ def auto_detect_current_transactions
41
+ classes = relevant_entity_classes
42
+ return if classes.nil? || classes.empty?
43
+
44
+ bases = classes.map(&:entity_base).uniq
45
+
46
+ bases.each do |base|
47
+ tx = base.current_transaction
48
+ transactions << tx if tx
49
+ end
50
+ end
51
+
52
+ def open_transaction
53
+ auto_detect_current_transactions
54
+
55
+ bases_not_needing_transaction = transactions.map(&:entity_base)
56
+
57
+ bases_needing_transaction = relevant_entity_classes.map(&:entity_base).uniq - bases_not_needing_transaction
58
+
59
+ bases_needing_transaction.each do |entity_base|
60
+ transaction = entity_base.transaction
61
+ transaction.open!
62
+ opened_transactions << transaction
63
+ transactions << transaction
64
+ end
65
+ end
66
+
67
+ def rollback_transaction
68
+ opened_transactions.reverse.each do |transaction|
69
+ if transaction.currently_open?
70
+ # Hard to test this because halting and other exceptions rollback the transactions via
71
+ # block form but to be safe keeping this
72
+ # :nocov:
73
+ transaction.rollback!
74
+ # :nocov:
75
+ end
76
+ end
77
+ end
78
+
79
+ def commit_transaction
80
+ opened_transactions.reverse.each(&:commit!)
81
+ end
82
+
83
+ def commit_transaction_if_open
84
+ opened_transactions.reverse.each do |tx|
85
+ if tx.currently_open?
86
+ tx.commit!
87
+ end
88
+ end
89
+ end
90
+
91
+ def use_transaction(&)
92
+ Persistence::EntityBase.using_transactions(transactions, &)
93
+ end
94
+ end
95
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.115
4
+ version: 0.0.116
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-01 00:00:00.000000000 Z
10
+ date: 2025-05-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -179,7 +179,6 @@ files:
179
179
  - projects/command/src/command_pattern_implementation/concerns/subcommands.rb
180
180
  - projects/command/src/command_pattern_implementation/concerns/transactions.rb
181
181
  - projects/command/src/state_machine.rb
182
- - projects/command/src/transformed_command.rb
183
182
  - projects/command_connectors/lib/foobara/command_connectors.rb
184
183
  - projects/command_connectors/src/authenticator.rb
185
184
  - projects/command_connectors/src/authenticator_selector.rb
@@ -216,6 +215,7 @@ files:
216
215
  - projects/command_connectors/src/serializers/record_store_serializer.rb
217
216
  - projects/command_connectors/src/serializers/success_serializer.rb
218
217
  - projects/command_connectors/src/serializers/yaml_serializer.rb
218
+ - projects/command_connectors/src/transformed_command.rb
219
219
  - projects/command_connectors/src/transformers/auth_errors_transformer.rb
220
220
  - projects/command_connectors/src/transformers/load_aggregates_pre_commit_transformer.rb
221
221
  - projects/command_connectors/src/transformers/load_delegated_attributes_entities_pre_commit_transformer.rb
@@ -369,6 +369,7 @@ files:
369
369
  - projects/namespace/src/prefixless_registry.rb
370
370
  - projects/namespace/src/scoped.rb
371
371
  - projects/namespace/src/unambiguous_registry.rb
372
+ - projects/nested_transactionable/lib/foobara/nested_transactionable.rb
372
373
  - projects/persistence/lib/foobara/persistence.rb
373
374
  - projects/persistence/src/entity_attributes_crud_driver.rb
374
375
  - projects/persistence/src/entity_base.rb
@@ -492,6 +493,7 @@ require_paths:
492
493
  - "./projects/model_attribute_helpers/lib"
493
494
  - "./projects/monorepo/lib"
494
495
  - "./projects/namespace/lib"
496
+ - "./projects/nested_transactionable/lib"
495
497
  - "./projects/persistence/lib"
496
498
  - "./projects/state_machine/lib"
497
499
  - "./projects/type_declarations/lib"