
Work with records in their original system
Some information already has a home: product prices in an ERP, invoices in an accounting system or a catalogue maintained by another team. Your application may need to show those records, and sometimes let people correct them, without taking responsibility for a second copy.
A virtual resource reads from the external system when the application asks for it. Wildo maps the remote fields into the resource’s declared shape, so the information appears in the application’s screens, API and permissions like any other record. When you open creates, updates or deletes, each change is sent to that system once, under the application’s own service account, and recorded.
The external system keeps the only copy. This is a useful fit for information people consult or correct in passing, especially when keeping a local copy would create another synchronization job.
Example — Consult and correct the company’s service catalogue
Wonder Todos reads billable services from the company’s Odoo catalogue. A manager sees the service name, price and unit in the application and can correct a price there; the change is written to Odoo, which remains the catalogue. Before writing, the application checks that nobody changed the service in Odoo since it was read, so most conflicting edits are refused rather than silently overwritten.
For engineers
Declare the source, its field mapping and its writes
The following excerpt from Wonder Todos’ billable-services.resources-config.ts selects the HTTP API adapter and binds it to the Odoo instance the application declares under systemsOfRecord. Source comments are omitted; the resource’s operation configurations follow this excerpt. HttpApiWriteVerb and HttpApiWriteConcurrencyMode come from @wildo-ai/saas-models/external-data.
persistenceAdapter: PersistenceAdapter.HTTP_API,
httpApiBinding: {
dialect: HttpApiTransportDialect.ODOO_JSONRPC,
system: 'odoo',
entityRef: 'product.template',
keyFields: [{ localField: 'odooProductId', remoteField: 'id', codec: HttpApiKeyComponentCodec.INTEGER }],
fieldMappings: [
{ localField: 'listPrice', remoteField: 'list_price' },
{ localField: 'unitOfMeasure', remoteField: 'uom_name' },
{ localField: 'lastUpdatedAt', remoteField: 'write_date' },
],
subjectRights: { posture: SystemOfRecordSubjectRightsPosture.NO_SUBJECT_DATA },
writableVerbs: [HttpApiWriteVerb.CREATE, HttpApiWriteVerb.UPDATE, HttpApiWriteVerb.DELETE],
writeConcurrency: { mode: HttpApiWriteConcurrencyMode.VERSION_CHECKED, versionField: 'lastUpdatedAt' },
},
coreOperations: [
CoreResourceOperation.READ,
CoreResourceOperation.LIST,
CoreResourceOperation.CREATE,
CoreResourceOperation.UPDATE,
CoreResourceOperation.DELETE,
],
system names the declared instance, and entityRef the remote model. keyFields defines how a remote row is identified: one integer component, so the row’s id is Odoo’s own. fieldMappings translates names where the application and remote system differ: listPrice reads list_price, while fields with matching names keep them.
writableVerbs opens the writes, and each opened verb must have its operation: the configuration factory refuses either without the other, and it derives a required idempotency key on every write. writeConcurrency states how an update treats a record changed since it was read. VERSION_CHECKED compares the write_date the caller read, mapped to lastUpdatedAt, with Odoo’s own just before writing, and refuses a stale update.
Declare only the queries the remote dialect can execute
The same Odoo resource’s LIST variant declares:
sortFields: ['name', 'listPrice', 'odooProductId'],
listPrice maps to the remote list_price field. A display value without an orderable remote counterpart should not be advertised as sortable.
| Dialect | LIST/SEARCH sort declaration |
|---|---|
ODOO_JSONRPC, ODATA or ODATA_V2 | Declare a nonempty set of sortable fields; startup refuses an absent declaration. |
REST_JSON | Do not declare sort fields: this adapter path does not translate them, so order belongs in the declared statement. |
The application also declares how callers address the resource. Wonder Todos routes its catalogue under the organization:
createResourcesRelationship(
CoreResourceType.ORGANIZATIONS, TasksManager_ResourceType.BILLABLE_SERVICES,
ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
{
nature: RelationshipNature.REFERENCE,
isPrimaryScope: true,
accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.REQUIRES_CONTEXT,
contextPolicy: { enabled: false },
}
),
This relationship mounts the collection under /organizations/{org}/billable-services, which is what lets the access check name an organization; the operation roles grant organization managers access. It invents no organization column in Odoo. The requested organization is compared with the organization that owns the system of record, and any other organization receives an empty list.
Keep source access and application access connected
system resolves the instance’s configured address and service credential rather than embedding credentials in the resource; each environment supplies them. The repository uses the shared HttpApiReadClient for transport and dialect-specific request construction. An ordinary authorized read becomes the corresponding remote request, whose result is projected into the local resource shape; an authorized write becomes one remote create, write or delete.
Rows belong to the organization that owns the application: each is stamped with that organization’s id, and the ordinary per-row check decides who sees it. When the instance is declared partitioned — Odoo, by company — rows are further confined to the organization units an operator assigned each company to, and a member confined to one company can create and change only that company’s records. See confining a system of record by company. The subject-rights posture describes the bound data and is a separate responsibility from transport configuration.
Choose virtual data when the source should keep the only copy
A virtual resource has no local record to enrich with extra columns, attachments or a retrieval index. Requests depend on the external system’s response time and availability, and the source’s dialect determines which queries can be translated.
For this catalogue, those properties fit the product: prices are maintained in Odoo, and a correction made in the application lands there. When people need local annotations, attached files or ordinary local relationships around imported records, use a copy-in pipeline instead.
The read-through resource and the pipeline share remote-request machinery, while keeping their different storage and ownership choices explicit.