Skip to main content

Attribute Values

Attributes are how OneEntry describes content: a page, product, block, user, order or form field carries a map of attribute values keyed by marker. The SDK normalizes every attribute of every response to the same shape, so the same field looks the same no matter which module returned it.

const page = await Pages.getPageByUrl('catalog');

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 typevalue
string, textstring
integer, float, realnumber — cast from the API's string form
image, file with one filethe file object itself
image, file with several filesan array of file objects
groupOfImagesalways an array — it is a collection by definition
listan array
timeIntervalan array of groups — see Time Intervals
no value setalways 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 an array.

const block = await Blocks.getBlockByMarker('promo');

// before: block.attributeValues.img.value[0].downloadLink
// now: block.attributeValues.img.value.downloadLink

This applies in every module. Previously the unwrapping ran only in products, menus, forms, forms-data, attribute-sets, integration-collections and Pages.searchPage, and only on the attributeValues key — 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, so consumers had to branch on the shape. Form attributes, form-data fields and nested additionalFields were never unwrapped at all.

⚠️ Migration: code that reads value[0] from products or menus is unaffected — those modules already returned the object. Code that reads value[0] from blocks, pages, users or orders must drop the index.

groupOfImages is a collection by definition and always stays an array. On the request side, IBodyTypeFile.value is typed IFileValue | IFileValue[] accordingly.

Numbers are numbers​

integer, float and real values are cast to a number. real used to be left as a string, so the same numeric field reached the consumer as 10 or as "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, which were skipped entirely — a rating field of an integer form attribute is a number, not a string.

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. The SDK used to pass it through for text-like types while numeric types became null — the same "no value" state had three representations. It is now always null.

if (page.attributeValues.notes.value === null) {
// nothing configured for this attribute
}

⚠️ Migration: an unset integer/float is no longer 0. Number(null) is 0, so an explicit null from the API used to be reported as a real zero — a value indistinguishable from a configured 0.

Everything is sorted by position​

attributeValues has always been returned in position order, and form attributes now are too. The API returns form fields unordered — a field with position: 10 could arrive after position: 14 — so rendering a form in CMS order required sorting on the consumer side.

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 there too.

Typing an attribute value​

IAttributeValue.value is typed unknown, because its shape depends on type at runtime — which also means the compiler cannot catch a wrong assumption about it. The single-file rule above is where that 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. Reach the value through the guards and helpers instead.

Files​

getAttributeFiles returns every file of an image, file or groupOfImages attribute, collapsing the single-file and array shapes into one list. Anything else yields an empty array, so it is safe on an arbitrary 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: filename, downloadLink, size, 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[] — the API returns an empty array when an attribute has none, and that union makes every field read 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 does not model (entity, date, custom ones) stay on IAttributeValue.

It 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 cannot notice it changing afterwards. For attribute values the failure mode is quiet — if a file field were renamed, getAttributeFiles would simply return an empty list and the page would render without images.

Turning on response validation makes that loud:

const api = defineOneEntry('your-url', {
token: 'your-app-token',
validation: { enabled: true, strictMode: false, logErrors: true },
});

attributeValues used to be exempt from validation entirely. It is now checked where the API's contract is fixed — the shape of the files of image, file and groupOfImages attributes, nested fields included — while everything defined by your own attribute set stays open. A file that lost or renamed a field is reported as a validation issue naming the marker and index; fields the API adds are accepted.

With strictMode: false (the recommended production setting) the data still comes through and the issue is logged; with strictMode: true the call returns an IError carrying validationErrors.