Helpers
Reference for the headless @seedprotocol/mapping API: the RSS and Markdown parsers, autoMap, applyMapping and applyMappingAsync, coerceValue, resolve callbacks, lookup and edge helpers, and saving a mapping as JSON.
Everything on this page comes from the main entry, @seedprotocol/mapping, and runs in Node and in the browser without React. A Vite production build that uses rssXmlToSources needs one alias; see In a browser. The Overview explains the types these functions take and return.
import {
rssXmlToSources,
markdownToSources,
autoMap,
applyMapping,
applyMappingAsync,
coerceValue,
} from '@seedprotocol/mapping'rssXmlToSources
Parses RSS or Atom XML and turns every entry into a list of sources.
function rssXmlToSources(xml: string): Promise<RssXmlSourcesResult>
type RssXmlSourcesResult = {
channel: { title?: string; link?: string; description?: string }
items: SourceNode[][]
}Parameters
| Name | Type | Description |
|---|---|---|
xml | string | The feed's XML. Fetching it is up to you; the package makes no network requests |
Returns
The channel's title, link and description, and one SourceNode[] per entry, in document order. It rejects when the XML can't be parsed.
Parsing uses rss-parser with the same extra fields as @seedprotocol/feed: content:encoded, featureImage, feature_image, seedUid, SeedUid, media:content and media:thumbnail. Each top-level field the parser produces becomes one source:
| Source ID | Holds |
|---|---|
rss-title | The title |
rss-link, rss-guid | The entry's URL and ID |
rss-content:encoded | The full HTML body, when the feed has one. rss-content:encodedSnippet is the same as plain text |
rss-content, rss-contentSnippet | The <description> (or Atom content), as HTML and as plain text |
rss-creator, rss-dc:creator, rss-author | The author's name, under whichever names the feed uses |
rss-pubDate, rss-isoDate | The date, as written and as ISO 8601 |
rss-categories | The categories, as a JSON array string such as ["intro","news"] |
rss-enclosure-0 | The enclosure's URL |
rss-mediaContent-N, rss-mediaThumbnail-N | media:content and media:thumbnail URLs |
rss-featureImage, rss-feature_image | A feature image URL, when the feed has the field |
The rules behind the IDs:
- IDs are
rss-plus the parser's field name, so the same field has the same ID in every entry and every feed. Print one entry's sources to find a field's ID. - Values are trimmed, and empty fields produce no source. Arrays become JSON strings. An object becomes its text content (
_orvalue) when it has one, otherwise a JSON string. - Media fields become one source per URL. Enclosures,
media:contentandmedia:thumbnailshare one counter, so with an enclosure and onemedia:contentyou getrss-enclosure-0andrss-mediaContent-1. Theirvalueis the URL and theirmetaholdsurl,contentType,lengthandclass(aUrlMediaClassfrom the MIME type or the file extension). - A
featureImageorfeature_imagevalue that starts withhttpgetsmeta.urlandmeta.classtoo.
Example
import { rssXmlToSources } from '@seedprotocol/mapping'
const xml = await (await fetch('https://blog.example.com/rss.xml')).text()
const { channel, items } = await rssXmlToSources(xml)
console.log(channel.title, items.length, 'entries')
for (const source of items[0] ?? []) console.log(source.id, '=', source.value.slice(0, 60))In a browser
Most feeds don't send CORS headers, so fetch them through your own server.
The XML parser inside rss-parser reads setImmediate from Node's timers module. In a Vite app, turn on the plugin's includeNodePolyfills, which covers timers:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { seedVitePlugin } from '@seedprotocol/vite'
export default defineConfig({
plugins: [react(), ...seedVitePlugin({ includeNodePolyfills: true })],
})Without it, Vite replaces timers with an empty module and logs Module "timers" has been externalized for browser compatibility, so the parser's setImmediate is undefined. Either way, vite build logs the same warning for http and https: rss-parser uses them to fetch URLs, and rssXmlToSources only parses the string you pass.
The rest of the main entry, rssItemToSources included, builds without the polyfills.
rssItemToSources
Turns one already-parsed entry into sources, with the same rules as rssXmlToSources. Use it when you parse the feed yourself, for example with @seedprotocol/feed's parser.
function rssItemToSources(item: Record<string, unknown>): SourceNode[]Keys named items or starting with $ are skipped.
markdownToSources
Turns a Markdown document with optional YAML frontmatter into sources.
function markdownToSources(markdown: string): SourceNode[]Sources come in this order:
| Source ID | kind | Holds |
|---|---|---|
fm-<key> | frontmatter | One per frontmatter key, e.g. fm-title. meta.valueType is 'string', 'boolean' or 'array' |
section-full | full | The whole body after the frontmatter, as Markdown. Always present, even when the body is empty. The ID is exported as SECTION_FULL_ID |
section-0, section-1, … | section | One per # or ## heading, numbered in order. The value is the text under the heading, or the heading itself when there's none. meta.level is 1 or 2 |
Limits of the frontmatter parser:
- Flat keys only. Nested YAML isn't supported; indented lines are ignored.
- Lists in brackets (
tags: [a, b]) become one comma-separated string,"a, b", not a JSON array. trueandfalsebecome"true"and"false".nulland empty values become"".###and deeper headings don't start a section; they stay in the section above them.
Bodies are copied as Markdown. Convert them to HTML yourself before you store them in an Html property.
An empty frontmatter field (summary: with no value) still produces a source with an empty value, and section-full is produced even for a document with no body. Mapping one sets the property to the empty value converted with coerceValue: '' for text types, 0 for Number, false for Boolean. To leave those properties out instead, drop empty sources before you map: sources.filter((s) => s.value !== '').
Example
import { markdownToSources } from '@seedprotocol/mapping'
const markdown = `---
title: "Hello"
tags: [intro, news]
---
# Intro
First paragraph.
`
const sources = markdownToSources(markdown)
// fm-title = "Hello", fm-tags = "intro, news", section-full = the body, section-0 = "First paragraph."parseMarkdownWithFrontmatter and parseMarkdownSections
The two steps markdownToSources is built from, if you need the parts.
function parseMarkdownWithFrontmatter(markdownContent: string): {
frontmatter: { id: string; key: string; value: string; valueType: 'string' | 'boolean' | 'array' }[]
body: string
}
function parseMarkdownSections(md: string): {
id: string
heading: string
level: number
content: string
}[]Without a frontmatter block, frontmatter is empty and body is the whole input.
autoMap
Guesses a first set of edges from field names.
function autoMap(sources: SourceNode[], targets: TargetProperty[], previous?: FieldMapping[]): FieldMapping[]Parameters
| Name | Type | Description |
|---|---|---|
sources | SourceNode[] | One sample entry. Sources of kind resolved are ignored |
targets | TargetProperty[] | The model's properties |
previous | FieldMapping[] | The current mappings, default []. A property that autoMap maps as a lookup again keeps the entries and split its lookup edge has here |
Returns
A new FieldMapping[], at most one edge per property. It never fills in lookup tables itself: a lookup edge's entries and split come from previous or stay empty.
Name matching ignores case, spaces, :, _ and -, and also accepts one name containing the other (image matches featureImage). The passes run in this order, except that 3 to 5 run together, one source at a time in source order:
- Whole document. A Markdown
section-fullsource goes to the first property whose name is exactly (in any case)text,content,body,fulltextormarkdown. - Full HTML.
content:encoded, or acontentsource that contains<, goes to the firstHtmlproperty or the first property named likehtml,content,bodyortext. - Links. A source named
linkorurlgoes to every free property named likeimportUrl,canonicalUrl,url,linkorpermalink, as a plain copy. - Media URLs. A URL source classified as an image goes to the first
Imageproperty (or one named likefeatureImage,image,coverorcoverImage) withresolve: 'file'. Audio and video URLs go to aFileproperty, or one named likeaudio,video,file,enclosure,attachmentormedia, withresolve: 'file'. - Aliases and names. Known RSS aliases are tried first:
title→title,name,headline;description/summary→summary,description,excerpt;creator/author→author,authors,creator,byline; and so on. Then any property whose name matches the source's. A match on a relation target getsresolve: 'lookup', with no entries unlessprevioushas some for that property. A raw URL is never copied onto anHtml,Text,ImageorFileproperty. - Extract fallback. If an
Html-like property is still free and there's alink,urlorpermalinksource with a URL, it gets that source withresolve: 'extract'.
Properties whose names don't match anything are left unmapped. In an RSS import that usually includes the summary (contentSnippet doesn't match summary), the date (isoDate doesn't match publishedAt) and the tags, so add those edges yourself.
When an entry has no content:encoded, pass 5 maps rss-content (the plain description) onto the first Html-like property. Because autoMap only sees the sample entry, check which entry you run it on, and remove that edge if you'd rather leave the body empty.
Example
import { autoMap, type FieldMapping, type SourceNode, type TargetProperty } from '@seedprotocol/mapping'
declare const sample: SourceNode[] // one entry from rssXmlToSources
const targets: TargetProperty[] = [
{ name: 'title', dataType: 'Text' },
{ name: 'body', dataType: 'Html' },
{ name: 'author', dataType: 'Relation', ref: 'Author' },
]
const mappings: FieldMapping[] = autoMap(sample, targets)
// [
// { sourceId: 'rss-content:encoded', propertyName: 'body' },
// { sourceId: 'rss-creator', propertyName: 'author', resolve: 'lookup' },
// { sourceId: 'rss-title', propertyName: 'title' },
// ]applyMapping
Applies edges to one entry synchronously and returns the property bag. It never throws for a missing source or property; it skips the edge.
function applyMapping(
sources: SourceNode[],
mappings: FieldMapping[],
targets: TargetProperty[],
options?: { onUnmatched?: (miss: UnmatchedLookup) => void },
): PropertyBag
type UnmatchedLookup = {
sourceId: string
propertyName: string
value: string
splitFrom?: string
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
sources | SourceNode[] | One entry's sources | |
mappings | FieldMapping[] | The edges | |
targets | TargetProperty[] | The model's properties. Edges to a property not listed here are skipped | |
options.onUnmatched | (miss: UnmatchedLookup) => void | none | Called once for each lookup value with no entry: value is the text that missed, and splitFrom the whole source value when the miss is one part of a split value |
Returns
A PropertyBag. For each edge:
- No
resolve: the source'svalue, even when it's empty, converted withcoerceValue. Edges onto relation targets are skipped: a name is never copied into a relation. resolve: 'lookup': the matching entry'srefs. AListgets all of them, aRelationthe first. Withsplit, the refs of the parts that have entries, in the value's order without repeats (see Relation lookups). No match leaves the property out and is reported toonUnmatched. An empty source value is no lookup: no property and no report.extract,file,derive,assemble: skipped. UseapplyMappingAsyncforextractandfile.
Use applyMapping for previews, or when your mappings have no extract or file edges.
applyMappingAsync
Applies edges to one entry, runs extract and file edges through your callback, and reports what failed.
function applyMappingAsync(
sources: SourceNode[],
mappings: FieldMapping[],
targets: TargetProperty[],
options?: ApplyMappingAsyncOptions,
): Promise<ApplyMappingAsyncResult>
type ApplyMappingAsyncOptions = {
resolve?: ResolveCallback
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
sources | SourceNode[] | One entry's sources | |
mappings | FieldMapping[] | The edges | |
targets | TargetProperty[] | The model's properties | |
options.resolve | ResolveCallback | none | Called once per extract or file edge. Not called for any other edge |
Returns
{ properties, errors }. Copy and lookup edges set the same values as in applyMapping. The cases that set nothing:
| Case | Result |
|---|---|
Edge to a property not in targets | Skipped, no error |
| Copy edge whose source the entry doesn't have | Skipped, no error |
| Copy edge onto a relation target | Skipped, no error |
| Lookup edge whose source is missing | Error Source not found: <sourceId> |
| Lookup edge with no entry for the source's text | Error No lookup entry for "<text>", property left out. An empty source value adds no error |
Lookup edge with split where a part has no entry | Error No lookup entry for "<part>" (split from "<value>") for each such part. The property still gets the other parts' refs |
extract/file edge whose source is missing (neither the ID nor its @extract/@file candidate) | Error Source not found: <sourceId>, reported even when there's no callback |
extract/file edge with no resolve callback | Error No resolve callback for <job> |
extract/file edge whose callback throws | Error with the thrown message (a thrown non-Error is turned into a string), property left out |
derive, assemble | Skipped, no error |
Edges run one after another, in order. The result is partial: one failed edge doesn't stop the others.
Example
import { applyMappingAsync, type FieldMapping, type SourceNode, type TargetProperty } from '@seedprotocol/mapping'
declare const sources: SourceNode[]
declare const mappings: FieldMapping[]
declare const targets: TargetProperty[]
const { properties, errors } = await applyMappingAsync(sources, mappings, targets, {
resolve: async ({ job, url }) => {
if (job === 'extract' && url) {
const html = await (await fetch(url)).text()
return html // extract the article body here
}
throw new Error(`Unsupported transform: ${job}`)
},
})
for (const e of errors) console.warn(`${e.propertyName}: ${e.message}`)Resolve callbacks
applyMappingAsync calls options.resolve with a ResolveContext for every extract or file edge and stores what it returns.
| Field | Value |
|---|---|
job | 'extract' or 'file' |
source | The source the edge points at |
target | The target property |
url | source.meta.url if set, otherwise the source's value, trimmed. '' when the value is empty |
contentType | source.meta.contentType (or meta.type), e.g. image/jpeg for an enclosure |
class | source.meta.class when it's a UrlMediaClass, e.g. 'image' |
rawValue | Never set. The type describes it for lookup edges, which never reach the callback |
The return value is converted for the target:
- A string goes through
coerceValue, so anHtmltarget keeps it as is. nullorundefinedbecomescoerceValue(''):''for text types,0forNumber,falseforBoolean.- Anything else is kept as is for
Image,File,Relation,ListandJsontargets, so you can return an item ID, an object or aBlobfor a file. For other types it's turned into a string first.
The package never fetches anything itself. For a file edge, download the URL and store it the way your app stores files (for example with the SDK), then return what the Image or File property should hold.
coerceValue
Converts a source string to a target data type. Both apply functions use it for every copied value.
function coerceValue(rawValue: string, dataType: string): unknowndataType | Result |
|---|---|
Number | parseFloat(rawValue), or 0 when that isn't a number. "12px" gives 12; "abc" gives 0 |
Boolean | true for "true", "1" or "yes", in any case and after trimming; otherwise false. "TRUE" and " yes " give true; "0", "no" and "" give false |
Json | JSON.parse(rawValue), or the string unchanged when it isn't valid JSON |
Date | Epoch milliseconds (new Date(rawValue).getTime()), or the string unchanged when it isn't a date |
anything else (Text, Html, List, Image, File, …) | The string unchanged |
Date values come back as numbers such as 1791882000000, and Item.create rejects a number for a Date property with ItemCreateValidationError (publishedAt: Expected string. Received: 1791882000000). Convert first: new Date(value).toISOString(). A List target gets the source text unchanged. Item.create accepts a JSON array string, which is what rss-categories gives, and stores the array; Markdown's "a, b" isn't JSON and is rejected, so split it into an array yourself.
Relation lookups
A lookup edge stores a table from source text to item IDs on the edge (lookup.entries). The apply functions read it; these helpers let you read and write it yourself.
| Function | Signature | What it does |
|---|---|---|
isRelationLookupTarget | (target: TargetProperty) => boolean | True for Relation, and for List with refValueType: 'Relation' or a ref |
normalizeLookupKey | (raw: string) => string | Unicode NFC, trimmed, runs of whitespace collapsed to one space; case is kept. The form entries are stored and shown in |
lookupKey | (raw: string) => string | The lookup key: normalizeLookupKey, then upper- and lower-cased, so ß becomes ss. Edge entries match when their lookup keys are equal. Full-width letters aren't folded to ASCII |
sampleValueFromSource | (source: SourceNode | undefined) => string | The source's value after normalizeLookupKey; '' for no source or an empty value |
collectLookupValues | (items: SourceNode[][], mapping: FieldMapping) => string[] | Every distinct value of the edge's source across all entries, one per lookup key, in first-seen order and spelling, empty values left out. Use it to offer every author in a feed for assignment, not only the sample's |
lookupEntriesFromMapping | (mapping: FieldMapping | undefined) => LookupEntry[] | The edge's entries in the { value, refs } shape (a stored { value, ref } comes back as refs: [ref]), [] when it has none |
lookupRefsFromEntries | (entries: LookupEntry[] | undefined, rawValue: string) => string[] | undefined | The refs of the first entry with the same lookup key as rawValue and at least one non-empty ref |
resolveLookup | (mapping: FieldMapping, rawValue: string) => LookupResolution | What the apply functions do for one value, as { refs, unmatched, split }; see below |
splitLookupValue | (rawValue: string, separators: readonly LookupSeparator[]) => string[] | The normalized parts of rawValue between the separators, empty parts dropped |
DEFAULT_LOOKUP_SEPARATORS | readonly LookupSeparator[] | ['and', '&']. The comma is left out because some feeds print a name as "Chen, Mira" |
shapeLookupValue | (mapped: string | string[], target: TargetProperty) => string | string[] | The list, trimmed, for List targets; the first ref for any other target |
withLookupEntries | (edge: FieldMapping, entries: LookupEntry[]) => FieldMapping | A copy of the edge with lookup.entries replaced; an empty list removes lookup |
withLookupSplit | (edge: FieldMapping, separators: readonly LookupSeparator[] | null) => FieldMapping | A copy of the edge with split set, or removed with null or [] |
resolveLookup looks for the whole value first, in the edge's entries by lookup key. That entry wins, so a joint byline can have its own entry with several refs. Without one, an edge with split breaks the value on its separators and looks up each part the same way. 'and' matches in any case and only with whitespace around it, so "Anderson" stays whole; '&' and ',' match with or without spaces. The result's refs are the matched parts' refs in the value's order, without repeats; unmatched lists the parts with no entry; split is true when the value broke into more than one part. A value with no separator, or an edge without split, is looked up whole and, when it misses, lands in unmatched as is.
import {
DEFAULT_LOOKUP_SEPARATORS,
resolveLookup,
withLookupEntries,
withLookupSplit,
type FieldMapping,
} from '@seedprotocol/mapping'
const edge: FieldMapping = withLookupSplit(
withLookupEntries(
{ sourceId: 'rss-creator', propertyName: 'authors', resolve: 'lookup' },
[
{ value: 'Mira Chen', refs: ['mira-local-id'] },
{ value: 'Sam Ortiz', refs: ['sam-local-id'] },
],
),
DEFAULT_LOOKUP_SEPARATORS,
)
resolveLookup(edge, 'sam ortiz & Mira Chen')
// { refs: ['sam-local-id', 'mira-local-id'], unmatched: [], split: true }
resolveLookup(edge, 'Mira Chen and Nobody')
// { refs: ['mira-local-id'], unmatched: ['Nobody'], split: true }To apply the entries your own app collects, replace them on the edge:
import { withLookupEntries, type FieldMapping, type LookupEntry } from '@seedprotocol/mapping'
declare const mappings: FieldMapping[]
declare const authorEntries: LookupEntry[] // e.g. [{ value: 'Jane Doe', refs: [author.seedLocalId] }]
const next = mappings.map((edge) =>
edge.resolve === 'lookup' && edge.propertyName === 'author' ? withLookupEntries(edge, authorEntries) : edge,
)Extract and file candidates
An edge's extract or file transform can also be expressed as an extra source, which is how the deprecated wires layout shows it. These helpers convert between the two forms.
| Export | Signature | What it does |
|---|---|---|
buildResolvedSourceNodes | (sources: SourceNode[]) => SourceNode[] | Returns the sources plus, for each source whose value or meta.url is an http(s), blob: or data: URL, two resolved sources: <id>@extract and <id>@file. Markdown body and section sources are skipped |
normalizeMappingFromSourceId | (sourceId: string, propertyName: string) => FieldMapping | Turns a pick of rss-link@extract into { sourceId: 'rss-link', propertyName, resolve: 'extract' }. Plain IDs pass through |
parseResolvedSourceId | (sourceId: string) => { originId: string; resolve: ResolveJob } | null | Splits a @extract/@file ID, null for any other |
resolvedSourceId | (originId: string, job: ResolveJob) => string | Builds <id>@extract or <id>@file; other transforms return the ID unchanged |
resolveMappingSource | (sources: SourceNode[], mapping: FieldMapping) => SourceNode | undefined | The edge's source, falling back to its @extract/@file candidate |
RESOLVE_EXTRACT_SUFFIX, RESOLVE_FILE_SUFFIX | '@extract', '@file' | The suffixes |
Always save edges in the normalized form (origin ID plus resolve). The default FieldMapper layout works with origin sources only, so you don't need these unless you build your own picker.
URL classification
function classifyUrl(input: { url: string; contentType?: string | null }): UrlMediaClass
function looksLikeUrl(value: string): booleanclassifyUrl returns 'image', 'audio', 'video' or 'html' from the MIME type when one is given and recognized, otherwise from the path's file extension, and 'unknown' when neither says. It does no I/O. looksLikeUrl is true for strings that, trimmed and in any case, start with http://, https://, blob: or data:. The type ClassifyUrlInput is exported for the argument.
Edge helpers
Small pure functions for editing edges without losing what's stored on them. FieldMapper uses them; reach for them if you build your own editor.
| Function | Signature | What it does |
|---|---|---|
isPresentEdge | (mapping: FieldMapping) => boolean | True when the edge has a property name and either a sourceId or resolve: 'derive'. This is what counts as "mapped" |
applyResolveToEdge | (prev: FieldMapping, nextResolve: ResolveJob | null) => FieldMapping | The edge after the user picks another transform. Keeps lookup and split for lookup, assemble for assemble, derive for copy and derive |
withPreservedExtras | (next: FieldMapping, prev: FieldMapping | undefined) => FieldMapping | Carries derive and assemble over from the previous edge when a new source is connected. When both edges are lookups, the new one gets the previous entries and split unless it has its own; otherwise lookup and split are dropped |
withDeriveSpec | (edge: FieldMapping, spec: DeriveSpec | null) => FieldMapping | Sets or removes derive |
withAssembleBlocks | (edge: FieldMapping, blocks: string[]) => FieldMapping | Sets assemble.blocks |
clearSourceKeepDerive | (prev: FieldMapping) => FieldMapping | A source-less derive edge for the same property |
isDeriveKeepOnClear | (prev: FieldMapping) => boolean | True when clearing the source should keep a derive edge |
omitEmptySourceId | (edge: FieldMapping) => FieldMapping | Removes an empty sourceId so '' isn't saved |
Saving and loading a mapping
There is no serialize or parse function: a MappingDocument is plain JSON. Write it with JSON.stringify and read it back with JSON.parse. The package doesn't validate what you load, so check the shape yourself:
import type { MappingDocument } from '@seedprotocol/mapping'
import { readFile, writeFile } from 'node:fs/promises'
export async function saveMapping(path: string, doc: MappingDocument): Promise<void> {
await writeFile(path, JSON.stringify(doc, null, 2))
}
export async function loadMapping(path: string): Promise<MappingDocument> {
const doc = JSON.parse(await readFile(path, 'utf8')) as Partial<MappingDocument>
if (doc.version !== 1 || !Array.isArray(doc.mappings)) {
throw new Error(`${path} is not a mapping document`)
}
return doc as MappingDocument
}Lookup tables are stored on their edges, so matched authors stay matched. Entries saved by 0.7.12 or earlier as { value, ref } still apply, read as refs: [ref].
A document saved by 0.7.13 or earlier can have a top-level lookups table (property, then source text, then one ID or a list). The package doesn't read it and nothing warns: the table's values count as lookup misses. Move it onto the edges before you apply the document:
import { lookupEntriesFromMapping, withLookupEntries, type MappingDocument } from '@seedprotocol/mapping'
type OlderDocument = MappingDocument & {
lookups?: Record<string, Record<string, string | string[]>>
}
export function moveLookupsOntoEdges({ lookups = {}, ...doc }: OlderDocument): MappingDocument {
const mappings = doc.mappings.map((edge) => {
const table = lookups[edge.propertyName]
if (edge.resolve !== 'lookup' || !table) return edge
const moved = Object.entries(table).map(([value, refs]) => ({ value, refs: [refs].flat() }))
return withLookupEntries(edge, [...lookupEntriesFromMapping(edge), ...moved])
})
return { ...doc, mappings }
}Entries already on the edge stay first, and the first match wins. Table rows for a property with no lookup edge are dropped.
Related
- Overview: the types and how the pieces fit
- FieldMapper UI: letting users edit the edges
- Import an RSS feed into your models
- @seedprotocol/feed: the other direction, published items out as a feed
FieldMapper UI
Reference for the FieldMapper React component and the useFieldMapper hook from @seedprotocol/mapping/react: every prop, the row layouts, lookup pickers, slots, theming and the deprecated props to avoid.
Overview
Reference for @seedprotocol/query: queryBySchema, getSeed, getSeeds, getSeedAttestations, pinned reads and the other exports, the SeedRecord shape, paging and UID prefixes, schema names, caching and frozen records, and the environment variables it reads.