Status: Not started
Priority: P1 (blocks integration with any API whose field-naming convention differs from the local schema — a common scenario)
Area: C++ (core) — cpp/sync/SyncOperationApplier.cpp, cpp/sync/SyncPushPhase.cpp; TypeScript — src/types/schemas/ISchemaDefinition.ts
Required skills: cpp, api-design
Description
Today the sync engine applies API data straight into SQLite with no field-name translation:
// cpp/sync/SyncPullPhase.cpp
guard.applyWithBypass([&] {
ApplyStats pageStats = applier.apply(entity, rows); // `rows` = raw API JSON, applied as-is
...
});
If the API uses a different naming convention than the local schema (e.g. server sends CUSTOMER_NAME in upper snake case, while the TS schema declares customerName), there is currently no translation point between the two — the row is written using whatever field names the API sent, which either breaks silently or fails if the column doesn't exist on the local table.
The same gap exists in the opposite direction: the push payload for insert/update is built by the SQL trigger (json_object(...) in MigrationEngine.cpp) using the local schema's column names — so if the API expects CUSTOMER_NAME, the push will send customerName instead, and the API will reject or silently ignore the field.
Why this matters
ISchemaDefinition (TS) and the native engine have no concept of a field mapper today. Since the project's core rule is that sync runs 100% natively (zero JS in background — see CLAUDE.md), this mapping cannot be an arbitrary JS callback: it needs to be declarative, interpreted natively, in the same spirit as the other declarative contracts already in place ($ref, JsonPath, RequestExpression).
Possible approaches (not decided)
- Add a declarative per-schema field, e.g.
sync.fieldMap: Record<string, string> (key = API field name, value = local column name), interpreted in both directions:
- Pull:
SyncOperationApplier::apply translates incoming JSON keys before writing to SQLite.
- Push:
SyncPushPhase (or whichever point serializes sync_queue.payload) translates local column names back to the API's expected names before sending.
- Validate at registration time (
Database.register) that every fieldMap entry corresponds to a real schema column, to fail fast instead of silently.
- Default behavior for unmapped fields: pass through unchanged — only fields that actually diverge need an entry.
Suggested acceptance criteria (not finalized)
Related
- CLAUDE.md — "Expression Interpreter → interprets declarative contracts ($ref, JsonPath)" (same pattern to follow here).
Status: Not started
Priority: P1 (blocks integration with any API whose field-naming convention differs from the local schema — a common scenario)
Area: C++ (core) —
cpp/sync/SyncOperationApplier.cpp,cpp/sync/SyncPushPhase.cpp; TypeScript —src/types/schemas/ISchemaDefinition.tsRequired skills:
cpp,api-designDescription
Today the sync engine applies API data straight into SQLite with no field-name translation:
If the API uses a different naming convention than the local schema (e.g. server sends
CUSTOMER_NAMEin upper snake case, while the TS schema declarescustomerName), there is currently no translation point between the two — the row is written using whatever field names the API sent, which either breaks silently or fails if the column doesn't exist on the local table.The same gap exists in the opposite direction: the push payload for
insert/updateis built by the SQL trigger (json_object(...)inMigrationEngine.cpp) using the local schema's column names — so if the API expectsCUSTOMER_NAME, the push will sendcustomerNameinstead, and the API will reject or silently ignore the field.Why this matters
ISchemaDefinition(TS) and the native engine have no concept of a field mapper today. Since the project's core rule is that sync runs 100% natively (zero JS in background — see CLAUDE.md), this mapping cannot be an arbitrary JS callback: it needs to be declarative, interpreted natively, in the same spirit as the other declarative contracts already in place ($ref,JsonPath,RequestExpression).Possible approaches (not decided)
sync.fieldMap: Record<string, string>(key = API field name, value = local column name), interpreted in both directions:SyncOperationApplier::applytranslates incoming JSON keys before writing to SQLite.SyncPushPhase(or whichever point serializessync_queue.payload) translates local column names back to the API's expected names before sending.Database.register) that everyfieldMapentry corresponds to a real schema column, to fail fast instead of silently.Suggested acceptance criteria (not finalized)
fieldMap(TS type onISchemaDefinition/ISyncDefinition).SyncOperationAppliertranslates incoming API fields (pull) viafieldMapbefore applying to SQLite.fieldMapentry keep passing through unchanged (preserve current behavior).triggerSynccycle (pull and push) with a schema usingfieldMap, through the real JSI bridge — no mocks.Related