hecks 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/postgres/outbox.rb +7 -4
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +19 -0
- data/lib/hecks/adapters/driven/postgres.rb +20 -17
- data/lib/hecks/adapters/driven/sqlite/projection.rb +5 -2
- data/lib/hecks/bluebook/assembly/marks.rb +13 -3
- data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +10 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +131 -16
- data/lib/hecks/ports/persistence/append_only.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +54 -5
- data/lib/hecks/projections/glossary.rb +374 -0
- data/lib/hecks/projections.rb +1 -0
- data/lib/hecks/projector/cli_projector.rb +29 -13
- data/lib/hecks/projector/exporter.rb +24 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
- data/lib/hecks/query_specification/common/order_by.rb +9 -2
- data/lib/hecks/query_specification/common/where_clause.rb +15 -2
- data/lib/hecks/runtime/interpreting.rb +16 -7
- data/lib/hecks/runtime/read_model_interpreter.rb +5 -4
- data/lib/hecks/runtime/value/coercion.rb +64 -1
- data/lib/hecks/version.rb +1 -1
- metadata +3 -1
|
@@ -77,15 +77,24 @@ module Hecks
|
|
|
77
77
|
# lost-update gap itself, via `step_save`'s CAS + `#call`'s own
|
|
78
78
|
# `StaleWrite` retry loop — an extra in-process lock here would be
|
|
79
79
|
# pointless overhead, not incorrect, so it's skipped for clarity.
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# `
|
|
80
|
+
# A repository that declares `:cross_process_lock` (PostgresEra —
|
|
81
|
+
# ADR 0036) holds a REAL Postgres advisory lock for the whole
|
|
82
|
+
# dispatch order instead: unlike Heki/Memory (confirmed
|
|
83
|
+
# process-local, never a second process writing the same store),
|
|
84
|
+
# PostgresEra's own tables can be dispatched against concurrently
|
|
85
|
+
# by `rust/host` from a separate OS process, and an in-process
|
|
86
|
+
# `Mutex` is invisible to that. Every OTHER repository gets the
|
|
87
|
+
# striped `Mutex` below, held for the WHOLE dispatch-order run, so
|
|
88
|
+
# a second thread's own hydrate can't start until the first
|
|
89
|
+
# thread's save has landed. `lock_key_id` is best-effort
|
|
90
|
+
# (`Identity.best_effort`) — `nil` still locks correctly, just
|
|
91
|
+
# coarser (by aggregate type).
|
|
86
92
|
def run_dispatch_order_with_isolation(order, ctx, lock_key_id:)
|
|
87
|
-
|
|
93
|
+
capabilities = ctx.repository.capabilities
|
|
94
|
+
if capabilities.include?(:optimistic_concurrency)
|
|
88
95
|
run_dispatch_order(order, ctx)
|
|
96
|
+
elsif capabilities.include?(:cross_process_lock)
|
|
97
|
+
ctx.repository.with_write_lock { run_dispatch_order(order, ctx) }
|
|
89
98
|
else
|
|
90
99
|
AggregateLock.for(ctx.domain, ctx.aggregate, lock_key_id).synchronize { run_dispatch_order(order, ctx) }
|
|
91
100
|
end
|
|
@@ -50,10 +50,11 @@ module Hecks
|
|
|
50
50
|
refuse_object_reference(model, args) unless rootless
|
|
51
51
|
reference_id = reference(args.fetch(model.reference_name)) unless rootless
|
|
52
52
|
# Computed off the ORIGINAL model, before TenantScope wraps it — the
|
|
53
|
-
# "which head do options apply to" question is about what the
|
|
53
|
+
# "which head(s) do options apply to" question is about what the
|
|
54
54
|
# bluebook author declared, not about the synthetic tenant clause
|
|
55
|
-
# the wrapper adds underneath.
|
|
56
|
-
eligible
|
|
55
|
+
# the wrapper adds underneath. Plural (ADR 0055) — `on:` lets more
|
|
56
|
+
# than one many-side head be eligible at once.
|
|
57
|
+
eligible = model.filtered_head_names
|
|
57
58
|
model = TenantScope.apply(model, args)
|
|
58
59
|
# A ROOTLESS, `group_by`-declared, or `count`/`median`-declared
|
|
59
60
|
# model skips the SQLite native escape hatch entirely (there is
|
|
@@ -119,7 +120,7 @@ module Hecks
|
|
|
119
120
|
end
|
|
120
121
|
end
|
|
121
122
|
end
|
|
122
|
-
rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as]
|
|
123
|
+
rows = Ports::Query::InMemory.execute(rows, model.options_for(head[:as]), args) if eligible.include?(head[:as])
|
|
123
124
|
projected << { aggregate: head[:aggregate], rows: rows }
|
|
124
125
|
rows_by_as[head[:as]] = head[:many] ? rows : rows.first
|
|
125
126
|
end
|
|
@@ -395,9 +395,27 @@ module Hecks
|
|
|
395
395
|
|
|
396
396
|
# Frozen through: a list read back out of the store is an answer,
|
|
397
397
|
# not a handle on what is stored.
|
|
398
|
+
#
|
|
399
|
+
# ADR 0047 — this used to bail (`return value unless entity`) the
|
|
400
|
+
# moment `attribute.type` named a value object rather than an
|
|
401
|
+
# entity, handing back the raw, un-hydrated argument untouched.
|
|
402
|
+
# A `sets :field` mutation sourced from a whole-array argument (as
|
|
403
|
+
# opposed to element-by-element `append:`) went straight through
|
|
404
|
+
# `for_attribute`'s `:list` branch, so `Banking::CardPayment.
|
|
405
|
+
# Authorize`'s own `sets :tags` (`list_of(Tag)`) stored plain
|
|
406
|
+
# Ruby Hashes as its `tags` elements forever — never a real
|
|
407
|
+
# `Value`, never through `Tag`'s own `pattern:`/`invariant`
|
|
408
|
+
# checks. `remove:`'s `==` comparison (a real `Value` against a
|
|
409
|
+
# raw `Hash`) then always failed, since `Hash#==` refuses anything
|
|
410
|
+
# that isn't itself a compatible Hash — the bug ADR 0047 traces in
|
|
411
|
+
# full. Delegating to `hydrate_value_object_list` below closes
|
|
412
|
+
# that gap the same way the ENTITY branch already worked: build a
|
|
413
|
+
# real, validated `Value` per element, reusing `for_attribute`'s
|
|
414
|
+
# own composite-construction path rather than inventing a second
|
|
415
|
+
# one.
|
|
398
416
|
def hydrate_entity_list(aggregate, attribute, value)
|
|
399
417
|
entity = find_entity(aggregate, attribute.type.to_s)
|
|
400
|
-
return value unless entity
|
|
418
|
+
return hydrate_value_object_list(aggregate, attribute, value) unless entity
|
|
401
419
|
|
|
402
420
|
hydrated = Array(value).map do |element|
|
|
403
421
|
next element unless element.is_a?(Hash)
|
|
@@ -411,6 +429,51 @@ module Hecks
|
|
|
411
429
|
Freezer.deep(hydrated)
|
|
412
430
|
end
|
|
413
431
|
|
|
432
|
+
# The value-object sibling of the entity branch above: an element
|
|
433
|
+
# already shaped like the target `Value` (or a `Hash`/scalar that
|
|
434
|
+
# `fields_for` can still open) is rebuilt through the SAME `build`
|
|
435
|
+
# a scalar composite attribute already uses (`for_attribute`'s own
|
|
436
|
+
# `coerced = ... build(value_object, fields_for(...), aggregate)`
|
|
437
|
+
# line) — same defaults, same `pattern:`/`admits:`/invariant
|
|
438
|
+
# checks, same `trusting_stored_state?` bypass on a trusted load.
|
|
439
|
+
# `attribute.type` naming neither an entity nor a value object
|
|
440
|
+
# (a `list_of(String)`, say) has no shape to rebuild into, so the
|
|
441
|
+
# element passes through unchanged, exactly as the entity branch's
|
|
442
|
+
# own non-Hash elements do.
|
|
443
|
+
#
|
|
444
|
+
# NOT `Array(value).map` (unlike the entity branch above) — `value`
|
|
445
|
+
# here is not always genuinely list-shaped. `MutationApplier#
|
|
446
|
+
# removed`'s own `Value.for_attribute(aggregate, attribute, value)`
|
|
447
|
+
# call (`attribute` = the LIST attribute, `mutation.target`; `value`
|
|
448
|
+
# = the single REMOVE-target argument, already a real `Value` by
|
|
449
|
+
# the time it gets here) reuses this exact branch — for `remove:`,
|
|
450
|
+
# not for a whole-list `sets`. `Array(a_real_Value)` alone would be
|
|
451
|
+
# harmless (`Value` defines neither `to_a` nor `to_ary`, so Kernel
|
|
452
|
+
# wraps it `[value]`), but `Array(a_Hash)` is NOT harmless: Ruby's
|
|
453
|
+
# `Array()` opens a bare Hash into its own `[[k, v], ...]` pairs,
|
|
454
|
+
# not `[hash]` — silently shredding a single-element Hash-shaped
|
|
455
|
+
# target into garbage instead of hydrating it. Branching on
|
|
456
|
+
# `value.is_a?(Array)` up front (true only for a genuine whole-list
|
|
457
|
+
# `sets`/hydrate load) keeps the single-target shape a single
|
|
458
|
+
# target, hydrated the same way, never listified.
|
|
459
|
+
def hydrate_value_object_list(aggregate, attribute, value)
|
|
460
|
+
return value unless aggregate.respond_to?(:value_object)
|
|
461
|
+
|
|
462
|
+
value_object = value_object_for(aggregate, attribute.type)
|
|
463
|
+
return value unless value_object
|
|
464
|
+
|
|
465
|
+
return hydrate_value_object_element(aggregate, attribute, value_object, value) unless value.is_a?(Array)
|
|
466
|
+
|
|
467
|
+
hydrated = value.map { |element| hydrate_value_object_element(aggregate, attribute, value_object, element) }
|
|
468
|
+
Freezer.deep(hydrated)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def hydrate_value_object_element(aggregate, attribute, value_object, element)
|
|
472
|
+
return element if element.is_a?(self) && element.type_name == value_object.hecks_name
|
|
473
|
+
|
|
474
|
+
build(value_object, fields_for(value_object, attribute.name, element), aggregate)
|
|
475
|
+
end
|
|
476
|
+
|
|
414
477
|
# `Value.identifier` used to live here: hand it a one-field value object
|
|
415
478
|
# and it opened it, so `identified_by :number` could pass for an identity
|
|
416
479
|
# and the runtime would guess which field was meant. THAT GUESS IS GONE.
|
data/lib/hecks/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/hecks/adapters/driven/postgres.rb
|
|
69
69
|
- lib/hecks/adapters/driven/postgres/codec.rb
|
|
70
70
|
- lib/hecks/adapters/driven/postgres/outbox.rb
|
|
71
|
+
- lib/hecks/adapters/driven/postgres/reconnect.rb
|
|
71
72
|
- lib/hecks/adapters/driven/postgres/schema_builder.rb
|
|
72
73
|
- lib/hecks/adapters/driven/postgres_era.adapter
|
|
73
74
|
- lib/hecks/adapters/driven/prism.adapter
|
|
@@ -343,6 +344,7 @@ files:
|
|
|
343
344
|
- lib/hecks/ports/query/ordering.rb
|
|
344
345
|
- lib/hecks/projections.rb
|
|
345
346
|
- lib/hecks/projections/diagrams.rb
|
|
347
|
+
- lib/hecks/projections/glossary.rb
|
|
346
348
|
- lib/hecks/projections/ir.rb
|
|
347
349
|
- lib/hecks/projections/model.rb
|
|
348
350
|
- lib/hecks/projections/model/deviations.rb
|