Overview
What @seedprotocol/mapping does, how to install it, its two entry points and the types a mapping is made of.
@seedprotocol/mapping connects fields of outside content (RSS or Atom entries, Markdown files with frontmatter) to the properties of a Seed model. You parse the content into sources, build a list of edges from source to property (by hand, with autoMap, or with the FieldMapper UI), then apply the edges to each entry to get a plain object of property values that you pass to Item.create. The package never fetches anything, never touches the SDK's database and doesn't depend on any other Seed package.
import { rssXmlToSources, autoMap, applyMappingAsync } from '@seedprotocol/mapping'
import { FieldMapper } from '@seedprotocol/mapping/react'For a walkthrough that imports a real feed into a Post model, see Import an RSS feed into your models.
Install
npm install @seedprotocol/mappingThe only runtime dependency is rss-parser. react and react-dom (18 or 19) are optional peer dependencies: install them only if you import @seedprotocol/mapping/react. The main entry has no React import, so it runs in Node scripts and servers without them.
Entry points
| Import | What it has | Needs React |
|---|---|---|
@seedprotocol/mapping | Parsers, autoMap, applyMapping/applyMappingAsync, coerceValue, lookup and edge helpers, all the types. See Helpers | No |
@seedprotocol/mapping/react | The FieldMapper component, the useFieldMapper hook, theme and slot helpers. See FieldMapper UI | Yes |
Import FieldMapper from /react, not from the main entry. There is no /node entry; the main entry is the Node one.
How the pieces fit
- Sources. A parser turns one entry (one RSS item, one Markdown file) into a list of
SourceNodes, one per field, each with a stable ID such asrss-titleorfm-summary. - Targets. You describe the model you're importing into as a list of
TargetPropertys. Build them from your schema file so they match it. - Edges. A mapping is a list of
FieldMappingedges, each from a source ID to a property name.autoMapguesses a first set from field names;FieldMapperlets a user edit them. A mapping depends on the shape of the feed, not on one entry, so you build it from a sample entry and reuse it for every entry. - Apply.
applyMapping(synchronous) orapplyMappingAsyncturns one entry's sources into aPropertyBag, converting each value to its target's type. Some edges need work only your app can do (fetch a page, download an image);applyMappingAsynchands those to a callback you supply. - Save. Wrap the edges in a
MappingDocumentand store it as JSON so the next import reuses it.
import { Item } from '@seedprotocol/sdk'
import {
rssXmlToSources,
autoMap,
applyMappingAsync,
type MappingDocument,
type TargetProperty,
} from '@seedprotocol/mapping'
const targets: TargetProperty[] = [
{ name: 'title', dataType: 'Text', required: true },
{ name: 'body', dataType: 'Html' },
{ name: 'publishedAt', dataType: 'Date' },
]
const xml = await (await fetch('https://blog.example.com/rss.xml')).text()
const { items } = await rssXmlToSources(xml)
const doc: MappingDocument = {
version: 1,
sourceKind: 'rss',
mappings: [...autoMap(items[0] ?? [], targets), { sourceId: 'rss-isoDate', propertyName: 'publishedAt' }],
}
for (const sources of items) {
const { properties, errors } = await applyMappingAsync(sources, doc.mappings, targets)
if (errors.length > 0) console.warn(errors)
// A Date target comes back as epoch milliseconds; the SDK stores ISO strings
const { publishedAt, ...rest } = properties
const values = {
modelName: 'Post',
...rest,
...(typeof publishedAt === 'number' ? { publishedAt: new Date(publishedAt).toISOString() } : {}),
}
await Item.create(values)
}Core types
All types are exported from the main entry.
SourceNode
One mappable field of one entry.
type SourceNode = {
id: string
label: string
subtitle?: string
kind: SourceKind
value: string
meta?: Record<string, unknown>
}
type SourceKind = 'frontmatter' | 'section' | 'full' | 'rssField' | 'xmlPath' | 'resolved'| Field | Description |
|---|---|
id | Stable ID, the same for the same field in every entry: rss-title, rss-enclosure-0, fm-title, section-full, section-0. Edges point at it |
label | Display name, usually the field name (title, content:encoded, enclosure[0]) |
subtitle | Optional second display line in the UI. Not saved in a mapping |
kind | Where it came from. rssField for RSS/Atom, frontmatter, full and section for Markdown, resolved for the extract/file candidates buildResolvedSourceNodes adds. The type includes xmlPath, but nothing in the package produces it |
value | The field's text. Apply copies value only, so an empty field stays empty rather than taking the label |
meta | Extra data. Media sources carry url, contentType, length and class (see UrlMediaClass); Markdown frontmatter carries valueType, sections carry level |
You can build SourceNodes yourself for any other format; the apply functions only look at id, value, label and meta.
TargetProperty
One property of the model you're importing into.
type TargetProperty = {
name: string
dataType: string
ref?: string
refValueType?: string
required?: boolean
}| Field | Description |
|---|---|
name | The property name, as in your schema |
dataType | The property's type: Text, Html, Number, Boolean, Date, Json, Image, File, Relation or List. Decides how values are converted |
ref | For Relation and List of Relation, the related model's name. Your schema file calls this model |
refValueType | For List, the element type, e.g. Relation or Text |
required | A hint for FieldMapper, which flags required properties nothing maps to. Apply ignores it |
A target is a relation target when dataType is Relation, or List with refValueType: 'Relation' or a ref. Relation targets never get a copied string; they need a lookup edge.
FieldMapping (an edge)
type FieldMapping = {
sourceId?: string
propertyName: string
resolve?: ResolveJob
lookup?: { entries: LookupEntry[] }
split?: { separators: LookupSeparator[] }
derive?: DeriveSpec
assemble?: AssembleSpec
}
type ResolveJob = 'extract' | 'file' | 'lookup' | 'derive' | 'assemble'
type LookupSeparator = 'and' | '&' | ','Rules for a list of edges:
- Each property takes at most one edge. If two edges name the same property, both apply functions process them in order and the last one wins.
FieldMapperin its default mode can't produce that state. - One source can feed several properties, e.g.
rss-linkinto bothimportUrlandcanonicalUrl. sourceIdis optional only for aderiveedge that takes its value from another property.
Without resolve, an edge copies the source's text and converts it to the target type. resolve names any other transform:
resolve | Meaning | Who applies it |
|---|---|---|
| (none) | Copy and convert | Both apply functions |
'lookup' | Translate the source's text (an author's name) into item IDs using the edge's lookup.entries. With split, a value that has no entry of its own is split on the separators and each part looked up (see Relation lookups) | Both apply functions, from the stored entries |
'extract' | The source is a URL; fetch the page and extract HTML | applyMappingAsync, through your resolve callback. applyMapping skips it |
'file' | The source is a media URL; download it and store it as a file | applyMappingAsync, through your resolve callback. applyMapping skips it |
'derive' | Compute this property from another property (derive.from), e.g. a slug from title | Your app. Both apply functions skip it |
'assemble' | Build stored HTML from named blocks (assemble.blocks) | Your app. Both apply functions skip it |
derive and assemble are stored on the edge so a mapping can record them, but the package doesn't interpret derive.from or the block names.
LookupEntry
type LookupEntry = { value: string; refs: string[] }One row of a lookup table: the source text (value, e.g. "Jane Doe") and the IDs to store (refs, typically seedLocalIds or seedUids), in the order the value names them, lead first. A joint byline such as "Sam Ortiz & Mira Chen" can have one entry with two refs. A List target gets every ref, a Relation target the first. An entry saved by 0.7.12 or earlier as { value, ref } still applies, read as refs: [ref].
Entries match on the lookup key (lookupKey): the text in Unicode NFC, trimmed, with runs of whitespace collapsed to one space, compared without case, so "MIRA CHEN" finds the "Mira Chen" entry. The package never checks that the refs exist.
DeriveSpec and AssembleSpec
type DeriveSpec = { from: string; fallback?: boolean }
type AssembleSpec = { blocks: string[] }DeriveSpec.from names the property to derive from. fallback: true means derive even when a mapped source is empty; a copy edge can carry a derive as that fallback. AssembleSpec.blocks is your own list of block names.
MappingDocument
The JSON you save.
type MappingDocument = {
version: 1
sourceKind: 'markdown' | 'rss' | 'xml'
mappings: FieldMapping[]
}Lookup tables live on their edges (lookup.entries), so a saved document keeps them. TargetProperty.required and SourceNode.subtitle aren't part of it. There is no load or validate function; see Saving and loading a mapping.
PropertyBag and results
type PropertyBag = Record<string, unknown>
type ApplyMappingAsyncResult = {
properties: PropertyBag
errors: ApplyMappingError[]
}
type ApplyMappingError = {
sourceId?: string
propertyName: string
resolve?: ResolveJob
message: string
}A PropertyBag maps property names to converted values. Properties the entry had no source for are absent, not undefined.
ResolveContext and ResolveCallback
type ResolveCallback = (ctx: ResolveContext) => Promise<unknown>
type ResolveContext = {
job: ResolveJob
source: SourceNode
target: TargetProperty
url?: string
contentType?: string
class?: UrlMediaClass
rawValue?: string
}
type UrlMediaClass = 'html' | 'image' | 'audio' | 'video' | 'unknown'The callback applyMappingAsync calls for extract and file edges. See Resolve callbacks.
Related
- Import an RSS feed into your models: the step-by-step guide
- Helpers: parsers,
autoMap, apply functions and edge helpers - FieldMapper UI: the React component and hook
- Item:
Item.create, where the property bag goes - Modeling relations: what a relation target points to
Hyper distribution (feed-hyper)
Reference for @seedprotocol/feed-hyper: publishFeed, serveFeed, openFeed, seedFeed and watchFeed, the seed-feed command and its flags, the drive layout, and its known limits.
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.