← Files UniformARCHIVED FILE
skills/uniform-mesh/references/data-connector.md
10.9 KB · Oct 3, 2026 · 06:34 UTC
# Data connector
The most common Mesh use case: connect an external system (CMS, commerce, search API) so
authors can browse and select records from it inside Uniform. A data connector has three
editor locations working together:
- **Data source editor** — configure the connection (base URL, credentials). One per
connector.
- **Data type editor** — define a request template (method, path, parameters, dynamic
variables) for a kind of query. One per archetype.
- **Data resource editor** — the author-facing picker that runs the query and selects
specific record(s). One per archetype.
And one optional fourth:
- **Data resource selector** — customises how an author picks a value *inside* the fetched
JSON, replacing Uniform's default JSON tree viewer. One per archetype.
## Manifest
A data connector is declared as a location: a **data source editor** for the connection,
plus one or more **archetypes**, each with its own **data type editor**, **data resource
(picker) editor**, and optional **selector**. Validate against the schema (see
`manifest.md`) — it is authoritative if these keys ever move:
```json
{
"locations": {
"dataConnectors": [
{
"type": "product-catalog",
"displayName": "Product catalog",
"dataSourceEditorUrl": "/data-source",
"dataArchetypes": {
"product-by-id": {
"displayName": "Product by id",
"typeEditorUrl": "/data-type",
"dataEditorUrl": "/data-resource",
"dataResourceSelectorUrl": "/data-selector"
}
}
}
]
}
}
```
Behavior worth knowing:
- Each connector has a globally unique `type`.
- A connector can opt into a preview/unpublished data variant — it surfaces as
`metadata.enableUnpublishedMode` in the data source editor.
- Omit any editor URL and Uniform renders its built-in HTTP editor for that location.
## Data source editor — `useMeshLocation<'dataSource'>()`
Store the connection and **secrets** here. Header/parameter values and `custom` are
encrypted at rest; `customPublic` is plaintext and readable by the data type / data
resource editors. Never put credentials in `settings` or in a data type value.
```tsx
import { DataSourceLocationValue, useMeshLocation, ValidationResult } from '@uniformdev/mesh-sdk-react';
import { Input, VerticalRhythm } from '@uniformdev/design-system';
export default function DataConnectionEditor() {
const { value, setValue, metadata } = useMeshLocation<'dataSource'>();
const custom = value.custom ?? {};
// read current creds from value.custom, edit in local state, then:
const save = (spaceId: string, token: string) =>
setValue(() => {
const newValue: DataSourceLocationValue = {
baseUrl: `https://api.example.com/spaces/${spaceId}`,
headers: [{ key: 'Content-Type', value: 'application/json' }],
parameters: [{ key: 'access_token', value: token }], // encrypted
custom: { spaceId, token }, // encrypted, editor-only
customPublic: { spaceId }, // plaintext, other editors can read
...(metadata.enableUnpublishedMode
? { variants: { unpublished: {
baseUrl: `https://preview.example.com/spaces/${spaceId}`,
headers: [{ key: 'Content-Type', value: 'application/json' }],
parameters: [{ key: 'access_token', value: token }],
} } }
: {}),
};
const options: ValidationResult = spaceId && token
? { isValid: true }
: { isValid: false, validationMessage: 'Space ID and token are required' };
return { newValue, options };
});
return (
<VerticalRhythm gap="base">
<Input label="Space ID" value={String(custom.spaceId ?? '')} onChange={/* … */} />
<Input label="Token" type="password" value={String(custom.token ?? '')} onChange={/* … */} />
</VerticalRhythm>
);
}
```
## Data type editor — `useMeshLocation<'dataType'>()`
Define the request template for this archetype: `method`, `path` (leading slash, with
`${variable}` placeholders), `parameters`, and `variables` (dynamic-input definitions).
Non-secret config goes in `custom`. `getDataResource` is available to fetch options (e.g.
list content types) using the parent connection.
```tsx
import { DataTypeLocationValue, useMeshLocation, LoadingOverlay } from '@uniformdev/mesh-sdk-react';
export default function TypeEditor() {
const { setValue, getDataResource } = useMeshLocation<'dataType'>();
// optionally call getDataResource(...) to populate selectable options for the editor UI
const persist = (allowedTypes: string[]) =>
setValue((prev: DataTypeLocationValue) => ({
newValue: {
...prev,
method: 'GET',
path: '/entries',
variables: { entryIds: { default: '', type: 'text', displayName: 'Entry Ids' } },
parameters: [
{ key: 'sys.id[in]', value: '${entryIds}' },
{ key: 'include', value: '1', omitIfEmpty: true },
],
custom: { allowedTypes },
},
}));
// …
}
```
## Data resource editor — dynamic inputs + result selection
The picker authors use. `useMeshLocation<'dataResource'>()` gives `value`, `setValue`,
`getDataResource`, and `metadata` (with `dataType.custom`, `dataSource.baseUrl`,
`dynamicInputs`, `archetype`). Keep the page thin and wrap the search UI in
`DataResourceDynamicInputProvider` + `ObjectSearchProvider`.
```tsx
import {
useMeshLocation, DataResourceDynamicInputProvider, ObjectSearchProvider,
ObjectSearchContainer, ObjectSearchFilter, ObjectSearchResultList, useObjectSearchContext,
GetDataResourceMessage,
} from '@uniformdev/mesh-sdk-react';
import { useAsync } from 'react-use';
type Entry = { id: string; title: string };
function Search({ getDataResource, onSelect, multiSelect }: {
getDataResource: <T>(m: GetDataResourceMessage) => Promise<T>;
onSelect: (ids: string[]) => void;
multiSelect?: boolean;
}) {
const { boundQuery, onSetList, selectedListItems } = useObjectSearchContext<unknown, Entry>();
const { value: items = [], loading } = useAsync(async () => {
const { keyword = '' } = boundQuery as { keyword?: string };
const res = await getDataResource<{ items: { sys: { id: string }; fields: { title: string } }[] }>({
method: 'GET', path: '/entries',
parameters: [{ key: 'query', value: keyword, omitIfEmpty: true }, { key: 'limit', value: '25' }],
});
return res.items.map((i) => ({ id: i.sys.id, title: i.fields.title }));
}, [boundQuery]);
useEffect(() => { if (!loading) onSetList({ items }); }, [items, loading, onSetList]);
useEffect(() => { onSelect(selectedListItems.map((i) => i.id)); }, [selectedListItems]);
return (
<ObjectSearchContainer label="Select entry" enableDynamicInputToResultId searchFilters={<ObjectSearchFilter />}>
<ObjectSearchResultList<Entry> whenNothingSelected="Nothing selected yet"
renderResultComponent={(v) => <div key={v.id}>{v.title}</div>} />
</ObjectSearchContainer>
);
}
export default function DataEditor() {
const { value, setValue, metadata, getDataResource } = useMeshLocation<'dataResource'>();
return (
<DataResourceDynamicInputProvider>
<ObjectSearchProvider isMulti={metadata.archetype === 'multipleEntry'}>
<Search getDataResource={getDataResource} multiSelect={metadata.archetype === 'multipleEntry'}
onSelect={(ids) => setValue(() => ({ newValue: { entryIds: ids.join(',') } }))} />
</ObjectSearchProvider>
</DataResourceDynamicInputProvider>
);
}
```
For per-field dynamic inputs (binding a field to a `${variable}` or a plain value), use
`InputVariables` with an `inputWhenNoVariables` fallback. `EntrySearch` is an older,
simpler all-in-one alternative to the `ObjectSearch*` family — prefer `ObjectSearch*` for
new work.
## Optional: data resource selector — `useMeshLocation<'dataResourceSelector'>()`
> ⚠️ The SDK tags this location `@deprecated` to mean **experimental — subject to change
> without notice**, not "being removed". Treat the
> shape below as less stable than the other locations.
Once an author has picked a record, they still have to point at a *value inside it* to bind a
component parameter — by default a raw JSON tree viewer. This location replaces that viewer
with domain-specific UI: a grid of product images, formatted prices, whatever makes the choice
obvious to a non-technical author.
Declared per archetype as `dataResourceSelectorUrl` (see the manifest above).
- **`value`** is a **JSON pointer string** into the resolved resource — `/title`,
`/images/0`, `/moves/0/move/name`.
- **`setValue`** takes the pointer, not the data: `setValue(() => ({ newValue: '/images/0' }))`.
- **`metadata`** carries `dataResourceValue` (the resolved JSON for the current resource),
`dataResourceName`, `dataTypeId`, `archetype` (useful when one selector serves several
archetypes), and `allowedTypes` — the bindable types the target parameter accepts, so you can
offer only pointers that fit. Canvas editor context comes along too.
- Also available, and easy to miss: **`getDataResource`** to fetch from the connector using the
current data source, **`editorState`** for inspecting or mutating the surrounding
composition/entry tree (see [editor-state.md](editor-state.md) — here it can be `undefined`,
so guard it), and **`isReadOnly`**.
```tsx
'use client'; // App Router only
import { useMeshLocation } from '@uniformdev/mesh-sdk-react';
import { ScrollableList, ScrollableListItem } from '@uniformdev/design-system';
export default function ProductSelector() {
const { value, setValue, metadata, isReadOnly } = useMeshLocation<'dataResourceSelector'>();
const product = metadata.dataResourceValue as { images?: string[] } | undefined;
return (
<ScrollableList label="Pick an image">
{(product?.images ?? []).map((src, i) => (
<ScrollableListItem
key={src}
buttonText={src}
active={value === `/images/${i}`}
onClick={() => !isReadOnly && setValue(() => ({ newValue: `/images/${i}` }))}
/>
))}
</ScrollableList>
);
}
```
Omit `dataResourceSelectorUrl` and authors get the default JSON tree viewer, which is a
perfectly good default — add a selector only when the shape of the data makes it painful.
## Optional: edgehancers
A connector can run managed JavaScript on Uniform's edge for each data-resource fetch, via two
hooks: `preRequest` (rewrite the request definition — auth tokens, draft vs published, cache
TTL and cache keys; Uniform still fetches) and `request` (replace the fetch entirely — batching
several resources into one call, OAuth exchanges, reshaping responses).
Which one you need is decided by caching: `preRequest` runs on every request and can change the
cache key, `request` runs only on a cache miss and cannot. See
[custom-edgehancers.md](custom-edgehancers.md) for the hook contracts, use cases, batching
helpers, deployment, and testing.
SHA-256: 98ec0041f7d3c90a4958cb58566838f6d50eafc5c639fdad013ac6dc62849c32