ModelProperty
Every property type and option you can declare on a model, how each type is stored, published and read back, and the ModelProperty class for working with property definitions in code.
A model property is the definition of one field on a model: its name, its data type and its options. You declare properties in seed.schema.json (or in Model.create), and the SDK represents each one as a ModelProperty instance. This page covers the definition format first, then the class.
import { ModelProperty, ModelPropertyDataTypes } from '@seedprotocol/sdk'A model property is a definition. The value stored on one item is an ItemProperty.
Property definition reference
The same options appear in two shapes. The schema file uses type and, for a relation's target, model. Code (Model.create, ModelProperty.create) uses dataType and ref.
"author": { "type": "Relation", "model": "Author", "required": true }author: { dataType: 'Relation', ref: 'Author', required: true }| Schema file | In code | Applies to | Description |
|---|---|---|---|
type | dataType | all | Required. One of the ten data types. Case-insensitive; string is accepted for Text. Any other name, such as Integer or blob, fails the schema load with unknown type "Integer". Expected one of: Text, Number, …, and in code fails validate() with code unknown_data_type. |
required | required | all | true marks the property as required. Not checked when you save locally; publishing checks required Relation, Image, File, Html and Json properties. |
validation | validation | see Validation | Rules checked when a value is saved. |
model | ref | Relation | The target model's name, in the same schema. |
refValueType | refValueType | List, Relation | For a List, the element type, for example Text or Relation. For a single Relation to Image, File or Html, that type: { "type": "Relation", "refValueType": "Image", "model": "Image" } saves an assigned data: or blob: URL as an Image. Without it, such a relation rejects a data: URL with Relation target not found. A File or Blob is rejected either way (ItemPropertySaveValidationError); declare { "type": "Image" } for uploads. |
ref | ref | List of Relation | The target model's name. With ref and no refValueType, the list is a list of relations. |
storage | storageType, localStorageDir, filenameSuffix | Text | See Storage. |
nameMatch | none | all | 'exact', 'normalized' or 'fuzzy': how loosely names from EAS match this property when syncing. Defaults to the model's, then the schema's, then 'fuzzy'. See nameMatch. |
description | none | all | Notes for people. Kept in the file, not loaded. |
id | id | all | The property's stable ID. Without one, the SDK keeps the stored property's ID or derives one from the schema, model and property names; see id. |
A List with neither refValueType nor ref fails to import, with List property "…" in model "…" requires "refValueType".
Data types
Every type except List holds one value; saving an array to any other type fails validation with code array_not_allowed. The ModelPropertyDataTypes enum has a member for each type (ModelPropertyDataTypes.Text is 'Text').
| Type | Write | Reads back as | Local storage | EAS field type |
|---|---|---|---|---|
Text | a string, up to 255 characters by default | the string | SQLite | string |
Number | a number | a string: '42' | SQLite | string |
Boolean | true or false | a string: 'true' or 'false' | SQLite | bool |
Date | an ISO 8601 date-time string | the string | SQLite | uint256 |
Json | a JSON string (JSON.stringify(value)) | the string | SQLite | bytes32 |
Html | an HTML string of any length | the HTML string | a file in the files directory | bytes32 |
Image | a File or Blob | see File and image properties | a file in the files directory | bytes32 |
File | a File, or a string (see ItemProperty) | see File and image properties | a file in the files directory | bytes32 |
Relation | the related item's seedLocalId or seedUid, as a string. An object such as { seedLocalId } is rejected (Expected union value) | the related item's ID, as a string | SQLite | bytes32 |
List | an array of refValueType values | an array | SQLite | bytes32[] |
Values passed to create() and values you assign later are both validated. See Item and ItemProperty for reading and writing values.
- Every value reads back as a string, except a
List. ConvertNumberandBooleanvalues yourself. Jsonobjects are stored as'[object Object]'. PassJSON.stringify(value)and parse when you read.
A Number publishes as its decimal string, so negative numbers, decimals and values above 255 publish. A stored value that isn't a finite number fails publishing with code publish_invalid_number.
What's published where
When you publish an item with @seedprotocol/publish, every property with a value gets its own EAS attestation. 0 and false are values; only null, undefined and '' are skipped. Html, Image and File content is uploaded to Arweave first, and the attestation holds a reference to it. Relation and List of Relation attestations reference the related items' Seed UIDs, so related items need to be published too. See Permanence.
Each attestation uses an EAS schema named after the field type and the property's EAS name. The SDK's easName function (also exported from the main entry) makes that name: it splits words at case changes and before the last capital of an acronym, turns spaces, hyphens and other characters into _, and lowercases the result:
| Property | EAS schema |
|---|---|
title: Text | string title |
publishedAt: Date | uint256 published_at |
wordCount: Number | string word_count |
coverURL: Text | string cover_url |
author: Relation | bytes32 author |
A single Relation, Image, File or Html property is attested as a bytes32 reference under its own name: author: Relation is bytes32 author and cover: Image is bytes32 cover. Locally, its values are stored as authorId, whether you wrote them or they were synced from EAS, and are matched to the property by its row, so they follow a rename. A List of Relation is named from the singular property name, the target model and Ids: authors with ref: 'Identity' becomes author_identity_ids.
Any app whose model has a property with the same name and type reads the same attestations, which is why property names and types shouldn't change once you publish. When syncing, the SDK matches the names on EAS against your property names as loosely as the property's nameMatch allows. By default that ignores case and separators, a plural s and, for relations, a trailing _id, so published_at and PublishedAt both reach a local publishedAt, and older author_id attestations reach author. Sync also checks types: it skips an attestation whose EAS type doesn't fit the local property, with a warning. A Number accepts any uint* or int* type and a numeric string, so values published as uint8 by older versions still sync; every other type accepts only the type it publishes. Synced Number values, and Date values attested as integers, are stored decoded, as a decimal string and an ISO 8601 string.
Since 0.7.4, a few names publish under a different EAS name, and so under different EAS schemas, than earlier versions used: an acronym followed by a word (HTMLPage was htmlpage, now html_page), a digit before a capital (ID3Tag: id3tag, now id3_tag), and names with spaces or hyphens. Plain camelCase and PascalCase names (publishedAt, coverURL, BlogPost) are unchanged. See Migration.
Validation
Rules go under validation on a property:
"title": { "type": "Text", "validation": { "maxLength": 120 } },
"status": { "type": "Text", "validation": { "enum": ["draft", "live"] } }| Rule | Type | Applies to | Description |
|---|---|---|---|
maxLength | integer, at least 1 | Text, Html | Maximum length. Text defaults to 255; set this to raise or lower it. Html has no default. |
minLength | integer, at least 0 | Text, Html | Minimum length. |
enum | array | any | The allowed values. A value not in the list fails with code enum_violation. |
pattern | string (regular expression) | Text, Html | The value must match. |
custom | string | none | Not supported, and ignored. A schema file with it loads and values save as if it weren't there; the SDK logs a console warning (Custom validators are not supported …; it is ignored.) when it first stores that file. The property's validate() stays valid and lists custom_validation_unsupported under warnings. |
pattern, minLength and maxLength on any other type (on a List, any element type but Text or Html) are an error with code rule_not_applicable. A schema file with one fails to load before anything is stored, with a message such as Post.wordCount: validation.maxLength only apply to Text and Html properties (this one is Number). Remove it or use enum.
Rules combine: pattern, minLength and maxLength all apply, and enum applies together with them. On a List, the rules apply to each element, and Text elements get the 255-character default.
A value that fails validation isn't saved. Assigning it doesn't throw; awaiting the property's save() throws an ItemPropertySaveValidationError whose validationErrors hold { field, message, code, severity } entries. The codes are type (a value of the wrong type, such as text given to a Number or an object given to a Relation), max_length_exceeded, min_length, pattern, enum_violation, format (a Date value that isn't a date) and array_not_allowed. A value outside the enum gets one enum_violation; one that also misses the pattern gets both. See ItemProperty.
Item.create checks the same rules before it writes a new item, and rejects with ItemCreateValidationError.
Rules on properties created in code (Model.create, ModelProperty.create) are stored with the property in the local database, so they apply after a restart that doesn't run the same code. Other devices don't have them: saveNewVersion() writes them into the schema's version file, and rules that must apply on every device belong in seed.schema.json. Since 0.7.4, pattern is enforced on Text and enum applies together with the other rules, so values stored by older versions may break rules that now apply.
Storage
A Text property can keep its value in a file instead of the database:
"notes": {
"type": "Text",
"storage": { "type": "ItemStorage", "path": "notes", "extension": ".md" }
}| Field | In code | Description |
|---|---|---|
storage.type | storageType | ItemStorage writes the value to a file named after the item, {seedUid or seedLocalId}{extension}, in the path folder of the files directory. Any other value is stored as PropertyStorage, which saves to the database as usual. |
storage.path | localStorageDir | The folder under the files directory. Defaults to files. |
storage.extension | filenameSuffix | The file extension, including the dot. |
An ItemStorage value passed to Item.create is written to its file before create resolves.
Html, Image and File are always kept in files, so they don't need storage.
Reserved property names
These names are used for fields the SDK manages on every item. Don't use them as property names:
localId, uid, seedLocalId, seedUid, schemaUid, attestationCreatedAt, attestationRaw, createdAt, updatedAt, lastVersionPublishedAt, latestVersionLocalId, latestVersionUid, lastLocalUpdateAt, modelName, refSeedType, refValueType, refResolvedValue, refResolvedDisplayValue, type, src, alt, versionLocalId, versionsCount, versionUid, _markedForDeletion, publisher, revokedAt.
Also avoid names ending in Id or Ids: the SDK stores relation and file values under those suffixes.
Reserved Solidity words
EAS schemas can't use a word Solidity's ABI reserves, so neither a model nor a property can publish under one:
constructor, error, event, fallback, function, receive, struct, calldata, memory, storage, payable, indexed, tuple, returns, constant, external, internal, private, public, pure, view, override.
The check is on the EAS name, so Returns (which publishes as returns) and a model named Event are refused too. A List of Relation is exempt, because it publishes as <name><Model>Ids.
A schema file or config.schema with such a name doesn't load, and client.init rejects:
Could not load config.schema: Schema blog: Post.view: Property "view" publishes as "view", which EAS schemas can't use (a reserved Solidity ABI word). Rename it.For a model, the message reads Schema blog: Event: Model "Event" publishes as "event", …. ModelProperty.create with such a name rejects with Failed to write property "view": Property "view" publishes as "view", …, and Model.create rejects with Model "Event" publishes as "event", … or Property "view" publishes as "view", …, whether the name is the model's or one in properties.
Don't rename a property to one of these words either. Assigning name doesn't throw, but the rename isn't saved to the local database and validate() returns isValid: false with an error of code reserved_name. While the bad name stands, saveNewVersion() rejects with the reserved-word message if the schema has other changes, and otherwise resolves with an empty string and saves nothing.
Getting ModelProperty instances
You rarely construct a ModelProperty yourself. A loaded model's properties array holds one for each property:
import { Model, ModelProperty } from '@seedprotocol/sdk'
const Post = await Model.find({ modelName: 'Post', schemaName: 'blog' })
const title: ModelProperty | undefined = Post?.properties.find(
(property: ModelProperty) => property.name === 'title',
)Model.properties is typed as any[], so annotate the instances as above. In React, use useModelProperties and useModelProperty; see React model property hooks.
Instance properties
| Property | Type | Description |
|---|---|---|
id | string | undefined | The property's stable ID: the schema file's, one derived from the names when the file has none, or a random one for a property made with ModelProperty.create. |
name | string | undefined | The property name. |
dataType | ModelPropertyDataTypes | undefined | The data type. |
ref | string | undefined | For a relation or list of relations, the target model's name. |
refValueType | ModelPropertyDataTypes | undefined | For a List, the element type. For a Relation to Image, File or Html, that type. |
modelName | string | undefined | The model the property belongs to. |
modelId | number | undefined | The model's database row ID. |
refModelId, refModelName | number, string | The target model's row ID and name, filled in as the SDK resolves them. |
storageType, localStorageDir, filenameSuffix | string | undefined | The storage settings. |
path | string | undefined | {filesDir}/{localStorageDir}/{name}{filenameSuffix} when all three are set. |
status | 'loading' | 'ready' | 'error' | The load status: 'loading' until the property's row is written (when it had none) and its definition is loaded, then 'ready', or 'error'. |
isEdited | boolean | true when the definition differs from what was loaded. |
validationErrors | ValidationError[] | Errors from the last validation of the definition. |
isValid | boolean | true when validationErrors is empty. |
waitForReady({ timeout }) resolves once status is 'ready'. It rejects with the load's error, or after timeout milliseconds (default 15000). While status is 'error', entityLoadError(property) returns that error.
required and validation aren't exposed as fields.
Editing a property
Assigning to name, dataType, ref, refValueType, storageType, localStorageDir or filenameSuffix changes the definition. The SDK validates the new definition and, if it's valid and differs from what was loaded, saves it to the local database and marks the schema as edited. This happens in the background; nothing is thrown, except on a property that was destroyed or evicted, where the assignment throws EntityUnloadedError.
A name that fails validation stays on the instance only: the row keeps the old name, and the instance shows the new one with isEdited: true and the reason in validationErrors. Assigning a valid name clears the errors and saves it.
Item properties that were already loaded don't take an edit made this way: on Node they keep checking new values against the definition they loaded with. A schema change that rewrites the property's row (a new version of the schema file applied, or updateModelProperties) does reach them, so the next value assigned is checked against the new rules.
if (title) {
title.name = 'headline'
}Renaming a property or changing its type changes the EAS schema its values are published under, so readers stop finding existing values. Prefer adding a new property. Items' stored values follow the rename: after it, item.headline returns them and item.title is undefined. The rename is kept in the local database across restarts, but other devices and new databases don't get it until it's in a schema file: saveNewVersion() writes it to a new version file, including after a restart. If your app passes seed.schema.json as config.schema, make the same change there and set the property's id (title.id) on it, or the file's property is a new one.
ModelProperty.create
Adds a property to a loaded model, or returns the cached instance if that model already has a property with this name.
static create(
property: {
name?: string
dataType: PropertyDataType // 'Text', 'Number', … or a ModelPropertyDataTypes member
modelName?: string
ref?: string
refValueType?: PropertyDataType
required?: boolean
storageType?: 'ItemStorage' | 'PropertyStorage'
localStorageDir?: string
filenameSuffix?: string
// …and the other fields of the SDK's TProperty type
},
options?: { waitForReady?: true; readyTimeout?: number; schemaName?: string; hold?: boolean },
): Promise<ModelProperty>The parameter type is simplified; the real type is TypeBox's Static<typeof TProperty>, which isn't exported. Passing { waitForReady: false } returns the instance instead of a Promise; every other call returns a Promise, and is typed that way.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
property.name | string | none | The property name. |
property.dataType | PropertyDataType | required | The type, as a string ('Text') or an enum member (ModelPropertyDataTypes.Text). |
property.modelName | string | none | The model to add the property to. |
property.ref | string | none | The target model, for a relation. |
options.schemaName | string | none | The schema the model belongs to. Pass it; model names are only unique within a schema. |
options.waitForReady | boolean | true | Wait for the instance to be ready. |
options.readyTimeout | number | 15000 | How long to wait, in milliseconds. |
options.hold | boolean | false | Take a cache hold, released with unload(). The property stays cached without one. A call that rejects gives its hold back. |
Example
import { ModelProperty } from '@seedprotocol/sdk'
const summary = await ModelProperty.create(
{ name: 'summary', dataType: 'Text', modelName: 'Post' },
{ schemaName: 'blog' },
)When it waits for ready (the default), the Promise resolves once the property's row is written to the local database. It rejects if the write fails, for example with Cannot write property "summary": model Post has no database row when the model isn't in the database, or if the write doesn't finish within readyTimeout. With { waitForReady: false }, write failures are only logged. To define a new model's properties, pass them to Model.create.
Other methods
| Method | Returns | Description |
|---|---|---|
ModelProperty.all(modelFileId, { waitForReady?, readyTimeout?, hold? }) | Promise<ModelProperty[]> | Every property of a model, by the model's ID (model.id). waitForReady defaults to false, readyTimeout to 15000. |
ModelProperty.find({ propertyFileId, waitForReady?, readyTimeout?, hold? }) | Promise<ModelProperty | undefined> | One property by its ID. Waits by default, up to 15000 ms. |
ModelProperty.getById(propertyFileId, { hold? }) | ModelProperty | undefined | Synchronous lookup of a loaded property. |
ModelProperty.retain(property) | boolean | Takes one more hold on a cached, running property, released with unload(). false, with no hold taken, when the property was stopped. |
validate() | Promise<{ isValid, errors }> | Validates the definition. Each error is { field, message, code, severity }. Resolves within 5 seconds. |
save() | void | Saves an edited definition to the local database. Returns at once; does nothing if the property isn't edited. Edits already save on assignment, so you rarely need it. Throws EntityUnloadedError on a destroyed or evicted property. |
setSchemaName(schemaName) | void | Tells the instance which schema it belongs to. |
unload() | void | Gives back one hold taken with hold: true or ModelProperty.retain; does nothing when there's none to give back. Releasing the last hold doesn't stop the property, and nothing in the database changes. |
destroy() | Promise<void> | Deletes the property from the local database and the schema, and stops the instance, whoever holds it. Items' stored values for it are kept but unlinked from the property (property_id set to null). Rejects if the delete fails. |
reload() | Promise<void> | Does nothing; reload the parent model or schema instead. |
getService() | actor | The underlying xstate actor. |
hold defaults to false on each lookup. A property stays cached and running until it, its model or its schema is destroyed or evicted, whether or not anything holds it; client.stop() evicts every cached property. A hold only counts your use of the instance. Once a property is destroyed or evicted, save() and assignments to its fields throw EntityUnloadedError, whose message names the property and says to look it up again with ModelProperty.find. EntityUnloadedError is exported from the main entry.
The class also exposes bookkeeping helpers used by the SDK (createById, refreshFromDb, holdForModel, evictForModels, evictForModel, trackPendingWrite, clearPendingWrite, getPendingPropertyIds, getPendingModelId). You don't need them in app code.
Data type helpers
Also exported from @seedprotocol/sdk:
| Export | Description |
|---|---|
ModelPropertyDataTypes | Enum of the ten type names. |
isDataType(value, expected) | Case-insensitive type comparison: isDataType('list', 'List') is true. |
normalizeDataType(value) | Normalizes a type name: 'string' and 'text' become 'Text'. |
Related
- Your first schema: property types in a walkthrough
- File and image properties
- Modeling relations
- Model, Schema and ItemProperty
- React model property hooks
Model
The Model class: look up the models in your schema, create items from them, list models, define new models in code, and how model edits are saved.
Item
The Item class: create, find and list items, read their identity fields, delete them, prepare them for publishing, export published ones as archive bags and check who owns them.