122 lines
5.3 KiB
Markdown
122 lines
5.3 KiB
Markdown
# 1C Adapter: universal write dispatcher and typed handlers
|
|
|
|
## Decision
|
|
|
|
The public write contract stays universal and name-first:
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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:
|
|
|
|
1. `can_handle(intent, evidence) -> supported | unsupported | ambiguous`.
|
|
2. `plan(intent, evidence) -> WritePlan` with exact guards and no mutation.
|
|
3. `apply(plan, context) -> WriteResult` only after shared gates succeed.
|
|
4. `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
|
|
|
|
1. **Completed:** add storage-free `write/contracts.py`, typed handler
|
|
declarations under `write/handlers/`, and `write/registry.py`; connect
|
|
`metadata.write` to the registry while delegating to existing writers
|
|
unchanged. The registry has a deny-by-default result for unknown target
|
|
kinds.
|
|
2. **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.
|
|
3. Move `saved_state.py` and `storage/sql_saved_state.py` first. The recently
|
|
proven hash-keyed extension prepare/rollback smoke is its acceptance test.
|
|
4. Extract the least coupled handlers: scheduled job, object property, object
|
|
member.
|
|
5. Extract form element and form command handlers.
|
|
6. Extract module and embedded-form-module handlers last; retain their exact
|
|
payload codec and paired-write tests.
|
|
7. Reduce `metadata_write` to 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.
|