5.3 KiB
1C Adapter: universal write dispatcher and typed handlers
Decision
The public write contract stays universal and name-first:
code.write / metadata.write
-> resolve public object and extension layer
-> plan and gates
-> select one typed handler
-> prepare saved state internally when required
-> apply, SQL-readback, rollback evidence
The caller never selects an SQL table, saved file, stream, payload codec, or
handler. If no handler has a proven capability for the requested object type
and operation, the dispatcher returns unsupported_write_target with a public
explanation. It must not fall back to a generic byte rewrite.
Current state
The behaviour is already logically separated, but is physically concentrated
in plugins/1c/connector/adapter_1c_server.py (about 3.5 MB). The main
dispatcher is metadata_write and currently branches to:
| Public target | Existing internal writer |
|---|---|
| BSL module | metadata_module_write_apply |
| BSL embedded in a managed form | form_embedded_module_handler_write_apply |
| Form element/property | metadata_form_element_write_apply |
| Form command/button caption | metadata_form_command_button_write |
| Scalar object/member property | metadata_object_property_write |
| Add object member | metadata_object_member_add |
| Scheduled-job schedule | metadata_scheduled_job_schedule_write |
This is a suitable functional base. The problem is coupling: routing, saved-state preparation, result shaping, codecs, SQL writes, and HTTP/RPC dispatch live in one module, so a change in one type is too likely to affect another.
Target module layout
plugins/1c/connector/
adapter_1c_server.py # HTTP, RPC registration, composition root only
write/
contracts.py # WriteIntent, WritePlan, WriteResult, capability errors
dispatcher.py # universal metadata.write dispatch; no SQL codecs
gates.py # layer, repository, optimistic-hash and mode gates
saved_state.py # Config→ConfigSave / ConfigCAS→ConfigCASSave prepare + receipt rollback
registry.py # handler registration and deterministic selection
handlers/
module.py
embedded_form_module.py
form_element.py
form_command.py
object_property.py
object_member.py
scheduled_job.py
unsupported.py
storage/
sql_saved_state.py # transactions, guarded row copy, backup/readback
extension_routes.py # active-to-saved route and cache refresh
parser/ remains the place for pure payload decoding/encoding. A handler may
use a parser codec only where its round-trip proof exists; SQL access is
provided through a narrow context rather than imported globals.
Handler contract
Each handler implements the same four operations:
can_handle(intent, evidence) -> supported | unsupported | ambiguous.plan(intent, evidence) -> WritePlanwith exact guards and no mutation.apply(plan, context) -> WriteResultonly after shared gates succeed.rollback(result, context)when the handler created reversible state.
The dispatcher selects exactly one handler. Zero handlers yields
unsupported_write_target; multiple handlers yield ambiguous_write_handler.
Handlers never select another extension layer after dispatch.
metadata.write.capabilities also returns registered_handlers. This makes
the runtime registry visible beside the broader capability matrix and prevents
an API claim from silently drifting away from the installed handlers.
Invariants owned centrally
- public name/ref and extension scope resolve before handler selection;
- only saved layers are writable;
- saved-state preparation is internal and idempotent;
- optimistic hash, audit event, backup/receipt, SQL readback and rollback policy are common infrastructure;
- SQL readback is not described as Configurator activation;
- low-level storage fields are redacted from name-first responses.
Safe migration order
- Completed: add storage-free
write/contracts.py, typed handler declarations underwrite/handlers/, andwrite/registry.py; connectmetadata.writeto the registry while delegating to existing writers unchanged. The registry has a deny-by-default result for unknown target kinds. - In progress: add
write/context.py; the scheduled-job route now enters its typed handler through this explicit context. The handler still delegates to the single existing implementation until its body moves in one change. - Move
saved_state.pyandstorage/sql_saved_state.pyfirst. The recently proven hash-keyed extension prepare/rollback smoke is its acceptance test. - Extract the least coupled handlers: scheduled job, object property, object member.
- Extract form element and form command handlers.
- Extract module and embedded-form-module handlers last; retain their exact payload codec and paired-write tests.
- Reduce
metadata_writeto validation, plan/gate orchestration and one registry call. Keep the old public API names and response schema intact.
Do not split by copying code into parallel paths. Each extraction must move one
authoritative implementation, keep the existing tests green, and add one
handler-level plan → apply_and_rollback → readback test in upo_test when a
matching fixture exists.