Attribute values
OneEntry describes content with attributes: a page, product, block, user, order or form field carries a map of attribute values keyed by marker. The SDK normalizes every attribute in every response to the same shape, so a field looks the same whichever module returned it.
const page = await Pages.getPageByUrl('catalog');
if ('statusCode' in page) throw new Error(page.message);
page.attributeValues.title.value; // "Catalog" — string
page.attributeValues.amount.value; // 5 — number
page.attributeValues.cover.value; // { downloadLink } — the file object itself
page.attributeValues.notes.value; // null — no value set
The normalized shape
An attribute value is an IAttributeValue: { type, value, position?, additionalFields? }. What value holds depends on type:
| Attribute type | value |
|---|---|
string, text | string |
integer, float, real | number, cast from the API's string form |
image, file with one file | the file object itself |
image, file with several files | an array of file objects |
groupOfImages | always an array (it is a collection by definition) |
list | an array |
timeInterval | an array of groups; see Time intervals |
| no value set | always null |
Single-file attributes are unwrapped
When an image or file attribute holds exactly one file, its value is the file object itself. Only values with two or more files stay arrays.
const block = await Blocks.getBlockByMarker('promo');
// before: block.attributeValues.img.value[0].downloadLink
// now: block.attributeValues.img.value.downloadLink
This applies in every module.
Migration: earlier versions unwrapped single files only in products, menus, forms, forms-data, attribute-sets, integration-collections and
Pages.searchPage, and only under theattributeValueskey. Everywhere else (blocks, all other pages methods,Products.getProductsEmptyPage,Products.getProductBlockById, admins, discounts, templates, orders, users) the same attribute arrived as a one-element array, and formattributes, form-data fields and nestedadditionalFieldswere never unwrapped. Code for products or menus already reads the object and is unaffected. Code that readsvalue[0]from blocks, pages, users or orders must drop the index.
groupOfImages always stays an array. Submissions follow their own rules: IBodyTypeFile.value accepts a File/Blob (or a FileList), the descriptor that FileUploading.upload returns, or an IFileValue, and these can be mixed in one array. A lone File/Blob is fine, but a descriptor or IFileValue must be sent inside an array, or the API answers with a 400 error.
Numbers are numbers
integer, float and real values are cast to numbers. (Earlier versions left real as a string, so the same numeric field arrived as 10 or "10" depending on which of the three types it was declared with.)
const page = await Pages.getPageByUrl('catalog');
// before: page.attributeValues.amount.value // "5"
// now: page.attributeValues.amount.value // 5
Numeric normalization also runs on form attributes and form-data fields: a rating field of an integer form attribute is a number, not a string. Earlier versions skipped them.
When submitting data, send a string: IBodyTypeStringNumberFloat.value is string | number | null, and responses come back normalized.
An empty value is always null
The API returns an empty localization map for an unset value, and the SDK turns it into null for every type. (Earlier versions passed the map through for text-like types and returned null for numeric ones, so "no value" had three representations.)
if (page.attributeValues.notes.value === null) {
// nothing configured for this attribute
}
Migration: an unset
integer/floatis no longer0.Number(null)is0, so an explicitnullfrom the API used to be reported as a real zero, indistinguishable from a configured0.
Everything is sorted by position
attributeValues and form attributes are returned in position order. The API returns form fields unordered (a field with position: 10 can arrive after position: 14); the SDK sorts them, so you don't have to.
A form with no attributes returns attributes: []. The API sends an empty object in that case, and the SDK normalizes it to an empty array, so attributes is always IFormAttribute[] and form.attributes.map(...) is safe on every form.
Nested fields: additionalFields
Nested attribute values arrive under additionalFields. By default the SDK converts the array the API returns into an object keyed by marker; set rawData: true in the config to keep the original array. See Additional fields format.
// default (rawData: false)
attribute.additionalFields['my_field'].value;
// rawData: true
attribute.additionalFields.find((f) => f.marker === 'my_field').value;
Nested additionalFields go through the same normalization as top-level attributes: single files are unwrapped and numbers are cast.
Typing an attribute value
IAttributeValue.value is typed unknown, because its shape depends on type at runtime. That also means the compiler can't catch a wrong assumption about it. The single-file rule above is where this bites: a helper written as
const file = Array.isArray(value) ? value[0] : undefined; // single image → undefined
compiles, builds and renders a page with no images and no error anywhere. Read the value through the guards and helpers instead.
Files
getAttributeFiles returns every file of an image, file or groupOfImages attribute as one list, whether the value is a single file or an array. For any other attribute it returns an empty array, so it's safe to call on any attribute:
import { getAttributeFile, getAttributeFiles } from 'oneentry';
const gallery = getAttributeFiles(product.attributeValues.images); // IAttributeFile[]
const cover = getAttributeFile(page.attributeValues.cover); // IAttributeFile | null
const blurDataURL = cover?.previewLink?.default?.[0]; // base64 placeholder
IAttributeFile is exported. It has filename, downloadLink, size and contentType, plus the optional previewLink (previews keyed by template, each a [base64 placeholder, url] pair) and defaultPreview.
Nested fields
additionalFields is Record<string, IAttributeValue> | unknown[], because the API returns an empty array when an attribute has none. With that union, every field read is a type error. getAdditionalFields collapses it to a map (and keys the rawData: true array by marker):
import { getAdditionalFields } from 'oneentry';
const alt = getAdditionalFields(page.attributeValues.cover).alt?.value;
Guards
isFileAttribute, isStringAttribute, isNumberAttribute, isListAttribute and isTimeIntervalAttribute narrow an IAttributeValue so its value is reachable without a cast:
import { isStringAttribute } from 'oneentry';
import type { IAttributeValues } from 'oneentry';
function readText(values: IAttributeValues, marker: string): string {
const attr = values[marker];
return isStringAttribute(attr) ? (attr.value ?? '') : '';
}
The discriminated union
ITypedAttributeValue is IAttributeValue as a union over type: IFileAttributeValue, IStringAttributeValue, INumberAttributeValue, IListAttributeValue, ITimeIntervalAttributeValue. It is closed on purpose: a loose member (type: string) would match every case and collapse the narrowing back to unknown. Types the SDK doesn't model (entity, date, custom ones) stay on IAttributeValue.
The union is opt-in. IAttributeValues keeps the loose IAttributeValue, so narrow into the union with the guards, or annotate a value you control:
import type { ITypedAttributeValue } from 'oneentry';
function render(attr: ITypedAttributeValue) {
switch (attr.type) {
case 'image':
case 'file':
return attr.value; // IAttributeFile | IAttributeFile[] | null
case 'integer':
return attr.value; // number | null
}
}
When the API changes
Types describe the API as it was when the SDK was built; they can't notice later changes. For attribute values the failure is silent: if a file field were renamed, getAttributeFiles would return an empty list and the page would render without images.
Response validation makes it visible:
const api = defineOneEntry('https://your-project.oneentry.cloud', {
token: 'your-app-token',
validation: { enabled: true, strictMode: false, logErrors: true },
});
Validation checks attributeValues where the API's contract is fixed: the shape of the files in image, file and groupOfImages attributes, nested fields included. Everything defined by your own attribute set stays open. A file that lost or renamed a field is reported as a validation issue with the marker and index; fields the API adds are accepted. (Earlier versions didn't validate attributeValues at all.)
With strictMode: false the data still comes through and the issue is logged; with strictMode: true the call returns an IError carrying validationErrors.
Related
- Time intervals: expanding
timeIntervalattributes into slots - Importing types:
IAttributeValue,IAttributeValuesand the rest - AttributesSets Module: how attributes are configured